android事件分发过程
View的基础知识
View是所有控件的基类,是所有界面层的抽象,一个View可以由一个控件组成也可以由一组控件组成,ViewGroup也继承自View,由此可以得出一个View树。
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {。。。}
View的参数位置
主要是根据四个顶点来决定,top是左上纵坐标(左上角距离父容器的纵坐标大小,以此类推),right是右下横坐标,left是左上横坐标,bottom是右下纵坐标,都是相对于父容器的坐标。
可得:
width = right - left
height = bottom - top
这四个元素对应View中的getXXXX(),比如getRight();
还有几个参数(相对父容器):
-
(x ,y) 是View左上角的坐标
-
translationX,translationY,默认值为0,是View左上角相对于父容器的偏移量。
可得:
x = left + translationX
y = top + translationY
点击事件
MotionEvent
-
ACTION_DOWN 刚接触屏幕
-
ACTION_MOVE 在屏幕上移动
-
ACTION_UP 从屏幕松开的一瞬间
两种典型事件:
-
down-up
-
down - move -move -。。。。-up
通过MotionEvent对象可以得到点击事件发生的x和y坐标,getX/getY是相对于当前View左上角的,getRawX/getRawY是相对于手机屏幕左上角。
TouchSlop
系统能够识别的最小滑动距离。可以用来过滤一些滑动操作。
ViewConfiguration.get(getContext()).getScaledTouchSlop()获得。
VelocityTracker 速度追踪器
在View的onTouchEvent中追踪当前点击事件的速度。
val velocityTracker:VelocityTracker = VelocityTracker.obtain()
velocityTracker.addMovement(event)
//然后可以使用下列方法获得滑动速度,获取速度之前要先计算,1000表示这个计算的是一秒内划过的像素数量,默认从上到下,从左到右为正
velocityTracker.computeCurrentVelocity(1000)//单位是ms
velocityTracker.xVelocity
velocityTracker.yVelocity
//停止使用的时候进行回收
velocityTracker.clear()
velocityTracker.recycle()
GestureDetetor 手势检测
用于辅助判断单机、长按、滑动、双击等过程。
使用:
val gestureDetector = GestureDetector(applicationContext, @RequiresApi(Build.VERSION_CODES.M)
object : GestureDetector.OnGestureListener{
//触碰屏幕瞬间
override fun onDown(e: MotionEvent?): Boolean {
TODO("Not yet implemented")
}
//尚未松开或拖动
override fun onShowPress(e: MotionEvent?) {
TODO("Not yet implemented")
}
//轻触后松开
override fun onSingleTapUp(e: MotionEvent?): Boolean {
TODO("Not yet implemented")
}
//按下屏幕然后拖动
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
TODO("Not yet implemented")
}
//长按不放开
override fun onLongPress(e: MotionEvent?) {
TODO("Not yet implemented")
}
//按下触碰屏幕后快速滑动然后松开
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
TODO("Not yet implemented")
}
})
还可以根据需要实现双击的监听行为:
gestureDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener{
//严格的单击
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
TODO("Not yet implemented")
}
//两个连续的单击,不可与onSingleTapConfirmed共存
override fun onDoubleTap(e: MotionEvent?): Boolean {
TODO("Not yet implemented")
}
//发生了双击行为,在双击期间down,move,up都会触发该回调
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
TODO("Not yet implemented")
}
})
然后接管View中的onTouchEvent方法即可。
View#onTouchEvent
return gestureDetector.onTouchEvent(event);
建议:若只是滑动则使用onTouchEvent,若要监听双击事件则使用GestureDetector。
Scroller
使用View的scrollTo和scrollBy方法进行滑动是瞬间完成的,可以使用scroller实现过渡效果,结合View与computeScroll完成。以下是典型代码:
val scroll = Scroller(this)
public void startScroll(int startX, int startY, int dx, int dy, int duration)
//duration单位是ms
View的滑动
-
通过View本身的scrollTo/scrollBy实现。
-
通过动画给View施加平移效果实现。
-
通过改变View的LayoutParams使得View重新布局。
通过View本身的scrollTo/scrollBy实现
scrollBy实际上也是调用了scrollTo
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
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();
}
}
}
mScrollX:View的内容左边缘和和View的左边缘的距离,以像素为单位。
mScrollY: View的内容上边缘和View内容的上边缘的距离,以像素为单位。
View边缘:View的位置,四个顶点组成。
View内容边缘:View中内容的位置。
scrollBy与scrollTo只可以改变内容的位置,不可以改变view的位置。
View左边缘在View内容左边缘的右边时,mScrollX为正值。反之为负。
View上边缘在View内容上边缘的下边时,mScrollY为正值,反之为负。
即内容向左向上滑为正,向右向下滑为负。
使用动画进行滑动
对View进行移动,主要是操作View的translationX与translationY元素
采用View动画代码
如果没有设置fillAfter= true会在结束的时候弹回原来位置,并且并不可以改变View的位置信息。只会改变影像信息。
采用属性动画
3.0以上可以解决以上问题。
首先引入nineoldandroids.jar
重写onTouchEvent
class MyView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : androidx.appcompat.widget.AppCompatTextView(context,attrs,defStyleAttr) {
private val TAG = "MyView"
//记录最后一个坐标
private var mLastX: Float = 0f
private var mLastY: Float = 0f
override fun onTouchEvent(event: MotionEvent?): Boolean {
val x = event?.rawX
val y = event?.rawY
when(event?.action){
MotionEvent.ACTION_DOWN ->{
Log.d(TAG, "onTouchEvent: ACTION_DOWN")
}
MotionEvent.ACTION_UP ->{
Log.d(TAG, "onTouchEvent: ACTION_UP")
}
MotionEvent.ACTION_MOVE ->{
val deltaX = x?.minus(mLastX)
val deltaY = y?.minus(mLastY)
//需要偏移到的位置
val translationX = ViewHelper.getTranslationX(this) + deltaX!!
val translationY = ViewHelper.getTranslationY(this) + deltaY!!
ViewHelper.setTranslationX(this, translationX)
ViewHelper.setTranslationY(this, translationY)
}
else ->{
}
}
if (y != null) {
mLastY = y
}
if (x != null) {
mLastX = x
}
return true
}
}
改变布局参数
改变布局参数,比如在旁边设置一个空的View然后增加其大小。
View的弹性滑动
Scroll
-
设置duration,在整个时间段内缓慢移动
//这个方法会根据时间的流逝计算出当前要滑动到的距离
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {