自定義View5---完整的自定義View

移步自定義View系列

  • 1.自定義view的分類
    • 自定義單一view(不含子view)
      • 繼承view
      • 繼承特定view如textview
    • 自定義viewGroup(含子view)
      • 繼承viewGroup
      • 繼承特定的viewGroup如LinearLayout
  • 2.使用注意點
    • 支持特殊屬性
      • wrap_content
      • padding
      • margin
    • 多線程直接使用post方式
      • 避免使用handler等其他方式
    • 避免內(nèi)存泄露
      • 線程/動畫要及時停止
    • 處理好滑動沖突
      • view帶有滑動嵌套情況
  • 3.具體實例
    • 實現(xiàn)基本自定義view
    • 支持wrap_content屬性
    • 支持padding屬性
    • 提供自定義屬性

1 自定義View的分類

自定義View一共分為兩大類,具體如下圖:


image

2 具體介紹 & 使用場景

image

3 使用注意點

image

3.1 支持特殊屬性--wrap_content

  • 支持wrap_content
    如果不在onMeasure()中對wrap_content作特殊處理,那么wrap_content屬性將失效
    自定義View2---View Measure過程
  • 在onMeasure()中的getDefaultSize()的默認實現(xiàn)中,當View的測量模式是AT_MOST或EXACTLY時,View的大小都會被設置成子View MeasureSpec的specSize。
  • 因為AT_MOST對應wrap_content;EXACTLY對應match_parent,所以,默認情況下,wrap_content和match_parent是具有相同的效果的。
  • 在計算子View MeasureSpec的getChildMeasureSpec()中,子View MeasureSpec在屬性被設置為wrap_content或match_parent情況下,子View MeasureSpec的specSize被設置成parenSize = 父容器當前剩余空間大小
  • 所以:wrap_content起到了和match_parent相同的作用:等于父容器當前剩余空間大小

3.1.1 默認情況getDefaultSize()

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
//參數(shù)說明:View的寬 / 高測量規(guī)格

//setMeasuredDimension()  用于獲得View寬/高的測量值
//這兩個參數(shù)是通過getDefaultSize()獲得的
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
           getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
}

public static int getDefaultSize(int size, int measureSpec) {  

//參數(shù)說明:
// 第一個參數(shù)size:提供的默認大小
// 第二個參數(shù):寬/高的測量規(guī)格(含模式 & 測量大小)

    //設置默認大小
    int result = size; 

    //獲取寬/高測量規(guī)格的模式 & 測量大小
    int specMode = MeasureSpec.getMode(measureSpec);  
    int specSize = MeasureSpec.getSize(measureSpec);  

    switch (specMode) {  
        // 模式為UNSPECIFIED時,使用提供的默認大小
        // 即第一個參數(shù):size 
        case MeasureSpec.UNSPECIFIED:  
            result = size;  
            break;  
        // 模式為AT_MOST,EXACTLY時,使用View測量后的寬/高值
        // 即measureSpec中的specSize
        case MeasureSpec.AT_MOST:  
        case MeasureSpec.EXACTLY:  
            result = specSize;  
            break;  
    }  

 //返回View的寬/高值
    return result;  
}

3.1.2 getChildMeasureSpec()

//作用:
/ 根據(jù)父視圖的MeasureSpec & 布局參數(shù)LayoutParams,計算單個子View的MeasureSpec
//即子view的確切大小由兩方面共同決定:父view的MeasureSpec 和 子view的LayoutParams屬性 


