<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Android系統內建的View不滿足我們的業務需求
通常情況下:
在values檔案中建立attr檔案,然後使用< declare-styleable >為自定義View新增屬性,在xml中設定相應的屬性值,然後再自定義View的構造方法中獲取屬性值(AtrributeSet),將獲取到的屬性值應用到View中去
FrameLayout rootView = findViewById(android.R.id.content); RelativeLayout relativeLayout = (LinearLayout) rootView.getChildAt(0);//獲取Activity的根部局
注意:無論是measure過程還是layout過程還是draw過程,永遠都是從View樹的根節點往下樹形遞迴的開始測量或者計算。
注意:
1、當view沒有發生動畫偏移的時候,getX()和getLeft()相等,如果由translation的時候,getX() = getLeft() + getTranslationX()
2、getLeft()等獲取的值是相對父容器而言的
View樹的繪製是交給ViewRootImpl去負責的,入口在 ViewRootImpl.setView() --> requestLayout()方法中進行的,最終呼叫到了一個叫做performTraversals()方法裡面,這裡面就開始了真正的繪製流程工作,平時寫的onDraw、onMeasure、onLayout也都在這裡邊。
1、系統為什麼需要measure過程
因為我們在寫佈局的時候要針對不同的機型做適配,不能寫死view的高度和寬度,經常使用wrap_content這種形式,為了適配這種自適應佈局的機制,所以系統需要進行measure測量
2、measure過程做了什麼事情
確定每個view在螢幕上顯示的時候所需要的真實的寬度和高度
3、ViewGroup如何向子View傳遞限制資訊
通過MeasureSpec,從名字上來看叫做測量規格,它封裝了父容器對子View的佈局上的限制,內部提供了寬高的資訊(SpecMode、SpecSize),SpecSize是指在某種情況下SpecMode下的參考尺寸。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = 0;//最終確定的寬度 int height = 0;//最終確定的高度 //1、首先測量自身 super.onMeasure(widthMeasureSpec, heightMeasureSpec); //2、為每個子view計算測量的限制資訊Mode/Size int widthMeasureSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthMeasureSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightMeasureSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightMeasureSpecSize = MeasureSpec.getSize(heightMeasureSpec); //3、測量子View;把上一步確定的限制資訊,傳遞給每一個子View,然後子View開始measure自己的尺寸 int childCount = getChildCount(); for(int i=0;i<childCount;i++){ View child = getChildAt(i); measureChild(child,widthMeasureSpec,heightMeasureSpec);//這個方法就是確定子view的測量大小 } //4、根據子View的測量尺寸以及自身的SpecMode計算自己的尺寸 switch (widthMeasureSpecMode) { case MeasureSpec.EXACTLY://如果是確定值,則使用確定值 width = widthMeasureSpecSize; case MeasureSpec.AT_MOST://如果是根據內容定的大小 case MeasureSpec.UNSPECIFIED://一般可以不用單獨處理 for(int i=0;i<childCount;i++){ View child = getChildAt(i); int childWidth = child.getMeasuredWidth();//這一步只有當measureChild方法執行完之後才能拿到 width = Math.max(childWidth,width); } default:break; } switch (heightMeasureSpecMode) { case MeasureSpec.EXACTLY://如果是確定值,則使用確定值 height = heightMeasureSpecSize; case MeasureSpec.AT_MOST://如果是根據內容定的大小 case MeasureSpec.UNSPECIFIED: for(int i=0;i<childCount;i++){ View child = getChildAt(i); int childHeight = child.getMeasuredHeight();//這一步只有當measureChild方法執行完之後才能拿到 height+=childHeight; } default:break; } //儲存自身測量後的寬和高 setMeasuredDimension(width,height); }
要明確一點,重寫自定義ViewGroup的onMeasure方法是為了確定這個View的真正的寬度和高度,很明顯這與它的子View脫離不了干係。
onMeasure()方法中的兩個引數,是這個自定義ViewGroup的父View給出的參考值,具體怎麼給出的呢,可以參考ViewGroup的measureChild()方法,這個方法我們在重寫onMeasure時也用到了,看這個方法的第一個引數好像是View,看起來好像跟我們自定義ViewGroup沒啥關係,但別忘了,ViewGroup也是一個View,所以,我們的自定義ViewGroup的onMeasure()方法中的兩個引數就是由下面的方法產生的,具體來講就是下面的 childWidthMeasureSpec和childHeightMeasureSpec。
總結一句話就是:子View(包括子ViewGroup)的WidthMeasureSpec和HeightMeasureSpec的確定是由子View本身的LayoutParams以及父View(包括父ViewGroup)的WidthMeasureSpec和HeightMeasureSpec確定的。這一段邏輯是ViewGroup#getChildMeasureSpec()。有個表格
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引數,下面就好辦了,那麼直接呼叫view.measure(childWidthMeasureSpec, childHeightMeasureSpec)完成自身的測量。
關鍵來了, 在View的measure方法裡面會呼叫onMeasure方法,如果當前View是一個普通的View,則直接執行這裡的方法,完成普通View的測量過程,但是, 如果當前View是一個ViewGroup就會呼叫自身重寫好的onMeasure方法,也就是我們重寫的方法。
對於自定義ViewGroup重寫的onMeasure方法需要結合子View的寬度和高度,以及自身的LayOutParams的模式來確定最終的寬度和高度
那麼對於普通View是否就不需要重寫onMeasure了呢,原始碼不是已經寫好了嗎?
看一下程式碼:
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; }
發現,無論是精確模式,還是wrap_content模式最後值都是之前由子View本身的LayoutParams以及父View(包括父ViewGroup)的WidthMeasureSpec和HeightMeasureSpec確定的measureSpecSize值大小,通過查表可知,如果當普通的自定義View的寬度或者高度被設定成了為了wrap_content的話,它的效果跟mathch_parent效果一樣,所以普通的自定義View需要對wrap_content這一情況進行完善,參考TextView
onLayout的中後四個引數,指的是,當前自定義ViewGroup在它的父佈局中的上下左右座標,通過這個座標可以得到當前自定義ViewGroup的測量寬度和高度,不過一般也不需要用到這個四個引數,因為可以直接通過 getMeasuredWidth() 方法得到
所以onLayout的核心目的就是計算每一個控制元件的left、top、right、bottom座標,然後通過 child.layout()方法set進去就行了,所以onLayout主要工作就在於如何確定這四個引數。
追蹤child.layout()方法進去看看:
流佈局:
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;//記錄每一行的高度,取決於每一行中最高的那個元件 int resH = 0;//最終的高度 int resW = 0;//最終的寬度 //1、首先測量自身 super.onMeasure(widthMeasureSpec, heightMeasureSpec); //2、為每個子view計算測量的限制資訊Mode/Size int widthMeasureSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthMeasureSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightMeasureSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightMeasureSpecSize = MeasureSpec.getSize(heightMeasureSpec); //3、測量每個子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) {//當前行的的寬度已經加上當前view的寬度已經大於建議值寬度了 //需要換行 resW = Math.max(resW, lineWidth); resH += lineHeight; //重新賦值 lineWidth = childMeasuredWidth; lineHeight = childMeasuredHeight; } else {//不需要換行則累加 lineWidth += childMeasuredWidth; lineHeight = Math.max(lineHeight,childMeasuredHeight);//取最高的那個 } if (i == childCount - 1) {//別忘了單獨處理最後一行的最後一個元素的情況 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;//累加當前行的行寬 int lineHeight = 0;//累加當前行的行高 int top = 0, left = 0;//當前控制元件的left座標和top座標 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; //根據是否要換行,來計算當前控制元件的top座標和Left座標,是否換行是需要考慮margin的 if (childMeasuredWidth + lineWidth > getMeasuredWidth()) { top += lineHeight; left = 0; lineHeight = childMeasuredHeight; lineWidth = childMeasuredWidth; } else { lineWidth += childMeasuredWidth; lineHeight = Math.max(lineHeight, childMeasuredHeight); } //在已知left和top情況下計算當前View的上下左右座標,在真正給當前View定位置時候需要考慮margin的 int lc = left + lp.leftMargin; int tc = top + lp.topMargin; int rc = lc + child.getMeasuredWidth();//注意在layout的時候沒有算上margin int bc = tc + child.getMeasuredHeight(); child.layout(lc, tc, rc, bc); left += childMeasuredWidth;//下一起點算上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); } }
注意:上述程式碼實際上可能不符合業務預期,在於 measureChild(child, widthMeasureSpec, heightMeasureSpec);這一句,我們直接呼叫系統的方法去獲得子View的MeasureSpec,但實際上獲取到的值不一定是我們想要的,即下圖的值不一定符合我們的業務,所以在真正測量子View的時候,需要針對子View的match_parent情況或者wrap_content情況進行特殊處理
一般情況下是針對子View是match_parent的情況做處理,比如我們自定義的FlowLayout,如果FlowLayout是match_parent、子View是match_parent的話,就需要特殊處理了,根據模式表子View所佔的空間將充滿整個父View的剩餘空間,這一點符合程式碼邏輯但是可能不會符合業務需求
1、getMeasuredWidth和getWidth的區別
getMeasuredWidth是在measure的過程結束後就可以獲得到的View測量寬度值;而getWidth是在layout過程結束後通過mRight-mLeft得到的;一般情況下,二者是相等的,但有可能不相等,getWidth取決於layout過程中怎麼算的四點座標值。
2、onDraw、onMeasure以及onLayout會多次呼叫,所以這裡面儘量不要頻繁的new 物件
3、呼叫view.invalidate()以及requestLayout()有什麼區別:
這個方法是用來重新整理整個檢視的,當檢視的內容,可見性發生變化,onDraw(Canvas canvas)方法會被呼叫。 呼叫invalidate()方法不會導致measure和layout方法被呼叫。
requestLayout()是在view的佈局發生變化時呼叫,佈局的變化包含位置,大小。重新觸發measure,layout,draw
注意:
到此這篇關於Android自定義View原理(實戰)的文章就介紹到這了,更多相關Android自定義View內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45