1. 程式人生 > 其它 >HarmonyOS上如何實現自定義控制元件的功能

HarmonyOS上如何實現自定義控制元件的功能

技術標籤:鴻蒙OS百科

來源 | HarmonyOS開發者

LinearLayout又稱作線性佈局,是一種非常常用的佈局。正如它的名字所描述的一樣,這個佈局會將它所包含的控制元件線上性方向上依次排列。既然是線性排列,肯定就不僅只有一個方向,這裡一般只有兩個方向:水平方向和垂直方向。

但在實際開發中,為了呈現更好的視覺體驗和互動效果,往往需要在LinearLayout外有其他的佈局,比如下圖這個手錶應用中,在LinearLayout最外側有個圓環。那麼這一效果的呈現,在HarmonyOS上如何實現呢?

首先,為了便於大家理解和對比,我們回顧一下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 程式碼

 <resources>
     <declare-styleable name="CircleProgressBar">
         <attr name="progress_color" format="color"/>
         <attr name="max_value" format="integer"/> <!--對應總進度-->
         <attr name="cur_value" format="integer"/><!--指的是當前進度-->
     </declare-styleable>
 </resources>

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 程式碼

 <com.huawei.watch.healthsport.ui.view.workoutrecord.CircleProgressBar
     android:id="@+id/workout_progress"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     app:progress_color="@color/progress_color"
     app:max_value="12"
     app:cur_value="@{sleepData.targetTime}">
     <!-- 其它UI控制元件配置 -->
 </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 程式碼

 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<? extends Component> constructor = mViewConstructorMap.get(viewName);
  
         if (constructor == null) {
             try {
                 Class<? extends Component> 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