Android(安卓)自定义LayoutManager
16lz
2021-01-26
Android 自定义LayoutManager
实现自定义LayoutManager主要的4个步骤:
- 指定默认的LayoutParams
- 计算每个ItemView的位置
- 添加滑动事件
- 实现缓存
其中,主要在onLayoutChildern() 这个回调方法中实现主要功能。
指定默认的 LayoutParams
当你继承LayoutManager之后,必须要重写generateDefaultLayoutParams()方法
这个方法指定了每一个子view默认的LayoutParams,并且这个LayoutParams会在你调用getViewForPosition()返回子view前应用到这个子view。
// 重写generateDefaultLayoutParams()方法@Overridepublic RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);}
2.1 实现简单的LayoutManager
在这段代码中,我们先调用detachAndScrapAttachedViews(recycler);将所有的ItemView标记为Scrap状态,然后在挨个取出来,计算他们应该布局到什么位置,并用成员变量totalHeight记录总高度,最后依次调用layoutDecorated()将ItemView布局上去。
@Overridepublic void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { if (getItemCount() <= 0 || state.isPreLayout()) { return; }// 先把所有的View先从RecyclerView中detach掉,然后标记为"Scrap"状态,表示 // 这些View处于可被重用状态(非显示中)。// 实际就是把View放到了Recycler中的一个集合中。 super.onLayoutChildren(recycler, state); detachAndScrapAttachedViews(recycler);/* 这个方法主要用于计算并保存每个ItemView的位置 */ calculateChildrenSite(recycler); recycleAndFillView(recycler, state);} private void calculateChildrenSite(RecyclerView.Recycler recycler) { totalHeight = 0; for (int i = 0; i < getItemCount(); i++) { // 遍历Recycler中保存的View取出来 View view = recycler.getViewForPosition(i); addView(view); // 因为刚刚进行了detach操作,所以现在可以重新添加 measureChildWithMargins(view, 0, 0); // 通知测量view的margin值 int width = getDecoratedMeasuredWidth(view); // 计算view实际大小,包括了ItemDecorator中设置的偏移量。 int height = getDecoratedMeasuredHeight(view); Rect mTmpRect = new Rect(); // 调用这个方法能够调整ItemView的大小,以除去ItemDecorator。 calculateItemDecorationsForChild(view, mTmpRect); // 调用这句我们指定了该View的显示区域,并将View显示上去,此时所有区域都用于显示View, //包括ItemDecorator设置的距离。 layoutDecorated(view, 0, totalHeight, width, totalHeight + height); totalHeight += height; }}
2.2 两列式的LayoutManager
有了上例的基础,我们只需要稍作调整,直接看下面代码,注意注释部分。
private void calculateChildrenSite(RecyclerView.Recycler recycler) { totalHeight = 0; for (int i = 0; i < getItemCount(); i++) { View view = recycler.getViewForPosition(i); addView(view); //我们自己指定ItemView的尺寸。 measureChildWithMargins(view, DisplayUtils.getScreenWidth() / 2, 0); int width = getDecoratedMeasuredWidth(view); int height = getDecoratedMeasuredHeight(view); Rect mTmpRect = new Rect(); calculateItemDecorationsForChild(view, mTmpRect); if (i % 2 == 0) { //当i能被2整除时,是左,否则是右。 //左 layoutDecoratedWithMargins(view, 0, totalHeight, DisplayUtils.getScreenWidth() / 2, totalHeight + height); } else { //右,需要换行 layoutDecoratedWithMargins(view, DisplayUtils.getScreenWidth() / 2, totalHeight, DisplayUtils.getScreenWidth(), totalHeight + height); totalHeight = totalHeight + height; LogUtils.e(i + "->" + totalHeight); } }}
处理滑动
滑动事件主要涉及到4个方法需要重写,我们直接来看代码:
@Overridepublic boolean canScrollVertically() { // 返回true表示可以纵向滑动 return true;} @Overridepublic int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { //每次滑动时先释放掉所有的View,因为后面调用recycleAndFillView()时会重新addView()。 detachAndScrapAttachedViews(recycler); // 列表向下滚动dy为正,列表向上滚动dy为负,这点与Android坐标系保持一致。 // 实际要滑动的距离 int travel = dy; LogUtils.e("dy = " + dy); // 如果滑动到最顶部 if (verticalScrollOffset + dy < 0) { travel = -verticalScrollOffset; } else if (verticalScrollOffset + dy > totalHeight - getVerticalSpace()) {// 如果滑动到最底部 travel = totalHeight - getVerticalSpace() - verticalScrollOffset; } // 调用该方法通知view在y方向上移动指定距离 offsetChildrenVertical(-travel); recycleAndFillView(recycler, state); //回收并显示View // 将竖直方向的偏移量+travel verticalScrollOffset += travel; return travel;} private int getVerticalSpace() { // 计算RecyclerView的可用高度,除去上下Padding值 return getHeight() - getPaddingBottom() - getPaddingTop();} @Overridepublic boolean canScrollHorizontally() { // 返回true表示可以横向滑动 return super.canScrollHorizontally();} @Overridepublic int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { // 在这个方法中处理水平滑动 return super.scrollHorizontallyBy(dx, recycler, state);}
实现缓存
缓存最主要的就是先把每个ItemView的位置信息保存起来,然后在滑动过程中通过判断每个ItemView的位置是否和当前RecyclerView应该显示的区域有重合,若有就显示它,若没有就移除并回收。
/** * 将滑出屏幕的Items回收到Recycle缓存中 */Rect childRect = new Rect();for (int i = 0; i < getChildCount(); i++) { //这个方法获取的是RecyclerView中的View,注意区别Recycler中的View //这获取的是实际的View View child = getChildAt(i); //下面几个方法能够获取每个View占用的空间的位置信息,包括ItemDecorator childRect.left = getDecoratedLeft(child); childRect.top = getDecoratedTop(child); childRect.right = getDecoratedRight(child); childRect.bottom = getDecoratedBottom(child); //如果Item没有在显示区域,就说明需要回收 if (!Rect.intersects(displayRect, childRect)) { //移除并回收掉滑出屏幕的View removeAndRecycleView(child, recycler); itemStates.put(i, false); //更新该View的状态为未依附 } } //重新显示需要出现在屏幕的子View for (int i = 0; i < getItemCount(); i++) { //判断ItemView的位置和当前显示区域是否重合 if (Rect.intersects(displayRect, allItemRects.get(i))) { //获得Recycler中缓存的View View itemView = recycler.getViewForPosition(i); measureChildWithMargins(itemView, DisplayUtils.getScreenWidth() / 2, 0); //添加View到RecyclerView上 addView(itemView); //取出先前存好的ItemView的位置矩形 Rect rect = allItemRects.get(i); //将这个item布局出来 layoutDecoratedWithMargins(itemView, rect.left, rect.top - verticalScrollOffset, //因为现在是复用View,所以想要显示在 rect.right, rect.bottom - verticalScrollOffset); itemStates.put(i, true); //更新该View的状态为依附 } } LogUtils.e("itemCount = " + getChildCount()); }
参考学习:
https://blog.csdn.net/qq_31370269/article/details/52932315
更多相关文章
- 【Android(安卓)界面效果43】Android(安卓)LayoutInflater的infl
- Android开发之MediaPlayer使用
- Android从零开始(十五)
- Android(安卓)strings.xml中空格符的标识方法附xml特殊字符表示
- Android(安卓)编码规范 | 代码风格指南
- Android(安卓)Cavans 基础应用
- RemoteViews 中暗藏的坑
- Android中简单实现夜间模式
- Appium(七):Appium API(一) 应用操作