高級 UI 成長之路 (一) View 的基礎知識你必須知道

前言

View 能夠說是在平常開發中,每天使用的角色,雖說 View 不屬於四大組件,可是它的做用跟重要程度你真不可小視。該類型的文章打算寫一個系列,對於本身複習或新手學習提供一個方式。css


高級 UI 成長之路 (一) View 的基礎知識你必須知道java

[高級 UI 成長之路 (二) 深刻理解 Android 8.0 View 觸摸事件分發機制]android

[高級 UI 成長之路 (三) 深刻理解 Android 8.0 View 工做原理]git

[高級 UI 成長之路 (四) Paint 詳解]github

[高級 UI 成長之路 (五) Canvas 詳解]canvas

[高級 UI 成長之路 (六) PathMeaure 的使用]app

[高級 UI 成長之路 (七) 實際開發中的屏幕適配總結]框架

[高級 UI 成長之路 (八) 屬性動畫詳解]ide


View 基礎知識

該篇主要介紹 View 的一些基礎知識,從而能夠爲後續文章內容作好鋪墊,主要涉及到的內容有 View 的位置參數、MotionEvent 和 TouchSlop 對象、VelocityTracker、GestureDetector 和 Scroller 對象,經過對這些基礎知識的介紹,相信你對更加複雜的操做也是手到擒來。oop

View 介紹

MNOC40.png

在介紹 View 的基礎知識以前,咱們須要知道它究竟是什麼? View 在 Android 中是全部控件的基類(結構參考上圖),無論是簡單的 TextView , 仍是複雜的 ViewGroup 、 CustomView 亦或者 RecyclerView 它們的共同頂級父類都是 View, 因此說, View 是一種界面層控制的一種抽象,它表明的是一個控件。從上圖可知 ViewGroup 是 View 的子類,ViewGroup 在視圖層它能夠有任意子 View 。

明白 View 的層級關係有助於理解 View 的工做機制。從上圖咱們也能夠知道實現自定義 View 控件能夠繼承自 View 也能夠繼承自 ViewGroup 。

View 位置參數

View 的位置主要由它的四個頂點來決定,分別對應於 View 的四個屬性: top、left、right、bottom , 其中 top 是左上角縱座標,left 是左上角橫座標,right 是右下角橫座標,bottom 是右下角縱座標。須要注意的是,這些座標都是相對於 View 的父容器,由於它是一種相對座標,View 的座標和父容器的關係能夠參考下圖,在 Android 中 ,x 軸 y 軸 的正方向分別爲右和下,這點不難理解,不只僅是 Android ,其實大部分顯示系統都是按照這個標準來定義座標系的。

座標系

根據上圖,咱們很容易得出 View 的寬高和座標的關係:

val width = right - left
val height = bottom - top
複製代碼

那麼如何獲得 View 的這四個參數呢?也很簡單,在 View 的源碼中它們對應於 mLeft 、mRight 、mTop 、和 mBottom 這四個成員變量,經過代碼獲取方式以下:

val left = left;
val right = right
val top = top
val bottom = bottom
複製代碼

從 Android 3.0 開始,View 增長了 額外的幾個參數,x 、y 、translationX 、translationY , 其中 x 和 y 是 View 左上角的座標,而 translationX 和 translationY 是 View 左上角相對於父容器的偏移量。這幾個參數也是相對於父容器的座標,而且 translationX 和 translationY 的默認值是 0 ,和 View 的四個基本的位置參數同樣,View 也爲他們提供了 set/get 方法,這幾個參數的換算關係以下所示:

val x = left + translationX
val y = top + translationY
複製代碼

須要注意的是,View 在平移過程當中,top 和 left 表示的是原始左上角的位置信息,其值並不會發生改變,此時發生改變的是 x 、y、translationX 、translationY 這四個參數。

MotionEvent 和 TouchSlop

MotionEvent

override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                println("手指按下")
            }
            MotionEvent.ACTION_UP -> {
                println("手指擡起")

            }
            MotionEvent.ACTION_MOVE -> {
                println("手指移動")
            }
        }
        return true

    }
複製代碼

在手指接觸屏幕後所產生的一系列事件中,經常使用的而且很是典型的事件類型有以下幾種:

  • MotionEvent.ACTION_DOWN: 手指剛接觸屏幕
  • MotionEvent.ACTION_MOVE: 手指在屏幕上滑動
  • **MotionEvent.ACTION_UP: ** 手指在屏幕上擡起的一瞬間觸發該事件

正常狀況下,一次手指觸摸屏幕的行爲會觸發一些列點擊事件,考慮有以下幾種狀況:

  • DOWN ---> UP: 點擊屏幕後馬上擡起手指鬆開屏幕觸發的事件
  • DOWN ---> MOVE ---> MOVE ---> MOVE ---> UP: 點擊屏幕而後隨着在屏幕上滑動以後在鬆開產生的事件

上述三種狀況是典型的事件序列,同時經過 MotionEvent 對象咱們能夠獲得點擊事件發生的 x 和 y 座標。所以,系統提供了兩組方法 getX / getY 和 getRawX / getRawY 它們的區別其實很簡單, 以下:

  • getX / getY : 返回相對於當前 View 左上角的 x 和 y 的座標
  • getRawX / getRawY : 返回的是相對於手機屏幕左上角的 x 和 y 座標。

TouchSlop

TouchSlop 官方解釋就是系統所能識別的被認爲是滑動的最小距離,通俗點說就是當手指在屏幕上滑動時,若是兩次滑動之間的距離小於這個常量,那麼系統就認爲你沒有在滑動,能夠經過下面的 API 獲取該常量值,

/** * 系統所能識別出來的被認爲滑動的最小距離 */
val scaledDoubleTapSlop = ViewConfiguration.get(context).scaledDoubleTapSlop;
複製代碼

這個常量能夠幫助咱們在處理滑動時,利用該數值來作一些過濾,好比當兩次滑動事件的滑動距離小於這個值,咱們就能夠未達到滑動距離的臨界點,所以就能夠認爲他們不是滑動,這樣作能夠有更好的用戶體驗。

VelocityTracker 、GestureDetector 和 Scroller

VelocityTracker

VelocityTracker 的做用是用於追蹤滑動過程當中的速度,包括水平和豎直方向的速度。它的使用過程很簡單,首先,在 View 的 onTouchEvent 方法中追蹤當前單擊事件的速度;

/** * 速度追蹤 */
 val velocityTracker = VelocityTracker.obtain()
   velocityTracker.addMovement(event)
複製代碼

接着,當咱們先知道當前的滑動速度時,這個時候能夠採用以下方式來得到當前的速度:

velocityTracker.computeCurrentVelocity(1000)
val xVelocity = velocityTracker.getXVelocity()
val yVelocity = velocityTracker.getYVelocity()
複製代碼

這一步有 2 點須要注意,其一 獲取速度以前必須先計算速度,既 getXVelocity 和 getYVelocity 這兩個方法的前面必需要調用 computeCurrentVelocity 方法,第二點,這裏的速度是指一段時間內手指所滑過的像素值,好比將時間間隔設爲 1000 ms 時,那麼就是在 1s 內手指在水平方向從左向右滑動 500 px 那麼水平速度就是 500,注意速度能夠爲負數,當手指從右往左滑動時,水平方向速度即爲負值,這個須要理解一下。速度的計算能夠用以下公式:

速度 = ( 終點位置 - 起點位置) / 時間段

根據上面的公式再加上 Android 系統的座標體系,能夠知道,手指逆着座標系的正方向滑動,所產生的速度就爲負值,另外,computeCurrentVelocity 這個方法的參數表示的是一個時間單元或者說時間間隔,它的單位是毫秒 (ms), 計算速度時獲得的速度就是在這個時間間隔內手指在水平或豎直方向上所滑動的像素值。

針對上面的例子,若是咱們經過 obtain.computeCurrentVelocity(1000) 來獲取速度,那麼獲得的速度就是手指在 1000 ms 毫秒內所滑過的 px 值,所以能夠直接套上面公式:

水平速度 = 500 px / 1000 ms

既水平速度爲 2 , 這裏須要好好理解一下。

最後,當不須要它的時候,須要調用 clear 方法來重置並回收內存:

velocityTracker.clear()
velocityTracker.recycle()
複製代碼

VelocityTracker 的 API 簡單明瞭,咱們能夠記住一個套路。

  1. 在觸摸事件爲 ACTION_DOWN或是進入 onTouchEvent方法時,經過 obtain獲取一個 VelocityTracker
  2. 在觸摸事件爲 ACTION_UP時,調用 recycle進行釋放 VelocityTracker
  3. 在進入 onTouchEvent方法或將 ACTION_DOWNACTION_MOVEACTION_UP的事件經過 addMovement方法添加進 VelocityTracker
  4. 在須要獲取速度的地方,先調用 computeCurrentVelocity方法,而後經過 getXVelocitygetYVelocity獲取對應方向的速度

