前言:
在日常的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種主要實現方式:
- 繼承已有的控件來實現自定義控件: 主要是當要實現的控件和已有的控件在很多方面比較類似, 通過對已有控件的擴展來滿足要求。
- 通過繼承一個佈局文件實現自定義控件,一般來說做組合控件時可以通過這個方式來實現。注意此時不用onDraw方法,在構造函數中通過Inflater加載自定義控件的佈局文件,再addView(view),自定義控件的圖形界面就加載進來了。
- 通過繼承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");
}
}
運行結果
根據不同的配置顯示的兩種效果