Chapter 5 advanced CustomView animation

It is difficult to achieve some complex animation effects only by changing the control properties, such as the Nexus boot animation. This will show how to use PathMeasure and SVG animation to achieve complex animation effects.

initialization:

PathMeasure is similar to a calculator. It can calculate some information of the specified path, such as the total length of the path, the coordinate points corresponding to the specified length, etc.

Construction mode 1:
PathMeasure pathMeasure = new PathMeasure();
setPath(Path path, boolean forceClosed);
Construction mode 2:
PathMeasure(Path path, boolean forceClosed);
● forceClosed: indicates whether Path needs to be closed finally. forceClosed only affects the measurement results of PathMeasure,
               For example, the Path of a broken line is not closed. When forceClosed is set to true,
               The calculation of PathMeasure will contain the last closed Path, which is different from the original Path.

Simple function use:

1.getLength() function

public float getLength() // Get the current calculated path length (not the entire path)
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.translate(50, 50);

    Path path = new Path();
    path.moveTo(0, 0);
    path.lineTo(0, 100);
    path.lineTo(100, 100);
    path.lineTo(100, 0);

    PathMeasure measure1 = new PathMeasure(path, false);
    PathMeasure measure2 = new PathMeasure(path, true);

    Log.d("TAG", "forceClosed=false---->" + measure1.getLength());
    Log.d("TAG", "forceClosed=true----->" + measure2.getLength());

    Paint paint = new Paint();
    paint.setColor(Color.BLACK);
    paint.setStrokeWidth(8);
    paint.setStyle(Paint.Style.STROKE);
    canvas.drawPath(path, paint);
}

D/TAG: forceClosed=false---->300.0
D/TAG: forceClosed=true----->400.0

Obviously, when forceClosed=false, the length of the current Path state is measured; if forceClosed=true, the closed length of Path is measured whether the Path is closed or not.

2.isClosed() function

public boolean isClosed()

If forceClosed is set to true when associating Path, the return value of this function must be true.
3.nextContour() function (contour: [ˈ k ː nt ʊ r] profile; contour)

Path can be composed of multiple curves, but whether it is getLength(), getSegment(), or other functions, only the first segment will be calculated. nextContour() is the function used to jump to the next curve. Returns true if the jump is successful or false if the jump fails.

canvas.translate(150, 150);
Path path = new Path();
path.addRect(-50, -50, 50, 50, Path.Direction.CW);
path.addRect(-100, -100, 100, 100, Path.Direction.CW);
path.addRect(-120, -120, 120, 120, Path.Direction.CW);
canvas.drawPath(path, paint);

PathMeasure measure = new PathMeasure(path, false);

do {
    float len = measure.getLength();
    Log.d("TAG", "len=" + len);
} while (measure.nextContour());

Through do...while loop and measure.nextContour() function is used to enumerate all curves in Path one by one.

The following conclusions can be drawn from this example:

● pass PathMeasure.nextContour The () function results in the same order of curves as added in Path.

● getLength() and other functions are all aimed at the current curve, not the entire Path.

getSegment() function:

1. Basic usage

boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
● startD: (D:distance) the length of the start position [distance from the Path start point].
● stopD: (D:distance) the length of [distance from Path start point] at the end of [intercept].
● dst: the intercepted Path will be added to the dst. Note that it's adding, not replacing.
● startWithMoveTo: call Path.moveTo The () function changes the starting point of the path to the starting point of the newly added path.
                   That is, if you draw segment0, then draw segment1, if startWithMoveTo=true,
                   The starting point of the path is the starting point of segment1, because the start with moveTo() function is called;
                   startWithMoveTo=false the starting point of the path is the same, that is, the end point of segment0, to ensure that the path is continuous.
                   Begin the segment with a moveTo if startWithMoveTo is true.
                   In a word, true: each Path is independent; false: each Path is continuous.
Note:
If the values of startD and stopD are not in the value range [0, getLength], or startD==stopD, false will be returned, and the contents in dst will not be changed.
When the hardware acceleration function is enabled, there will be problems in the drawing. Therefore, when using the getSegment() function, you need to disable the hardware acceleration function.

Because the Path saved in dst is added continuously, rather than being overwritten every time. If it is set to false, the new fragment will be calculated from the end of the last Path, so that the array of Path fragments intercepted can be saved continuously.

Example 1:

