1. 程式人生 > >android 自定義View 儀表盤 DashboardView 的實現

android 自定義View 儀表盤 DashboardView 的實現

有天上班,老闆突然扔給我一張圖,

說:這個東西能不能做一下。

我說應該可以。然後老闆那就沒有下文了,我想既然問了,那我就抽空做一下。

當我做出來的時候去找老闆,我說上次你給我發的那個圖,我已經做出來了,您要不要看一下。

老闆說,不用了,不需要了。

不需要了。。 不需要了 。。 不需要了!!

聽到這句話我的內心是幾乎是崩潰的,哭哭。

好吧,既然這樣,那麼就開源出來吧(github地址),並且寫下了這篇部落格作為實現過程的記錄,也希望能給一部分人帶來一些幫助。

另:本人可能姿勢水平不太高,如果有什麼錯誤還請大家幫忙指正 , 蟹蟹。

好了 , 進入正題

首先放一張實現完成的gif

第一步,我們先在attrs檔案下新增我們的自定義屬性:

<declare-styleable name="DashboardView">
        <attr name="arcColor" format="color"/>
        <attr name="padding" format="dimension"/>
        <attr name="android:text"/>
        <attr name="tikeCount" format="integer"/>
        <attr
name="Unit" format="string"/>
<attr name="android:textSize"/> <attr name="backgroundColor" format="color" /> <attr name="textColor" format="color"/> <attr name="startProgressColor" format="color" /> <attr name="endProgressColor" format
="color" />
<attr name="startNumber" format="integer" /> <attr name="maxNumber" format="integer" /> <attr name="progressColor" format="color"/> </declare-styleable>

第二部,新建一個java類,來管理這些屬性

public class DashboardViewAttr {
    private int mTikeCount;
    ...

    public DashboardViewAttr(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DashboardView, defStyleAttr, 0);
        mTikeCount = ta.getInt(R.styleable.DashboardView_tikeCount, 48);
        ...
        ta.recycle();
    }

    public int getmTikeCount() {
        return mTikeCount;
    }
   ...
}

注意,當使用 TypedArray 進行載入屬性的時候,最後記得要回收一下,即呼叫TypedArray.recycle()

最後,建立我們的DashboardView,使之繼承View

     首先,實現繼承自View的三個構造方法,並且新增初始化方法,在帶有三個引數的構造方法下面例項化我們剛剛建立的屬性管理類。
    public DashboardView(Context context) {
        this(context, null);
        init(context);
    }

    public DashboardView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        init(context);
    }

    public DashboardView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        dashboardViewattr = new DashboardViewAttr(context, attrs, defStyleAttr);
        init(context);
    }
    init方法中又分成兩個方法,1、初始化自定義屬性,2、初始化各個畫筆:
        //初始化自定義屬性
        mTikeCount = dashboardViewattr.getmTikeCount();
        mTextSize = dashboardViewattr.getmTextSize();
        mTextColor = dashboardViewattr.getTextColor();
        mText = dashboardViewattr.getmText();
        unit = dashboardViewattr.getUnit();
        backgroundColor = dashboardViewattr.getBackground();
        startColor = dashboardViewattr.getStartColor();
        endColor = dashboardViewattr.getEndColor();
        startNum = dashboardViewattr.getStartNumber();
        maxNum = dashboardViewattr.getMaxNumber();
        progressColor = dashboardViewattr.getProgressColor();
        //初始化畫筆
        paintProgress.setAntiAlias(true);//設定抗鋸齒
        paintProgress.setStrokeWidth(progressHeight);//設定畫筆寬度
        paintProgress.setStyle(Paint.Style.STROKE);//設定畫筆為空心
        paintProgress.setStrokeCap(Paint.Cap.ROUND);//設定畫筆筆觸為圓形
        paintProgress.setColor(progressColor);//設定畫筆顏色
        paintProgress.setDither(true);//設定防抖動

        ...
    現在我們來重寫我們的 onMeasure 方法, 目的是之在非EXACTLY模式下,也就是空間寬高指定為wrap_centent的時候,給他規定一個最大值。
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int realWidth = startMeasure(widthMeasureSpec);
        int realHeight = startMeasure(heightMeasureSpec);

        setMeasuredDimension(realWidth, realHeight);
    }


    private int startMeasure(int msSpec) {
        int result = 0;
        int mode = MeasureSpec.getMode(msSpec);
        int size = MeasureSpec.getSize(msSpec);
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            result = PxUtils.dpToPx(200, mContext);
        }
        return result;
    }
    終於到了最重要的環節之一了。也就是繪製環節,現在我們來重寫onDraw方法,獲取Canvas。

    首先,先把座標原點移動到中心,以方便繪製
    canvas.translate(mWidth/2,mHight/2);
    然後,繪製錶盤,也就是該View上不會動的東西。
    //繪製最外層的圓和背景色(如果設定了背景色的話)
    private void drawBackground(Canvas canvas) {
        //最外陰影線
        canvas.drawCircle(0, 0, mWidth / 2 - 2, paintOutCircle);
        canvas.save();
        //背景
        if (backgroundColor != 0) {
            paintBackground.setColor(backgroundColor);
            canvas.drawCircle(0, 0,mWidth / 2  -4, paintBackground);
        }
    }
    根據設定的刻度數,繪製刻度(預設48)
    private void drawerNum(Canvas canvas) {
        canvas.save(); //記錄畫布狀態
        canvas.rotate(-(180 - START_ARC + 90), 0, 0);
        int numY = -mHight / 2 + OFFSET + progressHeight;
        float rAngle = DURING_ARC / mTikeCount;
        for (int i = 0; i < mTikeCount + 1; i++) {
            canvas.save(); //記錄畫布狀態
            canvas.rotate(rAngle * i,0, 0);
            if (i == 0 || i  % 3 ==0){
                canvas.drawLine(0 , numY + 5, 0, numY + 25, paintNum);//畫長刻度線
            }else {
                canvas.drawLine(0 , numY + 5, 0, numY + 15, paintNum);//畫短刻度線
            }
            canvas.restore();
        }
        canvas.restore();

    }
    save()為儲存畫布,
    restore()為恢復上次儲存的畫布,
    retate()為旋轉畫布。

    利用這三個方法,可以很輕鬆的繪製刻度


