Android使用SVG向量圖打造酷炫動效!

GAStudio發表於2017-01-24

一個真正酷炫的動效往往讓人虎軀一震,話不多說,我們們先瞅瞅效果:

Android使用SVG向量圖打造酷炫動效!

如果你想看 GAStudio Github主頁,請戳這裡
如果你想看 GAStudio更多技術文章,請戳這裡
QQ技術交流群:277582728;
github地址: github.com/Ajian-studi…

這個效果我們需要考慮以下幾個問題:

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裡還定義了一些基本的圖形和效果:

Android使用SVG向量圖打造酷炫動效!

更多介紹和使用大家可以看 W3School

好,以上內容,我們已經知道 SVG 圖是通過 Xml 格式定義的,並且裡面用到了一些基本的指令對資料進行組裝,構成基本圖形或複雜的路徑;
而對於我們來說 ,這個xml 如何拿到呢?
1.我們根據最後要做的效果,利用PS等作圖軟體設計製作出想要的圖形;

Android使用SVG向量圖打造酷炫動效!

2.使用 GIMP 之類的向量圖軟體匯出圖片的SVG資料,方法如下:
先使用魔棒工具快速建立選區:
Android使用SVG向量圖打造酷炫動效!

然後將選區匯出為path:
Android使用SVG向量圖打造酷炫動效!

這個時候在軟體的右邊欄就可以看見生成的路徑了,然後將路徑匯出:
Android使用SVG向量圖打造酷炫動效!

經過以上幾步,我們就拿到了我們自己設計的文字或圖形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分享似的,沒毛線用,其實我的目的只有一個,那就是不管你是否能看懂程式碼,都能按照我上面所說做出自己想要的效果,並加以改變,靈活運用,畢竟輪子不需要重複造!

我本人也是對SVG向量圖剛有所瞭解,主要參考國外大神的一篇部落格,連結如下:www.willowtreeapps.com/blog/muzei-…

CSDN原始碼下載地址:download.csdn.net/detail/tian…


最後,附上GAStudio技術交流群和Github,喜歡的話歡迎follow和star:

如果你想看 GAStudio Github主頁,請戳這裡
如果你想看 GAStudio更多技術文章,請戳這裡
QQ技術交流群:277582728;
github地址: github.com/Ajian-studi…

Android使用SVG向量圖打造酷炫動效!

相關文章