在鴻蒙中實現類似瀑布流效果
簡介
鴻蒙OS 開發SDK中對於長列表的實現ListContainer的實現較為簡單,沒法想RecyclerView一樣透過使用不同的LayoutManager來實現複雜佈局因此沒法快速實現瀑布流效果。
但鴻蒙OS也都支援控制元件的Measure(onEstimateSize),layout(onArrange) 和事件的處理。完全可以在鴻蒙OS中自定義一個佈局來實現RecyclerView+LayoutManager的效果,以此來實現瀑布流等複雜效果。
自定義佈局
對於鴻蒙OS自定義佈局在官網上有介紹,主要實現onEstimateSize來測量控制元件大小和onArrange實現佈局,這裡我們將子控制元件的確定和測量擺放完全交LayoutManager來實現。同時我們要支援滑動,這裡用Component.DraggedListener實現。因此我們的佈局容器十分簡單,呼叫LayoutManager進行測量佈局,同時對於滑動事件,確定滑動後的視窗,呼叫LayoutManager的fill函式確定填滿視窗的子容器集合,然後觸發重新繪製。核心程式碼如下
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函式實現如下:
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的實現核心程式碼如下:
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://developer.huawei.com/consumer/cn/forum/topic/0202558139689270488?fid=0101303901040230869
原作者: zjwujlei
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69970551/viewspace-2774167/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- vue實現瀑布流Vue
- css實現瀑布流CSS
- 卡片瀑布流實現
- 瀑布流簡單實現
- 如何在直播軟體搭建中,實現圖片瀑布流效果?
- jQuery實現瀑布流佈局jQuery
- 深度解析:在 React 中實現類似 Vue 的 KeepAlive 元件ReactVue元件
- 記錄:瀑布流最佳實現方案
- JS實現動態瀑布流及放大切換圖片效果(js案例)JS
- 原生 js 實現瀑布流佈局、React 版本的瀑布流佈局元件JSReact元件
- Laravel 小技巧 - 讓路由實現類似 Model::query 的效果Laravel路由
- 純CSS實現瀑布流,你會嗎?CSS
- 如何在 web 頁面中實現類似 excel 固定表頭 / 標題行的效果?WebExcel
- 鴻蒙OS的系統呼叫是如何實現的? | 解讀鴻蒙原始碼鴻蒙原始碼
- 在直播軟體搭建中有哪些可以實現瀑布流的的方法?
- mysql 效果類似split函式MySql函式
- 【瀑布流】
- Flutter 實現類似美團外賣店鋪頁面滑動效果Flutter
- 優酷鴻蒙開發實踐 | 鴻蒙卡片開發鴻蒙
- 鴻蒙(HarmonyOS)實現隱私政策彈窗鴻蒙
- 在dotnet core實現類似crontab的定時任務
- 鴻蒙HarmonyOS實戰-ArkTS語言基礎類庫(容器類庫)鴻蒙
- SpringBoot+WebFlux透過流式響應實現類似ChatGPT的打字機效果Spring BootWebUXChatGPT
- PostgreSQL類似OracleMERGE功能的實現SQLOracle
- Bootstrap實戰 - 瀑布流佈局boot
- 鴻蒙HarmonyOS實戰-ArkTS語言基礎類庫(通知)鴻蒙
- 鴻蒙HarmonyOS實戰-ArkTS語言基礎類庫(XML)鴻蒙XML
- 鴻蒙HarmonyOS實戰-ArkTS語言基礎類庫(概述)鴻蒙
- 鴻蒙系統(HarmonyOS)全域性彈窗實現鴻蒙
- 短視訊app開發,Flutter StaggeredGridView的瀑布流效果APPFlutterView
- 談談實現瀑布流佈局的幾種思路
- RabbitMQ推出類似Kafka的流StreamMQKafka
- 在鴻蒙開發中,如何實現一個簡單的應用間通訊(Event Bus)功能?鴻蒙
- 小程式底部彈框 類似picker效果
- 鴻蒙(Harmony) NEXT - AlphabetIndexer實現聯絡人字母索引鴻蒙AlphabetIndex索引
- Material Design之RecyclerView基本講解與瀑布流的實現Material DesignView
- Flutter簡單實現手寫瀑布流 第二篇Flutter
- 手機直播原始碼,實現圖片瀑布流式滑動效果原始碼