0%

自定义View(二)

前面的文章介绍了自定义View的需要准备的基础知识,自定义View的属性获取、测量(Measure)过程,Measure过程是为了计算View的大小。本篇文章主要记载一下自定义View过程中的Layout过程,Layout过程用于计算View的位置。另外还有更关键的一步,那就是绘制(Draw)过程,一切都准备好了就需要开始绘制出我们的自定义View,后面会有一个绘制例子,在该示例中绘制了一个圆形且带有进度文字说明的进度条,通过这个示例切实感受一下自定义View的魅力。最后还有一个重点就是自定义View状态的存储与恢复。

Layout过程

前面说了在自定义View的时候,我们需要测量出View的宽和高,在某些情况下,需要多次测量才能确定View最终的宽/高;该情况下,Measure过程后得到的宽和高可能不准确,因此最好是在Layout过程中通过onLayout()方法去获取最终的宽和高。Layout过程用于计算View的位置,即计算View的四个顶点位置:Left、Top、Right、Bottom,还记得View的位置定义吗?

Layout过程也可以根据View的类型分为2种情况:对于单一View,仅仅需要计算本身View的位置即可。对于ViewGroup,除了计算自身View的位置外,还需要确定子View在父容器中的位置,即遍历调用所有子元素的measure()并且各子元素再递归去执行该流程。View树的位置是由包含的每个子视图的位置所决定的,所以如果想要计算整个View树的位置,则需递归计算每个子视图的位置,这与measure过程非常相似。

getWidth()、getHeight()与 getMeasuredWidth()、getMeasuredHeight()获取的宽高有什么区别呢?其实getWidth()、getHeight()是获得View最终的宽和高,而getMeasuredWidth()、getMeasuredHeight()是获得View测量的宽和高。一般情况下二者获取的宽和高是相等的,但是通过重写View的layout()方法强行设置后就会导致二者得到的结果不一致,所以在非人为设置的情况下,View的最终宽高getWidth()、getHeight()与View的测量宽高getMeasuredWidth()、getMeasuredHeight()永远是相等的。

Draw过程

Draw过程就是为了绘制出View视图,借助Paint、Canvas类,可以很轻松的实现绘制很多种图形,同时Draw也分为绘制单个View还是绘制ViewGroup,为了避免不必要的复杂度,示例中都是绘制单个View,所以Draw过程就是学习画笔(Paint)与画布(Canvas)的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 绘制
@Override
protected void onDraw(Canvas canvas) {
// 绘制圆
canvas.drawCircle(getWidth()/2, getHeight()/2, getHeight()/2 - paint.getStrokeWidth()/2, paint);

// 重新设置画笔宽度
paint.setStrokeWidth(2);

// 绘制直线
canvas.drawLine(0, getWidth()/2, getWidth(), getHeight()/2, paint);
canvas.drawLine(getWidth()/2, 0, getWidth()/2, getHeight(), paint);

// 绘制文本
paint.setTextSize(72);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(0);

String stringTest = "Tim"
canvas.drawText(stringTest, 0, stringTest.length(), 0, getHeight(), paint);
}

invalidate()方法用于刷新View,本质是调用View的onDraw()重新绘制。在主线程之外,用postInvalidate()。requestLayout()方法和invalidate()方法恰恰相反,requestLayout()方法只调用measure()和layout()过程,不会调用draw(),不会重新绘制任何视图包括该调用者本身。

canvas.save()与canvas.restore()用于保存/恢复画布的状态,比如我现在为了绘图方便需要将画图旋转90°,但是我又希望只是在这一段绘制过程中才需要旋转画布,于是需要将之前正常的画布存储起来,然后旋转画布90°,用完了再恢复为正常状态:

1
2
3
4
5
6
7
8
canvas.save(); //这时候保存的是画布没旋转之前的状态

canvas.rotate(90, px / 2, py / 2); // 画布开始旋转

canvas.drawLine(......); // 收到旋转操作的影响
canvas.drawLine(......); // 收到旋转操作的影响

canvas.restore(); // 还原正常状态

对于translate()方法与rotate()方法,可以将它们理解为是画布平移,画布翻转,但是把它理解为坐标系的平移与翻转则会更加形象。前面的文章中说了,默认绘图坐标零点位于屏幕左上角,那么在调用translate(x, y)方法之后,则将原点(0,0)移动到了(x, y)。之后的所有绘图操作都将以(x, y)为原点执行。同理,rotate()方法也是一样,它将坐标系旋转了一定的角度。大家也许会想,这样的操作有什么用呢?的确,没有这两个方法,同样可以绘图,只要算好坐标,没有什么画不出来的。所以说,这些方法是Android用来帮助我们简化绘图而创建的。

状态的恢复与存储

很多时候我们为获得在视图中自由绘制的能力,需要创建一个继承于View类的定制类,然后重写onTouchEvent方法处理触摸事件,重写onDraw绘制自定义视觉效果,但这里可能会被一个问题困扰,那就是设备旋转导致数据丢失的问题,好在View类为我们提供了onSaveInstanceState和onRestoreInstanceState两个方法,虽然这两个方法和Activity两个方法很相似,但是千万别认为是一样的,因为他们的使用方法完全不同。 看看下面这个示例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tim="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<!-- 必须加上Id,方便系统重建 -->
<cn.tim.custom_view.TestView
android:id="@+id/tv"
android:layout_centerInParent="true"
tim:test_bool="true"
tim:test_dimension="100dp"
tim:test_enum="top"
tim:test_integer="10086"
android:layout_width="100dp"
android:layout_height="100dp"/>