然後繪製中心的小圓和小環。
    private void drawInPoint(Canvas canvas) {
        mMinCircleRadius = mWidth / 15 ;
        mMinRingRadius = mMinCircleRadius *2 + mMinCircleRadius / 20;
        paintCenterRingPointer.setStrokeWidth(mMinCircleRadius);
        canvas.drawCircle(0, 0, mMinCircleRadius, paintCenterCirclePointer);//中心圓點
        canvas.drawCircle(0, 0, mMinRingRadius, paintCenterRingPointer);//中心小圓環

    }
接下來我們來繪製能動的部分

首先,弧形progressbar
    private void drawProgress(Canvas canvas, float percent) {
        rectF2 = new RectF( -mWidth/2  + OFFSET,  - mHight /2 + OFFSET, mWidth/2  - OFFSET, mHight/2 - OFFSET);

        canvas.drawArc(rectF2, START_ARC, DURING_ARC, false, paintProgressBackground);
        if (percent > 1.0f) {
            percent = 1.0f; //限制進度條在彈性的作用下不會超出
        }
        if (!(percent <= 0.0f )) {
            canvas.drawArc(rectF2, START_ARC, percent * DURING_ARC, false, paintProgress);
        }
    }
繪製錶針
    private void drawerPointer(Canvas canvas, float percent) {
        mMinCircleRadius = mWidth / 15 ;
        rectF1 = new RectF( - mMinCircleRadius / 2, - mMinCircleRadius / 2, mMinCircleRadius / 2 ,  mMinCircleRadius / 2);
        canvas.save();
        float angel = DURING_ARC * (percent - 0.5f) - 180 ;
        canvas.rotate(angel, 0, 0);//指標與外弧邊緣持平
        Path pathPointerRight = new Path();
        pathPointerRight.moveTo(0,  mMinCircleRadius / 2);
        pathPointerRight.arcTo(rectF1,270,-90);
        pathPointerRight.lineTo(0, mHight / 2  - OFFSET- progressHeight);
        pathPointerRight.lineTo(0,  mMinCircleRadius / 2);
        pathPointerRight.close();
        Path pathPointerLeft = new Path();
        pathPointerLeft.moveTo( 0,  mMinCircleRadius / 2);
        pathPointerLeft.arcTo(rectF1,270,90);
        pathPointerLeft.lineTo(0, mHight/2  - OFFSET- progressHeight);
        pathPointerLeft.lineTo(0,  mMinCircleRadius / 2);
        pathPointerLeft.close();
        Path pathCircle = new Path();
        pathCircle.addCircle(0, 0, mMinCircleRadius / 4, Path.Direction.CW);
        canvas.drawPath(pathPointerLeft,paintPointerLeft);
        canvas.drawPath(pathPointerRight, paintPointerRight);
        canvas.drawPath(pathCircle, paintPinterCircle);
        canvas.restore();

    }
