当现有的UI组件无法满足我们的开发需求时,例如我们想开发一个用于步行目标进度的显示,全部使用文本展示或一条直直的进度条加文本对于用户来是死板的。如果我们想用一个圆环之类的形式来展示,需要自定义组件了。
最终效果
目录
自定义布局
继承组件基类
组件的基类是Component,自定义组件时应继承Component或其子类并至少重写以下构造函数。
public class WalkView extends Component {
public WalkView(Context context) {
super(context);
}
public WalkView(Context context, AttrSet attrSet) {
super(context, attrSet);
}
}
一个参数的构造函数用于我们在JAVA代码中初始化的应用,两参数的构造函数用于在XML中使用。
获取组件大小
在绘制内容之前,我们需要知道组件的大小,根据组件的大小进行内容的调整。
获取组件的大小我们需要实现布局大小的监听类:Component.EstimateSizeListener
public class WalkView extends Component implements Component.EstimateSizeListener{
......
@Override
public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) {
// 根据配置获取宽高
int width = Component.EstimateSpec.getSize(widthEstimateConfig);
int height = Component.EstimateSpec.getSize(heightEstimateConfig);
// 将获取到宽高设置给组件
setEstimatedSize(
Component.EstimateSpec.getChildSizeWithMode(width, width, Component.EstimateSpec.PRECISE),
Component.EstimateSpec.getChildSizeWithMode(height, height, Component.EstimateSpec.PRECISE));
return true;
}
}
组件构成分析
分析:我们要实现上面的效果需要3个Paint,圆环背景、圆环前景与文字。
public class WalkView extends Component implements Component.EstimateSizeListener{
......
/**
* 背景圆环宽度
*/
private float backCircleWidth = 10f;
/**
* 进度圆环宽度
*/
private float scheduleCircleWidth = 16f;
/**
* 两个圆环画笔
*/
private Paint backCirclePaint;
private Paint scheduleCirclePaint;
/**
* 文字画笔
*/
private Paint textPaint;
/**
* 文字大小
*/
private int textSize = 50;
......
private void initPaint() {
backCirclePaint = new Paint();
// 描边
backCirclePaint.setStyle(Paint.Style.STROKE_STYLE);
// 圆角
backCirclePaint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
// 抗锯齿
backCirclePaint.setAntiAlias(true);
// 抗抖动
backCirclePaint.setDither(true);
backCirclePaint.setStrokeWidth(backCircleWidth);
backCirclePaint.setColor(Color.LTGRAY);
backCirclePaint.setAlpha(0.3f);
scheduleCirclePaint = new Paint();
scheduleCirclePaint.setStyle(Paint.Style.STROKE_STYLE);
scheduleCirclePaint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
scheduleCirclePaint.setAntiAlias(true);
scheduleCirclePaint.setDither(true);
scheduleCirclePaint.setStrokeWidth(scheduleCircleWidth);
// 渐变色数组
Color[] colors = new Color[]{new Color(Color.getIntColor("#9CECFB")), new Color(Color.getIntColor("#0052D4"))};
// 初始化扫描着色器,使其在中心位置。
SweepShader shader = new SweepShader(getWidth() / 2f, getHeight() / 2f, colors, null);
/*
* Shader默认是从0角度开始我们将其旋转270度使其0度在270的位置。
* 下方之所以旋转263度在ROUND_CAP模式下在圆环的开始处会出现渐变
* 色的最后一种,在BUTT_CAP模式下则没有。这个问题已经在论坛反馈
* 不知后续会不会修复
* */
Matrix matrix = new Matrix();
matrix.setRotate(263, getWidth() / 2f, getHeight() / 2f);
shader.setShaderMatrix(matrix);
// 为画笔设置着色器
scheduleCirclePaint.setShader(shader, Paint.ShaderType.SWEEP_SHADER);
// 文字画笔
textPaint = new Paint();
// 设置文字大小
textPaint.setTextSize(textSize);
}
......
}
绘制布局
画笔初始后我们需要在绘制任务中将我们想要的布局使用对应的画笔绘制到画笔中。绘制任务我们需要实现绘制任务接口:Component.DrawTask
分析:因为是进度效果,我们需要确定当前值和总值。也就是已经行走的步数和我们设定的目标步数以确定前景弧的长度。
public class WalkView extends Component implements Component.EstimateSizeListener, Component.DrawTask{
......
/**
* 当前步数,本文不讨论运动接口。假设当前已经行走3560步。
*/
private int walkNum = 3560;
/**
* 目标步数
*/
private int walkGoal = 5000;
......
@Override
public void onDraw(Component component, Canvas canvas) {
int width = getWidth();
int height = getHeight();
// 找到最小长度
int minLength = Math.min(width, height);
// 确定中心点
Point centerPoint = new Point(width / 2f, height / 2f);
int center = minLength / 2;
// 半径,由于是描边模式需要将边宽度减掉
int radius = center - (int) (backCircleWidth);
// 绘制背景圆环
canvas.drawCircle(centerPoint, radius, backCirclePaint);
// 弧所处矩形,上下左右点就是中心点到各边的半径距离。
RectFloat rectFloat = new RectFloat(
centerPoint.getPointX() - radius,
centerPoint.getPointY() - radius,
centerPoint.getPointX() + radius,
centerPoint.getPointY() + radius);
// 弧形进度。就是圆周360°乘上当前比率。
float progress = 360;
if (walkGoal > 0 && walkNum < walkGoal) {
progress = ((float)walkNum / walkGoal) * 360;
}
// 设置弧形的起始位置,弧长以及两端点是否连接。
Arc arc = new Arc(270, progress, false);
// 绘制弧形
canvas.drawArc(rectFloat, arc, scheduleCirclePaint);
String tile = "步数";
// 计算文字长度
float iLen = textPaint.measureText(walkNum + "");
float tLen = textPaint.measureText(tile);
// 设置文字起点左边,绘制文字
canvas.drawText(textPaint, walkNum + "", (float) width / 2 - iLen / 2, (float) height / 2 - textPaint.getTextSize() / 3f);
canvas.drawText(textPaint, tile, (float) width / 2 - tLen / 2, (float) height / 2 + textPaint.getTextSize());
}
......
}
初始化画笔函数的调用时机
initPaint函数中包含对布局大小的引用,因此其初始化的时机应当在布局大小确认后。
@Override
public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) {
......
setEstimatedSize(......);
// 初始化画笔
initPaint();
return true;
}
设置监听
我们实现了布局大小的监听和绘制任务接口还需要对其引用
public WalkView(Context context) {
super(context);
// 设置测量组件的侦听器
setEstimateSizeListener(this);
// 添加绘制任务
addDrawTask(this);
}
public WalkView(Context context, AttrSet attrSet) {
super(context, attrSet);
// 设置测量组件的侦听器
setEstimateSizeListener(this);
// 添加绘制任务
addDrawTask(this);
}
自定义XML属性
因为其中包含文字,描边的粗细等等,根据不同的场景我们可能会有多种格式,我们不能每次都去修改代码。尤其是封装成API调用后更不可能修改。能不能有向系统自带的组件我通过类似text_size的属性在XML中直接进行配置,答案是,能~。
首先我们在XML布局文件中引用 ohos-auto 自定义名称为app的空间
使用app空间设置(圆环宽度)ring_width和(字体大小)text_size两个属性。这里可以举一反三设置颜色等等属性。
<?xml version="1.0" encoding="utf-8"?>
<DependentLayout
xmlns:app="http://schemas.huawei.com/res/ohos-auto"
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent">
<com.example.helloharmony.myview.MyView
app:ring_width="50"
app:text_size="80"
ohos:height="200vp"
ohos:id="$+id:my_view"
ohos:visibility="visible"
ohos:width="200vp"
ohos:horizontal_center="true"
ohos:top_margin="80vp"/>
......
</DependentLayout>
引用自定义属性
在XML文件中自定义了属性后不会生效,我们还需要在JAVA代码中进行引用。这个时候两个参数的构造函数就排上用场了。
public WalkView(Context context, AttrSet attrSet) {
super(context, attrSet);
// 判断是否存在名称为 text_size 的属性
if (attrSet.getAttr("text_size").isPresent()) {
// 如果有我们就应用它
textSize = attrSet.getAttr("text_size").get().getIntegerValue();
}
// 判断是否存在名称为 ring_width 的属性
if (attrSet.getAttr("ring_width").isPresent()) {
// 如果有我们就应用它
float ringWidth = attrSet.getAttr("ring_width").get().getFloatValue();
backCircleWidth = ringWidth - 6;
scheduleCircleWidth = ringWidth + 12;
}
// 设置测量组件的侦听器
setEstimateSizeListener(this);
// 添加绘制任务
addDrawTask(this);
}
结束语
到此图片的效果就实现了。当然上面知识基本的常识,我们还需要对数据的更新、进度圆环的逐段绘制产生动效等等这里就不再赘述了。写博客的目的在于抛砖引玉和基础教程与学习,基础都有了其他都是小意思,加油!