天天看點

36、自定義控件詳解(一)-- 自定義屬性

一、自定義控件分類

1.1、原生控件拓展

修改原有控件我們隻需要建立一個類繼承該View(ViewGroup),再原有的邏輯上添加自己的實作即可。

a)文本框(TextView)預設是無法擷取焦點的,想讓它擷取焦點,我們可以通過自定義控件,并重寫isFocused來解決。

public class FocuseTextView extends TextView {
    public FocuseTextView(Context context) {
        this(context,null);
    }
    public FocuseTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    @ExportedProperty(category = "focus")
    public boolean isFocused() {
        //欺騙系統,誤以為textview得到了焦點
        return true;
    }
}      

b)繪制一個帶矩形邊框的TextView

public class MyTetView extends TextView {
    private Paint mPaint1;
    private Paint mpaint2;
    public MyTetView(Context context) {
        this(context,null);
    }

    public MyTetView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    private void initPaint() {
        mPaint1 = new Paint();
        mPaint1.setStyle(Paint.Style.FILL);
        mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
        
        mpaint2 = new Paint();
        mpaint2.setStyle(Paint.Style.FILL);
        mpaint2.setColor(Color.YELLOW);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        // 繪制外層矩形
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint1);
        // 繪制内層矩形
        canvas.drawRect(10, 10, getMeasuredWidth() - 10, getMeasuredHeight() - 10, mpaint2);
        canvas.save();
        super.onDraw(canvas);
        canvas.restore();
    }
}      

1.2、自定義組合控件

(1) 組合原生控件

在自定義組合控件時,我們并不需要自己去繪制視圖上顯示的内容,而隻是用系統原生的控件組合即可。

a) 我們首先制定好需要組合的控件,用它來達到我們想要的效果:

<RelativeLayout 
    android:id="@+id/rl_viewgroup"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView 
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="5dp"
        android:textSize="20sp"
        android:text="我是标題"/>
    <TextView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="5dp"
        android:layout_below="@id/tv_content"
        android:textColor="@android:color/darker_gray"
        android:text="我是未被選中的描述"/>
    <CheckBox 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:layout_marginRight="10dp"/>
</RelativeLayout>      

b) 在values目錄下建立attrs.xml檔案,并定義好屬性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name = "combinationView">
        <attr name = "title" format = "string"/>
        <attr name = "content" format = "string"/>
        <attr name = "focusable" format = "boolean"/>
    </declare-styleable>
</resources>      

c) 建立自定義控件類繼承自ViewGroup,并實作帶attrs的構造函數,再使用TypeArray來擷取屬性:

public class TextViewCheckBox extends RelativeLayout {

    private TextView mTvTitle,mTvContent;
    private CheckBox mCbClick;
    private String mTitle,mContentOn,mContentOff;
    
    public TextViewCheckBox(Context context) {
        this(context,null);
    }
    public TextViewCheckBox(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化布局和控件
        View view = View.inflate(context, R.layout.ui_text_checkbox, this);
        mTvTitle = (TextView) view.findViewById(R.id.tv_title);
        mTvContent = (TextView) view.findViewById(R.id.tv_content);
        mCbClick = (CheckBox) view.findViewById(R.id.cb_click);
        
        // 将attrs.xml中定義的所有屬性的值存儲到TypeArray中
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.combinationView);
        mTitle = array.getString(R.styleable.combinationView_title);
        mContentOn = array.getString(R.styleable.combinationView_content_on);
        mContentOff = array.getString(R.styleable.combinationView_content_off);
        array.recycle();
        
        // 初始化子控件描述和狀态
        if(mTitle != null){
            mTvTitle.setText(mTitle);
        }
        
        if(mContentOff != null){
            mTvContent.setText(mContentOff);
        }
    }
}      

d) 暴露方法給調用者來設定描述和狀态:

/**判斷是否被選中*/
public boolean isChecked(){
    return mCbClick.isChecked();
}

/**設定選中的狀态*/
public void setChecked(boolean isChecked){
    mCbClick.setChecked(isChecked);
    if(isChecked){
        mTvContent.setText(mContentOn);
    }else{
        mTvContent.setText(mContentOff);
    }
}      

e) 在布局中引用該控件,引入名稱空間,并設定自定義的屬性。

<cn.legend.review.TextViewCheckBox
    android:id="@+id/tvc_textchecked"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    review:title="我是标題"
    review:content_on = "控件被選中"
    review:content_off = "控件沒有選中"/>      

注意:在使用自定義控件時需要引入名稱空間:xmlns:review="http://schemas.android.com/apk/res/cn.legend.review"

