当前位置:   article > 正文

HarmonyOS上如何实现自定义控件的功能_鸿蒙自定义控件

鸿蒙自定义控件

来源  |  HarmonyOS开发者 公众平台

 

LinearLayout又称作线性布局,是一种非常常用的布局。正如它的名字所描述的一样,这个布局会将它所包含的控件在线性方向上依次排列。既然是线性排列,肯定就不仅只有一个方向,这里一般只有两个方向:水平方向和垂直方向。

但在实际开发中,为了呈现更好的视觉体验和交互效果,往往需要在LinearLayout外有其他的布局,比如下图这个手表应用中,在LinearLayout最外侧有个圆环。那么这一效果的呈现,在HarmonyOS上如何实现呢?

首先,为了便于大家理解和对比,我们回顾一下Android上的实现方式,分为几步。

1.创建一个LinearLayout的子类,如

Java 代码

  1. public class CircleProgressBar extends LinearLayout {
  2. private static final String TAG = "CircleProgressBar";
  3. private Color mProgressColor; // 自定义属性,圆环颜色
  4. private int mMaxProgress; // 自定义属性,总进度
  5. private int mProgress; // 自定义属性,当前进度

 2.为该自定义view里的自定义属性指定key值,方便在xml里配置

Xml 代码

  1. <resources>
  2. <declare-styleable name="CircleProgressBar">
  3. <attr name="progress_color" format="color"/>
  4. <attr name="max_value" format="integer"/> <!--对应总进度-->
  5. <attr name="cur_value" format="integer"/><!--指的是当前进度-->
  6. </declare-styleable>
  7. </resources>

 3.在构造函数里,解析用户的配置,对自定义属性mProgressColor, mMaxProgress, mProgress赋值

Java 代码

  1. public CircleProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
  2. super(context, attrs, defStyleAttr);
  3. TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar);
  4. mProgressColor = array.getColor(R.styleable.CircleProgressBar_progress_color, Color.RED);
  5. mMaxProgress = array.getInteger(R.styleable.CircleProgressBar_max_value, 100);
  6. mProgress = array.getInteger(R.styleable.CircleProgressBar_cur_value, 0);
  7. }

4. 实现onDraw函数,使用全局变量等进行绘制

Java 代码

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. super.onDraw(canvas);
  4. // 其余变量初始化过程略
  5. LocalLog.d(TAG, "onDraw()");
  6. canvas.drawCircle(mCenter, mCenter, mRadius, mRoundPaint); // 画圆形
  7. mOval.set(mCenter - mRadius, mCenter - mRadius, mCenter + mRadius, mCenter + mRadius);
  8. canvas.drawArc(mOval, ORIGIN_ANGLE, ARC_DEGRESS * getProgress() / MAX_PROGRESS,
  9. false, mProgressPaint); // 画弧形
  10. }

 

5.在xml里引用该控件

Xml 代码

  1. <com.huawei.watch.healthsport.ui.view.workoutrecord.CircleProgressBar
  2. android:id="@+id/workout_progress"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. app:progress_color="@color/progress_color"
  6. app:max_value="12"
  7. app:cur_value="@{sleepData.targetTime}">
  8. <!-- 其它UI控件配置 -->
  9. </com.huawei.watch.healthsport.ui.view.workoutrecord.CircleProgressBar>

这里有两个关键点:

1.android的图形子系统,在解析xml的时候,支持反射创建用户自定义的控件,并且调用自定义的控件里含有三个参数的构造方法;

2.android的图形子系统,在根节点decodeView绘制过程中,会调用每个子节点的onDraw方法,进行绘制。

 

众所周知,UI控件有很多,比如textView,ImageView,Button等,如果我们需要带有圆环效果的textView,带有圆环效果的ImageView,带有圆环效果的Button,那么在android上需要重复开发三次,实现三次onDraw方法。

 

回到到正题,我们来看看HarmonyOS上是如何实现的呢?

首先,我们看一下HarmonyOS图形子系统创建控件的代码实现。

1.从代码看,HarmonyOS图形子系统同样支持xml动态反射创建自定义控件,这点和Android是一致的。

HarmonyOS图形子系统预置的控件,也是由反射创建(如果xml节点里带,则认为是自定义控件)。

