The Road to Touch Event Practice

640?wx_fmt=png&wxfrom=5&wx_lazy=1


Technology News Today


Recently, it was reported that Tencent plans to spend more than 500 billion won (equivalent to RMB 2.95 billion) to acquire at least 10% of Bluehole, the South Korean developer of the popular online game "PUBG Mobile: Battle Royale". In September last year, it was rumored that Tencent was interested in acquiring Bluehole, but it was finally rejected by Bluehole shareholders on the grounds of sufficient funds. According to the report, Tencent intends to acquire at least 10% of the shares of Blue Hole. If the transaction is successful, Tencent will become the second largest shareholder of Blue Hole.


About the Author


Time flies a bit fast, the May Day holiday is over. However, we only need to work for three days this week, and everyone will continue to work hard in the new week.

This article is from  Leo  's contribution, sharing his experience related to touch events, let's take a look! Hope you all like it.

Leo  's blog address:

https://linxiaotao.github.io/


foreword


Recently, I helped the company interview for Android positions. Most of the interviewed students have more than two years of work experience, but found that many students are not very familiar with the distribution of touch events. But it is completely unfamiliar to me, which makes me feel surprised, because I think this very basic knowledge in Android, not only to be familiar with the distribution process, but also to be able to implement simple custom controls involving gesture processing. Therefore, this is also the purpose of this article, hoping to deepen the understanding of touch event distribution through some practical examples of touch event processing, and at the same time to achieve related requirements.

A brief review of touch events

We all know that the distribution of touch events, strictly speaking, starts from the Activity, but generally we talk about it, only from the ViewGroup.

640?wx_fmt=png

TouchEvent

The above picture is just a rough drawing of the distribution process of touch events. It starts from the dispatchTouchEvent of ViewGroup. First, judge whether the onInterceptTouchEvent of ViewGroup is intercepted. At the same time, you can also call requestDisallowInterceptTouchEvent of ViewGroup to make ViewGroup not call onInterceptTouchEvent. If the event is intercepted, call the onInterceptTouchEvent. The superclass of ViewGroup is the dispatchTouchEvent of View. Otherwise, the dispatchTouchEvent of the subview is called. Note: Here we do a simple process, assuming that the subview of the current ViewGroup is a View. If the subview is a ViewGroup, then execute the dispatchTouchEvent of the ViewGroup first.

In the end, it will be called to the onTouchEvent of the View. This method is to actually handle the touch event. Generally, if we need to handle the touch event by ourselves, it is also handled in this method.


text


Gesture determination

Above we briefly reviewed the touch event distribution. For details, see the source code. In fact, many students can give an overview of the distribution process mentioned above, but they do not know how to start when they are writing.

When we are in daily development, if we encounter a requirement that we do not know how to implement, the most direct way is to read other people's source code, whether it is a personal library or a library provided by Google, and a well-designed source code is worth reading. A whole bunch of blogs. It is also a reason to realize the need to handle touch events. The Android SDK has provided ListView, RecyclerView, ScrollView and other system controls that involve drag processing, so we can start by reading the source code of such controls to see how the system controls are. Handle touch events. Here we choose ScrollView for a simple analysis:

ScrollView inherits from FrameLayout and belongs to ViewGroup control, so it has onInterceptTouchEvent

因为篇幅有限,所以我们会省略去无关紧要的代码,各位同学可以对照着源码进行分析。源码部分基于 Android 26

  • onInterceptTouchEvent

if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
   return true;
}

用 mIsBeingDragged 变量来保存当前是否已经开始进行拖动手势,这个后面会讲到,同时当前分发事件类型为 ACTION_MOVE,那么直接返回 true,即拦截事件向子视图进行分发。这个是一个快捷的判断,省去了后面再进行一系列的判断,这个是个小技巧,我们在实现自定义控件的时候也可以用上。接下来的分析,为了更清晰,我们分为不同类型的 Touch Action 进行阅读。

TOUCH_DOWN

