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); } }
public interface ScrollingListener { void onScroll(int distance); void onStarted(); void onFinished(); void onJustify(); }
@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; }
if ((!mGestureDetector.onTouchEvent(event) || ((SlideStackView)v).isOverScrolled()) && (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL)){ justify(); }
@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; }
/** * 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; }
mDefaultDeceleration = SensorManager.GRAVITY_EARTH // g (m/s^2) * 39.37f // inch/meter * ppi // pixels per inch * ViewConfiguration.getScrollFriction() * 10.0f;
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); }
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); }
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); } … } }
@Override public void onScroll(int distance) { if (distance == 0){ return; } // LOG.d("onScroll " + distance); doScroll(distance); }
/** * 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(); } }
/** * 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); }
/** * 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; }
/** * 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); }
/** * 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; }
/** * 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); } }
/** * @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); }
/** * 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; } }
/** * 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; } }
/** * 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 * Returns the current velocity. * * @return The original velocity less the deceleration. Result may be * negative. */ public float getCurrVelocity() { return mVelocity - mDeceleration * timePassed() / 2000.0f; }
/** * 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; } }
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; }
/** * 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; }
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)); }
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); }
@Override public boolean dispatchTouchEvent(MotionEvent ev) { // LOG.v("dispatchTouchEvent: " + ev); if (getChildCount() == 0){ return false; } return super.dispatchTouchEvent(ev); }
Source: https://habr.com/ru/post/196912/
All Articles