RecyclerView之一切

此文较长,因为真的包括了一切,参考了大量已有资料和源码,也没啥图,谨慎阅读仅供记录- -

一些注意点

不论是listView or RecyclerView, getChildAt(Position)代表获取当前屏幕第position个可见的元素。所以经常开发会遇到NPE,

int firstItemPosition = layoutManager.findFirstVisibleItemPosition();

传入position - firstItemPosition即可。

对于StaggeredGridLayoutManager比较复杂:

int[] firstVisibleItems = null;

firstVisibleItems=  ((StaggeredGridLayoutManager)recycleview.getLayoutManager()).findFirstVisibleItemPositions(firstVisibleItems);

因为这个layoutManager会有spanCount多列,所以需要传入一个数组,result[0]是结果。

  • count区别: getChildCount() 指当前显示在前台的View个数 getItemCount() 指全部数据集大小

  • position区别: getLayoutPosition() 站在LayoutManager角度 getAdapterPosition() 站在Aadpter角度

When adapter contents change (and you call notify***()) RecyclerView requests a new layout. From that moment, until layout system decides to calculate a new layout (<16 ms), the layout position and adapter position may not match because layout has not reflected adapter changes yet.

  • getLayoutPosition() 布局上显示的position
  • getAdapterPosition() adapter内数据的position

因为有16ms的刷新延迟,所以必然两者可能会在一段时间内不一样

设置列数

gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int position) {
        return 2;
    }
});

对于Stagg..:

ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if(lp != null&& lp instanceof StaggeredGridLayoutManager.LayoutParams) {
    StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
    p.setFullSpan(true);
}

观察者模式

adapter.registerAdapterDataObserver

private AdapterDataObserver mObserver = new AdapterDataObserver() {
    @Override
    public void onChanged() {
        ...
    }

    public void onItemRangeChanged(int positionStart, int itemCount) {onChanged();}
    public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {onChanged();}
    public void onItemRangeRemoved(int positionStart, int itemCount) {onChanged();}
    public void onItemRangeInserted(int positionStart, int itemCount) {onChanged();}
    public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {onChanged();}
};

当外面调用notifyxxxx就会发生这个回调。

setFixedSize

void onItemsInsertedOrRemoved() {
   if (hasFixedSize) layoutChildren();
   else requestLayout();
}

假设你为RecyclerView设置了fixedSize,即固定的长宽,那就不会进行整体的requestLayoutrequestLayout相当于要走一遍完整的layout,耗性能。

Any time a view calls requestLayout(), the Android UI system needs to traverse the entire view hierarchy to find out how big each view needs to be. If it finds conflicting measurements, it may need to traverse the hierarchy multiple times.

分割线

ItemDecoration 允许应用给具体的View添加具体的图画或者layout的偏移,对于绘制View之间的分割线,视觉分组边界等等是非常有用的。
所有的ItemDecorations按照被添加的顺序在itemview之前(如果通过重写onDraw())或者itemview之后(如果通过重写 onDrawOver(Canvas, RecyclerView, RecyclerView.State))绘制。

  • getItemOffsets:通过Rect为每个Item设置偏移,用于绘制Decoration。
  • onDraw:通过该方法,在Canvas上绘制内容,在绘制Item之前调用。(如果没有通过getItemOffsets设置偏移的话,Item的内容会将其覆盖)
  • onDrawOver:通过该方法,在Canvas上绘制内容,在Item之后调用。(画的内容会覆盖在item的上层)

简单来说,getItemOffsets会在item上下左右赋予间距,然后onDraw在绘制item前调用,如果这个时候有间距,那就可以在间距上画上分割线,否则会覆盖item。而onDrawOver会在item绘制后调用。

private void drawVertical(Canvas c, RecyclerView parent) {
    // recyclerView是否设置了paddingLeft和paddingRight
    final int left = parent.getPaddingLeft();
    final int right = parent.getWidth() - parent.getPaddingRight();
    final int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = parent.getChildAt(i);
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                .getLayoutParams();
        final int top = child.getBottom() + params.bottomMargin;
        // divider的bottom就是top加上divider的高度了
        final int bottom = (int) (top + mDividerHeight);
        c.drawRect(left, top, right, bottom, mPaint);
    }
}

注意,一般设分割线的时候最右或最下都会比较特殊。那么在Grid中,为了做到每个item均分,就要注意每个item的paddingLeft&paddingRight或者paddingTop&paddintBottom可能会不同(等差数列),需要一些数学进行计算。

