Android自定義View系列(一)——打造一個愛心進度條

寫作原因:Android進階過程中有一個繞不開的話題——自定義View。這一塊是安卓程序員更好地實現功能自主化必須邁出的一步。下面這個系列博主將通過實現幾個例子來認識安卓自定義View的方法。從自定義View到自定義ViewGroup,View事件處理再到View深入分析(這一章如果水平未到位可能今后再補充),其中會涉及一些小的知識,包括Canvas的使用、動畫等等。系列第一篇文章通過繪制一個心形進度條來學習自定義View的整體流程和簡單地貝塞爾曲線的用法。下面開始折磨鍵盤吧。

最終效果

先看看今天我們要實現的效果:

具體功能就是一個心形的進度條,跟普通的進度條相似,但是顯示進度的方式和整體的外觀實現了自定義化。這個進度條會根據進度不斷加深顏色,效果還不錯。通過這個例子讀者可以學會基本的自定義View的方法。

基本思路

我們需要新建一個attrs.xml來描述HeartProgressBar的屬性,一個HeartProgressBar.java繼承ProgressBar(直接繼承View也行),然后通過取出屬性,測量,繪制幾步來實現自定義View全過程。

具體實現

一、定義XML屬性文件

新建values/attrs.xml,在XML中聲明好各個屬性,注意一下format,詳細的用法參考官方文檔。然后在

標簽下引入屬性,具體見下面:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="UnReachedColor" format="color"/>
    <attr name="ReachedColor" format="color"/>
    <attr name="InnerTextColor" format="color"/>
    <attr name="InnerTextSize" format="dimension"/>
    <attr name="Progress" format="integer"/>
    <declare-styleable name="HeartProgressBar">
        <attr name="UnReachedColor"/>
        <attr name="ReachedColor" />
        <attr name="InnerTextColor" />
        <attr name="InnerTextSize" />
        <attr name="Progress"/>
    </declare-styleable>
</resources>

 

二、獲取XML中的屬性

這一步我們使用obtainAttributes()方法來獲取開發者在布局中為我們的View設定的參數值。通過 TypedArray ta = getResources().obtainAttributes(attrs,R.styleable.HeartProgressBar); 獲得TypedArray對象,使用該對象的get系列方法來獲取參數值,如: unReachedColor = ta.getColor(R.styleable.HeartProgressBar_UnReachedColor,UNREACHEDCOLOR_DEFAULT); 。后面的UNREACHEDCOLOR_DEFAULT是默認參數值,是view創建者定義的,注意尺寸相關的需要進行單位轉換。這樣就取到了View的參數,這些參數是我們用來定義View的部分元素。

三、調用onMeasure()測量

這一步常常令許多人頭大甚至望而卻步,看了很多資料也理解不了。我在這里分享一下對于這一塊的理解,希望能夠幫助大家理解。首先得明白View的規格是受承載它的View或者ViewGroup影響的。這點很重要,因為這點才出現了三種測量模式:MeasureSpec.EXACTLY、MeasureSpec.UNSPECIFIED和MeasureSpec.AT_MOST。這三種測量模式分別對應三種情況:View有確定的寬高(包括match_parent和具體值兩種情況),此時使用EXACTLY,直接把MeasureSpec.getSize()返回就行;View沒有確定的寬高(即wrap_content),此時可能系統會使用MeasureSpec.UNSPECIFIED或者MeasureSpec.AT_MOST。在MeasureSpec.UNSPECIFIED中我們把盡量容納View的尺寸返回給父View去處理,而在MeasureSpec.AT_MOST中則由于父View對子View的限制需要比對父View的限制的最大情況和子View盡可能容納的尺寸,然后返回相對較小的值。看看本文的例子:

int usedHeight = getRealHeight(heightMeasureSpec);
int usedWidth = getRealWidth(widthMeasureSpec);
setMeasuredDimension(usedWidth,usedHeight);

這里將寬高測量后使用setMeasureDimension()返回給父View去處理。

以寬為例,代碼如下:

public int getRealWidth(int widthMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthVal = MeasureSpec.getSize(widthMeasureSpec);
        //取得父View或ViewGroup分配的默認為子View的大小和模式
        paddingLeft = getPaddingLeft();
        paddingRight = getPaddingRight();
        //注意處理一下Padding,把Padding值也賦給想要設置的子View的大小
        if(widthMode == MeasureSpec.EXACTLY){
            return paddingLeft+paddingRight+widthVal;
            //精確模式下返回具體值
        }else if(widthMode == MeasureSpec.UNSPECIFIED){
            return (int) (Math.abs(underPaint.ascent()-underPaint.descent()) + paddingLeft + paddingRight);
            //未確定模式下返回盡可能容納View的尺寸
        }else{
            return (int) Math.min((Math.abs(underPaint.ascent()-underPaint.descent()) + paddingLeft + paddingRight),widthVal);
            //AT_MOST下返回父View限制值和View盡可能大尺寸之間的最小值
        }
    }

