4.8【HarmonyOS 鸿蒙开发】自定义组件
作者:韩茹
公司:程序咖(北京)科技有限公司
鸿蒙巴士专栏作家
HarmonyOS 提供了一套简单且弱小的 Java UI 框架,其中 Component 提供内容显示,是界面中所有组件的基类。ComponentContainer 作为容器包容 Component 或 ComponentContainer 对象,并对它们进行布局。
Java UI 框架也提供了一部分 Component 和 ComponentContainer 的具体子类,即罕用的组件(比方:Text、Button、Image 等)和罕用的布局(比方:DirectionalLayout、DependentLayout 等)。如果现有的组件和布局无奈满足设计需要,例如仿遥控器的圆盘按钮、可滑动的环形控制器等,能够通过自定义组件和自定义布局来实现。
自定义组件是由开发者定义的具备肯定个性的组件,通过扩大 Component 或其子类实现,能够准确管制屏幕元素的外观,也可响应用户的点击、触摸、长按等操作。
自定义布局是由开发者定义的具备特定布局规定的容器类组件,通过扩大 ComponentContainer 或其子类实现,能够将各子组件摆放到指定的地位,也可响应用户的滑动、拖拽等事件。
一、罕用接口
当 Java UI 框架提供的组件无奈满足设计需要时,能够创立自定义组件,依据设计需要增加绘制工作,并定义组件的属性及事件响应,实现组件的自定义。
接口名 | 作用 |
---|---|
setEstimateSizeListener | 设置测量组件的侦听器。 |
onEstimateSize | 测量组件的大小以确定宽度和高度。 |
setEstimatedSize | 将测量的宽度和高度设置给组件。 |
EstimateSpec.getChildSizeWithMode | 基于指定的大小和模式为子组件创立度量标准。 |
EstimateSpec.getSize | 从提供的度量标准中提取大小。 |
EstimateSpec.getMode | 获取该组件的显示模式。 |
addDrawTask | 增加绘制工作。 |
onDraw | 通过绘制工作更新组件时调用。 |
二、如何实现自定义组件
上面以自定义圆环组件为例,介绍自定义组件的通用配置办法:在屏幕中绘制蓝色圆环,并实现点击变动圆环色彩的性能。
1、创立自定义组件的类,并继承 Component 或其子类,增加构造方法。
新建一个 java 文件:CustomComponent.java
示例代码如下:
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;
}
}
-
注意事项
- 自定义组件测量出的大小需通过 setEstimatedSize 设置给组件,并且必须返回 true 使测量值失效。
- 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;
}
return false;
}
}
- 注意事项
- 须要更新 UI 显示时,可调用 invalidate() 办法。
- 示例中展现 TouchEventListener 为响应触摸事件,除此之外还可实现 ClickedListener 响应点击事件、LongClickedListener 响应长按事件等。
5、在 onStart() 办法中,将自定义组件增加至 UI 界面中。
package com.example.hanrucustomcomponent.slice;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.content.Intent;
import ohos.agp.colors.RgbColor;
import ohos.agp.components.Component;
import ohos.agp.components.DirectionalLayout;
import ohos.agp.components.element.ShapeElement;
import ohos.agp.utils.LayoutAlignment;
public class MainAbilitySlice extends AbilitySlice {
@Override
public void onStart(Intent intent) {super.onStart(intent);
// super.setUIContent(ResourceTable.Layout_ability_main);
drawCustomComponent();}
// 自定义组件 1
public void drawCustomComponent(){
// 申明布局
DirectionalLayout myLayout = new DirectionalLayout(this);
DirectionalLayout.LayoutConfig config = new DirectionalLayout.LayoutConfig(DirectionalLayout.LayoutConfig.MATCH_PARENT, DirectionalLayout.LayoutConfig.MATCH_PARENT);
myLayout.setLayoutConfig(config);
ShapeElement shapeElement1 = new ShapeElement();
RgbColor rgbColor = new RgbColor(135,206,250);
shapeElement1.setRgbColor(rgbColor);
myLayout.setBackground(shapeElement1);
myLayout.setOrientation(Component.VERTICAL);
myLayout.setAlignment(LayoutAlignment.HORIZONTAL_CENTER);
CustomComponent customComponent = new CustomComponent(this);
DirectionalLayout.LayoutConfig layoutConfig = new DirectionalLayout.LayoutConfig(1000, 1000);
ShapeElement shapeElement2 = new ShapeElement();
RgbColor rgbColor2 = new RgbColor(219,112,147);
shapeElement2.setRgbColor(rgbColor2);
customComponent.setBackground(shapeElement2);
customComponent.setLayoutConfig(layoutConfig);
myLayout.addComponent(customComponent);
super.setUIContent(myLayout);
}
}
运行成果:
三、写个例子
利用自定义组件,绘制环形进度控制器,可通过滑动扭转以后进度,也可响应进度的扭转,UI 显示的款式也可通过设置属性进行调整。
咱们再创立一个自定义组件类 CustomControlBar:
package com.example.hanrucustomcomponent.slice;
import com.example.hanrucustomcomponent.ResourceTable;
import ohos.agp.components.Component;
import ohos.agp.render.Arc;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.PixelMapHolder;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.media.image.PixelMap;
import ohos.media.image.common.Size;
import ohos.multimodalinput.event.MmiPoint;
import ohos.multimodalinput.event.TouchEvent;
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;
}
}
而后在 onStart 中:
package com.example.hanrucustomcomponent.slice;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.content.Intent;
import ohos.agp.colors.RgbColor;
import ohos.agp.components.Component;
import ohos.agp.components.DirectionalLayout;
import ohos.agp.components.element.ShapeElement;
import ohos.agp.utils.LayoutAlignment;
public class MainAbilitySlice extends AbilitySlice {
@Override
public void onStart(Intent intent) {super.onStart(intent);
// super.setUIContent(ResourceTable.Layout_ability_main);
// drawCustomComponent();
drawCustomControlBar();}
...
// 自定义组件 2
public void drawCustomControlBar(){DirectionalLayout myLayout = new DirectionalLayout(this);
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);
}
}
运行后果:
更多内容:
1、社区:鸿蒙巴士 https://www.harmonybus.net/
2、公众号:HarmonyBus
3、技术交换 QQ 群:714518656
4、视频课:https://www.chengxuka.com