一.概述

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

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

 

 

 

 

 

 

 

 

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

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