天天看點

【安卓自定義控件系列】自繪控件打造界面超炫功能超強的圓形進度條

在前面我們講過了安卓自定義控件三種方式中的組合控件,現在我們來講解一下通過自繪的方式來實作自定義控件,本部落格将以自定義圓形進度條為例向大家講解自定義控件的知識,首先來看一下效果圖吧,這個是本人自定義圓形進度條demo工程的運作截圖:

【安卓自定義控件系列】自繪控件打造界面超炫功能超強的圓形進度條
【安卓自定義控件系列】自繪控件打造界面超炫功能超強的圓形進度條
【安卓自定義控件系列】自繪控件打造界面超炫功能超強的圓形進度條

首先說一下自己這個自定義圓形進度條要達到的目标:

1能夠支援設定進度條各種屬性,如圓環的大小,顔色,進度條的大小,顔色,進度條的顔色支援設定三種顔色來達到漸變色的效果。

2圓形進度條的内部支援設定三層文本,即上層的标題,如上圖的“您的等級超越全國”,中間層的進度值,如上圖的“700”,下層的附帶内容,如上圖的“萬的使用者”

3支援設定三層文本的大小與顔色,如上圖示題與底部文本為黑色,中間文本為紅色

4支援進度條從任意位置開始顯示,為何要支援該功能,是因為在不同的場合,進度條開始顯示的位置一般是不同的,如在某些手機助手類下載下傳App的應用中顯示下載下傳進度的時候都是從圓環的頂部開始,以順時針為方向逐漸遞增顯示,本例的第三個小圓環即是模仿的該場合,但是因為截的動态圖上傳出錯,隻能上傳幾張圖檔,是以看的不是很清楚,而在某些計步器類的app中進度的繪制一般是從左下角開始顯示,然後以順時針為方向達到對稱的位置,本例的最後一個大圓環即是模仿的該場合。

5支援設定部分圓弧,而不是整個圓,如本例的最後一個大圓環的進度條顯示效果,因為在某些場合是不需要繪制整個圓的,如在模拟汽車速度表盤的場合。

6具備極強的自适應能力,即wrap_content參數要能夠比較完美的适應使用者輸入的文本的長度。

在做這個自定義控件的工程時也遇到了一坑,不過都一一解決了,其中最難的就是最後一條,要求具備極強的自适應能力,即當我們在xml檔案中指定該自定義控件的寬度與高度為wrap_content時如何完美的适應使用者輸入的文本,這個是關鍵,這涉及到安卓中控件的繪制過程的知識和一些繪圖API的使用,主要是paint與draw這兩個類的API,而本人完美解決wrap_content涉及到了paint類中某些不常用的API,是以說這也是難以解決的一個原因,你必須對paint類的API非常熟悉,即使是相對而言很少使用的。

因為本人已打算将該元件開源,上傳到我的github上,大家可以到我的github上去fork我的代碼,是以本部落格不是對整個自定義圓形進度條做講解,因為這沒啥難度,另外網上很多這些方面的部落格,本部落格重點講解如何解決極強的自适應能力,是以下面重點說一下如何解決wrap_content.

在貼出自己解決方案的代碼之前,先給大家普及一下關于安卓中控件的繪制過程,因為你明白了這個過程才知道如何去解決wrap_content.

我們知道安卓中一個控件要顯示在界面上要經過三個過程,即測量,布局與繪制,對應onMeasure,onLayout,onDraw這三個函數,很顯然我們要解決的是測量過程,即當在xml中設定wrap_content的時候如何較準确的測量出我們自定義控件的大小,在安卓中一個控件的大小用MeasureSpec這個類來表示所,測量過程實際上可以說是得到該控件的MeasureSpec的過程。下面是一些關于MeasureSpec類的常識:

- MeasureSpec是通過将一個int(32)的數組成而成的,本質是一個int數,MeasureSpec由兩部分組成:SepcMode 和 SpecSize 。其中SpecMode為MeasueSpec的高2位,SpecSize為MeasureSpec的低30位。SpecMode有3類:

 - UNSPECIFIED:父容器不對View有任何限制,要多大有多大,這種情況一般用于系統内容。在我們使用過程中,一般不考慮這種模式。

 - EXACTLY:父容器已經檢測到了View所需要的精确大小,這個時候View的最終大小就是SpecSize所指定的大小。它對應的LayotParams中的match_parent 和具體的數值。

 - AT_MOST: 父容器指定了一個可用大小SpecSize,View的大小不能大于這個值,它對應于LayoutParams中的wrap_content。

