自定義view的基本流程

有時(shí)候我們需要一些特殊的效果或者功能,而系統(tǒng)控件無法滿足我們的需求,這時(shí)候就需要自己定義一個(gè)控件。

自定義view流程

繼承View

要自定義View首先需要需要繼承View或者其子類,如果需要實(shí)現(xiàn)的效果比較復(fù)雜,通常需要繼承View,有時(shí)候我們需要的是系統(tǒng)的控件再加上一些特殊的效果則可以繼承View的子類(如TextView)

如果是要自己設(shè)計(jì)一種布局或者要組合其他控件,這時(shí)候就需要繼承ViewGroup或者LinearLayout、FrameLayout等系統(tǒng)自帶的布局

重寫構(gòu)造方法

自定義View至少需要重寫兩個(gè)構(gòu)造方法

  • 在java代碼中直接創(chuàng)建控件所用的構(gòu)造方法
public CustomView(Context context) {
        this(context, null);
}
  • 在xml文件中定義View所用的構(gòu)造函數(shù)
public CustomView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
}

public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
}

自定義xml中的屬性

首先需要新建res/values/custom_view_attrs.xml,并在里面聲明如下

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomView">
        <attr name="custom_width" format="dimension" />
    </declare-styleable>
</resources>

然后就可以在xml布局文件中聲明了

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="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="cn.lkllkllkl.customview.MainActivity">

    <cn.lkllkllkl.customview.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:custom_width="100dp"/>

</LinearLayout>

接著我們就可以在自定義View的構(gòu)造方法中將該屬性的值取出使用了

public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        float customWidth = 0;
        TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
        customWidth = typeArray.getDimension(R.styleable.CustomView_custom_width, 100);
        // 需要記得回收
        typeArray.recycle();
    }

onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);    

MeasureSpec

這里要明白o(hù)nMeasure的兩個(gè)參數(shù)首先需要明白MeasureSpec這個(gè)類。

MeasureSpec通過將SpecMode(模式)和SpecSize(具體尺寸)打包成一個(gè)int(即widthMeasureSpec、heightMeasureSpec),前2位表示SpecMode,后30位表示尺寸。

SpecMode有三類

  • UNSPECIFIED: 父容器對View的大小沒有限制,一般我們自定義View用的較少

  • EXACTLY: 在xml文件中設(shè)置具體大小為多少dp,或者設(shè)置為match_parent則SpecMode為EXACTLY,此模式表示則直接使用SpecSize作為自定義View的尺寸

  • AT_MOST: 對應(yīng)xml文件中的wrap_content,表示設(shè)置的尺寸大小不能超過SpecSize

通常我們需要view支持wrap_content的時(shí)候才需要重寫onMeasure,如果不重寫則wrap_content效果和match_parent一樣。一般使用如下代碼就夠了

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

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        // dp轉(zhuǎn)換成px, 事先定義的默認(rèn)寬高單位為dp
        int defaultWidth = dp2px(getContext(), DEFAULT_WIDTH);
        int defaultHeight = dp2px(getContext(), DEFAULT_HEIGHT);

        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            widthSize = Math.min(defaultWidth, widthSize);
            heightSize = Math.min(defaultHeight, heightSize);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            widthSize = Math.min(defaultWidth, widthSize);
        } else if (heightMode == MeasureSpec.AT_MOST){
            heightSize = Math.min(defaultHeight, heightSize);
        }
        
        setMeasuredDimension(widthSize, heightSize);
    }

一般為了適配不同屏幕需要使用dp為單位,而setMeasuredDimension是使用px為單位的,所以上面的代碼將我們設(shè)置的默認(rèn)寬高轉(zhuǎn)換成px,轉(zhuǎn)換方法如下

public static int dp2px(Context context, float dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                dp, context.getResources().getDisplayMetrics());
    }

onLayout方法

一般自定義ViewGroup才需要重寫該方法對子View進(jìn)行排放

onDraw方法

通過onDraw方法我們可以將view繪制到屏幕上,如果要讓view支持padding屬性則需要在onDraw中做處理

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /* mPaint為成員變量,畫筆Paint的對象,
         * 在構(gòu)造函數(shù)中進(jìn)行初始化,可以設(shè)置一些在canvas上繪制的通用屬性
         */
        mPaint.setColor(Color.RED);
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        //在View所占區(qū)域繪制一條對角線
        canvas.drawLine(paddingLeft, paddingTop,
                getWidth() - paddingRight, getHeight() - paddingBottom, mPaint);
    }

注意事項(xiàng)

  • onMeasure、onLayout、onDraw三個(gè)方法可能會(huì)對此調(diào)用,特別是onDraw方法,所以最好不要在這些方法中創(chuàng)建對象避免頻繁分配內(nèi)存造成內(nèi)存抖動(dòng),或者做一些耗時(shí)操作導(dǎo)致跳幀

  • View中有提供post系列方法,所以不需要在View中使用Handler

  • View中若有創(chuàng)建線程或者動(dòng)畫需要及時(shí)停止,View#onDetachedFromWindow就是一個(gè)很好的時(shí)機(jī)

  • View中若有滑動(dòng)嵌套的情形,需要處理好滑動(dòng)沖突

reference

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容