在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();
}
}
}
效果圖如下
多畫三條線看先水準和轉動前後的幾個位置。
其實自定義View了解起來不算難,不過當效果過于華麗的時候就很麻煩,可能需要用很多方法甚至像貝塞爾曲線或者其他輔助自定義View的工具,當然如果隻是用到這些還不最難,慢慢了解運用,熟悉就好了,最可怕的是形狀或者運動軌迹過于複雜,這就可能很考驗數學功底了,沒準都得拿出演草紙算一下... ...不過我覺得當一個開發者能經常受到這樣的需求,沒事就頭腦風暴一下也是不錯的,就像學生時代做數學題一樣,譬如這個計步器我最開始就打算把刻度表正好放到Canva底部,這就需要用一下勾股定理算角度或者邊長了,以前我也見過大神寫的極其複雜的View,代碼寫的真跟數學題一樣,各種算,當時我就迷弟了。
這次部落格比較簡單,一方面不想把部落格都寫的那麼老長,一方面也是沒想出來應該再添加點什麼效果體驗,但還是希望能給有需要的朋友一點點幫助吧,以後有什麼想法了再去嘗試整理。