Android教你如何用程序“手繪”女友

先上圖:


nancy.gif


點子來自於一次情人節的禮物思考,想着能不能不俗套的去送花發紅包之類的,再加上妹子也是做技術的,所以就想着搞了一個這個。 


這個效果的原理是基於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圖,我對比了三種屬性的不同效果,看圖:


STROKE.png


看了上圖我們可以發現,如果我們使用的svg不是由單線條組成的,會感覺特別怪異,而Fill和Fill And Stroke則顯示的較爲舒服。更貼近svg在瀏覽器顯示出來的效果。
那麼問題來了! 如果我們使用Fill 屬性或者Fill And Stroke屬性,在線條繪製過程中會把所截取的Path的起點和重點連接起來形成一個閉合區域。我把這種情況叫做“繪製過度”(瞎取的),看圖:


Paste_Image.png


爲什麼會導致這種情況看我畫的這張圖你就會明白了;


Paste_Image.png


在path往回繪製的時候,paint並不知道接下來會如何填充,所以就直接連接了迂迴點和終點。

那麼如何消除Fill屬性帶來的影響呢?剛開始我想了大致兩個思路並進行了嘗試:

  1. 多保留一份Paths,在繪製的時候Clip原path路徑。
  2. 多保留一份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);
            }
        }
    }

看效果:


nancy.gif


仔細看效果發現其實還是有問題存在的,再線條迂迴的地方會把遺漏;


Paste_Image.png


爲什麼會導致這種情況,其實還是前面講到過的繪製過度。
於是我嘗試了下實現下思路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);
    }

效果如下:


nancy2.gif


關於爲什麼要使用PorterDuff.Mode.SRC_OUT,其實我是試出來的0.0,本以爲這樣就完美了,但是我發現當仔細看發現顏色他麼怎麼變成黑色了(我用的是灰色)!!!然後我嘗試了使用一張Bitmap的Canvas來代替view的Canvas再渲染像素點的顏色的時候,發現效果又亂了!!!!真是奇怪,爲了研究原因我將 canvas.clipPath(path);去掉,發現了新大陸,看圖:


noclip.gif


原來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哦!項目地址

<!--more-->

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章