先上圖:
**
點子來自于一次情人節的禮物思考,想著能不能不俗套的去送花發紅包之類的,再加上妹子也是做技術的,所以就想著搞了一個這個。
**
**
這個效果的原理是基于PathView的,可是PathView并不能滿足我的需求,于是乎我就開始下手自己修改了。
**
**
下面我會一邊分析PathView的實現過程,一邊描述我是如何修改的(GIF圖很多小心流量)。如果你不想看的話項目地址在這
https://github.com/MartinBZDQSM/PathDraw
**
動畫效果
如果你了解PathView的動畫的話,你就知道它的動畫分為兩種情況
1.getPathAnimator 并行效果
2.getSequentialPathAnimator 順序效果
如果你想知道它的實現原理建議查看PathView當中的兩個靜態內部類AnimatorBuilder和AnimatorSetBuilder。
但是當我使用AnimatorSetBuilder 進行順序繪制的時候我發現效果其實并不好,為什么不好哪里不好呢?看它的源碼:
/**
* 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;
}
看完以上代碼你就會知道PathView的作者計算出來的動畫時間是你設置的平均時間,也就是說不管我這條path的路徑到底有多長,所有path的執行時間都是一樣的。那我畫一個點和畫一條直線的時間都是一樣的是不是有點扯?所以我在這里增加了平均時間的計算,根據計算path的長度在總長度中的占比,然后單個設置時間,進行順序輪播,我也試過使用AnimatorSet單獨設置Animator的時間,但是好像并沒有效果,所以我用比較蠢點方法進行了實現,大致修改的代碼如下:
/**
* 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) {
}
}
畫筆動態跟蹤
PathView中線條漸變是通過截取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();
}
}
既然動畫的原理是通過改變截取的長度做到的,那么只要能獲取到截取長度最后的那個點是不是就可以充當軌跡了?所以這里只需要添加一個錨點,每當截取長度變化的時候,錨點也跟著改變,看代碼:
public void setLength(float length) {
path.reset();
measure.getSegment(0.0f, length, path, true);
measure.getPosTan(length, point, null);//跟蹤錨點
path.rLineTo(0.0f, 0.0f);
if (animationStepListener != null) {
animationStepListener.onAnimationStep();
}
}
筆尖移動的原理,需要提前計算好筆尖在畫筆圖片中的坐標,然后對照著錨點進行移動就行了。
Tips:這里我的畫筆圖片還沒有針對畫布寬高進行縮放,所以在不同分辨率的情況下畫筆顯示的大小可能是不一致的。
我認知的Fill
PathView中對于Path的Paint選的是Stroke屬性,而如果需要進行填充,則需要所有的線條繪制完成之后才能進行填充或者默認填充。看PathView的源碼:
@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);//直接進行填充
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);//線條繪制完成之后 在進行填充
mTempCanvas.restore();
applySolidColor(mTempBitmap);
canvas.drawBitmap(mTempBitmap,0,0,null);
}
}
其實這里選Stroke屬性還是Fill屬性都是看svg的情況而定,針對于我自己做的這個svg圖,我對比了三種屬性的不同效果,看圖:
看了上圖我們可以發現,如果我們使用的svg不是由單線條組成的,會感覺特別怪異,而Fill和Fill And Stroke則顯示的較為舒服。更貼近svg在瀏覽器顯示出來的效果。
那么問題來了! 如果我們使用Fill 屬性或者Fill And Stroke屬性,在線條繪制過程中會把所截取的Path的起點和重點連接起來形成一個閉合區域。我把這種情況叫做“繪制過度”(瞎取的),看圖:
為什么會導致這種情況看我畫的這張圖你就會明白了;
在path往回繪制的時候,paint并不知道接下來會如何填充,所以就直接連接了迂回點和終點。
那么如何消除Fill屬性帶來的影響呢?剛開始我想了大致兩個思路并進行了嘗試:
- 多保留一份Paths,在繪制的時候Clip原path路徑。
- 多保留一份Paths,使用PorterDuffXfermode,當繪制的時候顯示被繪制的path遮擋的部分。
我先實現了思路1,看我如何實現的:
@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);
//需要備用一個完整的path路徑,來修復pathPaint的Fill造成繪制過度
Path path = pathLayer.mDrawer.get(i);//這個pathLayer 指的就是Pathview中的SvgUtils
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) {//過濾初始為0的點
canvas.drawBitmap(paintLayer, svgPath.point[0] - nibPointf.x, svgPath.point[1] - nibPointf.y, null);
}
}
}
看效果:
仔細看效果發現其實還是有問題存在的,再線條迂回的地方會把遺漏;
為什么會導致這種情況,其實還是前面講到過的繪制過度。
于是我嘗試了下實現下思路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) {
//需要備用一個完整的path路徑,來修復pathPaint的Fill造成繪制過度
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);
}
效果如下:
關于為什么要使用PorterDuff.Mode.SRC_OUT,其實我是試出來的0.0,本以為這樣就完美了,但是我發現當仔細看發現顏色他么怎么變成黑色了(我用的是灰色)!!!然后我嘗試了使用一張Bitmap的Canvas來代替view的Canvas再渲染像素點的顏色的時候,發現效果又亂了!!!!真是奇怪,為了研究原因我將 canvas.clipPath(path);去掉,發現了新大陸,看圖:
原來PorterDuff.Mode.SRC_OUT將非覆蓋面生成了矩形塊,那么新思路就有了:
3.直接截取path的矩形塊:
if (isFill) {
//需要備用一個完整的path路徑,來修復pathPaint的Fill造成繪制過度
Path path = pathLayer.mDrawer.get(i);
canvas.clipPath(path);
svgPath.path.computeBounds(drawRect, true);
canvas.drawRect(drawRect, drawerPaint);
}
最終效果圖就和文章最開始的顯示效果一致了,哈哈 幾經波折終于出現好效果啦!
如何制作svg
關于如何制作成這樣的svg ,你可以考慮看我的文章:《如何將圖片生成svg》,使用的是Adobe Illustrator而不是GMIP2
最后,如果你喜歡或者有何意見,不妨Star或者給我提Issuses哦!項目地址
]