Android 自定义View 跳动的水果和文字

开头

这是自定义View和动画的第二篇,第一篇是Android drawPath实现QQ拖拽泡泡,主要介绍了drawPath 绘制二次贝塞尔曲线的过程。

话不多说,还是先上效果图吧!(今天手贱升级了Genymotion,就成这个傻逼样子了!)

效果图

全局配置

根据效果图,再来说说实现的基本过程。上面的Bitmap 的动画就是使用了属性动画ObjectAnimator,而下面的那个跳动的文字,主要就是使用了drawTextOnPath的方法,其实也是基于第一篇讲解的drawPath来实现的!所以总的来说就是属性动画+drawTextViewPath

动画介绍

这里一共定义了三个属性动画:

private ObjectAnimator distanceDownAnimator;//图片下降的动画
private ObjectAnimator distanceUpAnimator;//图片上升的动画
private ObjectAnimator offsetAnimator;//文字偏移动画

动画这里还要随便提一嘴动画插补器Interpolator

 private DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator();//减速插补器
    private LinearInterpolator linearInterpolator = new LinearInterpolator();//加速插补器
    private LinearOutSlowInInterpolator linearOutSlowInInterpolator = new LinearOutSlowInInterpolator();
    private FastOutSlowInInterpolator fastOutSlowInInterpolator = new FastOutSlowInInterpolator();
    private BounceInterpolator bounceInterpolator = new BounceInterpolator();//反弹插补器  

详细的请看这个兄弟的博客,有配图,很形象直观的!

这里的话,图片下落肯定是一个重力加速的过程 使用了LinearInterpolator,而上升的话,肯定是一个减速的过程,使用了DecelerateInterpolator,而文字的跳动,那就非BounceInterpolator 莫属了!

到这里,动画的基础讲解暂告一段落。

drawTextOnPath 方法使用介绍

Draw the text, with origin at (x,y), using the specified paint, along the specified path. The paint's Align setting determins where along the path to start the text.

其实这个方法就是drawText() 的方法的基础上,沿着指定的路径来绘制对应的文字!

drawTextOnPath热身

对应的代码:

 path.reset();
 path.moveTo(100, 100);
 path.lineTo(300, 200);
 path.lineTo(700, 600);
 canvas.translate(0, 100);
 paint.setStyle(Paint.Style.FILL);
 canvas.drawText(TEST, 0, 0, paint);//直接画文字 第一个
 canvas.translate(0, 300);
 canvas.drawTextOnPath(TEST, path, 0, 0, paint);//第二组
 paint.setStyle(Paint.Style.STROKE);
 canvas.drawPath(path, paint);
 path.reset();
 path.moveTo(0, 500);
 path.quadTo(400, 800, 800, 500);
 paint.setStyle(Paint.Style.FILL);
 canvas.drawTextOnPath(TEST, path, 0, 0, paint);//第三组,这个也差不多就是后面需要实现的效果了!
 paint.setStyle(Paint.Style.STROKE);
 canvas.drawPath(path, paint);

啦啦啦,通过热身,可以清楚的看到,要想实现跳动的文字其实很简单啦,就是动态的改变Path的路径,然后在这个路径上不断绘制出文字就好了!原理说着都是枯燥的,直接撸上代码!

OffsetAnimator && OffsetProperty

如上面的介绍,这个Animator就是来控制Path的绘制的。

offsetAnimator = ObjectAnimator.ofFloat(this, mOffsetProperty, 0);
offsetAnimator.setDuration(300);
offsetAnimator.setInterpolator(bounceInterpolator);

这里使用了自定义的属性OffsetProperty,这个是什么鬼呢?其实就是一个自己定义的属性啦!

 private Property<PathTextView, Float> mOffsetProperty = new Property<PathTextView, Float>(Float.class, "offset") {
    @Override
    public Float get(PathTextView object) {
        return object.getCurrentOffset();
    }

    @Override
    public void set(PathTextView object, Float value) {

        object.setCurrentOffset(value);
    }
};

 public void setCurrentOffset(Float currentOffset) {
    this.currentOffset = currentOffset;
    invalidate();
}

就是通过属性动画,得到新的currentOffset,然后再调用 invalidate() 不停的重画!在onDraw() 方法里,有一下代码片段来更新path,然后根据path绘制文字!!

