Android 自定義控件

前言:

在日常的Android開發中會經常和控件打交道,有時Android提供的控件未必能滿足業務的需求,這個時候就需要我們實現自定義一些控件,這裏將介紹自定義控件的原理和實現方法。

參考文章:

自定義控件要求:

  • 應當遵守Android標準的規範(命名,可配置,事件處理等)。
  • 在XML佈局中可配置控件的屬性。
  • 對交互應當有合適的反饋,比如按下,點擊等。
  • 具有兼容性, Android版本很多,應該具有廣泛的適用性。

創建自定義控件步驟:

下面以創建一個圓形百分比控件爲例,講解自定義控件的實現過程。

第一步:聲明屬性

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

使用<declare-styleable name="PercentView"> </declare-styleable>來定義一個屬性集合,name就是屬性集合的名字,這個名字一定要起的見名知意。

然後就是定義屬性值了,通過<attr name="textColor" format="color" />方式定義屬性值,屬性名字同樣也要起的見名知意,format表示這個屬性的值的類型,類型有以下幾種:

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

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

<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" /><!--圓形背景色-->
    </declare-styleable>

第二步:在佈局中引用

使用xmlns:fk="http://schemas.android.com/apk/res-auto"爲屬性集設置一個屬性集名稱,我這裏用的fk,建議在真正的項目中使用項目的縮寫,比如微信可能就是使用wx。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:fk="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

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

</LinearLayout>

第三步:編寫繼承自View的子類

一、View結構原理

Android系統的視圖結構的設計也採用了組合模式,即View作爲所有圖形的基類,Viewgroup對View繼承擴展爲視圖容器類。

View定義了繪圖的基本操作,基本操作由三個函數完成:measure()、layout()、draw(),其內部又分別包含了onMeasure()、onLayout()、onDraw()三個子方法。具體操作如下:

1、measure操作

measure操作主要用於計算視圖的大小,即視圖的寬度和長度。在view中定義爲final類型,要求子類不能修改。measure()函數中又會調用onMeasure()函數,視圖大小的將在這裏最終確定,也就是說measure只是對onMeasure的一個包裝,子類可以覆寫onMeasure()方法實現自己的計算視圖大小的方式,並通過setMeasuredDimension(width, height)保存計算結果。

2、layout操作

layout操作用於設置視圖在屏幕中顯示的位置。在view中定義爲final類型,要求子類不能修改。layout()函數中有兩個基本操作:

(1)setFrame(l,t,r,b),l,t,r,b即子視圖在父視圖中的具體位置,該函數用於將這些參數保存起來;

(2)onLayout(),在View中這個函數什麼都不會做,提供該函數主要是爲viewGroup類型佈局子視圖用的;

3、draw操作

draw操作利用前兩部得到的參數,將視圖顯示在屏幕上,到這裏也就完成了整個的視圖繪製工作。子類也不應該修改該方法,因爲其內部定義了繪圖的基本操作:

(1)繪製背景;

(2)如果要視圖顯示漸變框,這裏會做一些準備工作;

(3)繪製視圖本身,即調用onDraw()函數。在view中onDraw()是個空函數,也就是說具體的視圖都要覆寫該函數來實現自己的顯示(比如TextView在這裏實現了繪製文字的過程)。而對於ViewGroup則不需要實現該函數,因爲作爲容器是“沒有內容“的,其包含了多個子view,而子View已經實現了自己的繪製方法,因此只需要告訴子view繪製自己就可以了,也就是下面的dispatchDraw()方法;

(4)繪製子視圖,即dispatchDraw()函數。在view中這是個空函數,具體的視圖不需要實現該方法,它是專門爲容器類準備的,也就是容器類必須實現該方法;

(5)如果需要(應用程序調用了setVerticalFadingEdge或者setHorizontalFadingEdge),開始繪製漸變框;

(6)繪製滾動條;

從上面可以看出自定義View需要最少覆寫onMeasure()和onDraw()兩個方法。

二、View類的構造方法

