本篇文章已授權微信公眾號guolin_blog(郭霖)獨家釋出
本文用於記錄自定義View的基礎步驟以及一些基礎的資訊,後期可能針對具體的點寫一些補充性的文章。
一 、View中關於四個建構函式引數
自定義View中View的建構函式有四個
// 主要是在java程式碼中生命一個View時所呼叫,沒有任何引數,一個空的View物件
public ChildrenView(Context context) {
super(context);
}
// 在佈局檔案中使用該自定義view的時候會呼叫到,一般會呼叫到該方法
public ChildrenView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
//如果你不需要View隨著主題變化而變化,則上面兩個建構函式就可以了
//下面兩個是與主題相關的建構函式
public ChildrenView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
//
public ChildrenView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
複製程式碼
四個引數解釋:
context:上下文
AttributeSet attrs:從xml中定義的引數
intdefStyleAttr:主題中優先順序最高的屬性
intdefStyleRes: 優先順序次之的內建於View的style(這裡就是自定義View設定樣式的地方)
- 多個地方定義屬性,優先順序排序 Xml直接定義 > xml中style引用 > defStyleAttr>defStyleRes > theme直接定義 (參考文章:www.jianshu.com/p/7389287c0…)
二、自定義屬性說明
除了基本型別的不說 講一下其它幾個吧:-
color :引用顏色
-
dimension: 引用字型大小
//定義
<attr name = "text_size" format = "dimension" />
//使用:
app:text_size = "28sp"
或者
app:text_size = "@android:dimen/app_icon_size"
複製程式碼
- enum:列舉值
//定義
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
//使用:
app:orientation = "vertical"
複製程式碼
- flags:標誌 (位或執行) 主要作用=可以多個值
//定義
<attr name="gravity">
<flag name="top" value="0x01" />
<flag name="bottom" value="0x02" />
<flag name="left" value="0x04" />
<flag name="right" value="0x08" />
<flag name="center_vertical" value="0x16" />
</attr>
// 使用
app:gravity = Top|left
複製程式碼
- fraction:百分數:
//定義:
<attr name = "transparency" format = "fraction" />
//使用:
app:transparency = "80%"
複製程式碼
- reference:參考/引用某一資源ID
//定義:
<attr name="leftIcon" format="reference" />
//使用:
app:leftIcon = "@drawable/圖片ID"
複製程式碼
- 混合型別:屬性定義時指定多種型別值
//屬性定義
<attr name = "background" format = "reference|color" />
//使用
android:background = "@drawable/圖片ID"
//或者
android:background = "#FFFFFF"
複製程式碼
三、自定義控制元件型別
-
自定義組合控制元件步驟
1. 自定義屬性
在res/values
目錄下的attrs.xml
檔案中
<resources>
<declare-styleable name="CustomView">
<attr name="leftIcon" format="reference" />
<attr name="state" format="boolean"/>
<attr name="name" format="string"/>
</declare-styleable>
</resources>
複製程式碼
2. 佈局中使用自定義屬性
在佈局中使用
<com.myapplication.view.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:leftIcon="@mipmap/ic_temp"
app:name="溫度"
app:state="false" />
複製程式碼
3. view的建構函式獲取自定義屬性
class DigitalCustomView : LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_custom, this)
var ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView)
mIcon = ta.getResourceId(R.styleable.CustomView_leftIcon, -1) //左影像
mState = ta.getBoolean(R.styleable.DigitalCustomView_state, false)
mName = ta.getString(R.styleable.CustomView_name)
ta.recycle()
initView()
}
}
複製程式碼
上面給出大致的程式碼 記得獲取context.obtainStyledAttributes(attrs, R.styleable.CustomView)
最後要呼叫ta.recycle()
利用物件池回收ta加以複用
-
繼承系統控制元件
就是繼承系統已經提供好給我們的控制元件例如TextView、LinearLayout等,分為View型別或者ViewGroup型別的兩種。主要根據業務需求進行實現,實現重寫的空間也很大 主要看需求。
比如需求 :在文字後面加個顏色背景
根據需要一般這種情況下我們是希望可以複用系統的onMeaseur
和onLayout
流程.直接複寫onDraw
方法
class Practice02BeforeOnDrawView : AppCompatTextView {
internal var paint = Paint(Paint.ANTI_ALIAS_FLAG)
internal var bounds = RectF()
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
init {
paint.color = Color.parseColor("#FFC107")
}
override fun onDraw(canvas: Canvas) {
// 把下面的繪製程式碼移到 super.onDraw() 的上面,就可以讓原主體內容蓋住你的繪製程式碼了
// (或者你也可以把 super.onDraw() 移到這段程式碼的下面)
val layout = layout
bounds.left = layout.getLineLeft(1)
bounds.right = layout.getLineRight(1)
bounds.top = layout.getLineTop(1).toFloat()
bounds.bottom = layout.getLineBottom(1).toFloat()
//繪製方形背景
canvas.drawRect(bounds, paint)
super.onDraw(canvas)
}
}
複製程式碼
這裡會涉及到畫筆
Paint()
、畫布canvas
、路徑Path
、繪畫順序等的一些知識點,後面再詳細說明
-
直接繼承View
這種就是類似TextView等,不需要去輪訓子View
只需要根據自己的需求重寫onMeasure()
、onLayout()
、onDraw()
等方法便可以,要注意點就是記得Padding
等值要記得加入運算
private int getCalculateSize(int defaultSize, int measureSpec) {
int finallSize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
// 根據模式對
switch (mode) {
case MeasureSpec.EXACTLY: {
...
break;
}
case MeasureSpec.AT_MOST: {
...
break;
}
case MeasureSpec.UNSPECIFIED: {
...
break;
}
}
return finallSize;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getCalculateSize(120, widthMeasureSpec);
int height = getCalculateSize(120, heightMeasureSpec);
setMeasuredDimension(width, height);
}
//畫一個圓
@Override
protected void onDraw(Canvas canvas) {
//呼叫父View的onDraw函式,因為View這個類幫我們實現了一些基本的而繪製功能,比如繪製背景顏色、背景圖片等
super.onDraw(canvas);
int r = getMeasuredWidth() / 2;
//圓心的橫座標為當前的View的左邊起始位置+半徑
int centerX = getLeft() + r;
//圓心的縱座標為當前的View的頂部起始位置+半徑
int centerY = getTop() + r;
Paint paint = new Paint();
paint.setColor(Color.RED);
canvas.drawCircle(centerX, centerY, r, paint);
}
複製程式碼
-
直接繼承ViewGroup
類似實現LinearLayout等,可以去看那一下LinearLayout的實現
基本的你可能要重寫onMeasure()
、onLayout()
、onDraw()
方法,這塊很多問題要處理包括輪訓childView
的測量值以及模式進行大小邏輯計算等,這個篇幅過大後期加多個文章寫詳細的
這裡寫個簡單的需求,模仿LinearLayout
的垂直佈局
class CustomViewGroup :ViewGroup{
constructor(context:Context):super(context)
constructor(context: Context,attrs:AttributeSet):super(context,attrs){
//可獲取自定義的屬性等
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//將所有的子View進行測量,這會觸發每個子View的onMeasure函式
measureChildren(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val childCount = childCount
if (childCount == 0) {
//沒有子View的情況
setMeasuredDimension(0, 0)
} else {
//如果寬高都是包裹內容
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//我們將高度設定為所有子View的高度相加,寬度設為子View中最大的寬度
val height = getTotalHeight()
val width = getMaxChildWidth()
setMeasuredDimension(width, height)
} else if (heightMode == MeasureSpec.AT_MOST) {
//如果只有高度是包裹內容
//寬度設定為ViewGroup自己的測量寬度,高度設定為所有子View的高度總和
setMeasuredDimension(widthSize, getTotalHeight())
} else if (widthMode == MeasureSpec.AT_MOST) {//如果只有寬度是包裹內容
//寬度設定為子View中寬度最大的值,高度設定為ViewGroup自己的測量值
setMeasuredDimension(getMaxChildWidth(), heightSize)
}
}
/***
* 獲取子View中寬度最大的值
*/
private fun getMaxChildWidth(): Int {
val childCount = childCount
var maxWidth = 0
for (i in 0 until childCount) {
val childView = getChildAt(i)
if (childView.measuredWidth > maxWidth)
maxWidth = childView.measuredWidth
}
return maxWidth
}
/***
* 將所有子View的高度相加
*/
private fun getTotalHeight(): Int {
val childCount = childCount
var height = 0
for (i in 0 until childCount) {
val childView = getChildAt(i)
height += childView.measuredHeight
}
return height
}
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val count = childCount
var currentHeight = t
for (i in 0 until count) {
val child = getChildAt(i)
val h = child.measuredHeight
val w = child.measuredWidth
//擺放子view
child.layout(l, currentHeight, l + w, currentHeight + h)
currentHeight += h
}
}
}
複製程式碼
主要兩點 先
measureChildren()
輪訓遍歷子View
獲取寬高,並根據測量模式邏輯計算最後所有的控制元件的所需寬高,最後setMeasuredDimension()
儲存一下 ###四、 View的繪製流程相關 最基本的三個相關函式measure()
->layout()
->draw()
五、onMeasure()相關的知識點
1. MeasureSpec
MeasureSpec
是View
的內部類,它封裝了一個View
的尺寸,在onMeasure()
當中會根據這個MeasureSpec
的值來確定View
的寬高。
MeasureSpec
的資料是int
型別,有32
位。 高兩位表示模式,後面30
位表示大小size
。則MeasureSpec = mode+size
三種模式分別為:EXACTLY
,AT_MOST
,UNSPECIFIED
EXACTLY
: (match_parent
或者精確資料值
)精確模式,對應的數值就是MeasureSpec
當中的size
AT_MOST
:(wrap_content)
最大值模式,View的尺寸有一個最大值,View
不超過MeasureSpec
當中的Size
值
UNSPECIFIED
:(一般系統使用)無限制模式,View設定多大就給他多大
//獲取測量模式
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
//獲取測量大小
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
//通過Mode和Size構造MeasureSpec
val measureSpec = MeasureSpec.makeMeasureSpec(size, mode);
複製程式碼
2. View #onMeasure()原始碼
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
複製程式碼
-
setMeasuredDimension(int measuredWidth, int measuredHeight) :用來設定View的寬高,在我們自定義View儲存寬高也會要用到。
-
getSuggestedMinimumWidth():當View沒有設定背景時,預設大小就是
mMinWidth
,這個值對應Android:minWidth
屬性,如果沒有設定時預設為0. 如果有設定背景,則預設大小為mMinWidth
和mBackground.getMinimumWidth()
當中的較大值。 -
getDefaultSize(int size, int measureSpec):用來獲取View預設的寬高,在**getDefaultSize()**中對
MeasureSpec.AT_MOST
,MeasureSpec.EXACTLY
兩個的處理是一樣的,我們自定義View
的時候 要對兩種模式進行處理。
3. ViewGroup中並沒有measure()也沒有onMeasure()
因為ViewGroup除了測量自身的寬高,還需要測量各個子View
的寬高,不同的佈局測量方式不同 (例如 LinearLayout
跟RelativeLayout
等佈局),所以直接交由繼承者根據自己的需要去複寫。但是裡面因為子View
的測量是相對固定的,所以裡面已經提供了基本的measureChildren()
以及measureChild()
來幫助我們對子View
進行測量
這個可以看一下我另一篇文章:LinearLayout # onMeasure()
LinearLayout onMeasure原始碼閱讀
六、onLayout()相關
- View.java的onLayout方法是空實現:因為子View的位置,是由其父控制元件的onLayout方法來確定的。
- onLayout(int l, int t, int r, int b)中的引數l、t、r、b都是相對於其父 控制元件的位置。
- 自身的mLeft, mTop, mRight, mBottom都是相對於父控制元件的位置。
1. Android座標系
2. 內部View座標系跟點選座標
3. 看一下View#layout(int l, int t, int r, int b)原始碼
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// ....省略其它部分
}
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
// ....省略其它部分
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
invalidateParentCaches();
}
mPrivateFlags |= drawn;
mBackgroundSizeChanged = true;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}
複製程式碼
四個引數l、t、r、b
分別代表View
的左、上、右、下四個邊界相對於其父View
的距離。
在呼叫onLayout(changed, l, t, r, b);
之前都會呼叫到setFrame()
確定View
在父容器當中的位置,賦值給mLeft
,mTop
,mRight
,mBottom
。
在ViewGroup#onLayout()
跟View#onLayout()
都是空實現,交給繼承者根據自身需求去定位
部分零散知識點:
getMeasureWidth()
與getWidth()
getMeasureWidth()
返回的是mMeasuredWidth
,而該值是在setMeasureDimension()
中的setMeasureDimensionRaw()
中設定的。因此onMeasure()
後的所有方法都能獲取到這個值。getWidth
返回的是mRight-mLeft
,這兩個值,是在layout()
中的setFrame()
中設定的.getMeasureWidthAndState
中有一句:This should be used during measurement and layout calculations only. Use {@link #getWidth()} to see how wide a view is after layout.
總結:只有在測量過程中和佈局計算時,才用
getMeasuredWidth()
。在layout之後,用getWidth()
來獲取寬度
七、draw()繪畫過程
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
複製程式碼
上面是
draw()
裡面寫的繪畫順序。
- 繪製背景。
- 如果必要的話,儲存當前
canvas
- 繪製
View
的內容- 繪製子
View
- 如果必要的話,繪畫邊緣重新儲存圖層
- 畫裝飾(例如滾動條)
1. 看一下View#draw()原始碼的實現
public void draw(Canvas canvas) {
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
}
複製程式碼
由上面可以看到 先呼叫drawBackground(canvas)
->onDraw(canvas)
->dispatchDraw(canvas)
->onDrawForeground(canvas)
越是後面繪畫的越是覆蓋在最上層。
drawBackground(canvas):畫背景,不可重寫
onDraw(canvas):畫主體
- 程式碼寫在super.onDraw()前:會被父類的onDraw覆蓋
- 程式碼寫在super.onDraw()後:不會被父類的onDraw覆蓋
dispatchDraw() :繪製子 View 的方法
-
程式碼寫在super.dispatchDraw(canvas)前:把繪製程式碼寫在 super.dispatchDraw() 的上面,這段繪製就會在 onDraw() 之後、 super.dispatchDraw() 之前發生,也就是繪製內容會出現在主體內容和子 View 之間。而這個…… 其實和重寫 onDraw() 並把繪製程式碼寫在 super.onDraw() 之後的做法,效果是一樣的。
-
程式碼寫在super.dispatchDraw(canvas)後:只要重寫 dispatchDraw(),並在 super.dispatchDraw() 的下面寫上你的繪製程式碼,這段繪製程式碼就會發生在子 View 的繪製之後,從而讓繪製內容蓋住子 View 了。
onDrawForeground(canvas):包含了滑動邊緣漸變和滑動條跟前景
一般來說,一個 View(或 ViewGroup)的繪製不會這幾項全都包含,但必然逃不出這幾項,並且一定會嚴格遵守這個順序。例如通常一個 LinearLayout 只有背景和子 View,那麼它會先繪製背景再繪製子 View;一個 ImageView 有主體,有可能會再加上一層半透明的前景作為遮罩,那麼它的前景也會在主體之後進行繪製。需要注意,前景的支援是在 Android 6.0(也就是 API 23)才加入的;之前其實也有,不過只支援 FrameLayout,而直到 6.0 才把這個支援放進了 View 類裡。
2. 注意事項
2.1 在 ViewGroup 的子類中重寫除 dispatchDraw() 以外的繪製方法時,可能需要呼叫 setWillNotDraw(false);
出於效率的考慮,ViewGroup 預設會繞過 draw() 方法,換而直接執行 dispatchDraw(),以此來簡化繪製流程。所以如果你自定義了某個 ViewGroup 的子類(比如 LinearLayout)並且需要在它的除 dispatchDraw() 以外的任何一個繪製方法內繪製內容,你可能會需要呼叫 View.setWillNotDraw(false) 這行程式碼來切換到完整的繪製流程(是「可能」而不是「必須」的原因是,有些 ViewGroup 是已經呼叫過 setWillNotDraw(false) 了的,例如 ScrollView)。
2.2 在重寫的方法有多個選擇時,優先選擇 onDraw()
一段繪製程式碼寫在不同的繪製方法中效果是一樣的,這時你可以選一個自己喜歡或者習慣的繪製方法來重寫。但有一個例外:如果繪製程式碼既可以寫在 onDraw() 裡,也可以寫在其他繪製方法裡,那麼優先寫在 onDraw() ,因為 Android 有相關的優化,可以在不需要重繪的時候自動跳過 onDraw() 的重複執行,以提升開發效率。享受這種優化的只有 onDraw() 一個方法。
八、在Activity中獲取View的寬高的幾種方式
Activity 獲取 view 的寬高, 在 onCreate , onResume 等方法中獲取到的都是0, 因為 View 的測量過程並不是和 Activity 的宣告週期同步執行的
1. view.post post 可以將一個 runnable 投遞到訊息佇列的尾部,然後等待 Looper 呼叫此 runnable 的時候, View 也已經初始化好了
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
複製程式碼
2. ViewTreeObserver 使用 addOnGlobalLayoutListener 介面, 當 view 樹的狀態發生改變或者 View 樹內部的 view 的可見性發生改變時, onGlobalLayout 都會被呼叫, 需要注意的是, onGlobalLayout 方法可能被呼叫多次, 程式碼如下:
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
複製程式碼
3. onWindowFocusChanged
這個方法的含義是 View 已經初始化完畢了, 寬高已經準備好了, 需要注意的就是這個方法可能會呼叫多次, 在 Activity onResume
和onPause
的時候都會呼叫, 也會有多次呼叫的情況
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if(hasWindowFocus){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
複製程式碼
先到這裡吧,好久沒寫文章了,這些是整理了之前學自定義View的時候跟後期一些專案走過的坑,網路很多資料參考,如果我的部分小節對你有幫助就給贊吧感激不盡~~ 這裡推薦 扔物線大神的好文Android自定義View跟視訊 通俗易懂~~~