Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺

这篇具有很好参考价值的文章主要介绍了Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

我们看到内部又调用了父类 dispatchTouchEvent 方法, 所以最终是交给 ViewGroup 顶级 View 来处理分发了。

  1. 顶级 View 对点击事件的分发过程

在上一小节中我们知道了一个事件的传递流程,这里我们就大致在回顾一下。首先点击事件到达顶级 ViewGroup 之后,会调用自身的 dispatchTouchEvent 方法,之后如果自身的拦截方法 onInterceptTouchEvent 返回 true ,则事件不会继续下发给子类,如果自身设置了 mOnTouchListener 监听,则 onTouch 会被调用,否则 onTouchEvent 会被调用,如果 onTouchEvent 中设置了 mOnClickListener 那么 onClick 会调用。如果 ViewGroup 的 onInterceptTouchEvent 返回 false,则事件会传递到所点击的子 View 中,这时子 View 的 dispatchTouchEvent 会被调用。到此为止,事件已经从顶级 View 传递给了下一层 View ,接下来的传递过程和顶级 ViewGroup 一样,如此循环就完成了整个事件的分发。

在该小节的第一点中我们知道,在 DecorView 中的 superDispatchTouchEvent 方法内部调用了父类的 dispatchTouchEvent 方法,我们看它的实现,代码如下:

//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

if (actionMasked == MotionEvent.ACTION_DOWN) {
//这里主要是在新事件开始时处理完上一个事件
cancelAndClearTouchTargets(ev);
resetTouchState();
}

/** 检查事件拦截,表示事件是否拦截*/
final boolean intercepted;
/**

  • 1. 判断当前是否是按下
    */
    if (actionMasked == MotionEvent.ACTION_DOWN
    || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    //2. 子类可以通过 requestDisallowInterceptTouchEvent 方法来设置父类不要拦截
    if (!disallowIntercept) {
    //3
    intercepted = onInterceptTouchEvent(ev);
    //恢复事件防止其改变
    ev.setAction(action);
    } else {
    intercepted = false;
    }
    } else {
    intercepted = true;
    }

    }

从上面代码我们可以看出如果 actionMasked == MotionEvent.ACTION_DOWN 或者 mFirstTouchTarget != null 成立的话会执行注释 2 的判断(mFirstTouchTarget 的意思如果当前事件被子类消费了,就不成立,后面会提高),disallowIntercept 可以在子类中通过调用父类的 requestDisallowInterceptTouchEvent(true) 请求父类不要拦截分发事件,也就是阻止执行注释 3 的拦截子类接收按下的事件,反之执行 onInterceptTouchEvent(ev); 如果返回 true 说明拦截了事件 。

上面介绍了注释 1,2,3 onInterceptTouchEvent 返回 true 的情况,说明拦截了事件,下面我们来讲解 intercepted = false 当前 ViewGroup 不拦截事件的时候,事件会下发给它的子 View 进行处理,下面看子 View 处理的源码,代码如下:

//ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {

if (!canceled && !intercepted) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i–) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}

if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}

newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}

resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}

}


}

上面这段代码也很好理解,首先遍历 ViewGroup 子孩子,然后判断子元素是否在播放动画和点击事件是否落在了子元素的区域内。如果某个子元素满足这 2 个条件,那么事件就会传递给该子类来处理,可以看到 ,dispatchTransformedTouchEvent 实际上调用的就是子类的 dispatchTouchEvent 方法,在它的内部有如下一段内容,而在上面的代码中 child 传递不是 null ,因此它会直接调用子元素的 dispatchTouchEvent 方法,这样事件就交由子元素处理,从而完成了一轮事件分发。

//ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {

if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}

}

这里如果 child.dispatchTouchEvent(event) 返回 true , 那么 mFirstTouchTarget 就会被赋值同时跳出 for 循环,如下所示:

//ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;

}

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
//这个时候 mFirstTouchTarget 就代表子 View 成功处理了事件
mFirstTouchTarget = target;
return target;
}

这几行代码完成了 mFirstTouchTarget 的赋值并终止了对子元素的遍历。如果子元素的 dispatchTouchEvent 返回 false ,ViewGroup 会继续遍历进行事件分发给下一个子元素。

如果遍历所有的子元素后事件都没有被处理的时候,那么 ViewGroup 就会自己处理点击事件,这里包含 2 种情况下 ViewGroup 会自己处理事件 (其一: ViewGroup 没有子元素,其二:子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了false,这一般是在子元素的 onTouchEvent 中返回了 false )

