教你一步一步实现图标无缝变形切换

我的CSDN同步发布:教你一步一步实现图标无缝变形切换

转载请注明出处:【huachao1001的简书:http://www.jianshu.com/users/0a7e42698e4b/latest_articles】

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

本来这篇文章几天前就应该写好并发布出来的,由于要写小论文,被导师劈头盖脸的骂了几天,一直在搞论文,耽误了博文的编写。今天终于把小论文给投出去了,终于可以好好写博客了!

在上一篇文章《酷炫的Activity切换动画,打造更好的用户体验 》中,我们感受到了过渡切换动画带来的不一样的用户体验。如何你还意犹未尽,那么今天我们再体验一把图标切换动画。之前看过Material Design的图标切换,如下图:

图标切换

感觉效果挺好的,但是发现很多实现是通过多个图片切换产生的动画效果。如果想要定制属于自己的切换效果显然得要去制作很多张图片,导致apk变大不说,这得需要一定的flash功底啊,于是我就想是否可以通过属性动画,根据起始path数据和最终的path数据产生动画效果。先来个我们的最终效果图,让你更有动力往下看(PS:以下gif是放慢了的动画,另外gif丢帧导致不流畅,各位不要觉得很卡哈):

Path变形

旋转切换

加减变形

在API 21后,系统内置了AnimatedVectorDrawable ,它能将两个Path以动画方式切换。可是,毕竟不兼容5.0之前的版本,这个类还是过几年再用吧~。既然不用AnimatedVectorDrawable 类,我们就自己写一个呗~。

1 读取SVG path并显示

SVG绘制路径的命令虽然不多,如下(参考【W3School中SVG path教程】):

