实现上下回弹阻尼效果
通过继承 ScrollView
,重新写其方法overScrollBy
,可轻松实现ScrollView
的上下回弹效果,类似iOS
系统中的阻尼拉伸效果。
public class OverScrollView extends ScrollView {
public OverScrollView(Context context) {
super(context);
}
public OverScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public OverScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public OverScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected boolean overScrollBy(
int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent) {
return super.overScrollBy(
deltaX, deltaY, scrollX, scrollY,
scrollRangeX, scrollRangeY, maxOverScrollX, 150,
isTouchEvent);
}
}
以上实现了垂直方向上的回弹拉伸效果,如果是水平ScrollView
, 只需修改maxOverScrollX
的值即可。
ScrollView
有三个overscroll mode属性,表示允许回弹拉伸的时机:
/**
* Always allow a user to over-scroll this view, provided it is a
* view that can scroll.
*
* @see #getOverScrollMode()
* @see #setOverScrollMode(int)
*/
public static final int OVER_SCROLL_ALWAYS = 0;
/**
* Allow a user to over-scroll this view only if the content is large
* enough to meaningfully scroll, provided it is a view that can scroll.
*
* @see #getOverScrollMode()
* @see #setOverScrollMode(int)
*/
public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1;
/**
* Never allow a user to over-scroll this view.
*
* @see #getOverScrollMode()
* @see #setOverScrollMode(int)
*/
public static final int OVER_SCROLL_NEVER = 2;
默认为OVER_SCROLL_IF_CONTENT_SCROLLS
,您可根据需求在布局文件中声明overscroll mode:
实现top、bottom不同的拉伸距离
我们从overScrollBy
方法下手,来看下ScrollView各层级之间的继承关系:
View
→ViewGroup
→FrameLayout
→ScrollView
ViewGroup
、FrameLayout
、ScrollView
并未重写overScrollBy
方法,而是直接继承自View
的overScrollBy
方法。
下面我们在ScrollView
的子类OverScrollView
中重写overScrollBy
方法,直接复制View
中的overScrollBy
方法内的实现:
package com.example.demo.demo;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.widget.ScrollView;
public class OverScrollView extends ScrollView {
private int mTopOverScrollDistance = 150;
private int mBottomOverScrollDistance = 90;
@Override
protected boolean overScrollBy(
int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent) {
final int overScrollMode = getOverScrollMode();
final boolean canScrollHorizontal =
computeHorizontalScrollRange() > computeHorizontalScrollExtent();
final boolean canScrollVertical =
computeVerticalScrollRange() > computeVerticalScrollExtent();
final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS ||
(overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS ||
(overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);
int newScrollX = scrollX + deltaX;
if (!overScrollHorizontal) {
maxOverScrollX = 0;
}
int newScrollY = scrollY + deltaY;
if (!overScrollVertical) {
mTopOverScrollDistance = 0;
mBottomOverScrollDistance = 0;
}
// Clamp values if at the limits and record
final int left = -maxOverScrollX;
final int right = maxOverScrollX + scrollRangeX;
final int top = -mTopOverScrollDistance;
final int bottom = mBottomOverScrollDistance + scrollRangeY;
boolean clampedX = false;
if (newScrollX > right) {
newScrollX = right;
clampedX = true;
} else if (newScrollX < left) {
newScrollX = left;
clampedX = true;
}
boolean clampedY = false;
if (newScrollY > bottom) {
newScrollY = bottom;
clampedY = true;
} else if (newScrollY < top) {
newScrollY = top;
clampedY = true;
}
onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
return clampedX || clampedY;
}
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
}
}
但是需要将maxOverScrollY
用声明的top拉伸大小mTopOverScrollDistance
和bottom的拉伸大小mBottomOverScrollDistance
做如上替换。
设置top、bottom仅其一支持拉伸回弹
我们知道,一旦设置了overScrollMode
,上下回弹就会一起生效,可能有些需求只需保持top回弹拉伸效果,而bottom不需要,这里有几种实现方式:
- 令
mTopOverScrollDistance
或mBottomOverScrollDistance
的值为0,这是一种简单粗暴的实现方式。 - 修改
overScrollBy
方法,判断是否超过底部和顶部,并做逻辑处理。 - 修改
ScrollView
的onTouchEvent
方法源码。
在这里简单介绍下第二种方式的实现,其他方式可以自行尝试实现:
public class OverScrollView extends ScrollView {
private int mTopOverScrollDistance = 150;
private int mBottomOverScrollDistance = 90;
private boolean mTopOverScrollEnable;
private boolean mBottomOverScrollEnable;
public OverScrollView(Context context) {
super(context);
}
public OverScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public OverScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public OverScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected boolean overScrollBy(
int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent) {
final int overScrollMode = getOverScrollMode();
final boolean canScrollHorizontal =
computeHorizontalScrollRange() > computeHorizontalScrollExtent();
final boolean canScrollVertical =
computeVerticalScrollRange() > computeVerticalScrollExtent();
final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS ||
(overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS ||
(overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);
int newScrollX = scrollX + deltaX;
if (!overScrollHorizontal) {
maxOverScrollX = 0;
}
int newScrollY = scrollY + deltaY;
if (!overScrollVertical) {
mTopOverScrollDistance = 0;
mBottomOverScrollDistance = 0;
}
// Clamp values if at the limits and record
final int left = -maxOverScrollX;
final int right = maxOverScrollX + scrollRangeX;
final int top = -mTopOverScrollDistance;
final int bottom = mBottomOverScrollDistance + scrollRangeY;
boolean clampedX = false;
if (newScrollX > right) {
newScrollX = right;
clampedX = true;
} else if (newScrollX < left) {
newScrollX = left;
clampedX = true;
}
boolean clampedY = false;
if (newScrollY > scrollRangeY) { //判断是否超过底部
if (!mBottomOverScrollEnable) {
newScrollY = scrollRangeY;
clampedY = true;
}
} else if (newScrollY < 0) { //判断是否超过顶部
if (!mTopOverScrollEnable) {
newScrollY = 0;
clampedY = true;
}
}
if (newScrollY > bottom) {
newScrollY = bottom;
clampedY = true;
} else if (newScrollY < top) {
newScrollY = top;
clampedY = true;
}
onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
return clampedX || clampedY;
}
}
通过对ScrollView
进行滑动到底部和顶部的逻辑判断,然后修改newScrollY
的值,来屏蔽top或bottom的回弹拉伸,上述只实现了垂直方式的逻辑,水平方式需另行实现。
ListView拉伸回弹效果
ListView
也可以实现上述效果,实现方式与ScrollView
大同小异。
延伸
通过对ScrollView
或ListView
的子类实现自定义属性,对mTopOverScrollDistance
、mBottomOverScrollDistance
、mTopOverScrollEnable
、mBottomOverScrollEnable
提供单独的set方法,可动态修改拉伸回弹效果。
PS:本文系本人原创,未经许可,禁止转载。