canvas.translate(100, 100);
Path path = new Path();
path.addRect(-50, -50, 50, 50, Path.Direction.CW);
Path dst = new Path();
PathMeasure measure = new PathMeasure(path, false);
measure.getSegment(0, 150, dst, true);
canvas.drawPath(dst, paint);

adopt measure.getSegment(0, 150, dst, true); the intercept length is 0-150. After successful interception, the new path segment will be added to the dst path, and finally the dst path will be drawn:

Conclusion 1: path interception starts from the upper left corner of the path.

Because the way the path is generated specifies Path.Direction.CW (clockwise), so cut it clockwise.

Conclusion 2: the direction of path interception is the same as that of path generation.

Example 2: if dst path is not empty

Path path = new Path();
path.addRect(-50, -50, 50, 50, Path.Direction.CW);
Path dst = new Path();
dst.lineTo(10, 100);
PathMeasure measure = new PathMeasure(path, false);
measure.getSegment(0, 150, dst, true);
canvas.drawPath(dst, paint);

(there are two segment s in dst, which are draw n out)

Conclusion 3: the intercepted Path fragment will be added to the Path dst instead of replacing the content in the dst.

Example 3: if the startWithMoveTo parameter is false

Path path = new Path();
path.addRect(-50, -50, 50, 50, Path.Direction.CW);
Path dst = new Path();
dst.lineTo(10, 100);
PathMeasure measure = new PathMeasure(path, false);
measure.getSegment(0, 150, dst, false);
canvas.drawPath(dst, paint);

startWithMoveTo means whether to call the Path.MoveTo The () function changes the starting point of the path to the starting point of the newly added path. If it is set to true, the path start point will be moved to the start point of the newly added path, so that the shape of the currently added path can be maintained; if it is set to false, it will not be called Path.moveTo () function, which changes the starting point of the path to the ending point of the previous path to maintain continuity. The newly added path will not be changed except the starting point position.

(drawPath() draws segment0, segment1,... segmentN added to dst in turn)

Conclusion 4: if startWithMoveTo is true, the Path fragment intercepted will remain unchanged; if startWithMoveTo is false, the starting point of Path fragment intercepted will be moved to the last point of dst to ensure the continuity of dst Path.

2. Example: path loading animation

public GetSegmentView(Context context, AttributeSet attrs) {
    super(context, attrs);
    setLayerType(LAYER_TYPE_SOFTWARE, null);
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);// Anti aliasing logo
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(4);
    mPaint.setColor(Color.BLACK);
    mDstPath = new Path();
    mCirclePath = new Path();
    mCirclePath.addCircle(100, 100, 50, Path.Direction.CW);
    mPathMeasure = new PathMeasure(mCirclePath, true);
    ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
    animator.setRepeatCount(ValueAnimator.INFINITE);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        public void onAnimationUpdate(ValueAnimator animation) {
            mCurAnimValue = (Float) animation.getAnimatedValue();
            invalidate();
        }
    });
    animator.setDuration(2000);
    animator.start();
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.WHITE);
    float stopD = mPathMeasure.getLength() * mCurAnimValue;
    mDestPath.reset();// Better performance! Avoid continuous redraws!
    mPathMeasure.getSegment(0, stopD, mDstPath, true);
    canvas.drawPath(mDstPath, mPaint);
}

Since the path from every call to the getSegment() function is added to the mdstpath, call mDstPath.reset The () function clears the previously generated path. Finally, draw the generated path.

When animating, it always starts at the 0 position.

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.WHITE);
    float length = mPathMeasure.getLength();
    float stopD = length * mCurAnimValue;
    float startD = (float) (stopD - ((0.5 - Math.abs(mCurAnimValue - 0.5)) * length));
    mDstPath.reset();
    mPathMeasure.getSegment(startD, stopD, mDstPath, true);
    canvas.drawPath(mDstPath, mPaint);
}

Only the calculation method of starting point is changed here:

float startD = (float) (stop - ((0.5 - Math.abs(mCurAnimValue - 0.5)) * length));

According to the effect description, when the progress is 0-0.5, the starting point of the path is 0; when the progress is 0.5-1, the starting point of the path is gradually close to the end point; when the progress is 1, the two points coincide.

When the progress is less than 0.5, start=0; when the progress is greater than 0.5, start = 2 * mcuranimalvalue-1.

It is also possible to use if...else.. statements to calculate start, for example: (but when a pure number like 0.5 appears in the code, it is not easy to read and maintain)

