1. 实现一个自定义View
因为我们的动画需要自己来进行绘制,所以我们需要自定义 View
。
简单来说,自定义
View
是我们自己实现的一个继承于View
的类。在实现后,我们就可以在xml
文件中像调用正常的系统控件一样,来调用我们自己写的View
啦。
1.1 实现自定义View的步骤:
1. 定义一个类,继承于 View
类
class FloatingTabView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}
2. 可以选择性的定义你的 View
中的自定义属性
在我们的类中自定义属性之后,这些属性可以在
xml
中直接使用,就像我们平时用TextView
的android:text="..."
一样
- 首先,我们在
res/values
下新建一个attrs.xml
文件,这个文件将用来储存我们的自定义View属性。
接着,我们在
attrs.xml
中添加如下代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FloatingTabViewStyle">
<attr name="text_selected_color" format="color"/>
<attr name="text_normal_color" format="color"/>
<attr name="text_size" format="dimension"/>
<attr name="lottie_path" format="string"/>
<attr name="icon_normal" format="reference"/>
<attr name="tab_selected" format="boolean"/>
<attr name="tab_name" format="string"/>
</declare-styleable>
</resources>
在上面这段代码中,我们为自定义View添加了很多的自定义属性,如 text_selected_color
等等,而这些属性整体的 style 名字叫做 FloatingTabViewStyle
,一会我们将用这个名字来在我们的代码中声明这些属性。
在代码中获取这些属性,我们需要用到 TypedArray。
在我们的 类中添加如下代码:
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.FloatingTabViewStyle)
mIconNormal = typedArray.getDrawable(R.styleable.FloatingTabViewStyle_icon_normal)
mAnimationPath = typedArray.getString(R.styleable.FloatingTabViewStyle_lottie_path)
mTabName = typedArray.getString(R.styleable.FloatingTabViewStyle_tab_name)
isSelected = typedArray.getBoolean(R.styleable.FloatingTabViewStyle_tab_selected, false)
initAnimator()
}
3. 可以选择性的获取自定义View
的宽与高
获取自定义View
的宽与高有很多方式,这里主要介绍一种最常用的,在 onMeasure
方法中获取。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
screenWidth = measuredWidth.toFloat() //获取自定义View的宽
height = measuredHeight.toFloat() //获取自定义View的高
}
1.2 绘制自定义View:
自定义
View
的绘制在onDraw
方法中完成。在绘制过程中,我们主要用到三样工具:Paint
、Canvas
和Path
。
- 其中
Paint
代表画笔,需要我们自己进行初始化,比如我们可以设置画笔的颜色和线条宽度。 -
Canvas
为onDraw
方法传入的参数,代表画板,是一种绘制时的规则,比如我们可以调用canvas.drawRect()
来画一个矩形。 Canvas 的详细理解 -
Path
代表画画的路径,定义了绘制的顺序 & 区域,一般用于绘制复杂图形(比如我们的波纹动画)。 Path的详细理解
这里写一个简单的小例子,绘制一个紫色的矩形:
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val paint = Paint()
paint.color = R.color.purple_200
paint.strokeWidth = 10f;
canvas?.drawRect(0f, rectHeightTop, screenWidth, rectHeightBottom, paint);
}
2. 为自定义View加入动画效果
我们现在可以绘制图形了,那怎么让我们的图形动起来呢?
在这里,我们使用 ValueAnimator
来帮助我们进行动画处理。 ValueAnimator
是三种主要动画中的一种,具体的动画知识请移步 Carson带你学Android:这是一份全面&详细的动画知识学习攻略。
如下代码所示的例子,我们首先声明一个 animator
,通过 ValueAnimator.ofFloat
来指定它的值变化的范围,并且开启一个监听,让我们的 yVariance
时刻等于变化的值,这样就实现了让数值动起来。
val animator = ValueAnimator.ofFloat(startY.toFloat(), endY.toFloat())
animator?.addUpdateListener { valueAnimator ->
yVariance = valueAnimator.animatedValue as Float
invalidate()
}
animator?.duration = ANIM_TIME.toLong() // 设置一次动画持续时长
animator?.repeatCount = ValueAnimator.INFINITE; // 设置动画重复次数
animator?.start()
2.1 实现会动的贝塞尔曲线
从网上找到一个类似的 贝塞尔曲线实现,其中他的实现思路是把整个半圆分成3段来实现,每段用一个控制点,这样会造成曲线之间的衔接不平滑。于是在他的实现思路上做了如下优化:
如下图所示,我们将一个半圆分成两半,每一半除了起点、终点外,各自有两个控制点。
具体的代码实现如下图所示:
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawRect(0f, rectHeightTop, screenWidth, rectHeightBottom, paint);
val path = Path()
path.moveTo((arcStartX + xVariance), rectHeightTop);
path.cubicTo(
(arcStartX + arcWidth / 7 + xVariance / 2).toFloat(),
startY.toFloat() - abs(yVariance - startY) / 12,
(arcStartX + arcWidth / 7 + xVariance / 4).toFloat(),
yVariance.toFloat(),
arcStartX + arcWidth / 2,
yVariance.toFloat()
);
path.cubicTo(
(arcStartX + arcWidth * 6 / 7 - xVariance / 4).toFloat(),
yVariance.toFloat(),
(arcStartX + arcWidth * 6 / 7 - xVariance / 2).toFloat(),
startY.toFloat() - abs(yVariance - startY) / 12,
arcStartX + arcWidth - xVariance,
rectHeightTop
);
canvas?.drawPath(path, paint);
}
2.2 控制动画速度
控制动画速度方面我们用到了插值器。 详细了解插值器
因为想要实现一个回弹的效果,在研究了系统自带插值器之后发现,BounceInterpolator
比较接近想要的效果。在对 BounceInterpolator 的源码进行研究后发现,BounceInterpolator
的弹跳曲线如下图所示:
而想要实现的效果是弹到中间点A后,再迅速弹到最高点B,最终下降到中间点A。所以,我们需要对插值器进行自定义构建。
2.3 实现自定义插值器
实现自定义插值器,我们只需要构建一个类,让其继承于 TimeInterpolator
类,并实现其中的 getInterpolation
方法。在 getInterpolation
方法中,传入的参数 input
范围在 0~1 之间,代表整个动画运动的过程。我们可以针对动画运动的不同阶段,来为其返回不同的运动速度。如下方代码所示,在运动的前半段和后半段,我们采用了不同的运动速度。具体的动画实现效果大家可以自由设定和发挥。
class ValueChangeInterpolator : TimeInterpolator{
override fun getInterpolation(input: Float): Float {
var result = 0f
if(input <= 0.5){
result = ((sin(Math.PI * input)) / 2).toFloat();
}
else {
result = ((2 - sin(Math.PI * input)) / 2).toFloat()
}
return result
}
}
参考文档:
https://www.runoob.com/w3cnote/android-advance-custom-view.html
https://blog.csdn.net/mChenys/article/details/50408819
https://blog.csdn.net/carson_ho/article/details/60598775
https://segmentfault.com/a/1190000000721127
https://www.jianshu.com/p/2c19abde958c
https://www.jianshu.com/p/53759778284a
https://www.jianshu.com/p/2f19fe1e3ca1