</RelativeLayout>

对于自己的自定义View要想保存状态必须加上ID android:id="@+id/xxx",这样系统才能正确找到需要保存状态的View。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 为了演示重建后的数据恢复
@Override
public boolean onTouchEvent(MotionEvent event) {
// 点击此View的时候更新要显示的文字
stringTest = "Jone";
invalidate();
return true;
}

// UI重建后数据的保存
private static final String INSTANCE = "instance";
private static final String KEY_TEXT = "key_text";
@Nullable
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putString(KEY_TEXT, stringTest);
bundle.putParcelable(INSTANCE, super.onSaveInstanceState());
return bundle;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
if(state instanceof Bundle){
Bundle bundle = (Bundle) state;
Parcelable parcelable = bundle.getParcelable(INSTANCE);
super.onRestoreInstanceState(parcelable);
stringTest = bundle.getString(KEY_TEXT);
return;
}
super.onRestoreInstanceState(state);
}

自定义一个圆形进度条

在attrs.xml声明自定义View需要的属性:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RoundProgressBar">
<attr name="color" format="color"/>
<attr name="line_width" format="dimension"/>
<attr name="radius" format="dimension"/>
<attr name="android:progress" />
<attr name="android:textSize" />
</declare-styleable>
</resources>

编写自定义View类:RoundProgressBar.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package cn.tim.custom_view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;


public class RoundProgressBar extends View {

private int mRadius;
private int mColor;
private int mLineWidth;
private int mTextSize;
private int mProgress;

private Paint mPaint;

public RoundProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundProgressBar);

mRadius = (int) ta.getDimension(R.styleable.RoundProgressBar_radius, dp2px(30));
mColor = ta.getColor(R.styleable.RoundProgressBar_color, 0xffff0000);
mLineWidth = (int) ta.getDimension(R.styleable.RoundProgressBar_line_width, dp2px(3));
mTextSize = (int) ta.getDimension(R.styleable.RoundProgressBar_android_textSize, dp2px(36));
mProgress = ta.getInt(R.styleable.RoundProgressBar_android_progress, 30);

ta.recycle();

initPaint();
}

private float dp2px(int dpVal) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, getResources().getDisplayMetrics());
}

private void initPaint() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(mColor);
}

public void setProgress(int progress) {
mProgress = progress;
invalidate();
}

public int getProgress() {
return mProgress;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int width = 0;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
int needWidth = measureWidth() + getPaddingLeft() + getPaddingRight();
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(needWidth, widthSize);
} else {
width = needWidth;
}
}

int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int height = 0;

if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
}else {
int needHeight = measureHeight() + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(needHeight, heightSize);
} else { //MeasureSpec.UNSPECIFIED
height = needHeight;
}
}
setMeasuredDimension(width, height);
}

private int measureHeight()
{
return mRadius * 2;
}

private int measureWidth()
{
return mRadius * 2;
}


@Override
protected void onDraw(Canvas canvas) {
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mLineWidth * 1.0f / 4);

int width = getWidth();
int height = getHeight();

mPaint.setStrokeWidth(mLineWidth);
canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop());
float angle = mProgress * 1.0f / 100 * 360;
canvas.drawArc(new RectF(0, 0, width - getPaddingLeft() * 2, height - getPaddingLeft() * 2), 0, angle, false, mPaint);
canvas.restore();

String text = mProgress + "%";
mPaint.setStrokeWidth(0);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTextSize(mTextSize);
int y = getHeight() / 2;
Rect bound = new Rect();
mPaint.getTextBounds(text, 0, text.length(), bound);
int textHeight = bound.height();
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText(text, 0, text.length(), getWidth() / 2, y + textHeight / 2, mPaint);

mPaint.setStrokeWidth(0);
}


private static final String INSTANCE = "instance";
private static final String KEY_PROGRESS = "key_progress";

@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putInt(KEY_PROGRESS, mProgress);
bundle.putParcelable(INSTANCE, super.onSaveInstanceState());
return bundle;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
Parcelable parcelable = bundle.getParcelable(INSTANCE);
super.onRestoreInstanceState(parcelable);
mProgress = bundle.getInt(KEY_PROGRESS);
return;
}
super.onRestoreInstanceState(state);
}
}

在activity_main.xml中使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tim="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<cn.tim.custom_view.RoundProgressBar
android:id="@+id/id_pb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:padding="10dp"
android:progress="0"
android:textSize="18sp"
tim:color="#ea22e4"
tim:radius="36dp"
/>

</RelativeLayout>

在MainActivity.java动态更新进度:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package cn.tim.custom_view;

import androidx.appcompat.app.AppCompatActivity;

import android.animation.ObjectAnimator;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

final View view = findViewById(R.id.id_pb);
view.setOnClickListener((v)-> ObjectAnimator.ofInt(view, "progress", 0, 100).setDuration(3000).start());
}
}

  • 本文作者: Tim
  • 本文链接: https://zouchanglin.cn/1261209182.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!