一、前提
接到新需求,要求列表滑动过程增加阻尼回弹效果,且即使列表不能填充一整个屏幕的情况下也支持滑动。
有人说,给RecyclerView加上 android:overScrollMode="always" 就行了,事实证明,NO!这个东西只是在滑动到边缘是多了个水波阴影而已,没有阻尼回弹。
又有人说,给ListView加上 android:overScrollMode="always" 就行了,经过尝试,貌似可以,但是有bug,还相当严重。况且我还要把RecyclerView改成ListView,太麻烦了。
本着不重复造轮子的前提,搜索了一大波。github上面也有很多现成的框架,不过由于项目要求,不能随便引入框架,所以不敢直接depend,本来想copy源码改吧改吧,后来发现这些框架做的都很“大”,冗余功能代码量多,索性放弃。后来终于找到一个简单的实现方式。借鉴作者思路,继续修改使之符合我的功能要求。站在巨人的肩膀上会让成功来的更快!
二、直接上代码:
package com.zzz.test.view.widget;
import android.content.Context;
import android.graphics.Rect;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.LinearLayout;
/**
* @author xxx
* @date 20-4-29
*/
public class OverScrollLayout extends LinearLayout {
private static final int ANIM_TIME = 400;
private RecyclerView childView;
private Rect original = new Rect();
private boolean isMoved = false;
private float startYpos;
/**
* 阻尼系数
*/
private static final float DAMPING_COEFFICIENT = 0.3f;
private boolean isSuccess = false;
private ScrollListener mScrollListener;
public OverScrollLayout(Context context) {
this(context, null);
}
public OverScrollLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public OverScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
childView = (RecyclerView) getChildAt(0);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
original.set(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
}
public void setScrollListener(ScrollListener listener) {
mScrollListener = listener;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float touchYpos = ev.getY();
if (touchYpos >= original.bottom || touchYpos <= original.top) {
if (isMoved) {
recoverLayout();
}
return true;
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startYpos = ev.getY();
case MotionEvent.ACTION_MOVE:
int scrollYpos = (int) (ev.getY() - startYpos);
boolean pullDown = scrollYpos > 0 && canPullDown();
boolean pullUp = scrollYpos < 0 && canPullUp();
if (pullDown || pullUp) {
cancelChild(ev);
int offset = (int) (scrollYpos * DAMPING_COEFFICIENT);
childView.layout(original.left, original.top + offset, original.right, original.bottom + offset);
if (mScrollListener != null) {
mScrollListener.onScroll();
}
isMoved = true;
isSuccess = false;
return true;
} else {
startYpos = ev.getY();
isMoved = false;
isSuccess = true;
return super.dispatchTouchEvent(ev);
}
case MotionEvent.ACTION_UP:
if (isMoved) {
recoverLayout();
}
return !isSuccess || super.dispatchTouchEvent(ev);
default:
return true;
}
}
/**
* 取消子view已经处理的事件
*
* @param ev event
*/
private void cancelChild(MotionEvent ev) {
ev.setAction(MotionEvent.ACTION_CANCEL);
super.dispatchTouchEvent(ev);
}
/**
* 位置还原
*/
private void recoverLayout() {
TranslateAnimation anim = new TranslateAnimation(0, 0, childView.getTop() - original.top, 0);
anim.setDuration(ANIM_TIME);
childView.startAnimation(anim);
childView.layout(original.left, original.top, original.right, original.bottom);
isMoved = false;
}
/**
* 判断是否可以下拉
*
* @return true:可以,false:不可以
*/
private boolean canPullDown() {
final int firstVisiblePosition = ((LinearLayoutManager) childView.getLayoutManager()).findFirstVisibleItemPosition();
if (firstVisiblePosition != 0 && childView.getAdapter().getItemCount() != 0) {
return false;
}
int mostTop = (childView.getChildCount() > 0) ? childView.getChildAt(0).getTop() : 0;
return mostTop >= 0;
}
/**
* 判断是否可以上拉
*
* @return true:可以,false:不可以
*/
private boolean canPullUp() {
final int lastItemPosition = childView.getAdapter().getItemCount() - 1;
final int lastVisiblePosition = ((LinearLayoutManager) childView.getLayoutManager()).findLastVisibleItemPosition();
if (lastVisiblePosition >= lastItemPosition) {
final int childIndex = lastVisiblePosition - ((LinearLayoutManager) childView.getLayoutManager()).findFirstVisibleItemPosition();
final int childCount = childView.getChildCount();
final int index = Math.min(childIndex, childCount - 1);
final View lastVisibleChild = childView.getChildAt(index);
if (lastVisibleChild != null) {
return lastVisibleChild.getBottom() <= childView.getBottom() - childView.getTop();
}
}
return false;
}
public interface ScrollListener {
/**
* 滚动事件回调
*/
void onScroll();
}
}
使RecyclerView支持阻尼回弹的话,把RecyclerView放到OverScrollView中,其他照旧。要想监听阻尼滑动,直接设置scrollListener。