iOS 喜欢 Android 上的滚动效果

iOS like over scroll effect on Android

我想在我的应用中实现 iOS-like 反弹滚动效果。

我发现了这个 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);
    }
}

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

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

无论如何让我们从我的答案开始:我想出的解决方案适用于任何可滚动容器,无论是 ScrollViewListView 还是 RecyclerView 而你不需要 subclass any 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 分配给它的直接子视图。在这种情况下,我们要将 Behavior 分配给 NestedScrollView 以实现滚动反弹效果。

我们来看看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() 方法中我们 return true 因为我们对任何滚动事件感兴趣。在 onNestedScroll() 中,我们查看 dyUnconsumed 参数,它告诉我们有多少垂直滚动没有被滚动容器消耗(换句话说就是过度滚动),然后将滚动容器的子元素平移该数量.由于我们只是获取增量值,因此我们需要在 mOverscrollY 变量中对所有这些值求和。 onStopNestedScroll() 在滚动事件停止时调用。这是我们将滚动容器的所有子元素动画化回其原始位置的时候。

要将 Behavior 分配给 NestedScrollView,我们需要使用 layout_behavior xml 属性并传入 class 的完整名称 Behavior我们要用。在我的示例中,上面的 class 在包 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 的基础知识。因此,如果您不熟悉这些或有任何其他问题,请随时提出。

使用这个

Private ScrollView scrMain;

scrMain = (ScrollView) v.findViewbyId(R.id.scrMain);

OverScrollDecorHandler.setScrollView(scrMain); 

感谢 Xaver Kapeller,我使用 kotlin 编写了我的解决方案,其中包含覆盖 fling 和少量添加androidx

添加协调器依赖

implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"

创建一个扩展 CoordinatorLayout.Behavior

的新 class
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 标记

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