天天看點

Android自定義刻度計步器

    在Android開發中,View作為與使用者互動的媒介,其重要意義不用多說,雖然Google官方提供了不少優秀的View,但還是有很多複雜的視圖,是需要開發者自己去實作的,不知不覺我也寫了7篇部落格了,感覺怎麼也該寫篇關于自定義View的了,想起以前朋友說需要做一個計步器,好像還不太順利,也不知道是怎樣的需求,我就随便簡單寫一個吧。

    首先裝作很厲害的樣子講下自定義view的劃分,一般按繼承來分,第一類是繼承官方已經封裝一定功能的view:如TextView、Button等等,第二類是直接繼承View。Android進階神書《Android開發藝術》中,大神剛哥把自定義View分成了繼承View、ViewGroup、官方封裝好的View和ViewGroup四類,不過ViewGroup也是繼承自View,封裝了管理子view的功能,是以說分成兩類我覺得也是合适的,不過這都不是重點,可以忽略,Android萌新如果碰到無聊的面試官問可以這麼跟他吹下。

     接下來就來實作刻度計步器:

一、首先要繼承View,重寫構造類,一般選擇前三個。

public class StepNumView extends View {
    public StepNumView(Context context) {
        this(context, null);
    }

    public StepNumView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StepNumView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}
           

這三個構造器第一個是java代碼初始化View時用到的,第二個是xml檔案布局中用到的,第三個是自定義view時,其中defStyleAttr用于自定義屬性。為了簡便需要在構造器中初始化的操作,是以讓構造器依次調用下來,保證不管用什麼方式初始化View,都能走到三個參數的構造器方法,然後我們隻需在三個參數的構造器方法中做初始化操作就好了。

二、一般我們還需要重寫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);
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(stepViewWidth, stepViewHeight);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize, stepViewHeight);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(stepViewWidth, heightSize);
        }
    }
           

這個方法基本都通用了,擷取View的布局方式和大小,如果布局方式為wrap_content(自适應,對應MeasureSpec的AT_MOST),那麼就需要我們給一個預設大小。一般如果自定義ViewGroup還需要重寫onLayout方法,用來排布子View。

三、最重要的就是畫View了

1.首先寫出來步數

初始化畫筆和文字區域

textRect = new Rect();
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setColor(textColor);
textPaint.setTextSize(textSize);
           

畫出文字

private void drawText(Canvas canvas) {
        String stepStr = stepNum + "";
        textPaint.getTextBounds(stepStr, 0, stepStr.length(), textRect);
        canvas.drawText(stepStr, toCenterX - textRect.width() / 2, toCenterY, textPaint);
    }
           

2.然後刻度計步器要有一個刻度盤,我們将Canvas移動到刻度盤圓心點高度,然後逆時針旋轉60度,畫一條短線,然後旋轉3度再畫一條,知道旋轉180+60+60=300度的時候停止,Canvas恢複save記錄位置。

private void drawDevice(Canvas canvas) {
        canvas.save();
        canvas.translate(0, toCenterY);//移動到圓心所在直線
        canvas.drawLine(0, 0, toCenterX, 0, devicePaint);
        canvas.rotate(-60, toCenterX, 0);//逆時針旋轉60度
        canvas.drawLine(radiusView, 0, toCenterX, 0, devicePaint);
        for (int i = 1; i < 100; i++) {
            canvas.drawLine(toCenterX - radiusView, 0, toCenterX - radiusView + 7, 0, devicePaint);//在距離圓心半徑位置畫一條7機關的短線
            canvas.rotate(3, toCenterX, 0);//順時針旋轉3個機關
        }
        canvas.drawLine(radiusView, 0, toCenterX, 0, devicePaint);
        canvas.restore();
    }
           

3.步數進度,與刻度盤做法類似,我這裡是每一百步畫一條線

private void drawStep(Canvas canvas) {
        canvas.save();
        canvas.translate(0, toCenterY);//移動到圓心所在直線
        canvas.rotate(-60, toCenterX, 0);//逆時針旋轉60度
        for (int i = 1; i < stepNum / 100; i++) {
            canvas.drawLine(toCenterX - radiusView, 0, toCenterX - radiusView + 7, 0, stepPaint);//在距離圓心半徑位置畫一條7機關的短線
            canvas.rotate(3, toCenterX, 0);//順時針旋轉3個機關
        }
        canvas.restore();
    }
           

最後在onDraw方法裡依次調用這三個方法就可以了,定義一個set方法接受傳進來的步數,然後重繪,就能顯示進度和步數了。整體代碼如下:

自定義屬性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="StepNumView">
        <attr name="radiusView" format="dimension"/>
        <attr name="heightView" format="dimension"/>
        <attr name="colorDevice" format="color"/>
        <attr name="colorStep" format="color"/>
        <attr name="textColor" format="color"/>
        <attr name="textSize" format="dimension"/>
    </declare-styleable>
</resources>
           

自定義View:

package com.example.administrator;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by Administrator on 2018/4/4.
 */

public class StepNumView extends View {
    private final int stepViewWidth = 300;
    private final int stepViewHeight = 300;

