一.概述

在实际的开发过程中,往往系统自己自带的组件是不能满足开发需求的,需要自己按照系统的规则去定制自己的组件;

鸿蒙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:在此模式下,已为子组件指定最大大小

 

 

 

 

 

 

 

 

总结,以上就是鸿蒙自定义组件的基础内容,由于当前还看不到系统的源码,具体的实现逻辑还有待开源后补充;