float start = 0;
if (start >= 0.5) {
    start = 2 * mCurAnimValue -1;
}
...
mPathMeasure.getSegment(start, stop, mDstPath, true);

getPosTan() function:

1. Overview

The getPosTan() function is used to get the position of a certain length on the path and the tangent value of the position.

boolean getPosTan(float distance, float[] pos, float[] tan)
● distance: distance Path The length of the starting point. The value range is {0, getLength}
● pos: The coordinate value of the point. The position of the current point on the canvas has two values, which are x,y Coordinates. pos[0]express x Coordinates, pos[1]express y coordinate
● tan: The tangent value of this point is also a two-dimensional array{cos, sin}

    

In the above figure, the tangent value of point B coordinate is, so the return value of tan array is . The whole calculation process is as follows:

Since tan=sin/cos, the value of tan array is {cos, sin}.

In the Math class, there are two functions to find the arctangent value.

double atan(double d) // Parameter is radian value
double atan2(double y, double x) // Parameter is (x,y) coordinate value

Since tan={cos, sin}, tan=sin/cos, the radian value = atan2(tan[1], tan[0]).

There is an arrow rotating along the circle in the figure below. When the arrow rotates around the circle, it should rotate the direction of the arrow in real time to make its head match the circle line. For example, starting from the X axis, the situation after moving the angle of a is as follows:

After moving the angle a, the triangle should match the circular line, and the arrow should always follow the line direction. A = c, the tangent angle is as many degrees as it needs to be rotated.

Conclusion: if you want the moving point to rotate to coincide with the tangent, the rotation angle should be the same as the tangent angle.

public class GetPosTanView extends View {
    private Path mCirclePath, mDstPath;
    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private Float mCurAnimValue;
    private Bitmap mArrawBmp;
    public GetPosTanView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mArrawBmp = BitmapFactory.decodeResource(getResources(), R.drawable.arraw);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(4);
        mPaint.setColor(Color.BLACK);
        mDstPath = new Path();
        mCirclePath = new Path();
        mCirclePath.addCircle(100, 100, 50, Path.Direction.CW);
        mPathMeasure = new PathMeasure(mCirclePath, true);
        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mCurAnimValue = (Float) animation.getAnimatedValue();
            invalidate();
        }
    });
    animator.setDuration(2000);
    animator.start();
}

After loading the picture into memory, rotate the picture before painting it on the canvas each time you redraw it.

private float[] pos = new float[2];
private float[] tan = new float[2];
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.WHITE);
    float stopD = mPathMeasure.getLength() * mCurAnimValue;
    mDestPath.reset();
    mPathMeasure.getSegment(0, stopD, mDstPath, true);
    canvas.drawPath(mDstPath, mPaint);

    mPathMeasure.getPosTan(stopD, pos, tan);
    float degrees = (float) (Math.atan2 (tan[1], tan[0]) * 180.0 / Math.PI);
    Matrix matrix = new Matrix() ;
    matrix.postRotate(degrees, mArrawBmp.getwidth() / 2, mArrawBmp.getHeight() / 2);
    matrix.postTranslate(pos[0], Pos[1]);
    canvas.drawBitmap(mArrawBmp, matrix, mPaint);
}

It should be noted that:

(1) The pos and tan arrays must use the new keyword to allocate storage space when they are used PathMeasure.getPosTan The () function only assigns values to elements in an array. The getPosTan() function will not succeed if space is not allocated in advance.

(2) Through Math.atan2 The (Tan [1], Tan [0]) function gets the radian value instead of the angle value, so you need to use the( Math.atan2 (tan[1],tan[0])*180.0/ Math.PI )Converts the radian value to the angle value.

Use first matrix.postRotate The () function rotates the picture by a specified angle around the center point to coincide with the tangent.

And then use matrix.postTranslate The () function moves the picture from the default (0,0) to the front of the current path.

Finally, draw the picture on the canvas.

As can be seen from the rendering, although the arrow moves with the path path path, it is a little deviated.

    

When moving a picture, it starts from the top left corner of the picture, so after the original (0,0) point moves (pos[0],pos[1]) distance, the top left corner of the picture is at (pos[0],pos[1]). This shows that we have moved too much. We can move half the picture size less.

Change the code of moving picture, and move half picture size less.