public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  

 //參數(shù)說明
 * @param spec 父view的詳細測量值(MeasureSpec) 
 * @param padding view當前尺寸的的內(nèi)邊距和外邊距(padding,margin) 
 * @param childDimension 子視圖的布局參數(shù)(寬/高)

    //父view的測量模式
    int specMode = MeasureSpec.getMode(spec);     

    //父view的大小
    int specSize = MeasureSpec.getSize(spec);     

    //通過父view計算出的子view = 父大小-邊距(父要求的大小,但子view不一定用這個值)   
    int size = Math.max(0, specSize - padding);  

    //子view想要的實際大小和模式(需要計算)  
    int resultSize = 0;  
    int resultMode = 0;  

    //通過父view的MeasureSpec和子view的LayoutParams確定子view的大小  


    // 當父view的模式為EXACITY時,父view強加給子view確切的值
   //一般是父view設置為match_parent或者固定值的ViewGroup 
    switch (specMode) {  
    case MeasureSpec.EXACTLY:  
        // 當子view的LayoutParams>0,即有確切的值  
        if (childDimension >= 0) {  
            //子view大小為子自身所賦的值,模式大小為EXACTLY  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  

        // 當子view的LayoutParams為MATCH_PARENT時(-1)  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            //子view大小為父view大小,模式為EXACTLY  
            resultSize = size;  
            resultMode = MeasureSpec.EXACTLY;  

        // 當子view的LayoutParams為WRAP_CONTENT時(-2)      
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            //子view決定自己的大小,但最大不能超過父view,模式為AT_MOST  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  

    // 當父view的模式為AT_MOST時,父view強加給子view一個最大的值。(一般是父view設置為wrap_content)  
    case MeasureSpec.AT_MOST:  
        // 道理同上  
        if (childDimension >= 0) {  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  

    // 當父view的模式為UNSPECIFIED時,父容器不對view有任何限制,要多大給多大
    // 多見于ListView、GridView  
    case MeasureSpec.UNSPECIFIED:  
        if (childDimension >= 0) {  
            // 子view大小為子自身所賦的值  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            // 因為父view為UNSPECIFIED,所以MATCH_PARENT的話子類大小為0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            // 因為父view為UNSPECIFIED,所以WRAP_CONTENT的話子類大小為0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        }  
        break;  
    }  
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
image

3.1.3解決方案:

  • 當自定義View的布局參數(shù)設置成wrap_content時時,指定一個默認大小(寬 / 高)。
  • 具體是在復寫onMeasure()里進行設置
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);


        // 獲取寬-測量規(guī)則的模式和大小
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        // 獲取高-測量規(guī)則的模式和大小
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        // 設置wrap_content的默認寬 / 高值
        // 默認寬/高的設定并無固定依據(jù),根據(jù)需要靈活設置
        // 類似TextView,ImageView等針對wrap_content均在onMeasure()對設置默認寬 / 高值有特殊處理,具體讀者可以自行查看
        int mWidth = 400;
        int mHeight = 400;

      // 當布局參數(shù)設置為wrap_content時,設置默認值
        if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(mWidth, mHeight);
        // 寬 / 高任意一個布局參數(shù)為= wrap_content時,都設置默認值
        } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(mWidth, heightSize);
        } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(widthSize, mHeight);
        }
}

這樣,當你的自定義View的寬 / 高設置成wrap_content屬性時就會生效了。

3.2 支持特殊屬性-支持padding & margin

如果不支持,那么padding和margin(ViewGroup情況)的屬性將失效

  1. 對于繼承View的控件,padding是在draw()中處理
  2. 對于繼承ViewGroup的控件,padding和margin會直接影響measure和layout過程
    // 復寫onDraw()
    @Override
    protected void onDraw(Canvas canvas) {

        super.onDraw(canvas);

        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();


        // 獲取控件的高度和寬度
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;

        // 設置圓的半徑 = 寬,高最小值的2分之1
        int r = Math.min(width, height) / 2;

        // 畫出圓(藍色)
        // 圓心 = 控件的中央,半徑 = 寬,高最小值的2分之1
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, r, mPaint1);

    }

3.3 多線程應直接使用post方式

View的內(nèi)部本身提供了post系列的方法,完全可以替代Handler的作用,使用起來更加方便、直接。

3.4 避免內(nèi)存泄露

主要針對View中含有線程或動畫的情況:當View退出或不可見時,記得及時停止該View包含的線程和動畫,否則會造成內(nèi)存泄露問題。

啟動或停止線程/ 動畫的方式:

  1. 啟動線程/ 動畫:使用view.onAttachedToWindow(),因為該方法調(diào)用的時機是當包含View的Activity啟動的時刻
  2. 停止線程/ 動畫:使用view.onDetachedFromWindow(),因為該方法調(diào)用的時機是當包含View的Activity退出或當前View被remove的時刻

3.5 處理好滑動沖突

當View帶有滑動嵌套情況時,必須要處理好滑動沖突,否則會嚴重影響View的顯示效果。

3.6 提供自定義屬性

使用步驟有如下:

  1. 在values目錄下創(chuàng)建自定義屬性的xml文件
  2. 在自定義View的構(gòu)造方法中解析自定義屬性的值
  3. 在布局文件中使用自定義屬性

3.6.1 步驟1:在values目錄下創(chuàng)建自定義屬性的xml文件

attrs_circle_view.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--自定義屬性集合:CircleView-->
    <!--在該集合下,設置不同的自定義屬性-->
    <declare-styleable name="CircleView">
        <!--在attr標簽下設置需要的自定義屬性-->
        <!--此處定義了一個設置圖形的顏色:circle_color屬性,格式是color,代表顏色-->
        <!--格式有很多種,如資源id(reference)等等-->
        <attr name="circle_color" format="color"/>

    </declare-styleable>
</resources>

對于自定義屬性類型 & 格式如下:

<-- 1. reference:使用某一資源ID -->
<declare-styleable name="名稱">
    <attr name="background" format="reference" />
