开头
这是自定义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!!!
文/lovejjfg(简书)