if (!inChild((int) ev.getX(), (int) y)) {
   mIsBeingDragged = false;
   recycleVelocityTracker();
   break;
}

这段代码很简单,如果触摸事件没有作用于子视图范围内,那么就不处理,同时释放速度跟踪器,这个后面会讲到,一般用于 fling 手势的判定。

mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);

mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();

startNestedScroll(SCROLL_AXIS_VERTICAL);

这段代码主要是用于初始化判定之前的资源,比如 mLastMotionY 记录按下时的坐标信息,mActivePointerId 记录当前分发触摸事件的手指 id,这个一般用于多指的处理,initOrResetVelocityTracker 初始化速度跟踪器,同时使用 addMovement 记录当前触摸事件信息,mScroller 是一般用于 fling 手势处理,这里的作用是处理上一次的 fling,startNestedScroll 则是嵌套滚动机制的知识了,嵌套滚动机制也不难理解,但这里我们先不涉及,相信理解基础的触摸事件知识后,这个只要稍微阅读下源码,就能理解的,说句题外话,虽然嵌套滚动很容易理解,作用却非常大,基本解决了大部分的滑动冲突场景。

TOUCH_MOVE

final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
   break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
   break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
   mIsBeingDragged = true;
   mLastMotionY = y;
   initVelocityTrackerIfNotExists();
   mVelocityTracker.addMovement(ev);
   final ViewParent parent = getParent();
   if (parent != null) {
       parent.requestDisallowInterceptTouchEvent(true);
   }
}

这段代码先使用我们在 TOUCH_DOWN 中记录的 mActivePointerId 进行是否为有效的判断,如果有效,则通过 findPointerIndex 获取作用手指 id 的下标,记录为 pointerIndex ,为什么要获取这个值,我们知道现在的手机屏幕都是支持多指触摸的,所以我们需要根据某个按下的手指的触摸信息来进行处理。yDiff 是滑动的距离,mTouchSlop 则是 SDK 定义的可作为判定是否开始进行拖动的距离常量,可以通过 ViewConfiguration 的 getScaledTouchSlop 获取,如果大于这个值,我们可以认为开始了拖动的手势。 getNestedScrollAxes 这个同样是用于嵌套滚动机制的,可以略过。如果开始了拖动手势,mIsBeingDragged 标记为 true,同样使用速度跟踪器记录信息,这里还会调用 ViewParent 的 requestDisallowInterceptTouchEvent,防止父视图拦截了事件,即 onInterceptTouchEvent

TOUCH_CANCEL && TOUCH_UP

mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
stopNestedScroll();

一般我们都会在 TOUCH_CANCEL 和 TOUCH_UP 这两个类型的触摸事件分发中,进行一些释放资源的操作,比如 mIsBeingDragged 设置为 false,释放速度跟踪器等等

TOUCH_UP 是所有的手指(多指触摸)抬起时分发的事件,这个比较好理解,而 TOUCH_CANCEL 则是触摸取消事件类型,一般什么时候会分发这个事件呢?举个例子,如果某个子视图已经消费了 

TOUCH_DOWN,即在这个事件分发时,向父视图传递了 true 的返回值,那么一般情况下,父视图不会再拦截接下来的事件,比如 ACTION_MOVE 等,但是如果父视图在这种情况下,还拦截了事件传递,即在 onInterceptTouch 中返回了 true,那么在 ViewGroup 的 dispatchTouchEvent 中会给已经确认消费事件的子视图分发一个 TOUCH_CANCEL 的事件,具体可以阅读源码。

ACTION_POINTER_UP

这个为多指触摸时,某个手指抬起时分发的事件。

final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
               MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
   final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
   mLastMotionY = (int) ev.getY(newPointerIndex);
   mActivePointerId = ev.getPointerId(newPointerIndex);
   if (mVelocityTracker != null) {
       mVelocityTracker.clear();
   }
}

这段代码处理的是,当某个手指抬起时,而这个手指刚好是我们当前使用的,则重新初始化资源

小结

