【Android】從零實現一個美團同款底部導航欄波紋動畫

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) {
}

附:@JvmOverloads傳送門

2. 可以選擇性的定義你的 View 中的自定義屬性

在我們的類中自定義屬性之后,這些屬性可以在xml中直接使用,就像我們平時用 TextViewandroid:text="..." 一樣

  • 首先,我們在 res/values 下新建一個 attrs.xml 文件,這個文件將用來儲存我們的自定義View屬性。

attrs.xml

接著,我們在 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 方法中完成。在繪制過程中,我們主要用到三樣工具:PaintCanvasPath

  1. 其中 Paint 代表畫筆,需要我們自己進行初始化,比如我們可以設置畫筆的顏色和線條寬度。
  2. CanvasonDraw 方法傳入的參數,代表畫板,是一種繪制時的規則,比如我們可以調用 canvas.drawRect() 來畫一個矩形。 Canvas 的詳細理解
  3. 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 的彈跳曲線如下圖所示:

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容