First of all:

The idea comes from thinking about a gift for Valentine's Day, thinking about whether you can send flowers and red envelopes in a vulgar way, plus that your sister is also skilled, so you want to do this.
The principle of this effect is based on PathView But PathView I couldn't meet my needs, so I started to revise it myself.
Now I'll analyze it at the same time. PathView The implementation process, while describing how I modified (GIF diagram is a lot of careful traffic). The project address is here if you don't want to see it.
https://github.com/MartinBZDQSM/PathDraw
Animation effect
If you understand PathView In animation, you know that there are two kinds of animation.
1. getPath Animator Parallel Effect
2. The sequential effect of getSequential Path Animator
If you want to know how it works, check it out PathView Two static inner classes, AnimatorBuilder and AnimarSetBuilder, are included.
But when I use Animator Set Builder for sequential rendering, I find that the effect is not good. Why not? Look at its source code:
/**
* Sets the duration of the animation. Since the AnimatorSet sets the duration for each
* Animator, we have to divide it by the number of paths.
*
* @param duration - The duration of the animation.
* @return AnimatorSetBuilder.
*/
public AnimatorSetBuilder duration(final int duration) {
this.duration = duration / paths.size();
return this;
}
After reading the above code, you will know PathView The animation time calculated by the author is the average time you set. That is to say, no matter how long my path is, the execution time of all paths is the same. So I draw a point and draw a straight line at the same time, is it a bit ridiculous? So I added the calculation of average time here. According to the proportion of the length of the path in the total length, I set the time individually, and I tried to set the time of the Animator individually by using the Animator Set, but it didn't seem to work. So I implemented it with a more foolish method. The code is roughly revised as follows:
/**
* Default constructor.
*
* @param pathView The view that must be animated.
*/
public AnimatorSetBuilder(final PathDrawingView pathView) {
paths = pathView.mPaths;
if (pathViewAnimatorListener == null) {
pathViewAnimatorListener = new PathViewAnimatorListener();
}
for (PathLayer.SvgPath path : paths) {
path.setAnimationStepListener(pathView);
ObjectAnimator animation = ObjectAnimator.ofFloat(path, "length", 0.0f, path.getLength());
totalLenth = totalLenth + path.getLength();
animators.add(animation);
}
for (int i = 0; i < paths.size(); i++) {
long animationDuration = (long) (paths.get(i).getLength() * duration / totalLenth);
Animator animator = animators.get(i);
animator.setStartDelay(delay);
animator.setDuration(animationDuration);
animator.addListener(pathViewAnimatorListener);
}
}
/**
* Starts the animation.
*/
public void start() {
resetAllPaths();
for (Animator animator : animators) {
animator.cancel();
}
index = 0;
startAnimatorByIndex();
}
public void startAnimatorByIndex() {
if (index >= paths.size()) {
return;
}
Animator animator = animators.get(index);
animator.start();
}
/**
* Sets the length of all the paths to 0.
*/
private void resetAllPaths() {
for (PathLayer.SvgPath path : paths) {
path.setLength(0);
}
}
/**
* Called when the animation start.
*/
public interface ListenerStart {
/**
* Called when the path animation start.
*/
void onAnimationStart();
}
/**
* Called when the animation end.
*/
public interface ListenerEnd {
/**
* Called when the path animation end.
*/
void onAnimationEnd();
}
/**
* Animation listener to be able to provide callbacks for the caller.
*/
private class PathViewAnimatorListener implements Animator.AnimatorListener {
@Override
public void onAnimationStart(Animator animation) {
if (index < paths.size() - 1) {
paths.get(index).isMeasure = true;
PathDrawingView.isDrawing = true;
if (index == 0 && listenerStart != null)
listenerStart.onAnimationStart();
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (index >= paths.size() - 1) {
PathDrawingView.isDrawing = false;
if (animationEnd != null)
animationEnd.onAnimationEnd();
} else {
if (index < paths.size() - 1) {
paths.get(index).isMeasure = false;
index++;
startAnimatorByIndex();
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
}
Brush Dynamic Tracking
PathView The midline gradient is made by intercepting the segments in the path.
/**
* Sets the length of the path.
*
* @param length The length to be set.
*/
public void setLength(float length) {
path.reset();
measure.getSegment(0.0f, length, path, true);
path.rLineTo(0.0f, 0.0f);
if (animationStepListener != null) {
animationStepListener.onAnimationStep();
}
}
Since the principle of animation is achieved by changing the length of interception, can the last point of the interception length be obtained as a trajectory? So we just need to add an anchor point here. Every time the interception length changes, the anchor point changes. Look at the code:
public void setLength(float length) {
path.reset();
measure.getSegment(0.0f, length, path, true);
measure.getPosTan(length, point, null);//Tracking anchor point
path.rLineTo(0.0f, 0.0f);
if (animationStepListener != null) {
animationStepListener.onAnimationStep();
}
}
The principle of pen point movement requires that the coordinates of the pen point in the brush picture be calculated in advance and then moved against the anchor point.
Tips: Here, my brush image has not been zoomed in to the width and height of the canvas, so the size of the brush display may be inconsistent at different resolutions.
Fill I Cognize
PathView The Stroke attribute is selected for Path's Paint, and if filling is required, all lines need to be drawn before filling or default filling. see PathView Source code:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mTempBitmap==null || (mTempBitmap.getWidth()!=canvas.getWidth()||mTempBitmap.getHeight()!=canvas.getHeight()) )
{
mTempBitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);
mTempCanvas = new Canvas(mTempBitmap);
}
mTempBitmap.eraseColor(0);
synchronized (mSvgLock) {
mTempCanvas.save();
mTempCanvas.translate(getPaddingLeft(), getPaddingTop());
fill(mTempCanvas);//Fill directly
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;
mTempCanvas.drawPath(path, paint1);
}
fillAfter(mTempCanvas);//Fill in after line drawing is completed
mTempCanvas.restore();
applySolidColor(mTempBitmap);
canvas.drawBitmap(mTempBitmap,0,0,null);
}
}
In fact, the choice of Stroke or Fill attributes depends on the situation of svg. For my own SVG graph, I compared the different effects of three attributes, see the graph:

Looking at the figure above, we can see that if we use svg which is not made up of single lines, it will feel very strange, while Fill and Fill And Stroke show more comfortable. Closer to the effect of svg in the browser.
So here comes the question...! If we use Fill attributes or Fill And Stroke attributes, the starting point and focus of the intercepted Path will be connected to form a closed area in the process of drawing a line. I call this situation "over drawing" (blind), see the picture:

Why does this happen? Look at the picture I've drawn and you'll see.

When the path is drawn back and forth, paint does not know how to fill it up next, so it directly connects the roundabout point to the endpoint.
So how to eliminate the impact of Fill attributes? At first, I thought about two ideas and tried them:
- Keep one more Paths and Clip the original path path when drawing.
- Keep one more Paths, and use PorterDuffXfermode to display the occluded part of the path being drawn when drawing.
First, I realized train of thought 1 to see how I realized it.
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int sc = canvas.save(Canvas.ALL_SAVE_FLAG);
synchronized (mSvgLock) {
int count = mPaths.size();
for (int i = 0; i < count; i++) {
int pc = canvas.save(Canvas.ALL_SAVE_FLAG);
//A complete path path path needs to be backed up to repair the Fill of pathPaint causing overdrawing
Path path = pathLayer.mDrawer.get(i);//This pathLayer refers to SvgUtils in Pathview
canvas.clipPath(path);
PathLayer.SvgPath svgPath = mPaths.get(i);
canvas.drawPath(svgPath.path, pathPaint);
canvas.restoreToCount(pc);
}
}
canvas.restoreToCount(sc);
for (PathLayer.SvgPath svgPath : mPaths) {
if (isDrawing && svgPath.isMeasure) {//Points with an initial filter of 0
canvas.drawBitmap(paintLayer, svgPath.point[0] - nibPointf.x, svgPath.point[1] - nibPointf.y, null);
}
}
}
Look at the effect:

Looking at the effect carefully, we find that there are still problems, and then the roundabout places will be omitted.

Why does this happen? In fact, it's still over-rendering as mentioned earlier.
So I tried to achieve the next train of thought 2:
private PorterDuffXfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT);
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int sc = canvas.save(Canvas.ALL_SAVE_FLAG);
synchronized (mSvgLock) {
int count = mPaths.size();
for (int i = 0; i < count; i++) {
int pc = canvas.save(Canvas.ALL_SAVE_FLAG);
PathLayer.SvgPath svgPath = mPaths.get(i);
if (isFill) {
//A complete path path path needs to be backed up to repair the Fill of pathPaint causing overdrawing
Path path = pathLayer.mDrawer.get(i);
canvas.clipPath(path);
if (isDrawing && svgPath.isMeasure) {
canvas.drawPath(path, drawerPaint);
}
}
canvas.drawPath(svgPath.path, pathPaint);
canvas.restoreToCount(pc);
}
}
canvas.restoreToCount(sc);
}
The results are as follows:

As for why I use PorterDuff.Mode.SRC_OUT, I actually tried 0.0, which was supposed to be perfect, but I found out how the color turned black (I used grey)!!! Then I tried to use a Bitmap Canvas instead of the view Canvas to render the color of the pixels, and found that the effect was messy again!!! Strangely enough, for research reasons, I removed canvas.clipPath(path); and discovered the New World. Look at the picture:

Originally PorterDuff.Mode.SRC_OUT generated rectangular blocks from non-coverage, so the new idea is:
3. Cut the rectangular block of path directly:
if (isFill) {
//A complete path path path needs to be backed up to repair the Fill of pathPaint causing overdrawing
Path path = pathLayer.mDrawer.get(i);
canvas.clipPath(path);
svgPath.path.computeBounds(drawRect, true);
canvas.drawRect(drawRect, drawerPaint);
}
The final effect picture is consistent with the initial display effect of the article. Haha finally has a good effect after several twists and turns.
How to make svg
As for how to make such svg, you may consider reading my article: How to Generate Pictures into svg Using Adobe Illustrator instead of GMIP2
Finally, if you like it or have any comments, Star or give me Issuses! Project address
<!--more-->