android 對繪制的文本添加動畫

場景:

  • 存在較多繪制內(nèi)容的區(qū)域需要某些動畫效果怠苔,
  • 需要盡量少修改視圖的繪制方法仪糖,做到動畫與繪制分離。

看個簡單例子:

image

我在一個視圖上繪制了一行文字锅劝,先看一下繪制部分的代碼:

public class MyLayout extends LinearLayout
{
    private String mText = "show me the money";

    //……

    @Override
    protected void dispatchDraw(Canvas canvas)
    {
        super.dispatchDraw(canvas);

        doPaint(canvas, null);
    }

    public void doPaint(Canvas canvas, TextAnimationController.AnimationView aniView)
    {
        Paint paint = new Paint();
        paint.setColor(Color.RED);
        paint.setTextSize(80);
        paint.setStyle(Paint.Style.FILL);

        //切分字符串
        String[] words = mText.split("\\s+"); 

        //起始位置
        int x_start = 100;
        int y_start = 400;

        for (String subText : words)
        {
            //繪制文本
            canvas.drawText(subText, x_start, y_start, paint);

            float width = paint.measureText(subText);
            float height = paint.descent() - paint.ascent();
            x_start += width;

            //繪制空格
            String blank = " ";
            canvas.drawText(blank, x_start, y_start, paint);
            x_start += paint.measureText(blank);
        }
    }
}

現(xiàn)在需要做刪除最后一個單詞的動作并為之添加動畫故爵。

一個實現(xiàn)方法:

將屬性動畫結(jié)合到自己的視圖(View)中玻粪,通過數(shù)值發(fā)生器(ValueAnimator)和估值器(TypeEvaluator)不斷產(chǎn)生出的位置信息诬垂,修改單詞繪制時的位置和大小,進行重新繪制结窘。

這種做法會產(chǎn)生兩個問題:

  1. 動畫和繪制結(jié)合得太緊密,繪制的邏輯和動畫的邏輯攪和在一起隧枫,導(dǎo)致代碼復(fù)雜且混亂
  2. 頻繁的直接繪制會導(dǎo)致屏幕閃爍谓苟。(當(dāng)然协怒,這個可以用雙緩沖解決)

解決思路:

在執(zhí)行動畫時,可以在視圖上再蓋一層PopupWindow斤讥。強制調(diào)用原視圖的繪制方法,讓其在PopupWindow視圖的canvas上重新繪制一遍文本芭商,來參與動畫的顯示(雙緩沖):

image

對于動畫的繪制思路可參考下圖來理解:

image

說明:我們先把最終的狀態(tài)的Bitmap對象繪制到動畫視圖(PopupWindow的視圖)上搀缠,然后再把原始狀態(tài)Bitmap上的動畫字符區(qū)域經(jīng)過縮放繪制到動畫數(shù)值發(fā)生器給出的具體位置上

為了實現(xiàn)動畫,我們需要一些數(shù)據(jù)的支持:

  1. 動畫文本在默認(rèn)繪制時的坐標(biāo)位置及寬(原始位置)
  2. 通過數(shù)值發(fā)生器(ValueAnimator)和估值器(TypeEvaluator)不斷產(chǎn)生出的動畫文本的位置信息

首先艺普,我們需要修改原始視圖的繪制方法,添加文本位置的搜集器:

public void doPaint(Canvas canvas, TextAnimationController.AnimationView aniView)
{
    ……

    for (String subText : words)
    {
        //繪制文本
        canvas.drawText(subText, x_start, y_start, paint);

        float width = paint.measureText(subText);
        float height = paint.descent() - paint.ascent();
        //==============================================================================================
        if (aniView != null)
        {
            TextAnimationController.TextObject textObj = new TextAnimationController.TextObject();
            textObj.rcRangeSrc = new RectF(x_start, y_start + paint.descent() - height, x_start + width,
                    y_start + paint.descent());
            aniView.addTextObject(textObj);
        }
        //==============================================================================================
        x_start += width;


        //繪制空格
        String blank = " ";
        canvas.drawText(blank, x_start, y_start, paint);
        x_start += paint.measureText(blank);
    }
}

為了方便控制岸浑,我們創(chuàng)建一個動畫控制器類:TextAnimationController瑰步,所有的數(shù)據(jù)導(dǎo)入矢洲、動畫生成缩焦、彈層、控制都在這個類里袁滥。

public class TextAnimationController
{
    private MyLayout mOriginalView = null; //PopupWindow的視圖
    
    private AnimationView mAnimationView = null;
    private AnimationWindow mAnimationWindow = null;

    public TextAnimationController(MyLayout view)
    {
        mOriginalView = view;
    }

