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

帥.gif

]

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內容