📜 ⬆️ ⬇️

SlideStackView or Extending ViewGroup on Android (part 2)



Recently, I talked about my experience in developing SlideStackView in Mail.Ru mobile mail for Android. Then I promised that in the near future I will prepare the second part, in which I will talk about how to implement the most interesting part from the point of view of programming visual components. Naturally, we will talk about what adds interactivity to the application - about animation. All have long been accustomed to the fact that the mobile application must be responsive to user actions. Obviously, the main way to interact with your application is to use the Touch screen.

As we write the navigation controller, SlideStackView, then interactivity will be added to us by animated transitions between the main parts of the application. In the mobile mail Mail.Ru there are three fragments: a list of accounts added to the application, a list of folders inside the selected mailbox and a list of letters that shows the contents of one folder.

As I wrote in the first part, everything starts with learning how to locate and draw static slides. The next step is to learn how to animate these slides.
')
I am sure that most of the developers, within the framework of the tasks being solved, have come across this and have some idea of ​​how MotionEvent is processed in the Android Framework, but, nevertheless, I will start from the very beginning.

So, the base View class is designed in such a way that it implies the potential handling of MotionEvents. And in response to the corresponding action, the widget gets this very MotionEvent, which stores all the necessary information about how the user interacts with the TouchScreen.

In our example, we are interested only in those events that will help to make the slide scrolling inside the slide stack.

But before starting to write code, I always prefer to first collect all the available data, requirements and wishes in a heap in order to build for myself the most accurate model of what is happening, so that not a single detail will be left aside. Naturally, it is impossible to immediately think over all the little things, and as the code is being written, you will have to abandon any ideas in favor of simplicity of the architecture, or in order to shorten the development time. But, as I noted long ago, this is done with minimal losses if you sit down before drawing up, draw approximate flowcharts, interaction tables, break one cumbersome class into interacting components, whose behavior will be easily changed without affecting the independent parts of the system. As you write various widgets, this understanding will come by itself at the level of intuition, and you will not clutch your head at the thought that in order to make the necessary changes, you now have to shovel all the code written in the last month. Here rather simple and familiar to every programmer rules come to the fore:



With the last point, it will probably be the most difficult, because often in an impulse to achieve the desired result, all programmers compromise and often tend to do "just to work" now with the note "I will change later when there is time." I do not say that this is absolutely wrong, I just want to warn you that, most likely, you will remember this later, when it becomes too late to change something or even worse, it is impossible.

And the last point, which I consider necessary to note, is a slightly contradictory appeal. Do all of the above without fanaticism. That is, if you are faced with the task of writing a widget that should be used as a controller for navigating between the screens of your application, and the main task in the requirements is the convenience of managing via the Touch Screen, then you do not need to pay attention to moments such as, for example, touch control. Of course, to navigate by simply turning or moving the phone is very cool, and even convenient, but still it is not the main task, and it can be postponed, so to speak "until better times."

So back to our sheep.

At first glance, everything is quite simple, but it is only at first glance. If you had to handle scrolling before, then you probably know that among all kinds of MotionEvents, it is not so easy to determine the true intention of the user. Did he decide to just move the slide to the side, or does he click on the item that is inside the list, etc. All this logic can be implemented as if separately from the slidestack, so it will be convenient to isolate the entire scrolling processing algorithm into a separate SlideStackScroller component.

public static class SlideScroller extends Scroller implements OnTouchListener{ private final ScrollingListener mListener; private final GestureDetector mGestureDetector; public SlideScroller(Context context, ScrollingListener listener, OnGestureListener gestureListener) { super(context); this.mListener = listener; this.mGestureDetector = new GestureDetector(context, gestureListener); mGestureDetector.setIsLongpressEnabled(false); } } 