創建自定義控件的3種主要實現方式:

  1. 繼承已有的控件來實現自定義控件: 主要是當要實現的控件和已有的控件在很多方面比較類似, 通過對已有控件的擴展來滿足要求。
  2. 通過繼承一個佈局文件實現自定義控件,一般來說做組合控件時可以通過這個方式來實現。注意此時不用onDraw方法,在構造函數中通過Inflater加載自定義控件的佈局文件,再addView(view),自定義控件的圖形界面就加載進來了。
  3. 通過繼承view類來實現自定義控件,使用GDI繪製出組件界面,一般無法通過上述兩種方式來實現時用該方式。

三、獲取自定義屬性

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

TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.PercentView);
if (typedArray != null) {backgroundColor = typedArray.getColor(R.styleable.PercentView_percent_background_color, Color.GRAY);
    progressColor = typedArray.getColor(R.styleable.PercentView_percent_progress_color, Color.BLUE);
    radius = typedArray.getDimension(R.styleable.PercentView_percent_circle_radius, 0);
    progress = typedArray.getInt(R.styleable.PercentView_percent_circle_progress, 0);
    gravity = typedArray.getInt(R.styleable.PercentView_percent_circle_gravity, CENTER);
    typedArray.recycle();
}

下面是整個PrecentView類代碼:

package fk.androiddemo_035;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

/**
 * Created by FK on 2017/2/15.
 */

public class PercentView extends View {
    private final static String TAG = PercentView.class.getSimpleName();
    private Paint mPaint;
    private int backgroundColor = Color.GRAY;
    private int progressColor = Color.BLUE;
    private float radius;
    private int progress;
    private float centerX = 0;
    private float centerY = 0;
    public static final int LEFT = 0;
    public static final int TOP = 1;
    public static final int CENTER = 2;
    public static final int RIGHT = 3;
    public static final int BOTTOM = 4;
    private int gravity = CENTER;
    private RectF rectF;  //用於定義的圓弧的形狀和大小的界限

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

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

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

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

    private void initParams(Context context, AttributeSet attrs) {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        rectF = new RectF();
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PercentView);
        if (typedArray != null) {
            backgroundColor = typedArray.getColor(R.styleable.PercentView_percent_background_color, Color.GRAY);
            progressColor = typedArray.getColor(R.styleable.PercentView_percent_progress_color, Color.BLUE);
            radius = typedArray.getDimension(R.styleable.PercentView_percent_circle_radius, 0);
            progress = typedArray.getInt(R.styleable.PercentView_percent_circle_progress, 0);
            gravity = typedArray.getInt(R.styleable.PercentView_percent_circle_gravity, CENTER);
            typedArray.recycle();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(backgroundColor);
        // FILL填充, STROKE描邊,FILL_AND_STROKE填充和描邊
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        canvas.drawCircle(centerX, centerY, radius, mPaint);
        mPaint.setColor(progressColor);

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

    @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);
        Log.e(TAG, "onMeasure--widthMode-->" + widthMode);
        switch (widthMode) {
            case MeasureSpec.EXACTLY://
                break;
            case MeasureSpec.AT_MOST:
                break;
            case MeasureSpec.UNSPECIFIED:
                break;
        }
        Log.e(TAG, "onMeasure--widthSize-->" + widthSize);
        Log.e(TAG, "onMeasure--heightMode-->" + heightMode);
        Log.e(TAG, "onMeasure--heightSize-->" + heightSize);
        int with = getWidth();
        int height = getHeight();
        Log.e(TAG, "onDraw---->" + with + "*" + height);
        centerX = with / 2;
        centerY = with / 2;
        switch (gravity) {
            case LEFT:
                centerX = radius + getPaddingLeft();
                break;
            case TOP:
                centerY = radius + getPaddingTop();
                break;
            case CENTER:
                break;
            case RIGHT:
                centerX = with - radius - getPaddingRight();
                break;
            case BOTTOM:
                centerY = height - radius - getPaddingBottom();
                break;
        }
        float left = centerX - radius;
        float top = centerY - radius;
        float right = centerX + radius;
        float bottom = centerY + radius;
        rectF.set(left, top, right, bottom);
    }

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

運行結果

根據不同的配置顯示的兩種效果

這裏寫圖片描述

源碼下載

地址:http://download.csdn.net/detail/youmingyu/9755075

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章