Android自定義View(一膝但、初體驗(yàn)自定義TextView)

作者:open-Xu

來(lái)源:CSDN

原文:https://blog.csdn.net/xmxkf/article/details/51454685

版權(quán)聲明:本文為博主原創(chuàng)文章杂靶,轉(zhuǎn)載請(qǐng)附上博文鏈接!

目錄:

繼承View重寫(xiě)onDraw方法

自定義屬性

onMeasure方法

MeasureSpec

分析為什么我們自定義的MyTextView設(shè)置了wrap_content卻填充屏幕

重寫(xiě)onMeasure方法

自動(dòng)換行

源碼下載

  對(duì)于一個(gè)Android攻城獅來(lái)說(shuō),自定義控件是一項(xiàng)必須掌握的重要技能點(diǎn)辐益,然而對(duì)于大部分人而言祭示,感覺(jué)自定義控件并不是那么容易肄满。在工作過(guò)程中難免遇到一些特效需要自己定義控件實(shí)現(xiàn),如果你不會(huì),內(nèi)心會(huì)有強(qiáng)烈的挫敗感稠歉,這對(duì)一個(gè)程序員來(lái)說(shuō)是決不能容忍的掰担,接下來(lái)我將寫(xiě)一系列博客,和大家一起學(xué)習(xí)自定義控件怒炸,讓她赤裸裸的站在我們的面前带饱,讓我們?yōu)樗麨椤?:joy:

  言歸正傳,接觸到一個(gè)類阅羹,你不太了解他纠炮,如果貿(mào)然翻閱源碼只會(huì)讓你失去方向,不知從哪里下手灯蝴;所以我們應(yīng)該從文檔著手恢口,看看它是個(gè)什么東西,里面有哪些屬性和方法穷躁,都是用來(lái)干嘛的耕肩。下面我們看看官方文檔對(duì)View的介紹:

View這個(gè)類代表用戶界面組件的基本構(gòu)建塊。View在屏幕上占據(jù)一個(gè)矩形區(qū)域问潭,并負(fù)責(zé)繪制和事件處理猿诸。View是用于創(chuàng)建交互式用戶界面組件(按鈕、文本等)的基礎(chǔ)類狡忙。它的子類ViewGroup是所有布局的父類梳虽,它是一個(gè)可以包含其他view或者viewGroup并定義它們的布局屬性的看不見(jiàn)的容器。

實(shí)現(xiàn)一個(gè)自定義View灾茁,你通常會(huì)覆蓋一些framework層在所有view上調(diào)用的標(biāo)準(zhǔn)方法窜觉。你不需要重寫(xiě)所有這些方法。事實(shí)上北专,你可以只是重寫(xiě)onDraw(android.graphics.Canvas)禀挫。

分類 方法 說(shuō)明

構(gòu)造 Constructors 有一種形式的構(gòu)造方法是使用代碼創(chuàng)建的時(shí)候調(diào)用的,另一種形式是View被布局文件填充時(shí)被調(diào)用的拓颓。第二種形式應(yīng)該解析和使用一些屬性定義在布局文件中

onFinishInflate() 當(dāng)View和他的所有子控件被XML布局文件填充完成時(shí)被調(diào)用语婴。(這個(gè)方法里面可以完成一些初始化,比如初始化子控件)

布局 onMeasure(int, int) 當(dāng)決定view和他的孩子的尺寸需求時(shí)被調(diào)用(也就是測(cè)量控件大小時(shí)調(diào)用)

onLayout(boolean, int, int, int, int) 當(dāng)View給他的孩子分配大小和位置的時(shí)候調(diào)用(擺放子控件)

onSizeChanged(int, int, int, int) 當(dāng)view大小發(fā)生變化時(shí)調(diào)用

繪制 onDraw(android.graphics.Canvas) 當(dāng)視圖應(yīng)該呈現(xiàn)其內(nèi)容時(shí)調(diào)用(繪制)

事件處理 onKeyDown(int, KeyEvent) 按鍵時(shí)被調(diào)用

onKeyUp(int, KeyEvent) 按鍵被抬起時(shí)調(diào)用

onTrackballEvent(MotionEvent) Called when a trackball motion event occurs.

onTouchEvent(MotionEvent) 觸摸屏幕時(shí)調(diào)用

焦點(diǎn) onFocusChanged(boolean, int, android.graphics.Rect) 獲取到或者失去焦點(diǎn)是調(diào)用

onWindowFocusChanged(boolean) 窗口獲取或者失去焦點(diǎn)是調(diào)用

Attaching onAttachedToWindow() 當(dāng)視圖被連接到一個(gè)窗口時(shí)調(diào)用

onDetachedFromWindow() 當(dāng)視圖從窗口分離時(shí)調(diào)用