Java 代码

  1. private Component createViewElement(String elementName, AttrSet attrSet) {
  2. Component view = null;
  3. if (mFactory != null) {
  4. view = mFactory.onCreateView(elementName, attrSet);
  5. }
  6. if (view != null) {
  7. return view;
  8. }
  9. try {
  10. if (!elementName.contains(".")) {
  11. if ("View".equals(elementName)) {
  12. // View's path is different from other classes
  13. view = createViewByReflection("harmonyos.agp.view." + elementName, attrSet);
  14. } else if ("SurfaceProvider".equals(elementName)) {
  15. // SurfaceProvider's path is different from other classes
  16. view = createViewByReflection("harmonyos.agp.components.surfaceprovider." + elementName, attrSet);
  17. } else {
  18. view = createViewByReflection("harmonyos.agp.components." + elementName, attrSet);
  19. }
  20. } else {
  21. view = createViewByReflection(elementName, attrSet);
  22. }
  23. } catch (LayoutScatterException e) {
  24. HiLog.error(TAG, "Create view failed: %{public}s", e.getMessage());
  25. }
  26. return view;
  27. }
  28. private Component createViewByReflection(String viewName, AttrSet attrSet) {
  29. Constructor<? extends Component> constructor = mViewConstructorMap.get(viewName);
  30. if (constructor == null) {
  31. try {
  32. Class<? extends Component> viewClass = Class.forName(viewName, false, mContext.getClassloader())
  33. .asSubclass(Component.class);
  34. if (viewClass == null) {
  35. throw new LayoutScatterException("viewClass is null");
  36. }
  37. constructor = viewClass.getConstructor(Context.class, AttrSet.class);
  38. constructor.setAccessible(true);
  39. mViewConstructorMap.put(viewName, constructor);
  40. } catch (ClassNotFoundException e) {
  41. throw new LayoutScatterException("Can't not find the class: " + viewName, e);
  42. } catch (NoSuchMethodException e) {
  43. throw new LayoutScatterException("Can't not find the class constructor: " + viewName, e);
  44. }
  45. }
  46. try {
  47. return constructor.newInstance(mContext, attrSet);
  48. } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
  49. throw new LayoutScatterException("Can't create the view: " + viewName, e);
  50. }
  51. }

【注】HarmonyOS所有的控件基本上都是Component的子类,Android是view。

 

2.我们可以看到,鸿蒙图形子系统中移除了Component的onDraw方法,而是把onDraw方法放到了DrawTask里。

Component提供了addDrawTask方法,供自定义Component的实现。

Java 代码

  1. /**
  2. * Implements a draw task.
  3. *
  4. * You can use {@link View#addDrawTask(DrawTask)} and {@link View#addDrawTask(DrawTask, int)} to add a draw
  5. * task in a control, and invoke the callback when the control is updated by {@link View#invalidate()}.
  6. *
  7. * @since 1.0
  8. */
  9. public interface DrawTask {
  10. /**
  11. * Indicates that the draw task is implemented between the content and background of a control.
  12. */
  13. int BETWEEN_BACKGROUND_AND_CONTENT = 1;
  14. /**
  15. * Indicates that the draw task is implemented between the content and foreground of a control.
  16. */
  17. int BETWEEN_CONTENT_AND_FOREGROUND = 2;
  18. /**
  19. * Called when a view is updated through a draw task.
  20. *
  21. * The draw task uses the attributes of the parent canvas for drawing an object,
  22. * such as alpha, width, and height.
  23. *
  24. * @param view Indicates the parent {@code canvas}.
  25. * @param canvas Indicates the canvas used for drawing in this draw task.
  26. * @see View#addDrawTask(DrawTask)
  27. * @see View#addDrawTask(DrawTask, int)
  28. * @see View#invalidate()
  29. * @since 2.0
  30. */
  31. void onDraw(View view, Canvas canvas);
  32. } /**
  33. * Adds a draw task.
  34. *
  35. * The drawing of each view includes its foreground, content, and background.You can use this method to add a
  36. * drawing task between the foreground and the content or between the content and the background.
  37. *
  38. * @param task Indicates the drawing task to add.
  39. * @param layer Indicates the position of the drawing task. This value can only be
  40. * {@link DrawTask#BETWEEN_BACKGROUND_AND_CONTENT} or {@link DrawTask#BETWEEN_CONTENT_AND_FOREGROUND}.
  41. */
  42. public void addDrawTask(DrawTask task, int layer) {
  43. HiLog.debug(TAG, "addDrawTask");
  44. switch (layer) {
  45. case DrawTask.BETWEEN_BACKGROUND_AND_CONTENT: {
  46. mDrawTaskUnderContent = task;
  47. if (mCanvasForTaskUnderContent == null) {
  48. mCanvasForTaskUnderContent = new Canvas();
  49. }
  50. nativeAddDrawTaskUnderContent(
  51. mNativeViewPtr, mDrawTaskUnderContent, mCanvasForTaskUnderContent.getNativePtr());
  52. break;
  53. }
  54. case DrawTask.BETWEEN_CONTENT_AND_FOREGROUND: {
  55. mDrawTaskOverContent = task;
  56. if (mCanvasForTaskOverContent == null) {
  57. mCanvasForTaskOverContent = new Canvas();
  58. }
  59. nativeAddDrawTaskOverContent(
  60. mNativeViewPtr, mDrawTaskOverContent, mCanvasForTaskOverContent.getNativePtr());
  61. break;
  62. }
  63. default: {
  64. HiLog.error(TAG, "addDrawTask fail! Invalid number of layers.");
  65. }
  66. }
  67. }

 