if (currentOffset != -1) {
        path.quadTo(dXXX == 0 ? radioCenterX : radioCenterX + dXXX, currentOffset, textWidth, defaultY);
    } else {
        path.lineTo(textWidth, defaultY);
    }
...
canvas.drawTextOnPath(TEST, path, 0, 0, textPaint);

嗯,说到这里,其实今天要讲的跳动的问题,其实跳动的文字基本上就OK啦,但是水果忍者的话,就是接下来的重点实现了。


水果忍者

我们这里一共有三个动画:

private ObjectAnimator distanceDownAnimator;//图片下降的动画
private ObjectAnimator distanceUpAnimator;//图片上升的动画
private ObjectAnimator offsetAnimator;//文字偏移动画

动画的流程

distanceDownAnimator.start ---> distanceDownAnimator.onEnd ---> distanceUpAnimator.start && offsetAnimator.start ---> distanceUpAnimator.end ---> distanceDownAnimator.start

顺便提一嘴动画的回调监听:

  distanceUpAnimator.addListener(new SimpleAnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
            isUp = true;
            left = !left;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            distanceDownAnimator.start();
        }
    });


 distanceDownAnimator.addListener(new SimpleAnimatorListener() {

        @Override
        public void onAnimationStart(Animator animation) {
            isUp = false;
            dXXX = 0;
            if (++currentIndex >= bitmaps.size()) {
                currentIndex = 0;
            }
            currentBitmap = bitmaps.get(currentIndex);
            radioCenterY = currentBitmap.getHeight() / 2.0f;

        }

        @Override
        public void onAnimationEnd(Animator animation) {
            offsetAnimator.cancel();
            offsetAnimator.setDuration(200);
            offsetAnimator.setFloatValues(defaultY, defaultY + amplitude, defaultY);
            offsetAnimator.start();

            distanceUpAnimator.start();

        }
    });

效果图可以看到,目前我一共设计了三种水果的动画,先从简单的竖直方向掉落又上升说起吧!

这里面其实就是两个动画,一个Y轴的平移,一个是自身的旋转。

  • Y轴旋转,在动画里面直接指定Y轴的相关起点为终点,这个就可以实现了!
  • 自身的旋转: 这里其实Bitmap自己根本没有旋转,我是旋转了画布,从而达到了让水果看起来自己在旋转的情况。

相关问题明确

Q1: 水平方向中心怎么确定?

其实就是确认布局的宽度,布局的宽度就是文字的宽度

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    textHeight = textPaint.getFontMetrics().bottom - textPaint.getFontMetrics().top;
    widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) textPaint.measureText(TEST), MeasureSpec.EXACTLY);//强制使用精准的测量模式
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

Q2: 竖直方向起始位置和终点位置怎么确认?

其实就是确认文字的高度(下落的终点),(图片的高度(下落的起点))

     @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    Log.i(TAG, "onSizeChanged: size改变了!!!!");
    super.onSizeChanged(w, h, oldw, oldh);
    currentHeight = h;
    initAnim(h);
}

 private void initAnim(int currentHeight) {
    textHeight = textPaint.getFontMetrics().bottom - textPaint.getFontMetrics().top;//文字的高度获取
    defaultY = currentHeight - textHeight; //默认的最低处,到文字的顶部
    offsetAnimator.setFloatValues(defaultY, defaultY + amplitude, defaultY);
    radioCenterY = currentBitmap.getHeight() / 2.0f;//初始化默认高度
    distanceDownAnimator.setFloatValues(radioCenterY, defaultY);
    ....
    }

Q3:旋转的动画没有对应的Animator,如果控制?

直接获取 distanceDownAnimator 或者 distanceUpAnimator 的动画执行百分比:

distanceDownAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            fraction = animation.getAnimatedFraction();
        }
    });

Q4:旋转的中心点怎么确认?

图片宽高的一半(如果有水平方向移动也要加上水平偏移量)

float dX = (left ? radioCenterX * fraction : radioCenterX * fraction * -1.0f);//相对于中心点 0 的水平偏移量
radioCenterX = (defaultX + textWidth) / 2.0f;
radioCenterY = currentBitmap.getHeight() / 2.0f;
    
canvas.rotate(360 * fraction, radioCenterX + dX, radioCenterY);

Q5:图片切换如何实现的?

使用一个集合管理了所有的Bitmap,在Down动画开始执行的时候,去更新当前的图片!

