一個真正Android使用SVG矢量圖打造酷炫動效往往讓人虎軀一震,話不多說,咱們先看看效果:
這個效果我們需要考慮以下幾個問題:
1. 這是圖片還是文字;
2. 如果是圖片該如何拿到圖形的邊沿線坐標,如果是文字呢?
3. 如果拿到了邊沿線坐標,如何讓光線沿著路徑跑動;
4. 怎麼處理過程的銜接;
以上四個問題似乎不是太好處理,而這幾個問題也正好是這個效果精華所在,接下來咱們一個一個進行考慮,當然這種考慮已經基於一些國外大神的基礎之上;
首先這是圖片還是文字?
答案是:背景是圖片,表面的文字還是圖片,有些同學可能會說了,靠,這麼沒含量,一個幀動畫而已,還虎軀一震,XXXXX,當然,答案肯定不會是這樣的,背景我就不說了,普通的jpg或png圖,但文字則是SVG格式的矢量圖;
有了第一個問題的答案,我們來看第二個問題,如何拿到文字圖形的邊沿坐標;
要回答這個問題,我們先來簡單的了解一個SVG(矢量圖);
SVG 意為可縮放矢量圖形(Scalable Vector Graphics),是使用 XML 來描述二維圖形和繪圖程序的語言;
使用 SVG 的優勢在於:
1.SVG 可被非常多的工具讀取和修改(比如記事本),由於使用xml格式定義,所以可以直接被當作文本文件打開,看裡面的數據;
2.SVG 與 JPEG 和 GIF 圖像比起來,尺寸更小,且可壓縮性更強,SVG 圖就相當於保存了關鍵的數據點,比如要顯示一個圓,需要知道圓心和半徑,那麼SVG 就只保存圓心坐標和半徑數據,而平常我們用的位圖都是以像素點的形式根據圖片大小保存對應個數的像素點,因而SVG尺寸更小;
3.SVG 是可伸縮的,平常使用的位圖拉伸會發虛,壓縮會變形,而SVG格式圖片保存數據進行運算展示,不管多大多少,可以不失真顯示;
4.SVG 圖像可在任何的分辨率下被高質量地打印;
5.SVG 可在圖像質量不下降的情況下被放大;
6.SVG 圖像中的文本是可選的,同時也是可搜索的(很適合制作地圖);
7.SVG 可以與 Java 技術一起運行;
8.SVG 是開放的標准;
9.SVG 文件是純粹的 XML;
看起來好厲害的樣子,還是回到我們的問題,從SVG圖中我們可否拿到我們想要的數據點呢?根據上面的介紹,答案當然是肯定的,從SVG圖中我們可以拿到我們想要的所有數據;
好的,拿到數據之後,怎麼讓一條線沿著路徑跑起來呢?毋庸置疑,我們需要用到path;
最後我們根據效果的需要,設置幾個繪制過程,進行繪制;
接下來我們一起來解決以上問題:
既然SVG是公認的xml文件格式定義的,那麼我們則可以通過解析xml文件拿到對應SVG圖的所有數據,我們先看下 path 類型的SVG 數據:
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<path d="M250 150 L150 350 L350 350 Z" />
</svg>
上面有一個path 標簽,裡面用到了 M 和 Z 指令,M 就相當於 android Path 裡的moveTo(),Z 則相當於 Path 裡的close();
我們先看下SVG 裡關於path 有哪些指令:
M = moveto 相當於 android Path 裡的moveTo(),用於移動起始點
L = lineto 相當於 android Path 裡的lineTo(),用於畫線
H = horizontal lineto 用於畫水平線
V = vertical lineto 用於畫豎直線
C = curveto 相當於cubicTo(),三次貝塞爾曲線
S = smooth curveto 同樣三次貝塞爾曲線,更平滑
Q = quadratic Belzier curve quadTo(),二次貝塞爾曲線
T = smooth quadratic Belzier curveto 同樣二次貝塞爾曲線,更平滑
A = elliptical Arc 相當於arcTo(),用於畫弧
Z = closepath 相當於closeTo(),關閉path
了解了以上path相關的指令,就可以看懂path構成的SVG圖的數據了,除此之外,SVG裡還定義了一些基本的圖形和效果:
好,以上內容,我們已經知道 SVG 圖是通過 Xml 格式定義的,並且裡面用到了一些基本的指令對數據進行組裝,構成基本圖形或復雜的路徑;
而對於我們來說 ,這個xml 如何拿到呢?
1.我們根據最後要做的效果,利用PS等作圖軟件設計制作出想要的圖形;
2. 使用 GIMP 之類的矢量圖軟件導出圖片的SVG數據,方法如下:
先使用魔棒工具快速建立選區:
然後將選區導出為path:
這個時候在軟件的右邊欄就可以看見生成的路徑了,然後將路徑導出:
經過以上幾步,我們就拿到了我們自己設計的文字或圖形SVG圖的Path數據,上面圖片的SVG信息如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
width="6.95746in" height="1.82269in"
viewBox="0 0 668 175">
<path id="Selection"
fill="none" stroke="black" stroke-width="1"
d="M 530.00,34.00
C 530.00,34.00 526.08,59.00 526.08,59.00
526.08,59.00 518.00,105.00 518.00,105.00
518.00,105.00 515.42,119.00 515.42,119.00
515.42,119.00 513.26,125.01 513.26,125.01
513.26,125.01 506.00,126.00 506.00,126.00
506.00,126.00 496.00,126.00 496.00,126.00
496.00,126.00 496.00,120.00 496.00,120.00
490.87,124.16 486.71,126.42 480.00,126.91
475.71,127.22 471.06,126.94 467.00,125.44
454.13,120.68 451.86,110.19 452.00,98.00
452.22,79.34 465.14,64.55 484.00,63.18
492.14,62.59 498.96,65.71 504.00,72.00
504.00,72.00 510.00,34.00 510.00,34.00
510.00,34.00 530.00,34.00 530.00,34.00 Z
M 551.00,56.89
C 539.01,55.86 537.45,39.82 551.00,35.55
568.60,33.45 567.67,58.33 551.00,56.89 Z
中間段省略
M 263.00,134.00
C 263.00,134.00 263.00,145.00 263.00,145.00
263.00,145.00 202.00,145.00 202.00,145.00
202.00,145.00 202.00,134.00 202.00,134.00
202.00,134.00 263.00,134.00 263.00,134.00 Z" />
</svg>
根據圖形路徑的復雜度,生成的path數據復雜度也不一樣,但格式也算是非常的清楚,即采用一定的指令把數據點進行拼接;
現在有了這些數據點,我們需要做的則是對數據進行解析,封裝成我們要的Path;
解析的過程也無非是 遇到指令則采用android Path 裡的對應方法進行置換,解析方式如下:
public Path parsePath(String s) throws ParseException {
mCurrentPoint.set(Float.NaN, Float.NaN);
mPathString = s;
mIndex = 0;
mLength = mPathString.length();
PointF tempPoint1 = new PointF();
PointF tempPoint2 = new PointF();
PointF tempPoint3 = new PointF();
Path p = new Path();
p.setFillType(Path.FillType.WINDING);
boolean firstMove = true;
while (mIndex < mLength) {
char command = consumeCommand();
boolean relative = (mCurrentToken == TOKEN_RELATIVE_COMMAND);
switch (command) {
case 'M':
case 'm': {
// m指令,相當於android 裡的 moveTo()
boolean firstPoint = true;
while (advanceToNextToken() == TOKEN_VALUE) {
consumeAndTransformPoint(tempPoint1,
relative && mCurrentPoint.x != Float.NaN);
if (firstPoint) {
p.moveTo(tempPoint1.x, tempPoint1.y);
firstPoint = false;
if (firstMove) {
mCurrentPoint.set(tempPoint1);
firstMove = false;
}
} else {
p.lineTo(tempPoint1.x, tempPoint1.y);
}
}
mCurrentPoint.set(tempPoint1);
break;
}
case 'C':
case 'c': {
// c指令,相當於android 裡的 cubicTo()
if (mCurrentPoint.x == Float.NaN) {
throw new ParseException("Relative commands require current point", mIndex);
}
while (advanceToNextToken() == TOKEN_VALUE) {
consumeAndTransformPoint(tempPoint1, relative);
consumeAndTransformPoint(tempPoint2, relative);
consumeAndTransformPoint(tempPoint3, relative);
p.cubicTo(tempPoint1.x, tempPoint1.y, tempPoint2.x, tempPoint2.y,
tempPoint3.x, tempPoint3.y);
}
mCurrentPoint.set(tempPoint3);
break;
}
case 'L':
case 'l': {
// 相當於lineTo()進行畫直線
if (mCurrentPoint.x == Float.NaN) {
throw new ParseException("Relative commands require current point", mIndex);
}
while (advanceToNextToken() == TOKEN_VALUE) {
consumeAndTransformPoint(tempPoint1, relative);
p.lineTo(tempPoint1.x, tempPoint1.y);
}
mCurrentPoint.set(tempPoint1);
break;
}
case 'H':
case 'h': {
// 畫水平直線
if (mCurrentPoint.x == Float.NaN) {
throw new ParseException("Relative commands require current point", mIndex);
}
while (advanceToNextToken() == TOKEN_VALUE) {
float x = transformX(consumeValue());
if (relative) {
x += mCurrentPoint.x;
}
p.lineTo(x, mCurrentPoint.y);
}
mCurrentPoint.set(tempPoint1);
break;
}
case 'V':
case 'v': {
// 畫豎直直線
if (mCurrentPoint.x == Float.NaN) {
throw new ParseException("Relative commands require current point", mIndex);
}
while (advanceToNextToken() == TOKEN_VALUE) {
float y = transformY(consumeValue());
if (relative) {
y += mCurrentPoint.y;
}
p.lineTo(mCurrentPoint.x, y);
}
mCurrentPoint.set(tempPoint1);
break;
}
case 'Z':
case 'z': {
// 封閉path
p.close();
break;
}
}
}
return p;
}
有了圖形對應的path,我們只需要按照我們想要的效果進行繪制即可,具體過程不再細講,大家看代碼:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mState == STATE_NOT_STARTED || mGlyphData == null) {
return;
}
long t = System.currentTimeMillis() - mStartTime;
// 繪制出現前的邊沿線和跑動過程
for (int i = 0; i < mGlyphData.length; i++) {
float phase = MathUtil.constrain(0, 1,
(t - (mTraceTime - mTraceTimePerGlyph) * i * 1f / mGlyphData.length)
* 1f / mTraceTimePerGlyph);
float distance = INTERPOLATOR.getInterpolation(phase) * mGlyphData[i].length;
mGlyphData[i].paint.setColor(mTraceResidueColors[i]);
mGlyphData[i].paint.setPathEffect(new DashPathEffect(
new float[] {
distance, mGlyphData[i].length
}, 0));
canvas.drawPath(mGlyphData[i].path, mGlyphData[i].paint);
mGlyphData[i].paint.setColor(mTraceColors[i]);
mGlyphData[i].paint.setPathEffect(new DashPathEffect(
new float[] {
0, distance, phase > 0 ? mMarkerLength : 0,
mGlyphData[i].length
}, 0));
canvas.drawPath(mGlyphData[i].path, mGlyphData[i].paint);
}
if (t > mFillStart) {
if (mState < STATE_FILL_STARTED) {
changeState(STATE_FILL_STARTED);
}
// 繪制漸變出現的過程,即改變alpha過程
float phase = MathUtil.constrain(0, 1, (t - mFillStart) * 1f / mFillTime);
for (int i = 0; i < mGlyphData.length; i++) {
GlyphData glyphData = mGlyphData[i];
mFillPaint.setARGB((int) (phase * ((float) mFillAlphas[i] / (float) 255) * 255),
mFillReds[i],
mFillGreens[i],
mFillBlues[i]);
canvas.drawPath(glyphData.path, mFillPaint);
}
}
if (t < mFillStart + mFillTime) {
ViewCompat.postInvalidateOnAnimation(this);
} else {
changeState(STATE_FINISHED);
}
}
好了,主要的問題和思路基本如上,有些人可能會說,你這講的跟UX分享似的,沒毛線用,其實我的目的只有一個,那就是不管你是否能看懂代碼,都能按照我上面所說做出自己想要的效果,並加以改變,靈活運用,畢竟輪子不需要重復造!
源碼下載地址:
百度網盤下載: http://pan.baidu.com/s/1bnuK5L9
------------------------------------------分割線------------------------------------------
免費下載地址在 http://linux.linuxidc.com/
用戶名與密碼都是www.linuxidc.com
具體下載目錄在 /2015年資料/8月/20日/Android使用SVG矢量圖打造酷炫動畫效果/
下載方法見 http://www.linuxidc.com/Linux/2013-07/87684.htm
------------------------------------------分割線------------------------------------------