原理

class RecyclerView extends ViewGroup{
    public void draw(Canvas c) {
        super.draw(c); //调用View的draw(),该方法会先调用onDraw(),再调用dispatchDraw()绘制children

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
        ...
    }
    public void onDraw(Canvas c) {
        super.onDraw(c);
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }
}

动画

原理

对于删除,添加等,会有容器去容纳这些操作。(并不是实时的)

private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();

ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();

ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();

在需要进行动画时,会把要进行的操作加入数组中。

 ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);

postOnAnimation是指用于在系统进行下一次动画操作时(16ms限制),运行当前的线程

  • dispatchLayoutStep1()

记录重新调用onLayout之前,用户操作的View的相关信息(getLeft(), getRight(), getTop(), getBottom()等)

final ItemHolderInfo animationInfo = mItemAnimator  
        .recordPreLayoutInformation(mState, holder,  
                ItemAnimator.buildAdapterChangeFlagsForAnimations(holder), // mark if position changed 
                holder.getUnmodifiedPayloads());  
mViewInfoStore.addToPreLayout(holder, animationInfo);  

mViewInfoStore.addToPreLayout重要

recordPreLayoutInformation 顾名思义 记录该holder已有的状态

  • dispatchLayoutStep2()

进行真正意义上的layout工作,只是调用LayoutManager.onLayoutChildren方法递归摆放子控件的位置,此方法可能会被调用多次

  • dispatchLayoutStep3()

dispatchLayoutStep2()进行完layout操作之后,RecyclerView在此方法中记录下重新布局之后,用户所操作View的相关信息。这时候holder的信息就是经过layout以后得到的,就可以通过与step1保存的旧信息进行对比来产生动画。

processDisappeared/processAppeared/processPersistent/unused

  • PERSISTENT: 布局前和布局后都是可见状态
  • REMOVED: 重新布局前可见,重新布局后不可见
  • ADDED: 重新布局前不存在,当重新布局后由APP添加到视图当中
  • DISAPPEARING: 在适配器中的数据源中存在但是在屏幕上是由可见状态变为不可见状态,
  • APPEARING: 跟DISAPPEARING正好相反的状态

这里布局前即dispatchLayoutStep1,此时数据还没有进行更新,当dispatchLayoutStep2后布局变成更新,就可以进行对比

Appear举例:

@Override
public void processAppeared(ViewHolder viewHolder,
       ItemHolderInfo preInfo, ItemHolderInfo info) {
   animateAppearance(viewHolder, preInfo, info);
}

private void animateAppearance(@NonNull ViewHolder itemHolder,
       @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
   itemHolder.setIsRecyclable(false);
   if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) { //返回false就直接运行
       postAnimationRunner();
   }
}

@Override
public boolean animateAppearance(@NonNull ViewHolder viewHolder,
       @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
   // 该方法通过前后的布局信息来判断是移动还是添加。下面我们以添加为例分析
   if (preLayoutInfo != null && (preLayoutInfo.left != postLayoutInfo.left
           || preLayoutInfo.top != postLayoutInfo.top)) {
       return animateMove(viewHolder, preLayoutInfo.left, preLayoutInfo.top,
               postLayoutInfo.left, postLayoutInfo.top);
   } else {
       return animateAdd(viewHolder);
   }
}


 @Override
    public boolean animatePersistence(@NonNull ViewHolder viewHolder,
            @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
        if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {
            if (DEBUG) {
                Log.d(TAG, "PERSISTENT: " + viewHolder
                        + " with view " + viewHolder.itemView);
            }
            return animateMove(viewHolder,
                    preInfo.left, preInfo.top, postInfo.left, postInfo.top);
        }
        dispatchMoveFinished(viewHolder);
        return false;
    }

postAnimationRunner中通过ViewCompat.postOnAnimation进行。

DefaultItemAnimator举例