final ArrayList<Bitmap> bitmaps = new ArrayList<>();
bitmaps.add(BitmapFactory.decodeResource(getResources(),       R.drawable.fruit1));
bitmaps.add(BitmapFactory.decodeResource(getResources(), R.drawable.fruit2));
bitmaps.add(BitmapFactory.decodeResource(getResources(), R.drawable.fruit3));
...
 if (++currentIndex >= bitmaps.size()) {
                currentIndex = 0;
            }
currentBitmap = bitmaps.get(currentIndex);

到这里,起点位置终点位置以及旋转中心点位置已经确认完毕了!无论是下降,还是上升的动画,都是不断在改变radioCenterY 的值,原理同之前介绍的offset相同

    private Property<PathTextView, Float> mDistanceProperty = new Property<PathTextView, Float>(Float.class, "distance") {
    @Override
    public Float get(PathTextView object) {
        return object.getCurrentDistance();
    }

    @Override
    public void set(PathTextView object, Float value) {
        object.setCurrentDistance(value);

    }
};


    public void setCurrentDistance(Float currentDistance) {
    this.radioCenterY = currentDistance;
    invalidate();
}

三种动画切换

首先必须明确的是,这三种动画,都是修改UpAnimator的相关逻辑,跳动模式还需要OffsetAnimator的配合(这个稍后说!),其他两种无外乎就是修改了对应的动画执行时间以及一个透明度的效果,而透明度和之前说的旋转效果一直,都是通过fraction这个参数来控制的!

面向状态编程:

 switch (Mode) {
        case Default://默认模式
            distanceDownAnimator.setDuration(1000);
            distanceUpAnimator.setDuration(1000);
            distanceUpAnimator.setInterpolator(decelerateInterpolator);
            distanceUpAnimator.setFloatValues(defaultY - textHeight, radioCenterY);
            break;
        case Oblique://曲线模式
            distanceDownAnimator.setDuration(500);

            distanceUpAnimator.setDuration(1000);
            distanceUpAnimator.setInterpolator(decelerateInterpolator);
            distanceUpAnimator.setFloatValues(defaultY - textHeight, radioCenterY + currentBitmap.getHeight());//到达不了最高处
            break;
        case Bounce://跳动模式
         
            distanceDownAnimator.setDuration(1000);

            distanceUpAnimator.setDuration(2000);
            distanceUpAnimator.setInterpolator(linearOutSlowInInterpolator);
         
            distanceUpAnimator.setFloatValues(defaultY - textHeight , defaultY - 4 * textHeight, (int) (defaultY - textHeight + density * 1.5f), defaultY - 2 * textHeight);
            break;

最后一个模式中,是需要在radioCenterY在移动到最低处去开始执行Offset的动画的,但是这里就有一个问题:根据fraction没法去判断什么时候执行到了最低处所以这里我就让在这种模式的时候,我在setFloatValues()的方法中,第二次到达的最低点(defaultY - textHeight)的基础上再向下移动了2dp! 所以 所以 所以,重要的说三遍!,这个是有点儿不精准滴!影响就是可能它不会跳第二下!哈哈哈。。。

addUpdateListener()中:如果是跳动模式,那么就去获取对应的偏移量,并且重置offsetAnimator的一些参数!

 if (Mode == Bounce && (int) (defaultY - textHeight + density) == (int) f && !offsetAnimator.isRunning()) {

                dXXX = (left ? radioCenterX * fraction : radioCenterX * fraction * -1.0f);
                offsetAnimator.cancel();
                offsetAnimator.setDuration(300);
                offsetAnimator.setFloatValues(defaultY, defaultY + 50, defaultY);
                offsetAnimator.start();
                Log.i(TAG, "onAnimationUpdate: YY" + (int) f);
                Log.i(TAG, "onAnimationUpdate: XX" + (left ? radioCenterX * fraction : radioCenterX * fraction * -1.0f));
            }

详细代码请移步 Github_Circle
这个仓库都是自定义View onDraw相关的!目前正在建设中!喜欢请记得start fork!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • 系列文章之 Android中自定义View(一)系列文章之 Android中自定义View(二)系列文章之 And...
    YoungerDev阅读 2,099评论 0 4
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,004评论 5 13
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,617评论 4 59
  • 昨夜喝了八瓶啤酒,看了两集电视剧 早上突然发现往常卖早餐的大叔没有出现 翻出了多年前和朋友的合影,突然觉得当时的自...
    羌淄泫阅读 298评论 0 2