1. 程式人生 > >手把手教你寫一個完整的自定義View

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

前言


  • 自定義View是Android開發者必須瞭解的基礎
  • 今天,我將手把手教你寫一個自定義View,並理清自定義View所有應該的注意點

目錄

目錄

1. 自定義View的分類

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

2. 具體介紹 & 使用場景

對於自定義View的型別介紹及使用場景如下圖:
具體介紹 & 使用場景

3. 使用注意點

在使用自定義View時有很多注意點(坑),希望大家要非常留意:
使用注意點

3.1 支援特殊屬性

  • 支援wrap_content
    如果不在onMeasure()中對wrap_content作特殊處理,那麼wrap_content

    屬性將失效

  • 支援padding & margin
    如果不支援,那麼paddingmargin(ViewGroup情況)的屬性將失效

    1. 對於繼承View的控制元件,padding是在draw()中處理
    2. 對於繼承ViewGroup的控制元件,padding和margin會直接影響measure和layout過程

3.2 多執行緒應直接使用post方式

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

3.3 避免記憶體洩露

主要針對View中含有執行緒或動畫的情況:當View退出或不可見時,記得及時停止該View包含的執行緒和動畫,否則會造成記憶體洩露問題

啟動或停止執行緒/ 動畫的方式:
1. 啟動執行緒/ 動畫:使用view.onAttachedToWindow(),因為該方法呼叫的時機是當包含View的Activity啟動的時刻
2. 停止執行緒/ 動畫:使用view.onDetachedFromWindow(),因為該方法呼叫的時機是當包含View的Activity退出或當前View被remove的時刻

3.4 處理好滑動衝突

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

4. 具體例項

接下來,我將用自定義View中最常用的繼承View來說明自定義View的具體應用和需要注意的點

4.1 繼承VIew的介紹

Paste_Image.png

在下面的例子中,我將講解:

  • 如何實現一個基本的自定義View(繼承VIew)
  • 如何自身支援wrap_content & padding屬性
  • 如何為自定義View提供自定義屬性(如顏色等等)

  • 例項說明:畫一個實心圓

4.2 具體步驟

  1. 建立自定義View類(繼承View類)
  2. 佈局檔案新增自定義View元件
  3. 注意點設定(支援wrap_content & padding屬性自定義屬性等等)

下面我將逐個步驟進行說明:
步驟1:建立自定義View類(繼承View類)

CircleView.java

// 用於繪製自定義View的具體內容
// 具體繪製是在複寫的onDraw()內實現

public class CircleView extends View {

    // 設定畫筆變數
    Paint mPaint1;

    // 自定義View有四個建構函式
    // 如果View是在Java程式碼裡面new的,則呼叫第一個建構函式
    public CircleView(Context context){
        super(context);

        // 在建構函式裡初始化畫筆的操作
        init();
    }


// 如果View是在.xml裡宣告的,則呼叫第二個建構函式
// 自定義屬性是從AttributeSet引數傳進來的
    public CircleView(Context context,AttributeSet attrs){
        super(context, attrs);
        init();

    }

// 不會自動呼叫
// 一般是在第二個建構函式裡主動呼叫
// 如View有style屬性時
    public CircleView(Context context,AttributeSet attrs,int defStyleAttr ){
        super(context, attrs,defStyleAttr);
        init();
    }


    //API21之後才使用
    // 不會自動呼叫
    // 一般是在第二個建構函式裡主動呼叫
    // 如View有style屬性時
    public  CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

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

        // 建立畫筆
        mPaint1 = new Paint ();
        // 設定畫筆顏色為藍色
        mPaint1.setColor(Color.BLUE);
        // 設定畫筆寬度為10px
        mPaint1.setStrokeWidth(5f);
        //設定畫筆模式為填充
        mPaint1.setStyle(Paint.Style.FILL);

    }


    // 複寫onDraw()進行繪製  
    @Override
    protected void onDraw(Canvas canvas) {

        super.onDraw(canvas);

       // 獲取控制元件的高度和寬度
        int width = getWidth();
        int height = getHeight();

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

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

    }

}

