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