Android SVG技術(shù)入門:線條動(dòng)畫實(shí)現(xiàn)原理

SVG技術(shù)入門:線條動(dòng)畫實(shí)現(xiàn)原理

這是一個(gè)有點(diǎn)神奇的技術(shù):一副線條構(gòu)成的畫能自動(dòng)畫出自己,非常的酷源哩。SVG 意為可縮放矢量圖形(Scalable Vector Graphics)捉偏,是使用 XML 來(lái)描述二維圖形和繪圖程序的語(yǔ)言,可以任意放大圖形顯示立叛,但絕不會(huì)以犧牲圖像質(zhì)量為代價(jià);可在svg圖像中保留可編輯和可搜尋的狀態(tài);平均來(lái)講,svg文件比其它格式的圖像文件要小很多贡茅,因而下載也很快秘蛇。可以提前看兩個(gè)動(dòng)畫效果感受一下


登錄效果.gif

搜索效果.gif

1.用 SVG 的優(yōu)勢(shì):

1.SVG 可被非常多的工具讀取和修改(比如記事本),由于使用xml格式定義顶考,所以可以直接被當(dāng)作文本文件打開(kāi)赁还,看里面的數(shù)據(jù);
2.SVG 與 JPEG 和 GIF 圖像比起來(lái)驹沿,尺寸更小艘策,且可壓縮性更強(qiáng),SVG 圖就相當(dāng)于保存了關(guān)鍵的數(shù)據(jù)點(diǎn)渊季,比如要顯示一個(gè)圓朋蔫,需要知道圓心和半徑,那么SVG 就只保存圓心坐標(biāo)和半徑數(shù)據(jù)却汉,而平常我們用的位圖都是以像素點(diǎn)的形式根據(jù)圖片大小保存對(duì)應(yīng)個(gè)數(shù)的像素點(diǎn)驯妄,因而SVG尺寸更小合砂;
3.SVG 是可伸縮的青扔,平常使用的位圖拉伸會(huì)發(fā)虛,壓縮會(huì)變形,而SVG格式圖片保存數(shù)據(jù)進(jìn)行運(yùn)算展示微猖,不管多大多少谈息,可以不失真顯示;
4.SVG 圖像可在任何的分辨率下被高質(zhì)量地打印;
5.SVG 可在圖像質(zhì)量不下降的情況下被放大;
6.SVG 圖像中的文本是可選的励两,同時(shí)也是可搜索的(很適合制作地圖);
7.SVG 可以與 Java 技術(shù)一起運(yùn)行;
8.SVG 是開(kāi)放的標(biāo)準(zhǔn);
9.SVG 文件是純粹的 XML;

2.SVG的使用

既然SVG是公認(rèn)的xml文件格式定義的黎茎,那么我們則可以通過(guò)解析xml文件拿到對(duì)應(yīng)SVG圖的所有數(shù)據(jù)

2.1 添加一個(gè)svg文件

app:svg指定了一個(gè)SVG文件,這個(gè)文件放在raw目錄下面:

文件地址.jpg

path 類型的SVG 數(shù)據(jù):
代碼看上去很復(fù)雜当悔,如果說(shuō)它們是代碼的話傅瞻,但是我們可以注意到,這種書寫方式盲憎,有點(diǎn)類似于html嗅骄,都是使用標(biāo)簽使用最多的標(biāo)簽是path,也就是路徑

<?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>

SVG 里關(guān)于path 有哪些指令:

M = moveto   相當(dāng)于 android Path 里的moveTo(),用于移動(dòng)起始點(diǎn)  
L = lineto   相當(dāng)于 android Path 里的lineTo()饼疙,用于畫線  
H = horizontal lineto     用于畫水平線  
V = vertical lineto       用于畫豎直線  
C = curveto               相當(dāng)于cubicTo(),三次貝塞爾曲線  
S = smooth curveto        同樣三次貝塞爾曲線溺森,更平滑  
Q = quadratic Belzier curve             quadTo(),二次貝塞爾曲線  
T = smooth quadratic Belzier curveto    同樣二次貝塞爾曲線窑眯,更平滑  
A = elliptical Arc   相當(dāng)于arcTo()屏积,用于畫弧  
Z = closepath     相當(dāng)于closeTo(),關(guān)閉path  

SVG里還定義了一些基本的圖形和效果:(介紹和使用可以查看W3School)

命令.png

