《Android 开发艺术探索》笔记:(3)View 的事件体系

28 篇文章 0 订阅
3 篇文章 0 订阅

一、View 基础知识

View 是 Android 中所有控件的基类,ViewGroup 也继承了 View。
Android 中,x 轴和 y 轴的正方向分别为右和下。

1 位置参数(相对于父容器)

(left, top): View 左上角原始坐标
(right, bottom): View 右下角原始坐标(margin 会影响 left/top/right/bottom)
(x, y): View 左上角最终坐标
translationX: View 左上角横向偏移量
translationY: View 左上角纵向偏移量
x = left + translationX
y = top + translationY (setX/Y() 时其实就是改变 translationX/Y 的值)
width = right - left
height = bottom - top

1.1 MotionEvent 和 TouchSlop

1.1.1 MotionEvent

典型事件:ACTION_DOWN, ACTION_MOVE,ACTION_UP
意思也很容易理解,分别是落,动,起

一次触摸会触发一系列事件:

  • 点击屏幕后离开松开:DOWN -> UP
  • 点击屏幕滑动再松开:DOWN -> MOVE ->…-> MOVE -> UP

通过 MotionEvent 获得点击事件的坐标:

  • getX / getY : 相对于当前 View 左上角的 x 和 y 坐标
  • getRawX / getRawY:相对于手机屏幕左上角的 x 和 y 坐标
  • (View.getX / getY 获得的是相对于父容器的 x 和 y 坐标)

1.1.2 TouchSlop

滑动的最小距离,若没达到,则不认为是滑动,默认 8dp。

1.2 VelocityTracker、GestureDetector 和 Scroller

1.2.1 VelocityTracker

速度追踪,用于追踪手指在滑动过程中的速度。

在 View 的 onTouchEvent 方法中:

VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);// 1000ms内划过的像素数
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

回收:

velocityTracker.clear();
velocityTracker.recycle();

1.2.2 GestureDetector

手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
一般监听滑动相关,在 onTouchEvent 中自己实现,若是监听双击,则使用 GestureDetector。

1.2.3 Scroller

弹性滑动对象,用于实现 View 的弹性滑动,即有过过渡效果的滑动,与 View 的 computeScroll 方法配合使用。(下面会详细介绍)

二、View 的滑动

三种方式:

  • scrollTo/scrollBy
    - 操作简单,适合对 View 内容的滑动
    - 典型应用:ScrollView 的滑动
  • 动画
    - 操作简单,适合没有交互的 View 和实现复杂的动画效果
  • 改变 LayoutParams
    - 操作稍复杂,适合有交互的 View

三、弹性滑动

3.1 Scroller

典型用法:

private Scroller mScroller = new Scroller(context);

public void smoothScrollTo(int destX, int destY) {
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    mScroller.forceFinished(true);
    mScroller.startScroll(scrollX, 0, delta, 0, 5000);
    invalidate();
}

@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        scrollBy(mScroller.getCurrX(), mScroller.getCurrY());
        postInvalidate();
    }
}

当我们调用 startScroll() 时,Scroller 内部其实什么也没做,只是保存了我们传递的几个参数。所以仅仅调用 startScroll() 是不会产生滑动的,因为它内部没有做与滑动相关的事,Scroller 产生滑动,其实是因为 invalidate()。
invalidate() 会导致 View 重绘,在 View 的 Draw 方法中会调用 computeScroll(),computeScroll() 在 View 中是一个空实现,所以我们要自己实现,正如上所示。

过程:

invalidate() -->
View 重绘, draw() -->
computeScroll() ==>
scrollTo() -->
postInvalidate() 再次重绘 -->

computeScrollOffset() 会根据时间流逝算出当前的 ScrollX/Y,返回 true 即表示滑动还没有结束,继续滑动。

3.2 动画

动画本身就是一种渐进的过程,所以利用动画天然就具有弹性效果。
另外通过动画,也可以实现类似 Scroller 对 View 的弹性滑动。

3.3 使用延时策略

核心思想是通过发送一系列延时消息从而达到一种渐进式的效果,具体来说可以使用 Handler 或 View 的 postDelayed 方法,也可以使用线程的 sleep 方法。

四、View 的事件分发机制

4.1 传递过程