步驟2:在佈局檔案中新增自定義View類的元件

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="scut.carson_ho.diy_view.MainActivity">

<!-- 注意新增自定義View元件的標籤名:包名 + 自定義View類名-->
    <!--  控制元件背景設定為黑色-->
    <scut.carson_ho.diy_view.CircleView
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:background="#000000"

</RelativeLayout>

步驟3:在MainActivity類設定顯示

MainActivity.java

public class MainActivity extends AppCompatActivity {

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

效果圖

好了,至此,一個基本的自定義View已經實現了。接下來繼續看自定義View所有應該注意的點:

  • 如何手動支援wrap_content屬性
  • 如何手動支援padding屬性
  • 如何為自定義View提供自定義屬性(如顏色等等)

a. 手動支援wrap_content屬性

先來看wrap_content & match_parent屬性的區別

// 檢視的寬和高被設定成剛好適應檢視內容的最小尺寸
android:layout_width="wrap_content"

// 檢視的寬和高延伸至充滿整個父佈局
android:layout_width="match_parent"
// 在Android API 8之前叫作"fill_parent"

如果不手動設定支援wrap_content屬性,那麼wrap_content屬性是不會生效(顯示效果同match_parent

b. 支援padding屬性

padding屬性:用於設定控制元件內容相對控制元件邊緣的邊距;

區別與margin屬性(同樣稱為:邊距):控制元件邊緣相對父控制元件的邊距(父控制元件控制),具體區別如下:

Paste_Image.png

如果不手動設定支援padding屬性,那麼padding屬性在自定義View中是不會生效的。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="scut.carson_ho.diy_view.MainActivity">

    <scut.carson_ho.diy_view.CircleView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        /**  新增Padding屬性,但不會生效 **/
        android:padding="20dp"
         />
</RelativeLayout>

解決方案

繪製時考慮傳入的padding屬性值(四個方向)。

在自定義View類的複寫onDraw()進行設定

CircleView.java

// 僅看複寫的onDraw()
@Override
    protected void onDraw(Canvas canvas) {

        super.onDraw(canvas);

        // 獲取傳入的padding值
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();


        // 獲取繪製內容的高度和寬度(考慮了四個方向的padding值)
        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);

    }

效果圖

c. 提供自定義屬性

系統自帶屬性,如

// 基本是以android開頭
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#000000"
        android:padding="30dp"
  • 但有些時候需要一些系統所沒有的屬性,稱為自定義屬性
  • 使用步驟有如下:
    1. 在values目錄下建立自定義屬性的xml檔案
    2. 在自定義View的構造方法中解析自定義屬性的值
    3. 在佈局檔案中使用自定義屬性

下面我將對每個步驟進行具體介紹

步驟1:在values目錄下建立自定義屬性的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>
// 使用格式
<ImageView
    android:layout_width="42dip"
    android:layout_height="42dip"
    android: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:百分數 -->
<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" />

步驟2:在自定義View的構造方法中解析自定義屬性的值

此處是需要解析circle_color屬性的值

// 該建構函式需要重寫
  public CircleView(Context context, AttributeSet attrs) {

        this(context, attrs,0);
        // 原來是:super(context,attrs);
        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
        // 將解析的屬性傳入到畫圓的畫筆顏色變數當中(本質上是自定義畫圓畫筆的顏色)
        // 第二個引數是預設設定顏色(即無指定circle_color情況下使用)
        mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);

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

        init();

步驟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="scut.carson_ho.diy_view.MainActivity"
    >

<!-- 注意新增自定義View元件的標籤名:包名 + 自定義View類名-->
    <!--  控制元件背景設定為黑色-->
    <scut.carson_ho.diy_view.CircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"

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

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

Paste_Image.png

至此,一個較為規範的自定義View已經完成了。

完整程式碼下載

5. 總結

請幫頂或評論點贊!因為你們的贊同/鼓勵是我寫作的最大動力!