Android ScrollView滚动超过边界,松手回弹

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中最后,手指抬起的时候,加上一道判断,如果这个时候是超过边界的状态,弹回边界。这样基本上,可以解决问题。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Android中的ScrollView滚动监听可以通过设置OnScrollChangeListener来实现。具体步骤如下: 1. 在布局文件中添加ScrollView控件,并设置其id。 2. 在Java代码中获取ScrollView控件,并设置OnScrollChangeListener。 3. 在OnScrollChangeListener中实现滚动监听的逻辑。 示例代码如下: XML布局文件: <ScrollView android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 添加需要滚动的内容 --> </ScrollView> Java代码: ScrollView scrollView = findViewById(R.id.scrollView); scrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() { @Override public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { // 在此处实现滚动监听的逻辑 } }); 在onScrollChange方法中,可以获取ScrollView滚动位置scrollX和scrollY,以及上一次的滚动位置oldScrollX和oldScrollY。通过这些参数,可以实现各种滚动监听的逻辑,例如:滚动到底部加载更多数据、滚动到顶部显示“回到顶部”按钮等。 ### 回答2: AndroidScrollView控件是一个可以放置多个子控件的视图容器,用于实现可滚动的UI界面。在Android中,我们可以通过设置ScrollView滚动监听来监听ScrollView控件的滚动状态,从而对UI界面进行动态的调整和处理。 ScrollView控件的滚动监听主要包括两个方面:滚动状态的监听和滚动位置的监听。 滚动状态的监听可以通过设置ScrollView的setOnScrollChangeListener()方法来实现。该方法会在ScrollView滚动状态改变时被调用,并传递当前ScrollView滚动状态、滚动距离以及滚动速度等相关参数。我们可以根据这些参数来动态地调整UI界面的布局和显示效果。 例如,我们可以利用滚动状态的监听来实现下拉刷新的效果:当用户下拉ScrollView时,我们可以通过监听ScrollView滚动状态来判断用户已经下拉到了一定的距离,并在这个状态下展示一个带有下拉箭头的刷新提示框。当用户松开手指,ScrollView返回到原来的位置时,我们可以通过滚动状态的监听来判断滚动是否已经停止,并执行数据更新的操作。 滚动位置的监听可以通过重写ScrollView的onScrollChanged()方法来实现。该方法会在ScrollView滚动位置发生改变时被调用,并传递当前ScrollView相对于顶部的偏移量。我们可以根据这个偏移量来实现一些动态的效果,比如浮动的导航栏、滚动到一定位置后自动展开的菜单等。 总之,ScrollView滚动监听在Android中是非常常用的功能,尤其是对于需要实现可滚动界面的应用程序。通过设置滚动监听,我们可以实现一些非常丰富和动态的UI效果,让应用程序的用户体验更加流畅和便捷。 ### 回答3: Android ScrollView是一个常用的控件,用于在屏幕上展示超过屏幕高度的内容。随着用户向下或向上滑动,这些内容会随之滚动。 在某些情况下,我们希望在用户滚动过程中对ScrollView进行一些操作。这时,就需要使用ScrollView滚动监听功能。 ScrollView滚动监听可以通过setOnScrollChangeListener()方法来实现。该方法需要传入一个OnScrollChangeListener接口,该接口中包含一个onScrollChange()方法,可以在ScrollView滚动时被调用。下面是一个简单的示例: scrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() { @Override public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { // 在此处添加需要执行的操作,例如: if(scrollY > oldScrollY) { Log.d("ScrollView", "向下滚动"); } else { Log.d("ScrollView", "向上滚动"); } } }); 在onScrollChange()方法中,我们可以获取ScrollView当前的滚动位置(即scrollX和scrollY)。如果要执行某些需要在滚动过程中不断改变的操作,例如某个View的透明度或位置,我们可以在此处进行计算并实时更新View的状态。 除此之外,我们还可以通过ScrollView的getScrollX()和getScrollY()方法来获取ScrollView当前的滚动位置。此外,我们可以从onScrollChange()方法中获取被滚动的View对象(即ScrollView本身)。这些方法都可以在我们需要在滚动过程中对ScrollView进行控制时有很大帮助。 总体而言,ScrollView滚动监听功能为我们在ScrollView滚动过程中添加各种操作提供了便利。结合其他Android UI控件,我们可以创建出更灵活、更多样化且物美价廉的用户界面。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值