Scrolling is not an innovative task, so, of course, classes are available for us that can help us. Namely Scroller and GestureDetector. The first one provides a convenient interface for scrolling calculation, and the second helps to define standard types of gestures, in this case we are most interested in the fling gesture. In addition to the main advantage of using a ready-made solution (lazy programmers will understand me) - you don’t have to write this logic yourself - when using solutions provided by the platform, it is easier to achieve the so-called consistent user experience, or behavior that will be familiar to the user and will not stand out among other components platforms. And this is especially important to consider if you are developing an interactive part of the system.

The scroller must provide the slide stack with an interface through which it will report on important, from the point of view of the slide stack, events, namely:

  public interface ScrollingListener { void onScroll(int distance); void onStarted(); void onFinished(); void onJustify(); } 


About when the scrolling began, when the slide shifted by a certain number of pixels, that the scrolling is over and we need to trim the position of the slide, and that the scrolling is fully completed.

From the point of view of the slidestack, nothing could be simpler, we simply delegate the processing of all touch events to the scroller, and he will figure out what really happened and call the necessary callback.

As you probably noticed, all delegation occurs through the onTouch (View v, MotionEvent event) method of the android.view.View.OnTouchListener interface.

  @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN:{ int pointerId = MotionEventCompat.findPointerIndex(event, mActivePointerId); if (pointerId == INVALID_POINTER_ID){ break; } mLastTouchX = MotionEventCompat.getX(event, pointerId); mJustifying = false; forceFinished(true); clearMessages(); break; } case MotionEvent.ACTION_MOVE:{ int pointerId = MotionEventCompat.findPointerIndex(event, mActivePointerId); if (pointerId == INVALID_POINTER_ID){ break; } // perform scrolling float x = MotionEventCompat.getX(event, pointerId); int distanceX = (int)(x - mLastTouchX); if (distanceX != 0) { mTouchScrolling = true; startScrolling(); mListener.onScroll(-distanceX); mLastTouchX = x; } break; } case MotionEvent.ACTION_UP: mTouchScrolling = false; mActivePointerId = INVALID_POINTER_ID; break; } if ((!mGestureDetector.onTouchEvent(event) || ((SlideStackView)v).isOverScrolled()) && (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL)){ justify(); } return true; } 


We go in order:



At once I want to say that fling, which at first glance seems to be an additional and minor case, is the most frequent completion of the slide scrolling. The thing is that now most users are accustomed to horizontal svaypu both in the platform itself and in many applications, therefore it performs this gesture intuitively and quickly enough.

  if ((!mGestureDetector.onTouchEvent(event) || ((SlideStackView)v).isOverScrolled()) && (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL)){ justify(); } 


Look at this condition again. Here it is very important not only to check how the GestureDetector reacted to our gesture, but rather that all gestures pass through it. In this case, we are sure that we have not missed a single movement of the user and will be able to determine the gesture we need exactly when it happens. If in this condition we rearrange the order of testing, then our slide stack will stop responding to the svayp, because most MotionEvents simply do not get to Gesture Detector.

As a result, if the fling is completed, everything will end for us (or begin, for whom it is more convenient) by calling the onFling method (MotionEvent e1, MotionEvent e2, float velocityX, float velocityY):

  @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (mScroller.isTouchScrolling()){ LOG.w("mTouchScrolling in fling"); } SlideInfo slide = getSlideInfo(mSelected); int dx = getAdjustedTargetX(velocityX) - slide.mOffset; mScroller.fling(-(int)getVelocity(dx)); return true; } 


In this method, the final coordinate is considered, into which we have to get after the fling is completed, then an amendment is made to the current position of the slide, the initial speed for movement is calculated, and the fling itself begins.

