Android源码角度分析事件分发消费之应用篇
自定义简单ViewPager
首先我们做一个简单的ViewPager,废话不说了,上代码。java 代码
public class ScrollerLayout extends ViewGroup {
private final int mTouchSlop;
private Scroller mScroller;
private int leftBorder;
private int rightBorder;
private int downX;
private int lastMoveX;
private int moveX;
public ScrollerLayout(Context context) {
this(context,null);
}
public ScrollerLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public ScrollerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
//这个地方是获取系统默认判定为滑动的最小值
ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//为测量每个子控件的大小
int childCount = getChildCount();
for(int i =0;i<childCount;i++){
measureChild(getChildAt(i),widthMeasureSpec,heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if(!changed){
return;
}
int childCount = getChildCount();
for(int i = 0;i<childCount;i++){
View child = getChildAt(i);
child.layout(i*child.getMeasuredWidth(),0,(i+1)*child.getMeasuredWidth(),child.getMeasuredHeight());
}
leftBorder = getChildAt(0).getLeft();
rightBorder = getChildAt(getChildCount() - 1).getRight();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
downX = (int) ev.getX();
lastMoveX = downX;
break;
case MotionEvent.ACTION_MOVE:
moveX = (int) ev.getX();
float diff = Math.abs(moveX - downX);
lastMoveX = moveX;
// 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件!
if (diff > mTouchSlop) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = (int) event.getX();
lastMoveX = downX;
case MotionEvent.ACTION_MOVE:
//etX()是表示Widget相对于自身左上角的x坐标,而getRawX()是表示相对于屏幕左上角的x坐标值
moveX = (int) event.getRawX();
int scrolledX = (lastMoveX - moveX);
if (getScrollX() + scrolledX < leftBorder) {
scrollTo(leftBorder, 0);
return true;
} else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
scrollTo(rightBorder - getWidth(), 0);
return true;
}
scrollBy(scrolledX, 0);
lastMoveX = moveX;
break;
case MotionEvent.ACTION_UP:
// 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
int dx = targetIndex * getWidth() - getScrollX();
// 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
mScroller.startScroll(getScrollX(), 0, dx, 0);
invalidate();
break;
}
return super.onTouchEvent(event);
Flag2: //return true;
}
@Override
public void computeScroll() {
// 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
}
xml 布局
<com.weshape3d.customviews.mycostomviews.ScrollerLayout
android:layout_width="match_parent"
android:layout_height="50dp">
<TextView
android:gravity="center"
android:textColor="#ff0000"
android:text="1111"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:gravity="center"
android:textColor="#222"
android:text="USD发hi爱的是否"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:gravity="center"
android:textColor="#ff0000"
android:text="333"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.weshape3d.customviews.mycostomviews.ScrollerLayout>
主要实现的逻辑地方我加了注释,自定义ViewGroup中布局、测量不是本篇重点。 主要看根据手势滑动简单的实现的逻辑:
- 当按下手势触发的时候,记录下按下的X点不做拦截
- 当滑动且大于等于预先设定的判定滑动的值的时候在onIntercept()方法中拦截手势
- 拦截后会交给他自身的OnTouchEvent去处理 利用View 自身的scrollBy()方法做滑动
问题
大家会发现把这段代码运行之后,会有左右滑不动!!原因
原因很简单因为ACTION_DOWN手势按下的时候,没有找到消费它的目标就导致后续的ACTION_MOVE 和ACTION_UP等事件得不到分发了!解决
- 在布局中把TextView换成Button;这样之所以可以是因为Button控件默认是CLICKABLE的,这就导致当ACITION传递过来的时候,就被消费了,致使后续的事件也会传递过来,这样在onInterceptTouchEvent()方法中当符合滑动条件的时候,我们就把事件拦截,在onTouchEvent()方法中让ViewGroup做滑动。
- 代码中Flag2处,让ViewGroup的onTouchEvent,返回true。
RefreshSwipLayout怎么进行事件消费和分发
Now,满心欢喜的打开开RefreshSwipLayout源码
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = MotionEventCompat.getActionMasked(ev);
int pointerIndex;
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
if (!isEnabled() || mReturningToStart || canChildScrollUp()
|| mRefreshing || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
mActivePointerId = ev.getPointerId(0);
mIsBeingDragged = false;
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
mInitialDownY = ev.getY(pointerIndex);
break;
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return false;
}
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
final float y = ev.getY(pointerIndex);
startDragging(y);
break;
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
break;
}
return mIsBeingDragged;
}
咦?跟预想的不太一样!ACTION_POINTER_UP ACTION_POINTER_DOWN是什么鬼很高级的样子!先稳住。
- ACTION_POINTER_UP,ACTION_POINTER_DOWN表示第一个手指之外的其他手指按下和抬起的时候,触发的。
- 可以按这么多手机,我怎么知道哪是一个?有下面这些方法,有了这些就可以轻松的知道哪个手指了
//获取触控所属手指的位置,也就是按时间先后放在屏幕上排序第几个
int indext = event.getActionIndex();
//根据位置获取手指ID
int pointID = event.getPointerId(index);
//根据ID获取是第几个
int index = event.findPointerIndex(indext);
- 获取位置的时候
//获取第几个手指x
float x = ev.getX(pointerIndex);
看透他的实质,拆穿他的本质之前,我们先拎着几个问题:
- 像我们上面自定义ViewPager一样遇到的问题,RefreshSwipLayout是怎么解决的?
- 两个手指多个手势滑动的时候,怎么处理?(多点触控)
SwipeRefreshLayout中的onInterceptTouchEvent
看源码,必要的地方加了注释和删减
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();ensureTarget();
//获取多点触控的动作
final int action = MotionEventCompat.getActionMasked(ev);
int pointerIndex;
switch (action) {
case MotionEvent.ACTION_DOWN:
/**
第一个手指按下时,获取他的pointID。
pointID是从触摸点在整个从按下到抬起过程中,唯一不变的id
**/
mActivePointerId = ev.getPointerId(0);
mIsBeingDragged = false;
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
//记录下按下的位置
mInitialDownY = ev.getY(pointerIndex);
break;
case MotionEvent.ACTION_MOVE:
//找到要跟踪的触控点pointerIndex,用它来获取位置
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
final float y = ev.getY(pointerIndex);
//滑动处理
startDragging(y);
break;
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
break;
}
//返回是否拦截
return mIsBeingDragged;
}
private void startDragging(float y) {
final float yDiff = y - mInitialDownY;
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;
mIsBeingDragged = true;
mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
}
}
小结
- 化繁就简,在onInterceptTouchEvent()中做的主要的事情就是在按下的时候记录住触控点的ID也就是第一个触控点的ID并把这个点的Y坐标也记下来。
- 在MOVE中找出找出我们记录的触控点,并把他当前的Y坐标找出来,交给startDragging()判断要不要拦截
SwipeRefreshLayou的OnTouchEvent
public boolean onTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
int pointerIndex = -1;
switch (action) {
case MotionEvent.ACTION_DOWN:
//记录下要用的触控点的ID
mActivePointerId = ev.getPointerId(0);
mIsBeingDragged = false;
break;
case MotionEvent.ACTION_MOVE: {
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
return false;
}
final float y = ev.getY(pointerIndex);
startDragging(y);
if (mIsBeingDragged) {
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
if (overscrollTop > 0) {
//滑动圆圈
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
//当其他手指按下的时候,就把使用的触控点换成当前按下的这个
pointerIndex = MotionEventCompat.getActionIndex(ev);
if (pointerIndex < 0) {
Log.e(LOG_TAG,
"Got ACTION_POINTER_DOWN event but have an invalid action index.");
return false;
}
mActivePointerId = ev.getPointerId(pointerIndex);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP: {
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
return false;
}
//如果控件的状态为拖动过程中,返回初始状态
if (mIsBeingDragged) {
final float y = ev.getY(pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
mIsBeingDragged = false;
finishSpinner(overscrollTop);
}
mActivePointerId = INVALID_POINTER;
return false;
}
case MotionEvent.ACTION_CANCEL:
return false;
}
//默认消费这个事件
return true;
}
我认为必要的地方,都在上面做了注释。SwipRefreshLayout在onTouchEvent方法中做的主要工作简单的说主要三点
转载自:https://juejin.cn/post/6844903498333552647