自定義View的簡單流程

自定義View的步驟

1、自定義屬性

1)分析自定義的View中需要哪些的自定義屬性,在res/values/attrs.xml中聲明自定義屬性,如下所示:
res/values/attrs.xml

<resources>
    // CustomTitleView的自定義屬性
    <attr name="customTitleText" format="string"/>
    <attr name="customTitleTextSize" format="dimension"/>
    <attr name="customTitleTextColor" format="color"/>
    <declare-styleable name="CustomTitleView">
        <attr name="customTitleText"/>
        <attr name="customTitleTextSize"/>
        <attr name="customTitleTextColor"/>
    </declare-styleable>
</resources>

2)在自定義的View的構造方法中獲取自定義屬性。

public CustomTitleView(Context context) {
    this(context, null);
}

public CustomTitleView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    // 獲取自定義樣式屬性
    TypedArray a = context.getTheme()
            .obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyleAttr, 0);
    int n = a.getIndexCount();
    for (int i = 0; i < n; i++) {
        int attr = a.getIndex(i);
        switch (attr) {
            case R.styleable.CustomTitleView_customTitleText:
                mCustomTitleText = a.getString(attr);
                break;
            // 設置默認字體大小為16sp,TypeValue也可以把sp轉化為px。
            case R.styleable.CustomTitleView_customTitleTextSize:
                mCustomTitleTextSize = a.getDimensionPixelSize(attr,
                        (int) TypedValue.applyDimension(
                                TypedValue.COMPLEX_UNIT_SP,
                                16,
                                getResources().getDisplayMetrics()));
                break;
            // 設置默認字體顏色為黑色
            case R.styleable.CustomTitleView_customTitleTextColor:
                mCustomTitleTextColor = a.getColor(attr, Color.BLACK);
                break;
        }
    }
    // 最后記得釋放資源
    a.recycle();
}

在obtainStyledAttributes()中有四個參數,最后兩個參數分別是defStyleAttr和defStyleRes。defStyleAttr指定的是在Theme style中定義的一個attr;而defStyleRes是自己指定的一個style,當且僅當defStyleAttr為0或者在Theme中找不到defStyleAttr指定的屬性時才會生效。屬性指定的優先級優大概是:xml>style>defStyleAttr>defStyleRes>Theme指定,當defStyleAttr為0時,就跳過defStyleAttr指定的reference,所以一般用0就能滿足一些基本開發。
3)在XML布局文件中使用自定義View時設置自定義屬性。

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.jun.androidexample.customview.CustomTitleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:padding="12dp"
        custom:customTitleText="8745"
        custom:customTitleTextColor="#ff0000"
        custom:customTitleTextSize="30sp"/>
</RelativeLayout>
2、onMesure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
    /**
     * 計算View的寬度
     */
    // 獲取widthMeasureSpec的Mode和Size
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    // View的最終寬度
    int width;
    // 如果MeasureSpec的Mode是EXACTLY則最終的值直接等于MeasureSpec的Size
    if (widthMode == MeasureSpec.EXACTLY) {
        width = widthSize;
    } else { // 如果MeasureSpec的Mode是AT_MOST或UNSPECIFIED則需要測量其實際的值
        // 實際測量的值
        int desired = ...;
        // 如果MeasureSpec的Mode是AT_MOST則最大不能超過MeasureSpec的Size
        if (widthMode == MeasureSpec.AT_MOST) {
            // 所以最終的值等于MeasureSpec的Size和實際測量的值中的小的
            width = Math.min(widthSize, desired);
        } else {
            // 如果MeasureSpec的Mode是UNSPECIFIED則最終的值直接等于實際測量的值
            width = desired;
        }
    }

    /**
     * 計算View的高度
     */
    // 獲取heightMeasureSpec的Mode和Size
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    // View的最終高度
    int height;
    // 如果MeasureSpec的Mode是EXACTLY則最終的值直接等于MeasureSpec的Size
    if (heightMode == MeasureSpec.EXACTLY) {
        height = heightSize;
    } else { // 如果MeasureSpec的Mode是AT_MOST或UNSPECIFIED則需要測量其實際的值
        // 實際測量的值
        int desired = ...;
        // 如果MeasureSpec的Mode是AT_MOST則最大不能超過MeasureSpec的Size
        if (heightMode == MeasureSpec.AT_MOST) {
            // 所以最終的值等于MeasureSpec的Size和實際測量的值中的小的
            height = Math.min(heightSize, desired);
        } else {
            // 如果MeasureSpec的Mode是UNSPECIFIED則最終的值直接等于實際測量的值
            height = desired;
        }
    }

    // 設置測量的寬度和高度
    setMeasuredDimension(width, height);
}

int型的widthMeasureSpec和heightMeasureSpec,在MeasureSpce中(在java中int型由4個字節(32bit)組成)前2位表示mode,后30位表示size。MeasureSpec的mode一共有三種類型:
1)EXACTLY:一般是android:layout_width或android:layout_height屬性設置了明確的值或者是match_parent時,表示子View的大小就是MeasureSpec的size。
2)AT_MOST:一般是android:layout_width或android:layout_height屬性設置成wrap_content,表示子View最大不超過MeasureSpec的size。
3)UNSPECIFIED:表示子View的大小不受限制也就是等于它實際測量的大小。

3、onDraw()
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // 使用Canvas繪制View
    ...
}

根據實際的需求繪制View的形狀。

4、onTouchEvent()
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            ...
            break;
        case MotionEvent.ACTION_MOVE:
            ...
            break;
        case MotionEvent.ACTION_UP:
            ...
            break;
    }
    return true;
}

如果自定義的View需要自己處理特定的觸摸事件,就需要重寫onTouchEvent()。

示例:點擊隨機改變數字

效果
效果圖
實現

res/values/attrs.xml

<resources>
    // CustomTitleView的自定義屬性
    <attr name="customTitleText" format="string"/>
    <attr name="customTitleTextSize" format="dimension"/>
    <attr name="customTitleTextColor" format="color"/>
    <declare-styleable name="CustomTitleView">
        <attr name="customTitleText"/>
        <attr name="customTitleTextSize"/>
        <attr name="customTitleTextColor"/>
    </declare-styleable>
</resources>

CustomTitleView.java

/**
 * 自定義View
 */
public class CustomTitleView extends View {

    // 聲明自定義屬性
    private String mCustomTitleText;
    private float mCustomTitleTextSize;
    private int mCustomTitleTextColor;

    private Paint mPaint;
    private Rect mBound;

    public CustomTitleView(Context context) {
        this(context, null);
    }

    public CustomTitleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // 獲取自定義樣式屬性
        TypedArray a = context.getTheme()
                .obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyleAttr, 0);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.CustomTitleView_customTitleText:
                    mCustomTitleText = a.getString(attr);
                    break;
                // 設置默認字體大小為16sp,TypeValue也可以把sp轉化為px
                case R.styleable.CustomTitleView_customTitleTextSize:
                    mCustomTitleTextSize = a.getDimensionPixelSize(attr,
                            (int) TypedValue.applyDimension(
                                    TypedValue.COMPLEX_UNIT_SP,
                                    16,
                                    getResources().getDisplayMetrics()));
                    break;
                // 設置默認字體顏色為黑色
                case R.styleable.CustomTitleView_customTitleTextColor:
                    mCustomTitleTextColor = a.getColor(attr, Color.BLACK);
                    break;
            }
        }
        // 最后記得釋放資源
        a.recycle();

        // 初始化Paint和Rect
        mPaint = new Paint();
        mBound = new Rect();
    }

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

        // 獲取MeasureSpec的Mode和Size
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        // 聲明最終的值
        int width;
        int height;

        // 如果MeasureSpec的Mode是EXACTLY則最終的值直接等于MeasureSpec的Size
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else { // 如果MeasureSpec的Mode是AT_MOST或UNSPECIFIED則需要測量其實際的值
            mPaint.setTextSize(mCustomTitleTextSize);
            mPaint.getTextBounds(mCustomTitleText, 0, mCustomTitleText.length(), mBound);
            float textWidth = mBound.width();
            int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());

            if (widthMode == MeasureSpec.AT_MOST) {
                // 如果MeasureSpec的Mode是AT_MOST則最大不能超過MeasureSpec的Size
                // 所以最終的值等于MeasureSpec的Size和實際測量的值中的小的
                width = Math.min(widthSize, desired);
            } else {
                // 如果MeasureSpec的Mode是UNSPECIFIED則最終的值直接等于實際測量的值
                width = desired;
            }
        }

        // 如果MeasureSpec的Mode是EXACTLY則最終的值直接等于MeasureSpec的Size
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else { // 如果MeasureSpec的Mode是AT_MOST或UNSPECIFIED則需要測量其實際的值
            mPaint.setTextSize(mCustomTitleTextSize);
            mPaint.getTextBounds(mCustomTitleText, 0, mCustomTitleText.length(), mBound);
            float textHeight = mBound.height();
            int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());

            if (heightMode == MeasureSpec.AT_MOST) {
                // 如果MeasureSpec的Mode是AT_MOST則最大不能超過MeasureSpec的Size
                // 所以最終的值等于MeasureSpec的Size和實際測量的值中的小的
                height = Math.min(heightSize, desired);
            } else {
                // 如果MeasureSpec的Mode是UNSPECIFIED則最終的值直接等于實際測量的值
                height = desired;
            }
        }

        // 設置測量的寬高
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 繪制一個藍色矩形的背景
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);

        // 設置字體顏色
        mPaint.setColor(mCustomTitleTextColor);
        // 設置字體大小
        mPaint.setTextSize(mCustomTitleTextSize);
        // 獲取文字的邊界
        mPaint.getTextBounds(mCustomTitleText, 0, mCustomTitleText.length(), mBound);
        // 繪制文字
        canvas.drawText(mCustomTitleText,
                getWidth() / 2 - mBound.width() / 2,
                getHeight() / 2 + mBound.height() / 2,
                mPaint);
    }

    // 觸摸事件處理
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 隨機數Text
                mCustomTitleText = randomText();
                // 重繪
                invalidate();
                break;
        }
        return true;
    }

    // 隨機數Text
    private String randomText() {
        // 隨機數
        Random random = new Random();
        // 要求數字不能相同
        Set<Integer> set = new HashSet<>();

        // 循環4次
        while (set.size() < 4) {
            // [0,10)中的隨機數
            int randomInt = random.nextInt(10);
            set.add(randomInt);
        }

        // 將數字串連在一起
        StringBuffer sb = new StringBuffer();
        for (Integer i : set) {
            sb.append("" + i);
        }

        return sb.toString();
    }
}

其中,invalidate()在UI線程中調用,postInvalidate()在非UI線程中調用,都是重繪的意思就是會重新調用View的onDraw()方法。

activity_main.xml

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.jun.androidexample.customview.CustomTitleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:padding="12dp"
        custom:customTitleText="8745"
        custom:customTitleTextColor="#ff0000"
        custom:customTitleTextSize="30sp"/>
</RelativeLayout>

MianActivity.java

public class MianActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

參考

Android 自定義View (一)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容