Nesting slides, which we all know, are very important when we use CoordinatorLayout + AppBarLayout to design an interface. CoordinatorLayout is a coordinated layout, the purpose of which is to coordinate the linkage of multiple layouts. The linkage involves the mutual response of multiple views when they slide. In simple terms, when one View slides, another View may need to slide correspondingly. So how does this linkage work? In other words, how does a View know that another View is sliding? One might say, Well, Behavior is coordinating. After all, Behavior is realized by CoordinatorLayout and cannot be used in any View. That is to say, the callback of Behavior’s numerous methods still depends on some underlying mechanism of View. Then what is the underlying mechanism? That’s the nested sliding mechanism.

Returning to the title, the purpose of this article is to show you how to customize a View that produces nested slides. So since there are so many views that support nested sliding, why do we need to define our own? Naturally, the official can’t meet our requirements, which is also a lesson from my work. Recently, I was working on an interface redesign and the interaction of the new interface forced me to use CoordinatorLayout + AppBarLayout for development. When I was developing a module, I found that I needed to use a View that supports nested sliding. The original idea was to use a NestedScrollView, but the NestedScrollView would amortize the Child, and performance problems would naturally arise. So in order to pursue the extreme, we define our own View that can support nested sliding.

Before reading this article, you should be prepared to:

  1. How CoordinatorLayout works.
  2. Principle of nested sliding implementation.

This article will not deeply analyze the above two parts of knowledge, so I assume that everyone knows, interested students can refer to the following article:

  1. Android source code analysis – nested sliding mechanism implementation principle
  2. CoordinatorLayout learning (I) – The basic use of CoordinatorLayout
  3. CoordinatorLayout learning (II) – linkage analysis of RecyclerView and AppBarLayout
  4. A real-life experience of a common pit when using nested slides

1. Talk about nested sliding

After the nested sliding mechanism was bundled with views after API 21, Google’s father provided views that support nested sliding in the official library, and these views can be divided into two types:

  1. Views that generate nested sliding events: This type of View is its own premise can slide, if they can not slide, then nested sliding is useless. For example, RecyclerView, NestedScrollView and so on, is mainly to achieve NestedScrollingChild, NestedScrollingChild2, NestedScrollingChild3 these three interface View. (As for the differences between these three interfaces, I will analyze them later.)
  2. Views that handle nested sliding events: All of these views implement one of the NestedScrollingParent, NestedScrollingParent2, and NestedScrollingParent3 interfaces. For example, CoordinatorLayout, NestedScrollView, SwipeRefreshLayout, etc.

In general, in nested slides, these two kinds of views come in pairs, usually the View that generates the nested slide is a child of the View that handles the nested slide. On the other hand, the View that handles the nested slide is a ViewGroup. A View that generates nested sliding events can be a subclass of any View. At the same time, nesting slides will also fail if only one of these two types of Views appears.

NestedScrollingChild3: NestedScrollingParent3: NestedScrollingParent3: NestedScrollingParent3: NestedScrollingParent3: NestedScrollingParent3: NestedScrollingParent3: NestedScrollingParent3: NestedScrollingParent3: NestedScrollingParent3 This is why there is now a NestedScrollView set RecyclerView implementation scheme. I personally do not recommend this scheme, because NestedScrollView will amortize all the internal children, which means that RecyclerView will lose many features. That’s why this article was written to show you how to customize a View that generates nested sliding events.

NestedScrollingChildX NestedScrollingParentX NestedScrollingParentX NestedScrollingParentX NestedScrollingParentX The difference between NestedScrollingChildX and NestedScrollingChildX is as follows:

Let me analyze the main points of this diagram:

  1. NestedScrollingChild: This interface defines the key methods needed for nested slides, including preScroll, Scroll, preFling, and Fling.
  2. NestedScrollingChild2: This interface is a subinterface of NestedScrollingChild. Based on the original method, the type parameter is added to determine whether the TOUCH and non-touch cases, which is used to distinguish whether the finger is still on the screen
  3. NestedScrollingChild3: This interface is a subinterface of NestedScrollingChild2. It overrides dispatchNestedScroll and adds an consumed parameter to the original contact.

