版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
RecycleView 的分割线需要自定义控件去实现,继承一个 RecyclerView.ItemDecoration 的抽象类,这个网上有较多的实现类,不记录。
这边要采用的分割线方式是横线绘制,模仿 LinearLayoutCompat 去实现的。横线分割线有上下左右四个坐标,然后会有一个横线的厚度。然后在 RecyclerView 的 canvas 上面画这个矩形,矩形填充的内容可以是我们定义的 drawable。
LinearLayoutCompat 绘制分割线核心代码:
void drawDividersVertical(Canvas canvas) {
final int count = getVirtualChildCount();
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
if (hasDividerBeforeChildAt(i)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int top = child.getTop() - lp.topMargin - mDividerHeight;
drawHorizontalDivider(canvas, top);
}
}
}
if (hasDividerBeforeChildAt(count)) {
final View child = getVirtualChildAt(count - 1);
int bottom = 0;
if (child == null) {
bottom = getHeight() - getPaddingBottom() - mDividerHeight;
} else {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
bottom = child.getBottom() + lp.bottomMargin;
}
drawHorizontalDivider(canvas, bottom);
}
}
void drawDividersHorizontal(Canvas canvas) {
final int count = getVirtualChildCount();
final boolean isLayoutRtl = ViewUtils.isLayoutRtl(this);
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
if (hasDividerBeforeChildAt(i)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int position;
if (isLayoutRtl) {
position = child.getRight() + lp.rightMargin;
} else {
position = child.getLeft() - lp.leftMargin - mDividerWidth;
}
drawVerticalDivider(canvas, position);
}
}
}
if (hasDividerBeforeChildAt(count)) {
final View child = getVirtualChildAt(count - 1);
int position;
if (child == null) {
if (isLayoutRtl) {
position = getPaddingLeft();
} else {
position = getWidth() - getPaddingRight() - mDividerWidth;
}
} else {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (isLayoutRtl) {
position = child.getLeft() - lp.leftMargin - mDividerWidth;
} else {
position = child.getRight() + lp.rightMargin;
}
}
drawVerticalDivider(canvas, position);
}
}
void drawHorizontalDivider(Canvas canvas, int top) {
mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
mDivider.draw(canvas);
}
void drawVerticalDivider(Canvas canvas, int left) {
mDivider.setBounds(left, getPaddingTop() + mDividerPadding,
left + mDividerWidth, getHeight() - getPaddingBottom() - mDividerPadding);
mDivider.draw(canvas);
}
具体实现可以参考鸿洋大神的博客:http://blog.csdn.net/lmj623565791/article/details/45059587
在 Adapter 里面添加 Type 来动态加载 header 和 footer。这种比较简单,多定义两个类型,根据 item 下标去判断进行加载布局的 Type。(谷歌推荐就是这样处理)
仿照 ListView,让用户自定义 HeaderVIew 和 FooterView, 然后,包装这个用户传的 Adapter。
先来看一下 ListView 的几个方法。
ListView 的 setAdapter:
public void setAdapter(ListAdapter adapter) {
...
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
...
}
ListView 的 addHeaderView:
public void addHeaderView(View v, Object data, boolean isSelectable) {
...
if (mAdapter != null) {
if (!(mAdapter instanceof HeaderViewListAdapter)) {
wrapHeaderListAdapterInternal();
}
// In the case of re-adding a header view, or adding one later on,
// we need to notify the observer.
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
}
ListView 的 addFooterView:
public void addFooterView(View v, Object data, boolean isSelectable) {
...
// Wrap the adapter if it wasn't already wrapped.
if (mAdapter != null) {
if (!(mAdapter instanceof HeaderViewListAdapter)) {
wrapHeaderListAdapterInternal();
}
// In the case of re-adding a footer view, or adding one later on,
// we need to notify the observer.
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
}
ListView 的 wrapHeaderListAdapterInternal:
protected void wrapHeaderListAdapterInternal() {
mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, mAdapter);
}
可以发现,setAdapter、addHeaderView 和 addFooterView 都有调用到:
mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, mAdapter);
在这里进行 adapter 的偷偷修饰,把他换成 wrapHeaderListAdapterInternal,从而支持 header 和 footer。
根据这个思想,实现我们自己的 RecyclerView 以及封装的 Adapter。
WrapRecyclerView :
public class WrapRecyclerView extends RecyclerView {
private ArrayList mHeaderViewInfos = new ArrayList<>();
private ArrayList mFooterViewInfos = new ArrayList<>();
private Adapter mAdapter;
public WrapRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public void addHeaderView(View v) {
mHeaderViewInfos.add(v);
if (mAdapter != null) {
if (mAdapter instanceof HeaderViewRecyclerAdpater) {
mAdapter = new HeaderViewRecyclerAdpater(mHeaderViewInfos, mFooterViewInfos, mAdapter);
}
}
}
public void addFooterView(View v) {
mFooterViewInfos.add(v);
if (mAdapter != null) {
if (mAdapter instanceof HeaderViewRecyclerAdpater) {
mAdapter = new HeaderViewRecyclerAdpater(mHeaderViewInfos, mFooterViewInfos, mAdapter);
}
}
}
@Override
public void setAdapter(Adapter adapter) {
if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) {
mAdapter = new HeaderViewRecyclerAdpater(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
super.setAdapter(mAdapter);
}
}
HeaderViewRecyclerAdpater :
public class HeaderViewRecyclerAdpater extends RecyclerView.Adapter{
private ArrayList mHeaderViewInfos = new ArrayList<>();
private ArrayList mFooterViewInfos = new ArrayList<>();
private RecyclerView.Adapter mAdapter;
public HeaderViewRecyclerAdpater(ArrayList headerViewInfos, ArrayList footerViewInfos, RecyclerView.Adapter adapter) {
mAdapter = adapter;
if (mHeaderViewInfos == null) {
mHeaderViewInfos = new ArrayList<>();
} else {
mHeaderViewInfos = headerViewInfos;
}
if (mFooterViewInfos == null) {
mFooterViewInfos = new ArrayList<>();
} else {
mFooterViewInfos = footerViewInfos;
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//Headerview
if (viewType == RecyclerView.INVALID_TYPE) {
return new HeaderViewHolder(mHeaderViewInfos.get(0));
} else if (viewType == RecyclerView.INVALID_TYPE - 1) { // FooterView
return new HeaderViewHolder(mFooterViewInfos.get(0));
}
return mAdapter.onCreateViewHolder(parent, viewType);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
int viewType = getItemViewType(position);
if (viewType == RecyclerView.INVALID_TYPE) {
return ;
} else if (viewType == RecyclerView.INVALID_TYPE - 1) {
return ;
} else {
mAdapter.onBindViewHolder(holder, position - mHeaderViewInfos.size());
}
}
@Override
public int getItemViewType(int position) {
int numHeaders = mHeaderViewInfos.size();
if (numHeaders > position) {
return RecyclerView.INVALID_TYPE;
}
final int adjPosition = position - numHeaders;
int adapterCOunt = 0;
if (mAdapter != null) {
adapterCOunt = mAdapter.getItemCount();
if (adapterCOunt > adjPosition) {
return mAdapter.getItemViewType(adjPosition);
}
}
return RecyclerView.INVALID_TYPE - 1;
}
@Override
public int getItemCount() {
return mAdapter != null ? mAdapter.getItemCount() + mFooterViewInfos.size() + mHeaderViewInfos.size() :
mFooterViewInfos.size() + mHeaderViewInfos.size() ;
}
private static class HeaderViewHolder extends RecyclerView.ViewHolder {
public HeaderViewHolder(View view) {
super(view);
}
}
private static class FooterViewHolder extends RecyclerView.ViewHolder {
public FooterViewHolder(View view) {
super(view);
}
}
}
这样 RecyclerView 就可以像 LiseView 一样,直接调用 addHeaderView 和 addFooterView 进行 header 和 footer 的设置。
这是一个可以垂直滑动的 RecyclerView,每一个 item 是一个可以水平滑动的 RecyclerView。
这时候,当想要水平滑动一个 item,稍微倾斜一点,则很容易被执行为垂直滑动。(已经在最上面了,触发的是上方水波那个效果)
这个最终是一个事件分发的问题,手指滑动的事件被垂直的 RecyclerView 拦截了,所以先进行垂直的滑动,下面看一下源码来分析这个原因。
RecyclerView 的 onInterceptTouchEvent:
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
...
switch (action) {
...
case MotionEvent.ACTION_MOVE: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id " +
mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
if (mScrollState != SCROLL_STATE_DRAGGING) {
final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
} break;
...
}
return mScrollState == SCROLL_STATE_DRAGGING;
}
在 20 - 26 行,判断 x 方向与 y 方向上的移动距离是否大于 mTouchSlop(系统判断发生生滑动的最小距离)。大于的话,则在 29 行调用 setScrollState 方法。
RecyclerView 的 setScrollState:
void setScrollState(int state) {
if (state == mScrollState) {
return;
}
if (DEBUG) {
Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState,
new Exception());
}
mScrollState = state;
if (state != SCROLL_STATE_SETTLING) {
stopScrollersInternal();
}
dispatchOnScrollStateChanged(state);
}
setScrollState 主要是把 SCROLL_STATE_DRAGGING 赋值给 mScrollState,这时候 onInterceptTouchEvent 最后的 return 就是 true 了。也就是滑动事件被拦截了,不能继续往下传递。
这边要实现的是判断 X 方向与 Y 方向上滑动的距离哪个大,X 方向滑动的距离大的时候进行 item 的水平滑动。
实现了一个自定义控件,继承 RecyclerView,重新定义拦截规则,即重写 onInterceptTouchEvent 方法。
BetterRecyclerView:
public class BetterRecyclerView extends RecyclerView {
//touchSlop 为系统判断发生滑动的最小距离
//这边定义是因为 RecyclerView 中这个为私有变量,没办法获取
//处理与 RecyclerView 中一致
private int touchSlop;
private Context mContext;
private int INVALID_POINTER = -1;
private int scrollPointerId = INVALID_POINTER;
private int initialTouchX;
private int initialTouchY;
private final static String TAG = "BetterRecyclerView";
public BetterRecyclerView(Context context) {
// super(context);
this(context, null);
}
public BetterRecyclerView(Context context, @Nullable AttributeSet attrs) {
// super(context, attrs);
this(context, attrs, 0);
}
public BetterRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
ViewConfiguration vc = ViewConfiguration.get(context);
touchSlop = vc.getScaledEdgeSlop();
mContext = context;
}
@Override
public void setScrollingTouchSlop(int slopConstant) {
super.setScrollingTouchSlop(slopConstant);
ViewConfiguration vc = ViewConfiguration.get(mContext);
switch (slopConstant) {
case TOUCH_SLOP_DEFAULT:
touchSlop = vc.getScaledTouchSlop();
break;
case TOUCH_SLOP_PAGING:
touchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(vc);
break;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (e == null) {
return false;
}
int action = MotionEventCompat.getActionMasked(e);
int actionIndex = MotionEventCompat.getActionIndex(e);
switch (action) {
case MotionEvent.ACTION_DOWN :
//获取放下去的点的坐标
scrollPointerId = MotionEventCompat.getPointerId(e, 0);
initialTouchX = Math.round(e.getX() + 0.5f);
initialTouchY = Math.round(e.getY() + 0.5f);
return super.onInterceptTouchEvent(e);
case MotionEvent.ACTION_POINTER_DOWN:
//获取放下去的点的坐标
scrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
initialTouchX = Math.round(MotionEventCompat.getX(e, actionIndex) + 0.5f);
initialTouchY = Math.round(MotionEventCompat.getY(e, actionIndex) + 0.5f);
return super.onInterceptTouchEvent(e);
case MotionEvent.ACTION_MOVE:
int index = MotionEventCompat.findPointerIndex(e, scrollPointerId);
if (index < 0) {
return false;
}
//计算滑动偏移量
int x = Math.round(MotionEventCompat.getX(e, index) + 0.5f);
int y = Math.round(MotionEventCompat.getY(e, index) + 0.5f);
if (getScrollState() != SCROLL_STATE_DRAGGING ) {
int dx = x - initialTouchX;
int dy = y - initialTouchY;
boolean startScroll = false;
//将斜率添加进来,这样可以减少 startScroll 为 true 的机会。这个机会就会给需要这个返回值
if (getLayoutManager().canScrollHorizontally() && Math.abs(dx) > touchSlop &&
(getLayoutManager().canScrollVertically() || Math.abs(dx) > Math.abs(dy))) {
startScroll = true;
}
if(getLayoutManager().canScrollVertically() && Math.abs(dy) > touchSlop &&
(getLayoutManager().canScrollHorizontally() || Math.abs(dy) > Math.abs(dx))) {
startScroll = true;
}
Log.d(TAG, "startScroll: " + startScroll);
return startScroll && super.onInterceptTouchEvent(e);
}
return super.onInterceptTouchEvent(e);
default:
return super.onInterceptTouchEvent(e);
}
}
}
代码还是比较简单,就是减少了 onInterceptTouchEvent 返回值为 true 的概率(减少过滤事件)。
当 item 先横向滑动时候,快速切换到垂直滑动;或先垂直滑动,然后快速切换到横向滑动,这时候,后一个事件是不生效的。
RecyclerView 的 onInterceptTouchEvent:
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
...
switch (action) {
case MotionEvent.ACTION_DOWN:
...
if (mScrollState == SCROLL_STATE_SETTLING) {
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
}
...
}
在 RecyclerView 的 onInterceptTouchEvent 的方法里,手指 DOWN 事件的时候会判断是否正在滑动,是的话调用 requestDisallowInterceptTouchEvent 方法。这个方法在 RecyclerView 重写了:
RecyclerView 的 requestDisallowInterceptTouchEvent:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
final int listenerCount = mOnItemTouchListeners.size();
for (int i = 0; i < listenerCount; i++) {
final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
listener.onRequestDisallowInterceptTouchEvent(disallowIntercept);
}
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
在这里调用了 onRequestDisallowInterceptTouchEvent 方法。我们看一下 onRequestDisallowInterceptTouchEvent 的注解,调用这个方法则不会再去接收其他的事件。
/**
* Called when a child of RecyclerView does not want RecyclerView and its ancestors to
* intercept touch events with
* {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
*
* @param disallowIntercept True if the child does not want the parent to
* intercept touch events.
* @see ViewParent#requestDisallowInterceptTouchEvent(boolean)
*/
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
处理这个问题的方案也很简单,直接重写 requestDisallowInterceptTouchEvent 方法,空实现即可。
FeedRootRecyclerView:
public class FeedRootRecyclerView extends BetterRecyclerView {
public FeedRootRecyclerView(Context context) {
this(context,null);
}
public FeedRootRecyclerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public FeedRootRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
//这个接口的作用是不允许父类打断这个onTouch 事件,
//那么我设置一个空的函数,override 父类的方法,就可以达到相反的效果
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
// super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
RecycleView 里面有一个缓存机制,他有一个内部类 RecycledViewPool,按注释说的是允许在多个 RecyclerView 之间共享 View 的缓存。比如我们上面应用的例子,在比如 viewPager + adapter + tab 。
/**
* RecycledViewPool lets you share Views between multiple RecyclerViews.
*
* If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
* and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}.
*
* RecyclerView automatically creates a pool for itself if you don't provide one.
*
*/
public static class RecycledViewPool {
}
RecycleView 有三级缓存:
1、通过 recycler.getViewForPosition 方法,该方法返回ViewHolder对象,这个方法会按顺序检查 mChangedScrap (RecyclerView中需要改变的Viewholder)、 mAttachedScrap (还没有和RecyclerView 分离的 ViewHolder)、 mCachedViews(RecyclerView 的 ViewHolder 的缓存) ,如果有则返回 ViewHolder 进行复用。
2、调用 ViewCacheExtension.getViewForPositionAndType 方法。(只是一个接口,给开放者自己创建的缓存)
3、缓存池 RecyclerViewPool。
RecyclerViewPool 的使用也很简单,创建完 RecyclerViewPool 后调用 RecyclerViewPool 的 setRecycledViewPool 即可。使用的时候,系统会自动通过三级缓存去调用。