"main@11086" prio=5 tid=0x2 nid=NA runnable
  java.lang.Thread.State: RUNNABLE
      // ViewGroup
      at com.gdeer.gdtesthub.touchevent.deliver.MyTextView.dispatchTouchEvent(MyTextView.java:25)
      at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
      at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
      at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
      at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
      at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
      at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
      at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)      
      at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)

      // DecorView.superDispatchTouchEvent
      at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:440)
      at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1830)
      
      // Activity
      at android.app.Activity.dispatchTouchEvent(Activity.java:3400)
      at com.gdeer.gdtesthub.touchevent.deliver.TouchDeliverActivity.dispatchTouchEvent(TouchDeliverActivity.kt:18)
      
      // DecorView.dispatchTouchEvent
      at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:398)
      at android.view.View.dispatchPointerEvent(View.java:12752)
      
      // ViewRootImpl
      at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5106)
      at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4909)
      at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
      at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4479)
      at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4445)
      at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4585)
      at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4453)
      at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4642)
      at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
      at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4479)
      at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4445)
      at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4453)
      at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
      at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7092)
      at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7061)
      at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7022)

      // InputEventReceiver      
      at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7195)
      at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:186)

      // Looper
      at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-1)
      at android.os.MessageQueue.next(MessageQueue.java:326)
      at android.os.Looper.loop(Looper.java:160)
      at android.app.ActivityThread.main(ActivityThread.java:6669)
      at java.lang.reflect.Method.invoke(Method.java:-1)
      at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

Looper -->
InputEventReceiver -->
ViewRootImpl -->
DecorView.dispatchTouchEvent -->
Activity -->
DecorView.superDispatchTouchEvent -->
ViewGroup

InputEventReceiver 收到事件后转给 ViewRootImpl 处理;ViewRootImpl 传给 DecorView;DecorView 通过 dispatchTouchEvent 将事件传给 Activity(dispatchTouchEvent 会调用 window.callback,Activity 就是一个 window.callback);Activity 再传给 DecorView,接着 DecorView 就按照事件分发机制去分发事件(superDispatchTouchEvent,即 ViewGroup 的事件分发机制)。

如果一个 View 的 onTouchEvent 返回 false,那么它的父容器的 onTouchEvent 就会被调用,若全部 View 都不处理这个事件,那这个事件将最终传递给 Activity 来处理,即 Activity 的 onTouchEvent 会被调用。

如果设置了 OnTouchListener,onTouch 会先于 onTouchEvent 执行:

onTouchListener > onTouchEvent > onClickListener

4.2 伪代码

处理 down 事件:

boolean dispatchTouchEvent(MotionEvent ev) {
	// 决定自己是否拦截
	boolean handled = false;
	boolean intercepted = onInterceptTouchEvent(ev);
	// 传递给子view
	if (!intercepted) {
		handled = child.dispatchTouchEvent();
	}
	// 自己收尾
	if (!handled) {
		handled = onTouchEvent();
	}
	return handled;
}

处理 move 事件:

boolean dispatchTouchEvent(MotionEvent ev) {
	// 决定自己是否拦截
	boolean handled = false;
	boolean intercepted = false;
	if (mFirstTouchTarget == null) {
		intercepted = true;
	} else if (disallowIntercept) {
		intercepted = false;
	} else {
		intercepted = onInterceptTouchEvent(ev);
	}
	// 传递给子view
	if (!intercepted) {
		handled = child.dispatchTouchEvent();
	}
	// 自己不收尾
	return handled;
}

4.3 源码分析

4.3.1 ViewGroup 的 dispatchTouchEvent

一些概念:

  • 是否允许拦截:FLAG_DISALLOW_INTERCEPT 标记位是否为 true
  • 是否拦截:onInterceptTouchEvent 返回结果
  • 是否处理:onTouchEvent 是否调用
  • 是否处理成功:onTouchEvent 返回结果
4.3.1.1 决定自己是否拦截
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
	......
    // Handle an initial down.
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Throw away all previous state when starting a new touch gesture.
        // The framework may have dropped the up or cancel event for the previous gesture
        // due to an app switch, ANR, or some other state change.
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }

    // Check for interception.
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }
    ......
}
  • mFirstTouchTarget 会在 ViewGroup 的子 view 处理触摸事件成功后赋值,mFirstTouchTarget != null 说明自己的子 view 处理成功过事件(onTouchEvent 返回了 true)。
  • mGroupFlags 默认为 0,FLAG_DISALLOW_INTERCEPT 为 0x80000。FLAG_DISALLOW_INTERCEPT 会在执行过 requestDisallowInterceptTouchEvent(true) 后,被添加到 mGroupFlags 中。
    (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 即表示 mGroupFlags 拥有 FLAG_DISALLOW_INTERCEPT 这个 FLAG。

则上述代码的含义为,当要处理的事件是 down 事件,或自己的子 view 处理成功过之前的事件,则会去判断是否允许拦截,如果允许拦截,则去判断是否拦截。
如果不是 down 事件,且自己的 子 view 没有处理成功过之前的事件,那就不用判断了,直接决定自己拦截。

4.3.1.2 传递给子 view
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
	......
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
		......
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        	......
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }
		......
    }
    ......
}

这段代码,将事件传递给子 view。

dispatchTransformedTouchEvent 中会执行 child.dispatchTouchEvent