解鎖更多姿式能夠參考該文章

GestureDetector

GestureDetector 的做用用於輔助檢測用戶的單機、滑動、長按、雙擊等行爲。要使用 GestureDetector 也不復雜,參考以下過程:

  1. 首先建立一個 GestureDetector 對象並實現 OnGestureListener 接口,根據須要咱們還能夠實現 OnDoubleTapListener 從而可以監聽雙擊行爲;

    val mGetDetector = GestureDetector(context,this)
    //解決長按屏幕後沒法拖動的現象
    mGetDetector.setIsLongpressEnabled(false)
    複製代碼
  2. 接管目前 View 的 onTouchEvent 方法,在 View 的 onTouchEvent 方法中添加以下代碼:

    override fun onTouchEvent(event: MotionEvent) = mGetDetector.onTouchEvent(event)
    複製代碼

作完了上面這 2 步,咱們就能夠有選擇的實現 OnGestureListener 和 OnDoubleTapListener 中的方法了,這 2 個接口中的方法介紹以下所示:

OnGestureListener / 方法名 描述
onDown 手指輕輕觸摸屏幕的一瞬間,由 1 個 ACTION_DOWN 觸發
onShowPress 手指輕輕觸摸屏幕, 還沒有鬆開或拖動,由一個 ACTION_DOWN 觸發,它強調的是沒有鬆開或者拖動的狀態
onSingleTapUp 手指(輕輕觸摸屏幕後)鬆開,伴隨着 1 個 MotinEvent.ACTION_UP 而觸發,這是單擊行爲
onScroll 手指按下屏幕並拖動,由 1 個 ACTION_DOWN ,多個 ACTION_MOVE 觸發,這是拖動行爲
onLongPress 用戶長久的按着屏幕不放,既長按
onFling 用戶按下觸摸屏、快速滑動後鬆開,由 1 個 ACTION_DOWN 、多個 ACTION_MOVE 和 1 個 ACTION_UP 觸發,這是快速滑動行爲
OnDoubleTapListener / 方法名 描述
onDoubleTap 雙擊,由 2 次連續的單擊組成,它不可能和 onSingleTapConfirmed 共存
onSingleTapConfirmed 嚴格的單機行爲(注意它和 onSingleTapUp 的區別,若是觸發了 onSingleTapConfirmed, 那麼後面不可能再緊跟着另外一個單擊行爲,既這隻多是單擊,而不多是雙擊中的一次單擊)
onDoubleTapEvent 表示發生了雙擊行爲,在雙擊的期間, ACTION_DOWN 、ACTION_MOVE 和 ACTION_UP 都會觸發此回調

上面圖表裏面的方法不少,可是並非全部的方法都會被時經常使用到,在平常開發中,比較經常使用的有 onSingleTapUp 單擊、onFling 快速滑動 、onScroll 拖動 、onLongPress 長按、onDoubleTap 雙擊 。另外在說一下,在實際開發中,能夠不使用 GestureDector, 徹底能夠本身在 View 的 OnTouchEvent 方法中實現所須要的監聽,這個看實際場景跟我的喜愛了。

Scroller

Scroller 用於實現 View 的彈性滑動。咱們知道,當使用 View 的 scrollTo / scrollBy 方法進行滑動時,其過程是瞬間完成的,沒有一個過渡的效果體驗是不友好的,那麼這個時候就能夠藉助 Scroller 來實現過渡效果的滑動。Scroller 自己沒法讓 View 彈性滑動,它須要和 View 的 computeScroll 方法配合使用才能共同完成這個功能。那麼如何使用 Scroller 呢?它的典型代碼能夠說是固定的,以下所示:

class ScrollerSample_1 : LinearLayout {

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context) : super(context) 

    /** * 定義滑動 Scroller */
    private val mScroller = Scroller(context)


    public fun smoothScrollTo(destX: Int = -100, destY: Int = -100) {
        //滑動了的位置
        val scrollX = scrollY;
        val delta = destY - scrollY;
        //2000 ms 內滑動到 destX 位置,效果就是緩慢滑動
        mScroller.startScroll(scrollX, 0, 0, delta, 2000)
        invalidate()
    }

    override fun computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.currX, mScroller.currY)
            postInvalidate()
        }
    }
}
複製代碼

