Deploying the iconic ☺ are all the same — step by step

Reprint please indicate the source: [huachao1001 column: http://blog.csdn.net/huachao1001]

* This article has been published exclusively by guolin_blog, an official wechat account

Originally, this article should have been written and published a few days ago, but because I had to write a small paper, I was scolded by my tutor for a few days. I have been working on the paper and delayed the writing of my blog. Today, I finally put the small paper out, and I can finally write a blog!

In our last article, cool Activities to switch animations for a Better User experience, we saw the difference in user experience brought by transitioning animations. In case you’re still not satisfied, today we’re going to try another icon toggle animation. We have seen the Material Design icon switch before, as shown below:

Feel the effect is quite good, but found that many implementations are generated through multiple image switching animation effects. If you want to customize your own switching effects, you obviously need to make a lot of images, which will cause the APK to become large, but it requires some flash skills. So I thought if I could animate properties based on the initial path data and the final path data, I could generate animation effects. Here’s our final image to give you more motivation to look down:

AnimatedVectorDrawable built in after API 21, AnimatedVectorDrawable can animate between two paths. However, this class is not compatible with pre-5.0 versions, so it will be used in a few years. Since we don’t use AnimatedVectorDrawable, we’ll write our own.

There are few commands to draw paths in SVG, as follows:

M : The two parameters are equivalent to the x of the moving end point, y L: equivalent to the Line to the two parameters are x, y H: equivalent to the Line to the horizontal, a parameter is required to represent the Line to the x coordinate, the y coordinate is the current drawing point V: The vertical lineto (cubicTo) requires one parameter to represent the y-coordinate of lineTO (C) : Curveto (cubicTo) requires six parameters, representing the coordinates of the 1st and 2nd control points and the coordinate S of the end point respectively: Four parameters, the use of said smooth 3 order Bessel curve, another control point coordinates are omitted, we need to calculate the Q: quadratic bezier, four parameters, respectively, the control points and end points coordinates T: smooth using quadratic bezier curve, the end point of only two parameters, the control points we need to calculate A: To draw an arc, the parameters are relatively complex, with 7 parameters Z: equivalent to close path, no parameters

Among them, S, T and A commands are more complex, so this paper will not implement these commands, and interested children can implement them by themselves. First of all, a Path is composed of multiple paths. Since we need to animate the data in the Path, we need to dynamically change the Path. We encapsulate each Path “fragment” into an object. One “fragment” corresponds to one SVG path command, and since arguments are up to three points, we only need to encapsulate three Point objects:

class FragmentPath {
    
    public PathType pathType;
    
    public int dataLen;
    public Point p1;
    public Point p2;
    public Point p3;
}Copy the code

PathType is an enumeration type, which does not need To add V and H commands, because V and H are converted To Line To in the final drawing. The dataLen parameter is used To record the length of the string occupied by the current command. PathType Enumeration types are as follows:

  enum PathType {
    MOVE, LINE_TO, CURVE_TO, QUAD_TO,  CLOSE
}
Copy the code

There are so many operations on the SVG path that we wrapped them into a separate SVGUtil and set it to singleton mode:

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 svgCommandSet;
    private String[] command = { "M", "L", "H", "V", "C", "S", "Q", "T", "A",
            "Z" };

    private SVGUtil() {
        svgCommandSet = new HashSet();
        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 {
        
        public PathType pathType;
        
        public int dataLen;
        public Point p1;
        public Point p2;
        public Point p3;
    }
    static enum PathType {
        MOVE, LINE_TO, CURVE_TO, QUAD_TO, ARC, CLOSE
    }

}

Copy the code

Since data in an SVG path can be written in different formats, such as M commands, some people will write: M 100 100 while others will write M 100,100. This is good because it looks “regular”, separating strings with Spaces or commas to extract data. Some people might write M100,100, which means you don’t have Spaces around the command letters, so you can’t extract the data. And then there’s the fact that the user accidentally added a few Spaces, or a few commas, which makes reading a lot of trouble. It is also possible to write M as lowercase M. In SVG, case is different, but instead of implementing a standard SVG display, we can ignore case. We will just take a look at SVG commands and learn about SVG along the way. To introduce the topic of pre-processing user raw data, add the following functions to the SVGUtil class:

public ArrayList extractSvgData(String svgData) { Set hasReplaceSet = new HashSet(); Pattern pattern = Pattern.compile("[a-zA-Z]"); Matcher matcher = pattern.matcher(svgData); while (matcher.find()) { String s = matcher.group(); if (! hasReplaceSet.contains(s)) { svgData = svgData.replace(s, " " + s + " "); hasReplaceSet.add(s); } } svgData = svgData.replace(",", " ").trim().toUpperCase(); String[] ss = svgData.split(" "); ArrayList data = new ArrayList(); for (String s : ss) { if (s ! = null && !" ".equals(s)) { data.add(s); } } return data; }Copy the code

After preprocessing the raw data and actually converting the data to a Path object, add the following functions to the SVGUtil class:

public Path parsePath(ArrayList svgDataList, float widthFactor, float heightFactor) { Path path = new Path(); int startIndex = 0; Point lastPoint = new Point(0, 0); FragmentPath fp = nextFrag(svgDataList, startIndex, lastPoint); while (fp ! = null) { 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; } startIndex = startIndex + fp.dataLen + 1; fp = nextFrag(svgDataList, startIndex, lastPoint); } return path; }Copy the code