onWindowVisibilityChanged(int) 當(dāng)View的窗口的可見(jiàn)性發(fā)生改變時(shí)調(diào)用

  從上面官方文檔介紹我們可以知道驶睦,View是所有控件(包括ViewGroup)的父類砰左,它里面有一些常見(jiàn)的方法(上表),如果我們要自定義View场航,最簡(jiǎn)單的只需要重寫(xiě)onDraw(android.graphics.Canvas)即可缠导,聽(tīng)起來(lái)是不是很簡(jiǎn)單?那我們就動(dòng)手自定義一個(gè)屬于自己的TextView吧旗闽。

1. 繼承View酬核,重寫(xiě)onDraw方法

  創(chuàng)建一個(gè)類MyTextView繼承View蜜另,發(fā)現(xiàn)報(bào)錯(cuò),因?yàn)橐采w他的構(gòu)造方法(因?yàn)閂iew中沒(méi)有參數(shù)為空的構(gòu)造方法)嫡意,View有四種形式的構(gòu)造方法举瑰,其中四個(gè)參數(shù)的構(gòu)造方法是API 21才出現(xiàn),所以一般我們只需要重寫(xiě)其他三個(gè)構(gòu)造方法即可蔬螟。它們的參數(shù)不一樣分別對(duì)應(yīng)不同的創(chuàng)建方式此迅,比如只有一個(gè)Context參數(shù)的構(gòu)造方法通常是通過(guò)代碼初始化控件時(shí)使用;而兩個(gè)參數(shù)的構(gòu)造方法通常對(duì)應(yīng)布局文件中控件被映射成對(duì)象時(shí)調(diào)用(需要解析屬性)旧巾;通常我們讓這兩個(gè)構(gòu)造方法最終調(diào)用三個(gè)參數(shù)的構(gòu)造方法耸序,然后在第三個(gè)構(gòu)造方法中進(jìn)行一些初始化操作。

public class MyView extends View {

? ? /**

? ? * 需要繪制的文字

? ? */

? ? private String mText;

? ? /**

? ? * 文本的顏色

? ? */

? ? private int mTextColor;

? ? /**

? ? * 文本的大小

? ? */

? ? private int mTextSize;

? ? /**

? ? * 繪制時(shí)控制文本繪制的范圍

? ? */

? ? private Rect mBound;

? ? private Paint mPaint;

? ? 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);

//初始化

? ? ? ? mText = "Udf32fA";

? ? ? ? mTextColor = Color.BLACK;

? ? ? ? mTextSize = 100;

? ? ? ? mPaint = new Paint();

? ? ? ? mPaint.setTextSize(mTextSize);

? ? ? ? mPaint.setColor(mTextColor);

? ? ? ? //獲得繪制文本的寬和高

? ? ? ? mBound = new Rect();

? ? ? ? mPaint.getTextBounds(mText, 0, mText.length(), mBound);

? ? }

? ? //API21

//? ? public MyTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {

//? ? ? ? super(context, attrs, defStyleAttr, defStyleRes);

//? ? ? ? init();

//? ? }

? ? @Override

? ? protected void onDraw(Canvas canvas) {

? ? ? ? //繪制文字

? ? ? ? canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);

? ? }

}

布局文件:

<?xml version="1.0" encoding="utf-8"?>

<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">

? ? ? ? <view.openxu.com.mytextview.MyTextView

? ? ? ? ? ? android:layout_width="200dip"

? ? ? ? ? ? android:layout_height="100dip"

? ? ? ? ? ? android:textSize="20sp"

? ? ? ? ? ? android:text="按鈕"

? ? ? ? ? ? android:background="#ff0000"/>

</LinearLayout>

運(yùn)行結(jié)果:

  上面我只是重寫(xiě)了一個(gè)onDraw方法鲁猩,文本已經(jīng)繪制出來(lái)坎怪,說(shuō)明到此為止這個(gè)自定義控件已經(jīng)算成功了±眨可是發(fā)現(xiàn)了一個(gè)問(wèn)題搅窿,如果我要繪制另外的文本呢?比如寫(xiě)i love you隙券,那是不是又得重新定義一個(gè)自定義控件男应?跟上面一樣,只是需要修改mText就可以了娱仔;行沐飘,再寫(xiě)一遍,那如果我現(xiàn)在又想改變文字顏色為藍(lán)色呢牲迫?在寫(xiě)一遍耐朴?這時(shí)候就用到了新的知識(shí)點(diǎn):自定義屬性

2. 自定義屬性

  在res/values/下創(chuàng)建一個(gè)名為attrs.xml的文件,然后定義如下屬性:

format的意思是該屬性的取值是什么類型(支持的類型有string,color,demension,integer,enum,reference,float,boolean,fraction,flag)

<?xml version="1.0" encoding="utf-8"?>

<resources>

? ? <attr name="mText" format="string" />

? ? <attr name="mTextColor" format="color" />

? ? <attr name="mTextSize" format="dimension" />

? ? <declare-styleable name="MyTextView">

? ? ? ? <attr name="mText"/>

? ? ? ? <attr name="mTextColor"/>

? ? ? ? <attr name="mTextSize"/>

? ? </declare-styleable>

</resources>