主要實現有 3 步:

  • 第一步實例化 Scroller

  • 第一步調用 Scroller 的 startScroll 方法,讓其內部保存新的變量值

  • 第二步重寫 View 的 computeScroll 方法,調度自身的 scrollTo 方法,讓其緩慢彈性滑動

View 的滑動

上面我們介紹了 View 的一些基本知識和一些位置參數概念,該小節未來介紹一個重要的內容 View 的滑動

如今市面上全部軟件應該幾乎都具有滑動的功能吧?能夠說滑動功能是一個 APP 的標配,滑動在 Android 開發中具備很重要的做用,所以,掌握滑動的方法是實現優化用戶體驗的基礎。滑動能夠經過如下三種方式來實現,固然並非只有三種,其它還得靠本身去挖掘。其方式以下:

  1. 經過 View 自己提供的 scrollTo / scrollBy 方法來實現滑動(上一小節我們已經用到 scrollTo 方法了)
  2. 經過動畫給 View 施加平移效果來實現滑動
  3. 經過改變 View 的位置參數

scrollTo、scrollBy

爲了實現 View 的滑動看,自身專門提供了 scrollTo 和 scrollBy 方法來實現,以下所示:

//View.java
    public void scrollTo(int x, int y) {
        /** * 傳入的位置跟本身目前所滑動的位置不一致纔開始滑動 */
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

    public void scrollBy(int x, int y) {
        /** * 其內部也是調用了 View 的 scrollTo 方法,把當前滑動的 mScrollX,mScrollY 分別加上指定的位 * 置,而後滑動,屢次調用至關於接着上一次位置滑動 */
        scrollTo(mScrollX + x, mScrollY + y);
    }
複製代碼

經過上面的源碼咱們知道 scrollBy 方法內部是調用了 scrollTo 方法,那麼他們以前有什麼區別呢?請看下面分析:

scrollTo: 基於所傳遞的 x , y 座標來進行絕對滑動,重複點擊若是不改變滑動參數,那麼內部就會作判斷,相等就不會再滑動了。

scrollBy: 經過源碼咱們知道內部調用了 scrollTo 方法傳遞了 mScrollX + x, mScrollY + y 那麼這是什麼意思呢?其實就是基於當前的位置來作的相對滑動。重複點擊滑動會繼續在當前所在的位置上繼續滑動。

還有一個知識點咱們要知道,就是這裏出現了 2 個默認的變量 mScrollX , mScrollY 經過 scrollTo 內部實現咱們知道,其傳遞進去的 x,y 分別賦值給了 mScrollX 和 mScrollY 那麼它們在這裏這麼作的具體含義是什麼呢?它們能夠經過 getScrollX 和 getScrollY 來獲取具體的值。下面咱們就來具體分析下:

mScrollX: 在滑動過程當中,mScrollX 的值老是等於 View 左邊緣和 View 內容左邊緣在水平方向的距離。而且當 View 左邊緣在 View 內容左邊緣的右邊時, mScrollX 值爲正,反之爲負,通俗的來說就是若是從左向右滑動,那麼 mScrollX 爲 負值,反之爲正值。

mScrollY: 在滑動過程當中,mScrollY 的值老是等於 View 上邊緣和 View 內容上邊緣在水平方向的距離。而且當 View 上邊緣在 View 內容上邊緣下邊時,mScrollY 爲正,反之爲負,通俗的來說就是若是從上往下滑動,那麼 mScrollY 爲負值,反之爲正值。

上面解釋了這麼多,爲了更好的理解我這裏就畫一張水平跟豎值方向都滑動了 100 px, 而後來看對應的 mScrollX 和 mScrollY 值是多少,請看下圖:

M0GeGF.png

注意: 在使用 scrollBy / scrollTo 對 View 滑動時,只能將 View 的內容進行移動,並不能將 View 自己進行移動。

使用動畫

上一小節咱們知道能夠採用 View 自身的 scrollTo / scrollBy 方法來實現滑動功能,本小節介紹另一個實現滑動的方式,即便用動畫,經過動畫咱們可以讓一個 View 進行平移,而平移就是一種滑動。使用動畫來移動 View ,主要是操做 View 的 translationX 和 translationY 屬性,能夠採用傳統的 View 動畫,也可使用屬性動畫,若是採用屬性動畫注意要兼容 3.0 一下版本,固然如今都 androidX 版本了,能夠看實際項目狀況來具體處理,實現滑動的平移代碼以下:

  1. 採用 View 動畫,將 View 在 100ms 內從原始位置向右下角移動 100 px

    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
         android:fillAfter="true"
    >
        <translate
                android:duration="100"
                android:fromXDelta="0"
                android:fromYDelta="0"
                android:toXDelta="100"
                android:toYDelta="100"
                android:repeatCount="-1"            
        />
    </set>
    複製代碼

    注意: View 動畫並不能真正改變 View 的位置。

  2. 採用屬性動畫,將 View 在 100ms 內從原始位置向右平移 100 px

    //動畫屬性有 translationX 、translationY 、alpha 、rotation、rotationX、rotationY 、scaleX、scaleY 
    val objAnimator = ObjectAnimator.ofFloat(View targetView,"translationX",0f,100f).setDuration(100).start()
    複製代碼

改變 View LayoutParams

本小節將介紹第三種實現 View 滑動的方法,那就是直接改變佈局參數,即 LayoutParams。好比咱們想把一個 LinearLayout 向右平移 100px 只須要將它的 LayoutParams 內部的 marginLeft 參數的值增長 100 px 就行,代碼以下:

val layoutParams = scroller.layoutParams as LinearLayout.LayoutParams
layoutParams.let {
                it.leftMargin += 100
                it.weight += 100
            }
            scroller.requestLayout()
複製代碼

經過改變 LinearLayout 的 LayoutParams 參數一樣也實現了 View 的滑動。

滑動方式對比

上面分別介紹了 3 種不一樣的滑動方式,它們都能實現 View 的滑動,那麼它們之間的差別是什麼呢?請看下錶:

實現方式 優勢 缺點
scrollTo/scrollBy 專門用於 View 的滑動,比較方便地實現滑動效果且不影響自身的單機事件 只能滑動 View 的內容,不能滑動 View 自己
動畫 複雜動畫使用屬性動畫來完成比較簡單 View 動畫不能改變自身屬性
改變佈局參數 使用不簡潔

針對上面狀況這裏作一個小總結:

  • scrollTo/scrollBy 操做簡單,適合對 View 內容的滑動
  • 動畫操做簡單,主要適合用於沒有交互的 View 和實現複雜的動畫效果
  • 改變佈局參數操做稍微複雜,適用於有交互的 View

彈性滑動

知道了 View 如何滑動,咱們還要知道如何實現 View 的彈性滑動,比較生硬的滑動體驗確實不好,下面咱們介紹 View 如何實現彈性滑動

使用 Scroller

請參考該篇 View基礎知識#Scroller 介紹

經過動畫

M023wV.gif

利用動畫的特性來實現一些動畫不能實現的效果,模仿 Scroller 來實現 View 的彈性滑動,代碼以下:

val valueAnimator = ValueAnimator.ofInt(0, 1).setDuration(2000);
valueAnimator.addUpdateListener {
   val animatedFraction = it.animatedFraction
   scroller.scrollTo(- (100 * animatedFraction).toInt(), 0)
}
valueAnimator.start()
複製代碼

在上述代碼中,咱們的動畫本質上沒有做用於任何對象上,它只是在 2s 內完成了整個動畫過程,利用這個特性咱們就能夠在動畫的每一幀到來時獲取動畫完成的比例,而後根據這個比例計算滑動的距離。

經過延時策略

該小節咱們繼續介紹另外一種實現彈性滑動的效果,即延時策略,它的核心思想是經過發送一系列延時消息從而達到一種漸近式的效果,代碼以下:

val MESSAGE_SCROLLER_TO = 1;
    val FRAME_COUNT = 30;
    val DELAYED_TIME = 33L;
    var mCount = 0;
    private val mHandler = object : Handler() {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            when (msg.what) {
                MESSAGE_SCROLLER_TO -> {
                    mCount++
                    if (mCount <= FRAME_COUNT) {
                        val fraction = mCount / FRAME_COUNT.toFloat()
                        val scrollX = (fraction * 100).toInt()
                        scroller.scrollTo(scrollX, 0)
                        sendEmptyMessageDelayed(MESSAGE_SCROLLER_TO, DELAYED_TIME)
                    }
                }


            }
        }
    }
