前言
實習期間被分配到的第一個任務,完成大概如圖這樣一個界面,乍一看,整個界面的布局還是十分清晰的,即使是新手也能輕易完成,唯一的難題應該就是這個紅色的進度條了,我一開始考慮使用TextView的drawableLeft來實作,但又感覺不如自定義控制元件來得靈活,遂決定使用自定義控制元件的方式實作,然而,我高估了自己的水平😅,程序中遇到了不少坑,花了幾天才誤打誤撞地完成這個“簡易”進度條,也正因如此,才有了這篇文章來記錄一下思考程序、遇到的問題以及解決方案,

設計分析
實作這樣一個控制元件,要考慮的方面有:繪制(Draw)、測量(Measure)以及屬性(Attribute),
- 繪制,顯而易見,繪制一條直線和幾個圓形即可,
- 測量,我選擇以圓形的直徑為控制元件的寬(width),父容器的高度為控制元件的高(height),
- 屬性,比較自由,如直線的粗細、圓形的半徑、圓環的位置等,為簡便起見,只考慮幾個比較關鍵的屬性,
實作
根據上述分析,代碼的邏輯也基本理清了,實作起來應該是水到渠成,
創建類
新建MyProgressView類,繼承自View類,并實作相關構造方法,使代碼入口一致,
public class MyProgressView extends View {
private static final String TAG = "MyProgressView";
public MyProgressView(Context context) {
this(context, null);
}
public MyProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
設定屬性
為方便使用,部分屬性希望能在布局檔案中直接修改,將其添加至attrs.xml檔案中,并在控制元件的構造方法中完成初始化,另一部分屬性,可能不方便于布局檔案中修改或其他原因,為其設定get/set方法,
<declare-styleable name="MyProgressView">
<attr name="circleRadius" format="dimension"/>
<attr name="lineWidth" format="dimension"/>
<attr name="circlePosition1" format="float"/>
<attr name="circlePosition2" format="float"/>
<attr name="circlePosition3" format="float"/>
</declare-styleable>
public class MyProgressView extends View {
private static final String TAG = "MyProgressView";
//控制元件的高度
private int mHeight;
//繪制的起始點
private float mStartY = DEFAULT_START_Y;
public static final float DEFAULT_START_Y = 0;
//實線的寬度
private float mLineWidth;
//圓環的半徑
private float mRadius;
//圓環的位置(圓心)
private float[] mCirclePositions = new float[3];
//當前行程
private int mProgress = -1;
public MyProgressView(Context context) {
this(context, null);
}
public MyProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化屬性
initAttrs(context, attrs);
}
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyProgressView);
//圓環的半徑
int defaultRadius = SizeUtils.dip2px(5);
mRadius = a.getDimension(R.styleable.MyProgressView_circleRadius, defaultRadius);
//實線的寬度
int defaultWidth = SizeUtils.dip2px(2);
mLineWidth = a.getDimension(R.styleable.MyProgressView_lineWidth, defaultWidth);
/*
* 三個圓環的位置(y軸)
* 推薦使用setCirclePositions方法設定
*/
mCirclePositions[0] = a.getFloat(R.styleable.MyProgressView_circlePosition1, -1);
mCirclePositions[1] = a.getFloat(R.styleable.MyProgressView_circlePosition2, -1);
mCirclePositions[2] = a.getFloat(R.styleable.MyProgressView_circlePosition3, -1);
//回收
a.recycle();
}
/*
* 省略部分屬性的get/set方法
*/
}
繪制
繪制直線和圓形所需要的的相關屬性已經定義好了,除此之外還需要實體化對應的畫筆,
private void initPaints() {
//初始化實線畫筆
mLinePaint = new Paint();
mLinePaint.setColor(Color.parseColor("#FF0000"));
mLinePaint.setStrokeWidth(mLineWidth);
//初始化圓形畫筆
innerCirclePaint = new Paint();
outerCirclePaint = new Paint();
innerCirclePaint.setColor(Color.parseColor("#FFFFFF"));
outerCirclePaint.setColor(Color.parseColor("#D0021B"));
}
緊接著就是繪制程序,圓環利用兩個重合的大小圓形實作,根據mProgress的值決定是否只繪制一個圓形,以表示當前流程,
@Override
protected void onDraw(Canvas canvas) {
Log.i(TAG,"-- onDraw --");
super.onDraw(canvas);
//繪制實線
canvas.drawLine(mRadius, mStartY, mRadius, mHeight, mLinePaint);
//繪制圓環
if (mCirclePositions != null && mCirclePositions.length > 0) {
for (int i = 1; i <= 3; i ++) {
float f = mCirclePositions[i - 1];
if (f == -1) continue; //未設值,不繪制
canvas.drawCircle(mRadius, f, mRadius, outerCirclePaint);
if (i != mProgress) {
canvas.drawCircle(mRadius, f, mRadius / 2, innerCirclePaint);
}
}
}
}
測量
根據之前的分析,測量程序十分簡單,控制元件的寬(width)和高(height)都寫死,分別為圓形的直徑和父容器的高度,
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
Log.i(TAG, "heightSize --> " + heightSize);
Log.i(TAG, "widthSize --> "+ widthSize);
Log.i(TAG, "heightMode --> " + heightMode);
Log.i(TAG, "widthMode --> " + widthMode);
Log.i(TAG, "----------------------");
mHeight = heightSize;
setMeasuredDimension((int) mRadius * 2, heightSize);
}
使用測驗
至此,我認為自定義進度條已經完成,迫不及待地將其加入布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:id="@+id/container"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
tools:context=".MainActivity"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!--layout_width可以為任意值,不會影響結果-->
<com.mone.customview.progressView.MyProgressView
android:id="@+id/visit_progress_view"
android:layout_width="10dp"
android:layout_height="wrap_content"
app:circleRadius="6dp"
android:layout_alignParentStart="true" />
<TextView
android:id="@+id/title1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/text_title_style"
android:textColor="#000000"
android:text="流程1"/>
<TextView
android:id="@+id/content1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/title1"
style="@style/text_content_style"
android:textColor="#aaaaaa"
android:text="內容內容很多內容內容內容很多內容內容內容很多內容內容內容很多內容內容內容很多內容很多內容很多內容很多內容很多內容"/>
<TextView
android:id="@+id/title2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/content1"
style="@style/text_title_style"
android:textColor="#000000"
android:text="流程2"/>
<TextView
android:id="@+id/content2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/title2"
style="@style/text_content_style"
android:textColor="#aaaaaa"
android:text="內容內容很多內容內容內容很多內容內容內容很多內容內容內容很多內容內容內容很多內容很多內容很多內容很多內容很多內容很多內容很多內容很多內容"/>
<TextView
android:id="@+id/title3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/content2"
style="@style/text_title_style"
android:textColor="#000000"
android:text="流程3"/>
<TextView
android:id="@+id/content3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/title3"
style="@style/text_content_style"
android:textColor="#aaaaaa"
android:text="內容內容很多內容內容內容很多內容內容內容很多內容內容內容很多內容內容內容很多內容很多內容很多內容很多內容很多內容很多內容"/>
</RelativeLayout>
并設定好相應的屬性:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化組件
container = findViewById(R.id.container);
myProgressView = findViewById(R.id.visit_progress_view);
title1 = findViewById(R.id.title1);
title2 = findViewById(R.id.title2);
title3 = findViewById(R.id.title3);
//設定進度條
container.post(new Runnable() {
@Override
public void run() {
int padding = container.getPaddingTop();
float y1 = (title1.getTop() + title1.getBottom() - 2 * padding) >> 1;
float y2 = (title2.getTop() + title2.getBottom() - 2 * padding) >> 1;
float y3 = (title3.getTop() + title3.getBottom() - 2 * padding) >> 1;
myProgressView.setStartY(y1);
myProgressView.setCirclePositions(new float[]{y1, y2, y3});
myProgressView.invalidate();
}
});
myProgressView.setProgress(2);
}
啟動測驗,查看結果:

