Android自定義控件View(一)


雖然Android API給我們提供了眾多控件View來使用,但是鑒於Android的開發性,自然少不了根據需求自定義控件View了。比如說QQ頭像是圓形的,但是縱觀整個Android控件也找不到一個加載圓形圖片的Button或者ImageView,那么咋辦?廢話,肯定是自定義一個圓形RoundImageView控件啦!這里我們可以繼承ImageView重寫里面的方法來實現這一效果。還有一種自定義控件是繼承View重寫里面的onDraw()方法,這類自定義View需要定義自己的屬性以備在xml布局文件中使用。

自定義View的步驟

  1. 自定義View的屬性
  2. 在自定義View的構造方法中獲得View屬性值
  3. 重寫onMeasure(int,int)方法。(該方法可重寫可不重寫,具體看需求)
  4. 重寫onDraw(Canvas canvas)方法。
  5. 在xml布局文件中如何使用自定義view的屬性?

自定義View的屬性

在res/values下面新建attrs.xml屬性文件。我們看看atrrs.xml文件怎么寫?

<?xml version="1.0" encoding="utf-8"?> <resources> <!--name 是自定義屬性名,一般采用駝峰命名,可以隨意。 format 是屬性的單位--> <attr name="titleSize" format="dimension"></attr> <attr name="titleText" format="string"></attr> <attr name="titleColor" format="color"></attr> <attr name="titleBackgroundColor" format="color"></attr> <!--name 是自定義控件的類名--> <declare-styleable name="MyCustomView"> <attr name="titleSize"></attr> <attr name="titleText"></attr> <attr name="titleColor"></attr> <attr name="titleBackgroundColor"></attr> </declare-styleable> </resources>

自定義屬性分兩步:

  1. 定義公共屬性
  2. 定義控件的主題樣式

如上面的xml文件第一部分是公共的屬性,第二部分是自定義控件MyCustomView的主題樣式,該主題樣式里的屬性必須包含在公共屬性里面。言外之意就是公共屬性可以被多個自定義控件主題樣式使用。有些人可能會糾結format字段后面都有哪些屬性單位?如果你是使用AS開發的話IDE會自動有提示,基本包括如下:
dimension(字體大小)string(字符串)color(顏色)boolean(布爾類型)float(浮點型)integer(整型)enmu(枚舉)fraction(百分比)等。不了解的可以百度一把。

獲得View屬性值

自定義View一般需要實現一下三個構造方法

public MyCustomView(Context context) { this(context, null); } public MyCustomView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }

從代碼中不難看出,這三個構造方法是層調用一層的,是個遞進關系,因此,我們只需要在最后一個構造方法中來獲得View的屬性了。看代碼:

 public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr); final Resources.Theme theme = context.getTheme(); TypedArray a = theme.obtainStyledAttributes(attrs, R.styleable.MyCustomView, defStyleAttr, 0); if (null != a) { int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.MyCustomView_titleColor: titleColor = a.getColor(attr, Color.BLACK); break; case R.styleable.MyCustomView_titleSize: titleSize = a.getDimensionPixelSize(attr, titleSize); break; case R.styleable.MyCustomView_titleText: titleText = a.getString(attr); break; case R.styleable.MyCustomView_titleBackgroundColor: titleBackgroundColor = a.getColor(attr, Color.WHITE); break; } } a.recycle(); init(); } }

第一步通過theme.obtainStyledAttributes()方法獲得自定義控件的主題樣式數組。第二步就是遍歷每個屬性來獲得對應屬性的值,也就是我們在xml布局文件中寫的屬性值。注意:在分支case里R.styleable.后面的屬性名稱有一個規則:控件的樣式主題名 +“_”+ 屬性名,循環結束之后記得調用a.recycle()回收資源。至此就獲得了自定義控件的屬性值了。至於為什么這樣來獲得屬性值?具體可以參考Android 系統的TextView源碼里的構造方法。

重寫onDraw()方法來繪制View控件

這一步進行的操作是將你需要顯示的控件View的內容繪制到畫布Canvas上面。例如我們在一個圓里面寫字,先來效果圖

這里寫圖片描述

onDraw方法實現如下:

.............
 /** * 初始化 */ private void init() { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setTextSize(titleSize); /** * 得到自定義View的titleText內容的寬和高 */ mBound = new Rect(); mPaint.getTextBounds(titleText, 0, titleText.length(), mBound); } ................ @Override protected void onDraw(Canvas canvas) { mPaint.setColor(titleBackgroundColor); canvas.drawCircle(getWidth() / 2f, getWidth() / 2f, getWidth() / 2f, mPaint); mPaint.setColor(titleColor); canvas.drawText(titleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint); }

先new一個Paint實例初始化畫筆,給畫筆設置文字大小,然后先給畫筆設置一個背景顏色,在畫一個圓,再次設置畫筆的文字顏色,在繪制字符串到畫布,最后就得到如上圖片的效果了。

布局中使用自定義View

使用自定義View控件需要在根布局中添加xmlns:custom=”http://schemas.android.com/apk/res-auto”命名空間。其中前綴名:”custom” 也是自定義的,可以是除了被Android系統使用過的字眼以外的任何字符串,自然你這里了也可以寫成“myCustom”。不知道在Android哪個版本之前命名控件是這樣應用的xmlns:custom=”http://schemas.android.com/apk/res/com.xjp.customview。res/后面的是自定義控件所在的包名。當然只要你代碼不報錯兩種命名空間都是可以的。只是我用的AS開發,然后targetSdkVersion是21,因此我用的是第一種命名空間。

