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

一個真正酷炫的動效每每讓人虎軀一震,話很少說,我們先瞅瞅效果:
javascript

若是你想看 GAStudio Github主頁,請戳這裏
若是你想看 GAStudio更多技術文章,請戳這裏
QQ技術交流羣:277582728;
github地址: github.com/Ajian-studi…java

這個效果咱們須要考慮如下幾個問題:

1.這是圖片仍是文字;
2.若是是圖片該如何拿到圖形的邊沿線座標,若是是文字呢?
3.若是拿到了邊沿線座標,如何讓光線沿着路徑跑動;
4.怎麼處理過程的銜接;android

以上四個問題彷佛不是太好處理,而這幾個問題也正好是這個效果精華所在,接下來我們一個一個進行考慮,固然這種考慮已經基於一些國外大神的基礎之上;git

首先這是圖片仍是文字?github

答案是:背景是圖片,表面的文字仍是圖片,有些同窗可能會說了,靠,這麼沒含量,一個幀動畫而已,還虎軀一震,XXXXX,固然,答案確定不會是這樣的,背景我就不說了,普通的jpg或png圖,但文字則是SVG格式的矢量圖;canvas

有了第一個問題的答案,咱們來看第二個問題,如何拿到文字圖形的邊沿座標;app

要回答這個問題,咱們先來簡單的瞭解一個SVG(矢量圖);
SVG 意爲可縮放矢量圖形(Scalable Vector Graphics),是使用 XML 來描述二維圖形和繪圖程序的語言;ide

使用 SVG 的優點在於: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裏還定義了一些基本的圖形和效果:

更多介紹和使用你們能夠看 W3School

好,以上內容,咱們已經知道 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分享似的,沒毛線用,其實個人目的只有一個,那就是無論你是否能看懂代碼,都能按照我上面所說作出本身想要的效果,並加以改變,靈活運用,畢竟輪子不須要重複造!

我本人也是對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…

相關文章
相關標籤/搜索