呃,什麼是旋轉小按鈕?上圖:
自定義這個View的原因是我需要一個能點擊一下就能旋轉顯示正在刷新的小按鈕,等刷新結束後在使它停止旋轉並恢復到初始狀態,並且這個View的字體大小,字體顏色,進度條的顏色等都可以自由配置。
自定義View包含以下幾步:
1、自定義View的屬性
2、在XML佈局文件中使用自定義屬性
2、在View的構造方法中獲得我們配置的屬性
3、重寫onMesure
4、重寫onDraw
自定義屬性
1、自定義View的屬性,首先在res/values/ 下建立一個attrs.xml ,並在其中添加需要的屬性,本例如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RefreshView">
<attr name="textSize" format="dimension"></attr>
<attr name="textColor" format="color"></attr>
<attr name="text" format="string"></attr>
<attr name="processColor" format="color"></attr>
<attr name="processWidth" format="dimension"></attr>
</declare-styleable>
</resources>
declare-stylable標籤只是爲了給自定義屬性分類。一個項目中可能又多個自定義控件,但只能又一個attr.xml文件,因此我們需要對不同自定義控件中的自定義屬性進行分類,這也是爲什麼declare-stylable標籤中的name屬性往往定義成自定義控件的名稱(引用來自【Android - 自定義View】之自定義View淺析);
在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"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.sharenew.viewselfdefine.MainActivity">
<com.sharenew.viewselfdefine.RefreshView
android:layout_centerInParent="true"
android:id="@+id/refreshview"
android:layout_width="100dp"
android:layout_height="100dp"
android:padding="10dp"
custom:text="刷新"
custom:processWidth="3dp"
custom:processColor="@android:color/holo_orange_light"
custom:textColor="@android:color/holo_green_dark"
custom:textSize="22sp"
/>
</RelativeLayout>
我們的佈局文件只有自定義的一個組件,它位於Activity的中央。爲了使用自定義的屬性,我們首先要聲明自定的名稱空間,在Gradle構建的工程中,它總是這樣的:xmlns:custom=”http://schemas.android.com/apk/res-auto”
有了名稱空間,我們就可以在該xml中使用自定的屬性了。
在View的構造方法中獲得我們配置的屬性
public RefreshView(Context context) {
this(context, null);
}
public RefreshView(Context context, AttributeSet attrs) {
this(context,attrs,0);
}
public RefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
/**
* 獲得我們所定義的自定義樣式屬性
*/
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshView);
int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.RefreshView_text:
mText = a.getString(attr);
break;
case R.styleable.RefreshView_textColor:
// 默認顏色設置爲黑色
mTextColor = a.getColor(attr, Color.BLACK);
break;
case R.styleable.RefreshView_textSize:
// 默認設置爲16sp,TypeValue也可以把sp轉化爲px
mTextSize = (int) a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
case R.styleable.RefreshView_processColor:
mProcessColor = a.getColor(attr,Color.YELLOW);
break;
case R.styleable.RefreshView_processWidth:
mProcessWidth = (int) a.getDimensionPixelSize(attr,(int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 3, getResources().getDisplayMetrics()));
break;
}
}
a.recycle();
/**
* 獲得繪製文本的寬和高
*/
mPaint = new Paint();
mPaint.setAntiAlias(true);
if(mText!=null){
mTextBound = new Rect();
mPaint.setStrokeWidth(1);
mPaint.setTextSize(mTextSize);
mPaint.getTextBounds(mText,0,mText.length(),mTextBound);
}else {
mTextBound = new Rect(0,0,30,30);
}
/**
* 計算view的大小
*/
mProcessBound = new RectF(0,0,mTextBound.width()+mProcessWidth,mTextBound.height()+mProcessWidth);
}
我們重寫了3個構造方法。在java代碼中new出來的對象,默認使用的是一個參數的構造方法,使用佈局文件創造的對象,默認使用的是的是兩個參數的構造方法。關於爲什麼這兩個構造函數最終都要調用到三個參數的構造函數,呃,這似乎已經成爲了一種習慣…
注意:不管有沒有使用自定義屬性,都會默認調用兩個參數的構造方法,“使用了自定義屬性就會默認調用三個參數的構造方法”的說法是錯誤的。
obtainStyledAttributes方法會從attrs中提取出自定義的屬性,並將所有的屬性存放在TypedArray 的一個對象中,這樣通過簡單的解析這個對象,就能得到所有的自定義屬性的值。
重寫onMesure
一個View的繪製流程是,首先調用onMeasure,然後纔會調用onDraw。onMeasure用於測量這個view的大小。
對於自定的View,一定要處理WRAP_CONTENT的情況,不然配置WRAP_CONTENT會得到MATCH_PATENT的效果。
我們知道在ViewGroup中,給View分配的空間大小並不是確定的,有可能隨着具體的變化而變化,而這個變化的條件就是傳到specMode中決定的,specMode一共有三種可能:
MeasureSpec.EXACTLY:父視圖希望子視圖的大小應該是specSize中指定的。
MeasureSpec.AT_MOST:子視圖的大小最多是specSize中指定的值,也就是說不建議子視圖的大小超過specSize中給定的值。
MeasureSpec.UNSPECIFIED:我們可以隨意指定視圖的大小。
通過以上這些分析,可以知道視圖最終的大小由父視圖,子視圖以及程序員根據需要決定,良好的設計一般會根據子視圖的measureSpec設置合適的佈局大小。
本例實現onMeasure如下:
@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);
int width;
int height ;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
int maxTextSize = (mTextBound.width()>mTextBound.height())?mTextBound.width():mTextBound.height();
width = maxTextSize + mProcessWidth*2+20;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = width;
}
viewWidth = width;
viewHeight = height;
mProcessBound.set(mProcessWidth+getPaddingLeft(),mProcessWidth+getPaddingTop(),width-mProcessWidth-getPaddingRight(),height-mProcessWidth-getPaddingBottom());
setMeasuredDimension(width, height);
}
重寫onDraw
最後一步就是重寫onDraw了。onDraw()方法負責繪製,即如果我們希望得到的效果在Android原生控件中沒有現成的支持,那麼我們就需要自己繪製我們的自定義控件的顯示效果。要學習onDraw()方法,我們就需要學習在onDraw()方法中使用最多的兩個類:Paint和Canvas。
Paint類似一個畫筆,Canvas類似一個畫布。
畫筆的粗細,實心還是空心等都是在Paint的實例中設置的,具體的繪製是在Canvas中繪製,因此Canvas中有很多的draw函數,比如畫圓、畫矩形等。
一下爲本例實現的draw函數:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.DKGRAY);
mPaint.setStrokeWidth(mProcessWidth);
canvas.drawArc(mProcessBound,0,360,false,mPaint);
mPaint.setColor(mProcessColor);
canvas.drawArc(mProcessBound,startAngle,swapArea,false,mPaint);
if(mText!=null && mText!=""){
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText(mText,(int)(viewWidth/2-mTextBound.width()/2),(int)(viewHeight/2+mTextBound.height()/2),mPaint);
}
}
注意,本例中,圓圈中的文字一定是居中的,Paint中有相關的方法可以測量字符串所佔的像素大小,比如getTextBounds,measureText等。
更新動畫的參數是在子線程中完成的,子線程通知UI線程重新繪製使用的是postInvalidate方法。爲了啓動和停止動畫,我們必須提供start和stop接口,其實現如下:
public void start(){
startRun = true;
startAngle = 0;
swapArea = 60;
new Thread(new Runnable() {
boolean isSwapAreaReverse = false;
@Override
public void run() {
while (startRun){
startAngle++;
if(isSwapAreaReverse){
swapArea--;
startAngle++;
}else {
swapArea++;
}
if(swapArea==360)isSwapAreaReverse = true;
if(swapArea==0)isSwapAreaReverse=false;
RefreshView.this.postInvalidate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
public void stop(){
startRun = false;
startAngle = 0;
swapArea = 0;
invalidate();
}
以上便是自定義旋轉小按鈕的核心程序,需要全部代碼可從這裏下載:
Android 自定義View-旋轉小按鈕源碼