zoukankan      html  css  js  c++  java
  • 在鸿蒙中实现类似瀑布流效果

    简介

      鸿蒙OS 开发SDK中对于长列表的实现ListContainer的实现较为简单,没法想RecyclerView一样通过使用不同的LayoutManager来实现复杂布局因此没法快速实现瀑布流效果。
      但鸿蒙OS也都支持控件的Measure(onEstimateSize),layout(onArrange) 和事件的处理。完全可以在鸿蒙OS中自定义一个布局来实现RecyclerView+LayoutManager的效果,以此来实现瀑布流等复杂效果。


    自定义布局

      对于鸿蒙OS自定义布局在官网上有介绍,主要实现onEstimateSize来测量控件大小和onArrange实现布局,这里我们将子控件的确定和测量摆放完全交LayoutManager来实现。同时我们要支持滑动,这里用Component.DraggedListener实现。因此我们的布局容器十分简单,调用LayoutManager进行测量布局,同时对于滑动事件,确定滑动后的视窗,调用LayoutManager的fill函数确定填满视窗的子容器集合,然后触发重新绘制。核心代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    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函数实现如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    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的实现核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    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-lab/super-notes

    原文链接:https://developer.huawei.com/consumer/cn/forum/topic/0202558139689270488?fid=0101303901040230869

    原作者:zjwujlei

  • 相关阅读:
    08day 操作命令以及目录结构
    换工作
    json转为字典
    快速排序
    冒泡排序
    python函数-生成器
    关键字global
    函数的定义和参数调用
    count()函数与center()函数
    python字符串常用函数:strip()
  • 原文地址:https://www.cnblogs.com/developer-huawei/p/14817924.html
Copyright © 2011-2022 走看看