一個子控件的MeasureSpec與父容器的MeasureSpec和自身的LayoutParams相關,由二者共同決定。一個子控件的onMeasure方法一般由其父容器所調用,代碼如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }      

從上面的代碼可以看到AT_MOST和EXACTLY兩種模式都會被設定成specSize。我們也知道AT_MOST對應于wrap_content,而EXACTLY對應于match_parent和具體數值情況。也就說預設情況下wrap_content和match_parent是具有相同的效果的,這也是為何說在我們通過繼承自View類自繪的方式自定義控件的時候為何需要自己支援wrap_content的原因。

知道了原因我們就知道要解決的話則應該重寫自定義控件的onMeasure函數,在該函數中為自定義控件設定一個預設的寬與高即可,但是為了做到極強的自适應,這個寬與高必須非常恰當,那麼怎樣才算恰當呢?這個就和自定義控件的功能相關,如本人自定義控件中要求支援三層文本顯示,那麼很顯然所謂的恰當就是剛好能夠容納這三行文本中的最大長度的文本,是以此時解決思路就轉換為了如何求自繪文本的長度,這個涉及到了Paint類中的一個API getTextBounds,通過該API就可以知道文本的長度,進而較好的支援wrap_content,下面是解決wrap_content的完整代碼,注釋很詳細,大家應該可以看懂。

//必須重寫該方法,否則在xml檔案中定義warp_content與match_parent效果相同
  @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     int desiredWidth ;
     int desiredHeight ;

     //設定了文本,且屬性為wrap_content,那麼以輸入的文本的長度為寬度
     if (isSetToptitle) {
     Rect rect = new Rect();  
     titlePaint.getTextBounds(topTitle, 0, topTitle.length(), rect); 
//     desiredWidth =(int) (rect.width()+4*progressWidth);
//     desiredHeight = (int) (rect.width()+4*progressWidth);
     desiredWidth =(int) (1.5*rect.width());
     desiredHeight = (int) (1.5*rect.width());
     }
     else//沒設定文本那麼不可能設定wrap_content屬性,事實上這些設定無效
     {
       desiredWidth =500;
       desiredHeight = 500;
     }

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;

        //Measure Width
        if (widthMode == MeasureSpec.EXACTLY) {
            //Must be this size
            width = widthSize;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            //Can't be bigger than...
            width = Math.min(desiredWidth, widthSize);
        } else {
            //Be whatever you want
            width = desiredWidth;
        }

        //Measure Height
        if (heightMode == MeasureSpec.EXACTLY) {
            //Must be this size
            height = heightSize;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            //Can't be bigger than...
            height = Math.min(desiredHeight, heightSize);
        } else {
            //Be whatever you want
            height = desiredHeight;
        }

        //MUST CALL THIS
        setMeasuredDimension(width, height);

        center = getWidth()/2; //該方法必須在onDraw或者onMeasure中調用,否則不起作用d

      //圓環的半徑 ,此處必須是progressWidth與circleWidth中較大的一個
        //radius = (int) (center - progressWidth/2); 
      if(progressWidth>circleWidth)
        radius=(int)(center-progressWidth/2);
      else
        {radius=(int)(center-circleWidth/2);}


          sweepGradient = new SweepGradient(0, 0, colors, null);
          hideRect=new RectF(center - radius, center - radius, center  
                  + radius, center + radius); 

    }      

至于其它屬性因為很簡單,無非就是在atrs檔案中定義屬性,在構造函數中通過context.obtainStyledAttributes得到一個TypedArray,然後通過TypedArray擷取屬性,重寫onDraw函數,在該函數中處理這些擷取到的屬性而已,不涉及到很多原理上的思考,另外網上關于這方面的資料也很多,是以沒貼出代碼,大家如果感興趣可以follow我的github賬号,本人将上傳該項目工程到我的github上,這個應該是目前github上開源的相關元件中功能最強大的自定義圓形進度條了,歡迎大家follow,star與fork。