4.2 自定義控件 之 自定義屬性與引入佈局

點此進入:從零快速構建APP系列目錄導圖
點此進入:UI編程系列目錄導圖
點此進入:四大組件系列目錄導圖
點此進入:數據網絡和線程系列目錄導圖

本節例程下載地址:
WillFLowCustomAttribute
WillFlowInlcude

一、自定義屬性

(1)爲何要引入自定義屬性?

當Android提供的原生屬性不能滿足實際的需求的時候,比如我們需要自定義圓形百分比半徑大小、圓形背景、圓形顯示的位置、圓形進度的背景等等。這個時候就需要我們自定義屬性了。

(2)自定義屬性的基本步驟

在res/values文件下添加一個attrs.xml文件

如果項目比較大的話,會導致attrs.xml代碼相當龐大,這時可以根據相應的功能模塊起名字,方便查找,例如:登錄模塊相關attrs_login.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="RoundImageView">
        <attr name="borderRadius" />
        <attr name="type" />
    </declare-styleable>
</resources>
聲明一組屬性

使用來定義一個屬性集合,name就是屬性集合的名字,這個名字一定要起的見名知意。

   <declare-styleable name="PercentView">
       <!--添加屬性-->
   </declare-styleable>
定義屬性值

通過 方式定義屬性值,屬性名字同樣也要起的見名知意,format表示這個屬性的值的類型,類型有以下幾種:

  • float:浮點型
  • integer:整型
  • fraction:百分數
  • enum:枚舉類型
  • flag:位或運算
  • reference:引用資源
  • string:字符串
  • Color:顏色
  • boolean:布爾值
  • dimension:尺寸值

基於上面的要求,我們可以定義一下百分比控件屬性:

    <declare-styleable name="PercentView">
        <attr name="percent_circle_gravity"><!--圓形繪製的位置-->
            <flag name="left" value="0" />
            <flag name="top" value="1" />
            <flag name="center" value="2" />
            <flag name="right" value="3" />
            <flag name="bottom" value="4" />
        </attr>
        <attr name="percent_circle_radius" format="dimension" /><!--圓形半徑-->
        <attr name="percent_circle_progress" format="integer" /><!--當前進度值-->
        <attr name="percent_progress_color" format="color" /><!--進度顯示顏色-->
        <attr name="percent_background_color" format="color" /><!--圓形背景色-->
        <attr name="percent_offset" format="integer" /><!--圓形中心偏差-->
    </declare-styleable>
在佈局中使用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:lee="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.whoislcj.views.PercentView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_margin="10dp"
        android:background="@color/red"
        android:padding="10dp"
        lee:percent_background_color="@color/gray"
        lee:percent_circle_gravity="left"
        lee:percent_circle_progress="30"
        lee:percent_circle_radius="50dp"
        lee:percent_progress_color="@color/blue" />

</LinearLayout>

爲屬性集設置一個屬性集名稱,我這裏用的lee,我這是因爲實在想不起使用什麼屬性集名稱了,建議在真正的項目中使用項目的縮寫,比如微信可能就是使用wx。

在自定義控件中獲取自定義屬性

每一個屬性集合編譯之後都會對應一個styleable對象,通過styleable對象獲取TypedArray typedArray,然後通過鍵值對獲取屬性值,這點有點類似SharedPreference的取法。

    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PercentView);
    if (typedArray != null) {
        mBackgroundColor = typedArray.getColor(R.styleable.PercentView_percent_background_color, Color.YELLOW);
        mProgressColor = typedArray.getColor(R.styleable.PercentView_percent_progress_color, Color.RED);
        mRadius = typedArray.getDimension(R.styleable.PercentView_percent_circle_radius, 0);
        mProgress = typedArray.getInt(R.styleable.PercentView_percent_circle_progress, 0);
        mGravity = typedArray.getInt(R.styleable.PercentView_percent_circle_gravity, CENTER);
        mOffset = typedArray.getInt(R.styleable.PercentView_percent_offset, 0);
        typedArray.recycle();
    }

