聲明 本文轉載自:DK_BurNIng
原文連結:https://juejin.im/post/5b25bc136fb9a00e373bd0c8
已獲得作者授權,如需轉載請與作者聯繫
目的在於教會大家到底如何自定義viewgroup,自定義布局和自定義測量到底如何寫。很多網上隨便搜搜的概念和流程圖 這裡不再過多描述了,建議大家看本文之前,先看看基本的自定義viewgroup流程,心中有個大概即可。本文注重於實踐
viewgroup 的測量布局流程基本梳理稍微回顧下,基本的viewgroup繪製和布局流程中的重點:
1.view 在onMeasure()方法中進行自我測量和保存,也就是說對於view(不是viewgroup噢)來說一定在onMeasure方法中 計算出自己的尺寸並且保存下來
2.viewgroup實際上最終也是循環從上大小來調用子view的measure方法,注意子view的measure其實最終調用的是子view的onMeasure 方法。所以我們理解這個過程為: viewgroup循環遍歷調用所有子view的onmeasure方法,利用onmeasure方法計算出來的大小,來確定這些子view最終可以佔用的大小和所處的布局的位置。
3.measure方法是一個final方法,可以理解為做測量工作準備工作的,既然是final方法所以我們無法重寫它,不需要過多 關注他,因為measure最終要調用onmeasure ,這個onmeasure我們是可以重寫的。要關注這個。layout和onlayout是一樣的 關係。
4.父view調用子view的layout方法的時候會把之前measure階段確定的位置和大小都傳遞給子view。
5.對於自定義view/viewgroup來說 我們幾乎只需要關注下面三種需求:
對於已有的android自帶的view,我們只需要重寫他的onMeasure方法即可。修改一下這個尺寸即可完成需求。
對於android系統沒有的,屬於我們自定義的view,比上面那個要複雜一點,要完全重寫onMeasure方法。
第三種最複雜,需要重寫onmeasure和onlayout2個方法,來完成一個複雜viewgroup的測量和布局。
onMeasure方法的特殊說明:
如何理解父view對子view的限制?
onMeasure的兩個參數既然是父view對子view的限制,那麼這個限制的值到底是哪來的呢?
實際上,父view對子view的限制絕大多數就來自於我們開發者所設置的layout開頭的這些屬性
比方說我們給一個imageview設置了他的layout_width和layout_height 這2個屬性,那這2個屬性其實就是我們開發者 所期望的寬高屬性,但是要注意了, 設置的這2個屬性是給父view看的,實際上對於絕大多數的layout開頭的屬性這些屬性都是設置給父view看的
為什麼要給父view看?因為父view要知道這些屬性以後才知道要對子view的測量加以什麼限制?
到底是不限制(UNSPECIFIED)?還是限制個最大值(AT_MOST),讓子view不超過這個值?還是直接限制死,我讓你是多少就得是多少(EXACTLY)。
自定義一個BannerImageView 修改onMeasure方法所謂bannerImageview,就是很多電商其實都會放廣告圖,這個廣告圖的寬高比都是可變的,我們在日常開發過程中 也會經常接觸到這種需求:imageview的寬高比 在高保真中都標註出來,但是考慮到很多手機的屏幕寬度或者高度都不確定 所以我們通常都要手動來計算出這個imageview高度或者寬度,然後動態改變width或者height的值。這種方法可用但是很麻煩 這裡給出一個自定義的imageview,通過設置一個ratio的屬性即可動態的設置iv的高度。很是方便
看下效果
最後看下代碼,重要的部分都寫在注釋裡了,不再過多講了。
public class BannerImageView extends ImageView {
public BannerImageView(Context context) {
super(context);
}
public BannerImageView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BannerImageView);
ratio = typedArray.getFloat(R.styleable.BannerImageView_ratio, 1.0f);
typedArray.recycle();
}
public BannerImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mWidth = getMeasuredWidth();
int mHeight = (int) (mWidth * ratio);
}
}
首先明確一個結論:
對於完全自定義的view,完全自己寫的onMeasure方法來說,你保存的寬高必須要符合父view的限制,否則會發生bug, 保存父view對子view的限制的方法也很簡單直接調用resolveSize方法即可。
所以對於完全自定義的view onMeasure方法也不難寫了,
先算自己想要的寬高,比如你畫了個圓,那麼寬高就肯定是半徑的兩倍大小, 要是圓下面還有字, 那麼高度肯定除了半徑的兩倍還要有字體的大小。對吧。很簡單。這個純看你自定義view是啥樣的
算完自己想要的寬高以後 直接拿resolveSize 方法處理一下 即可。
最後setMeasuredDimension 保存。
範例:
public class LoadingView extends View {
int radius;
int left = 10, top = 30;
Paint mPaint = new Paint();
public LoadingView(Context context) {
super(context);
}
public LoadingView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
radius = typedArray.getInt(R.styleable.LoadingView_radius, 0);
}
public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = left + radius * 2;
int height = top + radius * 2;
width = resolveSize(width, widthMeasureSpec);
height = resolveSize(height, heightMeasureSpec); setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF oval = new RectF(left, top,
left + radius * 2, top + radius * 2);
mPaint.setColor(Color.BLUE);
canvas.drawRect(oval, mPaint);
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
canvas.drawArc(oval, -90, 360, false, mPaint);
}
}
布局文件:
<LinearLayout
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#000000"
android:orientation="horizontal">
<com.example.a16040657.customviewtest.LoadingView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/dly"
app:radius="200"></com.example.a16040657.customviewtest.LoadingView>
<com.example.a16040657.customviewtest.LoadingView
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/dly"
app:radius="200"></com.example.a16040657.customviewtest.LoadingView>
</LinearLayout>
最後效果:
自定義一個viewgroup這個其實也就是稍微複雜了一點,但是還是有跡可循的,只是稍微需要一點額外的耐心。
自定義一個viewgroup 需要注意的點如下:
一定是先重寫onMeasure確定子view的寬高和自己的寬高以後 才可以繼續寫onlayout 對這些子view進行布局噢~~
viewgroup 的onMeasure其實就是遍歷自己的view 對自己的每一個子view進行measure,絕大多數時候對子view的 measure都可以直接用 measureChild()這個方法來替代,簡化我們的寫法,如果你的viewgroup很複雜的話 無法就是自己寫一遍measureChild 而不是調用measureChild 罷了。
計算出viewgroup自己的尺寸並且保存,保存的方法還是哪個setMeasuredDimension 不要忘記了
逼不得已要重寫measureChild方法的時候,其實也不難無非就是對父view的測量和子view的測量 做一個取捨關係而已, 你看懂了基礎的measureChild方法,以後就肯定會寫自己的複雜的measureChild方法了。
下面是一個極簡的例子,一個很簡單的flowlayout的實現,沒有對margin paddding做處理,也假設了每一個tag的高度 是固定的,可以說是極為簡單了,但是麻雀雖小 五臟俱全,足夠你們好好理解自定義viewgroup的關鍵點了。
public class SimpleFlowLayout extends ViewGroup {
public SimpleFlowLayout(Context context) {
super(context);
}
public SimpleFlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SimpleFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childTop = 0;
int childLeft = 0;
int childRight = 0;
int childBottom = 0;
int usedWidth = 0;
int layoutWidth = getMeasuredWidth();
Log.v("wuyue", "layoutWidth==" + layoutWidth); for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
childLeft = 0;
usedWidth = 0;
childTop += childHeight;
childRight = childWidth;
childBottom = childTop + childHeight;
childView.layout(0, childTop, childRight, childBottom);
usedWidth = usedWidth + childWidth;
childLeft = childWidth; continue;
}
childRight = childLeft + childWidth;
childBottom = childTop + childHeight;
childView.layout(childLeft, childTop, childRight, childBottom);
childLeft = childLeft + childWidth;
usedWidth = usedWidth + childWidth;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int usedWidth = 0;
int remaining = 0;
int totalHeight = 0;
int lineHeight = 0;
View childView = getChildAt(i);
LayoutParams lp = childView.getLayoutParams();
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
remaining = widthSize - usedWidth;
usedWidth = 0;
totalHeight = totalHeight + lineHeight;
}
usedWidth = usedWidth + childView.getMeasuredWidth();
lineHeight = childView.getMeasuredHeight();
}
heightSize = totalHeight;
} setMeasuredDimension(widthSize, heightSize);
}
}
最後看下效果
推薦閱讀:
Android 常用設計模式之——單例模式
個人收藏視頻資源 | 一大波乾貨來襲
android應用開發者,你們真的了解Activity的生命周期嗎?
Android開發需要掌握的設計模式——工廠模式
輕鬆學習Java設計模式之責任鏈模式
人人都能組件化
溫馨提示:
我創建了一個技術交流群,群裡有各個行業的大佬都有,大家可以在群裡暢聊技術方面內容,以及文章推薦;如果有想加入的夥伴加我微信號【luotaosc】備註一下「加群」
另外關注公眾號,還有一些個人收藏的視頻:
回復「學習資源」 ,獲取學習視頻。
原創文章不易,如果覺得寫得好,掃碼關注一下點個讚,是我最大的動力。
備註:程序圈LT