首頁 > 軟體

Android ScrollView實現捲動超過邊界鬆手回彈

2022-04-19 10:01:37

ScrollView捲動超過邊界,鬆手回彈

Android原生的ScrollView滑動到邊界之後,就不能再滑動了,感覺很生硬。不及再多滑動一段距離,鬆手後回彈這種效果順滑一些。

先檢視下捲動裡面程式碼的處理

case MotionEvent.ACTION_MOVE:
  final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
  if (activePointerIndex == -1) {
                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                    break;
                }

                final int y = (int) ev.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;
                ………………………………
                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) {
                    // Scroll to follow the motion event
                    mLastMotionY = y - mScrollOffset[1];

                    final int oldY = mScrollY;
                    final int range = getScrollRange();
                    final int overscrollMode = getOverScrollMode();
                    boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

                    // Calling overScrollBy will call onOverScrolled, which
                    // calls onScrollChanged if applicable.
                    if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
                            && !hasNestedScrollingParent()) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }

                    ………………………………
      }
break;

先判斷手指的移動距離,超過了移動的預設距離,認為是處於mIsBeingDragged狀態,然後呼叫overScrollBy()函數,這個方法是實現捲動的關鍵。並且該方法有個引數傳遞的是mOverscrollDistance,通過名字可以知道是超過捲動距離,猜測這個是預留的實現超過捲動邊界的變數。
進入該方法看一下

protected boolean overScrollBy(int deltaX, int deltaY,
            int scrollX, int scrollY,
            int scrollRangeX, int scrollRangeY,
            int maxOverScrollX, int maxOverScrollY,
            boolean isTouchEvent) {
        final int overScrollMode = mOverScrollMode;
        final boolean canScrollHorizontal =
                computeHorizontalScrollRange() > computeHorizontalScrollExtent();
        final boolean canScrollVertical =
                computeVerticalScrollRange() > computeVerticalScrollExtent();
        final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS ||
                (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
        final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS ||
                (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);

        int newScrollX = scrollX + deltaX;
        if (!overScrollHorizontal) {
            maxOverScrollX = 0;
        }

        int newScrollY = scrollY + deltaY;
        if (!overScrollVertical) {
            maxOverScrollY = 0;
        }

        // Clamp values if at the limits and record
        final int left = -maxOverScrollX;
        final int right = maxOverScrollX + scrollRangeX;
        final int top = -maxOverScrollY;
        final int bottom = maxOverScrollY + scrollRangeY;

        boolean clampedX = false;
        if (newScrollX > right) {
            newScrollX = right;
            clampedX = true;
        } else if (newScrollX < left) {
            newScrollX = left;
            clampedX = true;
        }

        boolean clampedY = false;
        if (newScrollY > bottom) {
            newScrollY = bottom;
            clampedY = true;
        } else if (newScrollY < top) {
            newScrollY = top;
            clampedY = true;
        }

        onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);

        return clampedX || clampedY;
    }

ScrollView主要是豎直方向的捲動,主要看其Y軸方向的偏移。可以看到newScrollY的範圍,top是-maxOverScrollY,bottom是maxOverScrollY + scrollRangeY,其中scrollRangeY是mScrollY的範圍值,maxOverScrollY是超過邊界的範圍值。如果newScrollY的值小於top或者大於bottom,會對該值進行調整。
再進入onOverScrolled()方法看看,

@Override
protected void onOverScrolled(int scrollX, int scrollY,
            boolean clampedX, boolean clampedY) {
        // Treat animating scrolls differently; see #computeScroll() for why.
        if (!mScroller.isFinished()) {
            final int oldX = mScrollX;
            final int oldY = mScrollY;
            mScrollX = scrollX;
            mScrollY = scrollY;
            invalidateParentIfNeeded();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (clampedY) {
                mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
            }
        } else {
            super.scrollTo(scrollX, scrollY);
        }

        awakenScrollBars();
    }