As we can see, the parameters have width and height scaling. Why do we need scaled multiples? As we know, SVG is a vector image, and the sharpness of the image is not affected after scaling, so we need to add scaling multiples here. We also notice that there is a nextFrag function that extracts the next command and encapsulates it as a FragmentPath object. Add the following function to the SVGUtil class:


private FragmentPath nextFrag(ArrayList svgData, int startIndex,
        Point lastPoint) {
    if (svgData == null)
        return null;
    int svgDataSize = svgData.size();
    if (startIndex >= svgDataSize)
        return null;
    
    int i = startIndex + 1;
    
    int length = 0;
    FragmentPath fp = new FragmentPath();
    
    while (i < svgDataSize) {
        if (svgCommandSet.contains(svgData.get(i)))
            break;
        i++;
        length++;
    }
    
    fp.dataLen = length; 
    
    switch (length) {
    case 0: {
        Log.d("", svgData.get(startIndex) + " none data");
        break;
    }
    case 1: {
        int d = (int) Float.parseFloat(svgData.get(startIndex + 1));
        if (svgData.get(startIndex).equals("H")) {
            fp.p1 = new Point(d, lastPoint.y);

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

        }

        break;
    }
    case 2: {
        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: {
        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: {
        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;
}
Copy the code

Next comes the custom View. Since we need to animate the View, we will inherit the SurfaceView from the custom View:

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  { private ArrayList svgStartDataList; private ArrayList svgEndDataList; private SurfaceHolder surfaceHolder; private Bitmap mBitmap; private Canvas mCanvas; private Paint mPaint; private int mWidth; private int mHeight; 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); 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); } @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) { mWidth = width; mHeight = height; if (mViewWidth <= 0) { mViewWidth = width; } if (mViewHeight <= 0) { mViewHeight = height; } widthFactor = 1.f * width / mViewWidth; heightFactor = 1.f * height / mViewHeight; 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) { } }Copy the code

Finally, take a look at our layout file with our custom layout property: styles.xml added as follows:



    
    
    
    
    
    
Copy the code

activity_main.xml



   

Copy the code

In the layout file, you can see that we set the data in the path. The reference width and height of the path is 100.

M 50 14 L 90 50 M 10 50 H 90 M 50 86 L 90 50Copy the code

Eventually there will be an arrow showing the processing, which will scale regardless of the width and height of our SVGPathView. Take a look at the final image

To avoid generating a Path object by parsing a string each time, we need to convert ArrayList to ArrayList, which saves parsed commands and reduces the need for repeated parsing. Modify svgStartDataList and svgEndDataList in SVGPathView:


private ArrayList svgStartDataList;

private ArrayList svgEndDataList;
Copy the code

In the constructor, change the creation methods of svgStartDataList and svgEndDataList objects:

SVGUtil svgUtil = SVGUtil.getInstance();

ArrayList svgStartStrList = svgUtil.extractSvgData(svgStartPath);
ArrayList svgEndStrList = svgUtil.extractSvgData(svgEndPath);


svgStartDataList = svgUtil.strListToFragList(svgStartStrList);
svgEndDataList = svgUtil.strListToFragList(svgEndStrList);
Copy the code

Add strListToFragList to SVGUtil:

public ArrayList strListToFragList(ArrayList svgDataList) { ArrayList fragmentPaths = new ArrayList(); 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; }Copy the code

The drawPath function in the SVGPathView class also needs to be changed, because we dynamically generated the Path through the property animation, instead of directly parsing the raw data to generate the Path.

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);
}Copy the code

Add a new function startTransform to the SVGPathView class to start the animation as an entry function to start execution:


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);
}Copy the code

As you can see, the real core function is SVGUtil’s parseFragList function, which generates a new Path based on the starting and ending Path data, as well as the data at the time of the animation change. This function is also not complicated:

public Path parseFragList(ArrayList svgStartDataList, ArrayList 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); 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; }Copy the code

Ok, let’s watch the animation



Let’s add the rotation animation to make the switching effect more natural. First set the rotateDegree property and set theonAnimationUpdateAdd to functionrotateDegree = animatorFactor * 360;Note that it needs to be added before the drawPath function is executed.

Will the drawPath

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

Look at the effect

The animation time is set to 1 second, plus the Gif frame loss reason, so the above looks a bit awkward

Finally, note that the command format of the two paths must be exactly the same; otherwise, an error will occur !!!!

For example, to achieve the following effect



Path data must be written as:

M 10,50 H 90 M 50 10 V 90 M 10,50 H 90 M 10,50 H 90 M 10,50 H 90 M 10Copy the code

Although the minus sign can be drawn as follows

H M 10, 50, 90Copy the code

However, we need the final deformation position of the data in the middle and the second half of the plus sign, so we can’t omit the latter.

Finally presented source: download.csdn.net/download/hu…