    //開始動畫
    public void startAnimation(Point target)
    {
        //創(chuàng)建動畫視圖層
        mAnimationView = new AnimationView(mOriginalView.getContext(), target);
        mAnimationWindow = new AnimationWindow(mAnimationView, mOriginalView.getWidth(), mOriginalView.getHeight());
        mAnimationWindow.setFocusable(true);

        //加載動畫視圖層
        int[] location = new int[2];
        mOriginalView.getLocationOnScreen(location);
        mAnimationWindow.showAtLocation(mOriginalView, Gravity.TOP | Gravity.LEFT, location[0], location[1]);
    }

    public void endAnimation()
    {
        //關(guān)閉動畫層
        if (mAnimationWindow != null && mAnimationWindow.isShowing())
        {
            mAnimationWindow.dismiss();
            mAnimationWindow = null;
        }
    }


    static class TextObject
    {
        //原始尺寸
        RectF rcRangeSrc;
        //目標(biāo)尺寸
        RectF rcRangeDes;
        //中間步驟尺寸
        RectF rcRangeStep;
    }

    //PopupWindow彈層
    public class AnimationWindow extends PopupWindow
    {
        public AnimationWindow(View contentView, int width, int height)
        {
            super(contentView, width, height);
            
            setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
        }
    }
    
    //動畫視圖(PopupWindow的視圖)
    class AnimationView extends View
    {
        private LinkedList<TextObject> objectList = new LinkedList<>();
        private Paint paint = new Paint();
        private Rect rectSrc = new Rect();

        private Bitmap bitmapSrc = Bitmap
                .createBitmap(mOriginalView.getWidth(), mOriginalView.getHeight(), Bitmap.Config.ARGB_8888);
        private Bitmap bitmapFinal = Bitmap
                .createBitmap(mOriginalView.getWidth(), mOriginalView.getHeight(), Bitmap.Config.ARGB_8888);

        public AnimationView(Context context, final Point target)
        {
            super(context);

            Canvas bmpCanvas = new Canvas(bitmapSrc);
            mOriginalView.showStartText();
            mOriginalView.doPaint(bmpCanvas, this);
            bmpCanvas = new Canvas(bitmapFinal);
            mOriginalView.showEndText();
            mOriginalView.doPaint(bmpCanvas, null);

            post(new Runnable()
            {
                @Override
                public void run()
                {
                    startAnimation(target.x, target.y);
                }
            });
        }

        public void addTextObject(TextObject object)
        {
            objectList.add(object);
        }

        @Override
        protected void onDraw(Canvas canvas)
        {
            super.onDraw(canvas);

            //繪制底圖
            canvas.drawBitmap(bitmapFinal, 0, 0, paint);

            //繪制動畫文本圖
            int lastIndex = objectList.size() - 1;
            RectF rcSrc = objectList.get(lastIndex).rcRangeSrc;
            rectSrc.set((int) rcSrc.left, (int) rcSrc.top, (int) rcSrc.right, (int) rcSrc.bottom);

            RectF rcStep = objectList.get(lastIndex).rcRangeStep;
            if (rcStep != null)
            {
                canvas.drawBitmap(bitmapSrc, rectSrc, rcStep, paint);
            }

        }

        private void startAnimation(int desX, int desY)
        {
            for (int i = 0; i < objectList.size(); i++)
            {
                RectF src = objectList.get(i).rcRangeSrc;
                objectList.get(i).rcRangeDes = new RectF(desX, desY, desX + src.width(), desY + src.height());
                objectList.get(i).rcRangeStep = new RectF(src);
            }

            //==========================================================================================================

            ArrayList<Animator> animators = new ArrayList<>();

            for (int i = 0; i < objectList.size(); i++)
            {
                final RectF src = objectList.get(i).rcRangeSrc;
                RectF des = objectList.get(i).rcRangeDes;

                AnimatorSet aniDisappear = new AnimatorSet();

                final int index = i;

                //貝塞爾曲線動畫
                final int pointX = (int) (src.left + 200);
                final int pointY = (int) (src.top + (des.top - src.top) / 2);
                Point controlPoint = new Point(pointX, pointY); //控制點
                BezierEvaluator bezierEvaluator = new BezierEvaluator(controlPoint);

                ValueAnimator animBezier = ValueAnimator.ofObject(bezierEvaluator,
                        new Point((int)src.left, (int)src.top),
                        new Point((int)des.left, (int)des.top));
                animBezier.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
                {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation)
                    {
                        Point point = (Point) animation.getAnimatedValue();

                        RectF step = new RectF(objectList.get(index).rcRangeStep);
                        step.set(point.x, point.y, point.x + src.width(), point.y + src.height());
                        objectList.get(index).rcRangeStep.set(step);

                        invalidate();
                    }
                });