M : 相当于moveTo 两个参数表示移动终点位置的x,y
L :相当于lineto 两个参数表示x ,y
H :相当于水平的Line to,需要一个参数表示lineto的x坐标,y坐标则是当前绘制点的坐标
V :相当于垂直的line to需要一个参数表示lineto的y坐标
C :curveto(相当于cubicTo,需要6个参数,分别表示第1、2控制点坐标以及结束点的坐标
S :4个参数,表示平滑的使用3阶贝塞尔曲线,另一个控制点坐标被省略,需要我们去计算
Q :二阶贝塞尔曲线,4个参数,分别表示控制点和结束点坐标
T :平滑使用二阶贝塞尔曲线,只有2个参数表示结束点,控制点需要我们计算
A :绘制弧线,参数比较复杂,有7个参数
Z :相当于close path,无参数

其中S、T、A几个命令较复杂,本文先不去实现这几个命令,感兴趣的童鞋可以自己去实现。首先,一个Path是由多个Path组成,由于需要实现动画效果,也就是Path里面的数据我们需要动态变化,我们把各个Path“片段”封装到一个对象中。一个“片段”对应一个svg path的命令,因为参数最多是3个点(Point),我们只需封装3个Point对象:

class FragmentPath {
    //记录当前path片段的命令
    public PathType pathType;
    // 数据占用长度,同样是Line to,V、H与L后面携带的数据长度不同,这里需要记录
    public int dataLen;
    public Point p1;
    public Point p2;
    public Point p3;
}

其中,PathType是枚举类型,枚举类型无需加V、H命令,因为V、H在最终绘制的时候还是要转为Line To,dataLen参数用于记录当前的命令所占的字符串长度。PathType枚举类型如下:

  enum PathType {
    MOVE, LINE_TO, CURVE_TO, QUAD_TO,  CLOSE
}

对SVG path的操作太多,我们把这些操作单独封装到一个SVGUtil中,并将其设置为单例模式:

package com.hc.transformicon;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.graphics.Path;
import android.graphics.Point;
import android.util.Log;

public class SVGUtil {
    private static volatile SVGUtil svgUtil;
    private Set<String> svgCommandSet;
    private String[] command = { "M", "L", "H", "V", "C", "S", "Q", "T", "A",
            "Z" };

    private SVGUtil() {
        svgCommandSet = new HashSet<String>();
        for (String cmd : command) {
            svgCommandSet.add(cmd);
        }

    }

    public static SVGUtil getInstance() {
        if (svgUtil == null) {
            synchronized (SVGUtil.class) {
                if (svgUtil == null) {
                    svgUtil = new SVGUtil();
                }
            }
        }
        return svgUtil;
    }
    static class FragmentPath {
        //记录当前path片段的命令
        public PathType pathType;
        // 数据占用长度,同样是Line to,V、H与L后面携带的数据长度不同,这里需要记录
        public int dataLen;
        public Point p1;
        public Point p2;
        public Point p3;
    }
    static enum PathType {
        MOVE, LINE_TO, CURVE_TO, QUAD_TO, ARC, CLOSE
    }

}


由于SVG path中的数据可能写的格式不同,比如使用M命令,有些人会写成:M 100 100而有些人会写成M 100,100这还算好的了,因为看起来比较“规矩”,以空格或逗号分隔字符串就可以提取数据。有些人可能会写成M100,100,也就是在命令字母两边没有加空格,这就让你没办法提取数据了。另外还有就是用户不小心多加了几个空格,或者多加了几个逗号,这让你读取也会带来很多麻烦。还有就是用户还可能把M写成小写的m,在SVG中大小写的含义是不同的,但是我们不是去实现标准的SVG显示,我们可以去忽略大小写,我们只是借鉴一下SVG的命令,顺带学习一下SVG而已。说了那么多,就是为了引入一个话题:需要对用户原始数据进行预处理,在SVGUtil类中添加如下函数:

// 提取SVG数据
public ArrayList<String> extractSvgData(String svgData) {
    //以下为了将命令字母两边添加空格
    //保存已经替换过的字母
    Set<String> hasReplaceSet = new HashSet<String>();
    //正则表达式,用于匹配path里面的字母
    Pattern pattern = Pattern.compile("[a-zA-Z]");
    Matcher matcher = pattern.matcher(svgData);
    //遍历匹配正则表达式的字符串
    while (matcher.find()) {
        //s为匹配的字符串
        String s = matcher.group();
        //如果该字符串没有替换,则在改字符串两边加空格
        if (!hasReplaceSet.contains(s)) {
            svgData = svgData.replace(s, " " + s + " ");
            hasReplaceSet.add(s);
        }
    }
    //---end--命令字母两边添加字母结束---
    //将","替换为" ",并强制转为大写字母
    svgData = svgData.replace(",", " ").trim().toUpperCase();
    //以" "为分割符分割字符串
    String[] ss = svgData.split(" ");
    //将最终分割成的字符串数组转为List
    ArrayList<String> data = new ArrayList<String>();
    for (String s : ss) {
        //只有当前的字符串不是空格,才将该字符串加入到List中
        //相当于实现了自动删除多余的空格
        if (s != null && !"".equals(s)) {
            data.add(s);
        }
    }
    return data;
}

对原始数据做了预处理后,开始真正的将数据转换为Path对象了,在SVGUtil类中添加如下函数:

//根据ArrayList保存的数据,将path数据转为Android中的Path对象
//widthFactor,宽度放缩倍数
//heightFactor,高度放缩倍数
public Path parsePath(ArrayList<String> svgDataList, float widthFactor,
        float heightFactor) {
    //new一个需要返回的Path对象
    Path path = new Path();
    //解析字符串偏移位置
    int startIndex = 0;
    //上一次绘制的终点,默认为左上角
    Point lastPoint = new Point(0, 0);
    //提取下一条FragmentPath对象
    FragmentPath fp = nextFrag(svgDataList, startIndex, lastPoint);
    //如果下一条FragmentPath不为null,则循环
    while (fp != null) {
        //根据命令类型,执行Path的不同方法,主要,所有的坐标需要乘以放缩倍数
        switch (fp.pathType) {
        case MOVE: {
            path.moveTo(fp.p1.x * widthFactor, fp.p1.y * heightFactor);
            lastPoint = fp.p1;
            break;
        }
        case LINE_TO: {
            path.lineTo(fp.p1.x * widthFactor, fp.p1.y * heightFactor);
            lastPoint = fp.p1;
            break;
        }
        case CURVE_TO: {
            path.cubicTo(fp.p1.x * widthFactor, fp.p1.y * heightFactor,
                    fp.p2.x * widthFactor, fp.p2.y * heightFactor, fp.p3.x
                            * widthFactor, fp.p3.y * heightFactor);
            lastPoint = fp.p3;
            break;
        }
        case QUAD_TO: {
            path.quadTo(fp.p1.x * widthFactor, fp.p1.y * heightFactor,
                    fp.p2.x * widthFactor, fp.p2.y * heightFactor);
            lastPoint = fp.p2;
            break;
        }

        case CLOSE: {
            path.close();
        }
        default:
            break;
        }
        //设置下一条Path的偏移量,以便提取下一条命令
        startIndex = startIndex + fp.dataLen + 1;
        fp = nextFrag(svgDataList, startIndex, lastPoint);
    }
    return path;
}

我们看到,参数中有宽高的放缩倍数。为什么需要放缩倍数呢?我们知道,SVG是矢量图,放缩后图片清晰度是无影响的,因此我们这里需要加放缩倍数。另外我们注意到还有个nextFrag函数,用于提取下一条命令,并封装为FragmentPath对象,在SVGUtil类中添加如下函数:

//根据偏移量,解析下一条命令,并将命令封装为FragmentPath对象
private FragmentPath nextFrag(ArrayList<String> svgData, int startIndex,
        Point lastPoint) {
    if (svgData == null)
        return null;
    int svgDataSize = svgData.size();
    if (startIndex >= svgDataSize)
        return null;
    // 当前的path片段下标范围[startIndex,i)
    int i = startIndex + 1;
    //保存该命令的长度(指数据长度,不包括命令字母)
    int length = 0;
    FragmentPath fp = new FragmentPath();
    //计算命令的长度
    while (i < svgDataSize) {
        if (svgCommandSet.contains(svgData.get(i)))
            break;
        i++;
        length++;
    }
    //数据长度保存到FragmentPath对象中
    fp.dataLen = length; 
    // 根据数据的长度,把各个数据封装到Point对象,并保存到FragmentPath中
    switch (length) {
    case 0: {
        Log.d("", svgData.get(startIndex) + " none data");
        break;
    }
    case 1: {//如果数据只有一个,那么可能是H或V命令,我们需要根据上一次的终端推算x或y坐标
        int d = (int) Float.parseFloat(svgData.get(startIndex + 1));
        if (svgData.get(startIndex).equals("H")) {
            fp.p1 = new Point(d, lastPoint.y);

        } else {// "V"
            fp.p1 = new Point(lastPoint.x, d);

        }

        break;
    }
    case 2: {//两个数据,只有一个Point对象(x,y)
        int x = (int) Float.parseFloat(svgData.get(startIndex + 1));
        int y = (int) Float.parseFloat(svgData.get(startIndex + 2));
        fp.p1 = new Point(x, y);

        break;
    }
    case 4: {//4个数据,则封装到两个Point对象中
        int x1 = (int) Float.parseFloat(svgData.get(startIndex + 1));
        int y1 = (int) Float.parseFloat(svgData.get(startIndex + 2));
        int x2 = (int) Float.parseFloat(svgData.get(startIndex + 3));
        int y2 = (int) Float.parseFloat(svgData.get(startIndex + 4));
        fp.p1 = new Point(x1, y1);
        fp.p2 = new Point(x2, y2);

        break;
    }
    case 6: {//6个数据,封装到3个Point对象中
        int x1 = (int) Float.parseFloat(svgData.get(startIndex + 1));
        int y1 = (int) Float.parseFloat(svgData.get(startIndex + 2));
        int x2 = (int) Float.parseFloat(svgData.get(startIndex + 3));
        int y2 = (int) Float.parseFloat(svgData.get(startIndex + 4));
        int x3 = (int) Float.parseFloat(svgData.get(startIndex + 5));
        int y3 = (int) Float.parseFloat(svgData.get(startIndex + 6));
        fp.p1 = new Point(x1, y1);
        fp.p2 = new Point(x2, y2);
        fp.p3 = new Point(x3, y3);

        break;
    }
    default:
        break;
    }
    // 设置当前路径片段的绘制类型
    switch (svgData.get(startIndex)) {
    case "M": {
        fp.pathType = PathType.MOVE;
        break;
    }
    case "H":
    case "V":
    case "L": {
        fp.pathType = PathType.LINE_TO;
        break;
    }

    case "C": {
        fp.pathType = PathType.CURVE_TO;
        break;
    }

    case "Q": {
        fp.pathType = PathType.QUAD_TO;
        break;
    }
    case "Z": {
        fp.pathType = PathType.CLOSE;
        break;
    }

    }
    return fp;
}

接下来就是自定义View了,由于接下来我们需要实现动画效果,因此我们就将自定义的View继承SurfaceView:

package com.hc.transformicon;

import java.util.ArrayList;

import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Bitmap.Config;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;

/**
 * Created by HuaChao on 2016/6/17.
 */
public class SVGPathView extends SurfaceView implements SurfaceHolder.Callback {

    // 动画起始Path数据
    private ArrayList<String> svgStartDataList;
    // 动画结束时的Path数据
    private ArrayList<String> svgEndDataList;

    private SurfaceHolder surfaceHolder;
    // 用于SurfaceView显示的对象
    private Bitmap mBitmap;
    private Canvas mCanvas;
    private Paint mPaint;
    // view的宽高
    private int mWidth;
    private int mHeight;
    // SVG path里面的数据中参考的宽高
    private int mViewWidth;
    private int mViewHeight;
    // 绘制线条的宽度
    private int mPaintWidth;

    // 用于等比放缩
    private float widthFactor;
    private float heightFactor;
    private int mPaintColor;

    public SVGPathView(Context context) {
        super(context);
        init();
    }

    public SVGPathView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs,
                R.styleable.SVGPathView);
        // 读取布局文件设置的起始Path数据和结束Path数据
        String svgStartPath = ta
                .getString(R.styleable.SVGPathView_svg_start_path);
        String svgEndPath = ta.getString(R.styleable.SVGPathView_svg_end_path);
        // 如果二者有一个没有设置,就将没有设置的那个设定为已经设置的数据
        if (svgStartPath == null && svgEndPath != null) {
            svgStartPath = svgEndPath;
        } else if (svgStartPath != null && svgEndPath == null) {
            svgEndPath = svgStartPath;
        }
        // 读取布局文件的配置
        mViewWidth = ta.getInteger(R.styleable.SVGPathView_svg_view_width, -1);
        mViewHeight = ta
                .getInteger(R.styleable.SVGPathView_svg_view_height, -1);
        mPaintWidth = ta.getInteger(R.styleable.SVGPathView_svg_paint_width, 5);
        mPaintColor = ta.getColor(R.styleable.SVGPathView_svg_color,
                Color.BLACK);
        // 将原始数据做预处理
        svgStartDataList = SVGUtil.getInstance().extractSvgData(svgStartPath);
        svgEndDataList = SVGUtil.getInstance().extractSvgData(svgEndPath);

        ta.recycle();
        init();
    }

    // 初始化
    private void init() {
        surfaceHolder = getHolder();
        surfaceHolder.addCallback(this);
        mPaint = new Paint();
        mPaint.setStrokeJoin(Join.ROUND);
        mPaint.setStrokeCap(Cap.ROUND);
        mPaint.setColor(mPaintColor);

    }

    // 开始绘制
    public void drawPath() {
        clearCanvas();
        mPaint.setStyle(Style.STROKE);
        mPaint.setColor(mPaintColor);
        Path path = SVGUtil.getInstance().parsePath(svgStartDataList,
                widthFactor, heightFactor);
        mCanvas.drawPath(path, mPaint);
        Canvas canvas = surfaceHolder.lockCanvas();
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
        surfaceHolder.unlockCanvasAndPost(canvas);
    }

    // 清屏
    private void clearCanvas() {
        mPaint.setColor(Color.WHITE);
        mPaint.setStyle(Style.FILL);
        mCanvas.drawRect(0, 0, mWidth, mHeight, mPaint);

    }

    // 调用invalidate时,把Bitmap对象绘制到View中
    @Override
    public void invalidate() {
        super.invalidate();
        Canvas canvas = surfaceHolder.lockCanvas();
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
        surfaceHolder.unlockCanvasAndPost(canvas);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
        // 保存当前的View宽高
        mWidth = width;
        mHeight = height;
        // 如果没有设置Path的参考宽高,默认设置为View的宽高
        if (mViewWidth <= 0) {
            mViewWidth = width;
        }
        if (mViewHeight <= 0) {
            mViewHeight = height;
        }
        // 计算放缩倍数
        widthFactor = 1.f * width / mViewWidth;
        heightFactor = 1.f * height / mViewHeight;
        // 创建Bitmap对象,用于绘制到屏幕中
        mBitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
        mCanvas = new Canvas(mBitmap);
        // 将画笔绘制线条的宽度设置为经过放缩后的宽度
        mPaint.setStrokeWidth(mPaintWidth * widthFactor);
        // 清屏
        clearCanvas();
        // 将清屏结果绘制到屏幕
        invalidate();
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

    }

}