Matrix matrix = new Matrix();
matrix.postRotate(degrees, mArrawBmp.getwidth() / 2, mArrawBmp.getHeight() / 2);
matrix.postTranslate(pos[0] - mArrawBmp.getwidth() / 2,pos[1] - mArrawBmp.getHeight() / 2);
canvas.drawBitmap(mArrawBmp, matrix, mPaint);

getMatrix() function:

This function is used to get the matrix of the position of a certain length on the path and the tangent value of the position.

boolean getMatrix(float distance, Matrix matrix, int flags)
● distance: the length from the starting point of Path
 ● matrix: the matrix packaged according to flags will store different contents according to the settings of flags
 ● flags: used to specify which content will be stored in the matrix. There are two values for flags:
         PathMeasure.POSITION_MATRIX_FLAG: to obtain location information;
         PathMeasure.TANGENT_MATRIX_FLAG: indicates to obtain trimming information, so that the picture rotates according to Path.
         You can specify only one, or you can use the "|" (or operator) to specify at the same time.

Obviously, the getmatrix () function is just PathMeasure.getPosTan() function. The getPosTan() function saves the obtained position and trimming information in the pos and tan arrays respectively, while the getMatrix() function directly saves it in the matrix array.

Next, use the getMatrix() function instead of the getPosTan() function to implement the arrow loading animation.

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // Draw a path loading animation, as in the path loading animation example, omitted here
    ...
    // Calculate azimuth
    Matrix matrix = new Matrix();
    mPathMeasure.getMatrix(stopD, matrix, PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG);
    matrix.preTranslate(-mArrawBmp.getWidth()/2, -mArrawBmp.getHeight()/2);
    canvas.drawBitmap(mArrawBmp, matrix, mPaint);
}

Here, getMatrix() function is used to save the location information and trimming information to matrix. Because the current location information has been saved in the matrix, we only need to move the picture by half the size of the picture, so use the matrix.preTranslate - mArrawBmp.getWidth ()/2, - mArrawBmp.getHeight () / 2); move half picture size. As for why we use the preTranslate() function to move here, and the above code uses the postTranslate() function to move, which will be described in the matrix chapter later.

Example: Alipay paid successfully animated

From this figure, we can see how to calculate the process of each point of the coupler according to the circular radius mRadius on the basis of the center x (mcentery).

private int mCenterX = mCenterY = 100;
private int mRadius = 50;
public AliPayView(Context context, AttributeSet attrs) {
    super(context, attrs);
    setLayerType(LAYER_TYPE_SOFTWARE, null);
    // mPaint initialization is the same as the path load animation example, omitted here
    ...
    mDstPath = new Path();
    mCirclePath = new Path();

    mCirclePath.addCircle(mCenterX, mCenterY, mRadius, Path.Direction.CW);

    mCirclePath.moveTo(mCenterX-mRadius/2, mCenterY);
    mCirclePath.lineTo(mCenterX, mCenterY+mRadius/2);
    mCirclePath.lineTo(mCenterX+mRadius/2, mCenterY-mRadius/3);

    mPathMeasure = new PathMeasure(mCirclePath, false);
    ValueAnimator animator = ValueAnimator.ofFloat(0, 2);
    animator.addUpdateListener(new AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mCurAnimValue = (Float) animation.getAnimatedValue();
            invalidate();
        }
    });
    animator.setDuration(4000);
    animator.start();
}

Compared with the "path loading animation" example, there are two main changes here: first, when constructing mCirclePath, in addition to adding a circular path, a pair of hook paths are also added; second, when constructing the ValueAnimator animation, ofFloat(0, 2) This is because we have two paths. The first path is drawn between 0 and 1, and the second path is drawn between 1 and 2.

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.WHITE);
    if (mCurAnimValue < 1) {
        float stopD = mPathMeasure.getLength() * mCurAnimValue;
        mPathMeasure.getSegment(0, stopD, mDstPath, true);
    } else if (mCurAnimValue == 1) {
        mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDstPath, true);
        mPathMeasure.nextContour();
    } else {
        float stopD = mPathMeasure.getLength() * (mCurAnimValue -1);
        mPathMeasure.getSegment(0, stopD, mDstPath, true);
    }
    canvas.drawPath(mDstPath, mPaint);
}

Pay attention to the details of the animation, which can be understood as a path with its head and tail. It first appears half a circle slowly, then the tail starts to move, until it slowly catches up with the head, and finally integrates into one.