使用MeasureSpec.getMode()和getSize()分別取得測量模式和大小,然后處理三種測量模式下的大小值,最后再使用setMeasureDimension()將值返回。對于三種模式下的處理見上面的注釋。自定義View中的測量是一塊難點,應該詳細閱讀并實踐。

四、重寫onSizeChanged()獲取最終View的寬高值

當父View真正為子View分配好空間后會回調這個方法,所以我們應該在里面取得最終的大小值。代碼如下:

realWidth = w;
realHeight = h;

五、重寫onDraw()方法繪制圖像

這一步是整個流程中的重點步驟,所有的繪制工作都是在這里進行的。一般繪制的時候都要考慮尺寸問題,我們使用的寬高尺寸是onSizeChanged里面取出的,使用該尺寸來繪制,注意一定要對Padding進行處理(博主寫完發現沒處理,現在要修改發現很浪費時間就沒做處理了。。。讀者可以自行處理作為練習)這里有個難點,由于心形的特殊性,我們使用貝塞爾曲線來繪制。關于貝塞爾曲線,在這里就不班門弄斧了,只是說一下這種曲線可以將許多復雜的曲線轉化成數學公式來描述,曲線由兩種類型的點決定:起末點決定曲線的大概位置,其他點決定曲線的形狀和彎曲程度。貝塞爾曲線有一階二階三階高階之分,具體見下面圖片。

安卓中支持二階和三階貝塞爾曲線,方法分別為quadTo()和cubicTo()兩個。本文使用cubicTo來實現。但是想想心形的形狀,如果真的要自己用數學的方法去確定那幾個點的位置,呵呵,我是辦不到了。那怎么辦?博主找到一個方法,在線生成貝塞爾曲線然后用ps標尺工具來確定點與寬高的比例關系……雖然還是很麻煩,但我還沒想到別的方法或者工具。( Canvas二次貝塞爾曲線操作實例 ,這是一個為h5服務的貝塞爾曲線生成工具,將就用下……)如果讀者有希望能分享一下,謝謝!

這是我得到的測量圖(就是在這里忘了考慮padding,改起來又很麻煩):

接下來就是利用這張圖和Canvas來作圖了,主要路徑關注路徑和顏色深淺表示進度的實現:

float pro = ((float)progress)/100.0f;
      int nowColor = (int) argbEvaluator.evaluate(pro,unReachedColor,reachedColor);
      underPaint.setColor(nowColor);

上面代碼實現了View的顏色隨著進度的變化而從某個顏色向另一個顏色變化的功能。ArgbEvaluator類挺實用的,可以實現微信底部滑動顏色變化的功能,這里也是利用它來實現的。

path.moveTo((float) (0.5*realWidth), (float) (0.17*realHeight));
path.cubicTo((float) (0.15*realWidth), (float) (-0.35*realHeight), (float) (-0.4*realWidth), (float) (0.45*realHeight), (float) (0.5*realWidth),realHeight);
path.moveTo((float) (0.5*realWidth),realHeight);
path.cubicTo((float) (realWidth+0.4*realWidth), (float) (0.45*realHeight),(float) (realWidth-0.15*realWidth), (float) (-0.35*realHeight),(float) (0.5*realWidth), (float) (0.17*realHeight));
path.close();

上述代碼是path路徑的繪制,繪制了一個心形的path,如果對于這兩個方法有疑問的可以查看API文檔。

由于我們的進度條跟隨進度發生變化,所以我們要重寫setProgress()方法,使用invalidate()來刷新onDraw()重繪實現變化。代碼如下:

@Override
    public void setProgress(int progress) {
        this.progress = progress;
        invalidate();
    }

總結

Android自定義View除了上面的測量繪制之外還有對點擊事件的處理一大塊,這里每個地方都需要花時間去理解和實踐才能搞懂,下篇博主會就事件處理和動畫一塊再次自定義一個View,如果覺得寫得好的希望繼續關注并喜歡我的簡書,也可以關注我的博客。

附錄:View具體代碼

public class HeartProgressBar extends ProgressBar {

    private final static int UNREACHEDCOLOR_DEFAULT = 0xFF69B4;
    private final static int REACHEDCOLOR_DEFAULT = 0xFF1493;
    private final static int INNERTEXTCOLOR_DEFAULT = 0xDC143C;
    private final static int INNERTEXTSIZE_DEFAULT = 10;
    private static final int PROGRESS_DEFAULT = 0;
    private int unReachedColor;
    private int reachedColor;
    private int innerTextColor;
    private int innerTextSize;
    private int progress;
    private int realWidth;
    private int realHeight;
    private Paint underPaint;
    private Paint textPaint;
    private Path path;
    private int paddingTop;
    private int paddingBottom;
    private int paddingLeft;
    private int paddingRight;
    private ArgbEvaluator argbEvaluator;
    public HeartProgressBar(Context context) {
        this(context,null);
    }