出現這樣的結果是正常的,但這不符合需求,我希望進度條的高度根據右側的內容變化,而不是像這樣占滿了螢屏,這也是我實作這個自定義控制元件程序中最頭疼的一件事,為此,我特意去學習研究了onMeasure方法,也就是我的上一篇文章:自定義控制元件之onMeasure方法的研究整理,最終解決了這個問題,根源就在于處理不同情況下的測量程序,
| EXACTLY | AT_MOST | UNSPECIFIED | |
|---|---|---|---|
| dp/px | EXACTLY childSize | EXACTLY childSize | EXACTLY childSize |
| match_parent | EXACTLY parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
| wrap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
依舊搬出這張表,橫軸對應父容器的測量模式,縱軸對應子View的layoutParams,值為onMeasure方法中的對應引數,
剛才的布局中,RelativeLayout的layout_height屬性是wrap_content,對應AT_MOST模式,myProgressView的layout_height屬性是wrap_content,那么,從引數heightMeasureSpec中取出的值和模式就分別是parentSize和AT_MOST,也就出現了那樣的結果,
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//省略部分代碼
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
setMeasuredDimension((int) mRadius * 2, heightSize);
}
解決問題
既然如此,那么要解決這個問題的思路就很明確了,當回傳的模式為AT_MOST時,做不一樣的處理即可,我的解決方案如下:
首先修改onMeasure方法,當測驗模式為AT_MOST時,讓控制元件的測量高為0,
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
Log.i(TAG, "heightSize --> " + heightSize);
Log.i(TAG, "widthSize --> "+ widthSize);
Log.i(TAG, "heightMode --> " + heightMode);
Log.i(TAG, "widthMode --> " + widthMode);
Log.i(TAG, "----------------------");
mHeight = heightSize;
//處理不同模式下的測量值
if (heightMode == MeasureSpec.EXACTLY) {
setMeasuredDimension((int) mRadius * 2, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension((int) mRadius * 2, 0);
}
}
然后調整初始化進度條的方法,在那里才為進度條設定高度值(此時父容器已經測量完成,由于進度條的測量高度為0,父容器高度只根據TextView的測量高度和padding值決定),
/**
* 初始化進度條組件
*/
private void setupProgressView() {
container = findViewById(R.id.container);
container.post(new Runnable() {
@Override
public void run() {
int height = container.getMeasuredHeight();
Log.i(TAG, "容器的最終高度 --> " + height);
ViewGroup.LayoutParams layoutParams = myProgressView.getLayoutParams();
layoutParams.height = height;
myProgressView.setLayoutParams(layoutParams);
myProgressView.setHeight(height);
//設定進度條圓點位置
int padding = container.getPaddingTop();
float y1 = (title1.getTop() + title1.getBottom() - 2 * padding) >> 1;
float y2 = (title2.getTop() + title2.getBottom() - 2 * padding) >> 1;
float y3 = (title3.getTop() + title3.getBottom() - 2 * padding) >> 1;
myProgressView.setStartY(y1);
myProgressView.setCirclePositions(new float[]{y1, y2, y3});
}
});
//設定進度
myProgressView.setProgress(2);
}
這樣修改以后,最終的結果也符合需求:

