Android中是没有下拉和上拉的回弹效果的,但是有滚动到顶部和底部的光影效果,由EdgeEffect类来操作的。但是我们发现QQ,微信,支付宝等在下拉和上拉的都有一定的回弹效果,用户操作起来感觉蛮流畅的~
源码链接:https://github.com/chuwuwang/ZhouTools
效果
最近仿照着写来一个ListView和ScrollView的相同效果,用起来也是蛮方便的,先看下效果。
原理
在网上搜索了一下,也有不少的实现思路。
关于overScrollBy()方法,只要改变返回中的maxOverScrollY的值。比如,你改成200,那下拉和上拉就有200距离的回弹效果。经过测试,发现好像在4.4以上版本均有bug(4.4以下版本没有测试机,无法考证),需要我们修改,遂放弃用此方法。
@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, maxOverScrollY,
isTouchEvent);
}
实现
下面讲讲我的实现。
对于ListView的回弹效果,我用了改变padding值来实现的。也没有什么实现技巧,各位一看代码就明白了。说一点,当我们有回弹效果的时候,就应该不需要官方自带的光影效果了。
// 禁用下拉到两端发荧光的效果
setOverScrollMode(OVER_SCROLL_NEVER);
还有一点,如果当前正在回弹,这时候我们又来滑动,这时候需要处理下。因为我是用Handler来实现,只要清空所有的消息队列即可。
...
case MotionEvent.ACTION_MOVE:
// 清空所有消息队列
mHandler.removeCallbacksAndMessages(null);
...
对于ScrollView的回弹效果,我用了改变布局大小来实现的。主要需要判断ScrollView是滑动到顶部还是底部,ScrollView有onOverScrolled()和onScrollChanged()这两个方法,分别都可以判断是否滑动到顶部和底部,但是稍微有点区别,这里就不细说了。
如果不通过调用上面方法,手动计算是否滑动到顶部和底部的话。需要明白ScrollView的高度是怎样计算的。
无padding的情况可以转换为有padding的情况,即tp,bp=0。
mScrollY + H – tp – bp = h ===> mScrollY + H = h
大多数情况下都可以这样计算,但是有个别及其特殊的机器,该方法是不准确的。(少数,我测试过,大部分的小米,华为,魅族,锤子都是可以的,我们公司自己生产的机器不行,NND)
主要是因为对于底部的计算和ScrollView自己的padding和子View的margin有关。
万一遇到这种奇葩机型,我们还需要去除子View的margin值。
不管是ListView还是ScrollView,都有一个方法,设置回弹速度的。
/**
* 设置回弹的速度。值越大,速度越快。默认为30。
*/
public void setSpringBackSpeed(int speed) {
if (speed <= 0) {
throw new RuntimeException("speed 不能小于或者等于0");
}
this.speed = speed;
}
代码
SpringBackListView.java
package com.geek.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ListView;
/**
* 有回弹效果的ListView
*
* @author leeshenzhou on 2016/12/01.
*/
public class SpringBackListView extends ListView implements OnScrollListener {
private int paddingLeft;
private int paddingRight;
private int paddingTop;
private int paddingBottom;
// true下拉.false上拉
private boolean isPull;
// 是否滚动到第一行
private boolean isTop;
// 是否滚动到最后一行
private boolean isBottom;
private int scrollState;
private float mDownY;
private float mLastY;
private Handler mHandler = new Handler();
// 回弹的速度
private int speed = 20;
/**
* 设置回弹的速度。值越大,速度越快。默认为20。
*/
public void setSpringBackSpeed(int speed) {
if (speed <= 0) {
throw new RuntimeException("speed 不能小于或者等于0");
}
this.speed = speed;
}
public SpringBackListView(Context context) {
super(context);
init();
}
public SpringBackListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SpringBackListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
// 初始化padding的值
paddingLeft = getPaddingLeft();
paddingRight = getPaddingRight();
paddingTop = getPaddingTop();
paddingBottom = getPaddingBottom();
setOnScrollListener(this);
// 禁用下拉到两端发荧光的效果
setOverScrollMode(OVER_SCROLL_NEVER);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
// 清空所有消息队列
mHandler.removeCallbacksAndMessages(null);
mLastY = ev.getY();
int offset = (int) ((mLastY - mDownY) / 2.5);
isPull = offset > 0;
if (isPull) { // 下拉操作
if (isTop && scrollState != SCROLL_STATE_FLING) {
offset += paddingTop;
setPadding(paddingLeft, offset, paddingRight, paddingBottom);
setSelection(0); // 选中第一个item.不然没有下拉效果
}
} else { // 上拉
if (isBottom && scrollState != SCROLL_STATE_FLING) {
offset -= paddingBottom;
setPadding(paddingLeft, paddingTop, paddingRight, -offset);
setSelection(getCount() - 1); // 选中最后一个item.不然没有上拉效果
}
}
break;
case MotionEvent.ACTION_UP:
if (isPull) { // 下拉操作
int top = getPaddingTop();
int duration = 0;
while (top > paddingTop) {
top -= speed;
duration += 10;
final int pt = top;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (pt < paddingTop) {
// 如果回弹的距离小于初始的paddingTop值,则恢复原始状态
setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
} else {
setPadding(paddingLeft, pt, paddingRight, paddingBottom);
}
}
}, duration);
}
} else { // 上拉
int bottom = getPaddingBottom();
int duration = 0;
while (bottom > paddingBottom) {
bottom -= speed;
duration += 10;
final int pb = bottom;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (pb < paddingBottom) {
// 如果回弹的距离小于初始的paddingBottom值,则恢复原始状态
setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
} else {
setPadding(paddingLeft, paddingTop, paddingRight, pb);
}
}
}, duration);
}
}
break;
}
return super.onTouchEvent(ev);
}
@Override
public void onScroll(AbsListView lv, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
isTop = firstVisibleItem == 0;
isBottom = firstVisibleItem + visibleItemCount == totalItemCount;
}
@Override
public void onScrollStateChanged(AbsListView lv, int scrollState) {
this.scrollState = scrollState;
}
/**
* 这是一个很奇怪的方法。哈哈
*/
@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, maxOverScrollY,
isTouchEvent);
}
}
SpringBackScrollView.java
package com.geek.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ScrollView;
/**
* 有回弹效果的ScrollView
*
* @author lee.shenzhou
*
*/
public class SpringBackScrollView extends ScrollView {
private float mDownY;
private float mFirstY;
// 子View
private View childView;
// 初始的位置
private Rect normal = new Rect();
private Handler mHandler = new Handler();
private int speed = 30;
private boolean isPull;
/**
* 设置回弹的速度。值越大,速度越快。默认为30。
*/
public void setSpringBackSpeed(int speed) {
if (speed <= 0) {
throw new RuntimeException("speed 不能小于或者等于0");
}
this.speed = speed;
}
public SpringBackScrollView(Context context) {
super(context);
init();
}
public SpringBackScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
// 禁用下拉到两端发荧光的效果
setOverScrollMode(OVER_SCROLL_NEVER);
}
@Override
protected void onFinishInflate() {
childView = getChildAt(0);
if (childView != null) {
normal.set(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
}
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (childView != null) {
handleScrollTouchEvent(ev);
}
return super.onTouchEvent(ev);
}
public void handleScrollTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownY = ev.getY();
mFirstY = ev.getY();
break;
case MotionEvent.ACTION_UP:
springBackLocation();
break;
case MotionEvent.ACTION_MOVE:
// 移除滑动的消息队列
mHandler.removeCallbacksAndMessages(null);
final float preY = mDownY;
final float nowY = ev.getY();
isPull = nowY - mFirstY > 0;
int deltaY = (int) ((preY - nowY) / 2.5);
mDownY = nowY;
// 当滚动到最上或者最下时就不会再滚动,这时移动布局
if (isNeedMove()) {
// 保存正常的布局位置
if (normal.isEmpty()) {
normal.set(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
return;
}
// 移动布局
childView.layout(childView.getLeft(), childView.getTop() - deltaY,
childView.getRight(), childView.getBottom() - deltaY);
}
break;
default:
break;
}
}
/**
* 回弹到原始位置
*/
public void springBackLocation() {
final int nowTop = childView.getTop();
final int nowBottom = childView.getBottom();
final int originTop = normal.top;
final int originBottom = normal.bottom;
Log.i("nsz", "nowTop:" + nowTop + " nowBottom:" + nowBottom
+ " originTop:" + originTop + " originBottom:" + originBottom);
// 下拉回弹
if (isPull) {
int moveTop = nowTop;
int moveBottom = nowBottom;
int duration = 0;
while (moveTop >= originTop) {
moveTop -= speed;
moveBottom -= speed;
duration += 10;
final int offTop = moveTop;
final int offBottom = moveBottom;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (offTop <= originTop || offBottom <= originBottom) {
childView.layout(normal.left, normal.top, normal.right, normal.bottom);
} else {
childView.layout(normal.left, offTop, normal.right, offBottom);
}
}
}, duration);
}
}
// 上拉回弹
else {
int moveTop = nowTop;
int moveBottom = nowBottom;
int duration = 0;
while (moveTop <= originTop) {
moveTop += speed;
moveBottom += speed;
duration += 10;
final int offTop = moveTop;
final int offBottom = moveBottom;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (offTop >= originTop || offBottom >= originBottom) {
childView.layout(normal.left, normal.top, normal.right, normal.bottom);
} else {
childView.layout(normal.left, offTop, normal.right, offBottom);
}
}
}, duration);
}
}
}
/**
* 是否需要移动布局
*/
public boolean isNeedMove() {
// 注意:慎重选择
// 子View的margin和自己的padding对移动有影响,所以子View最好不要设置marginTop和marginBottom。
// 如果设置了,对判断滑动到底部有些不准确,需要加上下面注释掉margin值,但是不同的机器,测试出有点不一样。
// 获取到子View的margin值
// LayoutParams params = (LayoutParams) childView.getLayoutParams();
// int topMargin = params.topMargin;
// int bottomMargin = params.bottomMargin;
// int offset = childView.getHeight() - getHeight() + getPaddingBottom() + getPaddingTop() + topMargin + bottomMargin;
int offset = childView.getHeight() - getHeight() + getPaddingBottom() + getPaddingTop();
int scrollY = getScrollY();
if (scrollY == 0) {
return true;
} else if (scrollY == offset) {
return true;
}
return false;
}
}