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大大減少。