Android SVG技术入门:线条动画实现原理

SVG技术入门:线条动画实现原理

这是一个有点神奇的技术:一副线条构成的画能自动画出自己,非常的酷。SVG 意为可缩放矢量图形(Scalable Vector Graphics),是使用 XML 来描述二维图形和绘图程序的语言,可以任意放大图形显示,但绝不会以牺牲图像质量为代价;可在svg图像中保留可编辑和可搜寻的状态;平均来讲,svg文件比其它格式的图像文件要小很多,因而下载也很快。可以提前看两个动画效果感受一下


登录效果.gif

搜索效果.gif

1.用 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;

2.SVG的使用

既然SVG是公认的xml文件格式定义的,那么我们则可以通过解析xml文件拿到对应SVG图的所有数据

2.1 添加一个svg文件

app:svg指定了一个SVG文件,这个文件放在raw目录下面:

文件地址.jpg

path 类型的SVG 数据:
代码看上去很复杂,如果说它们是代码的话,但是我们可以注意到,这种书写方式,有点类似于html,都是使用标签使用最多的标签是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 里关于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  

SVG里还定义了一些基本的图形和效果:(介绍和使用可以查看W3School)

命令.png

我们根据最后要做的效果,利用PS等作图软件设计制作出想要的图形,然后使用矢量图软件导出图片的SVG数据,也可以在svg文件里通过代码来实现自己想要的动画效果查看svg文件编写

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

已经有人完成了这个工作,在Github上有开源控件 可以使用

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

<?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 绘制过程所需要的两个类

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画布,捕捉所有的路径进行渲染
 */  
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 插件对svg 指令的解析

查看androidsvg的源码了解他的解析,查看svg解析器SVGParser类可以看到他的解析,对svg文件里的指令采用Android Path 里的对应方法进行置换 ,如:使用parsePath(String val)方法解析绘图路径,源码中是使用ascii码来对比,内容可以看成如下

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;  
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容