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 级别上实现了正确的嵌套滚动行为。
无论如何让我们从我的答案开始:我想出的解决方案适用于任何可滚动容器,无论是 ScrollView
、ListView
还是 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_behavior
是 CoordinatorLayout
的自定义属性,所以我们需要在它前面加上正确的命名空间:
<?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
属性。
这就是您要做的一切!虽然这个答案比我预期的要长,但我跳过了一些关于 CoordinatorLayout
和 Behaviors
的基础知识。因此,如果您不熟悉这些或有任何其他问题,请随时提出。
使用这个
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 是最好的库,它最符合您的要求。
我想在我的应用中实现 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 级别上实现了正确的嵌套滚动行为。
无论如何让我们从我的答案开始:我想出的解决方案适用于任何可滚动容器,无论是 ScrollView
、ListView
还是 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_behavior
是 CoordinatorLayout
的自定义属性,所以我们需要在它前面加上正确的命名空间:
<?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
属性。
这就是您要做的一切!虽然这个答案比我预期的要长,但我跳过了一些关于 CoordinatorLayout
和 Behaviors
的基础知识。因此,如果您不熟悉这些或有任何其他问题,请随时提出。
使用这个
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
的新 classimport 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 是最好的库,它最符合您的要求。