代码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {

if (mFirstTouchTarget == null) {

handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}

}
复制代码

可以看到如果 mFirstTouchTarget == null 的时候,那么就是代表 ViewGroup 的子 View 没有被消费点击事件,将调用自身的 dispatchTransformedTouchEvent 方法。注意上面这段代码这里的第三个参数 child 为 null ,从前面的分析可以知道,它会调用 super.dispatchTouchEvent(event) ,显然,这里就会调用父类 View 的 dispatchTouchEvent 方法,即点击事件开始交由 View 处理,请看下面的分析:

  1. View 对点击事件的处理过程

其实 View 对点击事件的处理过程稍微简单一些,注意这里的 View 不包含 ViewGroup 。先看它的 dispatchTouchEvent 方法,代码如下:

//View.java
public boolean dispatchTouchEvent(MotionEvent event) {

if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}

ListenerInfo li = mListenerInfo;
//1.
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//2.
if (!result && onTouchEvent(event)) {
result = true;
}
}

return result;
}

View 中的事件处理逻辑比较简单,我们先看注释 1 处,如果我们外部设置了 mOnTouchListener 点击事件,那么就会执行 onTouch 回调,如果该回调的返回值为 false ,那么才会执行 onTouchEvent 方法,可见onTouchListener 优先级高于 onTouchEvent 方法,下面我们来分析 onTouchEvent 方法实现,代码如下:

//View.java
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

/**

  • 1. View 处于不可用状态下的点击事件的处理过程
    */
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
    setPressed(false);
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    // A disabled view that is clickable still consumes the touch
    // events, it just doesn’t respond to them.
    return clickable;
    }

/**

  • 2. 如果 View 设置了代理,那么还会执行 TouchDelegate 的 onTouchEvent 方法。
    */
    if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
    return true;
    }
    }

/**

  • 3. 如果 clickable 或 (viewFlags & TOOLTIP) == TOOLTIP 有一个成立那么就会处理该事件
    */
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
    case MotionEvent.ACTION_UP:
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    if ((viewFlags & TOOLTIP) == TOOLTIP) {
    handleTooltipUp();
    }
    if (!clickable) {
    removeTapCallback();
    removeLongPressCallback();
    mInContextButtonPress = false;
    mHasPerformedLongPress = false;
    mIgnoreNextUpEvent = false;
    break;
    }
    // 用于识别快速按下
    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {

boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}

if (prepressed) {
setPressed(true, x, y);
}

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
/**

  • 如果设置了点击事件 mOnClickListener 就会执行内部回调
    */
    if (!post(mPerformClick)) {
    performClick();
    }
    }
    }

if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}

if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}

removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;

case MotionEvent.ACTION_DOWN:

//判断是否是在滚动容器中
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
//发送一个延迟执行长按事件的操作
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;

case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
//移除一些回调比如长按事件
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;

case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
if (!pointInView(x, y, mTouchSlop)) {
//移除一些回调比如长按事件
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}

return true;
}

return false;
}

上面代码虽然比较多,但是逻辑还是很清楚的,我们来分析一下

  1. 判断 View 是否处于不可用的状态下使用,返回一个 clickable 。
  2. 判断 View 是否设置了代理,如果设置了代理将会执行 代理的 onTouchEvent 方法。
  3. 如果 clickable 或 (viewFlags & TOOLTIP) == TOOLTIP 有一个成立那么就会处理 MotionEvent 事件。
  4. 在 MotionEvent 事件中分别会在 up 和 down 中会执行点击 onClick 和 onLongClick 回调。

到这里点击事件的分发机制源码实现已经分析完了,结合之前分析的传递规则和下面这张图,然后结合源码相信你应该理解了事件分发跟事件处理机制了。

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

滑动冲突

本小节将介绍 View 体系中一个非常重要的知识点滑动冲突,相信在开发中特别是做一些滑动效果处理的时候而且还不止一层滑动,又的是嵌套好几层的滑动,那么它们之间如果不解决滑动冲突必定是不可行的,下面我们先来看看造成滑动冲突的场景。

滑动冲突场景及处理规则

1. 外部滑动方向和内部滑动方向不一致

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

主要是将 ViewPager 和 Fragment 配合使用所组成的页面滑动效果,主流应用几乎都会使用这个效果。在这种效果中,可以通过左右滑动来切换页面,而每个页面内部往往是一个 RecyclerView 。本来这种情况下是有滑动冲突的,但是 ViewPager 内部处理了这种滑动冲突,因此采用 ViewPager 时我们无须关注这个问题,但是如果我们采用的是 ScrollView 等滑动控件,那就必须手动处理滑动冲突了,否则造成的后果就是内外两层只能由一层能够滑动,这是因为两者之间的滑动事件有冲突。