                //縮放動畫
                ValueAnimator animScale = ValueAnimator.ofFloat(1.0f, 0.1f);
                animScale.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
                {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation)
                    {
                        float scaleSize = (Float) animation.getAnimatedValue();

                        RectF step = new RectF(objectList.get(index).rcRangeStep);
                        step.set(step.left, step.top, step.left + step.width() * scaleSize,
                                step.top + step.height() * scaleSize);
                        objectList.get(index).rcRangeStep.set(step);
                    }
                });


                aniDisappear.setDuration(1200);
                aniDisappear.setInterpolator(new AccelerateInterpolator());
                aniDisappear.play(animBezier).with(animScale);

                animators.add(aniDisappear);
            }

            AnimatorSet animatorSet = new AnimatorSet();
            animatorSet.addListener(new Animator.AnimatorListener()
            {
                @Override
                public void onAnimationStart(Animator animation)
                {

                }

                @Override
                public void onAnimationEnd(Animator animation)
                {
                    endAnimation();
                }

                @Override
                public void onAnimationCancel(Animator animation)
                {
                    endAnimation();
                }

                @Override
                public void onAnimationRepeat(Animator animation)
                {

                }
            });
            animatorSet.playTogether(animators);
            animatorSet.start();
        }
    }

    public class BezierEvaluator implements TypeEvaluator<Point>
    {
        private Point controlPoint;

        public BezierEvaluator(Point controlPoint)
        {
            this.controlPoint = controlPoint;
        }

        @Override
        public Point evaluate(float t, Point startValue, Point endValue)
        {
            int x = (int) ((1 - t) * (1 - t) * startValue.x + 2 * t * (1 - t) * controlPoint.x + t * t * endValue.x);
            int y = (int) ((1 - t) * (1 - t) * startValue.y + 2 * t * (1 - t) * controlPoint.y + t * t * endValue.y);
            return new Point(x, y);
        }
    }
}

ps:其中動畫文本的運動軌跡使用了貝塞爾曲線,關(guān)于貝塞爾曲線的TypeEvaluator嵌赠,參考了
http://www.reibang.com/p/d9a3ae9e806d

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市猾普,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌初家,老刑警劉巖乌助,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陌知,死亡現(xiàn)場離奇詭異他托,居然都是意外死亡仆葡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門沿盅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人腰涧,你說我怎么就攤上這事〗颜。” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵费彼,是天一觀的道長箍铲。 經(jīng)常有香客問我雇卷,道長虹钮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任芙粱,我火速辦了婚禮,結(jié)果婚禮上春畔,老公的妹妹穿的比我還像新娘。我一直安慰自己律姨,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布择份。 她就那樣靜靜地躺著,像睡著了一般荣赶。 火紅的嫁衣襯著肌膚如雪鸽斟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天富蓄,我揣著相機與錄音慢逾,去河邊找鬼立倍。 笑死侣滩,一個胖子當(dāng)著我的面吹牛口注,可吹牛的內(nèi)容都是我干的胜卤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼葛躏,長吁一口氣:“原來是場噩夢啊……” “哼悠菜!你這毒婦竟也來了舰攒?” 一聲冷哼從身側(cè)響起悔醋,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎芬骄,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體账阻,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年姻僧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撇贺。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖松嘶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情翠订,我是刑警寧澤巢音,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布蕴轨,位于F島的核電站,受9級特大地震影響橙弱,放射性物質(zhì)發(fā)生泄漏歧寺。R本人自食惡果不足惜棘脐,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蛀缝。 院中可真熱鬧,春花似錦屈梁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽构哺。三九已至,卻和暖如春曙强,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背碟嘴。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留臀防,地道東北人眠菇。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓袱衷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親致燥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,573評論 2 359

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

  • 【Android 動畫】 動畫分類補間動畫(Tween動畫)幀動畫(Frame 動畫)屬性動畫(Property ...
    Rtia閱讀 6,178評論 1 38
  • 1 背景 不能只分析源碼呀辐益,分析的同時也要整理歸納基礎(chǔ)知識断傲,剛好有人微博私信讓全面說說Android的動畫智政,所以今...
    未聞椛洺閱讀 2,716評論 0 10
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,297評論 25 707
  • 曾經(jīng)在某次大會上聽聞杭州逸酒店的微信做的特別好,既然是同行自然會關(guān)注续捂。于是心心念念大半年,終于還是決定去看看牙瓢。 其...
    NJ酒酒閱讀 353評論 0 0
  • 我想沖你發(fā)一場脾氣 好聚好散 覺得你累,其實我也很累矾克。 天天小心翼翼的守著那點心思。
    狗尾巴花_閱讀 179評論 0 0