【问题标题】:iOS like over scroll effect on AndroidiOS 像 Android 上的过度滚动效果
【发布时间】:2017-04-07 02:50:46
【问题描述】:

我想在我的应用中实现类似 iOS 的反弹过度滚动效果。

我遇到了这个link,它建议创建一个自定义ScrollView。但问题是,当我快速上下滚动时,它工作正常,但只要我拉屏幕的底部或顶部,它就会卡住,效果不再起作用。

作为我想要实现的那种动画的一个例子,你可以看看这个:

这是我目前拥有的代码:

public class ObservableScrollView extends ScrollView
{
    private static final int MAX_Y_OVERSCROLL_DISTANCE = 150;

    private Context mContext;
    private int mMaxYOverscrollDistance;

    public ObservableScrollView(Context context)
    {
        super(context);
        mContext = context;
        initBounceScrollView();
    }

    public ObservableScrollView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        mContext = context;
        initBounceScrollView();
    }

    public ObservableScrollView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
        mContext = context;
        initBounceScrollView();
    }

    private void initBounceScrollView()
    {
        //get the density of the screen and do some maths with it on the max overscroll distance
        //variable so that you get similar behaviors no matter what the screen size

        final DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
        final float density = metrics.density;

        mMaxYOverscrollDistance = (int) (density * MAX_Y_OVERSCROLL_DISTANCE);
    }

    @Override
    protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent)
    {
        //This is where the magic happens, we have replaced the incoming maxOverScrollY with our own custom variable mMaxYOverscrollDistance;
        return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, mMaxYOverscrollDistance, isTouchEvent);
    }
}

