您的位置:首页 > 博客中心 > 互联网 >

在鸿蒙中实现类似瀑布流效果

时间:2022-05-11 11:24

简介

  鸿蒙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             Span span = new Span();             span.index = i;             mSpans[i] = span;             span.left = (int) (left + i*spanWidth);         }     }       LinkedList attached = mRecycler.getAttachedScrap();     if(attached.isEmpty()){         mRecycler.getAllScrap().clear();         int count = mProvider.getCount();         int okSpan = 0;         for (int i=0;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()                     mRecycler.recycle(first);//recycle本身会remove                     first.span.startLine += first.getVSpace();                     first = attached.peekFirst();                 }                   Span minEndLineSpan = getMinSpanWithEndLine();                 int index = last.index+1;                 while(index//需要填充                     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 mAllScrap = new ArrayList<>();     private LinkedList mAttachedScrap = new LinkedList<>();     private LinkedList mCacheScrap = new LinkedList();     private BaseItemProvider mProvider;     private SpanLayout mSpanLayout;     private int direction = 0;       public Recycler(SpanLayout layout, BaseItemProvider provider) {         this.mSpanLayout = layout;         this.mProvider = provider;     }       public ArrayList getAllScrap() {         return mAllScrap;     }       public LinkedList 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。

 

原文链接:

原作者:zjwujlei

本类排行

今日推荐

热门手游