我們根據(jù)最后要做的效果,利用PS等作圖軟件設(shè)計(jì)制作出想要的圖形磅甩,然后使用矢量圖軟件導(dǎo)出圖片的SVG數(shù)據(jù)炊林,也可以在svg文件里通過(guò)代碼來(lái)實(shí)現(xiàn)自己想要的動(dòng)畫效果查看svg文件編寫

2.2 SVG的解析和解析后的繪制

已經(jīng)有人完成了這個(gè)工作,在Github上有開(kāi)源控件 可以使用

在布局文件里面添加自定義控件

<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout  
    xmlns:android="http://schemas.android.com/apk/res/android"  
    android:orientation="vertical"  
    android:background="#ff0000"  
    android:layout_width="fill_parent"  
    android:layout_height="fill_parent">  
  
    <org.curiouscreature.android.roadtrip.PathView  
        xmlns:app="http://schemas.android.com/apk/res-auto"  
        android:id="@+id/pathView"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        app:pathColor="@android:color/white"  
        app:svg="@raw/ironman_white"  
        app:pathWidth="5"/>  
</LinearLayout>  

activity中繪制

final PathView pathView = (PathView) findViewById(R.id.pathView);
pathView.getPathAnimator().
    delay(100).
    duration(1500).
    interpolator(new AccelerateDecelerateInterpolator()).
    start();

3.解析源碼了解

3.1 繪制過(guò)程所需要的兩個(gè)類

SVG解析工具類SvgUtils主要代碼:

/** 
 * Loading the svg from the resources. 
 * 從資源中加載SVG
 */  
public void load(Context context, int svgResource) {  
    if (mSvg != null) return;  
    try {  
        mSvg = SVG.getFromResource(context, svgResource);  
        mSvg.setDocumentPreserveAspectRatio(PreserveAspectRatio.UNSCALED);  
    } catch (SVGParseException e) {  
        Log.e(LOG_TAG, "Could not load specified SVG resource", e);  
    }  
}  

/** 
 * Draw the svg to the canvas. 
 * 繪制SVG畫布
 */  
public void drawSvgAfter(final Canvas canvas, final int width, final int height) {  
    final float strokeWidth = mSourcePaint.getStrokeWidth();  
    rescaleCanvas(width, height, strokeWidth, canvas);  
}  

/** 
 * Render the svg to canvas and catch all the paths while rendering. 
 * 渲染SVG畫布卷要,捕捉所有的路徑進(jìn)行渲染
 */  
public List<SvgPath> getPathsForViewport(final int width, final int height) {  
    final float strokeWidth = mSourcePaint.getStrokeWidth();  
    Canvas canvas = new Canvas() {  
        private final Matrix mMatrix = new Matrix();  

        @Override  
        public int getWidth() {  
            return width;  
        }  

        @Override  
        public int getHeight() {  
            return height;  
        }  

        @Override  
        public void drawPath(Path path, Paint paint) {  
            Path dst = new Path();  

            //noinspection deprecation  
            getMatrix(mMatrix);  
            path.transform(mMatrix, dst);  
            paint.setAntiAlias(true);  
            paint.setStyle(Paint.Style.STROKE);  
            paint.setStrokeWidth(strokeWidth);  
            mPaths.add(new SvgPath(dst, paint));  
        }  
    };  

    rescaleCanvas(width, height, strokeWidth, canvas);  

    return mPaths;  
}  

/** 
 * Rescale the canvas with specific width and height. 
 * 繪制到畫布
 */  
private void rescaleCanvas(int width, int height, float strokeWidth, Canvas canvas) {  
    final RectF viewBox = mSvg.getDocumentViewBox();  

    final float scale = Math.min(width  
                    / (viewBox.width() + strokeWidth),  
            height / (viewBox.height() + strokeWidth));  

    canvas.translate((width - viewBox.width() * scale) / 2.0f,  
            (height - viewBox.height() * scale) / 2.0f);  
    canvas.scale(scale, scale);  

    mSvg.renderToCanvas(canvas);  
}  

SVG控件類PathView 主要代碼:

@Override  
protected void onDraw(Canvas canvas) {  
    super.onDraw(canvas);  

    synchronized (mSvgLock) {  
        canvas.save();  
        canvas.translate(getPaddingLeft(), getPaddingTop());  
        final int count = paths.size();  
        for (int i = 0; i < count; i++) {  
            final SvgUtils.SvgPath svgPath = paths.get(i);  
            final Path path = svgPath.path;  
            final Paint paint1 = naturalColors ? svgPath.paint : paint;  
            canvas.drawPath(path, paint1);  
        }  
        fillAfter(canvas);  
        canvas.restore();  
    }  
} 

