一.概述
在实际的开发过程中,往往系统自己自带的组件是不能满足开发需求的,需要自己按照系统的规则去定制自己的组件;
鸿蒙os中的所有的组件的基类直接或者间接Component类,像Button,text,Image等等,能容纳别的组件的组件我们一般称之为容器类组件,这类组件的基类直接或者间接是ComponentContainer,像DirectionalLayout,DependentLayout等等;
二.自定义Component组件
1.继承Component类
public class TestComponent extends Component {
public TestComponent(Context context) {
super(context);
}
public TestComponent(Context context, AttrSet attrSet) {
super(context, attrSet);
}
public TestComponent(Context context, AttrSet attrSet, String styleName) {
super(context, attrSet, styleName);
}
public TestComponent(Context context, AttrSet attrSet, int resId) {
super(context, attrSet, resId);
}
}
创建TestComponent继承Component并覆写其中的构造方案,如果当前的自定义组件需要使用到xml中,那需要至少复现一个带AttrSet参数的方法,因为xml中的属性需要用这个类获取和设置,是不是跟android的感觉一样,后面会有跟多的相似的地方;
2.设置绘制的回调
(1)首先创建一个绘制的task
private DrawTask mDrawTask = new DrawTask() {
@Override
public void onDraw(Component component, Canvas canvas) {
TestComponent.this.onDraw(component,canvas);
}
};
(2)实现上一步骤中的onDraw方法
private void onDraw(Component component, Canvas canvas) {
//todo 这里就是使用canvas绘制我们需要内容
}
(3)在构造方法中加入Task
public TestComponent(Context context) {
super(context);
addDrawTask(mDrawTask);
}
public TestComponent(Context context, AttrSet attrSet) {
super(context, attrSet);
addDrawTask(mDrawTask);
}
这里只是展示了两个构造方案中调用addDrawTask,但不是只能这么用,因为鸿蒙os在绘制我们的组件的时候,会调用我们add进去的这个task,所以我们提前把我们业务逻辑add进去而已;
这样你只需要在private void onDraw中绘制我们的内容就可以了;
例如我们绘制一个画线的画板:绘制的代码如下
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.Path;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.app.Context;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.multimodalinput.event.MmiPoint;
import ohos.multimodalinput.event.TouchEvent;
public class TestComponent extends Component {
private static final String TAG = "TestComponent";
private DrawTask mDrawTask = (component, canvas) -> TestComponent.this.onDraw(component,canvas);
private TouchEventListener mTouchEventListener = (component, touchEvent) -> TestComponent.this.onTouchEvent(component, touchEvent);
private Paint mPaint;
private Path mPath;
private Point mPrePoint;
private Point mPreCtrlPoint;
public TestComponent(Context context) {
super(context);
initPaint();
addDrawTask(mDrawTask);
setTouchEventListener(mTouchEventListener);
}
public TestComponent(Context context, AttrSet attrSet) {
super(context, attrSet);
initPaint();
addDrawTask(mDrawTask);
setTouchEventListener(mTouchEventListener);
}
public TestComponent(Context context, AttrSet attrSet, String styleName) {
super(context, attrSet, styleName);
}
public TestComponent(Context context, AttrSet attrSet, int resId) {
super(context, attrSet, resId);
}
private void initPaint() {
//初始化paint
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setStrokeWidth(5f);
mPaint.setStyle(Paint.Style.STROKE_STYLE);
}
private void onDraw(Component component, Canvas canvas) {
canvas.drawPath(mPath, mPaint);
}
public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
switch (touchEvent.getAction()) {
case TouchEvent.PRIMARY_POINT_DOWN: {
//鸿蒙Log工具
HiLog.debug(new HiLogLabel(0, 0, TAG), "TouchEvent.PRIMARY_POINT_DOWN");
//获取点信息
MmiPoint point = touchEvent.getPointerPosition(touchEvent.getIndex());
mPath.reset();
mPath.moveTo(point.getX(), point.getY());
mPrePoint.position[0] = point.getX();
mPrePoint.position[1] = point.getY();
mPreCtrlPoint.position[0] = point.getX();
mPreCtrlPoint.position[1] = point.getY();
//PRIMARY_POINT_DOWN 一定要返回true
return true;
}
case TouchEvent.PRIMARY_POINT_UP:
break;
case TouchEvent.POINT_MOVE: {
HiLog.debug(new HiLogLabel(0, 0, TAG), "TouchEvent.POINT_MOVE");
MmiPoint point = touchEvent.getPointerPosition(touchEvent.getIndex());
Point currCtrlPoint = new Point((point.getX() + mPrePoint.position[0]) / 2,
(point.getY() + mPrePoint.position[1]) / 2);
//绘制三阶贝塞尔曲线
mPath.cubicTo(mPrePoint, mPreCtrlPoint, currCtrlPoint);
mPreCtrlPoint.position[0] = currCtrlPoint.position[0];
mPreCtrlPoint.position[1] = currCtrlPoint.position[1];
mPrePoint.position[0] = point.getX();
mPrePoint.position[1] = point.getY();
//更新显示
invalidate();
break;
}
}
return false;
}
}
上面就实现自定义普通组件的过程
三.自定义ComponentContainer组件
1.继承ComponentContainer类
public class TestContainer extends ComponentContainer {
public TestContainer(Context context) {
super(context);
}
public TestContainer(Context context, AttrSet attrSet) {
super(context, attrSet);
}
public TestContainer(Context context, AttrSet attrSet, String styleName) {
super(context, attrSet, styleName);
}
}
跟自定义Component一样继承容器类控件的基类,并实现其中的构造方法
2.添加测量,布局,绘制的回调
因为容器类组件是能容纳其他组件的特殊组件,所以它不仅仅需要关心自己,也需要关心它内部的子组件,故我们需要关注的方法变成了三个:
(1)初始化测量,布局,绘制的回调
//测量的回调
private EstimateSizeListener mEstimateSizeListener = new EstimateSizeListener() {
@Override
public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) {
return TestContainer.this.onEstimateSize(widthEstimateConfig,heightEstimateConfig);
}
};
//布局的回调
private ArrangeListener mArrangeListener = new ArrangeListener() {
@Override
public boolean onArrange(int left, int top, int width, int height) {
return TestContainer.this.onArrange(left,top,width,height);
}
};
//绘制的回调
private DrawTask mDrawTask = new DrawTask() {
@Override
public void onDraw(Component component, Canvas canvas) {
onDraw(component,canvas);
}
};
里面的回调,下面会讲解,先把基本步骤搞定;
(2)添加回调
public TestContainer(Context context) {
super(context);
//add测量的回调
setEstimateSizeListener(mEstimateSizeListener);
//add布局的回调
setArrangeListener(mArrangeListener);
//add绘制的回调
addDrawTask(mDrawTask);
}
public TestContainer(Context context, AttrSet attrSet) {
super(context, attrSet);
//add测量的回调
setEstimateSizeListener(mEstimateSizeListener);
//add布局的回调
setArrangeListener(mArrangeListener);
//add绘制的回调
addDrawTask(mDrawTask);
}
这就是自定义的ComponentContainer的基本步骤
上面步骤中
setEstimateSizeListener(mEstimateSizeListener); 这个是设置测量的回调,里面是一个接口,参数是int类型,分别是widthEstimateConfig和heightEstimateConfig
这两个值也是分为Mode和value的,获取这Mode和Value使用的是EstimateSpec这个类,如下:
int wideMode = EstimateSpec.getMode(widthEstimateConfig);
int wideSize = EstimateSpec.getSize(widthEstimateConfig);
int heightMode = EstimateSpec.getMode(heightEstimateConfig);
int heightSize = EstimateSpec.getSize(heightEstimateConfig);
Mode的值一共有如下三种:
NOT_EXCEED :在此模式下,已为子组件指定最大大小;
PRECISE :在这种模式下,父组件已经确定了子组件的确切尺寸;
UNCONSTRAINT :在这种模式下,父组件对子组件没有任何约束,这意味着子组件可以是所需的任何大小;
四.对比android,加深理解
android和鸿蒙对比
| android | 鸿蒙 |
基类 | View(普通组件)ViewGroup(容器类组件) | Component(普通组件)ComponentContainer(容器类组件) |
普通组件 | 覆写onDraw方法 | 添加绘制的任务(回调) 添加方法 addDrawTask 任务类 DrawTask |
容器类组件 | 覆写测量方法onMeasure | 添加测量的回调 设置方法 setEstimateSizeListener 回调接口的方法 onEstimateSize |
覆写布局方法onLayout | 添加布局的回调 设置方法 setArrangeListener 回调接口的方法 onArrange | |
覆写绘制方法onDraw | 添加绘制的任务(回调) 添加方法 addDrawTask 任务类 DrawTask | |
Mode值 | UNSPECIFIED:父容器没有对当前View有任何限制,当前View可以任意取尺寸 | UNCONSTRAINT:在这种模式下,父组件对子组件没有任何约束,这意味着子组件可以是所需的任何大小 |
EXACTLY:当前的尺寸就是当前View应该取的尺寸 | PRECISE:在这种模式下,父组件已经确定了子组件的确切尺寸 | |
AT_MOST:当前尺寸是当前View能取的最大尺寸 | NOT_EXCEED:在此模式下,已为子组件指定最大大小 |
总结,以上就是鸿蒙自定义组件的基础内容,由于当前还看不到系统的源码,具体的实现逻辑还有待开源后补充;