if (additionsPending) {
        final ArrayList<ViewHolder> additions = new ArrayList<>();
        additions.addAll(mPendingAdditions);
        mAdditionsList.add(additions);
        mPendingAdditions.clear();
        // 重要的是这个adder。其中重要的是 animateAddImpl(holder) 方法。那么来分析这个方法。
        Runnable adder = new Runnable() {
            public void run() {
                for (ViewHolder holder : additions) {
                    animateAddImpl(holder);
                }
                additions.clear();
                mAdditionsList.remove(additions);
            }
        };
        if (removalsPending || movesPending || changesPending) {
            long removeDuration = removalsPending ? getRemoveDuration() : 0;
            long moveDuration = movesPending ? getMoveDuration() : 0;
            long changeDuration = changesPending ? getChangeDuration() : 0;
            long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
            View view = additions.get(0).itemView;
            ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
        } else {
            adder.run();
        }
    }

可以看到在remove,move的动画进行后,会进行add的动画:

private void animateAddImpl(final ViewHolder holder) {
    final View view = holder.itemView;
    final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
    mAddAnimations.add(holder);
    animation.alpha(1).setDuration(getAddDuration()).
            setListener(new VpaListenerAdapter() {
                @Override
                public void onAnimationStart(View view) {
                    dispatchAddStarting(holder);
                }
                @Override
                public void onAnimationCancel(View view) {
                    ViewCompat.setAlpha(view, 1);
                }

                @Override
                public void onAnimationEnd(View view) {
                    animation.setListener(null);
                    dispatchAddFinished(holder);
                    mAddAnimations.remove(holder);
                    dispatchFinishedWhenDone();
                }
            }).start();
}

其实也就是属性动画,alpha从0到1。这里有一些接口可以给我们自定义,例如动画开始时的dispatchAddStarting...

LayoutManager

这里顺便跟局部刷新一起理了。以notifyItemRemoved为例吧

dispatchLayoutStep1

dispatchLayoutStep1中我们前面介绍过是addPreLayout,即把已经当前在屏幕上展示的View保存。这时候,如果发生了notifyItem...,会进行一些预处理(也是为了动画做准备)

processAdapterUpdatesAndSetAnimationFlags();

--> preProcess

void preProcess() {
    mOpReorderer.reorderOps(mPendingUpdates);
    final int count = mPendingUpdates.size();
    for (int i = 0; i < count; i++) {
        UpdateOp op = mPendingUpdates.get(i);
        switch (op.cmd) {
            case UpdateOp.ADD:
                applyAdd(op);
                break;
            case UpdateOp.REMOVE:
                applyRemove(op);
                break;
            case UpdateOp.UPDATE:
                applyUpdate(op);
                break;
            case UpdateOp.MOVE:
                applyMove(op);
                break;
        }
        if (mOnItemProcessedCallback != null) {
            mOnItemProcessedCallback.run();
        }
    }
    mPendingUpdates.clear();
}

applyXXX中,例如applyRemove,做两个操作:

  • 对要删除的ViewHolder加上FLAG_REMOVED标记
  • 对后面的ViewHolder.offsetPosition(-itemCount, applyToPreLayout);即调整它们当前对应的position(position-1咯)
private void postponeAndUpdateViewHolders(UpdateOp op) {
    if (DEBUG) {
        Log.d(TAG, "postponing " + op);
    }
    mPostponedList.add(op);
    switch (op.cmd) {
        case UpdateOp.ADD:
            mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
            break;
        case UpdateOp.MOVE:
            mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
            break;
        case UpdateOp.REMOVE:
            mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
                    op.itemCount);
            break;
        case UpdateOp.UPDATE:
            mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
            break;
        default:
            throw new IllegalArgumentException("Unknown update op type for " + op);
    }
}

void flagRemovedAndOffsetPosition(int mNewPosition, int offset, boolean applyToPreLayout) {
    addFlags(ViewHolder.FLAG_REMOVED);
    offsetPosition(offset, applyToPreLayout);
    mPosition = mNewPosition;
}

dispatchLayoutStep2

dispatchLayoutStep2中,RecyclerView把布局交给了layoutManager

// layout algorithm:

// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.找出锚点

// 2) fill towards start, stacking from bottom从锚点出发依次往上布局子View

// 3) fill towards end, stacking from top从锚点出发依次往下布局子View

// 4) scroll to fulfill requirements like stack from bottom.

先简单概括一下流程,layoutManager使用onLayoutChildren排布一个个item。排布时会根据屏幕上剩余的布局决定是否要layoutChunk,在layoutChunk中,使用View view = layoutState.next(recycler);得到要渲染在屏幕上的itemView

那么,如果是LinearLayoutManager,计算出一个itemView的长宽和position后,下一个item就会接着排布(线性)。