我们可以简单总结下,onInterceptTouchEvent 所进行的处理,即在 TOUCH_DOWN 资源初始化,TOUCH_MOVE 判断是否开始拖动手势,TOUCH_CANCEL && TOUCH_UP 中进行资源释放。这里涉及了多指触摸的处理。

  • onTouchEvent

onTouchEvent 要比 onInterceptTouch 的逻辑更复杂,因为这个方法是用于真正的消费触摸事件。同样的,我们只关心核心代码,略去无关紧要的代码片段。

TOUCH_DOWN

if (getChildCount() == 0) {
   return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
   final ViewParent parent = getParent();
   if (parent != null) {
       parent.requestDisallowInterceptTouchEvent(true);
   }
}

if (!mScroller.isFinished()) {
   mScroller.abortAnimation();
}

mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);

同样的,onTouchEvent 在 TOUCH_DOWN 事件分发中,主要是进行资源初始化,同时也处理上一次的 fling 任务,比如调用 Scroller 的 abortAnimation,如果 Scroller 还没结束 fling 计算,则中止处理。

TOUCH_MOVE

final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
   break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
   // 嵌套滚动处理
   deltaY -= mScrollConsumed[1];
   vtev.offsetLocation(0, mScrollOffset[1]);
   mNestedYOffset += mScrollOffset[1];
}
   if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
       final ViewParent parent = getParent();
       if (parent != null) {
           parent.requestDisallowInterceptTouchEvent(true);
       }
       mIsBeingDragged = true;
       if (deltaY > 0) {
           deltaY -= mTouchSlop;
       } else {
           deltaY += mTouchSlop;
       }
   }

   if (mIsBeingDragged) {
       /// 业务逻辑
   }

这段代码同样会进行多指处理,获取指定手指的触摸事件信息。mIsBeingDragged 为 false,同时会再进行一次拖动手势的判定,判定逻辑和 onInterceptTouchEvent 中类似,如果 mIsBeingDragged为 true,则开始进行真正的逻辑处理。

EdgeEffect 是用于拖动时,边缘的阴影效果,具体使用可以参考源码

TOUCH_UP

if (mIsBeingDragged) {
   final VelocityTracker velocityTracker = mVelocityTracker;
   velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
   int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);

   if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
       flingWithNestedDispatch(-initialVelocity);
   }

   mActivePointerId = INVALID_POINTER;
   endDrag();
}

当手指全部抬起时,可以使用速度跟踪器进行 fling 手势的判定,同时释放资源。通过 getYVelocity获取速度,在判断是否可以作为 fling 手势处理,mMaximumVelocity 是处理的最大速度,mMinimumVelocity 是处理的最小速度,这两个值同样可以通过 ViewConfiguration的 getScaledMaximumFlingVelocity 和 getScaledMinimumFlingVelocity 获取。一般情况对 fling 的处理是通过 Scroller 进行处理的,因为这里涉及复杂的数学知识,而 Scroller 可以帮我们简化这里的操作,使用如下:

int height = getHeight() - mPaddingBottom - mPaddingTop;
int bottom = getChildAt(0).getHeight();

mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
                   Math.max(0, bottom - height), 0, height/2);

postInvalidateOnAnimation();

通过传递当前拖动手势速度值来调用 fling 进行处理,然后在 computeScrollOffset 方法中,进行真正的滚动处理:

public void computeScroll() {
   if (mScroller.computeScrollOffset()) {
         // 逻辑处理
       int x = mScroller.getCurrX();
       int y = mScroller.getCurrY();
       postInvalidateOnAnimation();
   }
}

首先我们要知道 Scroller 并不会为我们进行滚动处理,它只是提供了计算的模型,通过调用 computeScrollOffset 进行计算,如果返回 true,表示计算还没结束,然后通过 getCurrX 或 getCurrY 获取计算后的值,最后进行真正的滚动处理,比如调用 scrollTo 等等,这里需要注意的是,需要调用 invalidate 来确保进行下一次的 computeScroll 调用,这里使用的 postInvalidateOnAnimation 其作用是类似的。