    private Paint devicePaint;//計步器畫筆
    private Paint stepPaint;//步數畫筆
    private Paint textPaint;//數字字元串畫筆
    private int radiusView;//計步器半徑
    private int heightView;//計步器view頂部距離canvas的高度
    private int colorDevice;//計步器顔色
    private int stepColor;//步數進度顔色
    private int textColor;//數字字元串顔色
    private int textSize;//數字字元大小
    private Rect textRect;

    public StepNumView(Context context) {
        this(context, null);
    }

    public StepNumView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StepNumView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.StepNumView, defStyleAttr, 0);
        radiusView = ta.getDimensionPixelSize(R.styleable.StepNumView_radiusView, 100);
        heightView = ta.getDimensionPixelSize(R.styleable.StepNumView_heightView, 10);
        colorDevice = ta.getColor(R.styleable.StepNumView_colorDevice, Color.GRAY);
        stepColor = ta.getColor(R.styleable.StepNumView_colorStep, Color.RED);
        textColor = ta.getColor(R.styleable.StepNumView_textColor, Color.BLACK);
        textSize = ta.getDimensionPixelSize(R.styleable.StepNumView_textSize, 18);
        ta.recycle();
        devicePaint = new Paint();
        devicePaint.setAntiAlias(true);
        devicePaint.setStyle(Paint.Style.FILL);
        devicePaint.setColor(colorDevice);
        stepPaint = new Paint();
        stepPaint.setAntiAlias(true);
        stepPaint.setStyle(Paint.Style.FILL);
        stepPaint.setColor(stepColor);

        textRect = new Rect();
        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setColor(textColor);
        textPaint.setTextSize(textSize);
    }

    @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);
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(stepViewWidth, stepViewHeight);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize, stepViewHeight);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(stepViewWidth, heightSize);
        }
    }

    private int stepNum = 0;//步數
    private int toCenterY;//畫布Y軸移動這個距離
    private int toCenterX;//圓心x軸位置位置

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        toCenterY = radiusView+heightView;
        toCenterX = width / 2;

        drawText(canvas);
        drawDevice(canvas);
        drawStep(canvas);
    }

    private void drawText(Canvas canvas) {
        String stepStr = stepNum + "";
        textPaint.getTextBounds(stepStr, 0, stepStr.length(), textRect);
        canvas.drawText(stepStr, toCenterX - textRect.width() / 2, toCenterY, textPaint);
    }

    private void drawDevice(Canvas canvas) {
        canvas.save();
        canvas.translate(0, toCenterY);//移動到圓心所在直線
        canvas.drawLine(0, 0, toCenterX, 0, devicePaint);
        canvas.rotate(-60, toCenterX, 0);//逆時針旋轉60度
        canvas.drawLine(radiusView, 0, toCenterX, 0, devicePaint);
        for (int i = 1; i < 100; i++) {
            canvas.drawLine(toCenterX - radiusView, 0, toCenterX - radiusView + 7, 0, devicePaint);//在距離圓心半徑位置畫一條7機關的短線
            canvas.rotate(3, toCenterX, 0);//順時針旋轉3個機關
        }
        canvas.drawLine(radiusView, 0, toCenterX, 0, devicePaint);
        canvas.restore();
    }

    private void drawStep(Canvas canvas) {
        canvas.save();
        canvas.translate(0, toCenterY);//移動到圓心所在直線
        canvas.rotate(-60, toCenterX, 0);//逆時針旋轉60度
        for (int i = 1; i < stepNum / 100; i++) {
            canvas.drawLine(toCenterX - radiusView, 0, toCenterX - radiusView + 7, 0, stepPaint);//在距離圓心半徑位置畫一條7機關的短線
            canvas.rotate(3, toCenterX, 0);//順時針旋轉3個機關
        }
        canvas.restore();
    }

    public void setStepNum(int stepNum) {
        this.stepNum = stepNum;
        if (stepNum != 0) {
            invalidate();
        }
    }
}
           

效果圖如下

Android自定義刻度計步器

多畫三條線看先水準和轉動前後的幾個位置。

其實自定義View了解起來不算難,不過當效果過于華麗的時候就很麻煩,可能需要用很多方法甚至像貝塞爾曲線或者其他輔助自定義View的工具,當然如果隻是用到這些還不最難,慢慢了解運用,熟悉就好了,最可怕的是形狀或者運動軌迹過于複雜,這就可能很考驗數學功底了,沒準都得拿出演草紙算一下... ...不過我覺得當一個開發者能經常受到這樣的需求,沒事就頭腦風暴一下也是不錯的,就像學生時代做數學題一樣,譬如這個計步器我最開始就打算把刻度表正好放到Canva底部,這就需要用一下勾股定理算角度或者邊長了,以前我也見過大神寫的極其複雜的View,代碼寫的真跟數學題一樣,各種算,當時我就迷弟了。

這次部落格比較簡單,一方面不想把部落格都寫的那麼老長,一方面也是沒想出來應該再添加點什麼效果體驗,但還是希望能給有需要的朋友一點點幫助吧,以後有什麼想法了再去嘗試整理。