【问题讨论】:

    标签: java android animation android-animation android-scrollview


    【解决方案1】:

    我很快就根据CoordinatorLayout.Behavior 制定了一个简单的解决方案。它并不完美,您也许可以花一些时间对其进行微调,但这还不错。无论如何,结果应该是这样的:

    在我开始回答之前,请注意一点:我强烈建议您使用支持库中的NestedScrollView,而不是普通的ScrollView。它们在任何方面都是相同的,但 NestedScrollView 在较低的 API 级别上实现了正确的嵌套滚动行为。

    无论如何,让我们从我的答案开始:我想出的解决方案适用于任何可滚动容器,无论是 ScrollViewListView 还是 RecyclerView,您不需要将任何 Views 子类化为实现它。

    首先,如果您还没有使用 Google 的设计支持库,您需要将它添加到您的项目中:

    compile 'com.android.support:design:25.0.1'
    

    请记住,如果您的目标不是 API 级别 25(顺便说一句,您应该这样做),那么您需要包含您的 API 级别的最新版本(例如,compile 'com.android.support:design:24.2.0' 用于 API 级别 24)。

    您使用的任何可滚动容器都需要在布局中包裹在CoordinatorLayout 中。在我的示例中,我使用的是 NestedScrollView:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <android.support.v4.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <!-- content -->
    
        </android.support.v4.widget.NestedScrollView>
    
    </android.support.design.widget.CoordinatorLayout>
    

    CoordinatorLayout 允许您将Behavior 分配给其直接子视图。在这种情况下,我们将分配一个BehaviorNestedScrollView,这将实现过度滚动反弹效果。

    我们来看看Behavior的代码:

    public class OverScrollBounceBehavior extends CoordinatorLayout.Behavior<View> {
    
        private int mOverScrollY;
    
        public OverScrollBounceBehavior() {
        }
    
        public OverScrollBounceBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
            mOverScrollY = 0;
            return true;
        }
    
        @Override
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            if (dyUnconsumed == 0) {
                return;
            }
    
            mOverScrollY -= dyUnconsumed;
            final ViewGroup group = (ViewGroup) target;
            final int count = group.getChildCount();
            for (int i = 0; i < count; i++) {
                final View view = group.getChildAt(i);
                view.setTranslationY(mOverScrollY);
            }
        }
    
        @Override
        public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
            final ViewGroup group = (ViewGroup) target;
            final int count = group.getChildCount();
            for (int i = 0; i < count; i++) {
                final View view = group.getChildAt(i);
                ViewCompat.animate(view).translationY(0).start();
            }
        }
    }
    

    解释Behavior 是什么以及它们如何工作超出了这个答案的范围,所以我将快速解释上述代码的作用。 Behavior 拦截在CoordinatorLayout 的直接子代中发生的所有滚动事件。在onStartNestedScroll() 方法中,我们返回true,因为我们对任何滚动事件都感兴趣。在onNestedScroll() 中,我们查看dyUnconsumed 参数,它告诉我们有多少垂直滚动没有被滚动容器消耗(换句话说,过度滚动),然后将滚动容器的子项转换该量。由于我们只是获得增量值,因此我们需要在 mOverscrollY 变量中汇总所有这些值。 onStopNestedScroll() 在滚动事件停止时被调用。这是我们将滚动容器的所有子项设置为动画回到其原始位置的时候。

    要将Behavior 分配给NestedScrollView,我们需要使用layout_behavior xml 属性并传入我们要使用的Behavior 的完整类名。在我的示例中,上述类位于包 com.github.wrdlbrnft.testapp 中,因此我必须将 com.github.wrdlbrnft.testapp.OverScrollBounceBehavior 设置为值。 layout_behaviorCoordinatorLayout 的自定义属性,所以我们需要在它前面加上正确的命名空间:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <android.support.v4.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="com.github.wrdlbrnft.testapp.OverScrollBounceBehavior">
    
            <!-- content -->
    
        </android.support.v4.widget.NestedScrollView>
    
    </android.support.design.widget.CoordinatorLayout>
    

    请注意我在CoordinatorLayout 上添加的命名空间和NestedScrollView 上添加的app:layout_behavior 属性。

    这就是你所要做的!虽然这个答案比我预期的要长,但我跳过了一些关于CoordinatorLayoutBehaviors 的基础知识。因此,如果您不熟悉这些内容或有任何其他问题,请随时提出。

    【讨论】:

    • 嗨@Xaver Kapler 感谢您的更新。还有一个疑问是,在此我们手动上下拉动以进行弹跳。但是在附加的屏幕 gif 中,如果我们滚动并且当它到达底部时,它会反弹回来。顶部也一样。我通过下载应用程序进行了检查。在scrollview中是如何实现的?
    • @Shadow 确定是的。您所需要的只是相同的数学运算,以指数方式钳制过度滚动。
    • @XaverKapeller 可以添加fling的相关代码吗?
    • @Bhargav 这只是一个简单的例子,展示了如何实现这一点的一种方式。添加更复杂的滚动和投掷逻辑会使它变得更加复杂,并使答案变得不那么有用。这不应该是一个现成的解决方案,每个人都可以复制和粘贴 - 它应该展示这个概念。如果你想完成一些可以在你的应用程序中使用的东西,然后寻找一个库 - 你应该在 GitHub 上找到很多。
    • @XaverKapeller 我觉得我不应该为此创建一个新问题,我想了解您如何处理 onFling 中的速度,我觉得从概念上(不需要代码)将它添加到这个答案应该使你的答案更完整。基本上你有velocityY,现在怎么确定距离?
    【解决方案2】:

    感谢 Xaver Kapeller,我使用 kotlinandroidx

    添加协调器依赖

    implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
    

    创建一个扩展 CoordinatorLayout.Behavior 的新类

    import android.content.Context
    import android.util.AttributeSet
    import android.view.View
    import android.view.ViewGroup
    import android.view.animation.AccelerateDecelerateInterpolator
    import androidx.coordinatorlayout.widget.CoordinatorLayout
    import androidx.core.view.ViewCompat
    
    class OverScrollBehavior(context: Context, attributeSet: AttributeSet)
    : CoordinatorLayout.Behavior<View>() {
    
    companion object {
        private const val OVER_SCROLL_AREA = 4
    }
    
    private var overScrollY = 0
    
    override fun onStartNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: View,
        directTargetChild: View,
        target: View,
        axes: Int,
        type: Int
    ): Boolean {
        overScrollY = 0
        return true
    }
    
    override fun onNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: View,
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int,
        consumed: IntArray
    ) {
        if (dyUnconsumed == 0) {
            return
        }
    
        overScrollY -= (dyUnconsumed/OVER_SCROLL_AREA)
        val group = target as ViewGroup
        val count = group.childCount
        for (i in 0 until count) {
            val view = group.getChildAt(i)
            view.translationY = overScrollY.toFloat()
        }
    }
    
    override fun onStopNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: View,
        target: View,
        type: Int
    ) {
        // Smooth animate to 0 when the user stops scrolling
        moveToDefPosition(target)
    }
    
    override fun onNestedPreFling(
        coordinatorLayout: CoordinatorLayout,
        child: View,
        target: View,
        velocityX: Float,
        velocityY: Float
    ): Boolean {
        // Scroll view by inertia when current position equals to 0
        if (overScrollY == 0) {
            return false
        }
        // Smooth animate to 0 when user fling view
        moveToDefPosition(target)
        return true
    }
    
    private fun moveToDefPosition(target: View) {
        val group = target as ViewGroup
        val count = group.childCount
        for (i in 0 until count) {
            val view = group.getChildAt(i)
            ViewCompat.animate(view)
                .translationY(0f)
                .setInterpolator(AccelerateDecelerateInterpolator())
                .start()
        }
    }
    
    }
    

    使用 CoordinatorLayout 和 NestedScrollView 创建 XML 文件

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.coordinatorlayout.widget.CoordinatorLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
        <androidx.core.widget.NestedScrollView
            android:id="@+id/scrollView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior=".OverScrollBehavior">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:textAlignment="center"
                android:padding="10dp"
                android:text="@string/Lorem"/>
        </androidx.core.widget.NestedScrollView>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
    

    别忘了添加

    app:layout_behavior=".OverScrollBehavior" // Or your file name
    

    NestedScrollView XML 标记的字段

    【讨论】:

      【解决方案3】:

      如果您想使用库,那么这个 Bouncy 是最符合您要求的最佳库。

      【讨论】:

        【解决方案4】:

        使用这个

        Private ScrollView scrMain;
        
        scrMain = (ScrollView) v.findViewbyId(R.id.scrMain);
        
        OverScrollDecorHandler.setScrollView(scrMain); 
        

        【讨论】:

        • 糟糕的解决方案
        猜你喜欢
        • 2013-07-22
        • 2015-03-02
        • 1970-01-01
        • 2020-02-26
        • 2021-11-26
        • 1970-01-01
        • 2021-07-17
        • 1970-01-01
        • 2013-04-09
        相关资源
        最近更新 更多