階練中學(xué)】第2課:自定義布局與繪制進(jìn)階)
本節(jié)目標(biāo)理解 ArkUI 布局引擎“測量—布局—繪制”三階段管線掌握自定義布局的觸發(fā)機(jī)制掌握onMeasureSize與onPlaceChildren兩個(gè)核心回調(diào)的用法與配合關(guān)系掌握Measurable與Layoutable接口的能力能夠?qū)ψ咏M件進(jìn)行精確的尺寸測量與位置擺放掌握自定義布局的性能優(yōu)化策略行級緩存、可視區(qū)剪裁、固定尺寸聲明掌握Canvas自定義繪制的基本用法理解CanvasRenderingContext2D的生命周期與繪制時(shí)機(jī)掌握XComponent的 SURFACE 與 TEXTURE 兩種渲染模式的區(qū)別與適用場景能夠根據(jù)需求選擇正確的自定義布局或繪制方案構(gòu)建高性能的自定義 UI 組件一、ArkUI 布局引擎與自定義布局概述1.1 布局引擎的三階段管線ArkUI 的布局引擎在渲染前會經(jīng)歷測量Measure—布局Layout—繪制Draw三輪管線。每引入一層 Column 或 Row子節(jié)點(diǎn)就需要重新參與約束傳遞和尺寸協(xié)商??蚣艿哪J(rèn)布局組件Row/Column/Stack/Flex/Grid已經(jīng)覆蓋了絕大多數(shù)排列需求但當(dāng)內(nèi)置布局無法滿足特定排列邏輯時(shí)就需要接管布局控制權(quán)。1.2 自定義布局的定位自定義組件的自定義布局允許開發(fā)者通過onMeasureSize和onPlaceChildren接口以數(shù)據(jù)計(jì)算的方式精確控制子組件的位置和尺寸實(shí)現(xiàn)更靈活的布局效果。適用于需要實(shí)現(xiàn)復(fù)雜非標(biāo)準(zhǔn)布局、內(nèi)置布局組件無法滿足特定排列需求、需要根據(jù)動(dòng)態(tài)數(shù)據(jù)計(jì)算子組件位置和尺寸等場景。關(guān)鍵規(guī)則在自定義組件內(nèi)實(shí)現(xiàn)onMeasureSize、onPlaceChildren任一方法即視為實(shí)現(xiàn)自定義布局。為實(shí)現(xiàn)完整的自定義布局效果推薦同時(shí)實(shí)現(xiàn)兩種方法onMeasureSize確定組件尺寸后onPlaceChildren才能根據(jù)該尺寸正確布局子組件。從 API version 20 開始自定義布局中若子組件設(shè)置了LayoutPolicy的fixAtIdealSize屬性表示尺寸將不受父組件約束完全按照開發(fā)者自定義的尺寸范圍布局。重要約束自定義布局內(nèi)不支持使用懶加載包含Repeat和LazyForEach。如果需要配合長列表使用應(yīng)采用LazyDynamicLayout等專用懶加載布局方案。二、onMeasureSize 與 onPlaceChildren2.1 onMeasureSize 測量接口onMeasureSize在 ArkUI 框架確定自定義組件尺寸時(shí)被調(diào)用將自定義組件的節(jié)點(diǎn)信息和尺寸范圍傳遞給開發(fā)者。返回值為SizeResult表示自定義組件的最終尺寸。onMeasureSize(selfLayoutInfo:GeometryInfo,children:ArrayMeasurable,constraint:ConstraintSizeOptions):SizeResult參數(shù)說明selfLayoutInfo是父組件自定義組件的布局信息第一次布局時(shí)以自身設(shè)置的屬性為準(zhǔn)。children是子組件的測量信息數(shù)組每個(gè)Measurable對象提供measure()方法用于測量子組件尺寸。constraint是父組件傳遞的尺寸約束信息。關(guān)鍵規(guī)則不允許在onMeasureSize函數(shù)中改變狀態(tài)變量。父容器上設(shè)置的尺寸信息除aspectRatio之外優(yōu)先級小于onMeasureSize設(shè)置的尺寸信息。2.2 onPlaceChildren 布局接口onPlaceChildren在 ArkUI 框架布局自定義組件時(shí)被調(diào)用將子節(jié)點(diǎn)自身的尺寸范圍傳遞給該自定義組件。返回值為 void通過調(diào)用子組件的layout()方法完成位置擺放。onPlaceChildren(selfLayoutInfo:GeometryInfo,children:ArrayLayoutable,constraint:ConstraintSizeOptions):voidchildren數(shù)組中的每個(gè)Layoutable對象提供layout()方法用于設(shè)置子組件的位置和尺寸。不允許在onPlaceChildren函數(shù)中改變狀態(tài)變量。2.3 完整自定義布局示例以下實(shí)現(xiàn)一個(gè)簡單的“對角線排列”自定義布局classDiagonalLayoutextendsArrayLayoutable{}Componentstruct DiagonalLayoutDemo{build(){Column(){DiagonalLayoutContainer({builder:this.childrenBuilder})}.width(100%).height(100%)}BuilderchildrenBuilder(){ForEach([1,2,3,4],(index:number){Text(Item${index}).fontSize(20).width(80).height(40).backgroundColor(#007DFF).fontColor(Color.White).textAlign(TextAlign.Center).borderRadius(8)})}}Componentstruct DiagonalLayoutContainer{BuilderParambuilder:()void;onMeasureSize(selfLayoutInfo:GeometryInfo,children:ArrayMeasurable,constraint:ConstraintSizeOptions):SizeResult{// 每個(gè)子組件固定 80x40依次沿對角線排列constitemWidth80;constitemHeight40;consttotalWidthchildren.length*itemWidth;consttotalHeightchildren.length*itemHeight;children.forEach((child:Measurable){child.measure({maxWidth:itemWidth,maxHeight:itemHeight,minWidth:itemWidth,minHeight:itemHeight});});return{width:Math.min(totalWidth,constraint.maxWidthasnumber),height:Math.min(totalHeight,constraint.maxHeightasnumber)};}onPlaceChildren(selfLayoutInfo:GeometryInfo,children:ArrayLayoutable,constraint:ConstraintSizeOptions):void{constitemWidth80;constitemHeight40;children.forEach((child:Layoutable,index:number){child.layout({x:index*itemWidth,y:index*itemHeight,width:itemWidth,height:itemHeight});});}build(){Column(){this.builder()}}}這個(gè)示例中onMeasureSize負(fù)責(zé)測量每個(gè)子組件并計(jì)算容器的總尺寸onPlaceChildren負(fù)責(zé)將每個(gè)子組件擺放到對角線的對應(yīng)位置。三、自定義布局的性能優(yōu)化3.1 自定義布局的性能陷阱自定義布局最常見的性能問題是“每次布局遍歷全部子節(jié)點(diǎn)”。假設(shè)有一個(gè)包含 2000 個(gè)商品卡片的網(wǎng)格布局每次滾動(dòng)時(shí)如果都重新測量全部 2000 個(gè)卡片即使屏幕上只顯示 10 個(gè)也會造成嚴(yán)重的性能問題。核心解法分三步第一步用行級緩存隔離測量范圍。在onMeasureSize中維護(hù)一個(gè)Mapnumber, RowLayoutkey 是起始索引value 存儲該行的測量結(jié)果高度、子節(jié)點(diǎn)邊界。滾動(dòng)偏移變化時(shí)只計(jì)算新進(jìn)入可視區(qū)的那幾行已測量過的行直接從緩存取。第二步用LazyForEachReusableV2驅(qū)動(dòng)節(jié)點(diǎn)復(fù)用。自定義布局只管“位置擺放”節(jié)點(diǎn)創(chuàng)建交給LazyForEach。配合ReusableV2滾動(dòng)時(shí)組件實(shí)例會被回收復(fù)用不會反復(fù)創(chuàng)建銷毀。LazyForEach的 key 必須用數(shù)據(jù)項(xiàng)的唯一 ID不能用 index。第三步在onPlaceChildren中只排布可視區(qū)的行。根據(jù)當(dāng)前滾動(dòng)偏移算出可視區(qū)的起止行號只對這幾行調(diào)用child.layout()可視區(qū)外的節(jié)點(diǎn)不參與布局計(jì)算。3.2 可視區(qū)剪裁與緩存實(shí)戰(zhàn)ComponentV2struct VirtualGrid{LocalscrollOffset:number0;privaterowCache:Mapnumber,RowLayoutnewMap();privateitemHeight:number200;privatecolumns:number2;onMeasureSize(self:LayoutSelf,children:LayoutChildren,constraint:LayoutConstraint){// 只測量可視區(qū) 緩沖區(qū)的新行constvisibleStartRowMath.floor(this.scrollOffset/this.itemHeight);constvisibleEndRowvisibleStartRowMath.ceil(constraint.maxHeight/this.itemHeight)2;for(letrowvisibleStartRow;rowvisibleEndRow;row){if(!this.rowCache.has(row)){this.measureRow(row,children,constraint);}}return{width:constraint.maxWidth,height:this.totalHeight};}onPlaceChildren(self:LayoutSelf,children:LayoutChildren,constraint:LayoutConstraint){constvisibleStartRowMath.floor(this.scrollOffset/this.itemHeight);constvisibleEndRowvisibleStartRowMath.ceil(constraint.maxHeight/this.itemHeight)2;for(letrowvisibleStartRow;rowvisibleEndRow;row){constlayoutthis.rowCache.get(row);if(layout){layout.children.forEach((child,col){child.layout({x:col*this.itemWidth,y:row*this.itemHeight});});}}}}核心原則測量結(jié)果緩存化、布局范圍可視區(qū)化、節(jié)點(diǎn)創(chuàng)建復(fù)用化。3.3 固定尺寸聲明如果子組件尺寸是固定的應(yīng)在子組件上顯式聲明width和height避免onMeasureSize中的重復(fù)測量。如果尺寸在運(yùn)行時(shí)才確定應(yīng)在onMeasureSize中將測量結(jié)果緩存起來避免重復(fù)調(diào)用child.measure()。四、Canvas 自定義繪制4.1 Canvas 組件的基本用法Canvas組件用于自定義繪制最大允許尺寸不超過 10000px × 10000px超過最大面積則無法正常創(chuàng)建。CanvasRenderingContext2D是 Canvas 組件的 2D 繪圖上下文對象用于在 Canvas 組件上進(jìn)行自定義繪圖。建議將CanvasRenderingContext2D對象與 Canvas 組件封裝到同一個(gè)自定義組件中保證兩者一一對應(yīng)且生命周期保持一致。Componentstruct CustomChart{privatesettings:RenderingContextSettingsnewRenderingContextSettings(true);privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(this.settings);build(){Canvas(this.context).width(100%).height(300).backgroundColor(#F5F7FA).onReady((){this.drawChart();})}privatedrawChart():void{constctxthis.context;// 繪制坐標(biāo)軸ctx.beginPath();ctx.moveTo(40,10);ctx.lineTo(40,260);ctx.lineTo(360,260);ctx.strokeStyle#CCCCCC;ctx.stroke();// 繪制折線constdata[80,140,100,200,160,220,180];ctx.beginPath();data.forEach((value:number,index:number){constx40index*(320/(data.length-1));consty260-value;if(index0){ctx.moveTo(x,y);}else{ctx.lineTo(x,y);}});ctx.strokeStyle#007DFF;ctx.lineWidth2;ctx.stroke();}}4.2 繪制時(shí)機(jī)與生命周期Canvas 的onReady回調(diào)觸發(fā)后畫布寬高才確定且可獲取才能使用 Canvas 相關(guān) API 進(jìn)行繪制。繪制接口在調(diào)用時(shí)會存入被關(guān)聯(lián)的 Canvas 組件的指令隊(duì)列中僅當(dāng)當(dāng)前幀進(jìn)入渲染階段且關(guān)聯(lián)的 Canvas 組件處于可見狀態(tài)時(shí)這些指令才會從隊(duì)列中被提取并執(zhí)行。不可見場景Canvas 不響應(yīng)繪制命令頁面進(jìn)入后臺、組件滑到窗口外、visibility屬性設(shè)置為隱藏等。4.3 重繪策略當(dāng)數(shù)據(jù)變化需要重繪時(shí)應(yīng)先調(diào)用context.clearRect()清除畫布再重新繪制。避免在onReady之外頻繁調(diào)用繪制方法應(yīng)通過狀態(tài)變量驅(qū)動(dòng)條件判斷后重繪。4.4 Canvas 的適用場景Canvas 適合以下場景金融軟件的股票行情圖、基金折線圖自定義圖表柱狀圖、餅圖、雷達(dá)圖手寫板、簽名板圖片裁剪與濾鏡處理游戲畫面繪制。五、XComponent 原生渲染5.1 XComponent 的定位XComponent組件作為一種渲染組件可用于 EGL/OpenGLES 和媒體數(shù)據(jù)寫入通過使用 XComponent 持有的“NativeWindow”來渲染畫面通常用于滿足開發(fā)者較為復(fù)雜的自定義渲染需求例如相機(jī)預(yù)覽流的顯示和游戲畫面的渲染。5.2 SURFACE 與 TEXTURE 兩種模式XComponent 通過type字段實(shí)現(xiàn)不同的渲染方式SURFACE 類型開發(fā)者將定制的繪制內(nèi)容單獨(dú)展示到屏幕上。獨(dú)立 Surface不和組件樹合成直接上屏?xí)趽跸路浇M件。系統(tǒng)合成層自動(dòng) Y 翻轉(zhuǎn)修正顯示正常。TEXTURE 類型將定制的繪制內(nèi)容和 XComponent 組件的內(nèi)容合成后展示到屏幕上。走 UI 紋理貼圖無自動(dòng)翻轉(zhuǎn)OpenGL UV 與圖像坐標(biāo)系沖突可能導(dǎo)致顯示倒置。選型建議游戲渲染、相機(jī)預(yù)覽等需要獨(dú)立高性能渲染的場景優(yōu)先使用 SURFACE 類型。需要與 ArkUI 組件樹混合渲染、需要圓角/裁剪/透明度等效果時(shí)使用 TEXTURE 類型。5.3 觸摸事件回傳XComponent 在 Native 側(cè)通過OH_NativeXComponent拿到觸摸事件后需要安全地回傳到 ArkTS 并更新 UI 狀態(tài)。推薦通過registerCallback注冊事件回調(diào)在回調(diào)中使用napi_call_function調(diào)用 ArkTS 側(cè)的 JS 函數(shù)注意處理好線程切換和對象生命周期。六、多元化習(xí)題習(xí)題 1判斷題題目在 ArkUI 中自定義布局的onMeasureSize和onPlaceChildren兩個(gè)回調(diào)中可以修改狀態(tài)變量來驅(qū)動(dòng) UI 刷新。答案錯(cuò)誤解讀onMeasureSize和onPlaceChildren中均不允許改變狀態(tài)變量。這兩個(gè)回調(diào)是布局計(jì)算階段的純函數(shù)修改狀態(tài)變量會導(dǎo)致布局循環(huán)或未定義行為。習(xí)題 2單選題題目以下關(guān)于自定義布局接口的說法正確的是 A. 只實(shí)現(xiàn)onMeasureSize即可完成完整的自定義布局B. 只實(shí)現(xiàn)onPlaceChildren即可完成完整的自定義布局C. 推薦同時(shí)實(shí)現(xiàn)onMeasureSize和onPlaceChildren兩個(gè)方法D.onMeasureSize在onPlaceChildren之后執(zhí)行答案C解讀在自定義組件內(nèi)實(shí)現(xiàn)任一方法即視為實(shí)現(xiàn)自定義布局但為實(shí)現(xiàn)完整的自定義布局效果推薦同時(shí)實(shí)現(xiàn)兩種方法。onMeasureSize確定組件尺寸后onPlaceChildren才能根據(jù)該尺寸正確布局子組件。執(zhí)行順序是先onMeasureSize后onPlaceChildren。習(xí)題 3多選題題目關(guān)于自定義布局的性能優(yōu)化以下說法正確的有多選A. 使用行級緩存隔離測量范圍只測量可視區(qū)的新行B. 使用LazyForEachReusableV2驅(qū)動(dòng)節(jié)點(diǎn)復(fù)用C. 在onPlaceChildren中只排布可視區(qū)的行D. 自定義布局中可以直接使用LazyForEach實(shí)現(xiàn)懶加載答案A、B、C解讀行級緩存隔離測量范圍、LazyForEachReusableV2驅(qū)動(dòng)節(jié)點(diǎn)復(fù)用、只排布可視區(qū)的行都是自定義布局性能優(yōu)化的核心策略。但自定義布局內(nèi)不支持使用懶加載包含Repeat和LazyForEach選項(xiàng) D 錯(cuò)誤。節(jié)點(diǎn)創(chuàng)建應(yīng)交給外層的LazyForEach自定義布局只負(fù)責(zé)位置擺放。習(xí)題 4代碼填空題題目請補(bǔ)全以下自定義布局代碼在onMeasureSize中測量每個(gè)子組件并返回容器尺寸。onMeasureSize(selfLayoutInfo:GeometryInfo,children:ArrayMeasurable,constraint:ConstraintSizeOptions):SizeResult{constitemWidth100;constitemHeight50;children.forEach((child:______________){child.______________({maxWidth:itemWidth,maxHeight:itemHeight,minWidth:itemWidth,minHeight:itemHeight});});return{width:children.length*itemWidth,height:itemHeight};}答案Measurable、measure解讀onMeasureSize的children參數(shù)類型為ArrayMeasurable每個(gè)Measurable對象提供measure()方法用于測量子組件尺寸。傳入的constraint對象包含maxWidth、maxHeight、minWidth、minHeight等約束信息。習(xí)題 5代碼改錯(cuò)題題目以下 Canvas 繪制代碼存在空白問題請指出問題并修正。Componentstruct MyChart{privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D();aboutToAppear():void{this.drawChart();// 在 aboutToAppear 中直接繪制}build(){Canvas(this.context).width(100%).height(300)}privatedrawChart():void{this.context.fillStyle#007DFF;this.context.fillRect(10,10,100,100);}}答案在aboutToAppear中調(diào)用繪制方法時(shí)Canvas 組件的寬高還未確定CanvasRenderingContext2D的繪圖表面也未初始化導(dǎo)致繪制內(nèi)容無法顯示。修正方案是將繪制邏輯放在onReady回調(diào)中build(){Canvas(this.context).width(100%).height(300).onReady((){this.drawChart();// onReady 回調(diào)觸發(fā)后畫布寬高才確定})}解讀Canvas 的onReady回調(diào)觸發(fā)后畫布寬高才確定且可獲取才能使用 Canvas 相關(guān) API 進(jìn)行繪制。在aboutToAppear中調(diào)用繪制時(shí)Canvas 組件尚未完成初始化。習(xí)題 6簡答題題目簡述 XComponent 的 SURFACE 和 TEXTURE 兩種渲染模式的區(qū)別及適用場景。答案SURFACE 類型將定制的繪制內(nèi)容單獨(dú)展示到屏幕上是獨(dú)立的 Surface不和組件樹合成直接上屏。系統(tǒng)合成層自動(dòng) Y 翻轉(zhuǎn)修正顯示正常。適用于游戲渲染、相機(jī)預(yù)覽等需要獨(dú)立高性能渲染的場景但會遮擋下方組件。TEXTURE 類型將定制的繪制內(nèi)容和 XComponent 組件的內(nèi)容合成后展示到屏幕上走 UI 紋理貼圖無自動(dòng)翻轉(zhuǎn)OpenGL UV 與圖像坐標(biāo)系沖突可能導(dǎo)致顯示倒置。適用于需要與 ArkUI 組件樹混合渲染、需要圓角/裁剪/透明度等效果的場景。選型時(shí)需要獨(dú)立高性能渲染用 SURFACE需要與 ArkUI 混合渲染用 TEXTURE。解讀SURFACE 和 TEXTURE 的核心區(qū)別在于是否與 ArkUI 組件樹合成。SURFACE 是“獨(dú)立上屏”TEXTURE 是“合成上屏”。選擇時(shí)需要綜合考慮渲染性能、混合渲染需求、顯示方向等因素。習(xí)題 7簡答題題目簡述自定義布局中行級緩存優(yōu)化的核心思路以及為什么自定義布局內(nèi)不支持使用LazyForEach。答案行級緩存優(yōu)化的核心思路是在onMeasureSize中維護(hù)一個(gè)Mapnumber, RowLayoutkey 是起始索引value 存儲該行的測量結(jié)果高度、子節(jié)點(diǎn)邊界。滾動(dòng)偏移變化時(shí)只計(jì)算新進(jìn)入可視區(qū)的那幾行已測量過的行直接從緩存取。在onPlaceChildren中根據(jù)當(dāng)前滾動(dòng)偏移算出可視區(qū)的起止行號只對這幾行調(diào)用child.layout()可視區(qū)外的節(jié)點(diǎn)不參與布局計(jì)算。自定義布局內(nèi)不支持使用LazyForEach是因?yàn)樽远x布局接管了子組件的測量與布局邏輯而LazyForEach的按需創(chuàng)建機(jī)制需要由框架的布局引擎驅(qū)動(dòng)。如果自定義布局內(nèi)使用LazyForEach框架無法在自定義布局的測量/布局回調(diào)中正確感知子組件的創(chuàng)建時(shí)機(jī)。解決方案是將LazyForEach放在自定義布局的外層由外層驅(qū)動(dòng)節(jié)點(diǎn)創(chuàng)建自定義布局只負(fù)責(zé)位置擺放。解讀自定義布局與懶加載的職責(zé)邊界需要明確LazyForEach負(fù)責(zé)“創(chuàng)建哪些節(jié)點(diǎn)”自定義布局負(fù)責(zé)“把已創(chuàng)建的節(jié)點(diǎn)擺在哪里”。兩者協(xié)作而非嵌套才能實(shí)現(xiàn)最佳性能。七、本節(jié)知識點(diǎn)總結(jié)布局引擎三階段ArkUI 布局引擎經(jīng)歷測量—布局—繪制三輪管線。自定義布局通過onMeasureSize和onPlaceChildren兩個(gè)回調(diào)接管布局控制權(quán)。onMeasureSize在組件確定尺寸時(shí)調(diào)用接收自身布局信息、子組件測量信息數(shù)組和父組件約束。通過child.measure()測量子組件返回SizeResult設(shè)定自身尺寸。不允許改變狀態(tài)變量。onPlaceChildren在組件布局時(shí)調(diào)用接收子組件布局信息數(shù)組。通過child.layout()設(shè)置子組件位置和尺寸。不允許改變狀態(tài)變量。性能優(yōu)化三原則測量結(jié)果緩存化行級緩存隔離測量范圍、布局范圍可視區(qū)化只排布可視區(qū)的行、節(jié)點(diǎn)創(chuàng)建復(fù)用化LazyForEachReusableV2驅(qū)動(dòng)節(jié)點(diǎn)復(fù)用。Canvas 自定義繪制CanvasRenderingContext2D與 Canvas 組件一一對應(yīng)繪制邏輯放在onReady回調(diào)中。不可見場景下 Canvas 不響應(yīng)繪制命令。適用于圖表、手寫板等自繪場景。XComponent 原生渲染SURFACE 類型獨(dú)立上屏適用于游戲渲染、相機(jī)預(yù)覽TEXTURE 類型合成上屏適用于與 ArkUI 混合渲染。通過 NativeWindow 對接 EGL/OpenGLES。下節(jié)預(yù)告第3課將進(jìn)入自定義組件與節(jié)點(diǎn)操作的學(xué)習(xí)涵蓋FrameNode自定義節(jié)點(diǎn)、RenderNode渲染節(jié)點(diǎn)、組件動(dòng)態(tài)創(chuàng)建與預(yù)創(chuàng)建以及節(jié)點(diǎn)樹操作與性能優(yōu)化。