自定義控件系列:
秒懂OnMeasure
秒懂OnLayout
讓自定義ViewGroup裏的子控件支持Margin
讓自定義ViewGroup支持Padding
自定義ViewGroup的一個綜合實踐 FlowLayout
效果
直接上源碼
關鍵是onMeasure裏caculateAtMostSize()方法和onLayout的邏輯,自定義View就是這樣小邏輯比較繞,需要仔細推敲,一定能寫得出來
其他的都是模板代碼
我認爲有了大的框架思維後,再去推敲細微部分的邏輯,會比較輕鬆,最怕大框架也沒有,細微部分的那種很繞的邏輯還不會寫,這就很難受
package com.view.custom.dosometest.view;
import android.content.Context;
import android.graphics.Point;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
/**
* 描述當前版本功能
*
* @Project: DoSomeTest
* @author: cjx
* @date: 2019-12-01 10:06 星期日
*/
public class FlowLayout extends ViewGroup {
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 首先計算一下在AtMost模式下,這個自定義view的寬高,
// 這裏把計算出來的寬高封裝在了一個Point裏,x爲寬,y爲高
Point point = caculateAtMostSize(widthMeasureSpec, heightMeasureSpec);
// 根據 默認寬高、AtMost下的寬高、MeasureSpec測量規格,計算出最終這個view的寬高
int width = measureSize(0, point.x, widthMeasureSpec);
int height = measureSize(0, point.y, heightMeasureSpec);
// 把上面計算出來的寬高作爲參數設置給setMeasuredDimension就ok了
setMeasuredDimension(width, height);
}
/**
* 通過widthMeasureSpec計算出這個View最終的寬高
*
* @param defalut 這個view的默認值,僅僅是爲了支持下UNSPECIFIED模式,但是這個模式其實用不到
* @param atMostSize AT_MOST下的尺寸
* @param measureSpec 測量規格(包含了模式+尺寸)
* @return
*/
private int measureSize(int defalut, int atMostSize, int measureSpec) {
int result = defalut;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = defalut;
break;
case MeasureSpec.AT_MOST:
//在AT_MOST模式下,系統傳來的specSize是一個父容器所能容納的最大值,你這個自定義view計算的尺寸不能大於這個值
result = Math.min(atMostSize, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
// ↓↓↓↓↓↓↓↓支持Margin的固定寫法,下面照抄就行了,至於爲什麼,可以去看源碼,但是我覺得直接記住就ok了↓↓↓↓↓↓↓↓
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
// ↑↑↑↑↑↑↑↑支持Margin的固定寫法,下面照抄就行了,至於爲什麼,可以去看源碼,但是我覺得直接記住就ok了↑↑↑↑↑↑↑↑
/**
* 計算本View在AtMost模式下的寬高
* 其他代碼都是不用動的,在這裏寫下你特有的邏輯就可以
*
* @param widthMeasureSpec
* @param heightMeasureSpec
* @return
*/
private Point caculateAtMostSize(int widthMeasureSpec, int heightMeasureSpec) {
int lineWidth = 0;//當前行的寬度
int lineHeight = 0;//當前行的高度
int totalWidth = 0;//自定義ViewGroup的總寬度
int totalHeight = 0;//自定義ViewGroup的總高度
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
// 測量一下子控件的寬高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 得到MarginLayoutParams,margin就在這裏保存着
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 獲得子控件的寬高(需要加上對應的margin,讓控件的寬高包含margin,
// 這樣才能讓自定義的viewgroup在計算自身在AtMost模式的尺寸時候考慮到這些margin)
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
// 前面說過,如果是wrap_content或match_parent,getMeasuredWidth()得到的是父容器的最大值
if (lineWidth + childWidth > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {
// 換行,總寬度更新,總高度更新
totalWidth = Math.max(lineWidth, totalWidth);
totalHeight += lineHeight;
//新的一行,初始化lineWidth,lineHeight
lineWidth = childWidth - lp.leftMargin;
lineHeight = childHeight;
} else {
// 在這行接着放子控件
lineWidth += childWidth;//行寬度增加
lineHeight = Math.max(lineHeight, childHeight);//本行高度爲本行最高的那個
}
// 上面的代碼裏只有在換行時候,才計算總寬高,
// 如果沒有換行的話,我們只是單純地累加計算了本行的高度和寬度
// 這樣會導致,沒有換行的的哪一行的寬高是沒計算到總寬高裏的,
// 那麼這樣的行其實只有一個,就是最後一行(如果只有一行的話,沒經過換行,這唯一的一行也相當於最後一行)
if (i == count - 1) {
totalWidth = Math.max(lineWidth, totalWidth);
totalHeight += lineHeight;
}
}
totalWidth += (getPaddingLeft() + getPaddingRight());
totalHeight += (getPaddingTop() + getPaddingBottom());
Log.e("ccc", totalWidth + "totalWidth");
Log.e("ccc", totalHeight + "totalHeight");
return new Point(totalWidth, totalHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int lineWidth = 0;//當前行的寬度
int lineHeight = 0;//當前行的高度
int top = getPaddingTop();
int left = getPaddingLeft();
int count = getChildCount();
for (int i = 0; i < count; i++) {
boolean isChangeLine = false;
View child = getChildAt(i);
// 得到MarginLayoutParams,margin就在這裏保存着
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 獲得子控件的寬高(需要加上對應的margin,讓控件的寬高包含margin)
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
// 前面說過,如果是wrap_content或match_parent,getMeasuredWidth()得到的是父容器的最大值
if (lineWidth + childWidth > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {
isChangeLine = true;
top += lineHeight;
left = getPaddingLeft();
//新的一行,初始化lineWidth,lineHeight
lineWidth = childWidth - lp.leftMargin;
lineHeight = childHeight;
} else {
isChangeLine = false;
lineHeight = Math.max(lineHeight, childHeight);
lineWidth += childWidth;
}
//如果換行了,那麼這個子控件的左邊距不生效(爲了保證左對齊,因爲使用的時候把所有的子控件都設置了左margin,第一個view別設置左margin)
int childLeft = isChangeLine ? left : left + lp.leftMargin;
int childTop = top + lp.topMargin;
int childRight = childLeft + child.getMeasuredWidth();
int childBottom = childTop + child.getMeasuredHeight();
child.layout(childLeft, childTop, childRight, childBottom);
// 佈局了一個子控件後,left往後移動一個子控件的寬度
left += childWidth;
}
}
}
參考:
《Android自定義開發入門與實踐》感謝大神的著作,對其中的實例做了部分修改