複製代碼

其效果都是同樣的,這裏就再也不貼效果了,在實際中能夠根據項目需求或靈活性來選擇到底使用哪種來實現彈性滑動。

基礎知識就講到這裏了,下面基於咱們所學的基礎知識練習幾道關於滑動的自定義 View

運用所學知識進行實戰

這裏由淺到深的案例練習。

1. View 隨着手指移動

public class SlideView1(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    /** * 記錄上次滑動的座標 */
    private var mLastX = 0;
    private var mLastY = 0;
    
    /** * 初始化畫筆 */
    val paint = Paint().apply {
        color = Color.BLACK
        isAntiAlias = true
        strokeWidth = 3f
    }


    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                //拿到相對於屏幕按下的座標點
                mLastX = event.getX().toInt();
                mLastY = event.getY().toInt();
                println("拿到相對於屏幕按下的座標點: x:$mLastX y:$mLastY")

            }
            MotionEvent.ACTION_MOVE -> {
                var offsetX = event.getX().toInt() - mLastX;//計算 View 新的擺放位置
                var offsetY = event.getY().toInt() - mLastY;
                //從新放置新的位置
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
            }

            MotionEvent.ACTION_UP -> {

            }
        }
        return true//消耗觸摸事件
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawCircle(300f, 300f, 150f, paint)
    }

}
複製代碼

第二種 setX/setY 方式

public class SlideView2(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    /** * 記錄上次滑動的座標 */
    private var mLastX = 0;
    private var mLastY = 0;

    private val mScroller = Scroller(context)

    /** * 初始化畫筆 */
    val paint = Paint().apply {
        color = Color.BLACK
        isAntiAlias = true
        strokeWidth = 3f
    }


    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                //拿到相對於屏幕按下的座標點
                mLastX = event.getRawX().toInt();
                mLastY = event.getRawY().toInt();
                println("拿到相對於屏幕按下的座標點: x:$mLastX y:$mLastY")

            }
            MotionEvent.ACTION_MOVE -> {
              //1
// x = event.getRawX() - mLastX
// y = event.getRawY() - mLastY

							//2
                translationX = event.getRawX() - mLastX
                translationY = event.getRawY() - mLastY
            }

            MotionEvent.ACTION_UP -> {

            }
        }
        return true//消耗觸摸事件
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawCircle(300f, 300f, 150f, paint)
    }
}
複製代碼

第二種方法是調用 View 的 setX、setY 其實內部就是調用的是 setTranslationX、setTranslationY 這 2 中方式其實都同樣 setX 內部也會調用 setTranslationX,能夠看一下源碼,以下:

//View.java
    public void setX(float x) {
        setTranslationX(x - mLeft);
    }
複製代碼

這裏爲了演示效果,代碼沒有作邊界判斷,下來感興趣的能夠本身去研究,還有其它隨着手指滑動的實現就靠本身去發掘了。

2.高仿 ViewPager

下面就以 Scroller 來實現一個簡版的 ViewPager 效果,要實現 Scroller 效果其固定步驟以下:

  1. 建立 Scroller 的實例
  2. 調用 startScroll() 方法來初始化滾動數據並刷新界面
  3. 重寫 computeScroll() 方法,並在其內部完成平滑滾動的邏輯
