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
http://www.lxweimin.com/p/2c19abde958c
http://www.lxweimin.com/p/53759778284a
http://www.lxweimin.com/p/2f19fe1e3ca1