Why consider speed, if it is already given to us? The fact is that the speed can be different, but in this case it is important for us that any fling is over at a specific point, and this point is calculated in the getAdjustedTargetX () method:

  /** * Defines target x coordinate of the slide. * It depends on fling direction * <p> * In case right fling it is calculated like this * <pre> * * getLeftEdge() getRightEdge() targetX * _|___________________________|______________________|__ * | | |rightOverScrollInFling| | * | _ _ _ _ _ _ _ _ _ _ _ _ _ _ <--------------------> | * | | | | | * | | * | | | | | * | mSelectedSlide | * | | | | | * | | * | |_ _ _ _ _ _ _ _ _ _ _ _ _ _| | | * | SlideStackView | * |_|___________________________|______________________|__| * </pre> * <p> * In case left fling it is calculated like this * <pre> * 0 * ________________________|_____________________________ * | |leftOverScrollInFling | | * | <--------------------> _ _ _ _ _ _ _ _ _ _ _ _ _ _ | * | | | | | * | | * | | | | | * | mSelectedSlide | * | | | | | * | | * | | |_ _ _ _ _ _ _ _ _ _ _ _ _ _| | * | SlideStackView | * |_|______________________|_____________________________| * </pre> * * @param velocityX velocity that defines direction of the fling * @return delta x in pixels that slide needs to scolled by * @see SlideStackView#getLeftEdge(int) * @see SlideStackView#getRightEdge(int) * @see SlideStackView#mRightOverScrollInFling * @see SlideStackView#mLeftOverScrollInFling */ private int getAdjustedTargetX(float velocityX) { int result = 0; if (velocityX > 0){ result = getRightEdge(mSelected) - getLeftEdge(mSelected) + mRightOverScrollInFling; // LOG.v("onFling " + targetX); } else { // relative to layout position of the slide result = 0 - mLeftOverScrollInFling; } return result; } 


As can be seen from the body of the method, the calculation of this point depends on the direction of the fling, but the final position remains unchanged. In the case of flinging to the right, this is the extreme right position of the slide + “skid” by the number of pixels by which the slide will fly further than the extreme right position. If fling to the left, then this is the leftmost position - a skid. And this is at any initial speed of movement. Such a restriction is made artificially, since the analysis of user experience has shown that specifically for this controller, this behavior looks more natural.

On the Android platform, the scroller deals with the calculation of the kinematics of an element during fling. As a model, developers from Google took the well-accelerated motion formula known from school: S = V 0 * t- (g * t 2 ) / 2. We know the initial speed of movement, we can measure time from the beginning of the movement, all that remains is to choose the acceleration with which the slide will stop.

The developers did not guess and took the acceleration of gravity as a basis:

  mDefaultDeceleration = SensorManager.GRAVITY_EARTH // g (m/s^2) * 39.37f // inch/meter * ppi // pixels per inch * ViewConfiguration.getScrollFriction() * 10.0f; 


If we ignore the realization we stopped at, we can single out 4 possible scenarios that will occur during fling at different initial speeds.

Take the position of the open and closed slide as follows:





Suppose we fling from the initial position. The 4 possible options are obtained as a result of enumerating the distance traveled relative to the leftmost (1 screenshot) and rightmost (2 screenshot) positions of the slide. Assume that the distance between these positions is S n



S <S n / 2

This situation suggests that the user performed the fling, giving the element an initial speed that is not enough for the element to cover half the distance to the end position. In this case, to emulate the natural behavior, you can fling with the speed that the user gave the element, and, waiting for the element to stop, return it to its original, i.e. extreme left position.



S n / 2 <S <S n

Here, the initial velocity of the element was sufficient to cover half the distance to the target, but not sufficient to reach the end point. Therefore, by defining such a scenario, you can give the element a little more speed — such that it is enough to hit the destination point. To do this, it is worth remembering the physics of uniformly accelerated motion, and, knowing the acceleration and the final distance, calculate the initial speed with which you need to fling.

S n <S <S n + overscroll

This result suggests that the user has given a sufficiently high speed to the element in which he ends the movement beyond the rightmost point, but does not reach the maximum allowable drift. In this case, everything will happen exactly as we want, namely: the user will see that the element has drifted abroad, as a result, after which it will align and return to the extreme right position.

S> S n + overscroll

This situation is a bit worse. The element's initial speed is so high that it will move on to the extreme right position, and its drift will be more than the maximum allowed. Theoretically, the difference can be much higher, which creates, albeit a natural, but ugly visual effect. The positive point is that with a very sharp movement, it is more difficult for the user to notice how we adjust the speed - the main thing is that everything happens quickly and the element does not fly far beyond the borders of the screen.

If we analyze the fling in the other direction, we will see all the same cases, except that the reference distances will be calculated slightly differently.

So, how to handle fling we figured out. Now it is necessary to decide how to create an animation during this movement. To do this, consider the following method:

  public void fling(int velocity){ mLastX = 0; final int maxX = 0x7FFFFFFF; final int minX = -maxX; fling(mLastX, 0, velocity, 0, minX, maxX, 0, 0); setNextMessage(MSG_SCROLL); } 


This method is necessary only for horizontal fling and does not take into account the initial and final positions. Therefore, we simply remember the initial position, set the maximum possible boundaries, start the fling itself from the class of the heir and send us a message that we need to process the scroll.

This method is extremely simple, in it we clear all messages previously placed there and send to the handler only one message required at the moment:

  private final Handler mAnimationHandler = new AnimationHandler(); private void setNextMessage(int message) { clearMessages(); mAnimationHandler.sendEmptyMessage(message); } private void clearMessages() { mAnimationHandler.removeMessages(MSG_SCROLL); mAnimationHandler.removeMessages(MSG_JUSTIFY); } 


mAnimationHandler is the usual auxiliary Inner class, which receives a message that we are doing some kind of scrolling, which is done after the user's contact with the TouchScreen:

  private final class AnimationHandler extends Handler { @Override public void handleMessage(Message msg) { computeScrollOffset(); int currX = getCurrX(); int delta = mLastX - currX; mLastX = currX; if (delta != 0) { mListener.onScroll(-delta); } … } } 


Everything is very simple: first we ask the scroller to calculate the current position of the animation at the moment when we received this message. Then we calculate the difference between the new position and the last known to us. At the end of the report slidestek that the scroll is completed. Just like we did from the body of the onTouchEvent () method.

Now it's up to you - in response to these messages, you need to change the position of the slides on the screen. The starting point will be the implementation of the onScroll () method:

  @Override public void onScroll(int distance) { if (distance == 0){ return; } // LOG.d("onScroll " + distance); doScroll(distance); } 


At once I want to reassure: inside the method, there will not be a single line followed by a call to the doScrollInternal () method, within which the method call is actuallyDoScroll () and so on. , , , , api, . .

  /** * Performs actual scrolling. Moves the views according * to the current selected slide number and distance * passed to the method. After the scrolling has been * performed method {@link #onScrollPerformed()} will be * called where you can apply some visual effects. * @param distance scroll distance */ private void doScroll(int distance) { adjustScrollDistance(distance); // LOG.d("scroll delta " + mScrollDelta); View selected = getChild(getSelectedViewIndex()); scrollChildBy(selected, mScrollDelta); notifyScrollPerformed(); onScrollPerformed(); fillViewsIn(); if (!mDirty.isEmpty()){ invalidate(mDirty.left, mDirty.top, mDirty.right, mDirty.bottom); mDirty.setEmpty(); } } 


adjustScrollDistance(). , , .
, , . . , .
, , , , , . , , . — , .

:

  /** * Moves the specified child by some amount of pixels * @param child child to move * @param scrollDelta scrolling delta */ private void scrollChildBy(View child, int scrollDelta) { SlideInfo info = getSlideInfo(child); // LOG.d("apply scroll " + info.mPosition + " delta " + scrollDelta); Rect childDirty = getChildRectWithShadow(child); info.mOffset -= scrollDelta; child.offsetLeftAndRight(-scrollDelta); childDirty.union(getChildRectWithShadow(child)); mDirty.union(childDirty); // LOG.d("apply scroll " + info.mPosition + " newoff " + info.mOffset); } 


, mDirty , , . , , . , .

  /** * Notifies scroll listeners about selected slide has been scrolled. * Do nothing if there is no scroll listener was set earlier. */ private void notifyScrollPerformed() { if (mScrollListener != null){ final float p = getSlidePositionInBounds(mSelected); // LOG.v("notifyScrollPerformed " + mSelected + ", " + p); mScrollListener.onSlideScrolled(mSelected, p); } } /** * Calculates position for the specified slide relative to it's * scrollable bounds. * <p> * <b>Note:</b> Slide position coulld be <code>< 0.0f</code> and * <code> > 1.0f * @param slidePosition * @return */ private float getSlidePositionInBounds(int slidePosition) { SlideInfo info = getSlideInfo(slidePosition); int offset = info.mOffset; int scrollBounds = getWidth() - getRightEdgeDelta(info.mPosition) - getLeftEdge(info.mPosition); float p = ((float) offset) / scrollBounds; return p; } 


, - , , , - , . - , . , . , :
, , , , , , , .
, , . [0, 1], «», .

  /** * Listener interface that informs about slide scrolling * related events such as current selected slide has changed, * or current selected slide scroll position has changed. * @author k.kharkov */ public interface OnSlideScrollListener{ /** * Called when the current selected position for slide * has changed. Usually it happen after scrolling finished. * @param selectedSlide */ void onSlideChanged(int selectedSlide); /** * Informs about changing scroll position of the slide. * @param position current selected slide position * @param p position of the slide inside it's scroll * bounds. 0.0f at left edge, 1.0f at right edge. If * <code>p < 0.0f || p > 1.0f</code> the slide is over * scrolled to left or to the right. */ void onSlideScrolled(int position, float p); } 


, , , . , , :

  /** * Retrieves slide's left edge coordinate in opened state * relative to parent. * @param position slide number in adapter's data set * @return coordinate of the slide's left in opened state */ private int getLeftEdge(int position){ return mAdapter == null ? 0 :mAdapter.getSlideOffset(position); } 


. , . , , , , .

@Mail.Ru , , . - , , - :

  /** * Retrieves coordinate of the slide's left edge in closed state * relative to parent. * @param position slide number in adapter's data set * @return coordinate of slide's left in closed state. */ private int getRightEdge(int position) { int rightEdge = getRight() - getRightEdgeDelta(position); return rightEdge; } /** * Just calculates delta between child's right edge and * parent's right edge * @param position position of the child (in adapter's * data set indexes) * @return delta in pixels */ private int getRightEdgeDelta(int position){ if (position < 0){ return 0; } int delta = mSlideInvisibleOffsetFirst + mSlideInvisibleOffset * position; return delta; } 


, , . , . . , - , , .

2 : onScrollPerformed(), adjustScrollDistance().

, - . , , , - . , -, . , , , . , . , - .

bouncing effect. , -, . , , Android- , iOS. , . look&feel. , , , iOS , . , . , , ,
, , - , .

, : , . -. adjustScrollDistance():

  /** * Processes scroll distance according to the current scroll * state of the slide stack view. Takes into account * over scrolling, justifying. * @param distance desired distance to scroll. */ private void adjustScrollDistance(int distance) { mScrollDelta = distance; if (mScroller.isJustifying()){ processNonOverScroll(distance); } else if (mScrollDelta < 0 && isRightOverScrolled()){ processOverScroll(distance); } else if (mScrollDelta > 0 && isLeftOverScrolled()){ processOverScroll(distance); } else { processNonOverScroll(distance); } } 


SlideStackView, , , , , , . , , .

. , :

  /** * @return <code>true</code> if slide stack over scrolled * to the right. <code>false</code> otherwise */ private boolean isRightOverScrolled(){ /** * info.mOffset - it is * the latest position of the slide's left side * so if it is over scrolled - return true * _________________ * | _____________|_ * | |lastSlide | | * |<->| | | * | | | | * | |_____________|_| * |_________________| * SlideStack */ SlideInfo info = getSlideInfo(getSelectedViewIndex()); if (mSelected == mFirst + getChildCount() - 1){ if (info.mOffset > getLeftEdge(info.mPosition)){ return true; } } /** * getRightEdge() - it is left bound of the slide * when it is hidden * ___________|______ * | | __|____________ * | | | |anySlide | * | |<->| | | * | | | | | * | | |__|____________| * |___________|______| * SlideStack */ int left = info.mOffset + getLeftEdge(mSelected); if (left > getRightEdge(mSelected)){ return true; } return false; } 


, , :



  /** * @return <code>true</code> if the slide stack view is * over scrolled to the left. <code>false</code> otherwise. */ private boolean isLeftOverScrolled(){ View selected = getChild(getSelectedViewIndex()); SlideInfo info = getSlideInfo(selected); return selected.getRight() + info.mOffset < getRightEdge(mSelected - 1); } 


, . , , , () . , , (. getRightEdge()).

, , :

  /** * Changes actual scroll delta in case over scroll. * Depends on whether we in fling mode or not. * @param distance */ private void processOverScroll(int distance) { // LOG.d("process overscroll " + distance); //process over scroll while in fling mode; if (!mScroller.isTouchScrolling()){ mScroller.setDecelerationFactor(mDecelerationFactor); } else{ // or just slow down while touch scrolling mOverScrollOffset += distance; int nOffsetAbsolute = (int) (mOverScrollOffset / mOverScrollFactor); int oldOffsetAbsolute = mLastOverScrollOffset; int scrollDelta = nOffsetAbsolute - oldOffsetAbsolute; mLastOverScrollOffset += scrollDelta; mScrollDelta = scrollDelta; } } 


, touchScrolling, , - . , : - , . , . , mOverscrollFactor . . , , . , overscroll factor = 5, , distance = 1. . . , , . , . , 5 , , , 1 . , , . For this we
mOverScrollOffset — , . mLastOverScrollOffset — , . , , .

: , , mOverScrollOffset mLastOverScrollOffset. , ( ), . , . :

  /** * We need assume that actual scroll delta is distance parameter, * we need adjust {@link #mLastOverScroll} if we will not go out * from over scroll mode and over scroll again. * @param distance raw distance passed from the scroller. */ private void processNonOverScroll(int distance) { mScrollDelta = distance; if (isOverScrolled()){ mLastOverScrollOffset += distance; mOverScrollOffset = (int) (mLastOverScrollOffset * mOverScrollFactor); } else { mLastOverScrollOffset = 0; mOverScrollOffset = 0; } } 


: mOverScrollOffset mLastOverScrollOffset , , , , . — .

: , .

, . , ( ) . , , , , Scroller. , :

S=V 0 *t-(g*t 2 )/2 ( )

, , . 3 :

S=V 0 *t-(g*t 2 )/2

,

S2=V k *t2- ((g*p)*〖t2〗 2 )/2

, , p .

S3=V k2 *t3- ((g*p*p)*〖t3〗 2 )/2

: , .
, .

mScroller.setDecelerationFactor(mDecelerationFactor);

, . , , :

  /** * Adjusts the current deceleration to slow down more or less. * @param factor if > 1.0 the scroller will slow down more. * if factor < 1.0 the scroller will slow down less. */ public void setDecelerationFactor(float factor){ mVelocity = mVelocity - mDeceleration * mPassed / 1000.0f; float velocity = mVelocity; mDeceleration *= factor; mDuration = (int) (1000.0f * mVelocity / mDeceleration); int startX = mStartX = mCurrX; int startY = mStartY = mCurrY; int totalDistance = (int) ((velocity * velocity) / (2.0f * mDeceleration)); mFinalX = startX + Math.round(totalDistance * mCoeffX); // Pin to mMinX <= mFinalX <= mMaxX mFinalX = Math.min(mFinalX, mMaxX); mFinalX = Math.max(mFinalX, mMinX); mFinalY = startY + Math.round(totalDistance * mCoeffY); // Pin to mMinY <= mFinalY <= mMaxY mFinalY = Math.min(mFinalY, mMaxY); mFinalY = Math.max(mFinalY, mMinY); mStartTime += mPassed; } 


, , , , hide :

  /** * @hide * Returns the current velocity. * * @return The original velocity less the deceleration. Result may be * negative. */ public float getCurrVelocity() { return mVelocity - mDeceleration * timePassed() / 2000.0f; } 


, v(t)=v 0 +at (1000 — ). - , , .

, event' , . , . , , , . Android Framework , . , . , ViewGroup.dispatchTouchEvent().

. , , . , , :



, , View .

, , . , . , event' , : .

Let's go in order. :

  /** * Determines whether the user tries to scroll the slide stack view * or just tries to scroll some scrollable content inside the slide. * <p> * {@inheritDoc} */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { 


, , , \\ .
, :

  final int action = MotionEventCompat.getActionMasked(ev); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP){ /* * That means we need to abort all scrolling and return to nearest * stable position in the slide stack. So justify position. */ mBeingDrag = false; mUnableToDrag = false; mScroller.justify(); // LOG.v("OnInterceptTouchEvent: action cancel | up"); return false; } /* * In case we have already determined whether we need this * touch event or not - just return immediately */ if (action != MotionEvent.ACTION_DOWN){ if (mBeingDrag){ // LOG.v("OnInterceptTouchEvent: already dragging"); return true; } if (mUnableToDrag){ // LOG.v("OnInterceptTouchEvent: already unable to drag"); return false; } } 


(, MotionEvent), event' , .

, , , event .

  switch (action){ case MotionEvent.ACTION_DOWN:{ /* * remember the start coordinates for the motion event * in order to determine drag event length */ mInitialX = ev.getX(); mInitialY = ev.getY(); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); /* * pass down event to the scroller after we have decided to intercept, * not here. It helps to start calculation motion event in case we * decide to intercept it. */ mScroller.setActivePointer(mActivePointerId); if (mScroller.isScrolling() || isHiddenSlideMove(false)){ /* * in case the user start the touch while we didn't * accomplish scrolling - intercept touch event. * */ mBeingDrag = true; mUnableToDrag = false; } else { /* * Otherwise let's start the process of detecting * who the touch event belongs to. */ mBeingDrag = false; mUnableToDrag = false; } // LOG.v("OnInterceptTouchEvent: DOWN being drag " + mBeingDrag + // ", unable to drag " + mUnableToDrag); return mBeingDrag; } 


event , . . , MotionEvent.ACTION_DOWN. event' , . , , event' child' . , isHiddenSlideMove(). , event' «», , , . , .

, :

  /** * Defines whether motion events has been started on the closed slide or not * * @param extend * if <code>true</code> it will take into account * {@link #mTouchExtension}. Otherwise this method will only take * into account {@link #mInitialX} and {@link #mInitialY} * @return <code>true</code> in case the motion event has been started to * the right of the last closed slide, <code>false</code> otherwise. */ private boolean isHiddenSlideMove(boolean extend) { int x = (int) mInitialX; int y = (int) mInitialY; Rect rightSide = new Rect(); boolean right = false; for (int i = getLastHiddenSlideIndex(); i >= 0 && !right; i--) { View view = getChild(i); Rect rect = new Rect(); view.getHitRect(rect); rightSide.union(rect); if (rightSide.contains(x, y) || (extend && rightSide.contains(x + mTouchExtension, y))) { right = true; } } return right; } 


— :

  case MotionEvent.ACTION_MOVE:{ final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER_ID) { // If we don't have a valid id, the touch down wasn't on content. break; } final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); final float x = MotionEventCompat.getX(ev, pointerIndex); final float dx = x - mInitialX; final float xDiff = Math.abs(dx); final float y = MotionEventCompat.getY(ev, pointerIndex); final float dy = y - mInitialY; final float yDiff = Math.abs(dy); if (dx != 0 && canScroll(this, false, (int) dx, (int) x, (int) y)) { // Nested view has scrollable area under this point. Let it be handled there. if(!isHiddenSlideMove(false)) { mUnableToDrag = true; return false; } } // if it seems to be horizontal scroll if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff){ // LOG.v("OnInterceptTouchEvent: MOVE start drag"); ev.setAction(MotionEvent.ACTION_DOWN); adjustSelectedSlide(); mScroller.onTouch(this, ev); mBeingDrag = true; } else if (yDiff > mTouchSlop){ // LOG.v("OnInterceptTouchEvent: MOVE unable to drag"); mUnableToDrag = true; } break; } 


Let's figure it out. . -, , , , , ( ). , , . ,
. , . . , , 22,5 , .



, , — . . , .

. , , , ( ), , . , Google (. ViewPager):

  if (dx != 0 && canScroll(this, false, (int) dx, (int) x, (int) y)) { // Nested view has scrollable area under this point. Let it be handled there. if(!isHiddenSlideMove(false)) { mUnableToDrag = true; return false; } } 

  /** * Tests scrollability within child views of v given a delta of dx. * * @param v View to test for horizontal scrollability * @param checkV Whether the view v passed should itself be checked for scrollability (true), * or just its children (false). * @param dx Delta scrolled in pixels * @param x X coordinate of the active touch point * @param y Y coordinate of the active touch point * @return true if child views of v can be scrolled by delta of dx. */ protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { if (v instanceof ViewGroup) { final ViewGroup group = (ViewGroup) v; final int scrollX = v.getScrollX(); final int scrollY = v.getScrollY(); final int count = group.getChildCount(); // Count backwards - let topmost views consume scroll distance first. for (int i = count - 1; i >= 0; i--) { final View child = group.getChildAt(i); if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && canScroll(child, true, dx, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) { return true; } } } return (checkV && ViewCompat.canScrollHorizontally(v, -dx)); } 


View, «», , , ViewCompat.canScrollHorizontally(). , , . ViewCompat, .

 public class ViewCompat { public static boolean canScrollHorizontally(View v, int direction){ if (v instanceof QuickActionView){ return ((QuickActionView)v).canScrollHorizontally(direction); } else { return android.support.v4.view.ViewCompat.canScrollHorizontally(v, direction); } } } } 


. .

  /** * {@inheritDoc} */ @Override public boolean onTouchEvent(MotionEvent event) { LOG.i("onTouchEvent: " + event); if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) { // Don't handle edge touches immediately -- they may actually belong to one of our // descendants. return false; } if ((event.getAction() & MotionEventCompat.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP){ continueWithSecondPointer(event); return true; } // adjust selected slide in case we didn't it in #onInterceptTouchEvent() method // if we have no touchable child under the touch event for instance if (!mScroller.isScrolling() && event.getAction() == MotionEvent.ACTION_DOWN){ adjustSelectedSlide(); } return mScroller.onTouch(this, event); } 


event' , . , , , View.

  @Override public boolean dispatchTouchEvent(MotionEvent ev) { // LOG.v("dispatchTouchEvent: " + ev); if (getChildCount() == 0){ return false; } return super.dispatchTouchEvent(ev); } 


. - , .

, , , MotionEvent'.

— . dispatchTouchEvent(), onInterceptTouchEvent() onTouchEvent(), , event. , , . developer.android.com , event'.

, . , , .

:)

Source: https://habr.com/ru/post/196912/


All Articles