ScrapView

onLayoutChildren时会ScrapView一次。即先回收所有View。(不要问我它为什么要这么做,大概不这么做因为View有各种属性会比较混乱)

  • mAttachedScrap: 用于缓存显示在屏幕上的 item 的 ViewHolder,RecyclerView 在 onLayout 时会先把 children 都移除掉,再重新添加进去,所以这个 List 应该是用在布局过程中临时存放 children 的,反正在 RecyclerView 滑动过程中不会在这里面来找复用的 ViewHolder 就是了。
  • mCachedViews 的大小默认为2。遍历 mCachedViews,找到 position 一致的 ViewHolder,之前说过,mCachedViews 里存放的 ViewHolder 的数据信息都保存着,所以 mCachedViews 可以理解成,只有原来的卡位可以重新复用这个 ViewHolder,新位置的卡位无法从 mCachedViews 里拿 ViewHolder出来用。
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
   final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.shouldIgnore()) {
        if (DEBUG) {
            Log.d(TAG, "ignoring view " + viewHolder);
        }
        return;
    }
    if (viewHolder.isInvalid() && !viewHolder.isRemoved() &&
            !mRecyclerView.mAdapter.hasStableIds()) {
        removeViewAt(index);
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        detachViewAt(index);
        recycler.scrapView(view);
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}
void scrapView(View view) {
  final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
            throw new IllegalArgumentException("Called scrap view with an invalid view."
                    + " Invalid views cannot be reused from scrap, they should rebound from"
                    + " recycler pool.");
        }
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
           mChangedScrap = new ArrayList<ViewHolder>();
        }
       holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}

<p>"Scrap" views are still attached to their parent RecyclerView but are eligible for rebinding and reuse. Requests for a view for a given position may return a reused or rebound scrap view instance.</p>

layoutChunk时会重新对要在屏幕上展示的View进行unScrap

【进阶】RecyclerView源码解析(三)——深度解析缓存机制 分析的很好

基于滑动场景解析RecyclerView的回收复用机制原理

next

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

1.检查mChangedScrap,若匹配到则返回相应holder

2.检查mAttachedScrap,若匹配到且holder有效则返回相应holder 从position获取

3.检查mCacheView,若匹配到则返回相应holder 从position获取

4.检查mAttachedScrap,若匹配到且holder有效则返回相应holder 从id获取

5.查查mCacheView,若匹配到则返回相应holder 从id获取

6.检查mViewCacheExtension,若匹配到则返回相应holder

7.检查mRecyclerPool,若匹配到则返回相应holder

8.否则执行Adapter.createViewHolder(),新建holder实例

9.返回holder.itemView

如果是从mAttachedScrapmCachedViews中获取的ViewHolder,则不会调用onBindViewHolder()

这个也很好理解,mAttach只是layout的一个暂存,肯定不用重新bind;mCachedView是根据id/position来重新使用的,内容肯定也都在,所以不用重新bind,不过只有两个。。

layoutChunk:

这个函数就是计算屏幕剩下的布局,计算每个item的高宽和position

btw,当发现ViewHolder.isRemoved时,会

void addToDisappearedInLayout(ViewHolder holder) {
    InfoRecord record = mLayoutHolderMap.get(holder);
    if (record == null) {
        record = InfoRecord.obtain();
        mLayoutHolderMap.put(holder, record);
    }
    record.flags |= FLAG_DISAPPEARED;
}

这也和前面动画处理相符合,在dispatchLayoutStep3中,当发现有FLAG_DISAPPEARED标记时就会进行processDisappeared的回调进行动画的展示。

点击事件

没啥好说,就在onBindViewHolder()直接set吧

https://stackoverflow.com/questions/24471109/recyclerview-onclick
https://antonioleiva.com/recyclerview-listener/
https://blog.csdn.net/liaoinstan/article/details/51200600

拖拽、侧滑删除

ItemTouchHelper了解一下

@Override
public int getMovementFlags(RecyclerView recyclerView,RecyclerView.ViewHolder viewHolder) {
}

@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
}

@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
}

这些是ItemTouchHelper.Callback的接口。

  • getMovementFlags(): 设置支持的拖拽和滑动的方向,可以支持的拖拽方向为上下,滑动方向为从左到右和从右到左,内部通过makeMovementFlags()设置

final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;

  • onMove(): 拖拽时回调。
  • onSwiped(): 滑动时回调。