When drawing, when mcuranimvalue < 1, draw the circle of the outer circle; when mCurAnimYalue=1, it means that the circle has been drawn. At this time, first use mPathMeasure.getSegment() the function will finish the circle and then call it. mPathMeasure.nextContour() function turns the path to the path of the couplers; when mcuranimvalue > 1, it means it is already on the path of the couplers.

If mCurAnimValue==1, add the following code:

mCount = 1;
mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDstPath, true);
    float len = mPathMeasure.getLength();
    while(mPathMeasure.nextContour()){
        mCount++;
    }
    Log.d("TAG", "len=" + len + ",count=" + mCount);
D/TAG: len=313.65173,2
D/TAG: len=83.37617,2

As can be seen from the log, there are only two paths:

By mathematical calculation: circumference = 2 * π * 50 = 314.1592, total length of check mark = √ ((25 + 50 / 3) ^ 2 + 25 ^ 2)) = 83.9466. There is a little deviation from the value printed by Log. This is as like as two peas.

❶ two paths are added to mCirclePath. Why are they two segment s?

Because mPathMeasure = new PathMeasure(mCirclePath, false); forceClosed = false, two independent paths.

❷ the above code must be called many times mPathMeasure.getSegment(0, stopD, mDstPath, true); why does nextContour() not take out the added fragment paths?

Path can be composed of multiple curves, but whether it is getLength(), getSegment(), or other functions, only the first segment will be calculated. nextContour() is the function used to jump to the next curve. therefore mPathMeasure.nextContour() the curve to jump to is independent of the curve added in mDstPath. There are two paths in mpathmeasure and several fragment paths in mDstPath.

For ease of understanding, many pure numbers are used in the examples in this section, which is not desirable. Because fixed numbers can't meet the changing needs, we only have two paths here. What if we want to add a cardioid Path to the hook? The code here has to be rewritten instead of just adding a cardioid Path. The key here is how to know how many paths there are. After knowing how many paths there are, you can enumerate and draw them one by one. Therefore, we can provide an interface to set the Path instance externally and specify the number of paths in the current Path instance; or, we can clone a Path instance and use the PathMeasure.nextContour() function, when the function returns false, it indicates the end of traversal. There are many solutions.

2, SVG animation

summary:

The full name of SVG is scalable vector graphics (scalable vector graphics). It can be seen that SVG is a vector graphics, and it is a vector graphics standard specially used for network. The loss map is composed of points. It is drawn by lines and curves through mathematical calculation. No matter how it is enlarged, there will be no mosaic phenomenon. Illustrator is a commonly used loss map drawing software.

What are the benefits of SVG compared to Bitmap

● SVG uses XML format to define graphics, which can be read and modified by many tools.

● SVG is stored by points, and drawn by computer according to point information, without distortion, without adapting multiple sets of icons according to resolution.

● the space occupied by SVG is significantly smaller than that of Bitmap. For example, a 500px × 500px image needs 20KB of space after being converted to SVG, while a PNG image needs 732KB of space.

● SVG can be converted into Path path, which can be combined with Path animation to form a richer animation.

Google added support for SVG graphics in Android 5.0. For models below 5.0, you can use the com.android.suppor:appcompat-v7:23.4.0 and above.

Svg has been widely used in HTML for a long time, such as the following SVG Code:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
    <rect x="25" y="25" height="200" fill="lime" stroke-width="4" stroke="pink" />
    <circle cx="125" cy="125" r="75" fill="orange" />
    <polyline points="50,150 50,200 200,200 200,100" stroke="red" stroke-width="4" fill="none" />
    <line x1="50" y1="50" x2="200" y2="200" stroke="blue" stroke-width="4" />
</svg>

vector label and image display:

<vector xmlna:android="http://schemas.android.com/apk/res/android"
    android:width="200dp"
    android:height="100dp"
    android:viewportWidth="100"
    android:viewportHeight="50">
    <path
        android:name="bar"
        android:pathDate="M50,25 L100,25"
        android:strokeWidth="2"
        android:strokeColor="@android:color/darker_gray"/>
</vector>

● width, height: indicates the specific size of the SVG graph.

● viewportWidth, viewportHeight: indicates the scale of SVG graph division. It refers to how many points are the width and high score of the canvas. The point coordinates in Path are based on the number of viewportWidth and viewportHeight, not dp value.

For example, divide the width of 200dp into 100 points and the height of 100dp into 50 points. In < Path > M means moveTo, L means lineTo. So, here we draw a line from point (50,25) to point (100,25). The coordinates here are based on the points specified by viewportWidth and viewportHeight, that is, a point has 2dp. Point 25 is the middle point on the height, point 50 is the middle point on the width, and point 100 is the end position of the width.