然后在布局文件中使用自定義屬性恩溅,記住一定要引入我們的命名空間xmlns:openxu="http://schemas.android.com/apk/res-auto"

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

? ? xmlns:tools="http://schemas.android.com/tools"

? ? xmlns:openxu="http://schemas.android.com/apk/res-auto"

? ? android:layout_width="match_parent"

? ? android:layout_height="match_parent">

? ? ? ? <view.openxu.com.mytextview.MyTextView

? ? ? ? ? ? android:layout_width="200dip"

? ? ? ? ? ? android:layout_height="100dip"

? ? ? ? ? ? openxu:mTextSize="25sp"

? ? ? ? ? ? openxu:mText="i love you"

? ? ? ? ? ? openxu:mTextColor ="#0000ff"

? ? ? ? ? ? android:background="#ff0000"/>

</LinearLayout>

在構(gòu)造方法中獲取自定義屬性的值:

public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {

? ? super(context, attrs, defStyleAttr);

? ? //獲取自定義屬性的值

? ? TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextView, defStyleAttr, 0);

? ? mText = a.getString(R.styleable.MyTextView_mText);

? ? mTextColor = a.getColor(R.styleable.MyTextView_mTextColor, Color.BLACK);

? ? mTextSize = a.getDimension(R.styleable.MyTextView_mTextSize, 100);

? ? a.recycle();? //注意回收

? ? mPaint = new Paint();

? ? mPaint.setTextSize(mTextSize);

? ? mPaint.setColor(mTextColor);

? ? //獲得繪制文本的寬和高

? ? mBound = new Rect();

? ? mPaint.getTextBounds(mText, 0, mText.length(), mBound);

}


運(yùn)行結(jié)果:

  通過(guò)運(yùn)行結(jié)果隔箍,我們已經(jīng)成功為MyTextView定義了屬性,并獲取到值脚乡,至于自定義屬性的詳細(xì)知識(shí)點(diǎn)到后面會(huì)專門(mén)寫(xiě)一篇博客去介紹。

到此為止滨达,發(fā)現(xiàn)自定義控件還是比較簡(jiǎn)單的嘛奶稠。看看結(jié)果捡遍,跟原生的TextView還有什么差別锌订?接下來(lái)做一點(diǎn)小變化:

讓繪制的文本長(zhǎng)一點(diǎn)openxu:mText="i love you i love you i love you",運(yùn)行結(jié)果:

  有沒(méi)有發(fā)現(xiàn)不和諧的現(xiàn)象画株?文本超度超出了控件邊界辆飘,控件太小啦辐,不足以顯示辣么長(zhǎng)的文本,我們將寬高改為wrap_content試試:

  什么鬼蜈项?不是包裹內(nèi)容嗎芹关?怎么填充整個(gè)屏幕了?根據(jù)頂部官方文檔的說(shuō)明紧卒,我們猜想肯定是控件的測(cè)量onMeasure方法出了問(wèn)題侥衬,接下來(lái)我們學(xué)習(xí)onMeasure方法。

3. onMeasure方法

①. MeasureSpec

  在學(xué)習(xí)onMasure方法之前跑芳,我們要先了解他的參數(shù)中的一個(gè)類MeasureSpec轴总,知己知彼才能百戰(zhàn)百勝 。

跟蹤一下源碼博个,發(fā)現(xiàn)它是View中的一個(gè)靜態(tài)內(nèi)部類怀樟,是由尺寸和模式組合而成的一個(gè)值,用來(lái)描述父控件對(duì)子控件尺寸的約束盆佣,看看他的部分源碼漂佩,一共有三種模式,然后提供了合成和分解的方法:

/**

* measurespec封裝了父控件對(duì)他的孩子的布局要求罪塔。

* 一個(gè)measurespec由大小和模式投蝉。有三種可能的模式:

*/

public static class MeasureSpec {

? ? private static final int MODE_SHIFT = 30;

? ? private static final int MODE_MASK? = 0x3 << MODE_SHIFT;

? ? //父控件不強(qiáng)加任何約束給子控件,它可以是它想要任何大小征堪。

? ? public static final int UNSPECIFIED = 0 << MODE_SHIFT;? //0

? ? //父控件決定給孩子一個(gè)精確的尺寸

? ? public static final int EXACTLY? ? = 1 << MODE_SHIFT;? //1073741824

? ? //父控件會(huì)給子控件盡可能大的尺寸

? ? public static final int AT_MOST? ? = 2 << MODE_SHIFT;? //-2147483648

? ? /**

? ? * 根據(jù)給定的尺寸和模式創(chuàng)建一個(gè)約束規(guī)范

? ? */

? ? public static int makeMeasureSpec(int size, int mode) {

? ? ? ? if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {

? ? ? ? ? ? return 0;

? ? ? ? }

? ? ? ? return makeMeasureSpec(size, mode);

? ? }

? ? /**

? ? * 從約束規(guī)范中獲取模式

? ? */

? ? public static int getMode(int measureSpec) {

? ? ? ? return (measureSpec & MODE_MASK);

? ? }

? ? /**

? ? * 從約束規(guī)范中獲取尺寸

? ? */

? ? public static int getSize(int measureSpec) {

? ? ? ? return (measureSpec & ~MODE_MASK);

? ? }

}


  這樣說(shuō)起來(lái)還是有點(diǎn)抽象瘩缆,舉一個(gè)小栗子大家就知道這三種約束到底是什么意思。我們自定義一個(gè)View佃蚜,為了方便起見(jiàn)庸娱,讓它繼承Button,布局文件中設(shè)置不同的寬高條件谐算,然后在onMeasure方法中打印一下他的參數(shù)(int widthMeasureSpec, int heightMeasureSpec)到底是個(gè)什么鬼

