Android自定義View原理(實(shí)戰(zhàn))
1、為什么需要自定義View
Android系統(tǒng)內(nèi)置的View不滿足我們的業(yè)務(wù)需求
2、自定義View的基本方法
- onMeasure:決定著View的大小
- onLayout:決定View在ViewGroup中的位置
- onDraw:決定繪制什么樣的View
通常情況下:
- 自定義View只需要重寫onMeasure和onDraw這兩個(gè)方法
- 自定義ViewGroup只需要重寫onMeasure和onLayout這兩個(gè)方法
3、自定義View的屬性如何操作
在values文件中創(chuàng)建attr文件,然后使用< declare-styleable >為自定義View添加屬性,在xml中設(shè)置相應(yīng)的屬性值,然后再自定義View的構(gòu)造方法中獲取屬性值(AtrributeSet),將獲取到的屬性值應(yīng)用到View中去
4、View的視圖結(jié)構(gòu)
- 1、每一個(gè)Activity都有一個(gè)Window,Window用于顯示我們的界面,Activity負(fù)責(zé)管理Window
- 2、每個(gè)Window都有一個(gè)根View->DecorView,Window本身不能顯示界面,需要依托于View
- 3、DecorView是一個(gè)FrameLayout,它主要由兩部分組成,一部分是ActionBar,一部分是一個(gè)id為android.R.content的FrameLayout,我們寫好的Activity的根部局的View就是被添加到這里去了,通過setContentView()方法
- 4、在往下就是一個(gè)樹形結(jié)構(gòu)的視圖結(jié)構(gòu),ViewGroup中嵌套ViewGroup和View
FrameLayout rootView = findViewById(android.R.id.content); RelativeLayout relativeLayout = (LinearLayout) rootView.getChildAt(0);//獲取Activity的根部局
注意:無論是measure過程還是layout過程還是draw過程,永遠(yuǎn)都是從View樹的根節(jié)點(diǎn)往下樹形遞歸的開始測(cè)量或者計(jì)算。
5、View的坐標(biāo)系
注意:
1、當(dāng)view沒有發(fā)生動(dòng)畫偏移的時(shí)候,getX()和getLeft()相等,如果由translation的時(shí)候,getX() = getLeft() + getTranslationX()
2、getLeft()等獲取的值是相對(duì)父容器而言的
6、View樹的繪制流程
View樹的繪制是交給ViewRootImpl去負(fù)責(zé)的,入口在 ViewRootImpl.setView() --> requestLayout()方法中進(jìn)行的,最終調(diào)用到了一個(gè)叫做performTraversals()方法里面,這里面就開始了真正的繪制流程工作,平時(shí)寫的onDraw、onMeasure、onLayout也都在這里邊。
6.1 measure過程
1、系統(tǒng)為什么需要measure過程
因?yàn)槲覀冊(cè)趯懖季值臅r(shí)候要針對(duì)不同的機(jī)型做適配,不能寫死view的高度和寬度,經(jīng)常使用wrap_content這種形式,為了適配這種自適應(yīng)布局的機(jī)制,所以系統(tǒng)需要進(jìn)行measure測(cè)量
2、measure過程做了什么事情
確定每個(gè)view在屏幕上顯示的時(shí)候所需要的真實(shí)的寬度和高度
3、ViewGroup如何向子View傳遞限制信息
通過MeasureSpec,從名字上來看叫做測(cè)量規(guī)格,它封裝了父容器對(duì)子View的布局上的限制,內(nèi)部提供了寬高的信息(SpecMode、SpecSize),SpecSize是指在某種情況下SpecMode下的參考尺寸。
6.2 分析自定義ViewGroup的onMeasure過程
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = 0;//最終確定的寬度 int height = 0;//最終確定的高度 //1、首先測(cè)量自身 super.onMeasure(widthMeasureSpec, heightMeasureSpec); //2、為每個(gè)子view計(jì)算測(cè)量的限制信息Mode/Size int widthMeasureSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthMeasureSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightMeasureSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightMeasureSpecSize = MeasureSpec.getSize(heightMeasureSpec); //3、測(cè)量子View;把上一步確定的限制信息,傳遞給每一個(gè)子View,然后子View開始measure自己的尺寸 int childCount = getChildCount(); for(int i=0;i<childCount;i++){ View child = getChildAt(i); measureChild(child,widthMeasureSpec,heightMeasureSpec);//這個(gè)方法就是確定子view的測(cè)量大小 } //4、根據(jù)子View的測(cè)量尺寸以及自身的SpecMode計(jì)算自己的尺寸 switch (widthMeasureSpecMode) { case MeasureSpec.EXACTLY://如果是確定值,則使用確定值 width = widthMeasureSpecSize; case MeasureSpec.AT_MOST://如果是根據(jù)內(nèi)容定的大小 case MeasureSpec.UNSPECIFIED://一般可以不用單獨(dú)處理 for(int i=0;i<childCount;i++){ View child = getChildAt(i); int childWidth = child.getMeasuredWidth();//這一步只有當(dāng)measureChild方法執(zhí)行完之后才能拿到 width = Math.max(childWidth,width); } default:break; } switch (heightMeasureSpecMode) { case MeasureSpec.EXACTLY://如果是確定值,則使用確定值 height = heightMeasureSpecSize; case MeasureSpec.AT_MOST://如果是根據(jù)內(nèi)容定的大小 case MeasureSpec.UNSPECIFIED: for(int i=0;i<childCount;i++){ View child = getChildAt(i); int childHeight = child.getMeasuredHeight();//這一步只有當(dāng)measureChild方法執(zhí)行完之后才能拿到 height+=childHeight; } default:break; } //保存自身測(cè)量后的寬和高 setMeasuredDimension(width,height); }
要明確一點(diǎn),重寫自定義ViewGroup的onMeasure方法是為了確定這個(gè)View的真正的寬度和高度,很明顯這與它的子View脫離不了干系。
onMeasure()方法中的兩個(gè)參數(shù),是這個(gè)自定義ViewGroup的父View給出的參考值,具體怎么給出的呢,可以參考ViewGroup的measureChild()方法,這個(gè)方法我們?cè)谥貙憃nMeasure時(shí)也用到了,看這個(gè)方法的第一個(gè)參數(shù)好像是View,看起來好像跟我們自定義ViewGroup沒啥關(guān)系,但別忘了,ViewGroup也是一個(gè)View,所以,我們的自定義ViewGroup的onMeasure()方法中的兩個(gè)參數(shù)就是由下面的方法產(chǎn)生的,具體來講就是下面的 childWidthMeasureSpec和childHeightMeasureSpec。
總結(jié)一句話就是:子View(包括子ViewGroup)的WidthMeasureSpec和HeightMeasureSpec的確定是由子View本身的LayoutParams以及父View(包括父ViewGroup)的WidthMeasureSpec和HeightMeasureSpec確定的。這一段邏輯是ViewGroup#getChildMeasureSpec()。有個(gè)表格
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
知道了自身的MeasureSpec參數(shù),下面就好辦了,那么直接調(diào)用view.measure(childWidthMeasureSpec, childHeightMeasureSpec)完成自身的測(cè)量。
關(guān)鍵來了, 在View的measure方法里面會(huì)調(diào)用onMeasure方法,如果當(dāng)前View是一個(gè)普通的View,則直接執(zhí)行這里的方法,完成普通View的測(cè)量過程,但是, 如果當(dāng)前View是一個(gè)ViewGroup就會(huì)調(diào)用自身重寫好的onMeasure方法,也就是我們重寫的方法。
對(duì)于自定義ViewGroup重寫的onMeasure方法需要結(jié)合子View的寬度和高度,以及自身的LayOutParams的模式來確定最終的寬度和高度
那么對(duì)于普通View是否就不需要重寫onMeasure了呢,源碼不是已經(jīng)寫好了嗎?
看一下代碼:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
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; }
發(fā)現(xiàn),無論是精確模式,還是wrap_content模式最后值都是之前由子View本身的LayoutParams以及父View(包括父ViewGroup)的WidthMeasureSpec和HeightMeasureSpec確定的measureSpecSize值大小,通過查表可知,如果當(dāng)普通的自定義View的寬度或者高度被設(shè)置成了為了wrap_content的話,它的效果跟mathch_parent效果一樣,所以普通的自定義View需要對(duì)wrap_content這一情況進(jìn)行完善,參考TextView
6.3 分析自定義ViewGroup的onLayout過程
onLayout的中后四個(gè)參數(shù),指的是,當(dāng)前自定義ViewGroup在它的父布局中的上下左右坐標(biāo),通過這個(gè)坐標(biāo)可以得到當(dāng)前自定義ViewGroup的測(cè)量寬度和高度,不過一般也不需要用到這個(gè)四個(gè)參數(shù),因?yàn)榭梢灾苯油ㄟ^ getMeasuredWidth() 方法得到
所以onLayout的核心目的就是計(jì)算每一個(gè)控件的left、top、right、bottom坐標(biāo),然后通過 child.layout()方法set進(jìn)去就行了,所以onLayout主要工作就在于如何確定這四個(gè)參數(shù)。
追蹤child.layout()方法進(jìn)去看看:
6.4 自定義Layout實(shí)戰(zhàn)
流布局:
package com.example.materialdesign.selfView; import android.content.Context; import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import androidx.annotation.RequiresApi; 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); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int lineWidth = 0;//記錄每一行的寬度,最終的寬度是由所有行中的最大值 int lineHeight = 0;//記錄每一行的高度,取決于每一行中最高的那個(gè)組件 int resH = 0;//最終的高度 int resW = 0;//最終的寬度 //1、首先測(cè)量自身 super.onMeasure(widthMeasureSpec, heightMeasureSpec); //2、為每個(gè)子view計(jì)算測(cè)量的限制信息Mode/Size int widthMeasureSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthMeasureSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightMeasureSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightMeasureSpecSize = MeasureSpec.getSize(heightMeasureSpec); //3、測(cè)量每個(gè)子view的寬度和高度 int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childMeasuredWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childMeasuredHeight = child.getMeasuredHeight() + lp.bottomMargin + lp.topMargin; if (lineWidth + childMeasuredWidth > widthMeasureSpecSize) {//當(dāng)前行的的寬度已經(jīng)加上當(dāng)前view的寬度已經(jīng)大于建議值寬度了 //需要換行 resW = Math.max(resW, lineWidth); resH += lineHeight; //重新賦值 lineWidth = childMeasuredWidth; lineHeight = childMeasuredHeight; } else {//不需要換行則累加 lineWidth += childMeasuredWidth; lineHeight = Math.max(lineHeight,childMeasuredHeight);//取最高的那個(gè) } if (i == childCount - 1) {//別忘了單獨(dú)處理最后一行的最后一個(gè)元素的情況 resH += lineHeight; resW = Math.max(resW, lineWidth); } } setMeasuredDimension((widthMeasureSpecMode==MeasureSpec.EXACTLY)?widthMeasureSpecSize:resW, (heightMeasureSpecMode==MeasureSpec.EXACTLY)?heightMeasureSpecSize:resH); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); int lineWidth = 0;//累加當(dāng)前行的行寬 int lineHeight = 0;//累加當(dāng)前行的行高 int top = 0, left = 0;//當(dāng)前控件的left坐標(biāo)和top坐標(biāo) for (int i = 0; i < count; i++) { View child = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childMeasuredWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childMeasuredHeight = child.getMeasuredHeight() + lp.bottomMargin + lp.topMargin; //根據(jù)是否要換行,來計(jì)算當(dāng)前控件的top坐標(biāo)和Left坐標(biāo),是否換行是需要考慮margin的 if (childMeasuredWidth + lineWidth > getMeasuredWidth()) { top += lineHeight; left = 0; lineHeight = childMeasuredHeight; lineWidth = childMeasuredWidth; } else { lineWidth += childMeasuredWidth; lineHeight = Math.max(lineHeight, childMeasuredHeight); } //在已知left和top情況下計(jì)算當(dāng)前View的上下左右坐標(biāo),在真正給當(dāng)前View定位置時(shí)候需要考慮margin的 int lc = left + lp.leftMargin; int tc = top + lp.topMargin; int rc = lc + child.getMeasuredWidth();//注意在layout的時(shí)候沒有算上margin int bc = tc + child.getMeasuredHeight(); child.layout(lc, tc, rc, bc); left += childMeasuredWidth;//下一起點(diǎn)算上margin } } @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); } }
注意:上述代碼實(shí)際上可能不符合業(yè)務(wù)預(yù)期,在于 measureChild(child, widthMeasureSpec, heightMeasureSpec);這一句,我們直接調(diào)用系統(tǒng)的方法去獲得子View的MeasureSpec,但實(shí)際上獲取到的值不一定是我們想要的,即下圖的值不一定符合我們的業(yè)務(wù),所以在真正測(cè)量子View的時(shí)候,需要針對(duì)子View的match_parent情況或者wrap_content情況進(jìn)行特殊處理
一般情況下是針對(duì)子View是match_parent的情況做處理,比如我們自定義的FlowLayout,如果FlowLayout是match_parent、子View是match_parent的話,就需要特殊處理了,根據(jù)模式表子View所占的空間將充滿整個(gè)父View的剩余空間,這一點(diǎn)符合代碼邏輯但是可能不會(huì)符合業(yè)務(wù)需求
6.5 細(xì)節(jié)
1、getMeasuredWidth和getWidth的區(qū)別
getMeasuredWidth是在measure的過程結(jié)束后就可以獲得到的View測(cè)量寬度值;而getWidth是在layout過程結(jié)束后通過mRight-mLeft得到的;一般情況下,二者是相等的,但有可能不相等,getWidth取決于layout過程中怎么算的四點(diǎn)坐標(biāo)值。
2、onDraw、onMeasure以及onLayout會(huì)多次調(diào)用,所以這里面盡量不要頻繁的new 對(duì)象
3、調(diào)用view.invalidate()以及requestLayout()有什么區(qū)別:
這個(gè)方法是用來刷新整個(gè)視圖的,當(dāng)視圖的內(nèi)容,可見性發(fā)生變化,onDraw(Canvas canvas)方法會(huì)被調(diào)用。 調(diào)用invalidate()方法不會(huì)導(dǎo)致measure和layout方法被調(diào)用。
requestLayout()是在view的布局發(fā)生變化時(shí)調(diào)用,布局的變化包含位置,大小。重新觸發(fā)measure,layout,draw
注意:
- 1.這個(gè)方法不能在正在布局的時(shí)候調(diào)用
- 2.調(diào)用這個(gè)方法,會(huì)導(dǎo)致布局重繪,調(diào)用measure,layout,draw的過程。
到此這篇關(guān)于Android自定義View原理(實(shí)戰(zhàn))的文章就介紹到這了,更多相關(guān)Android自定義View內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android ListView自定義Adapter實(shí)現(xiàn)仿QQ界面
這篇文章主要為大家詳細(xì)介紹了ListView自定義Adapter實(shí)現(xiàn)仿QQ界面,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10android傳送照片到FTP服務(wù)器的實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了android傳送照片到FTP服務(wù)器的實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06Android使用BroadcastReceiver監(jiān)聽網(wǎng)絡(luò)連接狀態(tài)的改變
這篇文章主要為大家詳細(xì)介紹了Android使用BroadcastReceiver監(jiān)聽網(wǎng)絡(luò)連接狀態(tài)的改變,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05Android 中clipToPadding 和 clipChildren區(qū)別和作用
這篇文章主要介紹了Android 中clipToPadding 和 clipChildren區(qū)別和作用的相關(guān)資料,需要的朋友可以參考下2017-06-06Android中NestedScrolling滑動(dòng)機(jī)制詳解
本篇文章主要介紹了Android中NestedScrolling滑動(dòng)機(jī)制詳解,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-02-02Android實(shí)現(xiàn)關(guān)機(jī)后數(shù)據(jù)不會(huì)丟失問題
這篇文章主要介紹了Android實(shí)現(xiàn)關(guān)機(jī)后數(shù)據(jù)不會(huì)丟失問題,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10利用Kotlin如何實(shí)現(xiàn)Android開發(fā)中的Parcelable詳解
這篇文章主要給大家介紹了關(guān)于利用Kotlin如何實(shí)現(xiàn)Android開發(fā)中的Parcelable的相關(guān)資料,并且給大家介紹了關(guān)于Kotlin使用parcelable出現(xiàn):BadParcelableException: Parcelable protocol requires a Parcelable.Creator...問題的解決方法,需要的朋友可以參考下。2017-12-12Android簡(jiǎn)單實(shí)現(xiàn)天氣預(yù)報(bào)App
這篇文章主要為大家詳細(xì)介紹了Android簡(jiǎn)單實(shí)現(xiàn)天氣預(yù)報(bào)App,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-09-09