如果想讓控件響應事件的話,則直接重寫事件即可,如果用到了wrap_content或match_parent則需要進行測量等操作。

1.3、自定義屬性種類

1. reference:參考某一資源ID。

(1)屬性定義:
        <declare-styleable name = "名稱">
            <attr name = "background" format = "reference" />
        </declare-styleable>
(2)屬性使用:
         <ImageView
            android:layout_width = "42dip"
            android:layout_height = "42dip"
            android:background = "@drawable/圖檔ID"/>      

2. color:顔色值

(1)屬性定義:
        <declare-styleable name = "名稱">
            <attr name = "textColor" format = "color" />
        </declare-styleable>
(2)屬性使用:
        <TextView
            android:layout_width = "42dip"
            android:layout_height = "42dip"
            android:textColor = "#00FF00"/>      

3. boolean:布爾值

1)屬性定義:
        <declare-styleable name = "名稱">
            <attr name = "focusable" format = "boolean" />
        </declare-styleable>
(2)屬性使用:
        <Button
            android:layout_width = "42dip"
            android:layout_height = "42dip"
            android:focusable = "true"/>      

4. dimension:尺寸值

(1)屬性定義:
        <declare-styleable name = "名稱">
            <attr name = "layout_width" format = "dimension" />
        </declare-styleable>
(2)屬性使用:
        <Button
            android:layout_width = "42dip"
            android:layout_height = "42dip"/>      

5. float:浮點值

(1)屬性定義:
        <declare-styleable name = "AlphaAnimation">
            <attr name = "fromAlpha" format = "float" />
            <attr name = "toAlpha" format = "float" />
        </declare-styleable>
(2)屬性使用:
        <alpha
           android:fromAlpha = "1.0"
           android:toAlpha = "0.7"/>      

6. integer:整型值

(1)屬性定義:
        <declare-styleable name = "AnimatedRotateDrawable">
            <attr name = "visible" />
            <attr name = "frameDuration" format="integer" />
            <attr name = "framesCount" format="integer" />
            <attr name = "pivotX" />
            <attr name = "pivotY" />
            <attr name = "drawable" />
        </declare-styleable>
(2)屬性使用:
        <animated-rotate
            xmlns:android = "http://schemas.android.com/apk/res/android" 
            android:drawable = "@drawable/圖檔ID" 
            android:pivotX = "50%" 
            android:pivotY = "50%" 
            android:framesCount = "12" 
            android:frameDuration = "100"/>      

7. string:字元串

(1)屬性定義:
        <declare-styleable name = "MapView">
            <attr name = "apiKey" format = "string" />
        </declare-styleable>
(2)屬性使用:
        <com.google.android.maps.MapView
            android:layout_width = "fill_parent"
            android:layout_height = "fill_parent"
            android:apiKey = "0jOkQ80oD1JL9C6HAja99uGXCRiS2CGjKO_bc_g"/>      

8. fraction:百分數

(1)屬性定義:
        <declare-styleable name="RotateDrawable">
            <attr name = "visible" />
            <attr name = "fromDegrees" format = "float" />
            <attr name = "toDegrees" format = "float" />
            <attr name = "pivotX" format = "fraction" />
            <attr name = "pivotY" format = "fraction" />
            <attr name = "drawable" />
        </declare-styleable>
(2)屬性使用:
        <rotate
            xmlns:android = "http://schemas.android.com/apk/res/android"
            android:interpolator = "@anim/動畫ID"
            android:fromDegrees = "0"    android:toDegrees = "360"
            android:pivotX = "200%"        android:pivotY = "300%"
            android:duration = "5000"    android:repeatMode = "restart"
            android:repeatCount = "infinite"/>      

9. enum:枚舉值

(1)屬性定義:
        <declare-styleable name="名稱">
            <attr name="orientation">
                  <enum name="horizontal" value="0" />
                  <enum name="vertical" value="1" />
            </attr>           
        </declare-styleable>
(2)屬性使用:
        <LinearLayout
            xmlns:android = "http://schemas.android.com/apk/res/android"
            android:orientation = "vertical"
            android:layout_width = "fill_parent"
            android:layout_height = "fill_parent"/>
        </LinearLayout>      

10. 位或運算