/**

* Created by openXu on 16/5/19.

*/

public class MyView extends Button {

? ? public MyView(Context context) {

? ? ? ? this(context, null);

? ? }

? ? public MyView(Context context, AttributeSet attrs) {

? ? ? ? this(context, attrs, 0);

? ? }

? ? public MyView(Context context, AttributeSet attrs, int defStyle) {

? ? ? ? super(context, attrs, defStyle);

? ? }

? ? @Override

? ? protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

? ? ? ? super.onMeasure(widthMeasureSpec, heightMeasureSpec);

? ? ? ? int widthMode = MeasureSpec.getMode(widthMeasureSpec);? //獲取寬的模式

? ? ? ? int heightMode = MeasureSpec.getMode(heightMeasureSpec); //獲取高的模式

? ? ? ? int widthSize = MeasureSpec.getSize(widthMeasureSpec);? //獲取寬的尺寸

? ? ? ? int heightSize = MeasureSpec.getSize(heightMeasureSpec); //獲取高的尺寸

? ? ? ? Log.v("openxu", "寬的模式:"+widthMode);

? ? ? ? Log.v("openxu", "高的模式:"+heightMode);

? ? ? ? Log.v("openxu", "寬的尺寸:"+widthSize);

? ? ? ? Log.v("openxu", "高的尺寸:"+heightSize);

? ? }

}


情形1熟尉,讓按鈕包裹內(nèi)容:

<?xml version="1.0" encoding="utf-8"?>

<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">

? ? ? ? <com.example.openxu.myview.MyView

? ? ? ? ? ? android:layout_width="wrap_content"

? ? ? ? ? ? android:layout_height="wrap_content"

? ? ? ? ? ? android:textSize="20sp"

? ? ? ? ? ? android:text="按鈕"

? ? ? ? ? ? android:background="#ff0000"/>

</LinearLayout>


log打印:

05-19 06:02:55.112: V/openxu(15599): 寬的模式:-2147483648

05-19 06:02:55.112: V/openxu(15599): 高的模式:-2147483648

05-19 06:02:55.112: V/openxu(15599): 寬的尺寸:1080

05-19 06:02:55.112: V/openxu(15599): 高的尺寸:1860

情形2洲脂,讓按鈕填充父窗體:

<?xml version="1.0" encoding="utf-8"?>

<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">

? ? ? ? <com.example.openxu.myview.MyView

? ? ? ? ? ? android:layout_width="match_parent"

? ? ? ? ? ? android:layout_height="match_parent"

? ? ? ? ? ? android:textSize="20sp"

? ? ? ? ? ? android:text="按鈕"

? ? ? ? ? ? android:background="#ff0000"/>

</LinearLayout>

log打咏锒:

05-19 06:05:37.302: V/openxu(15960): 寬的模式:1073741824

05-19 06:05:37.302: V/openxu(15960): 高的模式:1073741824

05-19 06:05:37.302: V/openxu(15960): 寬的尺寸:1080

05-19 06:05:37.302: V/openxu(15960): 高的尺寸:1860

情形3,給按鈕的寬設(shè)置為具體的值:

<?xml version="1.0" encoding="utf-8"?>

<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">

? ? ? ? <com.example.openxu.myview.MyView

? ? ? ? ? ? android:layout_width="100dip"

? ? ? ? ? ? android:layout_height="wrap_content"

? ? ? ? ? ? android:textSize="20sp"

? ? ? ? ? ? android:text="按鈕"

? ? ? ? ? ? android:background="#ff0000"/>

</LinearLayout>


log打涌纸酢:

05-19 06:07:48.932: V/openxu(16105): 寬的模式:1073741824

05-19 06:07:48.932: V/openxu(16105): 高的模式:-2147483648

05-19 06:07:48.932: V/openxu(16105): 寬的尺寸:300

05-19 06:07:48.932: V/openxu(16105): 高的尺寸:1860

  根據(jù)上面的測(cè)試往果,我們發(fā)現(xiàn),約束中分離出來(lái)的尺寸就是父控件剩余的寬高大幸磺Α(除了設(shè)置具體的寬高值外)陕贮;而幾種約束中的模式不就是對(duì)應(yīng)我們?cè)诓季治募性O(shè)置給按鈕的幾種情況嗎?如下:

約束 布局參數(shù) 輸出值 說(shuō)明

UNSPECIFIED(未指定) 0 父控件沒(méi)有對(duì)子控件施加任何約束潘飘,子控件可以得到任意想要的大小肮之。但是布局文件好像必須設(shè)置寬高掉缺,目前還沒(méi)找到與之對(duì)應(yīng)的布局參數(shù),使用較少

EXACTLY(完全) match_parent/具體寬高值 1073741824 父控件給子控件決定了確切大小戈擒,子控件將被限定在給定的邊界里而忽略它本身大小眶明。特別說(shuō)明如果是填充父窗體,說(shuō)明父控件已經(jīng)明確知道子控件想要多大的尺寸了(就是剩余的空間都要了)

AT_MOST(至多) wrap_content -2147483648 子控件至多達(dá)到指定大小的值峦甩。包裹內(nèi)容就是父窗體并不知道子控件到底需要多大尺寸(具體值)赘来,需要子控件自己測(cè)量之后再讓父控件給他一個(gè)盡可能大的尺寸以便讓內(nèi)容全部顯示但不能超過(guò)包裹內(nèi)容的大小

②. 分析為什么我們自定義的MyTextView設(shè)置了wrap_content卻填充屏幕

  通過(guò)上面對(duì)MeasureSpec的了解,我們現(xiàn)在就有能看懂View的onMeasure方法默認(rèn)是怎樣為控件測(cè)量大小的了

看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;

}


   onMeasure方法調(diào)用了setMeasuredDimension(int measuredWidth, int measuredHeight)方法凯傲,而傳入的參數(shù)已經(jīng)是測(cè)量過(guò)的默認(rèn)寬和高的值了犬辰;我們看看getDefaultSize 方法是怎么計(jì)算測(cè)量寬高的。根據(jù)父控件給予的約束冰单,發(fā)現(xiàn)AT_MOST (相當(dāng)于wrap_content )和EXACTLY (相當(dāng)于match_parent )兩種情況返回的測(cè)量寬高都是specSize幌缝,而這個(gè)specSize正是我們上面說(shuō)的父控件剩余的寬高,所以默認(rèn)onMeasure方法中wrap_content 和match_parent 的效果是一樣的诫欠,都是填充剩余的空間涵卵。

③. 重寫(xiě)onMeasure方法

  我們先忽略掉UNSPECIFIED 的情況(使用極少),只考慮AT_MOST 和EXACTLY 荒叼,現(xiàn)在的問(wèn)題是設(shè)置wrap_content 時(shí)轿偎,控件卻使用了match_parent 的效果,看下面怎么重寫(xiě)onMeasure(注釋比較詳細(xì)被廓,不做過(guò)多講解):

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

? ? super.onMeasure(widthMeasureSpec, heightMeasureSpec);

? ? int widthMode = MeasureSpec.getMode(widthMeasureSpec);? //獲取寬的模式

? ? int heightMode = MeasureSpec.getMode(heightMeasureSpec); //獲取高的模式

? ? int widthSize = MeasureSpec.getSize(widthMeasureSpec);? //獲取寬的尺寸

? ? int heightSize = MeasureSpec.getSize(heightMeasureSpec); //獲取高的尺寸

? ? Log.v("openxu", "寬的模式:"+widthMode);

? ? Log.v("openxu", "高的模式:"+heightMode);

? ? Log.v("openxu", "寬的尺寸:"+widthSize);

? ? Log.v("openxu", "高的尺寸:"+heightSize);

? ? int width;

? ? int height ;

? ? if (widthMode == MeasureSpec.EXACTLY) {

? ? ? ? //如果match_parent或者具體的值坏晦,直接賦值

? ? ? ? width = widthSize;

? ? } else {

? ? ? ? //如果是wrap_content,我們要得到控件需要多大的尺寸

? ? ? ? float textWidth = mBound.width();? //文本的寬度

? ? ? ? //控件的寬度就是文本的寬度加上兩邊的內(nèi)邊距嫁乘。內(nèi)邊距就是padding值昆婿,在構(gòu)造方法執(zhí)行完就被賦值

? ? ? ? width = (int) (getPaddingLeft() + textWidth + getPaddingRight());

? ? ? ? Log.v("openxu", "文本的寬度:"+textWidth + "控件的寬度:"+width);

? ? }

? ? //高度跟寬度處理方式一樣

? ? if (heightMode == MeasureSpec.EXACTLY) {

? ? ? ? height = heightSize;

? ? } else {

? ? ? ? float textHeight = mBound.height();

? ? ? ? height = (int) (getPaddingTop() + textHeight + getPaddingBottom());

? ? ? ? Log.v("openxu", "文本的高度:"+textHeight + "控件的高度:"+height);

? ? }

? ? //保存測(cè)量寬度和測(cè)量高度

? ? setMeasuredDimension(width, height);

}


布局文件:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