    public HeartProgressBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        argbEvaluator = new ArgbEvaluator();
        TypedArray ta = getResources().obtainAttributes(attrs,R.styleable.HeartProgressBar);
        unReachedColor = ta.getColor(R.styleable.HeartProgressBar_UnReachedColor,UNREACHEDCOLOR_DEFAULT);
        reachedColor = ta.getColor(R.styleable.HeartProgressBar_ReachedColor,REACHEDCOLOR_DEFAULT);
        innerTextColor = ta.getColor(R.styleable.HeartProgressBar_InnerTextColor,INNERTEXTCOLOR_DEFAULT);
        innerTextSize = (int) ta.getDimension(R.styleable.HeartProgressBar_InnerTextSize,INNERTEXTSIZE_DEFAULT);
        progress = ta.getInt(R.styleable.HeartProgressBar_Progress,PROGRESS_DEFAULT);
        ta.recycle();
        Log.i("nowColor",progress+"");
        //聲明區
        underPaint = new Paint();
        textPaint = new Paint();
        path = new Path();
        //構造畫筆區
        underPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        underPaint.setStrokeWidth(5.0f);
        textPaint.setColor(innerTextColor);
        textPaint.setTextSize(innerTextSize);
        textPaint.setTextAlign(Paint.Align.CENTER);
    }

    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int usedHeight = getRealHeight(heightMeasureSpec);
        int usedWidth = getRealWidth(widthMeasureSpec);
        setMeasuredDimension(usedWidth,usedHeight);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        realWidth = w;
        realHeight = h;
    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paddingBottom = getPaddingBottom();
        paddingTop = getPaddingTop();
        paddingLeft = getPaddingLeft();
        paddingRight = getPaddingRight();
        float pro = ((float)progress)/100.0f;
        Log.i("nowColor","pro"+pro+"");
        int nowColor = (int) argbEvaluator.evaluate(pro,unReachedColor,reachedColor);
        underPaint.setColor(nowColor);
        path.moveTo((float) (0.5*realWidth), (float) (0.17*realHeight));
        path.cubicTo((float) (0.15*realWidth), (float) (-0.35*realHeight), (float) (-0.4*realWidth), (float) (0.45*realHeight), (float) (0.5*realWidth),realHeight);
        path.moveTo((float) (0.5*realWidth),realHeight);
        path.cubicTo((float) (realWidth+0.4*realWidth), (float) (0.45*realHeight),(float) (realWidth-0.15*realWidth), (float) (-0.35*realHeight),(float) (0.5*realWidth), (float) (0.17*realHeight));
        path.close();
        canvas.drawPath(path,underPaint);
        canvas.drawText(String.valueOf(progress),realWidth/2,realHeight/2,textPaint);
    }


    public int getRealHeight(int heightMeasureSpec) {
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightVal = MeasureSpec.getSize(heightMeasureSpec);
        paddingTop = getPaddingTop();
        paddingBottom = getPaddingBottom();
        if(heightMode == MeasureSpec.EXACTLY){
            return paddingTop + paddingBottom + heightVal;
        }else if(heightMode == MeasureSpec.UNSPECIFIED){
            return (int) (Math.abs(underPaint.ascent()-underPaint.descent()) + paddingTop + paddingBottom);
        }else{
            return (int) Math.min((Math.abs(underPaint.ascent()-underPaint.descent()) + paddingTop + paddingBottom),heightVal);
        }
    }

    public int getRealWidth(int widthMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthVal = MeasureSpec.getSize(widthMeasureSpec);
        paddingLeft = getPaddingLeft();
        paddingRight = getPaddingRight();
        if(widthMode == MeasureSpec.EXACTLY){
            return paddingLeft+paddingRight+widthVal;
        }else if(widthMode == MeasureSpec.UNSPECIFIED){
            return (int) (Math.abs(underPaint.ascent()-underPaint.descent()) + paddingLeft + paddingRight);
        }else{
            return (int) Math.min((Math.abs(underPaint.ascent()-underPaint.descent()) + paddingLeft + paddingRight),widthVal);
        }
    }

    @Override
    public void setProgress(int progress) {
        this.progress = progress;
        invalidate();
    }
}

 

系列文章

Android自定義View系列(一)——打造一個愛心進度條

Android自定義View系列(二)——打造一個仿2K游戲搖桿

 

閱讀原文

 

 本文由用戶 licong1520 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!