Android 自定義View-旋轉小按鈕

呃,什麼是旋轉小按鈕?上圖:
這裏寫圖片描述
自定義這個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-旋轉小按鈕源碼

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