上几篇已经讲了自定义View即自定义View变色的基本使用,为了更熟悉自定义View的使用,上篇结尾有两个任务,这篇就来讲讲是怎么实现的,包括打造炫酷的进度条、仿58同城数据加载。
1. 打造炫酷的进度条
1.1 思路
我们要做的炫酷的进度条,可以继承View,也可以继承TextView,这里以继承TextView为例来实现,其实分为三部分,外圈、内圈、中间的字符串,这个跟《自定义View(3)仿QQ运动步数进度》可谓是非常相似了。不过对于外圈,除了可以用弧形表示外,也可以用圆来表示。
1.2 自定义View-炫酷的进度条
1.2.1 新建attrs.xml
首先还是新建attrs.xml
,定义需要用到的自定义属性。
<!-- /CustomView/view5/app/src/main/res/values/attrs.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ProgressView">
<attr name="outColor" format="color"/>
<attr name="innerColor" format="color"/>
<attr name="borderWidget" format="dimension"/>
</declare-styleable>
</resources>
1.2.2 新建ProgressView继承TextView
// /CustomView/View5/app/src/main/java/com/example/view5/ProgressView.java
public class ProgressView extends androidx.appcompat.widget.AppCompatTextView {
private Paint mOuterPaint; // 总进度条画笔
private int mOuterColor = Color.RED; // 总进度条颜色,并设置默认
private int mStartAngle = 0; // 弧度开始角度
private float mEndAngle = 360f; // 弧度结束角度
private Paint mInnerPaint; // 已走画笔
private int mInnerColor = Color.BLUE; // 已走颜色,并设置默认
private float mBorderWidget = 40; // 默认圆弧宽度
private Paint mTextPaint; // 文字画笔
private float mProgress = 0;
private String mProgressText; // 当前进度
public ProgressView(Context context) {
this(context,null);
}
public ProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mProgressText = getText().toString();
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
mBorderWidget = ta.getDimension(R.styleable.ProgressView_borderWidget, mBorderWidget);
mOuterColor = ta.getColor(R.styleable.ProgressView_outColor, mOuterColor);
mInnerColor = ta.getColor(R.styleable.ProgressView_innerColor, mInnerColor);
ta.recycle(); // 回收
mOuterPaint = new Paint(); // 创建总画笔
mOuterPaint.setAntiAlias(true); // 取消锯齿
mOuterPaint.setColor(mOuterColor); // 设置颜色
mOuterPaint.setStrokeWidth(mBorderWidget); // 设置圆弧宽度
mOuterPaint.setStyle(Paint.Style.STROKE); // 设置圆弧Style
mOuterPaint.setStrokeCap(Paint.Cap.ROUND); // 设置起始,结束圆角
mInnerPaint = new Paint(); // 创建进度画笔
mOuterPaint.setAntiAlias(true); // 抗锯齿
mInnerPaint.setColor(mInnerColor); // 设置颜色
mInnerPaint.setStrokeWidth(mBorderWidget); // 设置圆弧宽度
mInnerPaint.setStyle(Paint.Style.STROKE); // 设置圆弧Style
mInnerPaint.setStrokeCap(Paint.Cap.ROUND); // 设置起始,结束圆角
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true); // 设置抗锯齿
mTextPaint.setDither(true); // 设置放抖动
mTextPaint.setTextSize(getTextSize()); // 设置文字的大小,就是xml设置的大小
mTextPaint.setColor(Color.RED);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
// 画外圆弧
// 问题:边缘显示不全 描边有宽度 mBorderWidget
float half = mBorderWidget / 2;
// 描边有宽度borderWidget,如果不重新算rectF,绘制就会显示不全。这里将整个图多绘制半个宽度后,就刚刚好了。
@SuppressLint("DrawAllocation") RectF rectF = new RectF(half, half, getWidth() - half, getHeight() - half);
// canvas.drawArc(rectF, mStartAngle, mEndAngle, false, mOuterPaint); // 这里如果是True的话,下面就是闭合的
int center = getWidth()/2;
canvas.drawCircle(center,center,center-half,mOuterPaint); // 画一个360°的圆弧,或者画一个圆都行
// 画内圆弧
canvas.drawArc(rectF, mStartAngle, mEndAngle/100*mProgress, false, mInnerPaint);
// canvas.drawArc(rectF, mStartAngle, mProgress, false, mInnerPaint); // 第二种方法
// 画文本
mProgressText = mProgress + "%";
// mProgressText = Math.round(mProgress / mEndAngle * 100) + "%"; // 第二种方法
// 获取文字排版信息
Paint.FontMetricsInt fontMetricsInt = mTextPaint.getFontMetricsInt();
int baseLine = getHeight() / 2 + (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom; // 计算基线
// 获取文本宽度
float textWidget = mTextPaint.measureText(mProgressText);
int dx = (int) (getWidth() / 2 - textWidget / 2); // 控件的一半减去文字的一半,算出文字的初始绘制值
canvas.drawText(mProgressText, dx, baseLine, mTextPaint);
}
public synchronized void setProgress(int progress) {
this.mProgress = progress;
//不断绘制 invalidate中会调用onDraw方法
invalidate();
}
public synchronized void setAnimatorStep(int progress) {
// 属性动画
ValueAnimator valueAnimator = ObjectAnimator.ofFloat(0,progress); // 从0变化到1
valueAnimator.setDuration(3000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(@NonNull ValueAnimator animation) {
float currentStep = (float) animation.getAnimatedValue();
setProgress((int) currentStep);
}
});
valueAnimator.start();
}
}
1.2.3 修改activity_main.xml
<!-- /CustomView/View5/app/src/main/res/layout/activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<com.example.view5.ProgressView
android:layout_gravity="center"
android:id="@+id/progress_view"
android:background="@color/grey"
android:layout_width="200dp"
android:layout_height="200dp"
android:text="Hello World!"
android:textSize="40sp"
app:outColor="@color/white"
app:innerColor="@color/black"
/>
</LinearLayout>
1.2.4 修改MainActivity.java
// /CustomView/View5/app/src/main/java/com/example/view5/MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
@SuppressLint({
"MissingInflatedId", "LocalSuppress"}) final ProgressView progressView = (ProgressView) findViewById(R.id.progress_view);
progressView.setAnimatorStep(100);
// progressView.setAnimatorStep(360); 第二种方法
}
}
1.2.5 效果展示
2. 仿58同城数据加载
2.1 思路
画三个图形,分别是正方形、三角形、圆形。然后每秒钟画其中一个。这里的正方形和圆形都有现存的方法可以调用,而三角形相对比较复杂,需要用到Path。
2.2 自定义View-仿58同城数据加载
2.2.1 添加attrs.xml
<!-- /CustomView/View5/app/src/main/res/values/attrs.xml -->
...
<declare-styleable name="ShapeView">
<attr name="squareColor" format="color"/>
<attr name="triangleColor" format="color"/>
<attr name="circleColor" format="color"/>
</declare-styleable>
2.2.2 新建ShapeView继承View
// /CustomView/View5/app/src/main/java/com/example/view5/ShapeView.java
public class ShapeView extends View {
private Paint mSquarePaint;
private int mSquareColor = Color.RED;
private Paint mTrianglePaint;
private int mTriangleColor = Color.YELLOW;
private Paint mCirclePaint;
private int mCircleColor = Color.BLUE;
private String mProgress = "";
private Path mPath;
private final String[] shape = {
"Square","Triangle","Circle"};
int currentIndex = 0;
int aimIndex = 0;
public ShapeView(Context context) {
this(context, null);
}
public ShapeView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ShapeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ShapeView);
mSquareColor = ta.getColor(R.styleable.ShapeView_squareColor, mSquareColor);
mTriangleColor = ta.getColor(R.styleable.ShapeView_triangleColor, mTriangleColor);
mCircleColor = ta.getColor(R.styleable.ShapeView_circleColor, mCircleColor);
ta.recycle();
mSquarePaint = new Paint();
mSquarePaint.setAntiAlias(true); // 取消锯齿
mSquarePaint.setStyle(Paint.Style.FILL); // 实心,不设置其实默认也是实心
mSquarePaint.setColor(mSquareColor); // 设置颜色
mTrianglePaint = new Paint();
mTrianglePaint.setAntiAlias(true); // 取消锯齿
mTrianglePaint.setColor(mTriangleColor); // 设置颜色
mCirclePaint = new Paint();
mCirclePaint.setAntiAlias(true); // 取消锯齿
mCirclePaint.setColor(mCircleColor); // 设置颜色
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (Objects.equals(mProgress, "")) {
return;}
switch (mProgress){
case "Square":
canvas.drawRect(0,0,getWidth(),getHeight(),mSquarePaint);
break;
case "Triangle":
float y = (float) (0.5 * Math.sqrt(3) * getHeight());
if (mPath == null) {
// 添加if判断,不需要反复new Path(),减少开销
mPath = new Path(); // 通过路线来画,依次传入三个点的坐标,连起来。
mPath.moveTo(0, y);
mPath.lineTo(getWidth()/2, 0);
mPath.lineTo(getWidth(), y);
mPath.close(); // 路径闭合
}
canvas.drawPath(mPath, mTrianglePaint);
break;
case "Circle":
int center = getWidth()/2;
canvas.drawCircle(center,center,center,mCirclePaint); // 画一个360°的圆弧,或者画一个圆都行
break;
}
}
public synchronized void setProgress(String progress) {
this.mProgress = progress;
//不断绘制 invalidate中会调用onDraw方法
invalidate();
}
public synchronized void setAnimatorStep() {
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
Runnable task = new Runnable() {
@Override
public void run() {
setProgress(shape[currentIndex]);
currentIndex = (currentIndex + 1) % shape.length; // 循环数组
aimIndex++;
// 数组处理完毕,关闭执行器
if (aimIndex >= 100) {
// executorService.shutdown();
}
}
};
// 每秒执行一次任务
executorService.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);
}
}
2.2.3 修改activity_main.xml
<!-- /CustomView/View5/app/src/main/res/layout/activity_main.xml -->
...
<TextView
android:layout_width="match_parent"
android:layout_height="10dp" />
<com.example.view5.ShapeView
android:layout_gravity="center"
android:id="@+id/shape_view"
android:layout_width="200dp"
android:layout_height="200dp" />
2.2.4 修改
// /CustomView/View5/app/src/main/java/com/example/view5/MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
...
final ShapeView shapeView = findViewById(R.id.shape_view);
shapeView.setAnimatorStep();
}