最后,再看看我们的布局文件以及自定义的布局属性:
styles.xml添加如下:

<declare-styleable name="SVGPathView">
 
    <attr name="svg_start_path" format="reference" />
    <attr name="svg_end_path" format="reference" />
    <attr name="svg_paint_width" format="integer" />
    <attr name="svg_view_width" format="integer" />
    <attr name="svg_view_height" format="integer" />
    <attr name="svg_color" format="color" />
</declare-styleable>

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:app="http://schemas.android.com/apk/res/com.hc.transformicon"
   android:layout_width="match_parent"
   android:layout_height="match_parent"   >

   <com.hc.transformicon.SVGPathView
       android:id="@+id/svgPathView"
       android:layout_width="100dp"
       android:layout_height="100dp"
       app:svg_color="#00ff00"
       app:svg_paint_width="18"
       app:svg_start_path="@string/svg_back"
       app:svg_view_height="100"
       app:svg_view_width="100" />

</RelativeLayout>

布局文件中可以看到,我们设定的path里面的数据,参考的宽高是100,看看我们的path是怎么写的:

<string name="svg_back">M 50 14 L 90 50 M 10 50 H 90 M 50 86 L 90 50</string>

最终会有一个箭头显示处理,无论我们的SVGPathView宽高如何,都会等比放缩。先看看最后显示的图吧~

