标签:plain count() 多列 public fill函数 ima href start otto
鸿蒙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主要是用来确定子控件的布局,重点是要实现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 ; } |
对于长列表,肯定要有类似于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
标签:plain count() 多列 public fill函数 ima href start otto
原文地址:https://www.cnblogs.com/developer-huawei/p/14817924.html