简介
鸿蒙 OS 开发 SDK 中对于长列表的实现 ListContainer 的实现较为简单,没法想 RecyclerView 一样通过应用不同的 LayoutManager 来实现简单布局因而没法疾速实现瀑布流成果。
但鸿蒙 OS 也都反对控件的 Measure(onEstimateSize),layout(onArrange) 和事件的解决。齐全能够在鸿蒙 OS 中自定义一个布局来实现 RecyclerView+LayoutManager 的成果,以此来实现瀑布流等简单成果。
自定义布局
对于鸿蒙 OS 自定义布局在官网上有介绍,次要实现 onEstimateSize 来测量控件大小和 onArrange 实现布局,这里咱们将子控件的确定和测量摆放齐全交 LayoutManager 来实现。同时咱们要反对滑动,这里用 Component.DraggedListener 实现。因而咱们的布局容器非常简略,调用 LayoutManager 进行测量布局,同时对于滑动事件,确定滑动后的视窗,调用 LayoutManager 的 fill 函数确定填满视窗的子容器汇合,而后触发从新绘制。外围代码如下
public class SpanLayout extends ComponentContainer implements ComponentContainer.EstimateSizeListener,
ComponentContainer.ArrangeListener, Component.CanAcceptScrollListener, Component.ScrolledListener, Component.TouchEventListener, Component.DraggedListener {
private BaseItemProvider mProvider;
public SpanLayout(Context context) {super(context);
setEstimateSizeListener(this);
setArrangeListener(this);
setDraggedListener(DRAG_VERTICAL,this);
}
@Override
public boolean onEstimateSize(int widthEstimatedConfig, int heightEstimatedConfig) {int width = Component.EstimateSpec.getSize(widthEstimatedConfig);
int height = Component.EstimateSpec.getSize(heightEstimatedConfig);
setEstimatedSize(Component.EstimateSpec.getChildSizeWithMode(width, widthEstimatedConfig, EstimateSpec.UNCONSTRAINT),
Component.EstimateSpec.getChildSizeWithMode(height, heightEstimatedConfig, EstimateSpec.UNCONSTRAINT));
mLayoutManager.setEstimateSize(widthEstimatedConfig,heightEstimatedConfig);
// measureChild(widthEstimatedConfig,heightEstimatedConfig);
return true;
}
@Override
public boolean onArrange(int left, int top, int width, int height) {
// 第一次 fill,从 item0 开始始终到 leftHeight 和 rightHeight 都大于 height 为止。if(mRecycler.getAttachedScrap().isEmpty()){mLayoutManager.fill(left,top,left+width,top+height,DIRECTION_UP);
}
// removeAllComponents(); // 调用 removeAllComponents 的话会始终登程从新绘制。for(RecyclerItem item:mRecycler.getAttachedScrap()){item.child.arrange(item.positionX+item.marginLeft,scrollY+item.positionY+item.marginTop,item.width,item.height);
}
return true;
}
@Override
public void onDragStart(Component component, DragInfo dragInfo) {startY = dragInfo.startPoint.getPointYToInt();
}
@Override
public void onDragUpdate(Component component, DragInfo dragInfo) {int dt = dragInfo.updatePoint.getPointYToInt() - startY;
int tryScrollY = dt + scrollY;
startY = dragInfo.updatePoint.getPointYToInt();
mDirection = dt<0?DIRECTION_UP:DIRECTION_DOWN;
mChange = mLayoutManager.fill(0, -tryScrollY,getEstimatedWidth(),-tryScrollY+getEstimatedHeight(),mDirection);
if(mChange){
scrollY = tryScrollY;
postLayout();}
}
}
瀑布流 LayoutManager
LayoutManager 次要是用来确定子控件的布局,重点是要实现 fill 函数,用于确认对于一个视窗内的子控件。
咱们定义一个 Span 类,来记录某一列瀑布以后 startLine 和 endLine 状况,对于 spanNum 列的瀑布流,咱们创立 Span 数组来记录状况。
例如向上滚动,当一个子控件满足 bottom 小于视窗 top 时须要回收,当一个子控件的 bottom 小于视窗的 bottom 是阐明其下方需有子控件填充。因为瀑布流是多列的且每个子控件高度不同,因而咱们不能简略的判断以后显示的第一个子控件是否要回收,最初一个子控件下方是否须要填充来实现充斥视窗的工作。咱们用 while 循环 + 双端队列,通过保障所有的 Span 其 startLine 都小于视窗 top,endLine 都大于视窗 bottom 来实现充斥视窗的工作。外围 fill 函数实现如下:
public synchronized boolean fill(float left,float top,float right,float bottom,int direction){
int spanWidth = mWidthSize/mSpanNum;
if(mSpans == null){mSpans = new Span[mSpanNum];
for(int i=0;i<mSpanNum;i++){Span span = new Span();
span.index = i;
mSpans[i] = span;
span.left = (int) (left + i*spanWidth);
}
}
LinkedList<RecyclerItem> attached = mRecycler.getAttachedScrap();
if(attached.isEmpty()){mRecycler.getAllScrap().clear();
int count = mProvider.getCount();
int okSpan = 0;
for (int i=0;i<count;i++){Span span = getMinSpanWithEndLine();
RecyclerItem item = fillChild(span.left,span.endLine,i);
item.span = span;
if(item.positionY>=top && item.positionY<=bottom+item.height){// 在显示区域
mRecycler.addItem(i,item);
mRecycler.attachItemToEnd(item);
}else{mRecycler.recycle(item);
}
span.endLine += item.height+item.marginTop+item.marginBottom;
if(span.endLine>bottom){okSpan++;}
if(okSpan>=mSpanNum){break;}
}
return true;
}else{if(direction == DIRECTION_UP){RecyclerItem last = attached.peekLast();
int count = mProvider.getCount();
if(last.index == count-1 && last.getBottom()<=bottom){// 曾经到底
return false;
}else{
// 先回收
RecyclerItem first = attached.peekFirst();
while(first != null && first.getBottom()<top){mRecycler.recycle(first);//recycle 自身会 remove
first.span.startLine += first.getVSpace();
first = attached.peekFirst();}
Span minEndLineSpan = getMinSpanWithEndLine();
int index = last.index+1;
while(index<count && minEndLineSpan.endLine<=bottom){// 须要填充
RecyclerItem item;
if(mRecycler.getAllScrap().size()>index){item = mRecycler.getAllScrap().get(index);
mRecycler.recoverToEnd(item);
}else{item = fillChild(minEndLineSpan.left,minEndLineSpan.endLine,index);
item.span = minEndLineSpan;
mRecycler.attachItemToEnd(item);
mRecycler.addItem(index,item);
}
item.span.endLine += item.getVSpace();
minEndLineSpan = getMinSpanWithEndLine();
index++;
}
return true;
}
}else if(direction == DIRECTION_DOWN){RecyclerItem first = attached.peekFirst();
int count = mProvider.getCount();
if(first.index == 0 && first.getTop()>=top){// 曾经到顶
return false;
}else{
// 先回收
RecyclerItem last = attached.peekLast();
while(last != null && last.getTop()>bottom){mRecycler.recycle(last);//recycle 自身会 remove
last.span.endLine -= last.getVSpace();
last = attached.peekFirst();}
Span maxStartLineSpan = getMaxSpanWithStartLine();
int index = first.index-1;
while(index>=0 && maxStartLineSpan.startLine>=top){// 须要填充
RecyclerItem item = mRecycler.getAllScrap().get(index);
if(item != null){mRecycler.recoverToStart(item);
item.span.startLine -= item.getVSpace();}else{// 实践上不存在}
maxStartLineSpan = getMaxSpanWithStartLine();
index--;
}
return true;
}
}
}
return true;
}
Item 回收
对于长列表,必定要有相似于 RecyclerView 的回收机制。item 的回收和还原在 LayoutManager 的 fill 函数中触发,通过 Reycler 实现。
简略的应用了 mAttacthedScrap 来保留以后视窗上显示的 Item 和 mCacheScrap 来保留被回收的控件。这里的设计就是对 RecyclerView 的回收机制的简化。
不同的是参考 Flutter 中三棵树的概念,定义了 RecycleItem 类,用来记录每个 Item 的左上角坐标和宽高值,只有在视窗上显示的 Item 会绑定组件。因为未绑定组件时的 RecycleItem 是非常轻量级的,因而内存的损耗根本能够疏忽。咱们用 mAllScrap 来按程序保留所有的 RecycleItem 对象,用来复用。当复原一个 mAllScrap 中存在的 Item 时,其坐标和宽高都曾经确定。
Recycler 的实现外围代码如下:
public class Recycler {
public static final int DIRECTION_UP = 0;
public static final int DIRECTION_DOWN = 2;
private ArrayList<RecyclerItem> mAllScrap = new ArrayList<>();
private LinkedList<RecyclerItem> mAttachedScrap = new LinkedList<>();
private LinkedList<Component> mCacheScrap = new LinkedList<Component>();
private BaseItemProvider mProvider;
private SpanLayout mSpanLayout;
private int direction = 0;
public Recycler(SpanLayout layout, BaseItemProvider provider) {
this.mSpanLayout = layout;
this.mProvider = provider;
}
public ArrayList<RecyclerItem> getAllScrap() {return mAllScrap;}
public LinkedList<RecyclerItem> getAttachedScrap() {return mAttachedScrap;}
public void cacheItem(int index, RecyclerItem item) {mAllScrap.add(index, item);
}
public void attachComponent(RecyclerItem item) {mAttachedScrap.add(item);
}
public Component getView(int index, ComponentContainer container) {Component cache = mCacheScrap.poll();
return mProvider.getComponent(index, cache, container);
}
public void addItem(int index,RecyclerItem item) {mAllScrap.add(index,item);
}
public void attachItemToEnd(RecyclerItem item) {mAttachedScrap.add(item);
}
public void attachItemToStart(RecyclerItem item) {mAttachedScrap.add(0,item);
}
public void recycle(RecyclerItem item) {mSpanLayout.removeComponent(item.child);
mAttachedScrap.remove(item);
mCacheScrap.push(item.child);
item.child = null;
}
public void recoverToEnd(RecyclerItem item) {Component child = mProvider.getComponent(item.index, mCacheScrap.poll(), mSpanLayout);
child.estimateSize(Component.EstimateSpec.getSizeWithMode(item.width, Component.EstimateSpec.PRECISE),
Component.EstimateSpec.getSizeWithMode(item.height, Component.EstimateSpec.PRECISE)
);
item.child = child;
mAttachedScrap.add(item);
mSpanLayout.addComponent(child);
}
public void recoverToStart(RecyclerItem item) {Component child = mProvider.getComponent(item.index, mCacheScrap.poll(), mSpanLayout);
child.estimateSize(Component.EstimateSpec.getSizeWithMode(item.width, Component.EstimateSpec.PRECISE),
Component.EstimateSpec.getSizeWithMode(item.height, Component.EstimateSpec.PRECISE)
);
item.child = child;
mAttachedScrap.add(0,item);
mSpanLayout.addComponent(child);
}
}
总结
鸿蒙 OS 的开发 SDK 中根底能力都曾经提供全面了,齐全能够用来实现一些简单成果。这里实现的 SpanLayout+LayoutManager+Recycler 的根本是一个残缺的简单列表实现,其余布局成果也能够通过实现不同的 LayoutManager 来实现。
残缺代码在自己的码云我的项目上 , 在 com.profound.notes.component 包下,路过的请帮忙点个 star。https://gitee.com/profound-la…
原文链接:https://developer.huawei.com/…
原作者:zjwujlei