简介
  鸿蒙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