(3)完整示例

    public PercentView(Context context) {
        super(context);
        init();
    }

    public PercentView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initParams(context, attrs);
    }

    public PercentView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initParams(context, attrs);
    }

    private void initParams(Context context, AttributeSet attrs) {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mRectF = new RectF();
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PercentView);
        if (typedArray != null) {
            mBackgroundColor = typedArray.getColor(R.styleable.PercentView_percent_background_color, Color.YELLOW);
            mProgressColor = typedArray.getColor(R.styleable.PercentView_percent_progress_color, Color.RED);
            mRadius = typedArray.getDimension(R.styleable.PercentView_percent_circle_radius, 0);
            mProgress = typedArray.getInt(R.styleable.PercentView_percent_circle_progress, 0);
            mGravity = typedArray.getInt(R.styleable.PercentView_percent_circle_gravity, CENTER);
            mOffset = typedArray.getInt(R.styleable.PercentView_percent_offset, 0);
            typedArray.recycle();
        }
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mRectF = new RectF();
    }

    @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);

        switch (widthMode) {
            case MeasureSpec.EXACTLY:
                break;
            case MeasureSpec.AT_MOST:
                break;
            case MeasureSpec.UNSPECIFIED:
                break;
        }
        Log.i(TAG, "onMeasure() widthMode  : " + widthMode);
        Log.i(TAG, "onMeasure() widthSize  : " + widthSize);
        Log.i(TAG, "onMeasure() heightMode : " + heightMode);
        Log.i(TAG, "onMeasure() heightSize : " + heightSize);

        int with = getWidth();
        int height = getHeight();
        mCenterX = with / 2;
        mCenterY = with / 2;
        switch (mGravity) {
            case LEFT:
                mCenterX = mRadius + getPaddingLeft();
                break;
            case TOP:
                mCenterY = mRadius + getPaddingTop();
                break;
            case CENTER:
                break;
            case RIGHT:
                mCenterX = with - mRadius - getPaddingRight();
                break;
            case BOTTOM:
                mCenterY = height - mRadius - getPaddingBottom();
                break;
        }
        float left = mCenterX - mRadius;
        float top = mCenterY - mRadius;
        float right = mCenterX + mRadius;
        float bottom = mCenterY + mRadius;
        mRectF.set(left, top, right, bottom);
        Log.i(TAG, "left : " + left + ", top : " + top + ", right : " + right + ", bottom : " + bottom);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.i(TAG, "onLayout()  left : " + left + ", top : " + top + ", right : " + right + ", bottom : " + bottom);
    }

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

        mPaint.setColor(mBackgroundColor);
        // FILL:填充; STROKE:描邊; FILL_AND_STROKE:填充和描邊。
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        canvas.drawCircle(mCenterX, mCenterY, mRadius, mPaint);
        Log.i(TAG, "onDraw() mCenterX : " + mCenterX + ", mCenterY : " + mCenterY + ", mRadius : " + mRadius);

        mPaint.setColor(mProgressColor);
        double percent = mProgress * 1.0 / 100;
        int angle = (int) (percent * 360);
        canvas.drawArc(mRectF, 270, angle, true, mPaint);  // 根據進度畫圓弧
    }

通過自定義屬性可以達到自定義的控件也能像原生的控件一樣實現可配置。但是在實際的項目開發中,像本文介紹的這種自定義控件使用頻率並不是最高的,使用頻率較高的是通過自定義一個組合控件的方式,來達到佈局文件的複用,以減少項目維護成本以及開發成本,下篇文章將重點介紹如何自定義控件組合。

二、引入佈局

爲什麼要引入佈局?

我們在項目開發中經常會遇見很多相似或者相同的佈局,比如用過 iPhone 的同學都應該知道,幾乎每一個 iPhone 應用的界面頂部都會有一個標題欄,標題欄上會有一到兩個按鈕可用於返回或其他操作,因爲 iPhone 沒有實體返回鍵嘛。現在很多的Android 程序也都喜歡模仿 iPhone 的風格,在界面的頂部放置一個標題欄,雖然 Android 系統已經給每個活動提供了標題欄功能,但這裏我們還是決定創建一個自定義的標題欄,這樣就使得我們的界面佈局更加靈活。

一個標題欄佈局不是什麼困難的事情,只需要加入兩個 Button 和一個 TextView,然後在佈局中擺放好就可以了。可是這樣做存在的問題是:一般我們的程序中可能有很多個活動都需要這樣的標題欄,如果在每個活動的佈局中都編寫一遍同樣的標題欄代碼,明顯就會導致代碼的大量重複,這個時候我們就可以使用引入佈局的方式來解決這個問題。

(1)靜態引入

新建一個佈局 title.xml,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <ImageButton
        android:id="@+id/imageButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"
        android:padding="5dp"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="52dp"
        android:layout_alignBottom="@+id/imageButton"
        android:layout_alignParentTop="true"
        android:layout_toEndOf="@+id/imageButton"
        android:background="@color/colorPrimary"
        android:gravity="center_vertical"
        android:paddingLeft="50dp"
        android:text="自定義標題"
        android:textSize="24sp" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/textView"
        android:layout_alignParentEnd="true"
        android:layout_alignParentTop="true"
        android:text="右鍵"
        android:textSize="18sp" />

</RelativeLayout>

我們在 RelativeLayout 中分別加入了一個 Button 、一個 ImageButton 和一個 TextView,左邊的 ImageButton 可用於返回,右邊的 Button 可用於編輯,中間的 TextView 則可以顯示一段標題文本。上面的代碼中大多數的屬性我們都已經是見過的,不明白的可以到本系列開始的幾篇文章當中學習,裏面詳細列出了常用的所有屬性。