第三種模式
上一輪修改onMeasure方法時,只考慮了EXACTLY和AT_MOST模式,卻忽略了第三種模式:UNSPECIFIED模式,雖然大部分文章告訴我,這種模式很少用到,一般無需考慮,但我直接遇到了這種情況:當需要用滑動布局嵌套時,就需要考慮這種模式,
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<com.mone.customview.progressView.MyProgressView
android:id="@+id/visit_progress_view"
android:layout_width="10dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true" />
<!-- 省略部分代碼 -->
</RelativeLayout>
</androidx.core.widget.NestedScrollView>
可以看一下輸出的Log,其中heightMode = 0,即表示UNSPECIFIED模式,

不過解決的方式也很簡單,與AT_MOST模式類似,在測量程序中設定測量值為0(回傳值就是0,說明滑動布局有意這樣設計),在初始化的程序中再設定高度即可,
//處理不同模式下的測量值
if (heightMode == MeasureSpec.EXACTLY) {
setMeasuredDimension((int) mRadius * 2, heightSize);
} else {
setMeasuredDimension((int) mRadius * 2, 0);
}
完整代碼
流程進度條
/**
* 進度指示器
*/
public class MyProgressView extends View {
private static final String TAG = "MyProgressView";
//控制元件的高度
private int mHeight;
//繪制的起始點
private float mStartY = DEFAULT_START_Y;
public static final float DEFAULT_START_Y = 0;
//實線的寬度
private float mLineWidth;
//圓環的半徑
private float mRadius;
//圓環的位置(圓心)
private float[] mCirclePositions = new float[3];
//當前流程
private int mProgress = -1;
//畫筆
private Paint mLinePaint;
private Paint innerCirclePaint;
private Paint outerCirclePaint;
public MyProgressView(Context context) {
this(context, null);
}
public MyProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化屬性
initAttrs(context, attrs);
//初始化畫筆
initPaints();
}
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyProgressView);
//圓環的半徑
int defaultRadius = SizeUtils.dip2px(5);
mRadius = a.getDimension(R.styleable.MyProgressView_circleRadius, defaultRadius);
//實線的寬度
int defaultWidth = SizeUtils.dip2px(2);
mLineWidth = a.getDimension(R.styleable.MyProgressView_lineWidth, defaultWidth);
/*
* 三個圓環的位置(y軸)
* 推薦使用setCirclePositions方法設定
*/
mCirclePositions[0] = a.getFloat(R.styleable.MyProgressView_circlePosition1, -1);
mCirclePositions[1] = a.getFloat(R.styleable.MyProgressView_circlePosition2, -1);
mCirclePositions[2] = a.getFloat(R.styleable.MyProgressView_circlePosition3, -1);
//回收
a.recycle();
}
/**
* 初始化畫筆
*/
private void initPaints() {
//初始化實線畫筆
mLinePaint = new Paint();
mLinePaint.setColor(Color.parseColor("#FF0000"));
mLinePaint.setStrokeWidth(mLineWidth);
//初始化圓形畫筆
innerCirclePaint = new Paint();
outerCirclePaint = new Paint();
innerCirclePaint.setColor(Color.parseColor("#FFFFFF"));
outerCirclePaint.setColor(Color.parseColor("#D0021B"));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
Log.i(TAG, "heightSize --> " + heightSize);
Log.i(TAG, "widthSize --> "+ widthSize);
Log.i(TAG, "heightMode --> " + heightMode);
Log.i(TAG, "widthMode --> " + widthMode);
Log.i(TAG, "----------------------");
mHeight = heightSize;
if (heightMode == MeasureSpec.EXACTLY) {
setMeasuredDimension((int) mRadius * 2, heightSize);
} else {
setMeasuredDimension((int) mRadius * 2, 0);
}
}
@Override
protected void onDraw(Canvas canvas) {
Log.i(TAG,"-- onDraw --");
super.onDraw(canvas);
//繪制實線
canvas.drawLine(mRadius, mStartY, mRadius, mHeight, mLinePaint);
//繪制圓環
if (mCirclePositions != null && mCirclePositions.length > 0) {
for (int i = 1; i <= 3; i ++) {
float f = mCirclePositions[i - 1];
if (f == -1) continue; //未設值,不繪制
canvas.drawCircle(mRadius, f, mRadius, outerCirclePaint);
if (i != mProgress) {
canvas.drawCircle(mRadius, f, mRadius / 2, innerCirclePaint);
}
}
}
}
/**
* 設定當前進度
* @param num
*/
public void setProgress(int num) {
if (num < 1 || num > 3) return;
mProgress = num;
invalidate();
}
/**
* 設定View的高度
* @param height
*/
public void setHeight(int height) {
mHeight = height;
}
/**
* 設定繪制的Y軸起始點
* @param y
*/
public void setStartY(float y) {
mStartY = y;
}
/**
* 設定圓環的半徑
* @param radius
*/
public void setRadius(float radius) {
mRadius = radius;
}
public float getRadius() {
return mRadius;
}
/**
* 設定實線的寬度
* @param width
*/
public void setLineWidth(float width) {
mLineWidth = width;
mLinePaint.setStrokeWidth(mLineWidth);
}
/**
* 設定圓環的位置
* @param floats
*/
public void setCirclePositions(float[] floats) {
this.mCirclePositions = floats;
}
}
使用方式
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private RelativeLayout container;
private MyProgressView myProgressView;
private TextView title1, title2, title3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化組件
container = findViewById(R.id.container);
myProgressView = findViewById(R.id.visit_progress_view);
title1 = findViewById(R.id.title1);
title2 = findViewById(R.id.title2);
title3 = findViewById(R.id.title3);
//設定進度條
setupProgressView();
}
/**
* 初始化進度條組件
*/
private void setupProgressView() {
container.post(new Runnable() {
@Override
public void run() {
int height = container.getMeasuredHeight();
Log.i(TAG, "容器的最終高度 --> " + height);
ViewGroup.LayoutParams layoutParams = myProgressView.getLayoutParams();
layoutParams.height = height;
myProgressView.setLayoutParams(layoutParams);
myProgressView.setHeight(height);
//設定進度條圓點位置
int padding = container.getPaddingTop();
float y1 = (title1.getTop() + title1.getBottom() - 2 * padding) >> 1;
float y2 = (title2.getTop() + title2.getBottom() - 2 * padding) >> 1;
float y3 = (title3.getTop() + title3.getBottom() - 2 * padding) >> 1;
myProgressView.setStartY(y1);
myProgressView.setCirclePositions(new float[]{y1, y2, y3});
}
});
//設定進度
myProgressView.setProgress(2);
}
}
結尾碎碎念
能有這篇文章真的是歪打正著,實際上踩過的坑比描述的還要多,有一些礙于篇幅就沒有談到,而且實際上的開發程序完全是…歪曲且有趣?實際情況是這樣的:首先接到這個任務,撰寫完第一版進度條,也就是對應內容標題使用測驗為止的版本,此時恰好由于UI占滿了剩余空間,導致我沒有發現問題,誤以為輕松完成了任務(笑),之后被要求在外層添加滑動布局,此時才發現問題,于是才有了之后的研究,也就有了這篇文章,
另外,如果有更優解,歡迎分享討論,共同學習進步,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/236129.html
標籤:其他
下一篇:GitHub無法訪問,連接超時
