先上圖:
**
點(diǎn)子來(lái)自于一次情人節(jié)的禮物思考轻猖,想著能不能不俗套的去送花發(fā)紅包之類的能扒,再加上妹子也是做技術(shù)的嫁蛇,所以就想著搞了一個(gè)這個(gè)坎弯。
**
**
這個(gè)效果的原理是基于PathView的,可是PathView并不能滿足我的需求桦锄,于是乎我就開始下手自己修改了扎附。
**
**
下面我會(huì)一邊分析PathView的實(shí)現(xiàn)過(guò)程,一邊描述我是如何修改的(GIF圖很多小心流量)结耀。如果你不想看的話項(xiàng)目地址在這
https://github.com/MartinBZDQSM/PathDraw
**
動(dòng)畫效果
如果你了解PathView的動(dòng)畫的話留夜,你就知道它的動(dòng)畫分為兩種情況
1.getPathAnimator 并行效果
2.getSequentialPathAnimator 順序效果
如果你想知道它的實(shí)現(xiàn)原理建議查看PathView當(dāng)中的兩個(gè)靜態(tài)內(nèi)部類AnimatorBuilder和AnimatorSetBuilder。
但是當(dāng)我使用AnimatorSetBuilder 進(jìn)行順序繪制的時(shí)候我發(fā)現(xiàn)效果其實(shí)并不好图甜,為什么不好哪里不好呢碍粥?看它的源碼:
/**
* 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;
}
看完以上代碼你就會(huì)知道PathView的作者計(jì)算出來(lái)的動(dòng)畫時(shí)間是你設(shè)置的平均時(shí)間,也就是說(shuō)不管我這條path的路徑到底有多長(zhǎng)具则,所有path的執(zhí)行時(shí)間都是一樣的即纲。那我畫一個(gè)點(diǎn)和畫一條直線的時(shí)間都是一樣的是不是有點(diǎn)扯具帮?所以我在這里增加了平均時(shí)間的計(jì)算博肋,根據(jù)計(jì)算path的長(zhǎng)度在總長(zhǎng)度中的占比,然后單個(gè)設(shè)置時(shí)間,進(jìn)行順序輪播,我也試過(guò)使用AnimatorSet單獨(dú)設(shè)置Animator的時(shí)間,但是好像并沒有效果,所以我用比較蠢點(diǎn)方法進(jìn)行了實(shí)現(xiàn)蜂厅,大致修改的代碼如下:
/**
* 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) {
}
}
畫筆動(dòng)態(tài)跟蹤
PathView中線條漸變是通過(guò)截取path當(dāng)中的片段做成的匪凡,看碼:
/**
* 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();
}
}
既然動(dòng)畫的原理是通過(guò)改變截取的長(zhǎng)度做到的,那么只要能獲取到截取長(zhǎng)度最后的那個(gè)點(diǎn)是不是就可以充當(dāng)軌跡了掘猿?所以這里只需要添加一個(gè)錨點(diǎn)病游,每當(dāng)截取長(zhǎng)度變化的時(shí)候,錨點(diǎn)也跟著改變,看代碼:
public void setLength(float length) {
path.reset();
measure.getSegment(0.0f, length, path, true);
measure.getPosTan(length, point, null);//跟蹤錨點(diǎn)
path.rLineTo(0.0f, 0.0f);
if (animationStepListener != null) {
animationStepListener.onAnimationStep();
}
}
筆尖移動(dòng)的原理稠通,需要提前計(jì)算好筆尖在畫筆圖片中的坐標(biāo)衬衬,然后對(duì)照著錨點(diǎn)進(jìn)行移動(dòng)就行了。
Tips:這里我的畫筆圖片還沒有針對(duì)畫布寬高進(jìn)行縮放改橘,所以在不同分辨率的情況下畫筆顯示的大小可能是不一致的滋尉。
我認(rèn)知的Fill
PathView中對(duì)于Path的Paint選的是Stroke屬性,而如果需要進(jìn)行填充,則需要所有的線條繪制完成之后才能進(jìn)行填充或者默認(rèn)填充》芍鳎看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);//直接進(jìn)行填充
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);//線條繪制完成之后 在進(jìn)行填充
mTempCanvas.restore();
applySolidColor(mTempBitmap);
canvas.drawBitmap(mTempBitmap,0,0,null);
}
}
其實(shí)這里選Stroke屬性還是Fill屬性都是看svg的情況而定,針對(duì)于我自己做的這個(gè)svg圖狮惜,我對(duì)比了三種屬性的不同效果,看圖:
看了上圖我們可以發(fā)現(xiàn),如果我們使用的svg不是由單線條組成的,會(huì)感覺特別怪異,而Fill和Fill And Stroke則顯示的較為舒服高诺。更貼近svg在瀏覽器顯示出來(lái)的效果。
那么問(wèn)題來(lái)了碾篡! 如果我們使用Fill 屬性或者Fill And Stroke屬性虱而,在線條繪制過(guò)程中會(huì)把所截取的Path的起點(diǎn)和重點(diǎn)連接起來(lái)形成一個(gè)閉合區(qū)域。我把這種情況叫做“繪制過(guò)度”(瞎取的)开泽,看圖:
為什么會(huì)導(dǎo)致這種情況看我畫的這張圖你就會(huì)明白了牡拇;
在path往回繪制的時(shí)候,paint并不知道接下來(lái)會(huì)如何填充穆律,所以就直接連接了迂回點(diǎn)和終點(diǎn)诅迷。
那么如何消除Fill屬性帶來(lái)的影響呢?剛開始我想了大致兩個(gè)思路并進(jìn)行了嘗試:
- 多保留一份Paths众旗,在繪制的時(shí)候Clip原path路徑罢杉。
- 多保留一份Paths,使用PorterDuffXfermode贡歧,當(dāng)繪制的時(shí)候顯示被繪制的path遮擋的部分滩租。
我先實(shí)現(xiàn)了思路1,看我如何實(shí)現(xiàn)的:
@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);
//需要備用一個(gè)完整的path路徑利朵,來(lái)修復(fù)pathPaint的Fill造成繪制過(guò)度
Path path = pathLayer.mDrawer.get(i);//這個(gè)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) {//過(guò)濾初始為0的點(diǎn)
canvas.drawBitmap(paintLayer, svgPath.point[0] - nibPointf.x, svgPath.point[1] - nibPointf.y, null);
}
}
}
看效果:
仔細(xì)看效果發(fā)現(xiàn)其實(shí)還是有問(wèn)題存在的律想,再線條迂回的地方會(huì)把遺漏;
為什么會(huì)導(dǎo)致這種情況,其實(shí)還是前面講到過(guò)的繪制過(guò)度绍弟。
于是我嘗試了下實(shí)現(xiàn)下思路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) {
//需要備用一個(gè)完整的path路徑技即,來(lái)修復(fù)pathPaint的Fill造成繪制過(guò)度
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);
}
效果如下:
關(guān)于為什么要使用PorterDuff.Mode.SRC_OUT,其實(shí)我是試出來(lái)的0.0樟遣,本以為這樣就完美了而叼,但是我發(fā)現(xiàn)當(dāng)仔細(xì)看發(fā)現(xiàn)顏色他么怎么變成黑色了(我用的是灰色)!1葵陵!然后我嘗試了使用一張Bitmap的Canvas來(lái)代替view的Canvas再渲染像素點(diǎn)的顏色的時(shí)候,發(fā)現(xiàn)效果又亂了U胺稹M迅荨!伤柄!真是奇怪绊困,為了研究原因我將 canvas.clipPath(path);去掉,發(fā)現(xiàn)了新大陸,看圖:
原來(lái)PorterDuff.Mode.SRC_OUT將非覆蓋面生成了矩形塊适刀,那么新思路就有了:
3.直接截取path的矩形塊:
if (isFill) {
//需要備用一個(gè)完整的path路徑秤朗,來(lái)修復(fù)pathPaint的Fill造成繪制過(guò)度
Path path = pathLayer.mDrawer.get(i);
canvas.clipPath(path);
svgPath.path.computeBounds(drawRect, true);
canvas.drawRect(drawRect, drawerPaint);
}
最終效果圖就和文章最開始的顯示效果一致了,哈哈 幾經(jīng)波折終于出現(xiàn)好效果啦!
如何制作svg
關(guān)于如何制作成這樣的svg ,你可以考慮看我的文章:《如何將圖片生成svg》蔗彤,使用的是Adobe Illustrator而不是GMIP2
最后川梅,如果你喜歡或者有何意見疯兼,不妨Star或者給我提Issuses哦!項(xiàng)目地址
]