I’m sure you can tell the difference between 1 and 2, but 3 adds an consumed parameter. Why is that? Obviously, this is a way to mark how far the parent View has consumed, so what does this do? Basically, after calling dispatchNestedScroll, if there’s still unconsumed distance, the subview can stop sliding. This can solve a lot of strange problems. For example, when the RecyclerView reaches the edge, more loads are triggered and the Fling should stop. However, when the RecyclerView is nested, the Fling will continue. That’s why Fling doesn’t stop. However, the solution to this problem requires NestedScrollingChild3 in conjunction with NestedScrollingParent3 to be effective.

Let’s continue with the class diagram relationship between NestedScrollingParentX:

The difference between them is similar to that between Nesteds RollingChild, so I won’t repeat it here. NestedScrollingChild3 and NestedScrollingParent3 should be implemented as much as possible, because these two interface methods are the most complete, and it is best to implement all methods once. An AbstractMethodError exception may be thrown on some phones, especially phones under 21.

2. Preparation

Now that I’ve covered nested slides, I’m going to show you how to define a View that slides nested. The steps are mainly divided into four steps:

  1. The specified View implements NestedScrollingChild3 interface, implement relevant methods at the same time, use NestedScrollingChildHelper to distribute nested sliding at the same time. And invoke setNestedScrollingEnabled, is set to true, said that the View can produce nested sliding event, it is very important.
  2. On the basis of the first step, first support single finger nested sliding.
  3. On the basis of the second step, realize multi-finger nested sliding.
  4. Create nested slides that Fling based on step 3.

Note that CoordinatorLayout is used to handle nested slides.

Let’s take a look at the specific effects:

The CustomNestedViewGroup in the figure is the View to be implemented in this article. At the same time, because the first step is relatively simple, this article will not introduce the specific operation.

This article source address: NestedScrollActivity, interested students can refer to. The implementation code of this article mainly refers to NestedScrollView.

3. Single-finger sliding is supported

One-finger swiping is very simple, it just triggers the slide on ACTION_MOVE. But this kind of thing looks simple, in fact, there are a lot of details in the development process we need to pay attention to, is the so-called book to sleep shallow, must know this to practice. A theory is always a theory unless you try to write it yourself.

Okay, that’s a bit of nonsense, so let’s get started. If you need a View that supports finger sliding, the basic framework is to override the onInterceptTouchEvent and onTouchEvent methods (you don’t need to override the onInterceptTouchEvent method if you inherit the View class). So, let’s look at the implementation of these two methods separately.

(1). OnInterceptTouchEvent method