TOUCH_CANCEL

if (mIsBeingDragged && getChildCount() > 0) {
   if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
       postInvalidateOnAnimation();
   }
   mActivePointerId = INVALID_POINTER;
   endDrag();
}

同样的,一般我们都会在 TOUCH_CANCEL 中释放资源。

ACTION_POINTER_DOWN

当有新的手指按下时分发的事件

final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);

以新按下的手指的信息重新计算

ACTION_POINTER_UP

这里的处理和 onInterceptTouch 一致

小结

onTouchEvent 和 onInterceptTouchEvent 处理有些相似,主要是在 TOUCH_MOVE 中在判定为拖动手势后进行真正的业务逻辑处理,同时在 TOUCH_UP 中根据速度跟踪器的获取的速度,判定是否符合 fling 手势,如果符合,则使用 Scroller 进行计算。

  • 总结

在分析完 ScrollView 的触摸事件处理,我们应该对事件处理有个基本理解,可以按照这个思路去分析其他的类似的系统控件,比如 NestedScrollView、RecyclerView 等等,我们可以发现处理的思路基本一致,那我们是不是可以将这些判定逻辑封装下呢?是的,并且系统已经提供 GestureDetector 来进行手势的判定,我们只需要在相应的手势回调方法中进行我们的业务逻辑即可。还有更强大的 ViewDragHelper ,但不管怎样,只要能理解好触摸事件分发,对工具类的熟练使用就不在话下。

实战

理论说的再多,也是纸上谈兵,只有真正去实践,才能熟练掌握。因为业务需求或者兴趣爱好,我写了以下两个自定义控件,正好一个是纵向滑动和一个是横向滑动,效果如下:

640?wx_fmt=gif

WheelView

640?wx_fmt=gif

TapeView

详细的代码分析我们就不进行了,因为阅读源码是最快的方式,第一个是滚轮控件,项目地址如下:

https://github.com/LinXiaoTao/WheelView

第二个是之前参加活动的仿薄荷健康的卷尺控件,项目地址如下:

https://github.com/LinXiaoTao/CustomViewProject

在这里我们只分析部分代码:

  • 滚轮控件

在前面手势判定中的分析中,我们提到在 onTouchEvent 判定拖动手势成功后,进行真正的业务逻辑处理,在这个控件中也是一样的:

if (mIsBeingDragged) {
   mDistanceY += mLastY - moveY;
   postInvalidateOnAnimation();
}

Every time the TOUCH_MOVE event is dispatched, the difference value from the position information recorded in TOUCH_DOWN is calculated and saved as mDistanceY, and this value is used in onDraw to shift the Canvas and draw the UI of the new position.

  • tape measure control

The calculation of the drag distance is the same as that of the scroll wheel control, but it is recorded as mOffsetLeft. At the same time, both controls are dispatched in the ACTION_UP event of onTouchEvent to process the fling gesture. However, the tape measure control uses EdgeEffect to process edge effects, and interested students can take a look.


Epilogue


The main purpose of the article is not to teach how to handle the distribution of touch events, but to hope that through this example, everyone can develop the habit of reading source code, whether it is the system SDK or third-party libraries, as long as you master the core knowledge, You can use a variety of ready-made tool libraries proficiently, and achieve the effect of inferring others.

However, no matter how much theoretical knowledge is on paper, the most important thing is practice. For specific practice, you can do this: first understand the touch event distribution process, and then select a control, which can be a system control or other controls, as long as touch event processing is involved. , read, and then manually implement a control that scrolls in the opposite direction. For example, if you are reading a control that slides vertically, then implement a control that slides horizontally. This custom control needs to achieve the following effects:

  • The most basic drag gesture handling

  • Fling effect realization

  • If possible, then implement the edge effect

Thanks everyone for reading.


Welcome to long press the picture  ->  identify the QR code in the picture

Or  scan and  follow my official account

640.png?

640?wx_fmt=jpeg

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325743931&siteId=291194637