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

来源  |  HarmonyOS开发者 公众平台

 

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

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

HarmonyOS上如何实现自定义控件的功能_第1张图片

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

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

Java 代码

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

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

Xml 代码

 
     
         
          
         
     
 

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

Java 代码

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

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

Java 代码

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

 

5.在xml里引用该控件

Xml 代码

 
     
 

这里有两个关键点:

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 代码

 private Component createViewElement(String elementName, AttrSet attrSet) {
         Component view = null;
         if (mFactory != null) {
             view = mFactory.onCreateView(elementName, attrSet);
         }
         if (view != null) {
             return view;
         }
         try {
             if (!elementName.contains(".")) {
                 if ("View".equals(elementName)) {
                     // View's path is different from other classes
                     view = createViewByReflection("harmonyos.agp.view." + elementName, attrSet);
                 } else if ("SurfaceProvider".equals(elementName)) {
                     // SurfaceProvider's path is different from other classes
                     view = createViewByReflection("harmonyos.agp.components.surfaceprovider." + elementName, attrSet);
                 } else {
                     view = createViewByReflection("harmonyos.agp.components." + elementName, attrSet);
                 }
             } else {
                 view = createViewByReflection(elementName, attrSet);
             }
         } catch (LayoutScatterException e) {
             HiLog.error(TAG, "Create view failed: %{public}s", e.getMessage());
         }
         return view;
     }
  
     private Component createViewByReflection(String viewName, AttrSet attrSet) {
         Constructor constructor = mViewConstructorMap.get(viewName);
  
         if (constructor == null) {
             try {
                 Class viewClass = Class.forName(viewName, false, mContext.getClassloader())
                     .asSubclass(Component.class);
                 if (viewClass == null) {
                     throw new LayoutScatterException("viewClass is null");
                 }
                 constructor = viewClass.getConstructor(Context.class, AttrSet.class);
                 constructor.setAccessible(true);
                 mViewConstructorMap.put(viewName, constructor);
             } catch (ClassNotFoundException e) {
                 throw new LayoutScatterException("Can't not find the class: " + viewName, e);
             } catch (NoSuchMethodException e) {
                 throw new LayoutScatterException("Can't not find the class constructor: " + viewName, e);
             }
         }
  
         try {
             return constructor.newInstance(mContext, attrSet);
         } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
             throw new LayoutScatterException("Can't create the view: " + viewName, e);
         }
     }

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

 

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

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

Java 代码

 /**
      * Implements a draw task.
      *
      * You can use {@link View#addDrawTask(DrawTask)} and {@link View#addDrawTask(DrawTask, int)} to add a draw
      * task in a control, and invoke the callback when the control is updated by {@link View#invalidate()}.
      *
      * @since 1.0
      */
     public interface DrawTask {
         /**
          * Indicates that the draw task is implemented between the content and background of a control.
          */
         int BETWEEN_BACKGROUND_AND_CONTENT = 1;
  
         /**
          * Indicates that the draw task is implemented between the content and foreground of a control.
          */
         int BETWEEN_CONTENT_AND_FOREGROUND = 2;
  
         /**
          * Called when a view is updated through a draw task.
          *
          * The draw task uses the attributes of the parent canvas for drawing an object,
          * such as alpha, width, and height.
          *
          * @param view Indicates the parent {@code canvas}.
          * @param canvas Indicates the canvas used for drawing in this draw task.
          * @see View#addDrawTask(DrawTask)
          * @see View#addDrawTask(DrawTask, int)
          * @see View#invalidate()
          * @since 2.0
          */
         void onDraw(View view, Canvas canvas);
     } /**
      * Adds a draw task.
      *
      * The drawing of each view includes its foreground, content, and background.You can use this method to add a
      * drawing task between the foreground and the content or between the content and the background.
      *
      * @param task  Indicates the drawing task to add.
      * @param layer Indicates the position of the drawing task. This value can only be
      *              {@link DrawTask#BETWEEN_BACKGROUND_AND_CONTENT} or {@link DrawTask#BETWEEN_CONTENT_AND_FOREGROUND}.
      */
     public void addDrawTask(DrawTask task, int layer) {
         HiLog.debug(TAG, "addDrawTask");
         switch (layer) {
             case DrawTask.BETWEEN_BACKGROUND_AND_CONTENT: {
                 mDrawTaskUnderContent = task;
                 if (mCanvasForTaskUnderContent == null) {
                     mCanvasForTaskUnderContent = new Canvas();
                 }
                 nativeAddDrawTaskUnderContent(
                     mNativeViewPtr, mDrawTaskUnderContent, mCanvasForTaskUnderContent.getNativePtr());
                 break;
             }
             case DrawTask.BETWEEN_CONTENT_AND_FOREGROUND: {
                 mDrawTaskOverContent = task;
                 if (mCanvasForTaskOverContent == null) {
                     mCanvasForTaskOverContent = new Canvas();
                 }
                 nativeAddDrawTaskOverContent(
                     mNativeViewPtr, mDrawTaskOverContent, mCanvasForTaskOverContent.getNativePtr());
                 break;
             }
             default: {
                 HiLog.error(TAG, "addDrawTask fail! Invalid number of layers.");
             }
         }
     }

 

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

一、推荐版本:  

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

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

Java代码

mComponent.addDrawTask(this);

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

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

Java代码

 mComponent.invalidate();

整个代码如下:

Java 代码

 /*
  * Copyright (c) Huawei Technologies Co., Ltd. 2020-2020. All rights reserved.
  */
  
 package com.huawei.watch.common.view;
  
 import ohos.agp.components.Component;
 import ohos.agp.render.Arc;
 import ohos.agp.render.Canvas;
 import ohos.agp.render.LinearShader;
 import ohos.agp.render.Paint;
 import ohos.agp.render.Shader;
 import ohos.agp.utils.Color;
 import ohos.agp.utils.Point;
 import ohos.agp.utils.RectFloat;
  
 /**
  * 自定义带有圆环效果的LinearLayout。通过xml配置
  * 圆环的圆心在中间,x轴水平向右,y轴水平向下,按极坐标绘制。
  *
  * @author t00545831
  * @since 2020-05-22
  */
 public class CircleProgressDrawTask implements Component.DrawTask {
     // 业务模块可以在xml里配置, 用来配置圆环的粗细, 预留, 后续可以通过xml配置
     private static final String STROKE_WIDTH_KEY = "stroke_width";
  
     // 业务模块可以在xml里配置, 用来配置圆环的最大值
     private static final String MAX_PROGRESS_KEY = "max_progress";
  
     // 业务模块可以在xml里配置, 用来配置圆环的当前值
     private static final String CURRENT_PROGRESS_KEY = "current_progress";
  
     // 业务模块可以在xml里配置, 用来配置起始位置的颜色
     private static final String START_COLOR_KEY = "start_color";
  
     // 业务模块可以在xml里配置, 用来配置结束位置的颜色
     private static final String END_COLOR_KEY = "end_color";
  
     // 业务模块可以在xml里配置, 用来配置背景色
     private static final String BACKGROUND_COLOR_KEY = "background_color";
  
     // 业务模块可以在xml里配置, 用来起始位置的角度
     private static final String START_ANGLE = "start_angle";
  
     private static final float MAX_ARC = 360f;
  
     private static final int DEFAULT_STROKE_WIDTH = 20;
  
     private static final int DEFAULT_MAX_VALUE = 100;
  
     private static final int DEFAULT_START_COLOR = 0xFFB566FF;
  
     private static final int DEFAULT_END_COLOR = 0xFF8A2BE2;
  
     private static final int DEFAULT_BACKGROUND_COLOR = 0xA8FFFFFF;
  
     private static final int DEFAULT_START_ANGLE = -90;
  
     private static final float DEFAULT_LINER_MAX = 100f;
  
     private static final int HALF = 2;
  
     private static final int NEARLY_FULL_CIRCL = 350;
  
     // 圆环的宽度, 默认20个像素
     private int mStrokeWidth = DEFAULT_STROKE_WIDTH;
  
     // 最大的进度值, 默认是100
     private int mMaxValue = DEFAULT_MAX_VALUE;
  
     // 当前的进度值, 默认是0
     private int mCurrentValue = 0;
  
     // 起始位置的颜色, 默认浅紫色
     private Color mStartColor = new Color(DEFAULT_START_COLOR);
  
     // 结束位置的颜色, 默认深紫色
     private Color mEndColor = new Color(DEFAULT_END_COLOR);
  
     // 背景颜色, 默认浅灰色
     private Color mBackgroundColor = new Color(DEFAULT_BACKGROUND_COLOR);
  
     // 当前的进度值, 默认从-90度进行绘制
     private int mStartAngle = DEFAULT_START_ANGLE;
  
     private Component mComponent;
  
     /**
      * 传入要进行修改的view
      *
      * @param component 要进行修改的view
      */
     public CircleProgressDrawTask(Component component) {
         mComponent = component;
         mComponent.addDrawTask(this);
     }
  
     /**
      * 设置当前进度并且刷新所在的view
      *
      * @param value 当前进度
      */
     public void setCurrentValue(int value) {
         mCurrentValue = value;
         mComponent.invalidate();
     }
  
     /**
      * 设置最大的进度值并且刷新所在的view
      *
      * @param maxValue 最大的进度值
      */
     public void setMaxValue(int maxValue) {
         mMaxValue = maxValue;
         mComponent.invalidate();
     }
  
     @Override
     public void onDraw(Component component, Canvas canvas) {
         // 计算中心点的位置, 如果是长方形, 则应该是较短的部分
         int center = Math.min(component.getWidth() / HALF, component.getHeight() / HALF);
  
         // 使用背景色绘制圆环, 选择一个画刷,宽度为设置的宽度,然后画圆。
         Paint roundPaint = new Paint();
         roundPaint.setAntiAlias(true);
         roundPaint.setStyle(Paint.Style.STROKE_STYLE);
         roundPaint.setStrokeWidth(mStrokeWidth);
         roundPaint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
         roundPaint.setColor(mBackgroundColor);
         int radius = center - mStrokeWidth / HALF;
         canvas.drawCircle(center, center, radius, roundPaint);
  
         // 使用渐变色绘制弧形
         Paint paint = new Paint();
         paint.setAntiAlias(true);
         paint.setStyle(Paint.Style.STROKE_STYLE);
         paint.setStrokeWidth(mStrokeWidth);
         float sweepAngle = MAX_ARC * mCurrentValue / mMaxValue;
  
         // 绘制的弧形接近满圆的时候使用BUTT画笔头
         if (sweepAngle > NEARLY_FULL_CIRCL) {
             paint.setStrokeCap(Paint.StrokeCap.BUTT_CAP);
         } else {
             paint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
         }
         Point point1 = new Point(0, 0);
         Point point2 = new Point(DEFAULT_LINER_MAX, DEFAULT_LINER_MAX);
         Point[] points = {point1, point2};
         Color[] colors = {mStartColor, mEndColor};
         Shader shader = new LinearShader(points, null, colors, Shader.TileMode.CLAMP_TILEMODE);
         paint.setShader(shader, Paint.ShaderType.LINEAR_SHADER);
         RectFloat oval = new RectFloat(center - radius, center - radius, center + radius, center + radius);
         Arc arc = new Arc();
         arc.setArc(mStartAngle, sweepAngle, false);
         canvas.drawArc(oval, arc, paint);
     }
 }

调用的地方

Java 代码

 LayoutScatter scatter = LayoutScatter.getInstance(this);
 Component component = scatter.parse(Resource.Layout.layout_sleep, null, false);
 // 为layout_sleep里的根节点添加圆环
 mDrawTask = new CircleProgressDrawTask(component);
 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

你可能感兴趣的:(鸿蒙OS百科)