Android Studio实现类似iOS的bouncable弹性回弹ScrollView问题求助
Android平台iOS风格弹性回弹ScrollView实现方案
你之前自定义实现的卡顿核心原因是仅修改了overScrollBy的最大过滚阈值,没有适配嵌套滚动逻辑,也没有优化回弹动画的插值器参数,滚动事件和父容器冲突时就会出现偶发卡顿。以下是无第三方依赖、适配全版本的稳定实现方案:
自定义BounceScrollView完整实现
import android.content.Context; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.widget.Scroller; import android.widget.ScrollView; public class BounceScrollView extends ScrollView { // 回弹阻尼系数,越大拉动越费力,建议取值0.3-0.7 private static final float DAMPING_COEFFICIENT = 0.5f; // 回弹动画时长 private static final int ANIM_DURATION = 300; private Scroller mScroller; // 原始内容视图 private View mContentView; // 按下时的Y坐标 private float mDownY; // 内容视图原始的顶部边距 private int mOriginalTop; // 最大过滚距离 private int mMaxOverScrollY; public BounceScrollView(Context context) { super(context); init(); } public BounceScrollView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public BounceScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { // 禁用系统自带边缘发光效果,避免冲突 setOverScrollMode(OVER_SCROLL_NEVER); // 开启嵌套滚动支持,适配和RecyclerView、CoordinatorLayout等嵌套场景 setNestedScrollingEnabled(true); mScroller = new Scroller(getContext(), new DecelerateInterpolator(1.5f)); // 计算适配屏幕密度的最大过滚距离 DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); mMaxOverScrollY = (int) (metrics.density * 250); } @Override protected void onFinishInflate() { super.onFinishInflate(); if (getChildCount() > 0) { mContentView = getChildAt(0); mOriginalTop = mContentView.getTop(); } } @Override public boolean onTouchEvent(MotionEvent ev) { if (mContentView == null) return super.onTouchEvent(ev); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mDownY = ev.getY(); break; case MotionEvent.ACTION_MOVE: float deltaY = ev.getY() - mDownY; mDownY = ev.getY(); // 滚动到顶部/底部时触发回弹逻辑 if ((getScrollY() == 0 && deltaY > 0) || (getChildAt(0).getMeasuredHeight() <= getHeight() + getScrollY() && deltaY < 0)) { int offset = (int) (deltaY * DAMPING_COEFFICIENT); // 限制最大偏移量 if (Math.abs(mContentView.getTop() - mOriginalTop + offset) < mMaxOverScrollY) { mContentView.layout( mContentView.getLeft(), mContentView.getTop() + offset, mContentView.getRight(), mContentView.getBottom() + offset ); } return true; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // 松手后回弹到原始位置 int currentTop = mContentView.getTop(); int offset = currentTop - mOriginalTop; if (offset != 0) { mScroller.startScroll(0, currentTop, 0, -offset, ANIM_DURATION); invalidate(); } break; } return super.onTouchEvent(ev); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { mContentView.layout( mContentView.getLeft(), mScroller.getCurrY(), mContentView.getRight(), mScroller.getCurrY() + mContentView.getMeasuredHeight() ); postInvalidate(); } super.computeScroll(); } @Override protected void onDetachedFromWindow() { if (!mScroller.isFinished()) { mScroller.abortAnimation(); } super.onDetachedFromWindow(); } }
方案优势
- 无任何第三方依赖,兼容Android 5.0及以上所有系统版本
- 支持嵌套滚动,和CoordinatorLayout、RecyclerView、ViewPager2等组件嵌套无冲突
- 回弹动画使用系统原生Scroller实现,不会出现偶发卡顿问题
- 可自定义阻尼系数、最大过滚距离、动画时长参数,适配不同的体验需求
使用注意事项
- ScrollView内部仅保留一个直接子容器,符合系统ScrollView的标准使用规范
- 如果需要嵌套其他可滚动组件,给子滚动组件开启
android:nestedScrollingEnabled="true"属性即可
内容的提问来源于stack exchange,提问作者aland sahand
相关产品推荐
相关产品推荐