它的处理规则是:

当用户左右滑动时,需要让外部的 View 拦截点击事件,当用户上下滑动的时候,需要让内部的 View 拦截点击事件。这个时候我们就可以根据他们的特征来解决滑动冲突。具体来说就是可以通过判断滑动手势是水平方向还是竖直方向具体来对应拦截事件。

2. 外部滑动方向和内部滑动方向一致

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

这种情况就稍微复杂一些,当内外两层都在同一个方向可以滑动的时候,显然存在逻辑问题。因为当手指开始滑动的时候,系统无法知道用户到底是想让那一层滑动,所以当手指滑动的时候就会出现问题,要么只有一层能滑动,要么就是内外两层都滑动得很卡顿。在实际的开发中,这种场景主要是指内外两层同时能上下滑动或者内外两层同时能左右滑动。

它的处理规则是:

这种事比较特殊的,因为它无法根据滑动的角度、距离差以及速度差来做判断,但是这个时候一般都能在业务上找到突破点,比如业务有规定,当处理某种状态的时候需要外部 View 响应用户的滑动,而处于另外一种状态时则需要内部 View 来响应 View 的滑动,根据这种业务上的需求我们也能得出相应的处理规则,有了处理规则同样可以进行下一步处理。这种场景通过文字描述可能比较抽象,在下一小节中我们会通过实际例子来演示这种情况。

3. 1 + 2 场景的嵌套

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

场景三是场景一和场景二两种情况的嵌套,因此场景三的滑动冲突看起来就更加复杂了。比如在许多应用中会有这么一个效果:内层有一个场景 1 中的滑动效果,然后外层又有一个场景 2 中的滑动效果。虽然说场景三的滑动冲突看起来是比较复杂的,但是它是几个单一的滑动冲突的叠加,所以只需要分别处理内中外层之间的冲突就行了,处理方式跟场景 1 和 2 一致。

下面我们就来看一下滑动冲突的处理规则。

它的处理规则是:

它的滑动规则就更复杂了,和场景 2 一样,它也无法直接根据滑动的角度、距离以及速度差来做判断,同样还是只能从业务员上找到突破点,具体方法和场景 2 一样,都是从业务的需求上得出相应的处理规则,在下一节中同样会给出代码示例来进行演示。

滑动冲突的解决方式

上面说过针对场景 1 中的滑动,我们可以根据滑动的距离差来进行判断,这个距离差就是所谓的滑动规则。如果用 ViewPager 去实现场景 1 中的效果,我们不需要手动处理滑动冲突,因为 ViewPager 已经帮我们做了,但是这里为了更好的演示滑动冲突解决思想,没有采用 ViewPager 。其实在滑动过程中得到滑动的角度这个是相当简单的,但是到底要怎么做才能将点击事件交给合适的 View 去处理呢?这时就要用到 3.4 节所讲述的事件分发机制了。针对滑动冲突,这里给出 2 种解决滑动冲突的方式,外部拦截和内部拦截发。

  1. 外部拦截法

所谓外部拦截就是指点击事件先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写 onInterceptTouchEvent方法,在内部做响应的拦截即可,可以参考下面代码:

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
isIntercepted = false
}
MotionEvent.ACTION_MOVE -> {
//拦截子类的移动事件
if (true) {
println(“事件分发机制开始分发 ----> 拦截子类的移动事件 onInterceptTouchEvent”)
isIntercepted = true
} else {
isIntercepted = false
}

}
MotionEvent.ACTION_UP -> {
isIntercepted = false
}
}
return isIntercepted
}

上述代码是外部拦截的典型逻辑,针对不同的滑动冲突只需要修改父容器需要当前点击事件这个条件即可,其它均不做修改也不能修改。这里对上述代码再描述一下,在 onInterceptTouchEvent 方法中,首先是 ACTION_DOWN 这个事件,父容器必须返回 false 。既不拦截 ACTION_DOWN 事件,这是因为一旦父容器拦截了 ACTION_DOWN , 这是因为一旦父容器拦截 ACTION_DOWN, 那么后续的 ACTION_DOWN, 那么后续的 ACTION_MOVE 和 ACTION_UP 事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了;其次是 ACTION_MOVE 事件,这个事件可以根据需要来决定是否拦截,如果是 ACTION_UP 事件,这里必须要返回 false , 因为 ACTION_UP 事件本身没有太多意义。

