前语
在之前的一些文章中,咱们完结过各种各样的布局作用,当然也有吸顶作用,在写本篇之前能够看看之前的文章。
- 《Android RecyclerView多Tab列表吸顶作用》
- 《Android ScrollView 吸顶作用》
- 《Android NestedScrolling 联动机制为 RecyclerView 添加 Header》
- 《Android 地图上滑View完结》
上面的文章根本都是View内部布局办法完结的,当然也有Scrolling机制、ViewDragger、内部事情等。其实,按照Android官方的意图,从约束布局和RecyclerView上看,其目标是削减对View内部的实质性修正,而经过布局辅助器增强View的功能,由于不断的自界说View对运用者的学习成本比较高,甚至有许多人都疲倦去学习新的View用法,新View触及导包、api、布局等,经常要学习,一朝一夕运用率显着不太理想。而对于开发者比较了解的View上进行扩展,但又能让开发者快速接入,显着LayoutManager或许各种Helper办法显着作用更好一些。
下面是本篇的作用
Layout自界说知识点回忆
其实自界说Layout重点在丈量、布局、制作、事情处理,这儿其实咱们耳熟能详了。
根本知识
- 丈量:丈量子View或许本身View的大小,由外到内丈量,丈量有三种形式,但父View能够决定子View的形式。
- 布局:布置子View或许本身View的方位,由外到内丈量
- 制作:将View的图形描述制作到Canvas上
- 事情:一般指Touch事情和Key事情,前者在触屏形式运用,后者在焦点形式运用 (留意:我这儿说的形式,而不是设备,由于Android设备这两种都支撑)
咱们着重了解下事情,由于是陈词滥调的事情。
事情阻拦:
- 捕获事情必须承受DOWN事情
- KEY_EVENT能够直达焦点View,而Touch事情需求层层传递
- 同一ViewGroup的子View中,默许情况下,制作次序越靠后,越简单先接纳到事情,由于制作靠后的View是后续参加的,层级较高。
- 在事情传递的进程中,事情传递进程中ViewGroup至少有2次以上的阻拦机会。
- KEY_CENTERKEY_ENTER 等部分事情会被判定长按,其他事情会被判断为多次点击
- onClick和onLongClick是经过守时触发的
- hotspot 能够让drawable接纳到事情
- 事情承受时间是不接连的
- EventHub负责接纳手机,经过InputChannel向前台Activity传递事情
- Window接纳事情的次序是在Activity之后
….
requestLayout按捺
- 不要修正布局鸿沟,多用Matrix去处理,如scale、rotate、translate等
- 按照显现隐藏频度,高频运用INVSIBLE & VISIBLE
- 设置drawable之条件早设置drawable大小,避免setBackground内部触发requestLayout
- TextView固定大小或许自界说文本展现,避免requestLayout
- 进展类型,不要修正布局鸿沟,主张修正drawable的鸿沟
- 削减布局层级,下降requestLayout measure的几率
- 削减addView、removeView、offsetXXX办法的调用,适当运用removeViewInLayout或许addViewInLayout,当然addViewInLayout外部无法调用,那就运用detachViewFromParent和attachViewFromParent。
主张
避免过多的LayoutInflater,提高可移植性
尽或许削减requestLayout,提高制作帧率
高帧率异步渲染、必要时运用SurfaceView
尽或许运用Adapter完结View的复用
削减主线程耗时
…
吸顶作用原理
现在,网上有两种干流的完结计划:
运用ItemDecoration制作
这种有个比较显着的缺点便是点击事情很难呼应,由于制作区域无法阻拦事情
父View Wrapper
这种是运用父View,从Recycler缓存中拿一个和RecyclerView相同类型的View,能够处理事情,可是由于和RecyclerView上的Item是相互独立的因而需求进行状况同步,比如在RecyclerView上的是CheckBox,那么显着需求LiveData或许EventBus去处理,这样耦合逻辑会许多。
自界说LayoutManager
咱们这儿不是承继LayoutManager,由于毕竟RecyclerView原始逻辑很老练,咱们只需求承继LinearLayoutManager或许GridLayoutManager。
自界说LayoutManager的开源项目中你很难看到对这两者的扩展,毕竟实在是太杂乱了。
LinearLayoutManager和GridLayoutManager的布局思维
LayoutManager只初始化布局和布局item滑动时填充。
关于滑动
咱们之前许多自界说Layout的文章中提到过,在Android中View的滑动办法有两种:
- 第一种是“齿轮传动”,中心原理是Matrix 改换 (x,y,scale),代表View是ScrollView,当然这种功能很高,可是在View变多时功能会显著下降;
- 另一种是滑板派,一切子View的布局鸿沟联动(left、right、top、bottom),单一操作功能一般,可是合作Adapter不断复用回收,相比ScrollView在大量View的情况下功能显着高许多。
关于填充
由于要合作Recycler机制,LayoutManager需求不断回收和复用View,可是重点是其填充逻辑。
填充逻辑
LinearLayoutManager的填充逻辑是
- 测验移除View并回收
- 查找锚点(默许取第一个)
- 然后履行三种layout steps
- 布局完结
为什么很少有LinearLayoutManager的吸顶,主要是锚点问题,好消息是onAnchorReady这个办法是能够修正锚点的,换消息是只对包内子View敞开,所以你需求在androidx.recyclerview.widget下承继。
当然,本篇没有这么做,由于仍是太杂乱。
本篇主要分为三步:
- 釜底抽薪,不让吸顶View成为锚点
- 履行父类办法
- 从头布置吸顶View的方位
下面是中心进程
中心思维
釜底抽薪
首要,咱们要解决的是如何避免要吸顶的View不被挑选为锚点?由于一旦挑选为锚点,那么其他子View会参考锚点方位布局,所以,要在LayoutManager挑选锚点前“无改写移除”View,这儿咱们能够运用removeAndRecycleView。
这招能够称为“釜底抽薪”
这儿咱们只需求在布局之前将锚点移除
//先移除吸顶的View,避免LayoutManager将吸顶的View作为anchor 锚点
removeStickyView(recycler);
//让LayoutManager布局,其实这时候或许会将吸顶View参加进去,不过不要紧,RecyclerView的addView很强壮
super.onLayoutChildren(recycler, state);
相同纵向也是
//先移除吸顶的View,避免LayoutManager将吸顶的View作为anchor 锚点
removeStickyView(recycler);
//让LayoutManager布局,其实这时候或许会将吸顶View参加进去,不过不要紧,RecyclerView的addView很强壮
int scrollOffsetY = super.scrollVerticallyBy(dy, recycler, state);
删去可见View
删去怎样删呢,怎样知道哪些要被删去呢,其实咱们这儿需求界说ItemViewType,和Adapter中的itemViewType映射。
private int[] stickyItemTypes = null;
删去的时候,不是从缓存中拿View,而是删去上一次在界面上存在的View,当然,咱们要删的是吸顶的View和移出视觉区域的View,而不是一切的见面上的Sticky View。
/**
* 删去正在吸顶的View
* @param recycler
*/
private void removeStickyView(RecyclerView.Recycler recycler) {
int count = getChildCount();
if (count <= 0) {
return;
}
/**
* 留意,这儿必定要删去页面上的View,而不是从缓存中拿出来删,那样是无用功
*/
for (int i = 1; i < count; i++) {
View child = getChildAt(i);
if (child == null) continue;
int itemViewType = getItemViewType(child);
if (!isStickyItemType(itemViewType)) {
continue;
}
int decoratedTop = getDecoratedTop(child);
if (decoratedTop <= 0) {
//删去 top <= 0的吸顶View,由于正常情况下页面child要么在吸顶,要么不行见了
removeAndRecycleView(child, recycler);
}
}
}
先让LayoutManager自己布局
咱们要确保原始的布局逻辑坚持不变,可是这时候吸顶的View或许也被参加了布局。了解过自界说View机制你就会知道,在布局办法或许onSizeChanged办法中频繁删去和重建View并不会影响展现,因而,咱们能够把原有的View拿到,假如拿不到就从缓存中拿,拿到之后让其吸顶,且不会影响原有布局中的item方位。
咱们开始说过,RecyclerView属于滑板派,只需你不requestLayout,每个View的left、top、right、bottom仍是会坚持本来的方位。
addView魔法
咱们要知道的是,让其他ItemView不要盖住StickyView
咱们文章开始说过:
后参加的View最终制作,事情最优先接纳,显着吸顶的View要在最终参加,才能不被隐瞒。
问题是,吸顶的View或许现已参加进去了,怎样办?
咱们文章开始还说过:
“削减addView、removeView、offsetXXX办法的调用,适当运用removeViewInLayout或许addViewInLayout,当然addViewInLayout外部无法调用,那就运用detachViewFromParent和attachViewFromParent”,这些办法能够协助咱们调整View次序,当然这是开始的主意。可是现实是RecyclerView 似乎和这些有抵触,然后去看addView源码,无意间发现LayoutManager#addView居然能够移动View的次序。
显着咱们要做的是重置次序,当然有人会说View#bingToFront不行么?假如在ScrollView中是可行的,可是在RecyclerView中是不行的,由于其内部有调用requestLayout,不适合滑动进程布局。
咱们先看看addView中心逻辑,从代码中能够看到,其内部调用的办法很少触发requestLayout的条件,所以必定要知道的是,在滑动进程中切忌不要调用触发requestlayout的办法。
private void addViewInt(View child, int index, boolean disappearing) {
final ViewHolder holder = getChildViewHolderInt(child);
if (disappearing || holder.isRemoved()) {
mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder);
} else {
mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder);
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (holder.wasReturnedFromScrap() || holder.isScrap()) {
if (holder.isScrap()) {
holder.unScrap();
} else {
holder.clearReturnedFromScrapFlag();
}
mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
if (DISPATCH_TEMP_DETACH) {
ViewCompat.dispatchFinishTemporaryDetach(child);
}
} else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child
// ensure in correct position
int currentIndex = mChildHelper.indexOfChild(child);
if (index == -1) {
index = mChildHelper.getChildCount();
}
if (currentIndex == -1) {
throw new IllegalStateException("Added View has RecyclerView as parent but"
+ " view is not a real child. Unfiltered index:"
+ mRecyclerView.indexOfChild(child) + mRecyclerView.exceptionLabel());
}
if (currentIndex != index) {
mRecyclerView.mLayout.moveView(currentIndex, index);
}
} else {
mChildHelper.addView(child, index, false);
lp.mInsetsDirty = true;
if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
mSmoothScroller.onChildAttachedToWindow(child);
}
}
if (lp.mPendingInvalidate) {
if (DEBUG) {
Log.d(TAG, "consuming pending invalidate on child " + lp.mViewHolder);
}
holder.itemView.invalidate();
lp.mPendingInvalidate = false;
}
}
从头布局
首要咱们知道页面上第一个View的方位,咱们能够由此定位到其所在的分组itemViewType类型,假如其不属于要吸顶的item,那么继续向前搜索,假如是当即布局,下面首要查询能够吸顶且越第一个ItemView“血缘”最近的分组。
private View lookupStickyItemView(RecyclerView.Recycler recycler) {
int childCount = getChildCount();
if (childCount <= 0) {
return null;
}
//先看看第一个View是不是能够吸顶,假如不能够,则从缓存中查询
View view = getChildAt(0);
int itemViewType = getItemViewType(view);
int adapterPosition = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewAdapterPosition();
View groupView = null;
if (!isStickyItemType(itemViewType)) {
//一般来说下,吸顶View的itemType在前面查询,假如要改成吸底的则在后边查询,因而这儿逆序
for (int i = adapterPosition - 1; i >= 0; i--) {
//从缓存中查询
View childView = recycler.getViewForPosition(i);
//获取View类型
itemViewType = getItemViewType(childView);
if (isStickyItemType(itemViewType)) {
groupView = childView;
break;
}
}
} else {
//页面上第一个View便是吸顶的View
groupView = view;
}
if (groupView == null) {
Log.d(TAG, "not found " + itemViewType + " ,topChildPosition =" + adapterPosition);
return null;
}
return groupView;
}
布局
addView(currentStickyItemView);
//丈量多次没有问题,允许多次丈量
measureChildWithMargins(currentStickyItemView, 0, 0);
int top = 0;
int right = getDecoratedMeasuredWidth(currentStickyItemView);
layoutDecoratedWithMargins(currentStickyItemView, 0, 0, right, bottom);
问题是,页面上或许有多个吸顶ItemView,当向上滑动时吸顶的View要确保下面要吸顶的不被隐瞒,那就意味着吸顶的View需求滑动。
怎样做?
当然是查找当时吸顶View的下一个可吸顶的兄弟,当然咱们只需求在页面上查找,Adapter查找没有意义,由于只会用到离当时吸顶View最近的,不在页面或许没出生的必定不能算。
/**
* 获取当时页面布局区域内的一切吸顶View
* @return
*/
private List<View> getStickyItemViews() {
stickyAttachedViewList.clear();
int childCount = getChildCount();
if (childCount <= 0) {
return stickyAttachedViewList;
}
for (int i = 1; i < childCount; i++) {
View child = getChildAt(i);
if (child == null) continue;
int itemViewType = getItemViewType(child);
if (isStickyItemType(itemViewType)) {
stickyAttachedViewList.add(child);
}
}
return stickyAttachedViewList;
}
上面的查找必定也会查找到正在吸顶的ItemView,为了避免逻辑过错,咱们把其删去掉
/**
* 由于不能确保吸顶的View次序是最理想的按默许摆放,因而这儿正在西定的View在制作次序的最顶部,
* 可是其他能够吸顶的View是正常次序,因而删去掉,从开始方位核算,假如下一个离正在吸顶View最近的View顶到了它 (哈哈,莫要想歪了),
* 那么就得让他偏移
*/
stickyChildren.remove(currentStickyItemView);
那么方位核算呢?
首要吸顶的View top 默许是0,因而向上滑动top应该变成负值,咱们用下一个要吸顶的View的top减去当时吸顶View的高度即可,可是条件是这个高度必须现已触及了正在吸顶View的边缘。
for (int index = 0; index < size; index++) {
View nextChild = stickyChildren.get(index);
int nextStickyViewTop = getDecoratedTop(nextChild);
if (nextStickyViewTop < topStickyViewTop) {
continue;
}
if (nextStickyViewTop > topStickyViewHeight) {
continue;
}
top = nextStickyViewTop - topStickyViewHeight; //核算偏移间隔
break;
}
调整布局逻辑
int bottom = top + topStickyViewHeight;
layoutDecoratedWithMargins(currentStickyItemView, 0, top, getDecoratedMeasuredWidth(currentStickyItemView), bottom);
用法
为了便利运用,咱们其实运用GridLayoutManager完结了吸顶灯作用,下面是本文作用图的展现完结。
public class MainActivity extends Activity {
private RecyclerView recyclerView;
private QuickAdapter quickAdapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.recycle_main);
recyclerView = findViewById(R.id.recycleView);
int[] stickyItemTypes = new int[]{
ItemType.VIEW_TYPE_GROUP, //此类型需求吸顶
ItemType.VIEW_TYPE_GROUP_ICON //此类型需求吸顶
};
recyclerView.setLayoutManager(new StickyGridLayoutManager(this, stickyItemTypes,1));
quickAdapter = new QuickAdapter(createFakeDatas());
recyclerView.setAdapter(quickAdapter);
}
private List<DataModel> createFakeDatas() {
List<DataModel> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
DataModel child = new ItemDataModel("第" + 0 + "组第" + (i + 1) + "号");
list.add(child);
}
for (int g = 0; g < 10; g++) {
DataModel group = (g % 2 == 0) ? new GroupDataModel("第" + (g + 1) + "组") : new GroupDataModelIcon("第" + (g + 1) + "组");
list.add(group);
int count = (int) (10 + 10 * Math.random());
for (int i = 0; i < count; i++) {
DataModel child = new ItemDataModel("第" + (g + 1) + "组第" + (i + 1) + "号");
list.add(child);
}
}
return list;
}
}
总结
特点
到这儿咱们创建吸顶LayoutManager就结束了,相比网上的其他两种计划,这种计划优势显着:
- 耦合度更小
- 可移植性更高
- 状况不需求同步
- 支撑事情
- 不依赖itemDecoration
- 不依赖父布局
- 不依赖Adapter
悉数代码
按照惯例,这儿供给完结源码,便利咱们参考和改造。
public class StickyGridLayoutManager extends GridLayoutManager {
private static final String TAG = "StickyGridManager";
private final List<View> stickyAttachedViewList = new ArrayList<>();
private int[] stickyItemTypes = null;
public StickyGridLayoutManager(Context context, int[] stickyItemTypes, int spanCount) {
super(context, spanCount);
this.stickyItemTypes = stickyItemTypes;
}
public StickyGridLayoutManager(Context context, int[] stickyItemTypes, int spanCount, int orientation, boolean reverseLayout) {
super(context, spanCount, orientation, reverseLayout);
this.stickyItemTypes = stickyItemTypes;
}
public StickyGridLayoutManager(Context context, AttributeSet attrs, int[] stickyItemTypes, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.stickyItemTypes = stickyItemTypes;
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (this.stickyItemTypes == null
|| this.stickyItemTypes.length == 0
|| getOrientation() != RecyclerView.VERTICAL) {
super.onLayoutChildren(recycler, state);
return;
}
//先移除吸顶的View,避免LayoutManager将吸顶的View作为anchor 锚点
removeStickyView(recycler);
//让LayoutManager布局,其实这时候或许会将吸顶View参加进去,不过不要紧,RecyclerView的addView很强壮
super.onLayoutChildren(recycler, state);
//布局吸顶的View
layoutStickyView(recycler, state);
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (this.stickyItemTypes == null || this.stickyItemTypes.length == 0) {
return super.scrollVerticallyBy(dy, recycler, state);
}
//先移除吸顶的View,避免LayoutManager将吸顶的View作为anchor 锚点
removeStickyView(recycler);
//让LayoutManager布局,其实这时候或许会将吸顶View参加进去,不过不要紧,RecyclerView的addView很强壮
int scrollOffsetY = super.scrollVerticallyBy(dy, recycler, state);
//布局吸顶的View
layoutStickyView(recycler, state);
return scrollOffsetY;
}
private void layoutStickyView(RecyclerView.Recycler recycler, RecyclerView.State state) {
View currentStickyItemView = lookupStickyItemView(recycler);
if (currentStickyItemView == null) {
return;
}
/**
* 下面办法将当时要吸顶的View添加进去
* 留意1:addView被RecyclerView魔改正,正常情况下一个View只能被addView一次
* 留意2: LayoutManager的addView会尽或许按捺requestLayout,正常情况下,addView必然会requestLayout
* 留意3: LayoutManager多次addView同一个View,假如两次方位不一样,那只会改变View的参加次序和制作次序
* 留意4: 在Android体系的中,最终参加的View制作次序和承受事情的优先级是最高的。
*/
addView(currentStickyItemView);
measureChildWithMargins(currentStickyItemView, 0, 0);
List<View> stickyChildren = getStickyItemViews();
int top = 0;
int topStickyViewHeight = getDecoratedMeasuredHeight(currentStickyItemView);
int topStickyViewTop = getDecoratedTop(currentStickyItemView);
/**
* 由于不能确保吸顶的View次序是最理想的按默许摆放,因而这儿正在西定的View在制作次序的最顶部,
* 可是其他能够吸顶的View是正常次序,因而删去掉,从开始方位核算,假如下一个离正在吸顶View最近的View顶到了它 (哈哈,莫要想歪了),
* 那么就得让他偏移
*/
stickyChildren.remove(currentStickyItemView);
int size = stickyChildren.size();
for (int index = 0; index < size; index++) {
View nextChild = stickyChildren.get(index);
int nextStickyViewTop = getDecoratedTop(nextChild);
if (nextStickyViewTop < topStickyViewTop) {
continue;
}
if (nextStickyViewTop > topStickyViewHeight) {
continue;
}
top = nextStickyViewTop - topStickyViewHeight; //核算偏移间隔
break;
}
int bottom = top + topStickyViewHeight;
layoutDecoratedWithMargins(currentStickyItemView, 0, top, getDecoratedMeasuredWidth(currentStickyItemView), bottom);
}
/**
* 获取当时页面布局区域内的一切吸顶View
* @return
*/
private List<View> getStickyItemViews() {
stickyAttachedViewList.clear();
int childCount = getChildCount();
if (childCount <= 0) {
return stickyAttachedViewList;
}
for (int i = 1; i < childCount; i++) {
View child = getChildAt(i);
if (child == null) continue;
int itemViewType = getItemViewType(child);
if (isStickyItemType(itemViewType)) {
stickyAttachedViewList.add(child);
}
}
return stickyAttachedViewList;
}
@Nullable
private View lookupStickyItemView(RecyclerView.Recycler recycler) {
int childCount = getChildCount();
if (childCount <= 0) {
return null;
}
//先看看第一个View是不是能够吸顶,假如不能够,则从缓存中查询
View view = getChildAt(0);
int itemViewType = getItemViewType(view);
int adapterPosition = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewAdapterPosition();
View groupView = null;
if (!isStickyItemType(itemViewType)) {
//一般来说下,吸顶View的itemType在前面查询,假如要改成吸底的则在后边查询,因而这儿逆序
for (int i = adapterPosition - 1; i >= 0; i--) {
//从缓存中查询
View childView = recycler.getViewForPosition(i);
//获取View类型
itemViewType = getItemViewType(childView);
if (isStickyItemType(itemViewType)) {
groupView = childView;
break;
}
}
} else {
//页面上第一个View便是吸顶的View
groupView = view;
}
if (groupView == null) {
Log.d(TAG, "not found " + itemViewType + " ,topChildPosition =" + adapterPosition);
return null;
}
return groupView;
}
private boolean isStickyItemType(int itemViewType) {
if (this.stickyItemTypes == null || this.stickyItemTypes.length == 0) {
return false;
}
for (int i = 0; i < this.stickyItemTypes.length; i++) {
if(this.stickyItemTypes[i] == itemViewType){
return true;
}
}
return false;
}
/**
* 删去正在吸顶的View
* @param recycler
*/
private void removeStickyView(RecyclerView.Recycler recycler) {
int count = getChildCount();
if (count <= 0) {
return;
}
/**
* 留意,这儿必定要删去页面上的View,而不是从缓存中拿出来删,那样是无用功
*/
for (int i = 1; i < count; i++) {
View child = getChildAt(i);
if (child == null) continue;
int itemViewType = getItemViewType(child);
if (!isStickyItemType(itemViewType)) {
continue;
}
int decoratedTop = getDecoratedTop(child);
if (decoratedTop <= 0) {
//删去 top <= 0的吸顶View,由于正常情况下页面child要么在吸顶,要么不行见了
removeAndRecycleView(child, recycler);
}
}
}
}