Android -- 自定義view之StepView
先看看實現的效果:
2,首先我們來看看我們常規的自定義view的基礎步驟吧
|
|
- 初步分析
首先根據我們的上面效果,可以看到,主要是由直線、圓環、下面的文字組成,所以我們打算使用這三種view組合來形成我們上面的效果
- 準備工作
①首先我們要提供一個裝置下面文字的集合texts,我們文字有文字的大小屬性mTextSize、正常文字顏色mColorTextDefault、文字被選中時的顏色mColorTextSelect,最後還有文字距離上面圓環的距離mMarginTop
②然後我們提供相關的圓環相關的屬性,圓的半徑mCircleRadius、圓環被選中的顏色mColorCircleSelect、圓環正常時的顏色mColorCircleDefault
③再看看我們連結圓弧之間的直線屬性,直線的長度mLineLength、直線的高度mLineHeight,顏色和我們圓環預設顏色相同,就不用重新定義了
④還有一些需要定義的屬性,例如當前被選中的位置mSelectPosition,每一個測量的TextView儲存的Rect的集合mBounds,還有各種畫筆
所以我們就可以開始寫一寫程式碼了,首先建立StepView繼承View,然後初始化資料,並測量TextView,將測量資訊儲存在mBounds集合中
public class SlideStepView extends View { //先分析我們這次需要哪些預備的屬性 //存放下面文字集合 private List<String> texts; //文字大小 private int mTextSize; //文字常規顏色 private int mColorTextDefault; //文字被選擇時候的顏色 private int mColorTextSelect; //圓和文字之間的距離 private int mMarginTop; //線段和圓圈常規的顏色 private int mColorCircleDefault; //圓圈被選中的的顏色 private int mColorCircleSelect; //中間線段的整個長度 private float mLineLength; //中間線段寬度 private int mLineHeight; //圓圈的半徑 private int mCircleRadius; //選中後藍色的寬度 private int mSelectCircleStroke; //當前選中的下標 private int mSelectPosition; //儲存每個TextView的測量矩形資料 private List<Rect> mBounds; //各種畫筆 private Paint mTextPaint; private Paint mLinePaint; private Paint mCirclePaint; private Paint mCircleSelectPaint; public SlideStepView(Context context) { this(context, null); } public SlideStepView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public SlideStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //初始化數基本屬性 init(); } private void init() { //初始化資料來源容器 texts = new ArrayList<>(); mBounds = new ArrayList<>(); //新增加資料 texts.add("訂單已支付"); texts.add("商家已接單"); texts.add("騎手已接單"); texts.add("訂單已送達"); //將當前選中為2 mSelectPosition = 1; mMarginTop = 20; mCircleRadius = 30; mSelectCircleStroke = 3; //初始化文字屬性 mColorTextDefault = Color.GRAY; mColorTextSelect = Color.BLUE; mTextSize = 20; mTextPaint = new Paint(); mTextPaint.setTextSize(mTextSize); mTextPaint.setColor(mColorTextDefault); mTextPaint.setAntiAlias(true); //初始化圓圈屬性 mColorCircleDefault = Color.argb(255, 234, 234, 234); mCirclePaint = new Paint(); mCirclePaint.setColor(mColorCircleDefault); mCirclePaint.setStyle(Paint.Style.FILL); mCirclePaint.setAntiAlias(true); //初始化被選中的圓圈 mColorCircleSelect = Color.BLUE; mCircleSelectPaint = new Paint(); mCircleSelectPaint.setColor(mColorCircleSelect); mCircleSelectPaint.setStyle(Paint.Style.FILL); mCircleSelectPaint.setAntiAlias(true); // mCircleSelectPaint.setStrokeWidth(mSelectCircleStroke); //設定線段屬性 mLineHeight = 5; mLinePaint = new Paint(); mLinePaint.setColor(mColorCircleDefault); mLinePaint.setStyle(Paint.Style.FILL); mLinePaint.setStrokeWidth(mLineHeight); mLinePaint.setAntiAlias(true); //測量TextView measureText(); } /** * mTextPaint.getTextBounds 把textview 的寬度和高度,當做一個矩形,放在mBounds集合中, * 用來獲取字型的高度和寬度; * */ private void measureText() { for (int i = 0; i < texts.size(); i++) { Rect rect = new Rect(); mTextPaint.getTextBounds(texts.get(i), 0, texts.get(i).length(), rect); mBounds.add(rect); } } }
然後在onChangeSize中計算出mLineLength的長度(這裡很簡單 getWidth() - paddingLeft -paddingRight -2*mCircleRadius),重寫onDraw()方法
/**
*
* onSizeChanged() 在控制元件大小發生改變時呼叫。所以這裡初始化會被呼叫一次
*
* 作用:獲取控制元件的寬和高度的好時機
*
*
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//計算線段整條線段長度(總控制元件寬度 - Padding - 最左邊和最右邊的兩個圓的直徑)
mLineLength = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleRadius * 2;
}
/**
* 繪製view
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
//繪製線條
canvas.drawLine(mCircleRadius, mCircleRadius, getWidth() - mCircleRadius, mCircleRadius, mLinePaint);
//開始迴圈繪製view
for (int i = 0; i < texts.size(); i++) {
mTextPaint.setColor(mColorCircleDefault);
if (mSelectPosition == i) {
//繪製選中的圓圈
canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCircleSelectPaint);
mTextPaint.setColor(mColorCircleSelect);
} else {
//繪製默中的圓圈
canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCirclePaint);
}
//繪製文字
int startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop();
if (i == 0) {
canvas.drawText(texts.get(i), 0, startTextY, mTextPaint);
} else if (i == texts.size() - 1) {
canvas.drawText(texts.get(i), getWidth() - mBounds.get(i).width(), startTextY, mTextPaint);
} else {
canvas.drawText(texts.get(i), mCircleRadius + ((mLineLength / (texts.size() - 1)) * i) - (mBounds.get(i).width() / 2), startTextY, mTextPaint);
}
}
}
在佈局檔案引用 :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dip">
<com.qianmo.activitydetail.view.SlideStepView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#aaff0000"/>
</LinearLayout>
這樣應該可以實現基本效果了,看一看我們實現的效果
- 重寫onMeasure,改變測量的高度
這裡我們可以看到當我們設定我們控制元件的高度為wrap_content,控制元件缺填充了整個螢幕,這一點我們在之前的《onMeasure()原始碼分析》寫過,沒有了解過的同學,大家可以去看一下,所以我們要修改onMeasure中的方法
/**
* ##重寫onMeasure,改變測量的高度
* 這裡我們可以看到當我們設定我們控制元件的高度為wrap_content,控制元件缺填充了整個螢幕,
* 這一點我們在之前的《onMeasure()原始碼分析》寫過,沒有了解過的同學,大家可以去看一下,
* 所以我們要修改onMeasure中的方法
* <p>
* ## MeasureSpec.EXACTLY 按照自定義view的實際的高度來測量
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = mMarginTop + 2 * mCircleRadius + mBounds.get(0).height();
//高度
Log.i("wangjitao:", "mMarginTop:" + mMarginTop + ",mCircleRadius:" + mCircleRadius + ",mBounds:"
+ mBounds.get(0).height() + ",height" + height);
}
//儲存測量結果
setMeasuredDimension(widthSize, height);
}
再看一下我們的執行效果:
-
對canvas.drawText()方法進行理解
我們這時候將我們前面的init()方法中的mMarginTop修改為0,mMarginTop代表下面文字距離上面圓環的距離,設定為0的話就表示我們的文字的text剛好貼在這個圓環的下面,但是實際效果不是這個樣子的,看一下執行的效果
這裡我們可以看到我們的文字和我們的圓弧重疊了,這是為什麼呢? 我們的程式碼邏輯也問題啊,為什麼會出現這個問題呢?我們下來看一下下面這張text的展示圖就知道了
1 2 3 4 5 |
|
這是我們自定義View中text的一些屬性,有人會問,樓豬啊 ,為什麼要讓我們瞭解這個些知識呢?因為我們的上面出的重疊問題就是這一點的問題,在我們的正常思維的認知中我們的canvas.drawText的第三個引數是Y座標的起始點,而我們上面的程式碼Y座標的計算方式是 startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop();我們的主觀思維也感覺沒問題,但是讓我們看一下canvas.drawText()方法的原始碼
/**
* Draw the text, with origin at (x,y), using the specified paint. The
* origin is interpreted based on the Align setting in the paint.
*
* @param text The text to be drawn
* @param x The x-coordinate of the origin of the text being drawn
* @param y The y-coordinate of the baseline of the text being drawn
* @param paint The paint used for the text (e.g. color, size, style)
*/
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
native_drawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags,
paint.getNativeInstance(), paint.mNativeTypeface);
}
看到沒有“@param y The y-coordinate of the baseline of the text being drawn” 這個方法中我們的y引數表示我們的baseline,而不是我們之前的想當然的test的top屬性,所以我們要修改startTextY 的計算方式為
//這裡要對基線進行理解
int startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop(); //以前
Log.i("wangjitao", "以前:" + startTextY);
//現在是這樣的,首先獲取基線物件
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
startTextY = getHeight() - (int) fontMetrics.bottom;
另外要知道的知識點:
Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setTextSize(textSize);
mPaint.setTextAlign(Paint.Align.CENTER);
final float ascent = mPaint.ascent();
final float descent = mPaint.descent();
ok,再看看我們的執行效果
沒什麼問題了
-
重寫onTouch()方實現側滑更換當前選中位置
這個沒什麼好講的,就是向左滑動和向右滑動改變當前選中位置而已,程式碼如下:
private float downX;
private float upX;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
//按下手指的時候記錄下按下的位置
case MotionEvent.ACTION_DOWN:
Log.e("wangjitao", "手指按下: getX:" + downX);
downX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
Log.i("wangjitao", "手指滑動: ");
break;
case MotionEvent.ACTION_UP:
upX = event.getX();
Log.e("wangjitao", "手指擡起: " + upX);
if (downX - upX > 50) {
downX = 0;
upX = 0;
//向左滑動
//判斷做滑動的時候當前選擇點時候在在初始狀態下
if (mSelectPosition != 0) {
//更新view
mSelectPosition--;
} else {
mSelectPosition = texts.size() - 1;
}
invalidate();
} else if (upX - downX > 50) {
//向右滑動
downX = 0;
upX = 0;
//判斷做滑動的時候當前選擇點時候在最後一個點上
if (mSelectPosition != texts.size() - 1) {
//更新view
mSelectPosition++;
} else {
mSelectPosition = 0;
}
invalidate();
} else {
downX = 0;
upX = 0;
}
break;
}
return true;
}
再把最後所有的程式碼貼出來:
package com.githang.stepview.demo;
import android.content.Context;
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.util.Log;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
/**
* Created by dingxujun on 2018/12/10.
*
* @project StepView-master
*/
public class MyStepView extends View {
public static final String TAG = MyStepView.class.getName();
//先分析我們這次需要哪些預備的屬性
//存放下面文字集合
private List<String> texts;
//文字大小
private int mTextSize;
//文字常規顏色
private int mColorTextDefault;
//文字被選擇時候的顏色
private int mColorTextSelect;
//圓和文字之間的距離
private int mMarginTop;
//線段和圓圈常規的顏色
private int mColorCircleDefault;
//圓圈被選中的的顏色
private int mColorCircleSelect;
//中間線段的整個長度
private float mLineLength;
//中間線段寬度
private int mLineHeight;
//圓圈的半徑
private int mCircleRadius;
//選中後藍色的寬度
private int mSelectCircleStroke;
//當前選中的下標
private int mSelectPosition;
//儲存每個TextView的測量矩形資料
private List<Rect> mBounds;
//各種畫筆
private Paint mTextPaint;
private Paint mLinePaint;
private Paint mCirclePaint;
private Paint mCircleSelectPaint;
public MyStepView(Context context) {
this(context, null);
}
public MyStepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化數基本屬性
init();
}
private void init() {
//初始化資料來源容器
texts = new ArrayList<>();
mBounds = new ArrayList<>();
//新增加資料
texts.add("訂單已支付");
texts.add("商家已接單");
texts.add("騎手已接單");
texts.add("訂單已送達");
//將當前選中為2
mSelectPosition = 1;
mMarginTop = 0;
mCircleRadius = 30;
mSelectCircleStroke = 3;
//初始化文字屬性
mColorTextDefault = Color.GRAY;
mColorTextSelect = Color.BLUE;
mTextSize = 20;
mTextPaint = new Paint();
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mColorTextDefault);
mTextPaint.setAntiAlias(true);
//初始化圓圈屬性
mColorCircleDefault = Color.argb(255, 234, 234, 234);
mCirclePaint = new Paint();
mCirclePaint.setColor(mColorCircleDefault);
mCirclePaint.setStyle(Paint.Style.FILL);
mCirclePaint.setAntiAlias(true);
//初始化被選中的圓圈
mColorCircleSelect = Color.BLUE;
mCircleSelectPaint = new Paint();
mCircleSelectPaint.setColor(mColorCircleSelect);
mCircleSelectPaint.setStyle(Paint.Style.FILL);
mCircleSelectPaint.setAntiAlias(true);
// mCircleSelectPaint.setStrokeWidth(mSelectCircleStroke);
//設定線段屬性
mLineHeight = 5;
mLinePaint = new Paint();
mLinePaint.setColor(mColorCircleDefault);
mLinePaint.setStyle(Paint.Style.FILL);
mLinePaint.setStrokeWidth(mLineHeight);
mLinePaint.setAntiAlias(true);
//測量TextView
measureText();
}
private void measureText() {
for (int i = 0; i < texts.size(); i++) {
Rect rect = new Rect();
mTextPaint.getTextBounds(texts.get(i), 0, texts.get(i).length(), rect);
mBounds.add(rect);
}
}
/**
* ##重寫onMeasure,改變測量的高度
* 這裡我們可以看到當我們設定我們控制元件的高度為wrap_content,控制元件缺填充了整個螢幕,
* 這一點我們在之前的《onMeasure()原始碼分析》寫過,沒有了解過的同學,大家可以去看一下,
* 所以我們要修改onMeasure中的方法
* <p>
* ## MeasureSpec.EXACTLY 按照自定義view的實際的高度來測量
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = mMarginTop + 2 * mCircleRadius + mBounds.get(0).height();
//高度
Log.i("wangjitao:", "mMarginTop:" + mMarginTop + ",mCircleRadius:" + mCircleRadius + ",mBounds:"
+ mBounds.get(0).height() + ",height" + height);
}
//儲存測量結果
setMeasuredDimension(widthSize, height);
}
/**
* onSizeChanged() 在控制元件大小發生改變時呼叫。所以這裡初始化會被呼叫一次
* <p>
* 作用:獲取控制元件的寬和高度
* <p>
* 然後在onChangeSize中計算出mLineLength的長度
* (這裡很簡單 getWidth() - paddingLeft -paddingRight -2*mCircleRadius),重寫onDraw()方法
*
* @param w
* @param h
* @param oldw
* @param oldh
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//計算線段整條線段長度(總控制元件寬度 - Padding - 最左邊和最右邊的兩個圓的直徑)
mLineLength = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleRadius * 2;
Log.e(TAG, "線段長度" + mLineLength);
}
/**
* 繪製view
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
//繪製線條
canvas.drawLine(mCircleRadius, mCircleRadius, getWidth() - mCircleRadius, mCircleRadius, mLinePaint);
//開是迴圈繪製view
for (int i = 0; i < texts.size(); i++) {
mTextPaint.setColor(mColorCircleDefault);
if (mSelectPosition == i) {
//繪製選中的圓圈
canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCircleSelectPaint);
mTextPaint.setColor(mColorCircleSelect);
} else {
//繪製默中的圓圈
canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCirclePaint);
}
//繪製文字
//這裡要對基線進行理解
int startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop(); //以前
Log.i("wangjitao", "以前:" + startTextY);
//現在是這樣的,首先獲取基線物件
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
startTextY = getHeight() - (int) fontMetrics.bottom;
Log.i("wangjitao", "現在:" + startTextY);
if (i == 0) {
canvas.drawText(texts.get(i), 0, startTextY, mTextPaint);
} else if (i == texts.size() - 1) {
canvas.drawText(texts.get(i), getWidth() - mBounds.get(i).width(), startTextY, mTextPaint);
} else {
canvas.drawText(texts.get(i), mCircleRadius + ((mLineLength / (texts.size() - 1)) * i) - (mBounds.get(i).width() / 2), startTextY, mTextPaint);
}
}
}
private float downX;
private float upX;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
//按下手指的時候記錄下按下的位置
case MotionEvent.ACTION_DOWN:
downX = event.getX();
Log.e("wangjitao", "手指按下: getX:" + downX);
break;
case MotionEvent.ACTION_MOVE:
Log.i("wangjitao", "手指滑動: ");
break;
case MotionEvent.ACTION_UP:
upX = event.getX();
Log.e("wangjitao", "手指擡起: " + upX);
if (downX - upX > 50) {
downX = 0;
upX = 0;
//向左滑動
//判斷做滑動的時候當前選擇點時候在在初始狀態下
if (mSelectPosition != 0) {
//更新view
mSelectPosition--;
} else {
mSelectPosition = texts.size() - 1;
}
invalidate();
} else if (upX - downX > 50) {
//向右滑動
downX = 0;
upX = 0;
//判斷做滑動的時候當前選擇點時候在最後一個點上
if (mSelectPosition != texts.size() - 1) {
//更新view
mSelectPosition++;
} else {
mSelectPosition = 0;
}
invalidate();
} else {
downX = 0;
upX = 0;
}
break;
}
return true;
}
}
執行效果:
- 新增自定義屬性
這裡我們把好多控制元件的屬性都寫死了,我們可以用自定義屬性來實現佈局檔案中動態的改變的,不瞭解的同學可以看我之前的《深入瞭解自定義屬性》,這裡就不一起寫了,See You····