Obviously, the < vector > tag specifies the canvas size, while the < Path > tag specifies the path content.

1.path label

1) Common properties

•  android:name : declare a tag, similar to an ID, so that you can find the node when animating it.
•  android:pathData : description of SVG vector graph.
•  android:strokeWidth : the width of the brush.
•  android:fillColor : fill color.
•  android:fillAlpha : transparency of the fill color.
•  android:strokeColor : stroke color.
•  android:strokeWidth : stroke width.
•  android:strokeAlpha : stroke transparency.
•  android:strokeLineJoin : used to specify the corner shape of the polyline. The values are miter (sharp angle at the junction), round (arc at the junction), and bevel (straight line at the junction).
•  android:strokeLineCap : draw the shape (line cap) of the end point of the line. The values include but (wireless cap), round (round cap) and square (square cap).
•  android:strokeMiterLimit : sets the upper limit of the bevel. Note: when strokeLineJoin is set to miter, the two lines drawn are 
                            When segments intersect at acute angles, the resulting slope may be quite long. When the slope is too long, it becomes uncoordinated.
                            The strokeMiterLimit property sets an upper limit on the length of the slope. This property indicates the slope length
                            The default value of the ratio to the line length is 10, which means that the length of a slope should not exceed 10 times the line width.
                            If the slope reaches this length, it becomes an angle.
                            This property is not valid when strokeLineJoin is' round 'or' bevel '.

Among them, android:strokeLineJoin The effect of corresponds to setstrokejoin( Paint.Join  Join) function,
android:strokeLineCap The effect of corresponds to Paint.setStrokeCap ( Paint.Cap  Cap) function,
The effect of each value will be described in detail when the Paint class is explained later.

2) android:trimPathStart Attribute (trim:[tr ɪ m]: trim; cut; cut; cut; remove)

This attribute is used to specify where the path starts. The value is 0-1, which represents the percentage of the path start position. When the value is 0, it starts from the head; when the value is 1, the whole path is not visible.

To show the effect, the gray part represents the deleted path part, while the black part represents the displayed path.

3) android:trimPathEnd attribute

This attribute is used to specify where the path ends. The value is 0-1, which represents the percentage of the end position of the path. When the value is 0, it ends from the start position; when the value is 1, it ends normally.

4) android:trimPathOffset attribute

This attribute is used to specify the displacement position of the result path, with a value of 0-1. When the value is 0, no displacement is carried out; when the value is 1, the length of the whole path is displaced.

When trimPathOffset=1, the length of the whole path is shifted, and the intercepted path returns to its original position.

5) android:pathData attribute

In the < Path > tag, the display content of the SVG image is specified mainly through the pathData attribute.

● M = moveTo(M X,Y): move the brush to the specified coordinate position.
● L = lineto (LX, y): draw a straight line to the specified coordinate position.
● H = horizontal lintto(H X): draw a horizontal line to the specified X coordinate position.
● V = vertical lineto(V Y): draw a vertical line to the specified Y coordinate position.
● C = curveto(C X1,Y1,X2,Y2,ENDX,ENDY): Third Order Bezier curve.
● S = smooth curve to (S X2, Y2, endx, Endy): Third Order Bezier curve. Compared with the C instruction, the value transmitted here is less in X1 and Y1 coordinates, because the S instruction will take the end point of the previous instruction as the starting point of this instruction.
● Q = quadratic Belzier curve(Q X,Y,ENDX,ENDY): Second Order Bezier curve.
● t = smooth horizontal belzier curveto (t endx, end): the end point after mapping the previous path.
● A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y): arc.
● Z = closepath(): close path
The A command is used to draw an arc and allows the arc to be unclosed. The meaning of each parameter of instruction A is as follows:
● RX,RY: refers to the semi axis size of all ellipses.
● XROTATION: refers to the angle between the X axis of the ellipse and the horizontal clockwise direction, which can be like a horizontal ellipse rotating the XROTAION angle clockwise around the center point.
● FLAG1: there are only two values, 1 for large angle radian and 0 for small angle radian.
● FLAG2: there are only two values to determine the direction from the starting point to the end point. 1 indicates clockwise, and 0 indicates counterclockwise.
● X,Y: end coordinate
Note the following:
● coordinate axis (0,0) is the center, X axis is horizontal to the right, Y axis is vertical to the down.
● all instructions can be in upper and lower case. Upper case indicates absolute positioning, referring to the global coordinate system; lower case indicates relative positioning, referring to the parent container coordinate system.
● spaces between instructions and data can be omitted.
● only one instruction can be used for multiple occurrences of the same instruction.