//当长按选中item的时候(状态变化时回调)调用
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
//一共有三个状态,分别是ACTION_STATE_IDLE(空闲状态),ACTION_STATE_SWIPE(滑动状态),ACTION_STATE_DRAG(拖拽状态)。此方法中可以做一些状态变化时的处理,比如拖拽的时候修改背景色。
}

//当手指松开的时候(拖拽完成的时候)调用
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
//用户交互结束时回调。此方法可以做一些状态的清空,比如拖拽结束后还原背景色。
}

//isLongPressDragEnabled():
 //是否支持长按拖拽,默认为true。如果不想支持长按拖拽,则重写并返回false。

主要考虑一个显示侧滑菜单的场景。简单看了SwipeMenuLayout的源码。 其实就是在item content左右两边加上菜单,正常展示的时候是被隐藏的。当用户开始滑动时,使用scroll让菜单展示出来就可以了。。。

滑动机制

fill + layoutChunk

LinearLayoutManager中,当scrollBy时,可以看到先进入fill函数。

@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
        RecyclerView.State state) {
    if (mOrientation == HORIZONTAL) {
        return 0;
    }
    return scrollBy(dy, recycler, state);
}

fill

fill中,会根据滚动的距离对滑出屏幕的进行回收recycleByLayoutState
回收完后重新进行layoutChunk布局。此时新出现在屏幕上的item依旧通过上面分析过的layoutState.next(recycler)进行create&bind

offset

最后通过mOrientationHelper.offsetChildren(-scrolled);对屏幕上已经显示的item进行平滑移动。

issue

局部刷新的坑

@Override public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
    ((MyHolder)holder).textView.setText(a.get(position));
    ((MyHolder)holder).textView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            a.remove(position);
            notifyItemRemoved(position);
        }
    });

}

前面分析了一堆源码也知道,局部刷新例如删除的时候,被删除项下面的item只会做一个平移,不会重新onBind,也就导致,如果被删除的是第三条,第四条的item因为没有进行onBind刷新,position仍然为4,就导致索引错乱。

解决也很简单,网上都说在notifyItemRemove后重新notifyItemRangeChanged(position, getItemCount()),也没啥必要,一般这种错乱都是出现在click事件中,把click改成:

public void onClick(View view) {
    a.remove(holder.getAdapterPosition());
    notifyItemRemoved(holder.getAdapterPosition());
}

得到正确的position即可。

From documentation: "You should only use the position parameter while acquiring the related data item inside this method and should not keep a copy of it. If you need the position of an item later on (e.g. in a click listener), use RecyclerView.ViewHolder.getAdapterPosition() which will have the updated adapter position

stableIds

setHasStableIds(true)

Returns true if this adapter publishes a unique long value that can act as a key for the item at a given position in the data set.

当调用notifyDataSetChanged等改变数据时,如果设置stableIds,就会通过getItemId(position)看当前position的数据有没有改变。因为数据可以用a unique long value来唯一代表,所以只要这个value变了就代表数据变了。否则就不用刷新了。

这个至今我都没有实现成功。。。搜了网上issue也很多,我一设置setHasStableId就抛异常https://github.com/lsjwzh/RecyclerViewPager/issues/83 看源码只要设了observer就抛..那结果就必然抛异常。。 有知道该如何解决的可以留言一下

参考资料

ItemDecoration解析(一) getItemOffsets

btw 作为画图狂魔这次竟然没有图感到很羞愧>.<

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336

推荐阅读更多精彩内容

  • 【Android 控件 RecyclerView】 概述 RecyclerView是什么 从Android 5.0...
    Rtia阅读 307,230评论 27 439
  • 一、概述 对于RecyclerView的学习,主要是需要掌握以下几点: 数据:Adapter 使用:Recycle...
    泽毛阅读 7,245评论 1 23
  • RecyclerView是support:recyclerview-v7中提供的控件,最低兼容到android 3...
    8ba406212441阅读 714评论 0 0
  • 这篇文章分三个部分,简单跟大家讲一下 RecyclerView 的常用方法与奇葩用法;工作原理与ListView比...
    LucasAdam阅读 4,361评论 0 27
  • 01今天一起床,打开微信就看到了朋友圈有一个姑娘转发了自己和弟弟出行18天,四个省份六个城市的游记,青岛——烟台—...
    echo敏阅读 399评论 0 3