(1)屬性定義:
        <declare-styleable name="名稱">
            <attr name="windowSoftInputMode">
                    <flag name = "stateUnspecified" value = "0" />
                    <flag name = "stateUnchanged" value = "1" />
                    <flag name = "stateHidden" value = "2" />
                    <flag name = "stateAlwaysHidden" value = "3" />
                    <flag name = "stateVisible" value = "4" />
                    <flag name = "stateAlwaysVisible" value = "5" />
                    <flag name = "adjustUnspecified" value = "0x00" />
                    <flag name = "adjustResize" value = "0x10" />
                    <flag name = "adjustPan" value = "0x20" />
                    <flag name = "adjustNothing" value = "0x30" />
             </attr>        
        </declare-styleable>
(2)屬性使用:
       <activity
             android:name = ".StyleAndThemeActivity"
             android:label = "@string/app_name"
             android:windowSoftInputMode = "stateUnspecified | stateUnchanged | stateHidden">
             <intent-filter>
                  <action android:name = "android.intent.action.MAIN" />
                  <category android:name = "android.intent.category.LAUNCHER" />
             </intent-filter>
        </activity>      

注意:屬性定義時可以指定多種類型值,使用“|”進行隔離多種類型。

(1)屬性定義:
        <declare-styleable name = "名稱">
            <attr name = "background" format = "reference|color" />
        </declare-styleable>
(2)屬性使用:
         <ImageView
            android:layout_width = "42dip"
            android:layout_height = "42dip"
            android:background = "@drawable/圖檔ID|#00FF00"/>      

1.4、自定義屬性用法

在我們自定義的View中有需要自定義的屬性,則需要在values下建立attrs.xml,在其中定義你的屬性。

a)在res/values檔案下定義一個attrs.xml檔案,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ToolBar">
        <attr name="buttonNum" format="integer"/>
        <attr name="itemBackground" format="reference|color"/>
    </declare-styleable>
</resources>      

b)在布局xml中如下使用該屬性:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:toolbar="http://schemas.android.com/apk/res/cn.zzm.toolbar"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    <cn.zzm.toolbar.ToolBar
        android:id="@+id/gridview_toolbar"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="@drawable/control_bar"
        android:gravity="center"
        toolbar:buttonNum="5"
        toolbar:itemBackground="@drawable/control_bar_item_bg" />
</RelativeLayout>      

2、兩種擷取方式

我們可以在自定義View的構造函數中擷取屬性的值,有以下兩種方式:

第一種擷取方式:

TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.ToolBar);
int buttonNum = array.getInt(R.styleable.ToolBar_buttonNum, 5);
int itemBg = array.getResourceId(R.styleable.ToolBar_itemBackground, -1);
array.recycle();      

第二種擷取方式:

TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.ToolBar);
int count = array.getIndexCount();
for (int i = 0; i < count; i++) {
    int attr = array.getIndex(i);
    switch (attr) {
    case R.styleable.ToolBar_buttonNum:
        int buttonNum = array.getInt(attr, 5);
        break;
    case R.styleable.ToolBar_itemBackground:
        int itemBg = array.getResourceId(attr, -1);
        break;
    }
}      

3、使用要點

屬性類型:string、integer、dimension、reference、color、enum。

下面我看下使用自定義屬性需要注意的一些地方,首先看下attrs.xml檔案前面幾種聲明方式都是一緻的,例如:

<attr name="buttonNum" format="integer"/>      

隻有enum是不同的,用法舉例:

<attr name="testEnum">
<enum name="fill_parent" value="-1"/>
<enum name="wrap_content" value="-2"/>
</attr>      

如果該屬性可同時傳兩種不同的屬性,則可以用“|”分割開即可。讓我們再看看布局xml中需要注意的事項,在自定義元件的構造函數中使用:

TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.ToolBar);      

獲得對屬性集的引用,然後就可以用“array”的各種方法來擷取相應的屬性值。這裡需要注意的是,如果使用的方法和擷取值的類型不對的話,則會傳回預設值。

是以,如果一個屬性是帶兩個及以上不用類型的屬性,需要做多次判斷,直到讀取完畢後才能判斷應該賦予何值。

1.5、自定義View示範

編寫簡單驗證碼程式:

a) 自定義View的屬性,首先在res/value/下建立attrs.xml,裡面定義我們的屬性和聲明我們的整個樣式

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="titleText" format="string" />
    <attr name="titleTextColor" format="color" />
    <attr name="titleTextSize" format="dimension" />
    <declare-styleable name="CustomTitleView">
        <attr name="titleText" />
        <attr name="titleTextColor" />
        <attr name="titleTextSize" />
    </declare-styleable>
</resources>      

b) 我們定義字型、字型顔色、字型大小等3個屬性,foemat是指該屬性的取值類型。

