文章目錄
一、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
方法。我們重寫了這個方法,在這個方法中調用computeScrollOffset
,computeScrollOffset
方法會根據插值器和時間間隔獲取當前變化的值。如果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()
由此可知,屬性動畫不受屬性限制,任何屬性都可以使用屬性動畫。常見的屬性有translationX
、rotation
、alpha
,分別對應平移、旋轉、透明度動畫。
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屬性動畫完全解析