滑动列表实现仿ios的尼阻回弹,可作用于 listView , gridView ,RecyclerView等列表组件,别的组件没试过。有兴趣的可以试一试。
实现原理:定义一个外层容器,内层可以是recyclerview,Listview,ScrollView等可以滑动的组件,主要是在事件分发上动手脚,在事件分发dispatchTouchEvent中根据内层recyclerview中可见的item是否处于顶部或者底部来处理事件分发(自己消费掉或者将事件交由当前view的onlnterceptTouchEvent进行事件拦截,而此处事件不拦截,交给recycelrview进行消费)
下面的自定义控件是一个viewGroup,列表控件嵌套在其中,可实现尼阻回弹效果,
其中ANIM_TIME这个常量代表的是回弹的时间,值越大,回弹速度越快,这个根据自己设置就好。
不多说。直接复制代码干了。。。
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.TranslateAnimation;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ScrollView;
import java.util.ArrayList;
import java.util.List;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
/**
* 阻尼回弹效果
*
* @author lixinxiao
*/
public class PullRecyclerViewGroup extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener {
/**
* 滚动时间
*/
private static final long ANIM_TIME = 200;
/**
* 如果按下时不能上拉和下拉,会在手指移动时更新为当前手指的Y值
*/
private View childView;
/**
* 如果按下时不能上拉和下拉,会在手指移动时更新为当前手指的Y值
*/
private Rect originalRect = new Rect();
//滚动时,移动的view和位置
private List<View> mMoveViews = new ArrayList<>();
private List<Rect> mMoveRects = new ArrayList<>();
/**
* 如果按下时不能上拉和下拉,会在手指移动时更新为当前手指的Y值
*/
private boolean isMoved = false;
/**
* 如果按下时不能上拉和下拉,会在手指移动时更新为当前手指的Y值
*/
private float startY;
/**
* 阻尼
*/
private static final float OFFSET_RADIO = 0.5f;
private boolean isRecyclerReuslt = false;
public PullRecyclerViewGroup(Context context) {
this(context, null);
}
public PullRecyclerViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PullRecyclerViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//关闭右侧滚动条
this.setVerticalScrollBarEnabled(false);
}
/**
* 加载布局后初始化,这个方法会在加载完布局后调用
*/
@Override
protected void onFinishInflate() {
//此处为容器中的子view 必须有RecyclerView、ListView、ScrollView,当然这里忽略ListView和ScrollView
if (getChildCount() > 0) {
for (int i = 0; i < getChildCount(); i++) {
if (getChildAt(i) instanceof RecyclerView || getChildAt(i) instanceof ListView || getChildAt(i) instanceof ScrollView) {
if (childView == null) {
childView = getChildAt(i);
} else {
throw new RuntimeException("PullRecyclerViewGroup 中只能存在一个RecyclerView、ListView或者ScrollView");
}
}
}
}
if (childView == null) {
throw new RuntimeException("PullRecyclerViewGroup 子容器中必须有一个RecyclerView、ListView或者ScrollView");
}
//布局重绘监听,比如华为屏幕键盘可以弹出和隐藏,改变布局,加监听就可以虽键盘弹出关闭的变化而变化
getViewTreeObserver().addOnGlobalLayoutListener(this);
super.onFinishInflate();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//ScrollView中唯一的子控件的位置信息,这个位置在整个控件的生命周期中保持不变
originalRect.set(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
for (int i = 0; i < mMoveViews.size(); i++) {
final View v = mMoveViews.get(i);
v.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
Rect rect = new Rect();
rect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
mMoveRects.add(rect);
v.removeOnLayoutChangeListener(this);
}
});
}
}
/**
* 事件分发
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (childView == null) {
return super.dispatchTouchEvent(ev);
}
//如果当前view的Y上的位置
boolean isTouchOutOfScrollView = ev.getY() >= originalRect.bottom || ev.getY() <= originalRect.top;
//如果不在view的范围内
if (isTouchOutOfScrollView) {
//如果不在view的范围内
if (isMoved) {
recoverLayout();
}
return true;
}
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
//记录按下时的Y
startY = ev.getY();
case MotionEvent.ACTION_MOVE:
float nowY = ev.getY();
int scrollY = (int) (nowY - startY);
if ((isCanPullDown() && scrollY > 0) || (isCanPullUp() && scrollY < 0)
|| (isCanPullDown() && isCanPullUp())) {
int offset = (int) (scrollY * OFFSET_RADIO);
childView.layout(originalRect.left, originalRect.top + offset, originalRect.right, originalRect.bottom + offset);
for (int i = 0; i < mMoveViews.size(); i++) {
if (mMoveViews.get(i) != null) {
mMoveViews.get(i).layout(mMoveRects.get(i).left, mMoveRects.get(i).top + offset, mMoveRects.get(i).right, mMoveRects.get(i).bottom + offset);
}
}
isMoved = true;
isRecyclerReuslt = false;
return true;
} else {
startY = ev.getY();
isMoved = false;
isRecyclerReuslt = true;
recoverLayout();
return super.dispatchTouchEvent(ev);
}
case MotionEvent.ACTION_UP:
if (isMoved) {
recoverLayout();
}
if (isRecyclerReuslt) {
return super.dispatchTouchEvent(ev);
} else {
return true;
}
default:
return true;
}
}
/**
* 位置还原
*/
private void recoverLayout() {
if (!isMoved) {
//如果不在view的范围内
return;
}
for (int i = 0; i < mMoveViews.size(); i++) {
if (mMoveRects.get(i) != null) {
TranslateAnimation anims = new TranslateAnimation(0, 0, mMoveViews.get(i).getTop(), mMoveRects.get(i).top);
anims.setDuration(ANIM_TIME);
mMoveViews.get(i).startAnimation(anims);
mMoveViews.get(i).layout(mMoveRects.get(i).left, mMoveRects.get(i).top, mMoveRects.get(i).right, mMoveRects.get(i).bottom);
}
}
TranslateAnimation anim = new TranslateAnimation(0, 0, childView.getTop() - originalRect.top, 0);
anim.setDuration(ANIM_TIME);
childView.startAnimation(anim);
childView.layout(originalRect.left, originalRect.top, originalRect.right, originalRect.bottom);
isMoved = false;
}
/**
* 容器的的事件都在事件分发中处理,这里处理的是事件分发传递过来的事件,
*
* 传递过来的为RecyclerVIew的事件 不拦截,直接交给reyclerview处理
*
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
/**
* 判断是否可以下拉
*
* @return
*/
private boolean isCanPullDown() {
final RecyclerView.Adapter adapter = ((RecyclerView) childView).getAdapter();
if (null == adapter) {
return true;
}
final int firstVisiblePosition = ((LinearLayoutManager) ((RecyclerView) childView).getLayoutManager()).findFirstVisibleItemPosition();
if (firstVisiblePosition != 0 && adapter.getItemCount() != 0) {
return false;
}
int mostTop = (((RecyclerView) childView).getChildCount() > 0) ? ((RecyclerView) childView).getChildAt(0).getTop() : 0;
return mostTop >= 0;
}
/**
* 判断是否可以上拉
*
* @return
*/
private boolean isCanPullUp() {
final RecyclerView.Adapter adapter = ((RecyclerView) childView).getAdapter();
if (null == adapter) {
return true;
}
final int lastItemPosition = adapter.getItemCount() - 1;
final int lastVisiblePosition = ((LinearLayoutManager) ((RecyclerView) childView).getLayoutManager()).findLastVisibleItemPosition();
if (lastVisiblePosition >= lastItemPosition) {
final int childIndex = lastVisiblePosition - ((LinearLayoutManager) ((RecyclerView) childView).getLayoutManager()).findFirstVisibleItemPosition();
final int childCount = ((RecyclerView) childView).getChildCount();
final int index = Math.min(childIndex, childCount - 1);
final View lastVisibleChild = ((RecyclerView) childView).getChildAt(index);
if (lastVisibleChild != null) {
return lastVisibleChild.getBottom() <= childView.getBottom() - childView.getTop();
}
}
return false;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onGlobalLayout() {
//华为手机屏幕下方的返回、home键显示隐藏改变布局
requestLayout();
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
/**
* 跟随弹性移动的view
*
* @param view
*/
public void setMoveViews(View view) {
this.mMoveViews.add(view);
requestLayout();
}
}
上面是java代码的实现。下面再贴上xml代码
<com.yikelive.view.PullRecyclerViewGroup
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_siteTitle">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_live_detail_ticket"/>
com.yikelive.view.PullRecyclerViewGroup>
然后就没有了。。。