考虑一种情况,假设事件交由子元素处理,如果父容器在 ACTION_UP 时返回了 true ,就会导致子元素无法接收到 ACTION_UP 事件,这个时候子元素中的 onClick 事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而 ACTION_UP 作为最后一个事件也必定可以传递给父容器,即便父容器的 onInterceptTouchEvent 方法在 ACTION_UP 时返回了 false.

  1. 内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和 Android 中的事件分发机制不一致,在讲解源码的时候,我们讲解了 ,可以通过 requestDisalloWInterceptTouchEvent 方法才能正常工作,使用起来较外部拦截法稍显复杂,我们需要重写子元素的 dispatchTouchEvent 方法

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
println(“事件分发机制开始分发 ----> 子View dispatchTouchEvent ACTION_DOWN”)
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
println(“事件分发机制开始分发 ----> 子View dispatchTouchEvent ACTION_MOVE”)
if (true){
parent.requestDisallowInterceptTouchEvent(false)
}
}
MotionEvent.ACTION_UP -> {
println(“事件分发机制开始分发 ----> 子View dispatchTouchEvent ACTION_UP”)
}
}
return super.dispatchTouchEvent(event)
}

上述代码是内部拦截法的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其它不需要做改动而且也不能有改动,除了子元素需要做处理以外,父元素也要默认拦截除了 ACTION_DOWN 以外的其它事件,这样当子元素调用 parent.requestDisallowInterceptTouchEvent(false) ,父元素才能继续拦截所需的事件。

下面就以实战的 demo 具体来说明一下。

实战

场景一 滑动冲突案例

我们自定义一个 ViewPager + RecyclerView 包含左右 + 上下滑动,这样就满足了我们场景一的滑动冲突,我们先来看一下完整的效果图:

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

上面录屏的效果解决了上下滑动跟左右滑动冲突,实现方式就是自定义 ViewGroup 利用 Scroller 达到像 ViewPager 一样丝滑般的感觉 ,然后内部添加了 3 个 RecyclerView 。

我们看一下自定义 ViewGroup 实现:

class ScrollerViewPager(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
/**

  • 定义 Scroller 实例
    */
    private var mScroller = Scroller(context)

/**

  • 判断拖动的最小移动像素点
    */
    private var mTouchSlop = 0

/**

  • 手指按下屏幕的 x 坐标
    */
    private var mDownX = 0f

/**

  • 手指当前所在的坐标
    */
    private var mMoveX = 0f

/**

  • 记录上一次触发 按下是的坐标
    */
    private var mLastMoveX = 0f

/**

  • 界面可以滚动的左边界
    */
    private var mLeftBorder = 0

/**

  • 界面可以滚动的右边界
    */
    private var mRightBorder = 0

/**

  • 记录下一次拦截的 X,y
    */
    private var mLastXIntercept = 0
    private var mLastYIntercept = 0

/**

  • 是否拦截
    */
    private var interceptor = false

init {
init()
}

constructor(context: Context?) : this(context, null) {
}

private fun init() {
/**

  • 通过 ViewConfiguration 拿到认为手指滑动的最短的移动 px 值
    */
    mTouchSlop = ViewConfiguration.get(context).scaledPagingTouchSlop

}

/**

  • 测量 child 宽高
    */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    //拿到子View 个数
    val childCount = childCount
    for (index in 0…childCount - 1) {
    val childView = getChildAt(index)
    //为 ScrollerViewPager 中的每一个子控件测量大小
    measureChild(childView, widthMeasureSpec, heightMeasureSpec)

}
}

/**

  • 测量完之后,拿到 child 的大小然后开始对号入座
    */
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    if (changed) {
    val childCount = childCount
    for (child in 0…childCount - 1) {
    //拿到子View
    val childView = getChildAt(child)
    //开始对号入座
    childView.layout(
    child * childView.measuredWidth, 0,
    (child + 1) * childView.measuredWidth, childView.measuredHeight
    )
    }
    //初始化左右边界
    mLeftBorder = getChildAt(0).left
    mRightBorder = getChildAt(childCount - 1).right

}

}