/** * <pre> * author : devyk on 2019-11-16 19:23 * blog : https://juejin.im/user/578259398ac2470061f3a3fb/posts * github : https://github.com/yangkun19921001 * mailbox : yang1001yk@gmail.com * desc : This is ScrollerViewPager * </pre> */
class ScrollerViewPager(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
    /** * 第一步 定義 Scroller 實例 */
    private var mScroller = Scroller(context)

    /** * 判斷拖動的最小移動像素點 */
    private var mTouchSlop = 0

    /** * 手指按下屏幕的 x 座標 */
    private var mDownX = 0f

    /** * 手指當前所在的座標 */
    private var mMoveX = 0f

    /** * 記錄上一次觸發 按下是的座標 */
    private var mLastMoveX = 0f

    /** * 界面能夠滾動的左邊界 */
    private var mLeftBorder = 0

    /** * 界面能夠滾動的右邊界 */
    private var mRightBorder = 0


    init {
        init()
    }

    constructor(context: Context?) : this(context, null) {
    }


    private fun init() {
        /** * 經過 ViewConfiguration 拿到認爲手指滑動的最短的移動 px 值 */
        mTouchSlop = ViewConfiguration.get(context).scaledPagingTouchSlop
    }


    /** * 測量 child 寬高 */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //拿到子View 個數
        val childCount = childCount
        for (index in 0..childCount - 1) {
            val childView = getChildAt(index)
            //爲 ScrollerViewPager 中的每個子控件測量大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec)

        }
    }

    /** * 測量完以後,拿到 child 的大小而後開始對號入座 */
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        if (changed) {
            val childCount = childCount
            for (child in 0..childCount - 1) {
                //拿到子View
                val childView = getChildAt(child)
                //開始對號入座
                childView.layout(
                    child * childView.measuredWidth, 0,
                    (child + 1) * childView.measuredWidth, childView.measuredHeight
                )
            }
            //初始化左右邊界
            mLeftBorder = getChildAt(0).left
            mRightBorder = getChildAt(childCount - 1).right

        }

    }

    /** * 重寫攔截事件 */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                //拿到手指按下至關於屏幕的座標
                mDownX = ev.getRawX()
                mLastMoveX = mDownX
            }
            MotionEvent.ACTION_MOVE -> {
                //拿到當前移動的 x 座標
                mMoveX = ev.getRawX()
                //拿到差值
                val absDiff = Math.abs(mMoveX - mDownX)
                mLastMoveX = mMoveX
                //當手指拖動值大於 TouchSlop 值時,就認爲是在滑動,攔截子控件的觸摸事件
                if (absDiff > mTouchSlop)
                    return true
            }
        }


        return super.onInterceptTouchEvent(ev)
    }

    /** * 父容器沒有攔截事件,這裏就會接收到用戶的觸摸事件 */
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_MOVE -> {
                //拿到當前滑動的相對於屏幕左上角的座標
                mMoveX = event.getRawX()
                var scrolledX = (mLastMoveX - mMoveX).toInt()
                if (scrollX + scrolledX < mLeftBorder) {
                    scrollTo(mLeftBorder, 0)
                    return true
                }else if (scrollX + width + scrolledX > mRightBorder){
                    scrollTo(mRightBorder-width,0)
                    return true

                }
                scrollBy(scrolledX,0)
                mLastMoveX = mMoveX
            }
            MotionEvent.ACTION_UP -> {
                //當手指擡起是,根據當前滾動值來斷定應該回滾到哪一個子控件的界面上
                var targetIndex = (scrollX + width/2 ) / width
                var dx = targetIndex * width - scrollX
                /** 第二步 調用 startScroll 方法彈性回滾並刷新頁面*/
                mScroller.startScroll(scrollX,0,dx,0)
                invalidate()
            }
        }
    return super.onTouchEvent(event)
    }

    override fun computeScroll() {
        super.computeScroll()
        /** * 第三步 重寫 computeScroll 方法,並在其內部完成平滑滾動的邏輯 */
        if (mScroller.computeScrollOffset()){
            scrollTo(mScroller.currX,mScroller.currY)
            postInvalidate()
        }
    }
}
複製代碼
<?xml version="1.0" encoding="utf-8"?>
<com.devyk.customview.sample_1.ScrollerViewPager
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">


    <Button
            android:id="@+id/btn_1"
            android:text="1"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    <Button
            android:id="@+id/btn_2"
            android:text="2"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    <Button
            android:id="@+id/btn_3"
            android:text="3"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    <Button
            android:id="@+id/btn_4"
            android:text="4"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    <Button
            android:id="@+id/btn_5"
            android:text="5"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    <Button
            android:id="@+id/btn_6"
            android:text="6"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
</com.devyk.customview.sample_1.ScrollerViewPager>
複製代碼

經過上面自定義 ViewGroup 實現了跟 ViewPager 同樣的效果基本上用到了該篇文章所學知識,好比 Scroller、TouchSlop 還有下一節即將要分析的事件攔截處理及分發機制。

總結

該篇文章對於新手來講必定要掌握, 特別是文章中基礎知識和 View 的滑動實現,只有打好了基礎,看開源自定義框架或本身寫自定義才順手。

(ps:能夠看到上面代碼示例都是基於 Kotlin 來編寫了,若是有對 Kotlin 感興趣的或者是從 0 開始學的,看完 Kotlin 基礎語法 以後,我相信 新手入門級 Kotlin GitHub APP 正是你須要的)

感謝你的閱讀,謝謝。

參考

相關文章
相關標籤/搜索