The purpose of overriding the onInterceptTouchEvent method is to intercept events when appropriate, indicating that our View needs to consume subsequent events. Let’s look directly at the implementation of the onInterceptTouchEvent method:

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { val action = ev.actionMasked if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) { return true } when (action) {motionEvent.action_down -> {mLastMotionY = ev.y.toint () // Start creating nested sliding chains startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH) } MotionEvent.ACTION_MOVE -> { val y = ev.y.toInt() val deltaY = abs(y - mLastMotionY) if (deltaY  > mTouchSlop) { mIsBeingDragged = true mNestedYOffset = 0 mLastMotionY = y parent? .let { parent.requestDisallowInterceptTouchEvent(true) } } } MotionEvent.ACTION_CANCEL, ACTION_UP -> {mIsBeingDragged = false stopNestedScroll(ViewCompat.TYPE_TOUCH)}} return mIsBeingDragged }Copy the code

The onInterceptTouchEvent implementation is very simple, and I’ll focus on a few points here:

  1. We are inACTION_DOWN Call thestartNestedScrollMethod, which means to set up a nested sliding transfer chain. It is important to note that Type here is passed byViewCompat.TYPE_TOUCH, mainly to distinguish between subsequent Fling slides; Secondly, we areACTION_CANCELandACTION_UP Call thestopNestedScroll, means to cut the nested sliding transfer chain.
  2. inACTION_MOVE Inside try settingmIsBeingDraggedTo intercept the event for consumption.

The onInterceptTouchEvent method does not consume a move event. Instead, it sets some status values, such as:

MIsBeingDragged: Used to indicate whether the consumption time is needed. We can see that as long as the sliding distance exceeds mTouchSlop, we will consume. MLastMotionY: records the Y coordinate of the last event, mainly used to calculate the sliding distance generated by the current event compared with the last event. MNestedYOffset: Used to record how much sliding distance is consumed by the parent View. How do we understand this variable? In our case, assume that the View slides 100px. If the View and AppBarLayout move up 50px, then the mNestedYOffset will be 50. This variable is so important that it is needed to calculate the mLastMotionY and the initial speed of the Fling when the Fling is UP.

Since the onInterceptTouchEvent method does not consume events, where does it consume them? The onTouchEvent method, of course.

(2). OnTouchEvent method

When an event is not consumed by a Child within the View, or is intercepted by onInterceptTouchEvent, it is passed to the onTouchEvent method. And the onTouchEvent method is the function of the consumption event, to trigger the View internal content to slide, is implemented in this method.

Let’s go straight to the implementation of the onTouchEvent method:

override fun onTouchEvent(event: MotionEvent): Boolean { val action = event.actionMasked if (action == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0 } when (action) { MotionEvent.ACTION_DOWN -> { mLastMotionY = event.y.toInt() startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH) } MotionEvent.ACTION_MOVE -> { val y = event.y.toInt() var deltaY = mLastMotionY - y if (! mIsBeingDragged && abs(deltaY) > mTouchSlop) { parent? .let { requestDisallowInterceptTouchEvent(true) } mIsBeingDragged = true if (deltaY > 0) { deltaY -= mTouchSlop } else {  deltaY += mTouchSlop } } if (mIsBeingDragged) { // 1. Call dispatchNestedPreScroll for the parent View to slide before the internal content slides. if (dispatchNestedPreScroll( 0, deltaY, mScrollConsumed, mScrollOffset, Viewcompat.type_touch)) {deltaY -= mScrollConsumed[1] // Update parent View slide distance mNestedYOffset += mScrollOffset[1] } mLastMotionY = y-mscrolloffSet [1] val oldScrollY = scrollY val range = getScrollRange() // 2. Trigger content by sliding overScrollBy(0, deltaY, 0, oldScrollY, 0, range, 0, 0, true) val scrollDeltaY = scrollY - oldScrollY val unconsumedY = deltaY - scrollDeltaY mScrollConsumed[1] = 0 // 3. When the content has been swiped, if the swiping distance has not been consumed, then we call the dispatchNestedScroll method, Ask the parent View if dispatchNestedScroll(0, scrollDeltaY, 0, unconsumedY, mScrollOffset, ViewCompat.TYPE_TOUCH, MScrollConsumed) mLastMotionY -= mScrollOffset[1] mNestedYOffset += mScrollOffset[1]}} MotionEvent.ACTION_UP -> { endDrag() } MotionEvent.ACTION_CANCEL -> { endDrag() } } return true }Copy the code

The onTouchEvent method is long, but it’s all in move. Let’s break it down:

  1. Down events are basic implementations, such as updatesmLastMotionY And then there’s the callstartNestedScrollMethods.
  2. Both up and cancel are calledendDragThere are only two things you do in this method, resetmIsBeingDragged, and also calledstopNestedScroll.

The implementation of the move event is more complicated, and I’ll break it down into three steps:

  1. According to themLastMotionY Figure out how much sliding distance this time, and then calldispatchNestedPreScrollMethods. The goal is to ask the parent View if it wants to consume distance before sliding inside. Among themmScrollConsumedInside record parent View consumption distance, at the same timemScrollOffsetRepresents how far our View slides across the screen, mainly based ongetLocationInWindowTo calculate. When the parent View slides, it updates some state values, such as deltaY, mNestedYOffset, and mLastMotionY. One might wonder, why update mLastMotionY? Since the position of our View has been updated in the screen, the Y coordinate of the last event recorded should also be updated, otherwise there will be an error in the sliding distance calculated by the next event.
  2. calloverScrollByMethod to slide the contents of a View. Now, again, you might be wondering why we’re calling itoverScrollByInstead of callingscrollByorscrollYo? For example, if you have 100px left to slide, but the View can only slide 50px, you can’t slide 100px directly, so you need to trim the slide distancescrollByorscrollYo, we need to calculate the clipping distance ourselves, butoverScrollByThe method is clipped internally based on the scrollRange, so the first calloverScrollByIt’s so we don’t have to write tailored code ourselves.
  3. Call when the View is finished sliding by itselfdispatchNestedScrollAsk the parent View if it needs to consume the remaining distance. If the consumption, naturally to renewmLastMotionY andmNestedYOffset .

In nested slide flows, especially when nested slides need to be triggered in move events, the flow is fixed:

It seems fairly simple, but there are some premises that you need to know, and I’m going to say it again:

  1. callsetNestedScrollingEnabledMethod, set to true.
  2. In the calldispatchNestedPreScrollanddispatchNestedScrollBefore, it must be calledstartNestedScrollAnd the Type passed must be consistent.
  3. After the slide is complete, you need to callstopNestedScrollMethod to cut the transfer chain.

There are two types of Type:

  1. TYPE_TOUCH: Represents nested sliding events generated by a finger on the screen.
  2. TYPE_NON_TOUCH: Represents nested sliding events that do not occur on the screen, such as the Fling slide.

The realization of single finger sliding is introduced here, and it is relatively simple on the whole. Commit Message is a new Demo for customnestedScrollViewGroup, and the CustomNestedScrollView move event is completed.

4. Support multi-finger sliding

To support multi-finger sliding, first introduce a new action meaning, as follows:

  1. ACTION_POINTER_DOWN: Indicates that the non-first finger falls on the screen.
  2. ACTION_POINTER_UP: Indicates that the non-last finger leaves the screen.
  3. ACTION_UP: Indicates that the last finger leaves the screen
  4. ACTION_MOVE: Indicates that any finger is moving.
  5. ACTION_DOWN: Indicates that the first finger falls on the screen.

Also, as a whole, we need to define a variable that represents the last finger that fell on the screen; Also, we need to update the most recent finger of the record in real time between down and up. Finally, when obtaining the sliding coordinates, the finger Id needs to be passed in instead of being obtained directly by getY.

(1). Use finger Id

As mentioned earlier, we can’t call getY directly to get coordinates. Let’s see how to get coordinates. Here we’ll just look at onTouchEvent:

override fun onTouchEvent(event: MotionEvent): Boolean {// ····· when (action) {motionEvent.action_down -> {mActivePointerId = event.getPointerId(0) mLastMotionY =  event.y.toInt() startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH) } MotionEvent.ACTION_MOVE -> { val pointerIndex = event.findPointerIndex(mActivePointerId) if (pointerIndex == -1) {return true} val y = event.gety (pointerIndex).toint () var deltaY = mLastMotionY -y } return true}Copy the code

We found that in both ACTION_DOWN and ACTION_MOVE, there is a mActivePointerId, which is used to represent the Id of the recently active finger. An index can be found by this Id, and the corresponding coordinate of event can be obtained by index. There is also a small detail that we notice that mActivePointerId is initialized in ACTION_DOWN.

(2). Update finger Id

In addition to initializing the finger Id and using the finger Id, there is an essential step: update the finger Id. The timing of the update is reflected in the following places:

  1. ACTION_CANCEL, ACTION_UP
  2. ACTION_POINTER_DOWN
  3. ACTION_POINTER_UP

Let’s go straight to the code:

override fun onTouchEvent(event: MotionEvent): Boolean {/ / · · · · · · the when (action) {/ / · · · · · · MotionEvent. ACTION_UP - > {mActivePointerId = INVALID_POINTER / / / /......  } MotionEvent.ACTION_CANCEL -> { mActivePointerId = INVALID_POINTER endDrag() } MotionEvent.ACTION_POINTER_DOWN -> { val newPointerIndex = event.actionIndex mLastMotionY = event.getY(newPointerIndex).toInt() mActivePointerId = ACTION_POINTER_UP -> {onSecondaryPointerUp(event)}} // ······ return true }Copy the code

From the code, the differences are as follows:

  1. ACTION_CANCEL, ACTION_UPReset:mActivePointerId
  2. ACTION_POINTER_DOWNUpdate:mActivePointerId , because this time indicates that a finger falls into the screen, it is directly updated to the current finger.
  3. ACTION_POINTER_UP: callonSecondaryPointerUpMethod, try to updatemActivePointerId . Look at it in two ways: if the finger Id off the screen is notmActivePointerId If it’s recorded, ignore it; If it ismActivePointerId If it’s on the record, follow itpointerIndexTo judge, willmActivePointerId Update to the first finger, or any other finger. You can see the detailsonSecondaryPointerUpMethod implementation.

In general, the implementation of multi-finger sliding is relatively simple, after all, there is already a single finger sliding basis. The complete code can be found in KotlinDemo, where commit Message supports multi-finger sliding.

5. Support Fling

The best way to do this is to have a single finger slide and a multi-finger slide. The best way to do this is to have a list View. But let’s see how that works.

To have a View that supports Fling, you need two things:

  1. You need to figure out how fast your finger is sliding when it leaves the View. This speed can be used as the initial speed of the Fling.
  2. On the basis of the initial speed, to achieve the Fling.

(1). VelocityTracker

It is very simple to calculate the sliding speed when the finger leaves the screen. The official tool has been provided, that is VelocityTracker. We can calculate the speed we want by passing the corresponding Event into this tool class.

However, I need to emphasize one thing here, which is that because of the nesting slide involved, the View will change position on the screen, and passing the Event directly will cause our calculation speed to be incorrect. For example, suppose that the Y coordinate of the last move event was 500, and the move event generated a sliding distance of 100px and moved the View up 100px, then the Y coordinate of the next event would also be 500, actually unchanged. Because event.getY is the coordinate relative to View, the position of finger relative to View does not change, so the coordinate of event does not change. That’s why we need to update mLastMotionY in real time, so that the next time we calculate the slip distance is not correct; An mNestedYOffset is also defined, which records how far the View has moved in the screen during the whole event chain (down->move-> Up).

Let’s go straight to the implementation, once again the onTouchEvent method:

override fun onTouchEvent(event: MotionEvent): Boolean { val action = event.actionMasked if (action == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0 } val vtev = Motionevent.obtain (event) vtev.offsetLocation(0f, mnestedyoffset.tofloat ()) // ····· mVelocityTracker? .let { it.addMovement(vtev) } vtev.recycle() return true }Copy the code

In order to solve the speed calculation error mentioned above, adjust the coordinate of event before calling addMovement. The adjustment depends on the mNestedYOffset.

As long as we understand that, we don’t need to analyze much else.

(2). OverScroller

To achieve the Fling, use the OverScroller. The function of OverScroller is to have an initial speed and then continuously poll to generate the sliding distance. We can use this sliding distance to slide what we want. Here are two parts: the content of the parent View and the child View.

Let’s look at the implementation:

override fun onTouchEvent(event: MotionEvent): Boolean {// ···· motionEvent. ACTION_UP -> {mActivePointerId = INVALID_POINTER Val velocityTracker = mVelocityTracker  velocityTracker? .computecurrentVelocity (1000, mmaximumvelocity.tofloat ()) // Slide upward velocity is negative, Val initVelocity = velocityTracker?.getyVelocity (mActivePointerId)?.toint ()?: 0 if (abs(initVelocity) > mMinimumVelocity) { if (! dispatchNestedPreFling(0F, -initVelocity.toFloat())) { dispatchNestedFling(0F, -initVelocity.toFloat(), True) Fling (-initvelocity.tofloat ())}} endDrag()} return true}Copy the code

Fling is triggered by the UP method, but the parent View uses dispatchNestedPreFling before calling the fling method. The main purpose is to ask the parent View if it consumes the Fling event. If the value is false, the parent View does not consume the fling. The child View does not consume the fling itself. But before we look at the Fling method, let’s focus on another point: endDrag:

    private fun endDrag() {
        mIsBeingDragged = false
        stopNestedScroll(ViewCompat.TYPE_TOUCH)
    }
Copy the code

StopNestedScroll breaks the scrollchain of type TYPE_TOUCH in preparation for re-establishing the SCrollchain of TYPE_NON_TOUCH later.

Let’s go back to the Fling method:

    private fun fling(velocityY: Float) {
        mOverScroller.fling(0, scrollY, 0, velocityY.toInt(), 0, 0, Int.MIN_VALUE, Int.MAX_VALUE)
        runAnimatedScroll()
    }
Copy the code

The fling sliding is triggered by OvserScrolled and the runAnimatedScroll method is also called:

    private fun runAnimatedScroll() {
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH)
        mLastScrollY = scrollY
        ViewCompat.postInvalidateOnAnimation(this)
    }
Copy the code

There are three main things that this method does:

  1. To establishTYPE_NON_TOUCHThe transfer chain of.
  2. Record scrollY for calculationOverScrollerThe resulting slip distance.
  3. callpostInvalidateOnAnimationMethod to trigger a polling callbackcomputeScrollMethods.

The computeScroll method provides the actual sliding logic for fling.

override fun computeScroll() { if (mOverScroller.isFinished) { return } mOverScroller.computeScrollOffset() val y = mOverScroller.currY var deltaY = y - mLastScrollY mLastScrollY = y mScrollConsumed[1] = 0 dispatchNestedPreScroll(0, deltaY, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH) deltaY -= mScrollConsumed[1] val range = getScrollRange() if (deltaY ! = 0) { val oldScrollY = scrollY overScrollBy(0, deltaY, 0, oldScrollY, 0, range, 0, 0, false) val consumedY = scrollY - oldScrollY deltaY -= consumedY mScrollConsumed[1] = 0 dispatchNestedScroll( 0, consumedY, 0, deltaY, null, ViewCompat.TYPE_NON_TOUCH, mScrollConsumed ) deltaY -= mScrollConsumed[1] } if (deltaY ! = 0) { abortAnimateScroll() } if (! mOverScroller.isFinished) { ViewCompat.postInvalidateOnAnimation(this) } else { abortAnimateScroll() } }Copy the code

In terms of code implementation, the overall framework is similar to move’s implementation. First, calculate the generated sliding distance, and then ask the parent View whether to consume the sliding distance through dispatchNestedPreScroll, and then update the sliding distance, call the overScrollBy method, consume the sliding distance; When its consumption is complete, then call dispatchNestedScroll to ask whether the parent View has consumed.

There is a small detail here. When the end of the slide is not consumable, it means that you have reached the boundary and need to stop the Fling.

The whole logic of fling is clear. The complete code can be found at KotlinDemo. Commit Message supports sliding

6. Summary

This concludes my introduction to implementing nested sliding. This article focuses on defining a View that produces nested slides, not a View that handles nested slides.

I’ll leave that up to you, if you’re interestedCoordinatorLayout Implement one of your own. Before, I defined a ViewGroup that handles the upper and lower recyclerViews. The linkage between these two recyclerViews is really frustrating. Something like this:

The yellow part of the ViewGroup is something I need to define. So if you’re interested in trying to define a ViewGroup like this.

Let me make a brief summary of the content of this article:

  1. To define a View that produces nested slides, the implementation needs to be implementedNestedScrollingChildInterface, and callsetNestedScrollingEnabledMethod, set to true, indicating that the View can generate nested sliding events.
  2. To define a View that creates nested slides, you need to handle three problems: one-finger slides, multi-finger slides, and Fling slides.
  3. Single finger slide, need to be called when downstartNestedScrollMethods A nested sliding transfer chain was established. When moving, calculate the resulting slide distance, first calleddispatchNestedPreScrollMethod, ask the parent View to consume the slide distance, then consume the slide distance in its own, and finally calldispatchNestedScrollAgain, ask the parent View how far the consumption slide is.
  4. Multi-finger sliding, need to define an active finger Id when down; When moving, use this finger Id to calculate the Y coordinate of the event, so as to correctly calculate the sliding distance. Finally, update the finger Id correctly at the appropriate time (up, cancel).
  5. There are two problems to handle: calculating the slide speed and triggering the Fling slide. The slide speed can be calculated using the VelocityTracker and the Fling slide can be achieved using the OverScroller. However, what differentiates sliding from finger sliding is the nested slide transfer chain (Type:)TYPE_NON_TOUCH; And one-finger sliding isTYPE__TOUCH.