/**

  • 外部解决 1. 根据垂直或水平的距离来判断
    */
    // override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    // interceptor = false
    // var x = ev.x.toInt()
    // var y = ev.y.toInt()
    // when (ev.action) {
    // MotionEvent.ACTION_DOWN -> {
    // interceptor = false
    // }
    // MotionEvent.ACTION_MOVE -> {
    // var deltaX = x - mLastXIntercept
    // var deltaY = y - mLastYIntercept
    // interceptor = Math.abs(deltaX) > Math.abs(deltaY)
    // if (interceptor) {
    // mMoveX = ev.getRawX()
    // mLastMoveX = mMoveX
    // }
    // }
    // MotionEvent.ACTION_UP -> {
    // //拿到当前移动的 x 坐标
    // interceptor = false
    // println(“onInterceptTouchEvent—ACTION_UP”)
    //
    // }
    // }
    // mLastXIntercept = x
    // mLastYIntercept = y
    // return interceptor
    // }

/**

  • 外部解决 2. 根据第二点坐标 - 第一点坐标 如果差值大于 TouchSlop 就认为是在左右滑动
    */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    interceptor = false
    when (ev.action) {
    MotionEvent.ACTION_DOWN -> {
    //拿到手指按下相当于屏幕的坐标
    mDownX = ev.getRawX()
    mLastMoveX = mDownX
    interceptor = false
    }
    MotionEvent.ACTION_MOVE -> {
    //拿到当前移动的 x 坐标
    mMoveX = ev.getRawX()
    //拿到差值
    val absDiff = Math.abs(mMoveX - mDownX)
    mLastMoveX = mMoveX
    //当手指拖动值大于 TouchSlop 值时,就认为是在滑动,拦截子控件的触摸事件
    if (absDiff > mTouchSlop)
    interceptor = true
    }
    }
    return interceptor
    }

/**

  • 父容器没有拦截事件,这里就会接收到用户的触摸事件
    */
    override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
    MotionEvent.ACTION_MOVE -> {
    println("onInterceptTouchEvent—onTouchEvent–ACTION_MOVE ")
    mLastMoveX = mMoveX
    //拿到当前滑动的相对于屏幕左上角的坐标
    mMoveX = event.getRawX()
    var scrolledX = (mLastMoveX - mMoveX).toInt()
    if (scrollX + scrolledX < mLeftBorder) {
    scrollTo(mLeftBorder, 0)
    return true
    } else if (scrollX + width + scrolledX > mRightBorder) {
    scrollTo(mRightBorder - width, 0)
    return true

}
scrollBy(scrolledX, 0)
mLastMoveX = mMoveX
}
MotionEvent.ACTION_UP -> {
//当手指抬起是,根据当前滚动值来判定应该回滚到哪个子控件的界面上
var targetIndex = (scrollX + width / 2) / width
var dx = targetIndex * width - scrollX
/** 第二步 调用 startScroll 方法弹性回滚并刷新页面*/
mScroller.startScroll(scrollX, 0, dx, 0)
invalidate()
}
}
return super.onTouchEvent(event)
}

override fun computeScroll() {
super.computeScroll()
/**

  • 第三步 重写 computeScroll 方法,并在其内部完成平滑滚动的逻辑
    */
    if (mScroller.computeScrollOffset()) {
    scrollTo(mScroller.currX, mScroller.currY)
    postInvalidate()
    }
    }
    }
    复制代码

上面代码很简单,通过 2 种方式处理了外部拦截法冲突,分别是:

  • 根据垂直或水平的距离来判断
  • 根据第二点坐标 - 第一点坐标 如果差值大于 TouchSlop 就认为是在左右滑动

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

最后

我见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了5、6年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。

其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。

不断奔跑,你就知道学习的意义所在!

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!文章来源地址https://www.toymoban.com/news/detail-851582.html

-1712371858124)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺,程序员,android,ui

最后

我见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了5、6年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。

其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。

不断奔跑,你就知道学习的意义所在!

[外链图片转存中…(img-4vk9GwVx-1712371858124)]

