Andoid自定义View的OnMeasure详解和自定义属性
一,OnMeasure詳解
Android開發中偶爾會用到自定義View,一般情況下,自定義View都需要繼承View類的onMeasure方法,那么,為什么要繼承onMeasure()函數呢?什么情況下要繼承onMeasure()?系統默認的onMeasure()函數行為是怎樣的 ?本文就探究探究這些問題。這篇文章獲取可以加深多自定義view的理解。
首先,我們寫一個自定義View,直接調用系統默認的onMeasure函數,看看會是怎樣的現象:
package com.tuke.customviewonmeasure;import android.content.Context; import android.util.AttributeSet; import android.view.View;/*** 作者:tuke on 2017/6/15 11:05* 郵箱:2297535832@qq.com*/ public class CustomViewOnMeasure extends View {public CustomViewOnMeasure(Context context) {super(context);}public CustomViewOnMeasure(Context context, AttributeSet attrs) {super(context, attrs);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);} }
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><com.tuke.customviewonmeasure.CustomViewOnMeasureandroid:layout_width="match_parent"android:layout_height="match_parent"android:layout_margin="50dp"android:background="#000000"/> </LinearLayout> 1. 父控件使用match_parent,CustomView使用match_parent。
這里加了10dp的margin并且把View的背景設置為了黑色,是為了方便辨別我們的CustomView,效果如下:
這里的margin 屬性為什么也有效呢?是因為此自定義View的父控件是LinearLayout,在LinearLayout的源碼中的靜態內部類LinearLayout.LayoutParams->ViewGroup.MarginLayoutParams->ViewGroup.LayoutParams,在ViewGroup.MarginLayoutParams中系統自定義了margin屬性,所以在其他容器(LinearLayout,RelativeLayout)中margin屬性都有效,因為
LinearLayout.LayoutParams繼承了ViewGroup.MarginLayoutParams,并且增加了weight,gravity兩個屬性,而RelativeLayout.LayoutParams也繼承了ViewGroup.MarginLayoutParams,并且增加了對齊屬性,如left_to,right_to,above,below,align_baseline,align_left,align_top,align_right等等
所以在下一篇自定義ViewGroup中,如果要使得margin屬性有效,必須創建一個靜態內部類并繼承ViewGroup.MarginLayoutParams,這個下次再講。
2. 父控件使用match_parent,CustomView使用wrap_content
把layout文件中,CustomViewOnMeasure的layout_width/layout_height替換為wrap_content,你會發現,結果依然是充滿父控件。
3. 父控件使用match_parent,CustomView使用固定的值
把layout文件中,CustomView的layout_width/layout_height替換為200dp,你會發現,CustomView的顯示結果為200dpx200dp,如圖所示:
5 結論
如果當父控件設置為match_parent時,自定義的CustomView采用默認的onMeasure函數,行為如下:
(1) CustomView設置為 match_parent 或者 wrap_content 沒有任何區別,其顯示大小由父控件決定,它會填充滿整個父控件的空間。
(2) CustomView設置為固定的值,則其顯示大小為該設定的值。
如果你的自定義控件的大小計算就是跟系統默認的行為一致的話,那么你就不需要重寫onMeasure函數了。
就是說系統OnMeasure時,父控件設置為match_parent,自定義View是match_parent 和wrap_content 是一樣的,充滿整個父控件,自定義View設置固定值,就是該設定的值。這是為什么,看源碼。
那么我們就要看看這個默認的系統onMeasure函數是個什么樣子了。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()); } /*** 獲取默認的寬高值*/ 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; }
? ? ? ?
源碼可以看出,onMeasure方法調用了setMeasuredDimension(int measuredWidth, int measuredHeight)方法,而傳入的參數已經是測量過的默認寬和高的值了;我們看看getDefaultSize 方法是怎么計算測量寬高的。根據父控件給予的約束,發現AT_MOST (相當于wrap_content )和EXACTLY (相當于match_parent )兩種情況返回的測量寬高都是specSize,而這個specSize正是我們上面說的父控件剩余的寬高,所以默認onMeasure方法中wrap_content 和match_parent 的效果是一樣的,都是填充剩余的空間。
onMeasure方法的作用時測量空間的大小,什么時候需要測量控件的大小呢?創建一個View(執行構造方法)的時候不需要測量控件的大小,只有將這個view放入一個容器(父控件)中的時候才需要測量,而這個測量方法就是父控件喚起調用的。當控件的父控件要放置該控件的時候,父控件會調用子控件的onMeasure方法詢問子控件:“你有多大的尺寸,我要給你多大的地方才能容納你?”,然后傳入兩個參數(widthMeasureSpec和heightMeasureSpec),這兩個參數就是父控件告訴子控件可獲得的空間以及關于這個空間的約束條件(好比我在思考需要多大的碗盛菜的時候我要看一下碗柜里最大的碗有多大,菜的分量不能超過這個容積,這就是碗對菜的約束),子控件拿著這些條件就能正確的測量自身的寬高了。
那么現在就需要認識widthMeasureSpec和heightMeasureSpec參數,MeasureSpec類,自定義View的三種測量模式。
widthMeasureSpec和heightMeasureSpec參數其實就是此自定義View的父控件(此處是LinearLayout)根據layout文件中自定義控件的layout_height和layout_width設置的值測量的寬和高,并加入模式通過MeasureSpec類計算得出的widthMeasureSpec和heightMeasureSpec參數,傳入我們重寫的OnMeasure中,其實就是父控件已經知道了此自定義view的寬和高,傳過來供開發者參考修改。
widthMeasureSpec和heightMeasureSpec 并不是真正的寬高。是一個32位int值。高2位 代表SpecMode,低30為代表SpecSize 。
了解了這兩個參數的來源,還要知道這兩個值的作用。我們只取heightMeasureSpec作說明。這個值由高32位和低16位組成,高32位保存的值叫specMode,可以通過如代碼中所示的MeasureSpec.getMode()獲取;低16位為specSize,同樣可以由MeasureSpec.getSize()獲取。那么specMode和specSize的作用有是什么呢?要想知道這一點,我們需要知道代碼中的最后一行,所有的View的onMeasure()的最后一行都會調用setMeasureDimension()函數的作用——這個函數調用中傳進去的值是View最終的視圖大小。也就是說onMeasure()中之前所作的所有工作都是為了最后這一句話服務的。
MeasureSpec類
上面說到MeasureSpec約束是由父控件傳遞給子控件的,這個類里面到底封裝了什么東西?我們看一看源碼:
public static class MeasureSpec {private static final int MODE_SHIFT = 30;private static final int MODE_MASK = 0x3 << MODE_SHIFT;/*** 父控件不強加任何約束給子控件,它可以是它想要任何大小*/public static final int UNSPECIFIED = 0 << MODE_SHIFT;/*** 父控件已為子控件確定了一個確切的大小,孩子將被給予這些界限,不管子控件自己希望的是多大*/public static final int EXACTLY = 1 << MODE_SHIFT;/*** 父控件會給子控件盡可能大的尺寸*/public static final int AT_MOST = 2 << MODE_SHIFT;/*** 根據所提供的大小和模式創建一個測量規范*/public static int makeMeasureSpec(int size, int mode) {if (sUseBrokenMakeMeasureSpec) {return size + mode;} else {return (size & ~MODE_MASK) | (mode & MODE_MASK);}}/*** 從所提供的測量規范中提取模式*/public static int getMode(int measureSpec) {return (measureSpec & MODE_MASK);}/*** 從所提供的測量規范中提取尺寸*/public static int getSize(int measureSpec) {return (measureSpec & ~MODE_MASK);}... } 從源碼中我們知道,MeasureSpec其實就是尺寸和模式通過各種位運算計算出的一個整型值,它提供了三種模式。
1,EXACTLY:表示設置了自定義的view精確的值,一般當自定義的view設置其寬、高為精確值、match_parent時,其父控件為其計算出的widthMeasureSpec和heightMeasureSpec參數中的Mode為EXACTLY,specSize是父控件測量所得的值,就是精確值或者match_parent,父控件都能精確測量;
2,AT_MOST:表示自定義的view被限制在一個最大值內,一般當自定義的view設置其寬、高為wrap_content時,其父控件為其計算出的widthMeasureSpec和heightMeasureSpec參數中的Mode為為AT_MOST,specSize不確定,需要在重寫OnMeasure中為其設置最大值;
3,UNSPECIFIED:自定義view想要多大就多大,一般出現在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此種模式比較少見。
當自定義View不需要過多的考慮尺寸時,可以不重寫OnMeasure函數,因為系統View已經為什么提供了默認的計算方法,如上面的方法。只需要重寫OnDraw函數,其基本過程就是根據那三種測量模式,計算寬和高,然后set進去。也可以把這個過程自己寫一遍,如下:
package com.tuke.customviewonmeasure;import android.content.Context; import android.util.AttributeSet; import android.view.View;/*** 作者:tuke on 2017/6/15 19:42* 郵箱:2297535832@qq.com*/ public class CustomViewOnMeasure1 extends View {//設置默認的寬和高private static final int DEFUALT_VIEW_WIDTH=100;private static final int DEFUALT_VIEW_HEIGHT=100;public CustomViewOnMeasure1(Context context) {super(context);}public CustomViewOnMeasure1(Context context, AttributeSet attrs) {super(context, attrs);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int width=measureDimension(DEFUALT_VIEW_WIDTH,widthMeasureSpec);int height=measureDimension(DEFUALT_VIEW_HEIGHT,heightMeasureSpec);//將計算的寬和高設置進去,保存,最后一步一定要有setMeasuredDimension(width,height);}/*** @param defualtSize 設置的默認大小* @param measureSpec 父控件傳來的widthMeasureSpec,heightMeasureSpec* @return 結果*/public int measureDimension(int defualtSize,int measureSpec){int result=defualtSize;int specMode=MeasureSpec.getMode(measureSpec);int specSize=MeasureSpec.getSize(measureSpec);//1,layout中自定義組件給出來確定的值,比如100dp//2,layout中自定義組件使用的是match_parent,但父控件的size已經可以確定了,比如設置的具體的值或者match_parentif(specMode==MeasureSpec.EXACTLY){result=specSize;}//layout中自定義組件使用的wrap_contentelse if(specMode==MeasureSpec.AT_MOST){result=Math.min(defualtSize,specSize);//建議:result不能大于specSize}//UNSPECIFIED,沒有任何限制,所以可以設置任何大小else {result=defualtSize;}return result;} }
這樣重載了onMeasure函數之后,你會發現,當CustomView使用match_parent的時候,它會占滿整個父控件,而當CustomView使用wrap_content的時候,它的大小則是代碼中定義的默認大小100x100像素。當然,你也可以根據自己的需求改寫measureDimension()的實現。
二,自定義View過程
對于一個Android攻城獅來說,自定義控件是一項必須掌握的重要技能點,然而對于大部分人而言,感覺自定義控件并不是那么容易。在工作過程中難免遇到一些特效需要自己定義控件實現,如果你不會,內心會有強烈的挫敗感,這對一個程序員來說是決不能容忍的,接下來我將寫一系列博客,和大家一起學習自定義控件,讓她赤裸裸的站在我們的面前,讓我們為所欲為。
View這個類代表用戶界面組件的基本構建塊。View在屏幕上占據一個矩形區域,并負責繪制和事件處理。View是用于創建交互式用戶界面組件(按鈕、文本等)的基礎類。它的子類ViewGroup是所有布局的父類,它是一個可以包含其他view或者viewGroup并定義它們的布局屬性的看不見的容器。
實現一個自定義View,你通常會覆蓋一些framework層在所有view上調用的標準方法。你不需要重寫所有這些方法。事實上,你可以只是重寫onDraw(android.graphics.Canvas)。
如果我們要自定義View,最簡單的只需要重寫onDraw(android.graphics.Canvas)即可,聽起來是不是很簡單?那我們就動手自定義一個屬于自己的TextView吧。
| ? ? ? 分類 | ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?方法? | ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?說明 ? ? ? ? ? ? ? ? ?? |
構造 ? ? ? ? ? ? ? ? ? ? ?? | Constructors | 有一種形式的構造方法是使用代碼創建的時候調用的,另一種形式是View被布局文件填充時被調用的。第二種形式應該解析和使用一些屬性定義在布局文件中 |
| onFinishInflate() | 當View和他的所有子控件被XML布局文件填充完成時被調用。(這個方法里面可以完成一些初始化,比如初始化子控件) | |
| 布局 | onMeasure(int, int) | 當決定view和他的孩子的尺寸需求時被調用(也就是測量控件大小時調用) |
| onLayout(boolean, int, int, int, int) | 當View給他的孩子分配大小和位置的時候調用(擺放子控件) | |
| onSizeChanged(int, int, int, int) | 當view大小發生變化時調用 | |
| 繪制 | onDraw(android.graphics.Canvas) | 當視圖應該呈現其內容時調用(繪制) |
| 事件處理 | onKeyDown(int, KeyEvent) | 按鍵時被調用 |
| onKeyUp(int, KeyEvent) | 按鍵被抬起時調用 | |
| onTrackballEvent(MotionEvent) | Called when a trackball motion event occurs. | |
| onWindowFocusChanged(boolean) | 窗口獲取或者失去焦點是調用 | |
| Attaching | onAttachedToWindow() | 當視圖被連接到一個窗口時調用 |
| onDetachedFromWindow() | 當視圖從窗口分離時調用 | |
| onWindowVisibilityChanged(int) | 當View的窗口的可見性發生改變時調用 | |
| 焦點 | onFocusChanged(boolean, int, android.graphics.Rect) | 獲取到或者失去焦點是調用 |
| onTouchEvent(MotionEvent) | 觸摸屏幕時調用 |
1,繼承View,重寫onDraw方法
創建一個類MyTextView繼承View,發現報錯,因為要覆蓋他的構造方法(因為View中沒有參數為空的構造方法),View有四種形式的構造方法,其中四個參數的構造方法是API 21才出現,所以一般我們只需要重寫其他三個構造方法即可。它們的參數不一樣分別對應不同的創建方式,比如只有一個Context參數的構造方法通常是通過代碼初始化控件時使用;而兩個參數的構造方法通常對應布局文件中控件被映射成對象時調用(需要解析屬性);通常我們讓這兩個構造方法最終調用三個參數的構造方法,然后在第三個構造方法中進行一些初始化操作
package com.tuke.customviewonmeasure;import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.util.AttributeSet; import android.view.View;/*** 作者:tuke on 2017/6/16 10:21* 郵箱:2297535832@qq.com*/ public class MyTextView extends View {//設置默認的寬和高private static final int DEFUALT_VIEW_WIDTH=100;private static final int DEFUALT_VIEW_HEIGHT=100;//要繪制的文字private String mText;//文字顏色private int mTextColor;//文字大小private int mTextSize;//繪制時控制文本繪制范圍private Rect mBound;private Paint mPaint;//繪制文本的基坐標private float BaseX,BaseY;//這三個構造函數一定要重寫,因為在Android加載的layout文件中的組件的原理是初始化該組件實例,要調用該組件構造函數public MyTextView(Context context) {this(context,null);}public MyTextView(Context context, AttributeSet attrs) {this(context, attrs,0);}public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);//初始化工作init();}public void init(){//初始化mText="I LOVE YOU";mTextColor= Color.BLACK;mTextSize=50;//設置畫筆的文字大小和顏色mPaint=new Paint();mPaint.setTextSize(mTextSize);mPaint.setColor(mTextColor);mPaint.setTypeface(Typeface.DEFAULT_BOLD);mBound=new Rect();mPaint.getTextBounds(mText,0,mText.length(),mBound);}@Overrideprotected void onDraw(Canvas canvas) {//繪制文字BaseX=getWidth()/2-mBound.width()/2;BaseY=getHeight()/2+mBound.height()/2;canvas.drawText(mText,BaseX,BaseY,mPaint);/*Paint.FontMetrics fontMetrics=mPaint.getFontMetrics();BaseX=getWidth()/2-mPaint.measureText(mText)/2;BaseY=getHeight()/2+(fontMetrics.descent-fontMetrics.ascent)/2-fontMetrics.bottom;canvas.drawText(mText,BaseX,BaseY,mPaint);*/} }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><com.tuke.customviewonmeasure.MyTextViewandroid:layout_width="200dp"android:layout_height="200dp"android:text="TuKe"android:textSize="20sp"android:background="#ff0000"/> </LinearLayout> 運行結果
上面我只是重寫了一個onDraw方法,文本已經繪制出來,說明到此為止這個自定義控件已經算成功了。可是發現了一個問題,如果我要繪制另外的文本呢?比如寫i love you,那是不是又得重新定義一個自定義控件?跟上面一樣,只是需要修改mText就可以了;行,再寫一遍,那如果我現在又想改變文字顏色為藍色呢?在寫一遍?這時候就用到了新的知識點:自定義屬性
2,自定義屬性
Android自定義屬性的的使用步驟
1.首先定義自定義屬性。
2.其次在layout文件中使用自定義屬性,為屬性賦值。
3.在Java代碼中獲取自定義屬性,并使用屬性值。
2.1 在res/values/下創建一個名為attrs.xml的文件,然后定義如下屬性: format的意思是該屬性的取值是什么類型(支持的類型有string,color,demension,integer,enum,reference,float,boolean,fraction,flag)
其中資源文件根節點為<resources></resource>里面擁有的子節點可以為<attr> ,<declare-styleable>,
2.1.1 <attr>節點
attr節點的屬性有name,format.
name就是給自定義屬性命名的,最好能做到見明知意,
關于format是給自定義的屬性定義一個類型,有以下的類型
? ?-reference 引用類型 比如說自定義圖片屬性<attr name="icon" format="reference"></attr>
? ?-color 顏色類型 用來自定義顏色的類型
? ?-string 字符串類型 。。
? ?-demension 尺寸類型 用來定義尺寸類型
1.1.2 <declare-styleable>節點
declare-styleable節點也擁有子節點<attr>
把<resource>節點內的子節點<attr>節點定義的自定義屬性全部Copy到<declare-styleable>節點中,這個時候<declare-styleable>節點的子節點<attr>里面只需要name屬性,自定義屬性的format就可以去掉了(前提是你已經在resources節點下自定義屬性節點中聲明了format,當然你也可以在自定義屬性的時候不聲明fomat,在<declare-styleable>的子節點<attr>中來聲明)。
其中你需要給<declare-styleable>節點定義一個name屬性,給name屬性賦值,(自定義屬性多用在自定義組件,所以這里最好使用自定義組件的名字)
上面給name賦值,是為了使用自定義屬性,是非常有意義的,而且是必須的。下面貼下代碼加強理解
<?xml version="1.0" encoding="utf-8"?> <resources><!--屬性聲明--><attr name="mText" format="string"></attr><attr name="mTextColor" format="color"></attr><attr name="mTextSize" format="dimension"></attr><!--屬性使用--><declare-styleable name="MyTextView"><attr name="mText"></attr><attr name="mTextColor"></attr><attr name="mTextSize"></attr></declare-styleable></resources>
2.2自定義屬性完成后,接下來要使用自定義屬性了
首先我們來比較一下自定義屬性與系統自帶的屬性,比如說
android:layout_width="match_parent"android:layout_height="0dp"android命名空間完畢,使用自己的自定義屬性,給屬性值賦值,貼代碼命名空間完畢,使用自己的自定義屬性,給屬性值賦值,貼代碼:layout_weight="1" > 這些系統自帶的屬性 開頭都是android: 他們的命名空間在
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 這里我們要看到的是xmlns 意思是xml name space xml命名空間 在、、apk/res/android下面
而這里我們自己使用自己的自定義屬性 就必須要加入自己的命名空間,還是與自定義搭上邊
自定義命名空間的方法:
xmlns:tuke="http://schemas.android.com/apk/res-auto" 這里的空間名字"tuke"是可以隨便取得。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tuke="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#D6D6D6"><com.tuke.customviewonmeasure.MyTextViewandroid:layout_width="200dp"android:layout_height="200dp"tuke:mText="I AM FORM CHINA"tuke:mTextColor="#0000ff"tuke:mTextSize="20dp"android:background="#ff0000"/> </LinearLayout>
2.3最后一步了,是時候在java代碼中使用自定義的屬性了,并獲取自定義屬性的值去進行一系列操作
java代碼中獲取自定義的屬性值我們就不得不說到兩個重要的類的了,第一個重要的類是TypedArray,第二個重要的類是TypedValue
package com.tuke.customviewonmeasure;import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.util.AttributeSet; import android.view.View;/*** 作者:tuke on 2017/6/16 10:21* 郵箱:2297535832@qq.com*/ public class MyTextView extends View {//設置默認的寬和高private static final int DEFUALT_VIEW_WIDTH=100;private static final int DEFUALT_VIEW_HEIGHT=100;//要繪制的文字private String mText;//文字顏色private int mTextColor;//文字大小private int mTextSize;//繪制時控制文本繪制范圍private Rect mBound;private Paint mPaint;//繪制文本的基坐標private float BaseX,BaseY;//這三個構造函數一定要重寫,因為在Android加載的layout文件中的組件的原理是初始化該組件實例,要調用該組件構造函數public MyTextView(Context context) {this(context,null);}public MyTextView(Context context, AttributeSet attrs) {this(context, attrs,0);}public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);//獲取自定義屬性的值TypedArray a=context.obtainStyledAttributes(attrs,R.styleable.MyTextView, defStyleAttr,0);mText =a.getString(R.styleable.MyTextView_mText);mTextColor=a.getColor(R.styleable.MyTextView_mTextColor,Color.BLACK);mTextSize = (int) a.getDimension(R.styleable.MyTextView_mTextSize,100);a.recycle();;//初始化畫筆init();}public void init(){//設置畫筆的文字大小和顏色mPaint=new Paint();mPaint.setTextSize(mTextSize);mPaint.setColor(mTextColor);mPaint.setTypeface(Typeface.DEFAULT_BOLD);mBound=new Rect();mPaint.getTextBounds(mText,0,mText.length(),mBound);}@Overrideprotected void onDraw(Canvas canvas) {//繪制文字BaseX=getWidth()/2-mBound.width()/2;BaseY=getHeight()/2+mBound.height()/2;canvas.drawText(mText,BaseX,BaseY,mPaint);/*Paint.FontMetrics fontMetrics=mPaint.getFontMetrics();BaseX=getWidth()/2-mPaint.measureText(mText)/2;BaseY=getHeight()/2+(fontMetrics.descent-fontMetrics.ascent)/2-fontMetrics.bottom;canvas.drawText(mText,BaseX,BaseY,mPaint);*/} }
接下來做一點小變化:
讓繪制的文本長一點tuke:mText="I AM FORM CHINA,I AM FORM CHINA",運行結果:
有沒有發現不和諧的現象?文本超度超出了控件邊界,控件太小,不足以顯示辣么長的文本。
我們將自定義控件換成wrap_content試試。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tuke="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#D6D6D6"><com.tuke.customviewonmeasure.MyTextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"tuke:mText="I AM FORM CHINA,I AM FORM CHINA"tuke:mTextColor="#0000ff"tuke:mTextSize="20dp"android:background="#ff0000"/> </LinearLayout>
什么鬼?不是包裹內容嗎?怎么填充整個屏幕了?這就是前面講的了,當父控件設置的是match_parent且使用自定義控件的系統onMeasure時,自定義view無論設置的是match_parent還是wrap_content,都會填滿父控件,自定義View設置固定值時,現實結果才是固定值。
3,重寫onMeasure方法
解決辦法是不使用系統的onMeasure,我們自己重寫onMeasure,根據不同的情況設置自定義View的寬和高。
package com.tuke.customviewonmeasure;import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.util.AttributeSet; import android.view.View;/*** 作者:tuke on 2017/6/16 10:21* 郵箱:2297535832@qq.com*/ public class MyTextView extends View {//設置默認的寬和高private static final int DEFUALT_VIEW_WIDTH=100;private static final int DEFUALT_VIEW_HEIGHT=100;//要繪制的文字private String mText;//文字顏色private int mTextColor;//文字大小private int mTextSize;//繪制時控制文本繪制范圍private Rect mBound;private Paint mPaint;//繪制文本的基坐標private float BaseX,BaseY;//這三個構造函數一定要重寫,因為在Android加載的layout文件中的組件的原理是初始化該組件實例,要調用該組件構造函數public MyTextView(Context context) {this(context,null);}public MyTextView(Context context, AttributeSet attrs) {this(context, attrs,0);}public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);//獲取自定義屬性的值TypedArray a=context.obtainStyledAttributes(attrs,R.styleable.MyTextView, defStyleAttr,0);mText =a.getString(R.styleable.MyTextView_mText);mTextColor=a.getColor(R.styleable.MyTextView_mTextColor,Color.BLACK);mTextSize = (int) a.getDimension(R.styleable.MyTextView_mTextSize,100);a.recycle();//初始化畫筆init();}public void init(){//設置畫筆的文字大小和顏色mPaint=new Paint();mPaint.setTextSize(mTextSize);mPaint.setColor(mTextColor);mPaint.setTypeface(Typeface.DEFAULT_BOLD);mBound=new Rect();mPaint.getTextBounds(mText,0,mText.length(),mBound);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int widthMode=MeasureSpec.getMode(widthMeasureSpec);int widthSize=MeasureSpec.getSize(widthMeasureSpec);int heightMode=MeasureSpec.getMode(heightMeasureSpec);int heightSize=MeasureSpec.getSize(heightMeasureSpec);int width=10,height=10;//計算自定義View最終的寬和高if(widthMode==MeasureSpec.EXACTLY){//如果match_parent或者具體的值,直接賦值width=widthSize;}else if(widthMode==MeasureSpec.AT_MOST){//專門討論的wrap_content情況//如果是wrap_content,我們要得到控件需要多大的尺寸float textWidth=mBound.width();//文本的寬度width= (int) (getPaddingLeft() + textWidth + getPaddingRight());}//高度跟寬度處理方式一樣if(heightMode==MeasureSpec.EXACTLY){height=widthSize;}else if(heightMode==MeasureSpec.AT_MOST){float textHeight=mBound.height();height= (int) (getPaddingTop() + textHeight + getPaddingBottom());}//保存測量寬度和測量高度setMeasuredDimension(width,height);}@Overrideprotected void onDraw(Canvas canvas) {//繪制文字BaseX=getWidth()/2-mBound.width()/2;BaseY=getHeight()/2+mBound.height()/2;canvas.drawText(mText,BaseX,BaseY,mPaint);/*Paint.FontMetrics fontMetrics=mPaint.getFontMetrics();BaseX=getWidth()/2-mPaint.measureText(mText)/2;BaseY=getHeight()/2+(fontMetrics.descent-fontMetrics.ascent)/2-fontMetrics.bottom;canvas.drawText(mText,BaseX,BaseY,mPaint);*/} } <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tuke="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#D6D6D6"><com.tuke.customviewonmeasure.MyTextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:padding="20dip"tuke:mText="I AM FORM CHINA,I AM FORM CHINA"tuke:mTextColor="#0000ff"tuke:mTextSize="20dp"android:background="#ff0000"/> </LinearLayout>
到此為止,我們已經了解到自定義控件的基本步驟:
1. 繼承View,重寫構造方法
2. 自定義屬性,在構造方法中初始化屬性
3. 重寫onMeasure方法測量寬高
4. 重寫onDraw方法繪制控件
說白了,自定義View就是,兩個方法onMeasure,onDraw,一個自定義屬性。
總結
以上是生活随笔為你收集整理的Andoid自定义View的OnMeasure详解和自定义属性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android Studio新建类头部注
- 下一篇: FFmpeg4.1编译:mac+andr