代碼如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.xjp.customview.MyCustomView android:layout_width="wrap_content" android:layout_height="match_parent" custom:titleColor="@android:color/black" custom:titleSize="25sp" custom:titleBackgroundColor="#ff0000" custom:titleText="自定義的View" /> </RelativeLayout>

從上面的代碼你會發現,凡是自定義的屬性使用時候的前綴是命名空間名稱 custom。

至此,整個自定義View的流程就跑通了。貼出整個代碼部分如下:

package com.xjp.customview; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; /** * Description:自定義控件View * User: xjp * Date: 2015/5/27 * Time: 14:50 */ public class MyCustomView extends View { private static final String TAG = "MyCustomView"; private static final boolean DEBUG = false; private String titleText = "Hello world"; private int titleColor = Color.BLACK; private int titleBackgroundColor = Color.WHITE; private int titleSize = 16; private Paint mPaint; private Rect mBound; public MyCustomView(Context context) { this(context, null); } public MyCustomView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); final Resources.Theme theme = context.getTheme(); TypedArray a = theme.obtainStyledAttributes(attrs, R.styleable.MyCustomView, defStyleAttr, 0); if (null != a) { int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.MyCustomView_titleColor: titleColor = a.getColor(attr, Color.BLACK); break; case R.styleable.MyCustomView_titleSize: titleSize = a.getDimensionPixelSize(attr, titleSize); break; case R.styleable.MyCustomView_titleText: titleText = a.getString(attr); break; case R.styleable.MyCustomView_titleBackgroundColor: titleBackgroundColor = a.getColor(attr, Color.WHITE); break; } } a.recycle(); init(); } } /** * 初始化 */ private void init() { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setTextSize(titleSize); /** * 得到自定義View的titleText內容的寬和高 */ mBound = new Rect(); mPaint.getTextBounds(titleText, 0, titleText.length(), mBound); } @Override protected void onDraw(Canvas canvas) { mPaint.setColor(titleBackgroundColor); canvas.drawCircle(getWidth() / 2f, getWidth() / 2f, getWidth() / 2f, mPaint); mPaint.setColor(titleColor); canvas.drawText(titleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint); } } 

運行結果圖:
這里寫圖片描述
細心的你會發現,跟我們上面預期的效果圖有不一樣啊?怎么回事?布局大小的問題?

android:layout_width="wrap_content" android:layout_height="match_parent"

從布局大小來看寬度應該包裹內容,但是卻充滿了整個屏幕。接下來我們就要想到其實我們在自定義View的流程中還有一個onMeasure方法沒有重寫。

重寫onMeasure控制View大小

當你沒有重寫onMeasure方法時候,系統調用默認的onMeasure方法。

 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); }

這個方法的作用是:測量控件的大小。其實Android系統在加載布局的時候是由系統測量各子View的大小來告訴父View我需要占多大空間,然后父View會根據自己的大小來決定分配多大空間給子View。那從上面的效果來看:當你在布局中設置View的大小為”wrap_content”時,其實系統測量出來的大小是“match_parent”。為什么會是這樣子呢?那得從MeasureSpec的specMode模式說起了。一共有三種模式:

  1. MeasureSpec.EXACTLY:父視圖希望子視圖的大小是specSize中指定的大小;一般是設置了明確的值或者是MATCH_PARENT
  2. MeasureSpec.AT_MOST:子視圖的大小最多是specSize中的大小;表示子布局限制在一個最大值內,一般為WARP_CONTENT
  3. MeasureSpec.UNSPECIFIED:父視圖不對子視圖施加任何限制,子視圖可以得到任意想要的大小;表示子布局想要多大就多大,很少使用。

我們跳進源碼看看系統默認的 super.onMeasure(widthMeasureSpec, heightMeasureSpec);是怎么實現的

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

從上面的代碼getDefaultSize()方法中看出,原來MeasureSpec.AT_MOST和MeasureSpec.EXACTLY走的是同一個分支,也就是父視圖希望子視圖的大小是specSize中指定的大小。

int specSize = MeasureSpec.getSize(measureSpec);

得出來的默認值就是填充整個父布局。因此,不管你布局大小是”wrap_content”還是“match_parent”效果都是充滿整個父布局。那我想要”wrap_content”的效果怎么辦?不着急,只有重寫onMeasure方法了。

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { /** * 測量模式 */ int widthMode = MeasureSpec.getMode(widthMeasureSpec); /** * 父布局希望子布局的大小,如果布局里面設置的是固定值,這里取布局里面的固定值和父布局大小值中的最小值. * 如果設置的是match_parent,則取父布局的大小 */ int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (DEBUG) Log.e(TAG, "the widthSize:" + widthSize + " the heightSize" + heightSize); int width; int height; Rect mBounds = new Rect(); if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else { mPaint.setTextSize(titleSize); mPaint.getTextBounds(titleText, 0, titleText.length(), mBounds); float textWidth = mBounds.width(); int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight()); width = desired; } if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { height = width; } /** * 最后調用父類方法,把View的大小告訴父布局。 */ setMeasuredDimension(width, height); }

這樣就可以實現第一張圖片的效果了。解釋都在代碼里了。

源碼下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM