HarmonyOS自定義控件之測量與布局
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
在HarmonyOS中,控件最終展示到用戶界面上,會經(jīng)歷測量(Estimate)、布局(Arrange)、繪制(Draw)等過程。這里我們來分析一下測量與布局的用法,并且結(jié)合上一篇文章事件分發(fā)一起實現(xiàn)一個簡單的滾動視差布局ParallaxLayout。
ParallaxLayout效果圖:
測量Estimate
如何自定義測量過程
首先通過setEstimateSizeListener(Component.EstimateSizeListener listener)來設(shè)置測量的回調(diào):
- setEstimateSizeListener(new EstimateSizeListener() {
- @Override
- public boolean onEstimateSize(int widthEstimateSpec, int heightEstimateSpec) {
- return false;
- }
- });
在EstimateSizeListener 中,第一個參數(shù)為寬度測量參數(shù),第二個為高度測量參數(shù),我們可以通過EstimateSpec來獲取寬度、高度的模式(mode)與大小(size):
- int mode = EstimateSpec.getMode(widthEstimateSpec);
- int size = EstimateSpec.getSize(widthEstimateSpec);
計算了控件最終大小之后,我們可以調(diào)用setEstimatedSize(int estimatedWidth, int estimatedHeight)函數(shù)來設(shè)置最終的測量大小。注意:setEstimatedSize函數(shù)需要的是具體的大小,而非測量參數(shù),即EstimateSpec.getSize后的具體大小
- setEstimatedSize(widthSize, heightSize);
最后,如果需要讓設(shè)置的測量大小生效,我們應(yīng)該在onEstimateSize中返回true:
- setEstimateSizeListener(new EstimateSizeListener() {
- @Override
- public boolean onEstimateSize(int widthEstimateSpec, int heightEstimateSpec) {
- int widthSize = EstimateSpec.getSize(widthEstimateSpec);
- int heightSize = EstimateSpec.getSize(heightEstimateSpec);
- setEstimatedSize(widthSize, heightSize);
- return true;
- }
- });
如果onEstimateSize返回true,那么最終系統(tǒng)不會在native層來測量大小。如果返回了false,系統(tǒng)還是會繼續(xù)測量大小,最終的大小可能與setEstimatedSize設(shè)置的結(jié)果不一致。
在setEstimatedSize后我們就可以通過下面的函數(shù)來獲取我們設(shè)置的大?。?/p>
- getEstimatedWidth(); // 獲取測量的寬度
- getEstimatedHeight(); // 獲取測量的高度
EstimateSpec
EstimateSpec是Component的內(nèi)部類,EstimateSpec提供了一系列的操作測量參數(shù)的方法。
測量參數(shù)
測量參數(shù)是一個int值,它封裝了來自父控件的測量需求。測量參數(shù)由模式與大小兩個int值組合而成,公式如下:
- (size & ~EstimateSpec.ESTIMATED_STATE_BIT_MASK) | (mode & EstimateSpec.ESTIMATED_STATE_BIT_MASK)
其中size為當(dāng)前控件的大小,mode為模式。也可以通過下面的函數(shù)來,通過size和mode來生成一個測量參數(shù):
- int spec = EstimateSpec.getSizeWithMode(size, mode);
模式mode
通過EstimateSpec獲取的mode有三種取值:
- EstimateSpec.NOT_EXCEED 不超過:該模式表示父控件已經(jīng)規(guī)定了當(dāng)前控件大小的最大值
- EstimateSpec.PRECISE 精確:該模式表示父控件已經(jīng)規(guī)定了當(dāng)前控件大小的值
- EstimateSpec.UNCONSTRAINT 無約束:該模式表示父控件對當(dāng)前控件大小沒有約束,控件可以想要任何大小
在不同模式下,控件的大小是如何確定的呢?可以簡單的通過下面的代碼來理解:
- // size變量為控件期望的大小,estimateSpec變量為父控件的測量參數(shù)
- final int specMode = EstimateSpec.getMode(estimateSpec);
- final int specSize = EstimateSpec.getSize(estimateSpec);
- final int result;
- switch (specMode) {
- case EstimateSpec.NOT_EXCEED:
- result = Math.min(specSize, size);
- break;
- case EstimateSpec.PRECISE:
- result = specSize;
- break;
- case EstimateSpec.UNCONSTRAINT:
- default:
- result = size;
- }
- 當(dāng)mode為NOT_EXCEED時,控件的期望大小應(yīng)該小于等于父控件給定的size
- 當(dāng)mode為PRECISE時,控件的大小應(yīng)該等于父控件給定的size
- 當(dāng)mode為UNCONSTRAINT時,控件的大小可以為他期望的size
自定義布局
在自定義布局時,我們不僅僅要測量自己的大小,還需要測量子控件的大小。子控件可以通過estimateSize(int widthEstimatedConfig, int heightEstimatedConfig)函數(shù)來設(shè)置測量參數(shù):
- child.estimateSize(widthEstimatedConfig, heightEstimatedConfig);
注意:estimateSize的兩個參數(shù)需要的是測量參數(shù),而非具體的大小。這兩個參數(shù)會傳遞到子控件的onEstimateSize(int widthEstimateSpec, int heightEstimateSpec)回調(diào)中。
默認(rèn)情況下,子控件會根據(jù)widthEstimatedConfig與heightEstimatedConfig來確認(rèn)自己的最終大小,子控件也可以通過setEstimateSizeListener來自定義其測量過程,最終其參考的測量參數(shù)就是我們通過estimateSize函數(shù)設(shè)置的測量參數(shù)。
接下來我們只需要遍歷所有子控件來為他們設(shè)置測量參數(shù)就達(dá)到了測量子控件的大小的目的。自定義布局的測量過程基本就是包含了這兩個步驟:為所有子控件設(shè)置測量參數(shù)以及測量自己的大小。
子控件的測量參數(shù)
那么,最重要的問題是,我們?nèi)绾未_定子控件的測量參數(shù)到底應(yīng)該是多少,換句話說我們?nèi)绾紊苫蛘攉@取子控件的測量參數(shù)呢?子控件的測量參數(shù)與很多因素有關(guān),如父控件的測量參數(shù)、父控件的padding值、子控件自己的期望大小。我們可以根據(jù)這幾個參數(shù)來確定子控件的測量參數(shù)。
這里我們通過一個幫助函數(shù)來生成子控件的測量參數(shù),首先函數(shù)的定義應(yīng)該如下:
- /**
- * 根據(jù)父component的spec、padding以及子component的期望大小,生成子component的spec
- * @param spec 父component的spec
- * @param padding 父component的padding
- * @param childDimension 子component的期望大小
- * @return 子component的spec
- */
- public static int getChildEstimateSpec(int spec, int padding, int childDimension);
注意:childDimension應(yīng)該怎么獲取呢?實際上就是ComponentContainer.LayoutConfig中的width或者h(yuǎn)eight,測量高度就取height、寬度就取width。
接下來我們應(yīng)該根據(jù)父控件的mode以及childDimension來確定子控件的mode與size,并生成測量參數(shù)。具體參考如下代碼:
- public static int getChildEstimateSpec(int spec, int padding, int childDimension) {
- int specMode = EstimateSpec.getMode(spec);
- int specSize = EstimateSpec.getSize(spec);
- int size = Math.max(0, specSize - padding);
- int resultSize = 0;
- int resultMode = 0;
- switch (specMode) {
- // Parent has imposed an exact size on us
- case EstimateSpec.PRECISE:
- if (childDimension >= 0) {
- resultSize = childDimension;
- resultMode = EstimateSpec.PRECISE;
- } else if (childDimension == ComponentContainer.LayoutConfig.MATCH_PARENT) {
- // Child wants to be our size. So be it.
- resultSize = size;
- resultMode = EstimateSpec.PRECISE;
- } else if (childDimension == ComponentContainer.LayoutConfig.MATCH_CONTENT) {
- // Child wants to determine its own size. It can't be
- // bigger than us.
- resultSize = size;
- resultMode = EstimateSpec.NOT_EXCEED;
- if (size == 0) {
- // size will not be 0 when resultMode is NOT_EXCEED, don't know why
- resultMode = EstimateSpec.PRECISE;
- }
- }
- break;
- // Parent has imposed a maximum size on us
- case EstimateSpec.NOT_EXCEED:
- if (childDimension >= 0) {
- // Child wants a specific size... so be it
- resultSize = childDimension;
- resultMode = EstimateSpec.PRECISE;
- } else if (childDimension == ComponentContainer.LayoutConfig.MATCH_PARENT) {
- // Child wants to be our size, but our size is not fixed.
- // Constrain child to not be bigger than us.
- resultSize = size;
- resultMode = EstimateSpec.NOT_EXCEED;
- } else if (childDimension == ComponentContainer.LayoutConfig.MATCH_CONTENT) {
- // Child wants to determine its own size. It can't be
- // bigger than us.
- resultSize = size;
- resultMode = EstimateSpec.NOT_EXCEED;
- if (size == 0) {
- // size will not be 0 when resultMode is NOT_EXCEED, don't know why
- resultMode = EstimateSpec.PRECISE;
- }
- }
- break;
- // Parent asked to see how big we want to be
- case EstimateSpec.UNCONSTRAINT:
- if (childDimension >= 0) {
- // Child wants a specific size... let him have it
- resultSize = childDimension;
- resultMode = EstimateSpec.PRECISE;
- } else if (childDimension == ComponentContainer.LayoutConfig.MATCH_PARENT) {
- // Child wants to be our size... find out how big it should
- // be
- resultSize = size;
- resultMode = EstimateSpec.UNCONSTRAINT;
- } else if (childDimension == ComponentContainer.LayoutConfig.MATCH_CONTENT) {
- // Child wants to determine its own size.... find out how
- // big it should be
- resultSize = size;
- resultMode = EstimateSpec.UNCONSTRAINT;
- }
- break;
- }
- return makeEstimateSpec(resultSize, resultMode);
- }
makeEstimateSpec函數(shù)實際就是(size & ~EstimateSpec.ESTIMATED_STATE_BIT_MASK) | (mode & EstimateSpec.ESTIMATED_STATE_BIT_MASK)的值。
測量過程到此就基本結(jié)束,接下來看看稍微簡單一點的布局。
布局Arrange
如何自定義布局過程
首先通過setArrangeListener(ComponentContainer.ArrangeListener listener)來設(shè)置測量的回調(diào):
- setArrangeListener(new ArrangeListener() {
- @Override
- public boolean onArrange(int l, int t, int width, int height) {
- return false;
- }
- });
與測量類似,布局也是通過回調(diào)函數(shù)的方式來自定義。其中第一個參數(shù)為該控件的left值,第二個為top值,第三個為寬度,第四個為高度。
setArrangeListener是在ComponentContainer中定義的,Component中沒有。
與測量不同的是,控件本身的位置與寬高已經(jīng)由父控件確定了,即為onArrange回調(diào)中的四個參數(shù)。
在Arrange過程中,我們需要做的就是遞歸為每個子控件設(shè)置位置。通過調(diào)用子控件的arrange(int left, int top, int width, int height)函數(shù)來排列子元素:
- child.arrange(left, top, child.getEstimatedWidth(), child.getEstimatedHeight());
同樣的,onArrange回調(diào)需要返回true,才會使布局生效。
在調(diào)用了child的arrange函數(shù)后,就能通過child.getWidth()與child.getHeight()來獲取子控件的寬高了。
一個簡單的垂直順序排列布局的簡化代碼如下:
- @Override
- public boolean onArrange(int l, int t, int width, int height) {
- int childCount = getChildCount();
- int childTop = t;
- for(int i = 0; i < childCount; i++) {
- Component child = getComponentAt(i);
- int childHeight = child.getEstimatedHeight();
- child.arrange(l, childTop, child.getEstimatedWidth(), childHeight);
- childTop += childHeight;
- }
- return true;
- }
注意:不管是onArrange回調(diào)還是子控件的arrange函數(shù),最后兩個參數(shù)都是寬與高,而不是right與bottom。
綜合
接下來我們結(jié)合我們前一篇自定義控件之觸摸事件,與測量、布局一起,來自定義一個簡單的滾動視差布局ParallaxLayout。
- ParallaxLayout包含有兩個子控件,第一個固定150vp的Image。第二個是高度為match_parent的Text控件
- 在onEstimateSize中,主要是遍歷子控件為其設(shè)置測量參數(shù),并為自己設(shè)置測量結(jié)果。Image的測量高度為固定150vp,Text的高度與布局一致,我們需要通過測量參數(shù)與LayoutConfig計算出所有子控件的高度與自己的高度。
- 在onArrange中,按順序垂直排列子控件。由于Image+Text的高度已經(jīng)超出了自己的高度,因此Text的底部會有一部分顯示不出來。
- 在onTouchEvent中,通過計算手指的移動距離,為每個子控件setTranslateY,來實現(xiàn)位移的效果。最大位移距離為Image的高度。
- 通過為Image設(shè)置一半的translateY,為Text設(shè)置全部的translateY來實現(xiàn)滾動視差效果,關(guān)鍵代碼如下:
- for (int i = 0; i < childCount; i++) {
- Component child = getComponentAt(i);
- if (i == 0) {
- child.setTranslationY(deltaY / 2);
- } else {
- child.setTranslationY(deltaY);
- }
- }
具體代碼參考:parallax-layout
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)