来源 | HarmonyOS开发者 公众平台
LinearLayout又称作线性布局,是一种非常常用的布局。正如它的名字所描述的一样,这个布局会将它所包含的控件在线性方向上依次排列。既然是线性排列,肯定就不仅只有一个方向,这里一般只有两个方向:水平方向和垂直方向。
但在实际开发中,为了呈现更好的视觉体验和交互效果,往往需要在LinearLayout外有其他的布局,比如下图这个手表应用中,在LinearLayout最外侧有个圆环。那么这一效果的呈现,在HarmonyOS上如何实现呢?
1.创建一个LinearLayout的子类,如
Java 代码
public class CircleProgressBar extends LinearLayout {
private static final String TAG = "CircleProgressBar";
private Color mProgressColor; // 自定义属性,圆环颜色
private int mMaxProgress; // 自定义属性,总进度
private int mProgress; // 自定义属性,当前进度
2.为该自定义view里的自定义属性指定key值,方便在xml里配置
Xml 代码
3.在构造函数里,解析用户的配置,对自定义属性mProgressColor, mMaxProgress, mProgress赋值
Java 代码
public CircleProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar);
mProgressColor = array.getColor(R.styleable.CircleProgressBar_progress_color, Color.RED);
mMaxProgress = array.getInteger(R.styleable.CircleProgressBar_max_value, 100);
mProgress = array.getInteger(R.styleable.CircleProgressBar_cur_value, 0);
}
4. 实现onDraw函数,使用全局变量等进行绘制
Java 代码
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 其余变量初始化过程略
LocalLog.d(TAG, "onDraw()");
canvas.drawCircle(mCenter, mCenter, mRadius, mRoundPaint); // 画圆形
mOval.set(mCenter - mRadius, mCenter - mRadius, mCenter + mRadius, mCenter + mRadius);
canvas.drawArc(mOval, ORIGIN_ANGLE, ARC_DEGRESS * getProgress() / MAX_PROGRESS,
false, mProgressPaint); // 画弧形
}
5.在xml里引用该控件
Xml 代码
这里有两个关键点:
1.android的图形子系统,在解析xml的时候,支持反射创建用户自定义的控件,并且调用自定义的控件里含有三个参数的构造方法;
2.android的图形子系统,在根节点decodeView绘制过程中,会调用每个子节点的onDraw方法,进行绘制。
众所周知,UI控件有很多,比如textView,ImageView,Button等,如果我们需要带有圆环效果的textView,带有圆环效果的ImageView,带有圆环效果的Button,那么在android上需要重复开发三次,实现三次onDraw方法。
首先,我们看一下HarmonyOS图形子系统创建控件的代码实现。
1.从代码看,HarmonyOS图形子系统同样支持xml动态反射创建自定义控件,这点和Android是一致的。
HarmonyOS图形子系统预置的控件,也是由反射创建(如果xml节点里带,则认为是自定义控件)。
Java 代码
private Component createViewElement(String elementName, AttrSet attrSet) {
Component view = null;
if (mFactory != null) {
view = mFactory.onCreateView(elementName, attrSet);
}
if (view != null) {
return view;
}
try {
if (!elementName.contains(".")) {
if ("View".equals(elementName)) {
// View's path is different from other classes
view = createViewByReflection("harmonyos.agp.view." + elementName, attrSet);
} else if ("SurfaceProvider".equals(elementName)) {
// SurfaceProvider's path is different from other classes
view = createViewByReflection("harmonyos.agp.components.surfaceprovider." + elementName, attrSet);
} else {
view = createViewByReflection("harmonyos.agp.components." + elementName, attrSet);
}
} else {
view = createViewByReflection(elementName, attrSet);
}
} catch (LayoutScatterException e) {
HiLog.error(TAG, "Create view failed: %{public}s", e.getMessage());
}
return view;
}
private Component createViewByReflection(String viewName, AttrSet attrSet) {
Constructor extends Component> constructor = mViewConstructorMap.get(viewName);
if (constructor == null) {
try {
Class extends Component> viewClass = Class.forName(viewName, false, mContext.getClassloader())
.asSubclass(Component.class);
if (viewClass == null) {
throw new LayoutScatterException("viewClass is null");
}
constructor = viewClass.getConstructor(Context.class, AttrSet.class);
constructor.setAccessible(true);
mViewConstructorMap.put(viewName, constructor);
} catch (ClassNotFoundException e) {
throw new LayoutScatterException("Can't not find the class: " + viewName, e);
} catch (NoSuchMethodException e) {
throw new LayoutScatterException("Can't not find the class constructor: " + viewName, e);
}
}
try {
return constructor.newInstance(mContext, attrSet);
} catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
throw new LayoutScatterException("Can't create the view: " + viewName, e);
}
}
【注】HarmonyOS所有的控件基本上都是Component的子类,Android是view。
2.我们可以看到,鸿蒙图形子系统中移除了Component的onDraw方法,而是把onDraw方法放到了DrawTask里。
Component提供了addDrawTask方法,供自定义Component的实现。
Java 代码
/**
* Implements a draw task.
*
* You can use {@link View#addDrawTask(DrawTask)} and {@link View#addDrawTask(DrawTask, int)} to add a draw
* task in a control, and invoke the callback when the control is updated by {@link View#invalidate()}.
*
* @since 1.0
*/
public interface DrawTask {
/**
* Indicates that the draw task is implemented between the content and background of a control.
*/
int BETWEEN_BACKGROUND_AND_CONTENT = 1;
/**
* Indicates that the draw task is implemented between the content and foreground of a control.
*/
int BETWEEN_CONTENT_AND_FOREGROUND = 2;
/**
* Called when a view is updated through a draw task.
*
* The draw task uses the attributes of the parent canvas for drawing an object,
* such as alpha, width, and height.
*
* @param view Indicates the parent {@code canvas}.
* @param canvas Indicates the canvas used for drawing in this draw task.
* @see View#addDrawTask(DrawTask)
* @see View#addDrawTask(DrawTask, int)
* @see View#invalidate()
* @since 2.0
*/
void onDraw(View view, Canvas canvas);
} /**
* Adds a draw task.
*
* The drawing of each view includes its foreground, content, and background.You can use this method to add a
* drawing task between the foreground and the content or between the content and the background.
*
* @param task Indicates the drawing task to add.
* @param layer Indicates the position of the drawing task. This value can only be
* {@link DrawTask#BETWEEN_BACKGROUND_AND_CONTENT} or {@link DrawTask#BETWEEN_CONTENT_AND_FOREGROUND}.
*/
public void addDrawTask(DrawTask task, int layer) {
HiLog.debug(TAG, "addDrawTask");
switch (layer) {
case DrawTask.BETWEEN_BACKGROUND_AND_CONTENT: {
mDrawTaskUnderContent = task;
if (mCanvasForTaskUnderContent == null) {
mCanvasForTaskUnderContent = new Canvas();
}
nativeAddDrawTaskUnderContent(
mNativeViewPtr, mDrawTaskUnderContent, mCanvasForTaskUnderContent.getNativePtr());
break;
}
case DrawTask.BETWEEN_CONTENT_AND_FOREGROUND: {
mDrawTaskOverContent = task;
if (mCanvasForTaskOverContent == null) {
mCanvasForTaskOverContent = new Canvas();
}
nativeAddDrawTaskOverContent(
mNativeViewPtr, mDrawTaskOverContent, mCanvasForTaskOverContent.getNativePtr());
break;
}
default: {
HiLog.error(TAG, "addDrawTask fail! Invalid number of layers.");
}
}
}
一、推荐版本:
1.创建一个自定义DrawTask,里面包含跟业务相关的自定义属性。
2.给自定义的DrawTask绑定宿主Component,构造方法
Java代码
mComponent.addDrawTask(this);
3.实现自定义的ComponentDrawTask里的onDraw方法
4.在自定义属性的set里,加上
Java代码
mComponent.invalidate();
整个代码如下:
Java 代码
/*
* Copyright (c) Huawei Technologies Co., Ltd. 2020-2020. All rights reserved.
*/
package com.huawei.watch.common.view;
import ohos.agp.components.Component;
import ohos.agp.render.Arc;
import ohos.agp.render.Canvas;
import ohos.agp.render.LinearShader;
import ohos.agp.render.Paint;
import ohos.agp.render.Shader;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.agp.utils.RectFloat;
/**
* 自定义带有圆环效果的LinearLayout。通过xml配置
* 圆环的圆心在中间,x轴水平向右,y轴水平向下,按极坐标绘制。
*
* @author t00545831
* @since 2020-05-22
*/
public class CircleProgressDrawTask implements Component.DrawTask {
// 业务模块可以在xml里配置, 用来配置圆环的粗细, 预留, 后续可以通过xml配置
private static final String STROKE_WIDTH_KEY = "stroke_width";
// 业务模块可以在xml里配置, 用来配置圆环的最大值
private static final String MAX_PROGRESS_KEY = "max_progress";
// 业务模块可以在xml里配置, 用来配置圆环的当前值
private static final String CURRENT_PROGRESS_KEY = "current_progress";
// 业务模块可以在xml里配置, 用来配置起始位置的颜色
private static final String START_COLOR_KEY = "start_color";
// 业务模块可以在xml里配置, 用来配置结束位置的颜色
private static final String END_COLOR_KEY = "end_color";
// 业务模块可以在xml里配置, 用来配置背景色
private static final String BACKGROUND_COLOR_KEY = "background_color";
// 业务模块可以在xml里配置, 用来起始位置的角度
private static final String START_ANGLE = "start_angle";
private static final float MAX_ARC = 360f;
private static final int DEFAULT_STROKE_WIDTH = 20;
private static final int DEFAULT_MAX_VALUE = 100;
private static final int DEFAULT_START_COLOR = 0xFFB566FF;
private static final int DEFAULT_END_COLOR = 0xFF8A2BE2;
private static final int DEFAULT_BACKGROUND_COLOR = 0xA8FFFFFF;
private static final int DEFAULT_START_ANGLE = -90;
private static final float DEFAULT_LINER_MAX = 100f;
private static final int HALF = 2;
private static final int NEARLY_FULL_CIRCL = 350;
// 圆环的宽度, 默认20个像素
private int mStrokeWidth = DEFAULT_STROKE_WIDTH;
// 最大的进度值, 默认是100
private int mMaxValue = DEFAULT_MAX_VALUE;
// 当前的进度值, 默认是0
private int mCurrentValue = 0;
// 起始位置的颜色, 默认浅紫色
private Color mStartColor = new Color(DEFAULT_START_COLOR);
// 结束位置的颜色, 默认深紫色
private Color mEndColor = new Color(DEFAULT_END_COLOR);
// 背景颜色, 默认浅灰色
private Color mBackgroundColor = new Color(DEFAULT_BACKGROUND_COLOR);
// 当前的进度值, 默认从-90度进行绘制
private int mStartAngle = DEFAULT_START_ANGLE;
private Component mComponent;
/**
* 传入要进行修改的view
*
* @param component 要进行修改的view
*/
public CircleProgressDrawTask(Component component) {
mComponent = component;
mComponent.addDrawTask(this);
}
/**
* 设置当前进度并且刷新所在的view
*
* @param value 当前进度
*/
public void setCurrentValue(int value) {
mCurrentValue = value;
mComponent.invalidate();
}
/**
* 设置最大的进度值并且刷新所在的view
*
* @param maxValue 最大的进度值
*/
public void setMaxValue(int maxValue) {
mMaxValue = maxValue;
mComponent.invalidate();
}
@Override
public void onDraw(Component component, Canvas canvas) {
// 计算中心点的位置, 如果是长方形, 则应该是较短的部分
int center = Math.min(component.getWidth() / HALF, component.getHeight() / HALF);
// 使用背景色绘制圆环, 选择一个画刷,宽度为设置的宽度,然后画圆。
Paint roundPaint = new Paint();
roundPaint.setAntiAlias(true);
roundPaint.setStyle(Paint.Style.STROKE_STYLE);
roundPaint.setStrokeWidth(mStrokeWidth);
roundPaint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
roundPaint.setColor(mBackgroundColor);
int radius = center - mStrokeWidth / HALF;
canvas.drawCircle(center, center, radius, roundPaint);
// 使用渐变色绘制弧形
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE_STYLE);
paint.setStrokeWidth(mStrokeWidth);
float sweepAngle = MAX_ARC * mCurrentValue / mMaxValue;
// 绘制的弧形接近满圆的时候使用BUTT画笔头
if (sweepAngle > NEARLY_FULL_CIRCL) {
paint.setStrokeCap(Paint.StrokeCap.BUTT_CAP);
} else {
paint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
}
Point point1 = new Point(0, 0);
Point point2 = new Point(DEFAULT_LINER_MAX, DEFAULT_LINER_MAX);
Point[] points = {point1, point2};
Color[] colors = {mStartColor, mEndColor};
Shader shader = new LinearShader(points, null, colors, Shader.TileMode.CLAMP_TILEMODE);
paint.setShader(shader, Paint.ShaderType.LINEAR_SHADER);
RectFloat oval = new RectFloat(center - radius, center - radius, center + radius, center + radius);
Arc arc = new Arc();
arc.setArc(mStartAngle, sweepAngle, false);
canvas.drawArc(oval, arc, paint);
}
}
调用的地方
Java 代码
LayoutScatter scatter = LayoutScatter.getInstance(this);
Component component = scatter.parse(Resource.Layout.layout_sleep, null, false);
// 为layout_sleep里的根节点添加圆环
mDrawTask = new CircleProgressDrawTask(component);
mDrawTask.setMaxValue(MAX_SLEEP_TIME);
HarmonyOS的优点在于:可以为任何控件增加一个圆环,且仅需开发一个Drawtask, 即可让所有已知控件实现圆环效果,大大减少代码工作量。
不过,该实现方案无法通过xml文件进行配置。因为在鸿蒙图形子系统里不会通过反射去创建一个自定义的DrawTask,只能创建相应的自定义控件,在未来,HarmonyOS图形子系统将能支持xml反射DrawTask的功能。综上所述,HarmonyOS提供了用户程序框架、Ability框架以及UI框架,支持应用开发过程中多终端的业务逻辑和界面逻辑进行复用,能够实现应用的一次开发、多端部署,提升了跨设备应用的开发效率。
UI控件有很多,比如textView,ImageView,Button等,由于页面风格统一,我们通常需要页面统一带有圆环效果,那么在Android上需要重复开发多次,实现多次onDraw方法。
而HarmonyOS在框架层面将这个Draw方法抽取出来了,单独放到了Drawtask接口里,这样在HarmonyOS上仅需开发一个Drawtask, 即可让所有已知控件实现圆环效果,工作量比Android大大减少。
原文链接https://mp.weixin.qq.com/s/zzLIL_IdpkG2YNE7m_Qhpw