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

先上圖:


nancy.gif

**
點(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ì)比了三種屬性的不同效果,看圖:

STROKE.png

看了上圖我們可以發(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ò)度”(瞎取的)开泽,看圖:

Paste_Image.png

為什么會(huì)導(dǎo)致這種情況看我畫的這張圖你就會(huì)明白了牡拇;

Paste_Image.png

在path往回繪制的時(shí)候,paint并不知道接下來(lái)會(huì)如何填充穆律,所以就直接連接了迂回點(diǎn)和終點(diǎn)诅迷。

那么如何消除Fill屬性帶來(lái)的影響呢?剛開始我想了大致兩個(gè)思路并進(jìn)行了嘗試:

  1. 多保留一份Paths众旗,在繪制的時(shí)候Clip原path路徑罢杉。
  2. 多保留一份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);
            }
        }
    }

看效果:

nancy.gif

仔細(xì)看效果發(fā)現(xiàn)其實(shí)還是有問(wèn)題存在的律想,再線條迂回的地方會(huì)把遺漏;

Paste_Image.png

為什么會(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);
    }

效果如下:

nancy2.gif

關(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)了新大陸,看圖:

noclip.gif

原來(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)目地址

帥.gif

]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末贫途,一起剝皮案震驚了整個(gè)濱河市吧彪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌丢早,老刑警劉巖姨裸,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異怨酝,居然都是意外死亡傀缩,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門农猬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)赡艰,“玉大人,你說(shuō)我怎么就攤上這事斤葱】犊澹” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵揍堕,是天一觀的道長(zhǎng)料身。 經(jīng)常有香客問(wèn)我,道長(zhǎng)衩茸,這世上最難降的妖魔是什么芹血? 我笑而不...
    開封第一講書人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮楞慈,結(jié)果婚禮上幔烛,老公的妹妹穿的比我還像新娘。我一直安慰自己抖部,他們只是感情好说贝,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著慎颗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪言询。 梳的紋絲不亂的頭發(fā)上俯萎,一...
    開封第一講書人閱讀 51,554評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音运杭,去河邊找鬼夫啊。 笑死,一個(gè)胖子當(dāng)著我的面吹牛辆憔,可吹牛的內(nèi)容都是我干的撇眯。 我是一名探鬼主播报嵌,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼熊榛!你這毒婦竟也來(lái)了锚国?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤玄坦,失蹤者是張志新(化名)和其女友劉穎血筑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體煎楣,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡豺总,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了择懂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片喻喳。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖困曙,靈堂內(nèi)的尸體忽然破棺而出沸枯,到底是詐尸還是另有隱情,我是刑警寧澤赂弓,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布绑榴,位于F島的核電站,受9級(jí)特大地震影響盈魁,放射性物質(zhì)發(fā)生泄漏翔怎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一杨耙、第九天 我趴在偏房一處隱蔽的房頂上張望赤套。 院中可真熱鬧,春花似錦珊膜、人聲如沸容握。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)剔氏。三九已至,卻和暖如春竹祷,著一層夾襖步出監(jiān)牢的瞬間谈跛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工塑陵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留感憾,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓令花,卻偏偏與公主長(zhǎng)得像阻桅,于是被迫代替她去往敵國(guó)和親凉倚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容