</declare-styleable>
// 使用格式
  // 1. Java代碼
  private int ResID;
  private Drawable ResDraw;
  ResID = typedArray.getResourceId(R.styleable.SuperEditText_background, R.drawable.background); // 獲得資源ID
  ResDraw = getResources().getDrawable(ResID); // 獲得Drawble對象

  // 2. xml代碼
<ImageView
    android:layout_width="42dip"
    android:layout_height="42dip"
    app:background="@drawable/圖片ID" />

<--  2. color:顏色值 -->
<declare-styleable name="名稱">
    <attr name="textColor" format="color" />
</declare-styleable>
// 格式使用
<TextView
    android:layout_width="42dip"
    android:layout_height="42dip"
    android:textColor="#00FF00" />

<-- 3. boolean:布爾值 -->
<declare-styleable name="名稱">
    <attr name="focusable" format="boolean" />
</declare-styleable>
// 格式使用
<Button
    android:layout_width="42dip"
    android:layout_height="42dip"
    android:focusable="true" />

<-- 4. dimension:尺寸值 -->
<declare-styleable name="名稱">
    <attr name="layout_width" format="dimension" />
</declare-styleable>
// 格式使用:
<Button
    android:layout_width="42dip"
    android:layout_height="42dip" />

<-- 5. float:浮點值 -->
<declare-styleable name="AlphaAnimation">
    <attr name="fromAlpha" format="float" />
    <attr name="toAlpha" format="float" />
</declare-styleable>
// 格式使用
<alpha
    android:fromAlpha="1.0"
    android:toAlpha="0.7" />

<-- 6. integer:整型值 -->
<declare-styleable name="AnimatedRotateDrawable">
    <attr name="frameDuration" format="integer" />
    <attr name="framesCount" format="integer" />
</declare-styleable>
// 格式使用
<animated-rotate
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:frameDuration="100"
    android:framesCount="12"
 />

<-- 7. string:字符串 -->
<declare-styleable name="MapView">
    <attr name="apiKey" format="string" />
</declare-styleable>
// 格式使用
<com.google.android.maps.MapView
 android:apiKey="0jOkQ80oD1JL9C6HAja99uGXCRiS2CGjKO_bc_g" />

<-- 8. fraction:百分數(shù) -->
<declare-styleable name="RotateDrawable">
    <attr name="pivotX" format="fraction" />
    <attr name="pivotY" format="fraction" />
</declare-styleable>
// 格式使用
<rotate
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:pivotX="200%"
    android:pivotY="300%"
 />


<-- 9. enum:枚舉值 -->
<declare-styleable name="名稱">
    <attr name="orientation">
        <enum name="horizontal" value="0" />
        <enum name="vertical" value="1" />
    </attr>
</declare-styleable>
// 格式使用
<LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
/>

<-- 10. flag:位或運算 -->
<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>、
// 使用
<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>



<-- 特別注意:屬性定義時可以指定多種類型值 -->
<declare-styleable name="名稱">
    <attr name="background" format="reference|color" />
</declare-styleable>
// 使用
<ImageView
    android:layout_width="42dip"
    android:layout_height="42dip"
    android:background="@drawable/圖片ID|#00FF00" />

3.6.2 步驟2:在自定義View的構(gòu)造方法中解析自定義屬性的值

    // 自定義View的三個構(gòu)造函數(shù)
    public CircleView(Context context) {
        super(context);

        // 在構(gòu)造函數(shù)里初始化畫筆的操作
        init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
        init();

    }

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

        // 加載自定義屬性集合CircleView
        TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView);

        // 解析集合中的屬性circle_color屬性
        // 該屬性的id為:R.styleable.CircleView_circle_color
        // 第二個參數(shù)是默認設置顏色
        mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);

        // 解析后釋放資源
        a.recycle();

        init();
    }

    // 畫筆初始化
    private void init() {

        // 創(chuàng)建畫筆
        mPaint1 = new Paint();
        // 設置畫筆顏色為藍色
        mPaint1.setColor(mColor);
        // 設置畫筆寬度為10px
        mPaint1.setStrokeWidth(5f);
        //設置畫筆模式為填充
        mPaint1.setStyle(Paint.Style.FILL);

    }

3.6.3 步驟3:在布局文件中使用自定義屬性

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  <!--必須添加schemas聲明才能使用自定義屬性-->
    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="com.kailing.diy_view.MainActivity"
    >
  
<!-- 注意添加自定義View組件的標簽名:包名 + 自定義View類名-->
    <!--  控件背景設置為黑色-->
    <com.kailing.diy_view.CircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"

        android:background="#000000"
        android:padding="30dp"

    <!--設置自定義顏色-->
        app:circle_color="#FF4081"
         />
</RelativeLayout>

參考

手把手教你寫一個完整的自定義View

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

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