Android上的类似iOS过度滚动效果

22

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

我偶然发现了这个链接,它建议创建一个自定义的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);
    }
}
5个回答

41

我基于 CoordinatorLayout.Behavior 快速搭建了一个简单的解决方案。虽然不是完美的,但你可以花点时间微调一下,它还是不错的。无论如何,最终结果应该像这样:

enter image description here

在开始回答之前,先小小提示一下:强烈建议使用支持库中的 NestedScrollView,而不是普通的 ScrollView。两者没有任何不同,但是 NestedScrollView 在较低的 API 级别上实现了正确的嵌套滚动行为。

接下来让我们开始回答:我提出的解决方案适用于任何可滚动的容器,例如 ScrollViewListViewRecyclerView,而且你不需要子类化任何 Views 来实现它。

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

compile 'com.android.support:design:25.0.1'
记住,如果您没有针对API 25进行目标设置(顺便说一下,这是您应该做的),那么您需要包括适用于您的API级别的最新版本(例如,对于API级别24,使用compile 'com.android.support:design:24.2.0')。
无论您使用什么可滚动容器,在布局中都需要将其包装在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。在这种情况下,我们将为 NestedScrollView 分配一个实现过度滚动反弹效果的 Behavior

让我们来看一下 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中实现这个效果? - Star
@XaverKapeller,你能添加fling相关的代码吗? - Bhargav
@Bhargav 这只是一个简单的示例,展示了一种实现此功能的方法。添加更复杂的滚动和飞快逻辑会使其变得更加复杂,并使答案变得不太有用。这不应该是一个每个人都可以简单复制和粘贴的成品解决方案 - 它旨在演示概念。如果您想要一个已完成的可以直接在您的应用程序中使用的东西,请寻找库 - 您应该在GitHub上找到很多。 - Xaver Kapeller
@XaverKapeller 我觉得我不应该为此创建一个新问题,我想了解你在onFling中如何处理速度,我觉得将它概念化地添加到这个答案中(不需要代码)会让你的答案更完整。基本上你有velocityY,现在怎么确定距离? - Bhargav
@Bhargav 只需创建一个新问题并在评论中链接即可。 - CQM
显示剩余3条评论

5
感谢 Xaver Kapeller,我使用 Kotlin 和 AndroidX,通过覆盖 fling 和添加一些小功能来编写了我的解决方案。

enter image description here

添加协调者依赖
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) {
    if (overScrollY == 0) {
        return
    }
    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标记中。

1
如果你想要使用库,那么这个Bouncy 库是最适合你需求的。

0

这对我完美地工作

Overscroll-decor-android

将以下内容添加到您的模块的build.gradle文件中:

    dependencies {

  // ...

 compile 'me.everything:overscroll-decor-android:1.0.4' 
}

然后导入

import me.everything.android.ui.overscroll.OverScrollDecoratorHelper;

现在你可以使用以下代码设置任何视图

OverScrollDecoratorHelper.setUpStaticOverScroll(scrollView, OverScrollDecoratorHelper.ORIENTATION_VERTICAL);

-8

使用这个

Private ScrollView scrMain;

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

OverScrollDecorHandler.setScrollView(scrMain); 

糟糕的解决方案 - Alon Shlider

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接