錶針分為三個部分:1、指標左半部分,2、指標右半部分,3、指標上的圓點。

考慮到相容性的問題,在這裡繪製指標並沒有採用Path()的布林運算 ,而是使用的Path基礎的lineTo() , arcTo() ,moveTo()等方法。一樣能實現同樣的效果。

    最後,繪製文字。

    private void drawText(Canvas canvas, float percent) {
        float length ;
        paintText.setTextSize(mTextSize);
        length = paintText.measureText(mText);
        canvas.drawText(mText,-length /2, mMinRingRadius*2.0F,  paintText);
        paintText.setTextSize(mTextSize * 1.2f);
        speed = StringUtil.floatFormat(startNum + (maxNum - startNum) * percent) + unit;
        length = paintText.measureText(speed);
        canvas.drawText(speed, -length /2 , mMinRingRadius*2.5F, paintText);
    }
    可以看到,這三個方法比上面的多了個float percent 引數,我們將根據這個引數來改變指標的角度,progress的進度以及數字的變化。

    這個引數其實就是seekbar傳進來的progress值。其實到現在可以算是結束了。但是指標和progress的變化都特別生硬,所以我們要給他加上一個動畫效果。
public void setPercent(int percent) {
        setAnimator(percent);
    }
    private void setAnimator(final float percent) {
        //根據變化的幅度來調整動畫時長
        animatorDuration = (long) Math.abs(percent - oldPercent) * 20;

        valueAnimator = ValueAnimator.ofFloat(oldPercent,percent).setDuration(animatorDuration);

        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //把獲取到的
                DashboardView.this.percent = (float) animation.getAnimatedValue();
                invalidate();

            }

        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                oldPercent = percent;
            }
        });
        valueAnimator.start();

    }
    在這裡我們用到了ValueAnimator,ValuAnimator本質上就是通過設定一個起始值和結束值,來取到一個從起始值到結束值的一個逐漸增長的Animation值。在draw方法中使用這個值並且不斷的重繪,就能達到一種動畫效果。

    我們通過ofFloat來設定起始值和結束值,並且動態的設定了動畫時長。通過addUpdateListener(AnimatorUpdateListener)來獲取不斷變化的Animation值,並且重繪。

    最後,呼叫valueAnimator.start() 我們的動畫效果就這麼完成了,但是做完這步你會發現,動畫是有了,但是這指標是勻速轉動的,不太理想。

    這時候,就要用到我們的Interpolator插值器了,安卓自帶的插值器能夠使Animation值的變化產生加速增長、減速增長、先加速後減速、回彈等效果。但是安卓自帶的幾個插值器用在我們這裡也不太理想,不太符合真正的儀表盤的變化效果。

    所以在這裡我們決定自定義一個插值器來滿足我們的需求。

    首先確定插值器的曲線圖

    數學表示式為
    pow(2, -10 * x) * sin((x - factor / 4) * (2 * PI) / factor) + 1 
    factor = 0.4 這個我們在建構函式的時候指定

    新建類SpringInterpolator,繼承自BaseInterpolator,將上面的數學表示式轉化成程式碼,完整的程式碼為:

public class SpringInterpolator implements Interpolator {
    private final float mTension;
    public SpringInterpolator() {
        mTension = 0.4f;
    }
    public SpringInterpolator(float tension) {
        mTension = tension;
    }

    @Override
    public float getInterpolation(float input) {
        float result = (float) (Math.pow(2,-10 * input) *
                Math.sin((input - mTension / 4) * (2 * Math.PI)/mTension) + 1);
        return result;
    }
}
    好了,我們把這個自定義插值器設定給我們的VuleAnimation就能達到預期的效果了。

    當然,我們可以改變tension來改變指標的擺動效果,不過我認為0.4是一個很合理的值。

至此,我們的自定義View就算是完成了,感謝大家的耐心觀看,如果有什麼錯誤或者不足之處,還請多多指點。