如果mScroller.isFinished()為false,說明正在捲動動畫中(包括fling和springBack)。如果沒有捲動動畫,則直接呼叫scrollTo到新的滑動到的mScrollY。再經過繪製之後,就能看到介面捲動。
再回看overScrollBy()方法中,如果偏移距離到-maxOverScrollY與0之間,則是滑動超過上面邊界;如果偏移在scrollRangeY與maxOverScrollY + scrollRangeY之間,則是滑動超過下面邊界。
通過上面的分析可知,maxOverScrollY引數是預留的超過邊界的滑動距離,看一下傳遞過來的實參為成員變數mOverscrollDistance,改動一下該值應該就可以實現超過邊界滑動了。但是發現成員變數為private,並且也沒提供修改的方法,所以改變該變數的值可以通過反射修改。
下面為修改

class OverScrollDisScrollView(cont: Context, attrs: AttributeSet?): ScrollView(cont, attrs) {
    val tag = "OverScrollDisScrollView"
    private val overScrollDistance = 500

    constructor(cont: Context): this(cont, null)

    init {
        val sClass = ScrollView::class.java
        var field: Field? = null
        try {
            field = sClass.getDeclaredField("mOverscrollDistance")
            field.isAccessible = true
            field.set(this, overScrollDistance)
        } catch (e: NoSuchFieldException) {
            e.printStackTrace()
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
        }
        overScrollMode = OVER_SCROLL_ALWAYS
    }
}

這樣修改可以實現滑動超過邊界,不過有個問題,就是有時候鬆手了不能彈回,卡在超過邊界那了。需要看看手指擡起的程式碼處理,經過程式碼偵錯發現問題出在手指擡起的下列程式碼了

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

                    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {//手指擡起,有時不能彈回邊界
                        flingWithNestedDispatch(-initialVelocity);
                    } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
                            getScrollRange())) {
                        postInvalidateOnAnimation();
                    }

                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                }
                break;

假如現在手指向下滑動超過邊界的時候,計算出來的速度initialVelocity是正數,取個負然後傳到方法flingWithNestedDispatch()函數

private void flingWithNestedDispatch(int velocityY) {
        final boolean canFling = (mScrollY > 0 || velocityY > 0) &&
                (mScrollY < getScrollRange() || velocityY < 0);
        if (!dispatchNestedPreFling(0, velocityY)) {
            dispatchNestedFling(0, velocityY, canFling);
            if (canFling) {
                fling(velocityY);
            }
        }
    }

這個時候mScrollY小於0,velocityY小於0,所以canFling為false,導致後續的操作都不做了。這個時候,在介面上表現得就是卡在那裡不動了。
超過邊界不彈回,這個問題怎麼解決?經過偵錯,找到以下方法,見程式碼:

class OverScrollDisScrollView(cont: Context, attrs: AttributeSet?): ScrollView(cont, attrs) {
    val tag = "OverScrollDisScrollView"
    private val overScrollDistance = 500

    constructor(cont: Context): this(cont, null)

    init {
        val sClass = ScrollView::class.java
        var field: Field? = null
        try {
            field = sClass.getDeclaredField("mOverscrollDistance")
            field.isAccessible = true
            field.set(this, overScrollDistance)
        } catch (e: NoSuchFieldException) {
            e.printStackTrace()
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
        }
        overScrollMode = OVER_SCROLL_ALWAYS
    }

//    override fun onOverScrolled(scrollX: Int, scrollY: Int, clampedX: Boolean, clampedY: Boolean) {
//        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY)
//    }

    override fun onTouchEvent(ev: MotionEvent?): Boolean {
        super.onTouchEvent(ev)
        if (ev != null) {
            when(ev.action) {
                MotionEvent.ACTION_UP -> {
                    val yDown = getYDownScrollRange()
                    //解決超過邊界鬆手不回彈得問題
                    if (mScrollY < 0) {
                        scrollTo(0, 0)
//                        onOverScrolled(0, 0, false, false)
                    } else if (mScrollY > yDown) {
                        scrollTo(0, yDown)
//                        onOverScrolled(0, yDown, false, false)
                    }
                }

            }
        }

        return true
    }

    private fun getYDownScrollRange(): Int {
        var scrollRange = 0
        if (childCount > 0) {
            val child = getChildAt(0)
            scrollRange = Math.max(
                0,
                child.height - (height - mPaddingBottom - mPaddingTop)
            )
        }
        return scrollRange
    }
}

在onTouchEvent中最後,手指擡起的時候,加上一道判斷,如果這個時候是超過邊界的狀態,彈回邊界。這樣基本上,可以解決問題。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com