由此看来,在HarmonyOS上自定义Component的实现方法如下: 

一、推荐版本:  

1.创建一个自定义DrawTask,里面包含跟业务相关的自定义属性。

2.给自定义的DrawTask绑定宿主Component,构造方法

Java代码

mComponent.addDrawTask(this);

3.实现自定义的ComponentDrawTask里的onDraw方法

4.在自定义属性的set里,加上

Java代码

 mComponent.invalidate();

整个代码如下:

Java 代码

  1. /*
  2. * Copyright (c) Huawei Technologies Co., Ltd. 2020-2020. All rights reserved.
  3. */
  4. package com.huawei.watch.common.view;
  5. import ohos.agp.components.Component;
  6. import ohos.agp.render.Arc;
  7. import ohos.agp.render.Canvas;
  8. import ohos.agp.render.LinearShader;
  9. import ohos.agp.render.Paint;
  10. import ohos.agp.render.Shader;
  11. import ohos.agp.utils.Color;
  12. import ohos.agp.utils.Point;
  13. import ohos.agp.utils.RectFloat;
  14. /**
  15. * 自定义带有圆环效果的LinearLayout。通过xml配置
  16. * 圆环的圆心在中间,x轴水平向右,y轴水平向下,按极坐标绘制。
  17. *
  18. * @author t00545831
  19. * @since 2020-05-22
  20. */
  21. public class CircleProgressDrawTask implements Component.DrawTask {
  22. // 业务模块可以在xml里配置, 用来配置圆环的粗细, 预留, 后续可以通过xml配置
  23. private static final String STROKE_WIDTH_KEY = "stroke_width";
  24. // 业务模块可以在xml里配置, 用来配置圆环的最大值
  25. private static final String MAX_PROGRESS_KEY = "max_progress";
  26. // 业务模块可以在xml里配置, 用来配置圆环的当前值
  27. private static final String CURRENT_PROGRESS_KEY = "current_progress";
  28. // 业务模块可以在xml里配置, 用来配置起始位置的颜色
  29. private static final String START_COLOR_KEY = "start_color";
  30. // 业务模块可以在xml里配置, 用来配置结束位置的颜色
  31. private static final String END_COLOR_KEY = "end_color";
  32. // 业务模块可以在xml里配置, 用来配置背景色
  33. private static final String BACKGROUND_COLOR_KEY = "background_color";
  34. // 业务模块可以在xml里配置, 用来起始位置的角度
  35. private static final String START_ANGLE = "start_angle";
  36. private static final float MAX_ARC = 360f;
  37. private static final int DEFAULT_STROKE_WIDTH = 20;
  38. private static final int DEFAULT_MAX_VALUE = 100;
  39. private static final int DEFAULT_START_COLOR = 0xFFB566FF;
  40. private static final int DEFAULT_END_COLOR = 0xFF8A2BE2;
  41. private static final int DEFAULT_BACKGROUND_COLOR = 0xA8FFFFFF;
  42. private static final int DEFAULT_START_ANGLE = -90;
  43. private static final float DEFAULT_LINER_MAX = 100f;
  44. private static final int HALF = 2;
  45. private static final int NEARLY_FULL_CIRCL = 350;
  46. // 圆环的宽度, 默认20个像素
  47. private int mStrokeWidth = DEFAULT_STROKE_WIDTH;
  48. // 最大的进度值, 默认是100
  49. private int mMaxValue = DEFAULT_MAX_VALUE;
  50. // 当前的进度值, 默认是0
  51. private int mCurrentValue = 0;
  52. // 起始位置的颜色, 默认浅紫色
  53. private Color mStartColor = new Color(DEFAULT_START_COLOR);
  54. // 结束位置的颜色, 默认深紫色
  55. private Color mEndColor = new Color(DEFAULT_END_COLOR);
  56. // 背景颜色, 默认浅灰色
  57. private Color mBackgroundColor = new Color(DEFAULT_BACKGROUND_COLOR);
  58. // 当前的进度值, 默认从-90度进行绘制
  59. private int mStartAngle = DEFAULT_START_ANGLE;
  60. private Component mComponent;
  61. /**
  62. * 传入要进行修改的view
  63. *
  64. * @param component 要进行修改的view
  65. */
  66. public CircleProgressDrawTask(Component component) {
  67. mComponent = component;
  68. mComponent.addDrawTask(this);
  69. }
  70. /**
  71. * 设置当前进度并且刷新所在的view
  72. *
  73. * @param value 当前进度
  74. */
  75. public void setCurrentValue(int value) {
  76. mCurrentValue = value;
  77. mComponent.invalidate();
  78. }
  79. /**
  80. * 设置最大的进度值并且刷新所在的view
  81. *
  82. * @param maxValue 最大的进度值
  83. */
  84. public void setMaxValue(int maxValue) {
  85. mMaxValue = maxValue;
  86. mComponent.invalidate();
  87. }
  88. @Override
  89. public void onDraw(Component component, Canvas canvas) {
  90. // 计算中心点的位置, 如果是长方形, 则应该是较短的部分
  91. int center = Math.min(component.getWidth() / HALF, component.getHeight() / HALF);
  92. // 使用背景色绘制圆环, 选择一个画刷,宽度为设置的宽度,然后画圆。
  93. Paint roundPaint = new Paint();
  94. roundPaint.setAntiAlias(true);
  95. roundPaint.setStyle(Paint.Style.STROKE_STYLE);
  96. roundPaint.setStrokeWidth(mStrokeWidth);
  97. roundPaint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
  98. roundPaint.setColor(mBackgroundColor);
  99. int radius = center - mStrokeWidth / HALF;
  100. canvas.drawCircle(center, center, radius, roundPaint);
  101. // 使用渐变色绘制弧形
  102. Paint paint = new Paint();
  103. paint.setAntiAlias(true);
  104. paint.setStyle(Paint.Style.STROKE_STYLE);
  105. paint.setStrokeWidth(mStrokeWidth);
  106. float sweepAngle = MAX_ARC * mCurrentValue / mMaxValue;
  107. // 绘制的弧形接近满圆的时候使用BUTT画笔头
  108. if (sweepAngle > NEARLY_FULL_CIRCL) {
  109. paint.setStrokeCap(Paint.StrokeCap.BUTT_CAP);
  110. } else {
  111. paint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
  112. }
  113. Point point1 = new Point(0, 0);
  114. Point point2 = new Point(DEFAULT_LINER_MAX, DEFAULT_LINER_MAX);
  115. Point[] points = {point1, point2};
  116. Color[] colors = {mStartColor, mEndColor};
  117. Shader shader = new LinearShader(points, null, colors, Shader.TileMode.CLAMP_TILEMODE);
  118. paint.setShader(shader, Paint.ShaderType.LINEAR_SHADER);
  119. RectFloat oval = new RectFloat(center - radius, center - radius, center + radius, center + radius);
  120. Arc arc = new Arc();
  121. arc.setArc(mStartAngle, sweepAngle, false);
  122. canvas.drawArc(oval, arc, paint);
  123. }
  124. }