@Override  
protected void onSizeChanged(final int w, final int h, int oldw, int oldh) {  
    super.onSizeChanged(w, h, oldw, oldh);  

    if (mLoader != null) {  
        try {  
            mLoader.join();  
        } catch (InterruptedException e) {  
            Log.e(LOG_TAG, "Unexpected error", e);  
        }  
    }  
    if (svgResourceId != 0) {  
        mLoader = new Thread(new Runnable() {  
            @Override  
            public void run() {  

                svgUtils.load(getContext(), svgResourceId);  

                synchronized (mSvgLock) {  
                    width = w - getPaddingLeft() - getPaddingRight();  
                    height = h - getPaddingTop() - getPaddingBottom();  
                    paths = svgUtils.getPathsForViewport(width, height);  
                    updatePathsPhaseLocked();  
                }  
            }  
        }, "SVG Loader");  
        mLoader.start();  
    }  
}  

@Override  
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
    if (svgResourceId != 0) {  
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
        setMeasuredDimension(widthSize, heightSize);  
        return;  
    }  

    int desiredWidth = 0;  
    int desiredHeight = 0;  
    final float strokeWidth = paint.getStrokeWidth() / 2;  
    for (SvgUtils.SvgPath path : paths) {  
        desiredWidth += path.bounds.left + path.bounds.width() + strokeWidth;  
        desiredHeight += path.bounds.top + path.bounds.height() + strokeWidth;  
    }  
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
    int heightMode = MeasureSpec.getMode(widthMeasureSpec);  

    int measuredWidth, measuredHeight;  

    if (widthMode == MeasureSpec.AT_MOST) {  
        measuredWidth = desiredWidth;  
    } else {  
        measuredWidth = widthSize;  
    }  

    if (heightMode == MeasureSpec.AT_MOST) {  
        measuredHeight = desiredHeight;  
    } else {  
        measuredHeight = heightSize;  
    }  

    setMeasuredDimension(measuredWidth, measuredHeight);  
}  
3.2 androidsvg 插件對(duì)svg 指令的解析

查看androidsvg的源碼了解他的解析渣聚,查看svg解析器SVGParser類可以看到他的解析,對(duì)svg文件里的指令采用Android Path 里的對(duì)應(yīng)方法進(jìn)行置換 僧叉,如:使用parsePath(String val)方法解析繪圖路徑奕枝,源碼中是使用ascii碼來(lái)對(duì)比,內(nèi)容可以看成如下

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指令瓶堕,相當(dāng)于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指令隘道,相當(dāng)于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': {  
                // 相當(dāng)于lineTo()進(jìn)行畫直線  
                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;  
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市郎笆,隨后出現(xiàn)的幾起案子谭梗,更是在濱河造成了極大的恐慌,老刑警劉巖题画,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件默辨,死亡現(xiàn)場(chǎng)離奇詭異德频,居然都是意外死亡苍息,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)竞思,“玉大人表谊,你說(shuō)我怎么就攤上這事「桥纾” “怎么了爆办?”我有些...
    開(kāi)封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)课梳。 經(jīng)常有香客問(wèn)我距辆,道長(zhǎng),這世上最難降的妖魔是什么暮刃? 我笑而不...
    開(kāi)封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任跨算,我火速辦了婚禮,結(jié)果婚禮上椭懊,老公的妹妹穿的比我還像新娘诸蚕。我一直安慰自己,他們只是感情好氧猬,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開(kāi)白布背犯。 她就那樣靜靜地躺著,像睡著了一般盅抚。 火紅的嫁衣襯著肌膚如雪漠魏。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天泉哈,我揣著相機(jī)與錄音蛉幸,去河邊找鬼。 笑死丛晦,一個(gè)胖子當(dāng)著我的面吹牛奕纫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播烫沙,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼匹层,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了锌蓄?” 一聲冷哼從身側(cè)響起升筏,我...
    開(kāi)封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瘸爽,沒(méi)想到半個(gè)月后您访,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡剪决,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年灵汪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了檀训。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡享言,死狀恐怖峻凫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情览露,我是刑警寧澤荧琼,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站差牛,受9級(jí)特大地震影響命锄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜偏化,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一累舷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧夹孔,春花似錦被盈、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至怜俐,卻和暖如春身堡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拍鲤。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工贴谎, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人季稳。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓擅这,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親景鼠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子仲翎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容