然後在布局檔案中聲明我們自定義的View,注意:自定義屬性需要引入名稱空間

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res/cn.legend.demo"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="cn.legend.demo.MainActivity" >
    
    <cn.legend.demo.CustomTitleView 
        android:layout_width="200dp"
        android:layout_height="100dp"
        custom:titleText="3721"
        custom:titleTextColor = "#ff0000"
        custom:titleTextSize="40sp"/>
</RelativeLayout>      

c) 由于自定義屬性,我們的View必須實作帶attr的構造方法:

public class CustomTitleView extends View {
    private String mCustomText;
    private int mCustomTextColor;
    private int mCustomTextSize;
    private Rect mRect;
    private Paint mPaint;
    public CustomTitleView(Context context) {
        this(context, null);
    }
    public CustomTitleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomTitleView);
        mCustomText = array.getString(R.styleable.CustomTitleView_titleText);
        mCustomTextColor = array.getColor(R.styleable.CustomTitleView_titleTextColor, Color.BLACK);
        mCustomTextSize = (int) array.getDimension(R.styleable.CustomTitleView_titleTextSize, 16);
        array.recycle();
        
        initPaint();
    }
    private void initPaint() {
        mPaint = new Paint();
        mPaint.setTextSize(mCustomTextSize);
        mPaint.setColor(mCustomTextColor);
        
        // 文字的骨骼
        mRect = new Rect();
        mPaint.getTextBounds(mCustomText, 0, mCustomText.length(), mRect);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 畫背景
        mPaint.setColor(Color.YELLOW);
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
        
        mPaint.setColor(mCustomTextColor);
        canvas.drawText(mCustomText, getWidth() / 2 - mRect.width() /2, getHeight() / 2 + mRect.height() / 2, mPaint);
    }
}      

運作效果:

36、自定義控件詳解(一)-- 自定義屬性

d) 當我們将布局檔案中的寬和高為明确的值時,系統測量的結果就是我們設定的結果,如果wrap_content或match_parent時,

系統預設會讓寬和高和窗體同寬高,因為系統根本不知道我們的控件的具體大小,我們可以通過重寫onMeasure方法

重寫之前先了解MeasureSpec的specMode有三種類型:

  • EXACTLY:一般是設定了明确的值或者是MATCH_PARENT
  • AT_MOST:表示子布局限制在一個最大值内,一般為WARP_CONTENT
  • UNSPECIFIED:表示子布局想要多大就多大,很少使用

e) 測量是比較複雜的,我們來測量下該控件:

protected 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,height;
    
    if(widthMode == MeasureSpec.EXACTLY){// 明确的值或match_parent
        width = widthSize;
    }else{
        mPaint.setTextSize(mCustomTextSize);
        mPaint.getTextBounds(mCustomText, 0, mCustomText.length(), mRect);
        int textWidth = mRect.width();
        // 期望的值,左右邊距加上文本的寬度
        width = getPaddingLeft() + textWidth + getPaddingRight();
    }
    
    if(heightMode == MeasureSpec.EXACTLY){
        height = heightSize;
    }else{
        mPaint.setTextSize(mCustomTextSize);
        mPaint.getTextBounds(mCustomText, 0, mCustomText.length(), mRect);
        int textHeight = mRect.height();
        // 期望的值,上下邊距加上文本的高度
        height = getPaddingTop() + textHeight + getPaddingBottom();
    }
    setMeasuredDimension(width, height);
}      

由于我們讓寬度和高度分别是左邊距 + 右邊距 + 文本寬度 和 上邊距 + 下邊距 + 文本高度,是以我們布局必須加入padding值:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res/cn.legend.demo"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="cn.legend.demo.MainActivity" >
    
    <cn.legend.demo.CustomTitleView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        custom:titleText="3721"
        android:padding="10dp"
        custom:titleTextColor = "#ff0000"
        custom:titleTextSize="40sp"/>
</RelativeLayout>      
36、自定義控件詳解(一)-- 自定義屬性

e) 接下來給該控件加入事件

this.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        // 生成四位随機數
        Random random = new Random();
        Set<Integer> set = new HashSet<Integer>();
        while(set.size() < 4){
            int randomInt = random.nextInt(10);
            set.add(randomInt);
        }
        StringBuffer sb = new StringBuffer();
        for (Integer integer : set) {
            sb.append("" + integer);
        }
        mCustomText = sb.toString();
        // 重繪
        postInvalidate();
    }
});      

##############################################待續

轉載于:https://www.cnblogs.com/pengjingya/p/5510217.html