Android View 事件體系

一、View的位置參數

top、bottom、left、right都是相對於ViewGroup的,易知:

width = right - left
height = bottom - top

另外還有x、y、translationX、translationY參數,x、y表示View左上角的座標,translationX和translationY表示View的偏移量。在View平移的過程中,top、bottom、left、right不會改變,只有translationX、translationY、x、y會改變。

x = left + translationX
y = top + translationY

二、觸摸View時,MotionEvent事件的位置信息

注:rawY是包含狀態欄和ActionBar高度的。

三、最小滑動距離touchSlop

距離高於touchSlop的滑動纔算有效滑動,此參數與設備有關,獲取代碼如下:

ViewConfiguration.get(context).scaledTouchSlop

四、速度追蹤器VelocityTracker

使用方式如下:

customView.setOnTouchListener { v, event ->
    val velocityTracker = VelocityTracker.obtain()
    // 將event事件添加到VelocityTracker中追蹤速度
    velocityTracker.addMovement(event)
    // 獲取速度之前必須先計算速度,傳入的參數表示速度的時間單位,這裏表示:n像素/1000毫秒。注意它不是指多長時間計算一次速度,只是表示速度單位的不同
    velocityTracker.computeCurrentVelocity(1000)
    // 向下滑動時xVelocity爲正,向上滑動時xVelocity爲負。向右滑動時yVelocity爲正,向左滑動時yVelocity爲負
    Log.d("~~~","xVelocity = ${velocityTracker.xVelocity}, yVelocity = ${velocityTracker.yVelocity}")
    // 使用完後需要手動回收
    velocityTracker.clear()
    velocityTracker.recycle()
    true
}

注:向下滑動時xVelocity爲正,向上滑動時xVelocity爲負。向右滑動時yVelocity爲正,向左滑動時yVelocity爲負。

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

五、手勢檢測GestureDetector

手勢檢測回調接口如下:

1.OnGestureListener

  • onDown(MotionEvent e):手指按下屏幕,由ACTION_DOWN觸發

  • onShowPress(MotionEvent e):手指按下屏幕,尚未鬆開或拖動,強調的是沒有鬆開或者拖動的狀態。快速拖動時此回調不一定觸發

  • onLongPress(MotionEvent e):用戶長按後觸發,觸發之後不會觸發其他OnGestureListener回調,直至鬆開(UP事件)。

  • onScroll(MotionEvent e1, MotionEvent e2,float distanceX, float distanceY):手指按下屏幕並拖動,由一個ACTION_DOWN,多個ACTION_MOVE觸發,表示拖動行爲

  • onFling(MotionEvent e1, MotionEvent e2, float velocityX,float velocityY):用戶執行快速滑動操作之後的回調,MOVE事件之後手鬆開(UP事件)那一瞬間的x或者y方向速度,如果達到一定數值,就是快速滑動操作,由一個ACTION_DOWN、多個ACTION_MOVE和一個ACTION_UP觸發。

  • onSingleTapUp(MotionEvent e):用戶手指鬆開(UP事件)的時候如果沒有執行onScroll()和onLongPress()這兩個回調的話,就會回調這個,表示一個點擊擡起事件。

2.OnDoubleTapListener,這個Listener監聽雙擊和單擊事件。

  • onSingleTapConfirmed(MotionEvent e):可以確認這是一個單擊事件的時候會回調,注意和onSingleTapUp的區別,即這隻可能是單擊,而不可能是雙擊中的一次單擊。

  • onDoubleTap(MotionEvent e):雙擊,它不可能和onSingleTapConfirmed共存。

  • onDoubleTapEvent(MotionEvent e):onDoubleTap()回調之後的輸入事件(DOWN、MOVE、UP)都會回調這個方法(這個方法可以實現一些雙擊後的控制,如讓View雙擊後變得可拖動等)。

3.OnContextClickListener,用於檢測外部設備上的按鈕是否按下

  • onContextClick(MotionEvent e):當外部設備點擊時候的回調,如外接鍵盤、外接藍牙觸控筆等等

4.SimpleOnGestureListener

實現了上面三個接口的類,擁有上面三個的所有回調方法。

使用SimpleOnGestureListener只需要選取我們所需要的回調方法來重寫就可以了,減少了代碼量。

例如:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val gestureDetector =
            GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
                override fun onShowPress(e: MotionEvent?) {
                    Log.d("~~~", getCurrentMethod())
                }

                override fun onSingleTapUp(e: MotionEvent?): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return false
                }

                override fun onDown(e: MotionEvent?): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return true
                }

                override fun onFling(
                    e1: MotionEvent?,
                    e2: MotionEvent?,
                    velocityX: Float,
                    velocityY: Float
                ): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return false
                }

                override fun onScroll(
                    e1: MotionEvent?,
                    e2: MotionEvent?,
                    distanceX: Float,
                    distanceY: Float
                ): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return false
                }

                override fun onLongPress(e: MotionEvent?) {
                    Log.d("~~~", getCurrentMethod())
                }

                override fun onDoubleTap(e: MotionEvent?): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return false
                }

                override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return false
                }

                override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return false
                }

                override fun onContextClick(e: MotionEvent?): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return super.onContextClick(e)
                }
            })
        view.setOnTouchListener { v, event ->
            // down 0, up 1, move 2
            Log.d("~~~", getCurrentMethod() + ", ${event.action}")
            gestureDetector.onTouchEvent(event)
        }
    }

    fun getCurrentMethod(): String {
        return Thread.currentThread().stackTrace[3].methodName
    }
}

單擊一次,Log如下:

~~~: onTouch, 0
~~~: onDown
~~~: onTouch, 1
~~~: onSingleTapUp
~~~: onSingleTapConfirmed

雙擊一次,Log如下:

~~~: onTouch, 0
~~~: onDown
~~~: onTouch, 1
~~~: onSingleTapUp
~~~: onTouch, 0
~~~: onDoubleTap
~~~: onDoubleTapEvent
~~~: onDown
~~~: onTouch, 1
~~~: onDoubleTapEvent

長按,Log如下:

~~~: onTouch, 0
~~~: onDown
~~~: onShowPress
~~~: onLongPress
~~~: onTouch, 1

快速滑動,Log如下:

~~~: onTouch, 0
~~~: onDown
~~~: onShowPress
~~~: onTouch, 2
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 1
~~~: onFling

或者:

~~~: onTouch, 0
~~~: onDown
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 1
~~~: onFling

實際開發中,可以不使用GestureDetector,完全可以在View的onTouchEvent方法中實現所需的監聽。《Android開發藝術探索》中建議:如果只是監聽滑動相關的,建議在onTouchEvent中實現,如果監聽雙擊行爲的話,那就使用GestureDetector。

六、改變View位置的三種方式

1.使用Scroller的scrollTo和scrollBy

Scroller用來實現View的彈性滑動,Scroller的典型使用如下:

private val scroller by lazy { Scroller(context) }
private fun smoothScrollTo(destX: Int, destY: Int) {
    scroller.startScroll(scrollX, scrollY, destX - scrollX, destY - scrollY)
    invalidate()
}
override fun computeScroll() {
    super.computeScroll()
    if (scroller.computeScrollOffset()) {
        scrollTo(scroller.currX, scroller.currY)
        postInvalidate()
    }
}

Scroller的工作原理是根據起始位置和目標位置生成一系列變化的值,通過scrollTo方法一次滑動一小段直至滑動完成。

startScroll方法僅僅用來做初始化操作,然後我們調用了View的invalidate方法使View重繪,重繪時會調用computeScroll方法。我們重寫了這個方法,在這個方法中調用computeScrollOffsetcomputeScrollOffset方法會根據插值器和時間間隔獲取當前變化的值。如果computeScrollOffset返回true,表示尚未到達目標值,這時我們使用scrollTo(scroller.currX, scroller.currY)滑動一小段距離,然後調用postInvalidate使View再次重繪,重繪時又調用computeScroll方法,…,直至滑動完成。

例如,使用Scroller實現一個簡易的ViewPager:

class ScrollLayout @JvmOverloads constructor(
    context: Context,
    attr: AttributeSet,
    defStyleAttr: Int = 0
) : ViewGroup(context, attr, defStyleAttr) {
    private val scroller by lazy { Scroller(context) }
    private val touchSlop by lazy { ViewConfiguration.get(context).scaledTouchSlop }
    private var xLastTouch = 0f
    private var leftBorder = 0
    private var rightBorder = 0

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        if (!changed || childCount == 0) return
        children.withIndex().forEach {
            val index = it.index
            val view = it.value
            view.layout(
                index * view.measuredWidth,
                0,
                (index + 1) * view.measuredWidth,
                view.measuredHeight
            )
        }
        leftBorder = children.first().left
        rightBorder = children.last().right
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        children.forEach {
            measureChild(it, widthMeasureSpec, heightMeasureSpec)
        }
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                xLastTouch = ev.rawX
            }
            MotionEvent.ACTION_MOVE -> {
                val distance = Math.abs(ev.rawX - xLastTouch)
                xLastTouch = ev.rawX
                if (distance > touchSlop) return true
            }
        }
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_MOVE -> {
                val scrolledX = (xLastTouch - event.rawX).toInt()
                if (slideToBorder(scrolledX)) return true
                scrollBy(scrolledX, 0)
                xLastTouch = event.rawX
            }
            MotionEvent.ACTION_UP -> {
                val targetIndex = (scrollX + width / 2) / width
                val distanceX = targetIndex * width - scrollX
                scroller.startScroll(scrollX, 0, distanceX, 0)
                invalidate()
            }
        }
        return super.onTouchEvent(event)
    }

    /**
     * 是否滑到了邊界
     */
    private fun slideToBorder(scrolledX: Int): Boolean {
        if (scrolledX + scrollX < leftBorder) {
            scrollTo(leftBorder, 0)
            return true
        } else if (scrollX + width + scrolledX > rightBorder) {
            scrollTo(rightBorder - width, 0)
            return true
        }
        return false
    }

    override fun computeScroll() {
        super.computeScroll()
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.currX, scroller.currY)
            invalidate()
        }
    }
}

具體參見郭神的文章:Android Scroller完全解析,關於Scroller你所需知道的一切

2.View屬性動畫

早期版本的Android系統只支持補間動畫,造成的問題是View執行完動畫後,只有視圖移到了新位置,“真身”還在老位置,所以點擊新位置不能觸發點擊事件。Android3.0之後加入了屬性動畫,實現了View“真身”隨着視圖一起移動。早期的應用爲了讓屬性動畫兼容Android3.0之前的版本,需要使用JakeWharton大神的nineOldAndroids庫。而現在的Android應用基本都設置爲支持Android4.4以上,這個庫也已經被JakeWharton標記爲過時。所以我們只需學習屬性動畫即可。

屬性動畫使用很簡單,例如:

ObjectAnimator.ofFloat(button, "translationX", 0f, 100f).start()

內部原理是使用ValueAnimator生成了一系列變化的值,這一點和Scroller是類似的。例如上面的代碼和下面的代碼是等價的:

val valueAnimator = ValueAnimator.ofFloat(0f, 100f)
valueAnimator.addUpdateListener {
    button.translationX = it.animatedValue as Float
}
valueAnimator.start()

由此可知,屬性動畫不受屬性限制,任何屬性都可以使用屬性動畫。常見的屬性有translationXrotationalpha,分別對應平移、旋轉、透明度動畫。

ValueAnimator可以設置動畫時長、插值器、監聽器等,例如:

// 單位是毫秒,從源碼中可以看到默認值是300ms
valueAnimator.duration = 1000
valueAnimator.interpolator = BounceInterpolator()
valueAnimator.addListener(object : AnimatorListenerAdapter() {
    override fun onAnimationRepeat(animation: Animator?) {
        super.onAnimationRepeat(animation)
    }
    override fun onAnimationEnd(animation: Animator?) {
        super.onAnimationEnd(animation)
    }
    override fun onAnimationCancel(animation: Animator?) {
        super.onAnimationCancel(animation)
    }
    override fun onAnimationPause(animation: Animator?) {
        super.onAnimationPause(animation)
    }
    override fun onAnimationStart(animation: Animator?) {
        super.onAnimationStart(animation)
    }
    override fun onAnimationResume(animation: Animator?) {
        super.onAnimationResume(animation)
    }
})

這裏使用的BounceInterpolator是Android自帶的插值器,效果是反覆彈起。Android自帶以下插值器:

  • 反覆彈起的插值器BounceInterpolator
  • 不斷加速的插值器AccelerateInterpolator
  • 不斷減速的插值器DecelerateInterpolator
  • 先加速再減速的插值器AccelerateDecelerateInterpolator
  • 先後退再前衝的插值器AnticipateInterpolator
  • 正弦曲線插值器CycleInterpolator(1f)
  • 先超過目標位置再後退插值器OvershootInterpolator
  • 勻速插值器LinearInterpolator

默認插值器是勻速插值器LinearInterpolator。如果想要自定義插值器可以查看郭神的這篇文章:Android屬性動畫完全解析(下),Interpolator和ViewPropertyAnimator的用法

AnimatorListenerAdapter和手勢監聽器的SimpleOnGestureListener類似,是一個實現了Animator.AnimatorListener,Animator.AnimatorPauseListener接口的類,使用AnimatorListenerAdapter可以僅重寫自己需要的回調,減少代碼量。

使用AnimatorSet實現組合動畫,例如:

val moveIn = ObjectAnimator.ofFloat(button, "translationX", -500f, 0f)
val rotate = ObjectAnimator.ofFloat(button, "rotation", 0f, 360f)
val fadeInOut = ObjectAnimator.ofFloat(button, "alpha", 1f, 0f, 1f)
val animSet = AnimatorSet()
animSet.play(rotate).with(fadeInOut).after(moveIn)
animSet.duration = 5000
animSet.start()

AnimatorSet這個類提供了一個play()方法,如果我們向這個方法中傳入一個Animator對象,將會返回一個AnimatorSet.Builder的實例,AnimatorSet.Builder中包括以下四個方法:

  • after(Animator anim) 將現有動畫插入到傳入的動畫之後執行
  • after(long delay) 將現有動畫延遲指定毫秒後執行
  • before(Animator anim) 將現有動畫插入到傳入的動畫之前執行
  • with(Animator anim) 將現有動畫和傳入的動畫同時執行

3.改變佈局參數

以ConstraintLayout爲例:

val set= ConstraintSet().apply { clone(button.parent as ConstraintLayout) }
set.constrainWidth(R.id.button, 500)
set.constrainHeight(R.id.button, 500)
set.applyTo(button.parent as ConstraintLayout)

七、事件分發機制

View的事件分發機制使用的是典型的責任鏈模式,可以使用以下僞代碼表示:

fun dispatchTouchEvent(event: MotionEvent): Boolean {
    var consume: Boolean
    if (onInterceptTouchEvent(event)) {
        consume = onTouchEvent(event)
    } else {
        consume = child.dispatchTouchEvent(event)
    }
    return consume
}

具體可參考筆者的另一篇文章:通俗講解 Android 事件分發機制 —— 責任鏈模式的典型應用

參考文章

《Android開發藝術探索》
Android手勢檢測——GestureDetector全面分析
Android Scroller完全解析,關於Scroller你所需知道的一切
Android屬性動畫完全解析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章