自定义组件

当Java UI框架提供的组件无法满足设计需求时,可以创建自定义组件,根据设计需求添加绘制任务,并定义组件的属性及事件响应,完成组件的自定义。

常用接口

接口名 作用
setEstimateSizeListener 设置测量组件的侦听器。
onEstimateSize 测量组件的大小以确定宽度和高度。
setEstimatedSize 将测量的宽度和高度设置给组件。
EstimateSpec.getChildSizeWithMode 基于指定的大小和模式为子组件创建度量规范。
EstimateSpec.getSize 从提供的度量规范中提取大小。
EstimateSpec.getMode 获取该组件的显示模式。
addDrawTask 添加绘制任务。
onDraw 通过绘制任务更新组件时调用。

如何实现自定义组件

下面以自定义圆环组件为例,介绍自定义组件的通用配置方法:在屏幕中绘制蓝色圆环,并实现点击变化圆环颜色的功能。

图1 在界面中显示的自定义圆环组件 img

  1. 创建自定义组件的类,并继承Component或其子类,添加构造方法。

    示例代码如下:

    public class CustomComponent extends Component{    public CustomComponent(Context context) {        super(context);    }}
    
  2. 实现Component.EstimateSizeListener接口,在onEstimateSize方法中进行组件测量,并通过setEstimatedSize方法将测量的宽度和高度设置给组件。

    示例代码如下:

    public class CustomComponent extends Component implements Component.EstimateSizeListener {    public CustomComponent(Context context) {        super(context);
            ...
            // 设置测量组件的侦听器        setEstimateSizeListener(this);    }
        ...
        @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.NOT_EXCEED),            Component.EstimateSpec.getChildSizeWithMode(height, height, Component.EstimateSpec.NOT_EXCEED));        return true;    }}
    
    • 注意事项

      1. 自定义组件测量出的大小需通过setEstimatedSize设置给组件,并且必须返回true使测量值生效。
      2. setEstimatedSize方法的入参携带模式信息,可使用Component.EstimateSpec.getChildSizeWithMode方法进行拼接。
    • 测量模式

      测量组件的宽高需要携带模式信息,不同测量模式下的测量结果也不相同,需要根据实际需求选择适合的测量模式。

      | 模式 | 作用 | | ------------ | -------------------------------------------------- | | UNCONSTRAINT | 父组件对子组件没有约束,表示子组件可以任意大小。 | | PRECISE | 父组件已确定子组件的大小。 | | NOT_EXCEED | 已为子组件确定了最大大小,子组件不能超过指定大小。 |

  3. 实现Component.DrawTask接口,在onDraw方法中执行绘制任务,该方法提供的画布Canvas,可以精确控制屏幕元素的外观。在执行绘制任务之前,需要定义画笔Paint。

    示例代码如下:

    public class CustomComponent extends Component implements Component.DrawTask,Component.EstimateSizeListener {    // 圆环宽度    private static final float CIRCLE_STROKE_WIDTH = 100f;
        // 绘制圆环的画笔    private Paint circlePaint;            public CustomComponent(Context context) {        super(context);
            // 初始化画笔        initPaint();
            // 添加绘制任务        addDrawTask(this);    }
        private void initPaint(){        circlePaint = new Paint();        circlePaint.setColor(Color.BLUE);        circlePaint.setStrokeWidth(CIRCLE_STROKE_WIDTH);        circlePaint.setStyle(Paint.Style.STROKE_STYLE);    }
        @Override    public void onDraw(Component component, Canvas canvas) {
            // 在界面中绘制一个圆心坐标为(500,500),半径为400的圆        canvas.drawCircle(500,500,400,circlePaint);    }
        ...}
    
  4. 实现Component.TouchEventListener或其他事件的接口,使组件可响应用户输入。

    示例代码如下:

    public class CustomComponent extends Component implements Component.DrawTask, Component.EstimateSizeListener, Component.TouchEventListener {    ...    public CustomComponent(Context context) {        ...
            // 设置TouchEvent响应事件        setTouchEventListener(this);    }        ...
        @Override    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {        switch (touchEvent.getAction()) {            case TouchEvent.PRIMARY_POINT_DOWN:                circlePaint.setColor(Color.GREEN);                invalidate();                break;            case TouchEvent.PRIMARY_POINT_UP:                circlePaint.setColor(Color.YELLOW);                invalidate();                break;        }        return false;    }}
    
    • 注意事项
      1. 需要更新UI显示时,可调用invalidate()方法。
      2. 示例中展示TouchEventListener为响应触摸事件,除此之外还可实现ClickedListener响应点击事件、LongClickedListener响应长按事件等。
  5. 在onStart()方法中,将自定义组件添加至UI界面中。

    @Overrideprotected void onStart(Intent intent) {    super.onStart(intent);    DirectionalLayout.LayoutConfig config = new DirectionalLayout.LayoutConfig(        DirectionalLayout.LayoutConfig.MATCH_PARENT, DirectionalLayout.LayoutConfig.MATCH_PARENT);    myLayout.setLayoutConfig(config);
        CustomComponent customComponent = new CustomComponent(this);    DirectionalLayout.LayoutConfig layoutConfig = new DirectionalLayout.LayoutConfig(1080, 1000);    customComponent.setLayoutConfig(layoutConfig);
        myLayout.addComponent(customComponent);    super.setUIContent(myLayout);}
    

场景示例

利用自定义组件,绘制环形进度控制器,可通过滑动改变当前进度,也可响应进度的改变,UI显示的样式也可通过设置属性进行调整。

图2 自定义环形进度控制器 img

示例代码如下:

public class CustomControlBar extends Component implements Component.DrawTask,    Component.EstimateSizeListener, Component.TouchEventListener {    private final static float CIRCLE_ANGLE = 360.0f;
    private final static int DEF_UNFILL_COLOR = 0xFF808080;
    private final static int DEF_FILL_COLOR = 0xFF1E90FF;
    // 圆环轨道颜色    private Color unFillColor;
    // 圆环覆盖颜色    private Color fillColor;
    // 圆环宽度    private int circleWidth;
    // 画笔    private Paint paint;
    // 个数    private int count;
    // 当前进度    private int currentCount;
    // 间隙值    private int splitSize;
    // 内圆的正切方形    private RectFloat centerRectFloat;
    // 中心绘制的图片    private PixelMap image;
    // 原点坐标    private Point centerPoint;
    // 进度改变的事件响应    private ProgressChangeListener listener;
    public CustomControlBar(Context context) {        super(context);        paint = new Paint();        initData();        setEstimateSizeListener(this);        setTouchEventListener(this);        addDrawTask(this);    }
    // 初始化属性值    private void initData() {        unFillColor = new Color(DEF_UNFILL_COLOR);        fillColor = new Color(DEF_FILL_COLOR);        count = 10;        currentCount = 2;        splitSize = 15;        circleWidth = 60;        centerRectFloat = new RectFloat();        image = Utils.createPixelMapByResId(ResourceTable.Media_icon, getContext()).get();        listener = null;    }
    @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;    }
    @Override    public void onDraw(Component component, Canvas canvas) {        paint.setAntiAlias(true);        paint.setStrokeWidth(circleWidth);        paint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);        paint.setStyle(Paint.Style.STROKE_STYLE);
        int width = getWidth();        int center = width / 2;        centerPoint = new Point(center, center);        int radius = center - circleWidth / 2;        drawCount(canvas, center, radius);
        int inRadius = center - circleWidth;        double length = inRadius - Math.sqrt(2) * 1.0f / 2 * inRadius;        centerRectFloat.left = (float) (length + circleWidth);        centerRectFloat.top = (float) (length + circleWidth);        centerRectFloat.bottom = (float) (centerRectFloat.left + Math.sqrt(2) * inRadius);        centerRectFloat.right = (float) (centerRectFloat.left + Math.sqrt(2) * inRadius);
        // 如果图片比较小,那么根据图片的尺寸放置到正中心        Size imageSize = image.getImageInfo().size;        if (imageSize.width < Math.sqrt(2) * inRadius) {            centerRectFloat.left = (float) (centerRectFloat.left + Math.sqrt(2) * inRadius * 1.0f / 2 - imageSize.width * 1.0f / 2);            centerRectFloat.top = (float) (centerRectFloat.top + Math.sqrt(2) * inRadius * 1.0f / 2 - imageSize.height * 1.0f / 2);            centerRectFloat.right = centerRectFloat.left + imageSize.width;            centerRectFloat.bottom = centerRectFloat.top + imageSize.height;        }        canvas.drawPixelMapHolderRect(new PixelMapHolder(image), centerRectFloat, paint);    }
    private void drawCount(Canvas canvas, int centre, int radius) {        float itemSize = (CIRCLE_ANGLE - count * splitSize) / count;
        RectFloat oval = new RectFloat(centre - radius, centre - radius, centre + radius, centre + radius);
        paint.setColor(unFillColor);        for (int i = 0; i < count; i++) {            Arc arc = new Arc((i * (itemSize + splitSize)) - 90, itemSize, false);            canvas.drawArc(oval, arc, paint);        }
        paint.setColor(fillColor);        for (int i = 0; i < currentCount; i++) {            Arc arc = new Arc((i * (itemSize + splitSize)) - 90, itemSize, false);            canvas.drawArc(oval, arc, paint);        }    }
    @Override    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {        switch (touchEvent.getAction()) {            case TouchEvent.PRIMARY_POINT_DOWN:            case TouchEvent.POINT_MOVE: {                this.getContentPositionX();                MmiPoint absPoint = touchEvent.getPointerPosition(touchEvent.getIndex());                Point point = new Point(absPoint.getX() - getContentPositionX(),                    absPoint.getY() - getContentPositionY());                double angle = calcRotationAngleInDegrees(centerPoint, point);                double multiple = angle / (CIRCLE_ANGLE / count);                if ((multiple - (int) multiple) > 0.4) {                    currentCount = (int) multiple + 1;                } else {                    currentCount = (int) multiple;                }                if (listener != null) {                    listener.onProgressChangeListener(currentCount);                }                invalidate();                break;            }        }        return false;    }
    public interface ProgressChangeListener {        void onProgressChangeListener(int Progress);    }
    // 计算centerPt到targetPt的夹角,单位为度。返回范围为[0, 360),顺时针旋转。    private double calcRotationAngleInDegrees(Point centerPt, Point targetPt) {        double theta = Math.atan2(targetPt.getPointY()            - centerPt.getPointY(), targetPt.getPointX()            - centerPt.getPointX());        theta += Math.PI / 2.0;        double angle = Math.toDegrees(theta);        if (angle < 0) {            angle += CIRCLE_ANGLE;        }        return angle;    }
    public Color getUnFillColor() {        return unFillColor;    }
    public CustomControlBar setUnFillColor(Color unFillColor) {        this.unFillColor = unFillColor;        return this;    }
    public Color getFillColor() {        return fillColor;    }
    public CustomControlBar setFillColor(Color fillColor) {        this.fillColor = fillColor;        return this;    }
    public int getCircleWidth() {        return circleWidth;    }
    public CustomControlBar setCircleWidth(int circleWidth) {        this.circleWidth = circleWidth;        return this;    }
    public int getCount() {        return count;    }
    public CustomControlBar setCount(int count) {        this.count = count;        return this;    }
    public int getCurrentCount() {        return currentCount;    }
    public CustomControlBar setCurrentCount(int currentCount) {        this.currentCount = currentCount;        return this;    }
    public int getSplitSize() {        return splitSize;    }
    public CustomControlBar setSplitSize(int splitSize) {        this.splitSize = splitSize;        return this;    }
    public PixelMap getImage() {        return image;    }
    public CustomControlBar setImage(PixelMap image) {        this.image = image;        return this;    }
    public void build() {        invalidate();    }
    public void setProgressChangerListener(ProgressChangeListener listener) {        this.listener = listener;    }}

在绘制图片时使用到Utils工具类:

public class Utils {    private static final HiLogLabel TAG = new HiLogLabel(3, 0xD001100, "Utils");
    private static byte[] readResource(Resource resource) {        final int bufferSize = 1024;        final int ioEnd = -1;        byte[] byteArray;        byte[] buffer = new byte[bufferSize];        try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {            while (true) {                int readLen = resource.read(buffer, 0, bufferSize);                if (readLen == ioEnd) {                    HiLog.error(TAG, "readResource finish");                    byteArray = output.toByteArray();                    break;                }                output.write(buffer, 0, readLen);            }        } catch (IOException e) {            HiLog.debug(TAG, "readResource failed " + e.getLocalizedMessage());            return new byte[0];        }        HiLog.debug(TAG, "readResource len: " + byteArray.length);        return byteArray;    }
    /**     * Creates a {@code PixelMap} object based on the image resource ID.     * <p>     * This method only loads local image resources. If the image file does not exist or the loading fails,     * {@code null} is returned.     *     * @param resourceId Indicates the image resource ID.     * @param slice      Indicates the Context.     * @return Returns the image.     */    public static Optional<PixelMap> createPixelMapByResId(int resourceId, Context slice) {        ResourceManager manager = slice.getResourceManager();        if (manager == null) {            return Optional.empty();        }        try (Resource resource = manager.getResource(resourceId)) {            if (resource == null) {                return Optional.empty();            }            ImageSource.SourceOptions srcOpts = new ImageSource.SourceOptions();            srcOpts.formatHint = "image/png";            ImageSource imageSource = ImageSource.create(readResource(resource), srcOpts);            if (imageSource == null) {                return Optional.empty();            }            ImageSource.DecodingOptions decodingOpts = new ImageSource.DecodingOptions();            decodingOpts.desiredSize = new Size(0, 0);            decodingOpts.desiredRegion = new Rect(0, 0, 0, 0);            decodingOpts.desiredPixelFormat = PixelFormat.ARGB_8888;
            return Optional.of(imageSource.createPixelmap(decodingOpts));        } catch (NotExistException | IOException e) {            return Optional.empty();        }    }}

在onStart()方法里将此组件添加到界面中,并可自由设置其事件响应、颜色、大小等属性。

@Overrideprotected void onStart(Intent intent) {    super.onStart(intent);    DirectionalLayout.LayoutConfig config = new DirectionalLayout.LayoutConfig(        DirectionalLayout.LayoutConfig.MATCH_PARENT, DirectionalLayout.LayoutConfig.MATCH_PARENT);    myLayout.setLayoutConfig(config);
    // 在此创建自定义组件,并可设置其属性    CustomControlBar controlBar = new CustomControlBar(this);    controlBar.setClickable(true);    DirectionalLayout.LayoutConfig layoutConfig = new DirectionalLayout.LayoutConfig(        600, 600);    controlBar.setLayoutConfig(layoutConfig);    ShapeElement element = new ShapeElement();    element.setRgbColor(new RgbColor(0, 0, 0));    controlBar.setBackground(element);
    // 将此组件添加至布局,并在界面中显示    myLayout.addComponent(controlBar);    super.setUIContent(myLayout);}

相关实例

针对自定义组件开发,有以下实例可供参考:

  • 自定义组件

    通过一个圆形抽奖转盘演示HarmonyOS自定义组件的实现。

  • 分布式地图导航

    基于分布式能力,实现地图导航信息在手机-车机-智能穿戴设备之间流转。

results matching ""

    No results matching ""