? ? xmlns:tools="http://schemas.android.com/tools"

? ? xmlns:openxu="http://schemas.android.com/apk/res-auto"

? ? android:layout_width="match_parent"

? ? android:layout_height="match_parent">

? ? ? ? <view.openxu.com.mytextview.MyTextView

? ? ? ? ? ? android:layout_width="wrap_content"

? ? ? ? ? ? android:layout_height="wrap_content"

? ? ? ? ? ? android:padding="20dip"

? ? ? ? ? ? openxu:mTextSize="25sp"

? ? ? ? ? ? openxu:mText="i love you i love you i love you"

? ? ? ? ? ? openxu:mTextColor ="#0000ff"

? ? ? ? ? ? android:background="#ff0000"/>

</LinearLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

下面是輸出的log:

05-19 01:29:12.662 27380-27380/view.openxu.com.mytextview V/openxu: 寬的模式:-2147483648

05-19 01:29:12.662 27380-27380/view.openxu.com.mytextview V/openxu: 高的模式:-2147483648

05-19 01:29:12.662 27380-27380/view.openxu.com.mytextview V/openxu: 寬的尺寸:720

05-19 01:29:12.666 27380-27380/view.openxu.com.mytextview V/openxu: 高的尺寸:1230

05-19 01:29:12.678 27380-27380/view.openxu.com.mytextview V/openxu: 文本的寬度:652.0控件的寬度:732

05-19 01:29:12.690 27380-27380/view.openxu.com.mytextview V/openxu: 文本的高度:49.0控件的高度:129

  我的模擬器是720x1280的,根據(jù)log顯示蜓斧,文本的寬度是652仓蛆,加上兩邊的內(nèi)邊距,控件的寬度為732挎春,確實(shí)實(shí)現(xiàn)了包裹內(nèi)容的效果看疙,運(yùn)行程序結(jié)果如下:

  但是發(fā)現(xiàn)寬度已經(jīng)超出了屏幕,還不能像TextView一樣換行搂蜓;下面我們簡(jiǎn)單的模擬一下?lián)Q行的功能狼荞,做的不夠好,但有這個(gè)效果帮碰,不是重點(diǎn),不需要重點(diǎn)掌握

4. 自動(dòng)換行

  只需要在測(cè)量的時(shí)候拾积,根據(jù)文字的總長(zhǎng)度和控件的寬度殉挽,就可以知道需要繪制幾行丰涉,然后將文本分割成小段放入集合中,在onDraw方法中分別繪制斯碌;

  需要注意的是一死,onMeasure方法不只調(diào)用一次,所以在分段文本是需要判斷傻唾,不要重復(fù)分段投慈,否則會(huì)報(bào)錯(cuò)。代碼如下(僅供參考):

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.text.TextUtils;

import android.util.AttributeSet;

import android.util.Log;

import android.view.View;

import java.util.ArrayList;

/**

* Created by openXu on 16/5/19.

*/

public class MyTextView extends View {

? ? /**

? ? * 需要繪制的文字

? ? */

? ? private String mText;

? ? private ArrayList<String> mTextList;

? ? /**

? ? * 文本的顏色

? ? */

? ? private int mTextColor;

? ? /**

? ? * 文本的大小

? ? */

? ? private float mTextSize;

? ? /**

? ? * 繪制時(shí)控制文本繪制的范圍

? ? */

? ? private Rect mBound;

? ? private Paint mPaint;

? ? 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);

? ? ? ? mTextList = new ArrayList<String>();

? ? ? ? //獲取自定義屬性的值

? ? ? ? TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextView, defStyleAttr, 0);

? ? ? ? mText = a.getString(R.styleable.MyTextView_mText);

? ? ? ? mTextColor = a.getColor(R.styleable.MyTextView_mTextColor, Color.BLACK);

? ? ? ? mTextSize = a.getDimension(R.styleable.MyTextView_mTextSize, 100);

? ? ? ? a.recycle();? //注意回收

? ? ? ? Log.v("openxu", "文本總長(zhǎng)度:"+mText);

? ? ? ? mPaint = new Paint();

? ? ? ? mPaint.setTextSize(mTextSize);

? ? ? ? mPaint.setColor(mTextColor);

? ? ? ? //獲得繪制文本的寬和高

? ? ? ? mBound = new Rect();

? ? ? ? mPaint.getTextBounds(mText, 0, mText.length(), mBound);

? ? }

? ? //API21

//? ? public MyTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {

//? ? ? ? super(context, attrs, defStyleAttr, defStyleRes);

//? ? ? ? init();

//? ? }

? ? @Override