SVG显示

2 两个Path以动画方式变形

为了避免每次都通过解析字符串的方式来生成Path对象,我们需要把ArrayList<String> 转为ArrayList<FragmentPath>即保存已经解析过的命令,减少重复解析。修改SVGPathView类中的svgStartDataListsvgEndDataList

// 动画起始Path数据
private ArrayList<FragmentPath> svgStartDataList;
// 动画结束时的Path数据
private ArrayList<FragmentPath> svgEndDataList;

并在构造函数中,修改svgStartDataListsvgEndDataList对象创建方式:

SVGUtil svgUtil = SVGUtil.getInstance();
// 将原始数据做预处理
ArrayList<String> svgStartStrList = svgUtil.extractSvgData(svgStartPath);
ArrayList<String> svgEndStrList = svgUtil.extractSvgData(svgEndPath);

// 将经过预处理后的path数据,转为FragmentPath列表
svgStartDataList = svgUtil.strListToFragList(svgStartStrList);
svgEndDataList = svgUtil.strListToFragList(svgEndStrList);

SVGUtil中添加strListToFragList函数:

// 将path字符串列表转为封装成FramentPath片段的列表
public ArrayList<FragmentPath> strListToFragList(ArrayList<String> svgDataList) {
    ArrayList<FragmentPath> fragmentPaths = new ArrayList<SVGUtil.FragmentPath>();
    int startIndex = 0;
    Point lastPoint = new Point(0, 0);
    FragmentPath fp = nextFrag(svgDataList, startIndex, lastPoint);
    while (fp != null) {
        fragmentPaths.add(fp);
        switch (fp.pathType) {
        case MOVE:
        case LINE_TO: {
            lastPoint = fp.p1;
            break;
        }
        case CURVE_TO: {
            lastPoint = fp.p3;
            break;
        }
        case QUAD_TO: {
            lastPoint = fp.p2;
            break;
        }

        default:
            break;
        }
        startIndex = startIndex + fp.dataLen + 1;
        fp = nextFrag(svgDataList, startIndex, lastPoint);
    }
    return fragmentPaths;
}