2.group label

The path label is used to define a paintable path, while the group label is used to define a series of paths or group the path labels. In static image display, there is no substantive difference between using only one path tag or using a group of path tags, which is mainly used in animation. In the animation, we can specify each path to make a specific animation. Through the group label, we can divide the content originally implemented by a path path into multiple path to achieve. Each path can specify a specific animation, so that the effect display will be colorful.

The use of group label is very arbitrary. There can be one or more group labels and path labels under the vector label at the same time. For example, the usage shown in the following figure is allowed.

The group tag has the following common attributes:

●  android:name : the name of the group to associate with the animation.
●  android:rotation : Specifies the number of degrees of rotation for this group of images.
●  android:pivotX : defines the X reference point when scaling and rotating the group. This value is specified relative to the viewport value of the vector.
●  android:pivotY : defines the Y reference point for scaling and rotating the group. This value is specified relative to the viewport value of the vector.
●  android:scaleX : Specifies the X-axis scale size of the group.
●  android:scaleY : Specifies the scale size of the group's Y axis.
●  android:translateX : Specifies the distance the group is translated along the X axis.
●  android:translateY : Specifies the distance the group is translated along the Y axis.
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="200dp"
    android:height="100dp"
    android:viewportWidth="100"
    android:viewportHeight="50">
    <group
        android:rotation="90"
        android:pivotX="50"
        android:pivotY="25">
        <path
            android:name="bar"
            android:pathData="M50,23 L100,23"
            android:strokeWidth="2"
            android:strokeColor="@android:color/darker_gray"/>
    </group>
</vector>

In the above example, the horizontal path of path is rotated 90 ° around the center point of the screen:

3. Make SVG image

You can use Illustrator, or online SVG imaging tools: Method Draw , or download the SVG source file and edit it.

There are many icoont open source websites, such as Alibaba's loss Map Library in China: http://www.iconfont.cn

4. Introduce SVG image into Android

Svg image parsing is not supported in Android. We have to convert SVG image to vector tag description. There are two methods.

Method 1: online conversion

Drag SVG image directly to online conversion website Android SVG to VectorDrawable

Method 2: introduce through Android Studio

  

You can select local SVG file and the SVG file that comes with IDE to adjust the size and transparency.

Enable auto mirroring for RTL layout: China's habit is to mirror the flip icon horizontally when it appears from right to left.

5. Examples

1) Introduce compatible package

android {
    defaultConfig {
        vectorDrawables.useSupportLibrary = true
    }
}

2) Generate Vector image

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="200dp"
        android:height="100dp"
        android:viewportWidth="100"
        android:viewportHeight="50">
    <path
            android:name="bar"
            android:pathData="M50,25 L100,25"
            android:strokeWidth="2"
            android:strokeColor="@android:color/darker_gray"/>
</vector>

3) Use in ImageView, ImageButton

<ImageView
    android:id="@+id/iv"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:srcCompat="@drawable/svg_line"/>
ImageView iv = (ImageView) findViewById(R.id.iv);
iv.setImageResource(R.drawable.svg_line);

4) Use in Button, RadioButton

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/svg_line" android:state_pressed="true"/>
    <item android:drawable="@drawable/svg_line"/>
</selector>
<Button
    android:id="@+id/btn"
    android:layout_width="70dp"
    android:layout_height="70dp"
    android:background="@drawable/selector_svg_line"/>

However, it can't be run directly here because there is a flaw in the compatibility package. We need to put the following code in front of the Activity.

public class MainActivity extends AppCompatActivity {
    static {
        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
    }
}

Dynamic Vector:

The dynamic SVG effect realized by dynamic Vector is the essence of SVG image in Android application.

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="200dp"
        android:height="100dp"
        android:viewportWidth="100"
        android:viewportHeight="50">
    <path
            android:name="bar"
            android:pathData="M50,25 L100,25"
            android:strokeWidth="2"
            android:strokeColor="@android:color/darker_gray"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="trimPathStart"
    android:valueFrom="0"
    android:valueTo="1"
    android:duration="2000" />
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:drawable="@drawable/svg_line">
    <target
            android:name="bar"
            android:animation="@animator/anim_trim_start"/>
