一个具有回弹效果的RecyclerView,本文通过实现RecyclerView外层的容器的上下滑动达到了回弹的效果,在整个滑动的事件分发机制中,外层容器的事件拦截机制进行判断是否拦截事件,判断标准为RecyclerView是否滚动到了第一个item或者最后一个item,如果下滑滚动到了第一个item还继续下滑,外层容器的事件拦截机制将此事件进行拦截,交给外层容器的onTouchEvent进行消费;上滑,同理。如果不拦截,将交给子view即RecyclerView进行消费。
下面是我自己总结的事件分发机制,希望对你能有帮助。不清楚的童鞋可以打印出来进行记忆。每天翻看几遍,死记住,以后你会慢慢的明白其原理。
dispatchTouchEvent(MotionEvent ev)事件分发
1.返回true,表示这件事由dispatchTouchEvent消费掉了,事件停止向下传递。
2.返回false,表示事件不分发,返回给上一层activity或者父控件中的onTouchEvent进行消费。
3.返回super.dispatchTouchEvent(ev),事件交由当前view的onlnterceptTouchEvent进行事件拦截。
onlnterceptTouchEvent(MotionEvent ev) 事件拦截
1.返回true,表示事件拦截成功,交由当前view的onTouchEvent消费。
2.返回false,表示事件放行,将事件传递给下一个子view进行处理(下一个子view的dispatchTouchEvent进行处理)。
3.返回super.onlnterceptTouchEvent(ev),表示事件拦截,交由当前view的onTouchEvent消费,同true。
onTouchEvent(MotionEvent ev) 事件响应
前提:当前控件事件获得响应
1.返回 false,事件将从当前的view向上传递,由父view的onTouchEvent来接收消费事件。
2.返回 true,事件被当前的view接收并消费掉。
3.返回onTouchEvent(ev),同false。
下面是这个容器的代码:
容器类
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Scroller;
/**
* Created by Sick on 2016/8/8.
* 自定义可滚动组件的弹性容器,仿IOS回弹效果
*/
public class RVScrollLayout extends LinearLayout {
private final String TAG = this.getClass().getSimpleName();
/**
* 容器中的组件
*/
private View convertView;
/**
* 如果容器中的组件为RecyclerView
*/
private RecyclerView recyclerView;
/**
* 滚动结束
*/
private int mStart;
/**
* 滚动结束
*/
private int mEnd;
/**
* 上一次滑动的坐标
*/
private int mLastY;
/**
* 滚动辅助类
*/
private Scroller mScroller;
public RVScrollLayout(Context context) {
this(context, null);
}
public RVScrollLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RVScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() > 1) {
throw new RuntimeException(RVScrollLayout.class.getSimpleName() + "只能有一个子控件");
}
convertView = getChildAt(0);
//TODO 可以拓展ListView等可滑动的组件
if (convertView instanceof RecyclerView) {
recyclerView = (RecyclerView) convertView;
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed) {
View view = getChildAt(0);
view.layout(left, top, right, bottom);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE:
if (!mScroller.isFinished()) {
mScroller.abortAnimation(); //终止动画
}
scrollTo(0, (int) ((mLastY - y) * 0.4));
break;
case MotionEvent.ACTION_UP:
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
/**
* 回弹动画,第一二个参数为开始的x,y
* 第三个和第四个参数为滚动的距离(注意方向问题)
* 第五个参数是回弹时间
*/
mScroller.startScroll(0, mEnd, 0, -dScrollY, 1000);
break;
}
postInvalidate();
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int y = (int) ev.getY();
Log.d(TAG, "相对于组件滑过的距离==getY():" + y);
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
/**
* 下面两个判断来自于 BGARefreshLayout 框架中的判断,github 上搜索 BGARefreshLayout
*/
if (convertView instanceof RecyclerView) {
if (y - mLastY > 0) {
if (Util.isRecyclerViewToTop(recyclerView)) {
Log.d(TAG, "滑倒顶部时时间拦截成功");
return true;
}
}
if (y - mLastY < 0) {
if (Util.isRecyclerViewToBottom(recyclerView)) {
Log.d(TAG, "滑倒底部时时间拦截成功");
return true;
}
}
}
break;
}
return false;
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
}
}
区别scrollBy()和scrollTo(),前者是在上一个动作的基础上移动,后者是从view的最开始移动,移动是相对于括号中的参数而言。
还有最重要的两点:第一,它们移动的是当前view中的content,当前viewgroup中的子view,在recyclerview中相当于移动的它里面的子item;第二,他们的移动,为什么说他们的移动是一个重点呢,因为如果你将移动的x,y直接写入参数,你会发现视图是反方向的移动,这里你可以将子item看成一个很大的画板,手机(recyclerview)看成一个中空的挡板,移动中空挡板就相当于子item移动了,所以它们的参数应该是一个负值。 即scrollBy(-offsetX,-offsetY)
getScrollY() 获取的是view或这viewgroup滚动过的距离。
再捋一捋这一流程的事件分发,首先滑动的时候事件是从外层向内层传递,本文中外层是容器,内层是recyclerview组件,事件分发给容器,容器进行判断是否拦截,return true表示拦截,在容器自身的onTouchEvent()中消费,如果return false将交给recyclerview进行消费,这里将不关注recyclerview。
起初我的思路是重写recyclerview的事件分发的方法,对外层容器传递过来的事件进行不分发返回给上层容器onTouchEvent()进行消费 或者 在它的OnTouchEvent()事件响应中返回return false 交给上层容器onTouchEvent()进行消费来达到容器可以回弹的效果。
父容器的回弹效果可以通过很多种方式进行实现,可以设置父容器的marginTop和marginBottom达到弹性的效果,而本文采用了Scroller辅助类滚动来实现的。
配置类(判断recyclerview滚动)
import android.graphics.Rect;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.View;
import android.view.ViewParent;
import java.lang.reflect.Field;
import cn.bingoogolapple.refreshlayout.BGAStickyNavLayout;
/**
* Created by Sick on 2016/8/10.
*/
public class Util {
public static boolean isRecyclerViewToTop(RecyclerView recyclerView) {
if (recyclerView != null) {
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if (manager == null) {
return true;
}
if (manager.getItemCount() == 0) {
return true;
}
if (manager instanceof LinearLayoutManager) {
LinearLayoutManager layoutManager = (LinearLayoutManager) manager;
int firstChildTop = 0;
if (recyclerView.getChildCount() > 0) {
// 处理item高度超过一屏幕时的情况
View firstVisibleChild = recyclerView.getChildAt(0);
if (firstVisibleChild != null && firstVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) {
if (android.os.Build.VERSION.SDK_INT < 14) {
return !(ViewCompat.canScrollVertically(recyclerView, -1) || recyclerView.getScrollY() > 0);
} else {
return !ViewCompat.canScrollVertically(recyclerView, -1);
}
}
// 如果RecyclerView的子控件数量不为0,获取第一个子控件的top
// 解决item的topMargin不为0时不能触发下拉刷新
View firstChild = recyclerView.getChildAt(0);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) firstChild.getLayoutParams();
firstChildTop = firstChild.getTop() - layoutParams.topMargin - getRecyclerViewItemTopInset(layoutParams) - recyclerView.getPaddingTop();
}
if (layoutManager.findFirstCompletelyVisibleItemPosition() < 1 && firstChildTop == 0) {
return true;
}
}
}
return false;
}
/**
* 通过反射获取RecyclerView的item的topInset
*
* @param layoutParams
* @return
*/
private static int getRecyclerViewItemTopInset(RecyclerView.LayoutParams layoutParams) {
try {
Field field = RecyclerView.LayoutParams.class.getDeclaredField("mDecorInsets");
field.setAccessible(true);
// 开发者自定义的滚动监听器
Rect decorInsets = (Rect) field.get(layoutParams);
return decorInsets.top;
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
public static boolean isRecyclerViewToBottom(RecyclerView recyclerView) {
if (recyclerView != null) {
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if (manager == null || manager.getItemCount() == 0) {
return false;
}
if (manager instanceof LinearLayoutManager) {
// 处理item高度超过一屏幕时的情况
View lastVisibleChild = recyclerView.getChildAt(recyclerView.getChildCount() - 1);
if (lastVisibleChild != null && lastVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) {
if (android.os.Build.VERSION.SDK_INT < 14) {
return !(ViewCompat.canScrollVertically(recyclerView, 1) || recyclerView.getScrollY() < 0);
} else {
return !ViewCompat.canScrollVertically(recyclerView, 1);
}
}
LinearLayoutManager layoutManager = (LinearLayoutManager) manager;
if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.getItemCount() - 1) {
BGAStickyNavLayout stickyNavLayout = getStickyNavLayout(recyclerView);
if (stickyNavLayout != null) {
// 处理BGAStickyNavLayout中findLastCompletelyVisibleItemPosition失效问题
View lastCompletelyVisibleChild = layoutManager.getChildAt(layoutManager.findLastCompletelyVisibleItemPosition());
if (lastCompletelyVisibleChild == null) {
return true;
} else {
// 0表示x,1表示y
int[] location = new int[2];
lastCompletelyVisibleChild.getLocationOnScreen(location);
int lastChildBottomOnScreen = location[1] + lastCompletelyVisibleChild.getMeasuredHeight();
stickyNavLayout.getLocationOnScreen(location);
int stickyNavLayoutBottomOnScreen = location[1] + stickyNavLayout.getMeasuredHeight();
return lastChildBottomOnScreen <= stickyNavLayoutBottomOnScreen;
}
} else {
return true;
}
}
} else if (manager instanceof StaggeredGridLayoutManager) {
StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) manager;
int[] out = layoutManager.findLastCompletelyVisibleItemPositions(null);
int lastPosition = layoutManager.getItemCount() - 1;
for (int position : out) {
if (position == lastPosition) {
return true;
}
}
}
}
return false;
}
public static BGAStickyNavLayout getStickyNavLayout(View view) {
ViewParent viewParent = view.getParent();
while (viewParent != null) {
if (viewParent instanceof BGAStickyNavLayout) {
return (BGAStickyNavLayout) viewParent;
}
viewParent = viewParent.getParent();
}
return null;
}
}
Activity类
import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.ArrayList;
/**
* Created by Sick on 2016/8/8.
*/
public class TestActivity extends Activity {
private RecyclerView rvCustomList;
private ArrayList data;
private RVAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bbrecyclerveiw);
initData();
initView();
}
private void initData() {
data = new ArrayList();
for (int i = 0; i <20; i++) {
data.add("测试"+i);
}
}
private void initView() {
rvCustomList = (RecyclerView) findViewById(R.id.rv_custom_list);
rvCustomList.setLayoutManager(new LinearLayoutManager(this));
adapter = new RVAdapter();
rvCustomList.setAdapter(adapter);
}
public class RVAdapter extends RecyclerView.Adapter{
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
MyHolderView holder = new MyHolderView(LayoutInflater.from(TestActivity.this).inflate(R.layout.item_data,parent,false));
return holder;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof MyHolderView){
((MyHolderView) holder).tvData.setText(data.get(position));
}
}
@Override
public int getItemCount() {
return data.size();
}
private class MyHolderView extends RecyclerView.ViewHolder{
TextView tvData;
public MyHolderView(View itemView) {
super(itemView);
tvData = (TextView) itemView.findViewById(R.id.tv_data);
}
}
}
}
代码直接copy到你的项目里面就可以使用,大神勿喷,小弟只是一个beginner,你有更好的想法更健壮的代码可以联系我![email protected]