[外链图片转存中…(img-J0Cc1Zw3-1712371858124)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

到了这里,关于Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android-高级-UI-进阶之路-(七)-SVG-基础使用-+-绘制中国地图,Android面试中常问的MMAP到底是啥东东

    iv.setImageDrawable(animatedVectorDrawable) val animatable = iv.drawable as Animatable animatable.start() } } 输入搜索动画 利用在线绘制 SVG 图标网站 制作搜索图标 可以自己随意捣鼓绘制,绘制好了之后点击视图-源代码,将 SVG 代码复制出来保存成 search_svg.xml 在线转换 svg2vector 点击空白或者直接将

    2024年04月25日
    浏览(50)
  • 后端进阶之路——深入理解Spring Security配置(二)

    「作者主页」 :雪碧有白泡泡 「个人网站」 :雪碧的个人网站 「推荐专栏」 : ★ java一站式服务 ★ ★ 前端炫酷代码分享 ★ ★ uniapp-从构建到提升 ★ ★ 从0到英雄,vue成神之路 ★ ★ 解决算法,一个专栏就够了 ★ ★ 架构咱们从0说 ★ ★ 数据流通的精妙之道★ ★后端进

    2024年02月14日
    浏览(52)
  • 如何应对Android面试官->实战高级UI,用自定义View画一条锦鲤(下)

    上一章我们用自定义View绘制了一条小鱼,本章我们让这条小鱼游动起来; 涉及的知识点 实现小鱼的摆动,我们可以通过属性动画 ValueAnimator 来实现,这里先简单介绍下属性动画 ValueAnimator 没有重绘,所以需要自己调用 addUpdateListener 方法,结合 AnimatorUpdateListener 使用; 操作

    2024年02月22日
    浏览(43)
  • Android架构进阶之高级UI系列(精编解析,值得收藏)

    public FrameHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_DO_FRAME: // 执行doFrame // 如果启用VSYNC机制,当VSYNC信号到来时触发 doFrame(System.nanoTime(), 0); break; case MSG_DO_SCHEDULE_VSYNC: // 申请VSYNC信号,例如当前需要绘制任务时 doScheduleVsync()

    2024年04月14日
    浏览(60)
  • Android架构进阶之高级UI系列(精编解析,值得收藏),Android开发面试技能介绍

    CallbackRecord callbacks; synchronized (mLock) { final long now = System.nanoTime(); // 根据指定的类型CallbackkQueue中查找到达执行时间的CallbackRecord callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked( now / TimeUtils.NANOS_PER_MS); if (callbacks == null) { return; } mCallbacksRunning = true; if (callbackType == Choreograph

    2024年04月13日
    浏览(42)
  • 带你深入理解Android 中 UI 的刷新机制

    Android中的UI刷新机制是指Android系统如何更新和绘制UI界面以响应用户的操作和数据变化。UI的刷新过程涉及到多个关键概念和组件,包括主线程、UI线程、消息循环、View树、View的测量和布局、绘制等。下面将详细解释Android中的UI刷新机制,并提供相应的代码示例。 主线程和

    2024年02月14日
    浏览(34)
  • Canvas中的裁剪师讲解与实战——Android高级UI(1),Android体系化进阶学习图谱

    从今天开始我们聊一聊 Canvas 的API,因为Canvas的API较多,所以我们分为几次分享,首先分享的是裁剪类型的API使用。话不多说,先上实战图。 老夫的少女心 源码地址文末会给出,了解原理才能更好地驾驭。 分享前,我们先来聊聊,在我们生活中如何绘制一张如下的图。 我们

    2024年04月13日
    浏览(86)
  • 【Spring底层原理高级进阶】轻松掌握 Spring MVC 的拦截器机制:深入理解 HandlerInterceptor 接口和其实现类的用法

     🎉🎉欢迎光临🎉🎉 🏅我是苏泽,一位对技术充满热情的探索者和分享者。🚀🚀 🌟特别推荐给大家我的最新专栏 《Spring 狂野之旅:底层原理高级进阶》 🚀 本专栏纯属为爱发电永久免费!!! 这是苏泽的个人主页可以看到我其他的内容哦👇👇 努力的苏泽 http://suze

    2024年02月20日
    浏览(54)
  • 高级-UI-从零到整-(一)-View-的基础知识你必须知道

    getX / getY : 返回相对于当前 View 左上角的 x 和 y 的坐标 getRawX / getRawY : 返回的是相对于手机屏幕左上角的 x 和 y 坐标。 TouchSlop TouchSlop 官方解释就是系统所能识别的被认为是滑动的最小距离,通俗点说就是当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么

    2024年04月26日
    浏览(46)
  • 【Linux进阶之路】理解UDP,成为TCP。

      学了TCP 和UDP之后,感觉UDP就像是 初入职场的年轻人 ,两耳不闻 “窗外事”,只管尽力地把自己的事情做好,但收获的却是 不可靠 ,而TCP更像是 涉世极深的\\\"职场老油条\\\" ,给人的感觉就是 “城府极深,深不可测”,不仅事事考虑的周全,懂得人情世故,而且深得\\\"上层

    2024年04月26日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包