</animated-vector>

First, through the android:drawable Attribute to specify the Vector image; then associate the path with the animation through the < target > tag android:name The attribute is the name of the specified < Path > tag, which corresponds to the < Path > tag in the Vector file. The two must be the same, which represents which < Path > tag to animate. Finally, the android:animat The ion attribute specifies the animation corresponding to the < Path > tag. There can be many < target > tags in the < animated Vector > tag. Each target tag can associate a path with the Animator.

Finally, use in the code:

ImageView imgView = (ImageView) findViewById(R.id.iv);
AnimatedVectorDrawableCompat animatedVectorDrawableCompat = AnimatedVectorDrawableCompat.create(MainActivity.this, R.drawable.line_animated_vector);
imgView.setImageDrawable(animatedVectorDrawableCompat);
((Animatable) imgView.getDrawable()).start();

Example: enter search animation

1. Prepare SVG image (drawable/vector_search_bar.xml):

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="150dp"
    android:height="24dp"
    android:viewportWidth="150"
    android:viewportHeight="24">

    <!--Search for graphics-->
    <path
        android:name="search"
        android:pathData="M141,17 A9,9 0 1,1 142,16 L149,23"
        android:strokeWidth="2"
        android:strokeColor="@android:color/darker_gray"/>

    <!--Bottom horizontal line-->
    <path
        android:name="bar"
        android:trimPathStart="1"
        android:pathData="M0,23 L149,23"
        android:strokeWidth="2"
        android:strokeColor="@android:color/darker_gray"/>
</vector>

2. Prepare animation (animator/anim_bar_trim_start.xml):

For the bottom horizontal line, it gradually decreases from left to right, so it is the operation of the starting point position. (animator/anim_search_trim_start.xml )

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="trimPathStart"
    android:valueFrom="0"
    android:valueTo="1"
    android:valueType="floatType"
    android:duration="500" />

For the search graph, it is displayed from scratch, so it is the operation of the end position. (animator/anim_search_trim_end.xml )

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:propertyName="trimPathEnd"
    android:valueFrom="0"
    android:valueTo="1"
    android:valueType="floatType" />

Associate the SVG image with the animation through the < animated vector > tag. (drawable/animated_vector_search.xml )

<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/vector_search_bar" >
    <target
        android:animation="@animator/anim_search_trim_end"
        android:name="search"/>
    <target
        android:animation="@animator/anim_bar_trim_start"
        android:name="bar"/>
</animated-vector>

3. Layout and start animation

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="20dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <EditText
            android:id="@+id/edit"
            android:hint="Click input"
            android:layout_width="150dp"
            android:layout_height="24dp"
            android:background="@null"/>
    <ImageView
            android:id="@+id/anim_img"
            android:layout_width="150dp"
            android:layout_height="24dp"/>
</FrameLayout>
public class SearchEditActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.svg_edit_search_activity);
        final ImageView imageView = (ImageView) findViewById(R.id.anim_img);

        //Focus on ImageView
        imageView.setFocusable(true);
        imageView.setFocusableInTouchMode(true);
        imageView.requestFocus();
        imageView.requestFocusFromTouch();
        EditText  editText = (EditText)findViewById(R.id.edit);

        //Start animation when EditText gets focus
        editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (hasFocus) {
                    AnimatedVectorDrawableCompat animatedVectorDrawableCompat = AnimatedVectorDrawableCompat.create(
                    SearchEditActivity.this, R.drawable.animated_vecotr_search);
                    imageView.setImageDrawable(animatedVectorDrawableCompat);
                    ((Animatable) imageView.getDrawable()).start();
                }
           }
       });
    }
}

Because EditText will get the focus by default, we need to first focus on ImageView, and then when the user clicks EditText, EditText will get the focus, and the animation will start.

Here we generate and explain how to display SVG image and use SVG animation in Android 2.1 platform through compatibility package. Compared with native SVG support above 5.0, compatible packages are not supported for the following content.

● Path Morphing: path transformation animation cannot be used in the Android pre-l version.

● Path Interpolator: Path Interpolator. In Android pre-L version, only the interpolator of the system can be used, and the interpolator cannot be customized.

Tags: Android xml Attribute encoding

Posted on Thu, 11 Jun 2020 03:34:19 -0400 by steve m