? ? protected void onDraw(Canvas canvas) {

? ? ? ? //繪制文字

? ? ? ? for(int i = 0; i<mTextList.size(); i++){

? ? ? ? ? ? mPaint.getTextBounds(mTextList.get(i), 0, mTextList.get(i).length(), mBound);

? ? ? ? ? ? Log.v("openxu", "mBound.h:"+mBound.height());

? ? ? ? ? ? Log.v("openxu", "在X:" + (getWidth() / 2 - mBound.width() / 2)+"? Y:"+(getPaddingTop() + (mBound.height() *i))+"? 繪制:"+mTextList.get(i));

? ? ? ? ? ? canvas.drawText(mTextList.get(i), (getWidth() / 2 - mBound.width() / 2), (getPaddingTop() + (mBound.height() *i)), mPaint);

? ? ? ? }

? ? }

? ? boolean isOneLines = true;

? ? float lineNum;

? ? float spLineNum;

? ? @Override

? ? protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

? ? ? ? super.onMeasure(widthMeasureSpec, heightMeasureSpec);

? ? ? ? int widthMode = MeasureSpec.getMode(widthMeasureSpec);? //獲取寬的模式

? ? ? ? int heightMode = MeasureSpec.getMode(heightMeasureSpec); //獲取高的模式

? ? ? ? int widthSize = MeasureSpec.getSize(widthMeasureSpec);? //獲取寬的尺寸

? ? ? ? int heightSize = MeasureSpec.getSize(heightMeasureSpec); //獲取高的尺寸

? ? ? ? Log.v("openxu", "寬的模式:"+widthMode);

? ? ? ? Log.v("openxu", "高的模式:"+heightMode);

? ? ? ? Log.v("openxu", "寬的尺寸:"+widthSize);

? ? ? ? Log.v("openxu", "高的尺寸:"+heightSize);

? ? ? ? float textWidth = mBound.width();? //文本的寬度