現在標題欄佈局已經編寫完成了,剩下的就是如何在程序中使用這個標題欄了,在
activity_main.xml 的代碼中添加

    <include
        layout="@layout/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    </include>

是的,我們只需要通過一行 include 語句將標題欄佈局引入進來就可以了,最後在 MainActivity 中將系統自帶的標題欄隱藏掉,代碼如下所示:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);

        if (getSupportActionBar() != null){
            getSupportActionBar().hide();
        }
    }

這裏還要說明一點:如果你的代碼的Activity是繼承自AppCompatActivity,那麼使用該示例代碼就可以了,但是如果你的代碼的Activity是繼承自Activity,那麼則需要使用requestWindowFeature(Window.FEATURE_NO_TITLE);來去掉原有的標題欄。但是無論如何,使用這種方式,不管有多少佈局需要添加標題欄,只需一行 include 語句就可以了。

(2)動態引入

靜態引入佈局的技巧確實解決了重複編寫佈局代碼的問題,但是如果佈局中有一些控件要求能夠響應事件,我們還是需要在每個Activity中爲這些控件單獨編寫一次事件註冊的代碼。比如說標題欄中的返回按鈕,其實不管是在哪一個Activity中,這個按鈕的功能都是相同的,即銷燬掉當前Activity。而如果在每一個Activity中都需要重新註冊一遍返回按鈕的點擊事件,無疑又是增加了很多重複代碼,這種情況最好是使用動態引入的方式來解決。

新建 TitleLayout 繼承自 RelativeLayout,讓它成爲我們自定義的標題欄控件,代碼如下所示:

    public TitleLayout(Context context) {
        super(context);
        mContext = context;
        init();
    }

    public TitleLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init();
    }

    public TitleLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        init();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public TitleLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mContext = context;
    }


    private void init() {
        LayoutInflater.from(mContext).inflate(R.layout.title, this);
        mButton = (Button) findViewById(R.id.button);
        mImageButton = (ImageButton) findViewById(R.id.imageButton);
    }

首先我們重寫了 RelativeLayout 中的幾個構造函數,在佈局中引入 TitleLayout 控件就會調用這個init()方法。然後在init()方法中需要對標題欄佈局進行動態加載,這就要藉助 LayoutInflater 來實現了。通過 LayoutInflater 的 from() 方法可以構建出一個 LayoutInflater 對象,然後調用 inflate() 方法就可以動態加載一個佈局文件,inflate()方法接收兩個參數,第一個參數是要加載的佈局文件的 id,這裏我們傳入 R.layout.title,第二個參數是給加載好的佈局再添加一個父佈局, 這裏我們想要指定爲 TitleLayout,於是直接傳入 this。

現在自定義控件已經創建好了,然後我們需要在佈局文件中添加這個自定義控件,在 activity_main.xml 中添加如下代碼:

    <com.wgh.willflowinlcude.TitleLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    </com.wgh.willflowinlcude.TitleLayout>

添加自定義控件和添加普通控件的方式基本是一樣的,只不過在添加自定義控件的時候我們需要指明控件的完整類名,包名在這裏是不可以省略的。

重新運行程序,你會發現此時效果和使用引入佈局方式的效果是一樣的,然後我們來嘗試爲標題欄中的按鈕註冊點擊事件,修改 TitleLayout 中的代碼,如下所示:

    private void init() {
        LayoutInflater.from(mContext).inflate(R.layout.title, this);
        mButton = (Button) findViewById(R.id.button);
        mImageButton = (ImageButton) findViewById(R.id.imageButton);
        mButton.setOnClickListener(this);
        mImageButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button :
                showToast("單擊了右鍵", view);
                break;
            case R.id.imageButton :
                showToast("單擊了左鍵", view);
                break;
        }
    }

首先還是通過 findViewById() 方法得到按鈕的實例,然後分別調用 setOnClickListener() 方法給兩個按鈕註冊了點擊事件,當點擊返回按鈕時銷燬掉當前的Activity,當點擊按鈕時用Toast彈出一段文本。這樣的話,每當我們在一個佈局中引入 TitleLayout,返回按鈕和編輯按鈕的點擊事件就已經自動實現好了,這就省去了很多編寫重複代碼的工作。

這裏你能看到我們的Toast和平時使用的有些區別,這是因爲我們使用了自定義的Toast,當然如果你對之前的有所瞭解,你自然知道除此之外的四種Toast用法,如果沒有的話,可以到這裏查看:
2.6 通知類控件 Toast、Menu

點此進入:GitHub開源項目“愛閱”。“愛閱”專注於收集優質的文章、站點、教程,與大家共分享。下面是“愛閱”的效果圖:


聯繫方式:

簡書:WillFlow
CSDN:WillFlow
微信公衆號:WillFlow

微信公衆號:WillFlow

發佈了81 篇原創文章 · 獲贊 21 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章