SVGPathView类中的drawPath函数也需要修改,因为我们是通过属性动画动态生成Path了,而不是当初直接解析原始数据生成Path,将drawPath修改如下:

public void drawPath(Path path) {
    clearCanvas();
    mPaint.setStyle(Style.STROKE);
    mPaint.setColor(mPaintColor);

    mCanvas.drawPath(path, mPaint);
    Canvas canvas = surfaceHolder.lockCanvas();
    canvas.drawBitmap(mBitmap, 0, 0, mPaint);
    surfaceHolder.unlockCanvasAndPost(canvas);
}

SVGPathView类中新加一个函数startTransform,用于开启动画,作为开始执行的入口函数:


public void startTransform() {
if (!isAnim) {
    isAnim = true;
    ValueAnimator va = ValueAnimator.ofFloat(0, 1f);
    va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float animatorFactor = (float) animation.getAnimatedValue();
            Path path = SVGUtil.getInstance().parseFragList(
                    svgStartDataList, svgEndDataList, widthFactor,
                    heightFactor, animatorFactor);
            drawPath(path);
        }
    });
    va.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
        }

        @Override
        public void onAnimationRepeat(Animator animation) {
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            isAnim = false;
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            isAnim = false;
        }
    });
    va.setDuration(1000).start();
     
}
}