调用的地方

Java 代码

  1. LayoutScatter scatter = LayoutScatter.getInstance(this);
  2. Component component = scatter.parse(Resource.Layout.layout_sleep, null, false);
  3. // 为layout_sleep里的根节点添加圆环
  4. mDrawTask = new CircleProgressDrawTask(component);
  5. mDrawTask.setMaxValue(MAX_SLEEP_TIME);

HarmonyOS的优点在于:可以为任何控件增加一个圆环,且仅需开发一个Drawtask, 即可让所有已知控件实现圆环效果,大大减少代码工作量。

不过,该实现方案无法通过xml文件进行配置。因为在鸿蒙图形子系统里不会通过反射去创建一个自定义的DrawTask,只能创建相应的自定义控件,在未来,HarmonyOS图形子系统将能支持xml反射DrawTask的功能。综上所述,HarmonyOS提供了用户程序框架、Ability框架以及UI框架,支持应用开发过程中多终端的业务逻辑和界面逻辑进行复用,能够实现应用的一次开发、多端部署,提升了跨设备应用的开发效率。

UI控件有很多,比如textView,ImageView,Button等,由于页面风格统一,我们通常需要页面统一带有圆环效果,那么在Android上需要重复开发多次,实现多次onDraw方法。

而HarmonyOS在框架层面将这个Draw方法抽取出来了,单独放到了Drawtask接口里,这样在HarmonyOS上仅需开发一个Drawtask, 即可让所有已知控件实现圆环效果,工作量比Android大大减少。

 


原文链接https://mp.weixin.qq.com/s/zzLIL_IdpkG2YNE7m_Qhpw

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/223689
推荐阅读
相关标签
  

闽ICP备14008679号