? ? ? ? if(mTextList.size()==0){

? ? ? ? ? ? //將文本分段

? ? ? ? ? ? int padding = getPaddingLeft() + getPaddingRight();

? ? ? ? ? ? int specWidth = widthSize - padding; //能夠顯示文本的最大寬度

? ? ? ? ? ? if(textWidth<specWidth){

? ? ? ? ? ? ? ? //說(shuō)明一行足矣顯示

? ? ? ? ? ? ? ? lineNum = 1;

? ? ? ? ? ? ? ? mTextList.add(mText);

? ? ? ? ? ? }else{

? ? ? ? ? ? ? ? //超過(guò)一行

? ? ? ? ? ? ? ? isOneLines = false;

? ? ? ? ? ? ? ? spLineNum = textWidth/specWidth;

? ? ? ? ? ? ? ? if((spLineNum+"").contains(".")){

? ? ? ? ? ? ? ? ? ? lineNum = Integer.parseInt((spLineNum+"").substring(0,(spLineNum+"").indexOf(".") ))+1;

? ? ? ? ? ? ? ? }else{

? ? ? ? ? ? ? ? ? ? lineNum = spLineNum;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? int lineLength = (int)(mText.length()/spLineNum);

? ? ? ? ? ? ? ? Log.v("openxu", "文本總長(zhǎng)度:"+mText);

? ? ? ? ? ? ? ? Log.v("openxu", "文本總長(zhǎng)度:"+mText.length());

? ? ? ? ? ? ? ? Log.v("openxu", "能繪制文本的寬度:"+lineLength);

? ? ? ? ? ? ? ? Log.v("openxu", "需要繪制:"+lineNum+"行");

? ? ? ? ? ? ? ? Log.v("openxu", "lineLength:"+lineLength);

? ? ? ? ? ? ? ? for(int i = 0; i<lineNum; i++){

? ? ? ? ? ? ? ? ? ? String lineStr;

? ? ? ? ? ? ? ? ? ? if(mText.length()<lineLength){

? ? ? ? ? ? ? ? ? ? ? ? lineStr = mText.substring(0, mText.length());

? ? ? ? ? ? ? ? ? ? }else{

? ? ? ? ? ? ? ? ? ? ? ? lineStr = mText.substring(0, lineLength);

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? Log.v("openxu", "lineStr:"+lineStr);

? ? ? ? ? ? ? ? ? ? mTextList.add(lineStr);

? ? ? ? ? ? ? ? ? ? if(!TextUtils.isEmpty(mText)) {

? ? ? ? ? ? ? ? ? ? ? ? if(mText.length()<lineLength){

? ? ? ? ? ? ? ? ? ? ? ? ? ? mText = mText.substring(0, mText.length());

? ? ? ? ? ? ? ? ? ? ? ? }else{

? ? ? ? ? ? ? ? ? ? ? ? ? ? mText = mText.substring(lineLength, mText.length());

? ? ? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? }else{

? ? ? ? ? ? ? ? ? ? ? ? break;

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? int width;

? ? ? ? int height ;

? ? ? ? if (widthMode == MeasureSpec.EXACTLY) {

? ? ? ? ? ? //如果match_parent或者具體的值冠骄,直接賦值

? ? ? ? ? ? width = widthSize;

? ? ? ? } else {

? ? ? ? ? ? //如果是wrap_content伪煤,我們要得到控件需要多大的尺寸

? ? ? ? ? ? if(isOneLines){

? ? ? ? ? ? ? ? //控件的寬度就是文本的寬度加上兩邊的內(nèi)邊距。內(nèi)邊距就是padding值凛辣,在構(gòu)造方法執(zhí)行完就被賦值

? ? ? ? ? ? ? ? width = (int) (getPaddingLeft() + textWidth + getPaddingRight());

? ? ? ? ? ? }else{

? ? ? ? ? ? ? ? //如果是多行抱既,說(shuō)明控件寬度應(yīng)該填充父窗體

? ? ? ? ? ? ? ? width = widthSize;

? ? ? ? ? ? }

? ? ? ? ? ? Log.v("openxu", "文本的寬度:"+textWidth + "控件的寬度:"+width);

? ? ? ? }

? ? ? ? //高度跟寬度處理方式一樣

? ? ? ? if (heightMode == MeasureSpec.EXACTLY) {

? ? ? ? ? ? height = heightSize;

? ? ? ? } else {

? ? ? ? ? ? float textHeight = mBound.height();

? ? ? ? ? ? if(isOneLines){

? ? ? ? ? ? ? ? height = (int) (getPaddingTop() + textHeight + getPaddingBottom());

? ? ? ? ? ? }else{

? ? ? ? ? ? ? ? //如果是多行

? ? ? ? ? ? ? ? height = (int) (getPaddingTop() + textHeight*lineNum + getPaddingBottom());;

? ? ? ? ? ? }

? ? ? ? ? ? Log.v("openxu", "文本的高度:"+textHeight + "控件的高度:"+height);

? ? ? ? }

? ? ? ? //保存測(cè)量寬度和測(cè)量高度

? ? ? ? setMeasuredDimension(width, height);

? ? }

}


布局文件:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

? ? xmlns:tools="http://schemas.android.com/tools"

? ? xmlns:openxu="http://schemas.android.com/apk/res-auto"

? ? android:layout_width="match_parent"

? ? android:layout_height="match_parent">

? ? ? ? <view.openxu.com.mytextview.MyTextView

? ? ? ? ? ? android:layout_width="wrap_content"

? ? ? ? ? ? android:layout_height="wrap_content"

? ? ? ? ? ? android:padding="20dip"

? ? ? ? ? ? openxu:mTextSize="25sp"

? ? ? ? ? ? openxu:mText="門(mén)前大橋下,游過(guò)一群鴨扁誓,快來(lái)快來(lái)數(shù)一數(shù)防泵,二四六七八。帽兒破蝗敢,鞋兒破捷泞,身上滴袈裟破,你笑我"

? ? ? ? ? ? openxu:mTextColor ="#0000ff"

? ? ? ? ? ? android:background="#ff0000"/>

</LinearLayout>


運(yùn)行效果:

到此為止寿谴,我們已經(jīng)了解到自定義控件的基本步驟:

1. 繼承View锁右,重寫(xiě)構(gòu)造方法

2. 自定義屬性,在構(gòu)造方法中初始化屬性

3. 重寫(xiě)onMeasure方法測(cè)量寬高

4. 重寫(xiě)onDraw方法繪制控件

各位看官拭卿,看到這里辛苦了骡湖,順便留個(gè)言、點(diǎn)個(gè)贊唄~ ~謝過(guò)了

源碼下載:

https://github.com/openXu/MyTextView

---------------------

作者:open-Xu

來(lái)源:CSDN

原文:https://blog.csdn.net/xmxkf/article/details/51454685

版權(quán)聲明:本文為博主原創(chuàng)文章峻厚,轉(zhuǎn)載請(qǐng)附上博文鏈接响蕴!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市惠桃,隨后出現(xiàn)的幾起案子浦夷,更是在濱河造成了極大的恐慌,老刑警劉巖辜王,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件劈狐,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡呐馆,警方通過(guò)查閱死者的電腦和手機(jī)肥缔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)汹来,“玉大人续膳,你說(shuō)我怎么就攤上這事改艇。” “怎么了坟岔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵谒兄,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我社付,道長(zhǎng)承疲,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任鸥咖,我火速辦了婚禮燕鸽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘扛或。我一直安慰自己绵咱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布熙兔。 她就那樣靜靜地躺著悲伶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪住涉。 梳的紋絲不亂的頭發(fā)上麸锉,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音舆声,去河邊找鬼花沉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛媳握,可吹牛的內(nèi)容都是我干的碱屁。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蛾找,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼娩脾!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起打毛,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤柿赊,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后幻枉,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體碰声,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年熬甫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了胰挑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖洽腺,靈堂內(nèi)的尸體忽然破棺而出脚粟,到底是詐尸還是另有隱情覆旱,我是刑警寧澤蘸朋,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站扣唱,受9級(jí)特大地震影響藕坯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜噪沙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一炼彪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧正歼,春花似錦辐马、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至萄唇,卻和暖如春檩帐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背另萤。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工湃密, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人四敞。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓泛源,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親忿危。 傳聞我的和親對(duì)象是個(gè)殘疾皇子达箍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容