HarmonyOS提供了一套复杂且强大的Java UI框架,其中Component提供内容显示,是界面中所有组件的基类。ComponentContainer作为容器容纳Component或ComponentContainer对象,并对它们进行布局。
Java UI框架也提供了一部分Component和ComponentContainer的具体子类,即常用的组件(比如:Text、Button、Image等)和常用的布局(比如:DirectionalLayout、DependentLayout等)。如果现有的组件和布局无法满足设计需求,例如仿遥控器的圆盘按钮、可滑动的环形控制器等,可以通过自定义组件和自定义布局来实现。
自定义组件是由开发者定义的具有一定特性的组件,通过扩展Component或其子类实现,可以精确控制屏幕元素的外观,也可响应用户的点击、触摸、长按等操作。
自定义布局是由开发者定义的具有特定布局规则的容器类组件,通过扩展ComponentContainer或其子类实现,可以将各子组件摆放到指定的位置,也可响应用户的滑动、拖拽等事件。
自定义组件
当Java UI框架提供的组件无法满足设计需求时,可以创建自定义组件,根据设计需求添加绘制任务,并定义组件的属性及事件响应,完成组件的自定义。
常用接口
如何实现自定义组件
下面以自定义圆环组件为例,介绍自定义组件的通用配置方法:在屏幕中绘制圆环,并实现点击改变圆环颜色的功能。
在界面中显示的自定义圆环组件
1. 创建自定义组件的类,并继承Component或其子类,添加构造方法。
示例代码如下
public class CustomComponent extends Component{ public CustomComponent(Context context) { this(context, null); } //如需支持xml创建自定义组件,必须添加该构造方法 public CustomComponent(Context context, AttrSet attrSet) { super(context, attrSet); } }
2. 实现Component.EstimateSizeListener接口,在onEstimateSize方法中进行组件测量,并通过setEstimatedSize方法通知组件。
示例代码如下:
public class CustomComponent extends Component implements Component.EstimateSizeListener { //240为组件默认大小 public int width = 240; public int height = 240; public CustomComponent(Context context, AttrSet attrSet) { ... // 设置测量组件的侦听器 setEstimateSizeListener(this); } ... @Override public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) { int widthSpce = EstimateSpec.getMode(widthEstimateConfig); int heightSpce = EstimateSpec.getMode(heightEstimateConfig); int widthConfig = 0; switch (widthSpce) { case EstimateSpec.UNCONSTRAINT: case EstimateSpec.PRECISE: width = EstimateSpec.getSize(widthEstimateConfig); widthConfig = EstimateSpec.getSizeWithMode(width, EstimateSpec.PRECISE); break; case EstimateSpec.NOT_EXCEED: widthConfig = EstimateSpec.getSizeWithMode(width, EstimateSpec.PRECISE); break; default: break; } int heightConfig = 0; switch (heightSpce) { case EstimateSpec.UNCONSTRAINT: case EstimateSpec.PRECISE: height = EstimateSpec.getSize(heightEstimateConfig); heightConfig = EstimateSpec.getSizeWithMode(height, EstimateSpec.PRECISE); break; case EstimateSpec.NOT_EXCEED: heightConfig = EstimateSpec.getSizeWithMode(height, EstimateSpec.PRECISE); break; default: break; } setEstimatedSize(widthConfig, heightConfig); return true; } }
注意事项
1.自定义组件测量出的大小需通过setEstimatedSize通知组件,并且必须返回true使测量值生效。
2.setEstimatedSize方法的入参携带模式信息,可使用Component.EstimateSpec.getSizeWithMode方法进行拼接。
测量模式
测量组件的宽高需要携带模式信息,不同测量模式下的测量结果也不相同,需要根据实际需求选择适合的测量模式。
测量模式信息
3. 自定义xml属性,通过构造方法中携带的参数attrSet,可以获取到在xml中配置的属性值,并应用在该自定义组件中。
示例代码如下:
public class CustomComponent extends Component implements Component.EstimateSizeListener { private static final String ATTR_RING_WIDTH = "ring_width"; private static final String ATTR_RING_RADIUS = "ring_radius"; private static final String ATTR_DEFAULT_COLOR = "default_color"; private static final String ATTR_PRESSED_COLOR = "pressed_color"; public float ringWidth = 20f; //圆环宽度 public float ringRadius = 100f; //圆环半径 public Color defaultColor = Color.YELLOW; //默认颜色 public Color pressedColor = Color.CYAN; //按压态颜色 public CustomComponent(Context context, AttrSet attrSet) { ... //初始化xml属性 initAttrSet(attrSet); } private void initAttrSet(AttrSet attrSet) { if (attrSet == null) return; if (attrSet.getAttr(ATTR_DEFAULT_COLOR).isPresent()) { defaultColor = attrSet.getAttr(ATTR_DEFAULT_COLOR).get().getColorValue(); } if (attrSet.getAttr(ATTR_RING_WIDTH).isPresent()) { ringWidth = attrSet.getAttr(ATTR_RING_WIDTH).get().getDimensionValue(); } if (attrSet.getAttr(ATTR_RING_RADIUS).isPresent()) { ringRadius = attrSet.getAttr(ATTR_RING_RADIUS).get().getDimensionValue(); } if (attrSet.getAttr(ATTR_PRESSED_COLOR).isPresent()) { pressedColor = attrSet.getAttr(ATTR_PRESSED_COLOR).get().getColorValue(); } } }
4. 实现Component.DrawTask接口,在onDraw方法中执行绘制任务,该方法提供的画布Canvas,可以精确控制屏幕元素的外观。在执行绘制任务之前,需要定义画笔Paint。
示例代码如下:
public class CustomComponent extends Component implements Component.DrawTask,Component.EstimateSizeListener { // 绘制圆环的画笔 private Paint circlePaint; public CustomComponen(Context context, AttrSet attrSet) { ... // 初始化画笔 initPaint(); // 添加绘制任务 addDrawTask(this); } private void initPaint(){ circlePaint = new Paint(); circlePaint.setColor(defaultColor); circlePaint.setStrokeWidth(ringWidth); circlePaint.setStyle(Paint.Style.STROKE_STYLE); } @Override public void onDraw(Component component, Canvas canvas) { int x = width / 2; int y = height / 2; canvas.drawCircle(x, y, ringRadius, circlePaint); } ... }
5. 实现Component.TouchEventListener或其他事件的接口,使组件可响应用户输入。
示例代码如下
public class CustomComponent extends Component implements Component.DrawTask, Component.EstimateSizeListener, Component.TouchEventListener { ... public CustomComponent(Context context, AttrSet attrSet) { ... // 设置TouchEvent响应事件 setTouchEventListener(this); } ... @Override public boolean onTouchEvent(Component component, TouchEvent touchEvent) { switch (touchEvent.getAction()) { case TouchEvent.PRIMARY_POINT_DOWN: circlePaint.setColor(pressedColor); invalidate(); break; case TouchEvent.PRIMARY_POINT_UP: circlePaint.setColor(defaultColor); invalidate(); break; } return true; } }
注意:
1.需要更新UI显示时,可调用invalidate()方法。
2.示例中展示TouchEventListener为响应触摸事件,除此之外还可实现ClickedListener响应点击事件、LongClickedListener响应长按事件等。
6. 在xml文件中创建并配置自定义组件
<?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" xmlns:custom="http://schemas.huawei.com/res/custom" ohos:height="match_parent" ohos:width="match_parent" ohos:orientation="vertical"> <!-- 请根据实际包名和文件路径引入--> <com.huawei.harmonyosdemo.custom.CustomComponent ohos:height="300vp" ohos:width="match_parent" ohos:background_element="black" ohos:clickable="true" custom:default_color="gray" custom:pressed_color="red" custom:ring_width="20vp" custom:ring_radius="120vp"/> </DirectionalLayout>
场景示例
利用自定义组件,绘制环形进度控制器,可通过滑动改变当前进度,也可响应进度的改变,UI显示的样式也可通过设置属性进行调整。
自定义环形进度控制器
示例代码如下:
public class CustomControlBar extends Component implements Component.DrawTask, Component.EstimateSizeListener, Component.TouchEventListener { private final static String ATTR_UN_FILL_COLOR = "unfill_color"; private final static String ATTR_FILL_COLOR = "fill_color"; private final static String ATTR_CIRCLE_WIDTH = "circle_width"; private final static String ATTR_COUNT = "count"; private final static String ATTR_CURRENT_PROGRESS = "current_progress"; private final static String ATTR_SPLIT_SIZE = "split_size"; private final static String ATTR_CIRCLE_RADIUS = "circle_radius"; private final static String ATTR_CENTER_PIXELMAP = "center_pixelmap"; private final static float CIRCLE_ANGLE = 360.0f; private final static int DEF_UNFILL_COLOR = 0xFF808080; private final static int DEF_FILL_COLOR = 0xFF1E90FF; public int width = 240; public int height = 240; // 圆环轨道颜色 private Color unFillColor = new Color(DEF_UNFILL_COLOR); // 圆环覆盖颜色 private Color fillColor = new Color(DEF_FILL_COLOR); // 圆环宽度 private int circleWidth = 30; // 画笔 private final Paint paint; // 个数 private int count = 10; // 当前进度 private int currentCount = 0; // 间隙值 private int splitSize = 10; // 内圆的正切方形 private final RectFloat centerRectFloat = new RectFloat(); // 中心绘制的图片 private PixelMap image = null; private int radius = 100; // 原点坐标 private Point centerPoint; // 进度改变的事件响应 private ProgressChangeListener listener; public CustomControlBar(Context context) { this(context, null); } public CustomControlBar(Context context, AttrSet attrSet) { super(context, attrSet); paint = new Paint(); initAttrSet(attrSet); setEstimateSizeListener(this); if (!isClickable()) setClickable(true); setTouchEventListener(this); addDrawTask(this); listener = null; } // 初始化属性值 private void initAttrSet(AttrSet attrSet) { if (attrSet == null) return; if (attrSet.getAttr(ATTR_UN_FILL_COLOR).isPresent()) { unFillColor = attrSet.getAttr(ATTR_UN_FILL_COLOR).get().getColorValue(); } if (attrSet.getAttr(ATTR_FILL_COLOR).isPresent()) { fillColor = attrSet.getAttr(ATTR_FILL_COLOR).get().getColorValue(); } if (attrSet.getAttr(ATTR_CIRCLE_WIDTH).isPresent()) { circleWidth = attrSet.getAttr(ATTR_CIRCLE_WIDTH).get().getDimensionValue(); } if (attrSet.getAttr(ATTR_COUNT).isPresent()) { count = attrSet.getAttr(ATTR_COUNT).get().getIntegerValue(); } if (attrSet.getAttr(ATTR_CURRENT_PROGRESS).isPresent()) { currentCount = attrSet.getAttr(ATTR_CURRENT_PROGRESS).get().getIntegerValue(); } if (attrSet.getAttr(ATTR_SPLIT_SIZE).isPresent()) { splitSize = attrSet.getAttr(ATTR_SPLIT_SIZE).get().getIntegerValue(); } if (attrSet.getAttr(ATTR_CIRCLE_RADIUS).isPresent()) { radius = attrSet.getAttr(ATTR_CIRCLE_RADIUS).get().getDimensionValue(); } if (attrSet.getAttr(ATTR_CENTER_PIXELMAP).isPresent()) { Element element = attrSet.getAttr(ATTR_CENTER_PIXELMAP).get().getElement(); if (element instanceof PixelMapElement) { image = ((PixelMapElement) element).getPixelMap(); } } } @Override public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) { int widthSpce = EstimateSpec.getMode(widthEstimateConfig); int heightSpce = EstimateSpec.getMode(heightEstimateConfig); int widthConfig = 0; switch (widthSpce) { case EstimateSpec.UNCONSTRAINT: case EstimateSpec.PRECISE: width = EstimateSpec.getSize(widthEstimateConfig); widthConfig = EstimateSpec.getSizeWithMode(width, EstimateSpec.PRECISE); break; case EstimateSpec.NOT_EXCEED: widthConfig = EstimateSpec.getSizeWithMode(width, EstimateSpec.PRECISE); break; default: break; } int heightConfig = 0; switch (heightSpce) { case EstimateSpec.UNCONSTRAINT: case EstimateSpec.PRECISE: height = EstimateSpec.getSize(heightEstimateConfig); heightConfig = EstimateSpec.getSizeWithMode(height, EstimateSpec.PRECISE); break; case EstimateSpec.NOT_EXCEED: heightConfig = EstimateSpec.getSizeWithMode(height, EstimateSpec.PRECISE); break; default: break; } System.out.println("WYT_width:" + width + " height:" + height + " width_spec:" + widthSpce + " height_spec:" + heightSpce); setEstimatedSize(widthConfig, heightConfig); 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 min = Math.min(width, height); radius = (min >> 1) - circleWidth; centerPoint = new Point(width >> 1, height >> 1); drawCount(canvas); if (image != null) { int inRadius = radius - (circleWidth >> 1); centerRectFloat.left = (float) (width / 2 - Math.sqrt(2) * inRadius); centerRectFloat.top = (float) (height / 2 - Math.sqrt(2) * inRadius); centerRectFloat.right = (float) (width / 2 + Math.sqrt(2) * inRadius); centerRectFloat.bottom = (float) (height / 2 + Math.sqrt(2) * inRadius); // 如果图片比较小,那么根据图片的尺寸放置到正中心 Size imageSize = image.getImageInfo().size; if (imageSize.width < Math.sqrt(2) * inRadius) { centerRectFloat.left = (width - imageSize.width * 1.0f) / 2; centerRectFloat.top = (height - imageSize.height * 1.0f) / 2; centerRectFloat.right = (width + imageSize.width * 1.0f) / 2; centerRectFloat.bottom = (height + imageSize.height * 1.0f) / 2; } canvas.drawPixelMapHolderRect(new PixelMapHolder(image), centerRectFloat, paint); } } private void drawCount(Canvas canvas) { float itemSize = (CIRCLE_ANGLE - count * splitSize) / count; RectFloat oval = new RectFloat(centerPoint.getPointX() - radius, centerPoint.getPointY() - radius, centerPoint.getPointX() + radius, centerPoint.getPointY() + 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: MmiPoint absPoint = touchEvent.getPointerPosition(touchEvent.getIndex()); Point point = new Point(absPoint.getX(), absPoint.getY()); System.out.println("wyt_centerPoint:" + centerPoint + " point:" + point); 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 true; } 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; } }
在xml中创建该自定义组件,并设置其属性。
<?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" xmlns:custom="http://schemas.huawei.com/res/custom" ohos:height="match_parent" ohos:width="match_parent" ohos:orientation="vertical"> <!-- 请根据实际包名和文件路径引入--> <com.huawei.harmonyosdemo.custom.CustomControlBar ohos:id="$+id:custom_control_bar" ohos:height="200vp" ohos:width="match_parent" ohos:background_element="black" ohos:top_margin="50vp" custom:center_pixelmap="$media:icon" custom:circle_radius="80vp" custom:circle_width="15vp" custom:count="10" custom:current_progress="5" custom:fill_color="#1e90ff" custom:split_size="13" custom:unfill_color="gray"/> </DirectionalLayout>