// 开始绘制
public void drawPath(Path path) {
clearCanvas();
mPaint.setStyle(Style.STROKE);
mPaint.setColor(mPaintColor);

mCanvas.drawPath(path, mPaint);
Canvas canvas = surfaceHolder.lockCanvas();
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
surfaceHolder.unlockCanvasAndPost(canvas);
}

可以看到,真正的核心函数是SVGUtilparseFragList函数,这个函数是根据起始的Path数据和终止的Path数据,以及动画变化时刻的数据,生成新的Path,这个函数也不复杂:

public Path parseFragList(ArrayList<FragmentPath> svgStartDataList,
        ArrayList<FragmentPath> svgEndDataList, float widthFactor,
        float heightFactor, float animatorFactor) {
    Path path = new Path();

    for (int i = 0; i < svgStartDataList.size(); i++) {
        FragmentPath startFp = svgStartDataList.get(i);
        FragmentPath endFp = svgEndDataList.get(i);
        //计算出当前的3个点的位置
        int x1 = 0;
        int y1 = 0;
        int x2 = 0;
        int y2 = 0;
        int x3 = 0;
        int y3 = 0;
        if (startFp.p1 != null) {
            x1 = (int) (startFp.p1.x + (endFp.p1.x - startFp.p1.x)
                    * animatorFactor);
            y1 = (int) (startFp.p1.y + (endFp.p1.y - startFp.p1.y)
                    * animatorFactor);
        }

        if (startFp.p2 != null) {
            x2 = (int) (startFp.p2.x + (endFp.p2.x - startFp.p2.x)
                    * animatorFactor);
            y2 = (int) (startFp.p2.y + (endFp.p2.y - startFp.p2.y)
                    * animatorFactor);
        }

        if (startFp.p3 != null) {
            x3 = (int) (startFp.p3.x + (endFp.p3.x - startFp.p3.x)
                    * animatorFactor);
            y3 = (int) (startFp.p3.y + (endFp.p3.y - startFp.p3.y)
                    * animatorFactor);
        }
        switch (startFp.pathType) {
        case MOVE: {

            path.moveTo(x1 * widthFactor, y1 * heightFactor);
            break;
        }
        case LINE_TO: {

            path.lineTo(x1 * widthFactor, y1 * heightFactor);
            break;
        }
        case CURVE_TO: {

            path.cubicTo(x1 * widthFactor, y1 * heightFactor, x2
                    * widthFactor, y2 * heightFactor, x3 * widthFactor, y3
                    * heightFactor);
            break;
        }
        case QUAD_TO: {
            path.quadTo(x1 * widthFactor, y1 * heightFactor, x2
                    * widthFactor, y2 * heightFactor);
            break;
        }
        case CLOSE: {
            path.close();
        }
        default:
            break;
        }
    }
    return path;
}

好啦,看看动画吧~

Path变形

我们再加上旋转动画一起执行,让切换效果更自然一点,先设置rotateDegree属性,并在onAnimationUpdate函数中添加rotateDegree = animatorFactor * 360;注意,需要在drawPath函数执行之前添加。
将drawPath中的

 mCanvas.drawPath(path, mPaint);

改为

mCanvas.save(); 
mCanvas.rotate(rotateDegree, mWidth / 2, mHeight / 2);
mCanvas.drawPath(path, mPaint);

看看效果吧~

旋转切换

动画设置时间为1秒,加上Gif丢帧的原因,所以上面效果看起似乎有点不流畅

最后,请注意,两个变形的Path数据中,对应的命令格式一定要一模一样,否则会出错!!!!
比如,要实现如下效果


加减变形

path数据则必须写成:

<string name="svg_add">   M 10,50 H 90 M 50 10 V 90 </string>
<string name="svg_remove">M 10,50 H 90 M 10 50 H 90</string>

虽然减号可以通过如下就可以画出来

<string name="svg_remove">M 10,50 H 90 </string>

但是,我们需要加号中后半段数据的最终变形位置,因此不可以省去后面的。

最后献上源码:http://download.csdn.net/download/huachao1001/9554503

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

推荐阅读更多精彩内容