網站首頁 編程語言 正文
前言
前面幾篇我們簡單的復習了一下自定義 View 的測量與繪制,并且回顧了常見的一些事件的處理方式。
那么如果我們想自定義 ViewGroup 的話,它和自定義View又有什么區別呢?其實我們把 ViewGroup 當做 View 來用的話也不是不可以。但是既然我們用到了容器 ViewGroup 當時是想用它的一些特殊的特性了。
比如 ViewGroup 的測量,ViewGroup的布局,ViewGroup的繪制。
- ViewGroup的測量:與 View 的測量不同,ViewGroup 的測量會遍歷子 View ,獲取子 View 的大小,從而決定自己的大小。當然我們也可以通過指定的模式來指定自身的大小。
- ViewGroup的布局:這個是 ViewGroup 核心與常用的功能。找到對于的子View 布局到指定的位置。
- ViewGroup的繪制:一般我們不會重寫這個方法,因為一般來說它本身不需要繪制,并且當我們沒有設置ViewGroup的背景的時候,onDraw()方法都不會被調用,一般來說 ViewGroup 只是會使用 dispatchDraw()方法來繪制其子View,其過程同樣是通過遍歷所有子View,并調用子View的繪制方法來完成繪制工作。
下面我們一起復習一下ViewGroup的測量布局方式。我們以入門級的 FlowLayout 為例,看看流式布局是如何測量與布局的。
話不多說,Let's go
一、基本的測量與布局
我們先回顧一下ViewGroup的
一個經典的ViewGroup測量是怎樣實現?一般來說,最簡單的測量如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for(int i = 0; i < getChildCount(); i++){
View childView = getChildAt(i);
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
}
}
或者我們直接使用封裝之后的默認方法
measureChildren(widthMeasureSpec,heightMeasureSpec);
其內部也是遍歷子View來實現的。當然如果有自定義的一些寬高測量規則,就不能使用這個方法,就需要自己遍歷找到View自定義實現了。
需要注意的是,這里我們測量子布局傳遞的 widthMeasureSpec 和 heightMeasureSpec 是父布局的測量模式。
當父布局設置為固定寬度的時候,子View是不能超過這個寬度的,比如父控件設置為match_parent,自定義View無論是match_parent 還是 wrap_content 都是一樣的,充滿整個父控件。
相當于父布局調用子控件的onMeasure方法的時候告訴子控件,我就這么大,你看著辦,不能超過它。
而父布局傳遞的是自適應AT_MOST模式,那么就是由子View來決定父布局的寬高。
相當于父布局調用子控件的onMeasure方法的時候問子控件,我也不知道我多大,你需要多大的位置?我又需要多大的地方才能容納你?
其實也很好理解。那么一個經典的ViewGroup布局又是怎樣實現?重寫 onLayout 并且遍歷拿到每一個View,進行Layout操作。
比如如下的代碼,我們每一個View的高度設置為固定高度,并且垂直排列,類似一個ListView 的布局:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
//設置子View的高度
MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
params.height = mFixedHeight * childCount;
setLayoutParams(params);
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
child.layout(l, i * mFixedHeight, r, (i + 1) * mFixedHeight);
}
}
}
注意我們 onLayout() 的參數
展示的效果就是這樣:
二、流式的布局的layout
首先我們先不管測量,我們先指定ViewGroup的寬高為固定寬高,指定為match_parent。我們先做布局的操作:
我們自定義 ViewGroup 中重寫測量與布局的方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec,heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* @param changed 當前ViewGroup的尺寸或者位置是否發生了改變
* @param l,t,r,b 當前ViewGroup相對于父控件的坐標位置,
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int mViewGroupWidth = getMeasuredWidth(); //當前ViewGroup的總寬度
int layoutChildViewCurX = l; //當前繪制View的X坐標
int layoutChildViewCurY = t; //當前繪制View的Y坐標
int childCount = getChildCount(); //子控件的數量
//遍歷所有子控件,并在其位置上繪制子控件
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//子控件的寬和高
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
//如果剩余控件不夠,則移到下一行開始位置
if (layoutChildViewCurX + width > mViewGroupWidth) {
layoutChildViewCurX = l;
//如果換行,則需要修改當前繪制的高度位置
layoutChildViewCurY += height;
}
//執行childView的布局與繪制(右和下的位置加上自身的寬高即可)
childView.layout(layoutChildViewCurX, layoutChildViewCurY, layoutChildViewCurX + width, layoutChildViewCurY + height);
//布局完成之后,下一次繪制的X坐標需要加上寬度
layoutChildViewCurX += width;
}
}
最后我們就能得到對應的換行效果,如下:
通過上面我們的基礎學習,我們應該能理解這樣的布局方式,跟上面的基礎布局方式相比,就是多了一個 layoutChildViewCurX 和 layoutChildViewCurY 。關于其它的邏輯這里已經注釋的非常清楚了。
但是這樣的效果好丑,我們加上間距 margin 試試?
并沒有效果,其實是內部 View 的 LayoutParams 就不支持 margin,我們需要定義一個內部類繼承 ViewGroup.MarginLayoutParams,并重寫generateLayoutParams() 方法。
//要使子控件的margin屬性有效必須繼承此LayoutParams,內部還可以定制一些別的屬性
public static class LayoutParams extends MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams layoutParams) {
super(layoutParams);
}
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new ViewGroup2.LayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
然后修改一下代碼,在 layout 子布局的時候我們手動的把 margin 加上。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int mViewGroupWidth = getMeasuredWidth(); //當前ViewGroup的總寬度
int layoutChildViewCurX = l; //當前繪制View的X坐標
int layoutChildViewCurY = t; //當前繪制View的Y坐標
int childCount = getChildCount(); //子控件的數量
//遍歷所有子控件,并在其位置上繪制子控件
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//子控件的寬和高
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
final LayoutParams lp = (LayoutParams) childView.getLayoutParams();
//如果剩余控件不夠,則移到下一行開始位置
if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > mViewGroupWidth) {
layoutChildViewCurX = l;
//如果換行,則需要修改當前繪制的高度位置
layoutChildViewCurY += height + lp.topMargin + lp.bottomMargin;
}
//執行childView的布局與繪制(右和下的位置加上自身的寬高即可)
childView.layout(
layoutChildViewCurX + lp.leftMargin,
layoutChildViewCurY + lp.topMargin,
layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin,
layoutChildViewCurY + height + lp.topMargin + lp.bottomMargin);
//布局完成之后,下一次繪制的X坐標需要加上寬度
layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin;
}
}
此時的效果就能生效了:
三、流式的布局的Measure
前面的設置我們都是使用的寬高 match_parent。那我們修改 ViewGroup 的高度為 wrap_content ,能實現高度自適應嗎?
這...并不是我們想要的效果。并沒有自適應高度。因為我們沒有寫測量的邏輯。
我們想一下,如果我們的寬度是固定的,想要高度自適應,那么我們就需要測量每一個子View的高度,計算出對應的高度,當換行之后我們再加上行的高度。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - this.getPaddingRight() - this.getPaddingLeft();
final int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
final int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - this.getPaddingTop() - this.getPaddingBottom();
final int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
if (modeWidth == MeasureSpec.EXACTLY && modeHeight == MeasureSpec.EXACTLY) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} else if (modeWidth == MeasureSpec.EXACTLY && modeHeight == MeasureSpec.AT_MOST) {
int layoutChildViewCurX = this.getPaddingLeft();
int totalControlHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
final View childView = this.getChildAt(i);
if (childView.getVisibility() == GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) childView.getLayoutParams();
childView.measure(
getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight(), lp.width),
getChildMeasureSpec(heightMeasureSpec, this.getPaddingTop() + this.getPaddingBottom(), lp.height)
);
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
if (totalControlHeight == 0) {
totalControlHeight = height + lp.topMargin + lp.bottomMargin;
}
//如果剩余控件不夠,則移到下一行開始位置
if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > sizeWidth) {
layoutChildViewCurX = this.getPaddingLeft();
totalControlHeight += height + lp.topMargin + lp.bottomMargin;
}
layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin;
}
//最后確定整個布局的高度和寬度
int cachedTotalWith = resolveSize(sizeWidth, widthMeasureSpec);
int cachedTotalHeight = resolveSize(totalControlHeight, heightMeasureSpec);
this.setMeasuredDimension(cachedTotalWith, cachedTotalHeight);
}
寬度固定和高度自適應的情況下,我們是這么處理的。計算出子View的總高度,然后設置 setMeasuredDimension 為ViewGroup的測量寬度和子View的總高度。即為最終 ViewGroup 的寬高。
這樣我們就能實現高度的自適應了。那么寬度能不能自適應呢?
當然可以,我們只需要記錄每一行的寬度,然后最終 setMeasuredDimension 的時候傳入所有行中的最大寬度,就是 ViewGroup 的最終寬度,而高度的計算是和上面的方式一樣的。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
else if (modeWidth == MeasureSpec.AT_MOST && modeHeight == MeasureSpec.AT_MOST) {
//如果寬高都是Wrap-Content
int layoutChildViewCurX = this.getPaddingLeft();
//總寬度和總高度
int totalControlWidth = 0;
int totalControlHeight = 0;
//由于寬度是非固定的,所以用一個List接收每一行的最大寬度
List<Integer> lineLenghts = new ArrayList<>();
for (int i = 0; i < getChildCount(); i++) {
final View childView = this.getChildAt(i);
if (childView.getVisibility() == GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) childView.getLayoutParams();
childView.measure(
getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight(), lp.width),
getChildMeasureSpec(heightMeasureSpec, this.getPaddingTop() + this.getPaddingBottom(), lp.height)
);
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
if (totalControlHeight == 0) {
totalControlHeight = height + lp.topMargin + lp.bottomMargin;
}
//如果剩余控件不夠,則移到下一行開始位置
if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > sizeWidth) {
lineLenghts.add(layoutChildViewCurX);
layoutChildViewCurX = this.getPaddingLeft();
totalControlHeight += height + lp.topMargin + lp.bottomMargin;
}
layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin;
}
//計算每一行的寬度,選出最大值
YYLogUtils.w("每一行的寬度 :" + lineLenghts.toString());
totalControlWidth = Collections.max(lineLenghts);
YYLogUtils.w("選出最大寬度 :" + totalControlWidth);
//最后確定整個布局的高度和寬度
int cachedTotalWith = resolveSize(totalControlWidth, widthMeasureSpec);
int cachedTotalHeight = resolveSize(totalControlHeight, heightMeasureSpec);
this.setMeasuredDimension(cachedTotalWith, cachedTotalHeight);
}
}
為了效果,我們把第一行的最后一個View寬度多一點,方便查看效果。
這樣就可以得到ViewGroup自適應的寬度和高度了。并不復雜對不對!
后記
這樣是不是就能實現一個簡單的流式布局了呢?當然這些只是為方便學習和理解,真正的實戰中并不推薦直接這樣使用,因為內部還有一些兼容的邏輯沒處理,一些邏輯沒有封裝,屬性沒有抽取。甚至連每一個View的高度,和每一行的最大高度也沒有處理,其實這樣健壯性并不好。
原文鏈接:https://juejin.cn/post/7174677919317360698
相關推薦
- 2022-10-17 Python實現在一行中交換兩個變量_python
- 2022-04-04 react 報錯Assign arrow function to a variable before
- 2023-01-05 C++模板?index_sequence使用示例詳解_C 語言
- 2023-12-21 npm ERR! code EPERM npm ERR! syscall unlink npm ER
- 2022-03-29 C語言中的盜賊(小偷)問題詳解_C 語言
- 2023-03-29 Pytorch損失函數torch.nn.NLLLoss()的使用_python
- 2022-09-03 PyQt5實現tableWidget?居中顯示_python
- 2022-08-18 C++詳解如何實現動態數組_C 語言
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支