如果 dispatchTransformedTouchEvent 返回了 true,说明子 view 成功处理了该事件,在 addTouchTarget 中,会给 mFirstTouchTarget 赋值。

4.3.1.3 自己收尾
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
	......
	// Dispatch to touch targets.
	 if (mFirstTouchTarget == null) {
	     // No touch targets so treat this as an ordinary view.
	     handled = dispatchTransformedTouchEvent(ev, canceled, null,
	             TouchTarget.ALL_POINTER_IDS);
	 } else {
	     // Dispatch to touch targets, excluding the new touch target if we already
	     // dispatched to it.  Cancel touch targets if necessary.
	     ......
	 }
	 ......
	 return handled;
 }

如果子 view 没有处理成功,即 mFirstTouchTarget == null,那会在 dispatchTransformedTouchEvent 中调用自己的 onTouchEvent 方法。

4.3.2 View 的 dispatchTouchEvent

view 没有 onInterceptTouchEvent 方法,dispatchTouchEvent 里会直接调用 onTouchEvent 方法。

public boolean dispatchTouchEvent(MotionEvent event) {
	......
    boolean result = false;
	......
    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
        result = true;
    }
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }

    if (!result && onTouchEvent(event)) {
        result = true;
    }
	......
    return result;
}

由上可以看出,在 view enabled 的时候,OnTouchListener.onTouch 会先于 onTouchEvent 调用,如果 onTouch 返回了 true,那 onTouchEvent 就不会调用。

在 view disabled 的时候,OnTouchListener.onTouch 不会调用,但 onTouchEvent 还是会调用。

五、View 的滑动冲突

常见的三种滑动冲突场景:

  1. 外部滑动与内部滑动的方向不一致
  2. 外部滑动与内部滑动的方向一致
  3. 以上两种的嵌套

这里写图片描述

5.1 滑动冲突的处理规则

对场景一:根据滑动是水平滑动还是竖直滑动来判断到底是由谁来拦截事件。由滑动轨迹的起终点的坐标即可判断为水平还是竖直(距离差、角度、速度差等)。

对场景二:根据业务需求具体分析。

对场景三:以上两者的混合。

5.2 滑动冲突的解决方式

5.2.1 外部拦截法

指事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截。需重写父容器的 onInterceptTouchEvent 方法。

伪代码:

public boolean onInterceptTouchEvent(MotionEvent event){
	boolean intercepted = false;
	switch(event.getAction()){
		case MotionEvent.ACTION_DOWN:
			intercepted = false;
			break;
		case MotionEvent.ACTION_MOVE:
			if(父容器需要当前点击事件){
				intercepted = true;
			} else {
				intercepted = false;
			}
			break;
		case MotionEvent.ACTION_UP:
			intercepted = false;
			break;
		default:
			break;
	}
	return intercepted;
}
  • 对于 ACTION_DOWN 事件,父容器必须返回 false,因为一旦拦截了 ACTION_DOWN,后续的 ACTION_MOVE,ACTION_UP 都会交给父容器处理。
  • 对于 ACTION_UP 事件,父容器也返回 false,一旦拦截,子元素的 onClick 事件便无法触发。父容器(ViewGroup)比较特殊,一旦开始拦截(ACTION_DOWN 的 onTouchEvent 返回 true),那么后续事件也都会交给它处理(并且 onInterceptTouchEvent 不会再被调用),而 ACTION_UP 作为最后一个事件也一定能够传到父容器(因为 onInterceptTouchEvent 不会被调用,所以返回 false 也就不起作用)。

5.2.2 内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,需要配合 requestDisallowInterceptTouchEvent 方法来设置(控制 FLAG_DISALLOW_INTERCEPT 标志位,设置后 ViewGroup 将无法拦截除了 ACTION_DOWN 以外的点击事件,ACTION_DOWN 不受此标志位约束),需重写子元素的 dispatchTouchEvent 方法(子元素,view 无 onInterceptTouchEvent 方法)。

子元素:

public boolean dispatchTouchEvent(MotionEvent event){
	switch(event.getAction()){
		case MotionEvent.ACTION_DOWN:
			parent.requestDisallowInterceptTouchEvent(true);
		case MotionEvent.ACTION_MOVE:
			if(父容器需要当前点击事件){
				parent.requestDisallowInterceptTouchEvent(false);
			}
			break;
		case MotionEvent.ACTION_UP:
			break;
		default:
			break;
	}
	return super.dispatchTouchEvent(event);
}

因为 ACTION_DOWN 不受 requestDisallowInterceptTouchEvent 方法影响,父元素也要做相应处理:

public boolean onInterceptTouchEvent(MotionEvent event){
	int action = event.getAction();
	if (action == ACTION_DOWN){
		return false;
	} else {
		return true;
	}
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值