網站首頁 編程語言 正文
前言
在之前的文章我們復習了 ViewGroup 的測量與布局,那么我們這一篇效果就可以在之前的基礎上實現一個靈活的九宮格布局。
那么一個九宮格的 ViewGroup 如何定義,我們分解為如下的幾個步驟來實現:
- 先計算與測量九宮格內部的子View的寬度與高度。
- 再計算整體九宮格的寬度和高度。
- 進行子View九宮格的布局。
- 對單獨的圖片和四宮格的圖片進行單獨的布局處理
- 對填充的子View的方式進行抽取,可以自由添加布局。
- 對自定義屬性的抽取,設置通用的屬性。
只要在前文的基礎上掌握了 ViewGroup 的測量與布局,其實實現起來一點都不難,甚至我們還能實現一些特別的效果。
好了,話不多說,Let's go
一、九宮格的測量
之前的文章,我們的測量方式是已經知道子 View 的具體大小了,讓我們的父布局做寬高的適配,所以我們的邏輯順序也是先布局,然后再測量,對 ViewGroup 的寬高做限制。
但是在我們做九宮格控件的時候,就和之前有所區別了。我們不管子 View 的寬高測量模式是怎樣的,我們都是通過九宮格控件的寬度對子 View 的寬高進行強制賦值。
public class AbstractNineGridLayout extends ViewGroup {
private static final int MAX_CHILDREN_COUNT = 9; //最大的子View數量
private int horizontalSpacing = 20; //每一個Item的左右間距
private int verticalSpacing = 20; //每一個Item的上下間距
private int itemWidth;
private int itemHeight;
public AbstractNineGridLayout(Context context) {
this(context, null);
}
public AbstractNineGridLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AbstractNineGridLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
for (int i = 0; i < MAX_CHILDREN_COUNT; i++) {
ImageView imageView = new ImageView(context);
imageView.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
imageView.setBackgroundColor(Color.RED);
addView(imageView);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int notGoneChildCount = getNotGoneChildCount();
//不管什么模式,都是指定的固定寬高
itemWidth = (widthSize - horizontalSpacing * 2) / 3;
itemHeight = itemWidth;
//measureChildren內部調用measureChild,這里我們就可以指定寬高
measureChildren(MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(itemHeight, MeasureSpec.EXACTLY));
if (heightMode == MeasureSpec.EXACTLY) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} else {
notGoneChildCount = Math.min(notGoneChildCount, MAX_CHILDREN_COUNT);
int heightSize = ((notGoneChildCount - 1) / 3 + 1) *
(itemHeight + verticalSpacing) - verticalSpacing + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(widthSize, heightSize);
}
}
}
剛開始的時候我們在布局初始化的時候先添加5個 mathc_parent 的9個子 View 作為測試。那么我們在布局的時候,就需要對寬度進行分割,并且強制性的測量每一個子 View 的寬高為 EXACTLY 模式。
測量完每一個子 View 之后,我們再動態的給 ViewGroup 設置寬高。
這樣測量之后的效果為:
為了方便查看效果,加上了測試的灰色背景,看著大小是符合預期的。接下來我們就開始布局。
二、九宮格的布局
在之前流式布局的 onLayout 方法中,我們是通過動態的拿到每一個子 View 的寬度去判斷當前是否會超過總寬度,是否需要換行。
而這里我們就無需這么做了,因為每一個子 View 都是固定的寬度,一行就是三個,一列最多也是三個。我們直接通過子 View 的數量就可以確定當前的行數與列數。
然后我們就能行數和列數進行布局了,具體的看代碼:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int notGoneChildCount = getNotGoneChildCount();
int position = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
int row = position / 3; //當前子View是第幾行(索引)
int column = position % 3; //當前子View是第幾列(索引)
//當前需要繪制的光標的X與Y值
int x = column * itemWidth + getPaddingLeft() + horizontalSpacing * column;
int y = row * itemHeight + getPaddingTop() + verticalSpacing * row;
child.layout(x, y, x + itemWidth, y + itemHeight);
//最多只擺放9個
position++;
if (position == MAX_CHILDREN_COUNT) {
break;
}
}
}
效果為:
如果對行和列的計算不清楚的,我們可以對每一個子 View 的位置進行回顧,總共最多也就 9 個,當為第 0 個子 View 的時候,position為 0 ,那么 position / 3 是 0,row 就是 0, position % 3 也是 0,就是第最左上角的位置了。
當為第1個子 View 的時候,position為1 ,那么 position / 3 還是0,row就是0, position % 3是1了,就是第一排中間的位置了。
只有當View超過三個之后,position /3 就是 1 了,row為 1 之后,才是第二行的位置。依次類推就可以定位到每一個子 View 需要繪制的位置。
而 x 與 y 的值與計算邏輯,我們可以想象為需要繪制當前 View 的時候,當前畫筆需要所在的位置。加上左右和上下的間距之后,我們通過這樣的方式也可以實現 margin 的效果。還記得前文流式布局是怎么實現 margin 效果的嗎?殊途同歸的效果。
最后具體的 child.layout 反而是最簡單的,只需要繪制子 View 本身的寬高即可。
三、單圖片與四宮格的單獨處理
一般來說我們需要單獨的處理一張圖片與四張圖片的邏輯。包括測量與布局都需要單獨的處理。
一張圖片的時候,我們需要通過方法單獨的指定圖片的寬度與高度。而四張圖片我們需要固定兩行的高度即可。
public class AbstractNineGridLayout extends ViewGroup {
private static final int MAX_CHILDREN_COUNT = 9; //最大的子View數量
private int horizontalSpacing = 20; //每一個Item的左右間距
private int verticalSpacing = 20; //每一個Item的上下間距
private boolean fourGridMode = true; //是否支持四宮格模式
private boolean singleMode = true; //是否支持單布局模式
private boolean singleModeScale = true; //是否支持單布局模式按比例縮放
private int singleWidth;
private int singleHeight;
private int itemWidth;
private int itemHeight;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int notGoneChildCount = getNotGoneChildCount();
if (notGoneChildCount == 1 && singleMode) {
itemWidth = singleWidth > 0 ? singleWidth : widthSize;
itemHeight = singleHeight > 0 ? singleHeight : widthSize;
if (itemWidth > widthSize && singleModeScale) {
itemWidth = widthSize; //單張圖片先定寬度。
itemHeight = (int) (widthSize * 1f / singleWidth * singleHeight); //根據寬度計算高度
}
} else {
//除了單布局模式,其他的都是指定的固定寬高
itemWidth = (widthSize - horizontalSpacing * 2) / 3;
itemHeight = itemWidth;
}
...
}
/**
* 設置單獨布局的寬和高
*/
public void setSingleModeSize(int w, int h) {
if (w != 0 && h != 0) {
this.singleMode = true;
this.singleWidth = w;
this.singleHeight = h;
}
}
}
測量的時候我們對單布局進行測量,并且對超過寬度的一些布局做等比例的縮放。然后再測量父布局。
findViewById<AbstractNineGridLayout>(R.id.nine_grid).setSingleModeSize(dp2px(200f), dp2px(400f))
效果:
而如果是四宮格模式,我們好像也不需要重新測量,反正也是二行的高度,但是布局的時候我們需要處理一下,不然第三個子 View 的位置就會不對了。我們只需要修改x 與 y的計算方式,它們是根據行和列動態計算你的,那么修改行和列的計算方式即可。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int notGoneChildCount = getNotGoneChildCount();
int position = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
int row = position / 3; //當前子View是第幾行(索引)
int column = position % 3; //當前子View是第幾列(索引)
if (notGoneChildCount == 4 && fourGridMode) {
row = position / 2;
column = position % 2;
}
//當前需要繪制的光標的X與Y值
int x = column * itemWidth + getPaddingLeft() + horizontalSpacing * column;
int y = row * itemHeight + getPaddingTop() + verticalSpacing * row;
child.layout(x, y, x + itemWidth, y + itemHeight);
//最多只擺放9個
position++;
if (position == MAX_CHILDREN_COUNT) {
break;
}
}
}
/**
* 單獨設置是否支持四宮格模式
*/
public void setFourGridMode(boolean enable) {
this.fourGridMode = enable;
}
這樣我們就可以支持四宮格的布局模式,效果如下:
到此,我們的九宮格控件大體上是完工了,但是還不夠靈活,內部的子 View 都是我們自己 new 出來的,我們接下來就要暴露出去讓其可以自定義布局。
四、自定義布局的抽取
如何把填充布局的邏輯抽取出來呢?一般分為兩種思路:
- 每次初始化九宮格的時候就把九個布局全部添加進來,先測量布局了再說,然后通過暴露的方法隱藏多余的布局。
- 通過一個定義一個數據適配器Adapter,內部封裝一些邏輯,讓具體實現的類去完成具體的邏輯。
兩種方法都可以,沒有好壞之分。但是使用數據適配器的方案由于內部的View會少,性能會好那么一丟丟,總體來說差別不大。
4.1 先布局再隱藏的思路
一般我們在抽象的九宮格類中就需要暴露這兩個重要方法,一個是填充子布局的,一個是填充數據并且隱藏多余的布局。
//子類去實現-填充布局文件
protected abstract void fillChildView();
//子類去實現-對布局文件賦值數據(一般專門去給adapter去調用的)
public abstract void renderData(T data);
例如我們的實現類:
@Override
protected void fillChildView() {
inflateChildLayout(R.layout.item_image_grid);
imageViews = findInChildren(R.id.iv_image, ImageView.class);
}
@Override
public void renderData(List<ImageInfo> imageInfos) {
setSingleModeSize(imageInfos.get(0).getImageViewWidth(), imageInfos.get(0).getImageViewHeight());
setDisplayCount(imageInfos.size());
for (int i = 0; i < imageInfos.size(); i++) {
String url = imageInfos.get(i).getThumbnailUrl();
ImageView imageView = imageViews[i];
//使用自定義的Loader加載
mImageLoader.onDisplayImage(getContext(), imageView, url);
//點擊事件
setClickListener(imageView, i, imageInfos);
}
}
重點是填充的方法 inflateChildLayout 分為兩種情況,一種是布局都一樣的情況,一種是根據索引填充不同的布局情況。
/**
* 可以為每一個子布局加載對應的布局文件(不同的文件)
*/
protected void inflateChildLayoutCustom(ViewGetter viewGetter) {
removeAllViews();
for (int i = 0; i < MAX_CHILDREN_COUNT; i++) {
addView(viewGetter.getView(i));
}
}
/**
* 一般用這個方法填充布局,每一個小布局的布局文件(相同的文件)
*/
protected void inflateChildLayout(int layoutId) {
removeAllViews();
for (int i = 0; i < MAX_CHILDREN_COUNT; i++) {
LayoutInflater.from(getContext()).inflate(layoutId, this);
}
}
而我們設置數據的方法中調用的 setDisplayCount 方法則是隱藏多余的控件的。
/**
* 設置顯示的數量
*/
public void setDisplayCount(int count) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).setVisibility(i < count ? VISIBLE : GONE);
}
}
效果:
4.2 數據適配器的思路
而使用數據適配器的方案,就無需每次上來就先填充9個子布局,而是通過Adapter動態的配置當前需要填充的數量,并且創建對應的子 View 和綁定對應的子 View 的數據。
聽起來是不是很像RV的Apdater,沒錯就是參考它的實現方式。
我們先創建一個基類的Adapter:
public static abstract class Adapter {
//返回總共子View的數量
public abstract int getItemCount();
//根據索引創建不同的布局類型,如果都是一樣的布局則不需要重寫
public int getItemViewType(int position) {
return 0;
}
//根據類型創建對應的View布局
public abstract View onCreateItemView(Context context, ViewGroup parent, int itemType);
//可以根據類型或索引綁定數據
public abstract void onBindItemView(View itemView, int itemType, int position);
}
然后我們需要暴露一個方法,設置Adapter,設置完成之后我們就可以添加對應的布局了。
public void setAdapter(Adapter adapter) {
mAdapter = adapter;
inflateAllViews();
}
private void inflateAllViews() {
removeAllViewsInLayout();
if (mAdapter == null || mAdapter.getItemCount() == 0) {
return;
}
int displayCount = Math.min(mAdapter.getItemCount(), MAX_CHILDREN_COUNT);
//單布局處理
if (singleMode && displayCount == 1) {
View view = mAdapter.onCreateItemView(getContext(), this, -1);
addView(view);
requestLayout();
return;
}
//多布局處理
for (int i = 0; i < displayCount; i++) {
int itemType = mAdapter.getItemViewType(i);
View view = mAdapter.onCreateItemView(getContext(), this, itemType);
view.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
addView(view);
}
requestLayout();
}
需要注意的是我們再測量的布局的時候,如果沒有 Adpter 或者沒有子布局的時候,我們需要單獨處理一下九宮格ViewGroup的高度。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int notGoneChildCount = getNotGoneChildCount();
if (mAdapter == null || mAdapter.getItemCount() == 0 || notGoneChildCount == 0) {
setMeasuredDimension(widthSize, 0);
return;
}
...
}
那么如何綁定布局呢?在我們 onLayout完成之后我們就可以綁定數據了。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
...
performBind();
}
/**
* 布局完成之后綁定對應的數據到對應的ItemView
*/
private void performBind() {
if (mAdapter == null || mAdapter.getItemCount() == 0) {
return;
}
post(() -> {
for (int i = 0; i < getNotGoneChildCount(); i++) {
int itemType = mAdapter.getItemViewType(i);
View view = getChildAt(i);
mAdapter.onBindItemView(view, itemType, i);
}
});
}
具體的實現就是在 Adapter 中實現了。
例如我們創建一個最簡單的圖片九宮格適配器。
public class ImageNineGridAdapter extends AbstractNineGridLayout.Adapter {
private List<String> mDatas = new ArrayList<>();
public ImageNineGridAdapter(List<String> data) {
mDatas.addAll(data);
}
@Override
public int getItemCount() {
return mDatas.size();
}
@Override
public View onCreateItemView(Context context, ViewGroup parent, int itemType) {
return LayoutInflater.from(context).inflate(R.layout.item_img, parent, false);
}
@Override
public void onBindItemView(View itemView, int itemType, int position) {
itemView.findViewById(R.id.iv_img).setBackgroundColor(Color.RED);
}
}
在Activity中設置對應的數據適配器:
findViewById<AbstractNineGridLayout>(R.id.nine_grid).run {
setSingleModeSize(dp2px(200f), dp2px(400f))
setAdapter(ImageNineGridAdapter(imgs))
}
我們就能得到同樣的效果:
如果想九宮格內使用不同的布局,不同的索引展示不同的邏輯,都可以很方便的實現:
public class ImageNineGridAdapter extends AbstractNineGridLayout.Adapter {
private List<String> mDatas = new ArrayList<>();
public ImageNineGridAdapter(List<String> data) {
mDatas.addAll(data);
}
@Override
public int getItemViewType(int position) {
if (position == 1) {
return 10;
} else {
return 0;
}
}
@Override
public int getItemCount() {
return mDatas.size();
}
@Override
public View onCreateItemView(Context context, ViewGroup parent, int itemType) {
if (itemType == 0) {
return LayoutInflater.from(context).inflate(R.layout.item_img, parent, false);
} else {
return LayoutInflater.from(context).inflate(R.layout.item_img_icon, parent, false);
}
}
@Override
public void onBindItemView(View itemView, int itemType, int position) {
if (itemType == 0) {
itemView.findViewById(R.id.iv_img).setBackgroundColor(position == 0 ? Color.RED : Color.YELLOW);
}
}
}
效果:
到這里我們的控件就基本上能實現大部分業務需求了,接下來我會對一些屬性與配置進行抽取,并開源上傳到云端。
原文鏈接:https://juejin.cn/post/7175085561538379833
相關推薦
- 2022-09-18 Go語言實現服務端消息接收和發送_Golang
- 2021-12-13 C語言數據結構與算法之鏈表(二)_C 語言
- 2022-09-25 apache+fastcgi如何使用Content-length和Transfer-Encoding
- 2022-03-16 部署.NET6項目到IIS_實用技巧
- 2023-07-15 css 多余部分用省略號代替
- 2022-12-21 Android?RecyclerView四級緩存源碼層詳細分析_Android
- 2022-10-22 C++lambda表達式使用介紹_C 語言
- 2024-03-09 【Redis】什么是緩存雪崩,如何預防緩存雪崩?
- 最近更新
-
- 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同步修改后的遠程分支