在鴻蒙中實現類似瀑布流效果


簡介

  鴻蒙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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM