本文介绍了HarmonyOS Java应用的项目结构、资源文件、UI组件与布局、Page生命周期与导航、HiLog日志、ORM数据库等知识,展示了大部分UI组件的用法和页面效果。

本文是Hero系列文章之一,实现了完整的CRUD功能、支持国际化,可以运行在TV模拟器中,进行多页面交互与操作。另外,演示了Wearable Java应用开发。本文源于官方文档,但不仅仅是简单的复制,您可以先按本文快速创建应用、掌握基础知识,然后再进一步学习官方文档。

代码已上传到GitHub [Heroes-HarmonyOS],希望对初学者有所帮助,能为HarmonyOS贡献一点星星之火。代码、文档均会与DevEco Studio和SDK同步更新。

项目概览

创建项目

首先通过DevEco Studio的模板创建我们的项目,进入File -> New -> New Project,然后选择TV设备、Empty Feature Ability(Java)模板: 填写项目相关信息,创建项目。 待下载编译完成后,进入Tools -> HVD Manager,启动TV模拟器: 点击DevEco Studio工具栏中的【Run】按钮运行工程,在弹出的Select Deployment Target界面选择Connected Devices,启动后在Run日志中将输出如下信息:

12/16 17:52:13: Launching io.itrunner.heroes
$ hdc shell am force-stop io.itrunner.heroes
$ hdc shell bm uninstall io.itrunner.heroes
$ hdc file send E:/Workspace/heroes-harmony/entry/build/outputs/hap/debug/entry-debug-unsigned.hap /sdcard/4d5f647f7a4a44b79bc765a2bc8f76c6/entry-debug-unsigned.hap
$ hdc shell bm install -p /sdcard/4d5f647f7a4a44b79bc765a2bc8f76c6/
$ hdc shell rm -rf /sdcard/4d5f647f7a4a44b79bc765a2bc8f76c6
$ hdc shell am start -n "io.itrunner.heroes/io.itrunner.heroes.MainAbilityShellActivity"

从日志可以看到,启动项目后会把hap包发送到模拟器的sdcard目录进行安装。

成功启动后,模拟器将显示如下界面: 点击模拟器右侧工具栏中的圆形【Home】按钮回到主页面,按右箭头或拖动下方图标至末尾,点击【全部应用】,在打开的界面中可以找到新部署的Heroes应用,点击即可重新进入应用。

项目结构

entry下有三个目录build、libs、src,分别保存编译后的代码或包、库文件、源代码。src采用了标准的Java工程目录结构,其中config.json为应用配置文件,主要包括应用的全局配置、设备配置、模块配置,详细信息请查阅官方文档配置文件的元素。例如,如果在"module"的"deviceType"属性中增加"wearable",则可以在手表中运行应用。 resources目录存放资源文件,可以存放element、media、animation、layout、graphic、profile、rawfile等资源。编译时根据资源类型、名称、ID等自动生成ResourceTable类。注意,不能手动修改这个文件。 ResourceTable

/*
 * Copyright(c) Huawei Technologies Co., Ltd. 2019 - 2020. All rights reserved.
 * Description: This header was automatically generated by restool from the resource data it found.
 *              It provides resource index information for applications, and should not be modified by hand.
 */

package io.itrunner.heroes;

public final class ResourceTable {
    public static final int Graphic_background_ability_main = 0x1000003;

    public static final int Id_text_helloworld = 0x1000005;

    public static final int Layout_ability_main = 0x1000004;

    public static final int Media_icon = 0x1000002;

    public static final int String_app_name = 0x1000000;
    public static final int String_mainability_description = 0x1000001;
}

在Java程序中需要使用ResourceTable来引用资源:

public class MainAbilitySlice extends AbilitySlice {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_ability_main);
    }
}

源码中创建了MyApplication、MainAbility、MainAbilitySlice三个Java类,三者分别继承了AbilityPackage、Ability、AbilitySlice,它们都是AbilityContext的子类。AbilityPackage是hap(模块)初始化的入口;一个Ability(这里指Page Ability,Feature Ability唯一支持的类型)即一个Page;AbilitySlice主要用于承载Ability的具体逻辑实现和界面UI,是应用显示、运行和跳转的最小单元。 预览器 当前,DevEco已支持手机(Phone)、平板(Tablet)、车机(Car)、智慧屏(TV)和智能穿戴(Wearable)的Java应用预览器功能,可以在开发过程中查看应用效果。Java应用支持JavaUI和XML两种布局方式,其中JavaUI布局(AbilitySlice.java或Ability.java文件)可以动态预览应用的交互效果,如点击、跳转、滑动等互动式操作,修改了布局代码后,点击预览器窗口中的刷新按钮,可以查看修改后的布局效果;XML布局文件可以实时预览,修改和保存了XML代码后,预览器会实时展示应用的布局效果。

资源文件

Resources目录

resources目录包括两大类,一类为base与限定词目录,另一类为rawfile目录。

resources
|---base  // 默认存在的目录
|   |---element
|   |   |---string.json
|   |---media
|   |   |---icon.png
|---en_GB-vertical-car-mdpi // 限定词目录示例,需要开发者自行创建   
|   |---element
|   |   |---string.json
|   |---media
|   |   |---icon.png
|---rawfile  // 默认存在的目录

base与rawfile目录是默认存在的目录,限定词目录需要开发者自行创建。

base与限定词目录 base与限定词目录均按照两级目录形式来组织,用来存放特定类型的资源文件。编译时,目录中的资源文件会被编译成二进制文件,并赋予资源文件ID。两者均可以创建以下资源组目录:

资源组目录 目录说明 资源文件
element 元素资源,采用JSON文件格式,支持boolean(布尔型)、color(颜色)、float(浮点型)、intarray(整型数组)、integer(整型)、pattern(样式)、plural(复数形式)、strarray(字符串数组)、string(字符串) 每个文件中只能包含同一类型的数据,建议使用与下面一致的文件名称:boolean.json、color.json、float.json、intarray.json、integer.json、pattern.json、plural.json、strarray.json、string.json
media 媒体资源,包括图片、音频、视频等非文本格式的文件 可自定义文件名
animation 动画资源,采用XML文件格式 可自定义文件名
layout 布局资源,采用XML文件格式 可自定义文件名
graphic 可绘制资源,采用XML文件格式 可自定义文件名
profile 其他类型文件,以原始文件形式保存 可自定义文件名

rawfile目录 rawfile目录支持创建多层子目录,目录名称可以自定义,其内可以自由放置各类资源文件。编译时,目录中的资源文件会被直接打包,不会赋予资源文件ID。

限定词目录

目录名称由一个或多个表征应用场景或设备特征的限定词组合而成,包括语言、文字、国家或地区、横竖屏、设备类型和屏幕密度六个维度。根据不同的应用场景或设备特征加载不同的资源文件,以此实现不同设备不同配置、不同屏幕不同布局、国际化等。当resources目录中没有匹配的限定词目录时,会自动引用base目录中的资源文件。 限定词目录的命名规则

  • 限定词组合顺序:语言_文字_国家或地区-横竖屏-设备类型-屏幕密度。可以根据应用的使用场景和设备特征,选择其中的一类或几类限定词组成目录名称。
  • 限定词连接方式:语言、文字、国家或地区之间采用下划线连接,其他限定词之间均采用中划线连接。例如:zh_Hant_CN、zh_CN-car-ldpi。

限定词取值范围 横竖屏: vertical(竖屏)、horizontal(横屏) 设备类型:phone(手机)、tablet(平板)、tv(智慧屏)、car(车机)、wearable(智能穿戴)、liteWearable(轻量级智能穿戴)等 屏幕密度:

  • sdpi:小规模的屏幕密度(Small-scale Dots Per Inch),适用于120dpi及以下的设备。
  • mdpi:中规模的屏幕密度(Medium-scale Dots Per Inch),适用于120dpi~160dpi的设备。
  • ldpi:大规模的屏幕密度(Large-scale Dots Per Inch),适用于160dpi~240dpi的设备。
  • xldpi:特大规模的屏幕密度(Extra Large-scale Dots Per Inch),适用于240dpi~320dpi的设备。
  • xxldpi:超大规模的屏幕密度(Extra Extra Large-scale Dots Per Inch),适用于320dpi~480dpi的设备。
  • xxxldpi:超特大规模的屏幕密度(Extra Extra Extra Large-scale Dots Per Inch),适用于480dpi~640dpi的设备。

Element资源

element资源均采用JSON文件表示,其中“name”和“value”属性是必需的,注释属性"comment"是可选的。

boolean.json、color.json、float.json、integer.json、string.json文件格式是一致的。

boolean.json示例

{
  "boolean": [
    {
      "name": "show_title_bar",
      "value": true
    }
  ]
}

color.json示例

{
  "color": [
    {
      "name": "red",
      "value": "#ff0000"
    },
    {
      "name": "green",
      "value": "#00ff00"
    }
  ]
}

string.json示例

{
  "string": [
    {
      "name": "app_name",
      "value": "Heroes"
    },
    {
      "name": "mainability_description",
      "value": "Java_TV_Empty Feature Ability"
    }
  ]
}

strarray.json和intarray.json同为数组,但采用了不同的格式。

strarray.json示例

{
  "strarray": [
    {
      "name": "heroes",
      "value": [
        {
          "value": "Dr Nice"
        },
        {
          "value": "Narco"
        },
        {
          "value": "Bombasto"
        },
        {
          "value": "Celeritas"
        },
        {
          "value": "Magneta"
        },
        {
          "value": "RubberMan"
        },
        {
          "value": "Dynama"
        },
        {
          "value": "Dr IQ"
        },
        {
          "value": "Magma"
        },
        {
          "value": "Tornado"
        }
      ]
    }
  ]
}

intarray.json示例

{
  "intarray": [
    {
      "name": "page_size_options",
      "value": [
        5,
        10,
        20,
        50
      ]
    }
  ]
}

pattern.json示例

{
  "pattern": [
    {
      "name": "base",
      "value": [
        {
          "name": "width",
          "value": "200vp"
        },
        {
          "name": "height",
          "value": "100vp"
        },
        {
          "name": "size",
          "value": "25px"
        }
      ]
    },
    {
      "name": "child",
      "parent": "base",
      "value": [
        {
          "name": "noTitle",
          "value": "Yes"
        }
      ]
    }
  ]
}

其中"child"继承了"base"的所有属性。

plural.json示例

{
  "plural": [
    {
      "name": "hero",
      "value": [
        {
          "quantity": "zero",
          "value": "no hero"
        },
        {
          "quantity": "one",
          "value": "one hero"
        },
        {
          "quantity": "other",
          "value": "%d heroes"
        }
      ]
    }
  ]
}

参数化 在string、plural类型的资源中,支持参数化,即value中可以使用%s、%d等参数,例如:

{
  "string": [
    {
      "name": "components_title",
      "value": "Travel dream A thousand miles Meeting in %s",
      "comment": "the title of the components page"
    }
  ]
}

系统资源

目前支持的系统资源:

系统资源名称 含义 类型
ic_app 应用的默认图标 media
request_location_reminder_title “请求使用设备定位功能”的提示标题 string
request_location_reminder_content “请求使用设备定位功能”的提示内容 string

引用资源

引用base与限定词目录中的资源

  • Java中引用资源文件的格式:ResourceTable.type_name。特别地,如果引用系统资源,则采用:ohos.global.systemres.ResourceTable.type_name。

示例一,调用AbilityContext的getResourceManager()方法,获得ResourceManager,用它来引用element资源:

ResourceManager resources = getResourceManager();

// 获取系统资源
String reminderContent = resources.getElement(ohos.global.systemres.ResourceTable.String_request_location_reminder_content).getString();

// 获取string.json中的“components_title”并传递参数
String componentsTitle = resources.getElement(ResourceTable.String_components_title).getString("HarmonyOS");

// 获取strarray.json中的"heroes"
String[] heroes = resources.getElement(ResourceTable.Strarray_heroes).getStringArray();

// 获取intarray.json中的"page_size_options"
int[] pageSize = resources.getElement(ResourceTable.Intarray_page_size_options).getIntArray();

// 获取color.json中的"red"
int color = resources.getElement(ResourceTable.Color_red).getColor();

// 获取pattern.json中的"base"和"child"
Pattern base = resources.getElement(ResourceTable.Pattern_base).getPattern();
Pattern child = resources.getElement(ResourceTable.Pattern_child).getPattern();

示例二,Text组件的setText()方法可以直接引用string资源:

Text text = (Text) findComponentById(ResourceTable.Id_text_app_name);
text.setText(ResourceTable.String_app_name);

示例三,引用graphic资源创建ShapeElement:

ShapeElement background = new ShapeElement(getContext(), ResourceTable.Graphic_background_ability_main);

示例四,引用graphic资源创建FrameAnimationElement来创建帧动画:

FrameAnimationElement frameAnimationElement = new FrameAnimationElement(this, ResourceTable.Graphic_animation_pandas);
Component animation = findComponentById(ResourceTable.Id_animation_pandas);
animation.setBackground(frameAnimationElement);
frameAnimationElement.start();

示例五,引用media资源创建Image:

Image image = new Image(getContext());
image.setPixelMap(ResourceTable.Media_icon);

示例六,调用LayoutScatter的parse()方法从XML布局创建组件:

Component component = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_list_item, null, false);
  • XML和json文件中引用资源的格式:$type:name。特别地,如果引用系统资源,则采用:$ohos:type:name。

示例一,在XML文件中引用资源:

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_parent"
    ohos:width="match_parent"
    ohos:alignment="center"
    ohos:orientation="vertical">

    <Text
        ohos:id="$+id:text_app_name"
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:background_element="$graphic:background_ability_main"
        ohos:text="$string:app_name"
        ohos:text_color="$color:black"
        ohos:text_size="50"/>

    <Image
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:image_src="$media:icon"
        ohos:top_margin="20vp"/>
</DirectionalLayout>

示例二,在string.json中引用资源:

{
  "string": [
    {
      "name": "app_name",
      "value": "Heroes"
    },
    {
      "name": "app_sys_ref",
      "value": "$ohos:string:request_location_reminder_title"
    }
  ]
}

示例三,在config.json中引用资源:

"icon": "$media:icon",
"description": "$string:mainability_description",

引用rawfile目录中的资源 在Java中,引用路径为“resources/rawfile/example.js”的资源文件,如下:

RawFileEntry rawFileEntry = getResourceManager().getRawFileEntry("resources/rawfile/example.js");

Java UI

组件和布局

根据组件的功能,可以将组件分为布局类、显示类、交互类三类:

组件类别 组件名称 功能描述
布局类 PositionLayout、DirectionalLayout、StackLayout、DependentLayout、TableLayout、AdaptiveBoxLayout 提供了不同布局规范的组件容器。例如,按水平或者垂直方向排列的DirectionalLayout、按相对位置排列的DependentLayout、按确切位置排列的PositionLayout、按层叠方式排列的StackLayout等。
显示类 Text、Image、Clock、TickTimer、ProgressBar 提供内容显示功能
交互类 TextField、Button、Checkbox、RadioButton/RadioContainer、Switch、ToggleButton、Slider、ScrollView、TabList、ListContainer、PageSlider、PageFlipper、PageSliderIndicator、Picker、TimePicker、DatePicker、SurfaceProvider、ComponentProvider 提供交互响应功能

组件在未被添加到布局中时,既无法显示也无法交互,因此一个用户界面至少包含一个布局。

在Java UI框架中,提供了代码中创建布局和XML中声明布局两种开发方式。

代码中创建布局 在代码中创建Component和ComponentContainer对象,为这些对象设置布局参数和属性值,并将Component添加到ComponentContainer中。

public class MainAbilitySlice extends AbilitySlice {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);

        DirectionalLayout myLayout = new DirectionalLayout(this);
        DirectionalLayout.LayoutConfig config = new DirectionalLayout.LayoutConfig(DirectionalLayout.LayoutConfig.MATCH_PARENT, DirectionalLayout.LayoutConfig.MATCH_PARENT);
        myLayout.setLayoutConfig(config);
        ShapeElement element = new ShapeElement();
        element.setRgbColor(new RgbColor(255, 255, 255));
        myLayout.setBackground(element);

        Text text = new Text(this);
        text.setLayoutConfig(config);
        text.setText("Hello World");
        text.setTextColor(new Color(0xFF000000));
        text.setTextSize(50);
        text.setTextAlignment(TextAlignment.CENTER);
        myLayout.addComponent(text);

        setUIContent(myLayout);
    }
}

XML中声明布局 按层级结构描述Component和ComponentContainer的关系,给组件节点设定布局参数和属性值,代码中可直接加载生成此布局。

在layout目录上点击右键,在弹出的菜单中选择New > Layout Resource File,可以新建布局文件: Hello World布局示例:

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:id="$+id:root"
    ohos:height="match_parent"
    ohos:width="match_parent"
    ohos:alignment="center"
    ohos:orientation="vertical">

    <Text
        ohos:id="$+id:text_hello_world"
        ohos:height="50vp"
        ohos:width="200vp"
        ohos:text="Hello World"
        ohos:text_alignment="center"
        ohos:text_size="30fp"/>
</DirectionalLayout>

在Java代码中加载、调整布局,绑定Listener:

public class MainAbilitySlice extends AbilitySlice {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_ability_main);

        Text text = (Text) findComponentById(ResourceTable.Id_text_hello_world);
        // 调整Text样式
        text.setTextColor(Color.RED);
        text.setFont(Font.DEFAULT_BOLD);
        // 绑定ClickedListener
        text.setClickedListener(component -> text.setText("Hello HarmonyOS"));

        // 添加Image组件
        DirectionalLayout root = (DirectionalLayout) findComponentById(ResourceTable.Id_root);
        Image image = new Image(this);
        image.setPixelMap(ResourceTable.Media_icon);
        image.setMarginTop(20);
        root.addComponent(image);
    }
}

两种布局方式没有本质差别,在XML中声明布局,在代码中加载后可对该布局进行修改。XML布局更直观、简洁,是更常用的方式。

隐藏Title Bar 默认,模拟器顶部会显示Title Bar,在confi.json的"module"内添加以下配置,可以隐藏Title Bar:

"metaData": {
  "customizeData": [
    {
      "name": "hwc-theme",
      "value": "androidhwext:style/Theme.Emui.NoTitleBar",
      "extra": ""
    }
  ]
},

官方文档已提供较为完整的组件示例代码,您也可以下载本文源码了解组件的基本用法,这里不再一一展示。

Shape Element

默认,大多数组件是没有边框、圆角等背景样式的,需要自已使用Shape Element定义。

同布局一样,可以在代码中创建Shape Element,也可以在XML文件中声明。

代码中创建Shape Element 示例,为Text设置渐变背景,为Image设置弧形背景

// 设置Text背景
ShapeElement textBackground = new ShapeElement();
textBackground.setShape(ShapeElement.RECTANGLE);
textBackground.setCornerRadius(10);
textBackground.setGradientOrientation(ShapeElement.Orientation.LEFT_TO_RIGHT);
textBackground.setRgbColors(new RgbColor[]{RgbPalette.RED, RgbPalette.YELLOW});
Text text = (Text) findComponentById(ResourceTable.Id_text_hello_world);
text.setBackground(textBackground);

// 创建Image
Image image = new Image(this);
image.setPixelMap(ResourceTable.Media_icon);
image.setMarginTop(20);
image.setWidth(200);
image.setHeight(200);
image.setScaleMode(Image.ScaleMode.INSIDE);

// 设置Image背景
ShapeElement imageBackground = new ShapeElement();
imageBackground.setShape(ShapeElement.ARC);
imageBackground.setArc(new Arc(0, 180, false));
imageBackground.setStroke(5, RgbPalette.GREEN);
image.setBackground(imageBackground);

// 添加Image到布局中
DirectionalLayout root = (DirectionalLayout) findComponentById(ResourceTable.Id_root);
root.addComponent(image);

效果如下: XML中声明Shape Element 在XML布局中,简单的背景色、背景图片可以直接在background_element属性中设置,比如:

ohos:background_element="#607D8B"
ohos:background_element="green"
ohos:background_element="$color:blue"
ohos:background_element="$media:hero"

复杂背景则需在graphic目录下定义shape element。以下是一些例子:

渐变圆角矩形【gradient_element.xml】:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:shape="rectangle">
    <corners ohos:radius="10"/>
    <gradient ohos:orientation="LEFT_TO_RIGHT"/>
    <solid ohos:colors="red,yellow"/>
</shape>

灰色圆角矩形【gray_element.xml】

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:shape="rectangle">
    <corners ohos:radius="10"/>
    <solid ohos:color="gray"/>
</shape>

蓝色椭圆【blue_oval_button_element.xml】

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:shape="oval">
    <solid ohos:color="#FF007DFF"/>
</shape>

绿色圆环【green_ring_button_element.xml】

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:shape="oval">
    <stroke ohos:width="5" ohos:color="#ff008B00"/>
    <solid ohos:color="#ffeeeeee"/>
</shape>

黑色圆角边框【black_border_element.xml】

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:shape="rectangle">
    <corners ohos:radius="10"/>
    <stroke ohos:width="2" ohos:color="black"/>
</shape>

在XML布局中引用Shape Element,如下:

ohos:background_element="$graphic:gradient_element"

Vector Element

目前HarmonyOS不支持SVG格式图片,需要将其转为为XML格式的文件,即Vector Element格式。转换方法如下:

选中应用模块,点击鼠标右键,选择New>Svg To Xml: 在弹出的窗口中选择需要转换的svg文件,并命名,点击OK即可: 示例 设置Rating组件样式

// 设置Rating样式
// 全填充样式
VectorElement filledElement = new VectorElement(this, ResourceTable.Graphic_star_full);
// 半填充样式
VectorElement halfFilledElement = new VectorElement(this, ResourceTable.Graphic_star_half);
// 未填充样式
VectorElement unfilledElement = new VectorElement(this, ResourceTable.Graphic_star_gray);
// 设置样式
Rating rating = (Rating) findComponentById(ResourceTable.Id_rating);
rating.setFilledElement(filledElement);
rating.setHalfFilledElement(halfFilledElement);
rating.setUnfilledElement(unfilledElement);
// 设置步长
rating.setGrainSize(0.5f);
// 设置星级
rating.setScore(3.5f);

XML声明如下:

<Rating
    ohos:id="$+id:rating"
    ohos:height="50vp"
    ohos:width="250vp"
    ohos:left_margin="20vp"
    ohos:rating_items="5"/>

效果如下:

State Element

Checkbox、RadioButton、Switch、ToggleButton等组件是有选中或切换状态的,当状态发生变化时,其图标或背景随着变化。StateElement是容纳不同状态Element的容器。

ComponentState类定义了以下的状态:

  • COMPONENT_STATE_CHECKED 选中状态
  • COMPONENT_STATE_DISABLED 禁用状态
  • COMPONENT_STATE_EMPTY 空状态
  • COMPONENT_STATE_FOCUSED 聚焦状态
  • COMPONENT_STATE_HOVERED 悬停状态
  • COMPONENT_STATE_PRESSED 按下状态
  • COMPONENT_STATE_SELECTED 选中状态

构建StateElement的方法如下:

private StateElement stateElement(Element on, Element off) {
    StateElement stateElement = new StateElement();
    stateElement.addState(new int[]{ComponentState.COMPONENT_STATE_CHECKED}, on);
    stateElement.addState(new int[]{ComponentState.COMPONENT_STATE_EMPTY}, off);
    return stateElement;
}

示例一 设置RadioButton不同状态的样式

// 设置单选钮样式
// 选中样式
ShapeElement radioOn = new ShapeElement();
radioOn.setShape(ShapeElement.OVAL);
radioOn.setRgbColor(RgbPalette.BLUE);
// 未选样式
ShapeElement radioOff = new ShapeElement();
radioOff.setShape(ShapeElement.OVAL);
radioOff.setStroke(3, RgbPalette.BLUE);
// 设置样式
RadioButton radioButton1 = (RadioButton) findComponentById(ResourceTable.Id_radio_button_1);
RadioButton radioButton2 = (RadioButton) findComponentById(ResourceTable.Id_radio_button_2);
radioButton1.setButtonElement(stateElement(radioOn, radioOff));
radioButton2.setButtonElement(stateElement(radioOn, radioOff));
radioButton2.setChecked(true);

XML声明如下:

<RadioContainer
    ohos:height="match_content"
    ohos:width="match_content"
    ohos:left_margin="20vp"
    ohos:orientation="horizontal">

    <RadioButton
        ohos:id="$+id:radio_button_1"
        ohos:height="50vp"
        ohos:width="match_content"
        ohos:text="Radio Button 1"
        ohos:text_size="20fp"/>

    <RadioButton
        ohos:id="$+id:radio_button_2"
        ohos:height="50vp"
        ohos:width="match_content"
        ohos:left_margin="20vp"
        ohos:text="Radio Button 2"
        ohos:text_size="20fp"/>
</RadioContainer>

效果如下: 示例二 设置Switch不同状态的样式

// 设置Switch的样式
// 开启状态下滑块的样式
ShapeElement elementThumbOn = new ShapeElement();
elementThumbOn.setShape(ShapeElement.OVAL);
elementThumbOn.setRgbColor(RgbColor.fromArgbInt(0xFFFF90FF));
// 关闭状态下滑块的样式
ShapeElement elementThumbOff = new ShapeElement();
elementThumbOff.setShape(ShapeElement.OVAL);
elementThumbOff.setRgbColor(RgbColor.fromArgbInt(0xFF0000FF));
// 开启状态下轨迹样式
ShapeElement elementTrackOn = new ShapeElement();
elementTrackOn.setShape(ShapeElement.RECTANGLE);
elementTrackOn.setRgbColor(RgbColor.fromArgbInt(0xFF87CEFA));
elementTrackOn.setCornerRadius(50);
// 关闭状态下轨迹样式
ShapeElement elementTrackOff = new ShapeElement();
elementTrackOff.setShape(ShapeElement.RECTANGLE);
elementTrackOff.setRgbColor(RgbColor.fromArgbInt(0xFF808080));
elementTrackOff.setCornerRadius(50);
// 设置样式
Switch btnSwitch = (Switch) findComponentById(ResourceTable.Id_btn_switch);
btnSwitch.setTrackElement(stateElement(elementTrackOn, elementTrackOff));
btnSwitch.setThumbElement(stateElement(elementThumbOn, elementThumbOff));

效果如下:

HiLog日志

HarmonyOS Java SDK提供了HiLog来记录日志。DevEco专门提供了HiLog日志查看窗口。

HiLog

使用HiLog输出日志前需要定义HiLogLabel:

private static final HiLogLabel LOG_LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00101, "Hero Database");

HiLogLabel有三个参数type、domain和tag,意义和用法如下:

  • type 日志类型,其值为HiLog.LOG_APP,表明为应用日志。
  • domain service domain,十六进制整型,范围从0x0 到 0xFFFFF,建议使用0xAAABB格式,前三位代表子系统,后两位代表模块。
  • tag 字符串常量,表明调用方法的类或服务。

HiLog支持DEBUG、INFO、WARN、ERROR、FATAL等日志级别,支持参数化输出:

HiLog.info(LOG_LABEL, "create database: %{public}s", DATABASE_NAME_ALIAS);

参数支持隐私标识符 {public} 和 {private},当未提供时则为{private},日志输出时则显示为<private>。

11-11 15:46:39.545 9579-9579/io.itrunner.heroes I 00101/Hero Database: create database: <private>

查看HiLog

DevEco的HiLog窗口支持根据设备、包、日志级别显示日志,还可以输入domain、tag等关键字查询日志。

Page Ability

HarmonyOS Ability可以分为FA(Feature Ability)和PA(Particle Ability)两种类型。Page Ability是FA的唯一类型,提供与用户交互的能力。PA支持Service Ability和Data Ability,Service Ability用于运行后台任务,Data Ability用于提供统一的数据访问抽象。

每个Ability都必须在config.json中注册,例如:

"module": {
  ...
  "abilities": [
    {
      "skills": [
        {
          "entities": [
            "entity.system.home"
          ],
          "actions": [
            "action.system.home"
          ]
        }
      ],
      "orientation": "landscape",
      "formEnabled": false,
      "name": "io.itrunner.heroes.MainAbility",
      "icon": "$media:icon",
      "description": "$string:mainability_description",
      "label": "Heroes",
      "type": "page",
      "launchType": "standard"
    }
  ]
}

必须设定ability的type属性,可选值有page、service、data。通过DevEco菜单新建Ability会自动在config.json注册。

本文只涉及Page Ability。

Page与AbilitySlice

一个Page可以由一个或多个AbilitySlice构成(最多1024个,超出将crash),这些AbilitySlice页面提供的业务能力应具有高度相关性。 在Ability中必须重写onStart()方法,在其中调用setMainRoute()指定默认的AbilitySlice。

public class MainAbility extends Ability {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setMainRoute(MainAbilitySlice.class.getName());
    }
}

AbilitySlice承载具体的页面,必须重写onStart()方法,在其中调用setUIContent()设置页面,例如:

public class MainAbilitySlice extends AbilitySlice {

    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_ability_main);
    }

}

setUIContent()方法支持ComponentContainer和layoutResourceID(整型)两种参数类型,即可以使用Java代码创建布局,也可以引用XML布局。

也可以不使用AbilitySlice,而直接使用Ability来创建页面(一般不采用这种方式),这时在Ability中调用setUIContent()方法即可,例如:

public class MainAbility extends Ability {

    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_ability_main);
    }
		
}		

生命周期

Page生命周期 Page的四种生命周期状态:

  • INITIAL Page加载到内存,但尚未运行。
  • INACTIVE Page变为ACTIVE或BACKGROUND前的中间状态,UI可能可见,但不接受输入事件。
  • ACTIVE Page可见可交互。
  • BACKGROUND Page不可见,如果系统内存不足,此状态的Page首先被销毁。

下图展示了page的完整生命周期: 重写生命周期回调方法时,必须先调用相应的父类方法。

  • onStart() 当系统创建Page实例时,触发该回调。对于一个Page实例,该回调在其生命周期过程中仅触发一次。
  • onActive() 通常需要成对实现onActive()和onInactive(),并在onActive()中获取在onInactive()中被释放的资源。
  • onInactive() 在此回调中实现Page失去焦点时应表现的行为。
  • onBackground() 应在此回调中释放Page不可见时无用的资源,或在此回调中执行较为耗时的状态保存操作。
  • onForeground() 处于BACKGROUND状态的Page仍然驻留在内存中,当重新回到前台时(比如用户重新导航到此Page),系统将调用onForeground()。应在此回调中重新申请在onBackground()中释放的资源。
  • onStop() 系统要销毁Page时触发此回调函数,通知用户释放系统资源。销毁Page的可能原因包括以下几个方面:
  1. 用户通过系统管理能力关闭Page,例如使用任务管理器关闭Page。
  2. 用户行为触发Page的terminateAbility()方法调用,例如使用应用的退出功能。
  3. 配置变更导致系统暂时销毁Page并重建。
  4. 系统出于资源管理目的,自动触发对处于BACKGROUND状态Page的销毁。

AbilitySlice生命周期 AbilitySlice作为Page的组成单元,其生命周期是依托于所属Page生命周期的。AbilitySlice和Page具有相同的生命周期状态和同名的回调,当Page生命周期发生变化时,它的AbilitySlice也会发生相同的生命周期变化。此外,AbilitySlice还具有独立于Page的生命周期变化,这发生在同一Page中的AbilitySlice之间导航时,此时Page的生命周期状态不会改变。

AbilitySlice实例创建和管理通常由应用负责,系统仅在特定情况下会创建AbilitySlice实例。例如,通过Ability的onStart()方法配置的路由导航到某个AbilitySlice时,但是在同一个Page中不同的AbilitySlice间导航时则由应用负责实例化。

Intent

Intent是对象之间传递信息的载体。例如,当一个Ability启动另一个Ability时,或者一个AbilitySlice导航到另一个AbilitySlice时,可以通过Intent指定启动的目标同时携带相关数据。Intent的构成元素包括Operation与Parameters,Parameters支持自定义参数,Operation支持以下参数:

属性 描述
AbilityName 待启动的Ability名称。如果同时指定了BundleName和AbilityName,则Intent可以直接匹配到指定的Ability。
Action 表示动作,通常使用系统预置Action,应用也可以自定义Action。例如Intent.ACTION_HOME表示返回桌面动作。
BundleName 包名称,如果同时指定了BundleName和AbilityName,则Intent可以直接匹配到指定的Ability。
DeviceId 指定设备ID,空串表示当前设备。
Entity 表示类别,通常使用系统预置Entity,应用也可以自定义Entity。例如Intent.ENTITY_HOME表示在桌面显示图标。
Flags 表示处理Intent的方式。例如Intent.FLAG_ABILITY_CONTINUATION标记在本地的一个Ability是否可以迁移到远端设备继续运行。
Uri 如果指定了Uri,则Intent将匹配指定的Uri信息,包括scheme, schemeSpecificPart, authority和path信息。

Intent的用法请见接下来的Page和AbilitySlice导航章节。

Page导航

通过Intent指定目标Ability参数,然后调用startAbility()方法启动新的Ability来实现Page间导航。

根据Ability的全称导航 我们新建一个HeroesAbility,选择工程的entry目录,点击鼠标右键,在弹出菜单中依次选择New > Ability > Empty Feature Ability(Java),填写Ability相关信息,创建Ability。然后为MainAbilitySlice的文本添加导航到HeroesAbility的ClickedListener,如下:

text.setClickedListener(component -> {
    Intent toHeroes = new Intent();

    // 通过Intent中的OperationBuilder类构造operation对象,指定设备标识(空串表示当前设备)、应用包名、Ability名称
    Operation operation = new Intent.OperationBuilder()
            .withDeviceId("")
            .withBundleName("io.itrunner.heroes")
            .withAbilityName("io.itrunner.heroes.HeroesAbility")
            .build();

    toHeroes.setOperation(operation);
    startAbility(toHeroes);
});

您可以为MainAbility和HeroesAbility的生命周期回调方法添加日志,查看状态变化。下面是应用启动后,依次点击 MainAbility文本 > 模拟器Back按钮 > 模拟器Home按钮的情况:

11-12 13:07:13.057 4689-4689/? I 00101/MainAbility: onStart
11-12 13:07:13.104 4689-4689/? I 00101/MainAbility: onActive
11-12 13:07:34.063 4689-4689/io.itrunner.heroes I 00101/MainAbility: onInactive
11-12 13:07:34.118 4689-4689/io.itrunner.heroes I 00101/HeroesAbility: onStart
11-12 13:07:34.140 4689-4689/io.itrunner.heroes I 00101/HeroesAbility: onActive
11-12 13:07:35.341 4689-4689/io.itrunner.heroes I 00101/MainAbility: onBackground
11-12 13:07:45.361 4689-4689/io.itrunner.heroes I 00101/HeroesAbility: onInactive
11-12 13:07:45.371 4689-4689/io.itrunner.heroes I 00101/MainAbility: onForeground
11-12 13:07:45.374 4689-4689/io.itrunner.heroes I 00101/MainAbility: onActive
11-12 13:07:46.494 4689-4689/io.itrunner.heroes I 00101/HeroesAbility: onBackground
11-12 13:07:46.496 4689-4689/io.itrunner.heroes I 00101/HeroesAbility: onStop
11-12 13:07:48.779 4689-4689/io.itrunner.heroes I 00101/MainAbility: onInactive

Back 我们注意到当点击模拟器Back按钮时,HeroesAbility被销毁。那自己怎样实现同样的back操作呢?仅需调用terminateAbility()方法。将HeroesAbilitySlice的文本内容改为“Back”,然后为其添加ClickedListener,如下:

text.setClickedListener(component -> terminateAbility());

Action导航 下面我们使用Action属性导航到HeroesAbility。首先在config.json的HeroesAbility中增加action配置,声明对外提供的能力,如下:

{
  "skills": [
    {
      "actions": [
        "action.hero.heroes"
      ]
    }
  ],
  "orientation": "landscape",
  "formEnabled": false,
  "name": "io.itrunner.heroes.HeroesAbility",
  "icon": "$media:icon",
  "description": "$string:heroesability_description",
  "label": "entry",
  "type": "page",
  "launchType": "standard"
}

修改MainAbilitySlice的导航事件,如下:

text.setClickedListener(component -> {
    Intent toHeroes = new Intent();
    Operation operation = new Intent.OperationBuilder()
            .withAction("action.hero.heroes")
            .build();
    toHeroes.setOperation(operation);
    startAbility(toHeroes);
});

导航到指定AbilitySlice并返回结果 在上面的导航中,实际上是导航到Page的默认AbilitySlice页面了。如果Page有多个AbilitySlice页面,如何导航到指定AbilitySlice呢?除要在config.json中注册action外,还要在Ability中添加ActionRoute。

  • 目标Ability

为了演示,我们复制HeroesAbilitySlice,将其重命名为HeroDetailsAbilitySlice,然后在config.json中添加action:

...
"skills": [
  {
    "actions": [
      "action.hero.heroes",
      "action.hero.details"
    ]
  }
],
...

在HeroesAbility中添加ActionRoute:

@Override
public void onStart(Intent intent) {
    HiLog.info(LOG_LABEL, "onStart");
    super.onStart(intent);
    super.setMainRoute(HeroesAbilitySlice.class.getName());

    addActionRoute("action.hero.details", HeroDetailsAbilitySlice.class.getName());
}

为了返回结果,需要在Ability内调用setResult()方法:

@Override
protected void onActive() {
    HiLog.info(LOG_LABEL, "onActive");
    super.onActive();

    Intent resultIntent = new Intent();
    resultIntent.setParam("name", "Jason");
    setResult(0, resultIntent); 
}
  • 请求Ability

若要从目标Ability返回时,能够获得其返回结果,应使用startAbilityForResult()方法发起请求,修改MainAbilitySlice的导航事件,如下:

text.setClickedListener(component -> {
    Intent toHeroDetails = new Intent();
    Operation operation = new Intent.OperationBuilder()
            .withAction("action.hero.details")
            .build();
    toHeroDetails.setOperation(operation);
    startAbilityForResult(toHeroDetails, 0);
});

然后重写onAbilityResult()方法,对请求结果进行处理:

@Override
protected void onAbilityResult(int requestCode, int resultCode, Intent resultData) {
    switch (requestCode) {
        case 0:
            text.setText("Hello " + resultData.getStringParam("name"));
            return;
    }
}

再次运行应用并导航到HeroesAbilitySlice,然后点击Back,MainAbility将收到结果。

AbilitySlice导航

同一Page中不同的AbilitySlice间导航,通过调用present()方法来实现,如下:

Button heroesBtn = ...;
heroesBtn.setClickedListener(component -> present(new HeroesAbilitySlice(), new Intent()));

如果要从导航目标AbilitySlice返回时获得返回值,则应使用presentForResult():

Button heroesBtn  = ...;
heroesBtn.setClickedListener(component -> presentForResult(new HeroesAbilitySlice(), new Intent(), 0));

从目标AbilitySlice返回时,系统将回调onResult()来接收和处理返回结果:

@Override
protected void onResult(int requestCode, Intent resultIntent) {
    if (requestCode == 0) {
        text.setText("Hello " + resultIntent.getStringParam("name"));
    }
}

返回结果由目标AbilitySlice通过setResult()进行设置,然后调用terminate()方法返回请求方:

text.setClickedListener(component -> {
    Intent resultIntent = new Intent();
    resultIntent.setParam("name", "Jason");
    setResult(resultIntent);

    terminate();
});

ORM数据库

HarmonyOS支持关系数据库、ORM数据库、轻量级偏好数据库、分布式数据服务、分布式文件服务等数据管理方式。本文使用ORM数据库来管理我们的数据。

HarmonyOS对象关系映射(ORM)数据库是一款基于SQLite的数据库框架,提供单设备上结构化数据的存储和访问能力。ORM数据库跟关系数据库一样,都使用SQLite作为持久化引擎,底层使用的是同一套数据库连接池和数据库连接机制,在关系型数据库操作的基础上又实现了对象关系映射等特性,提供了增删改查等面向对象接口。

基础配置

使用ORM数据库前,需要配置“build.gradle”文件,在其中的“ohos”节点中添加以下配置:

compileOptions{        
    annotationEnabled true    
} 

创建数据库

  1. 定义数据库类,继承OrmDatabase,再通过@Database注解内的entities属性指定数据库的Entity。
package io.itrunner.heroes.data;

import ohos.data.orm.OrmDatabase;
import ohos.data.orm.annotation.Database;

@Database(entities = {Hero.class}, version = 1)
public abstract class HeroStore extends OrmDatabase {
}

version为数据库版本号。

  1. 定义Entity,创建一个继承OrmObject并用@Entity注解的类。
package io.itrunner.heroes.data;

import ohos.data.orm.OrmObject;
import ohos.data.orm.annotation.Column;
import ohos.data.orm.annotation.Entity;
import ohos.data.orm.annotation.Index;
import ohos.data.orm.annotation.PrimaryKey;

@Entity(tableName = "hero", indices = {@Index(value = {"hero_name"}, name = "name_index", unique = true)})
public class Hero extends OrmObject {
    @PrimaryKey(autoGenerate = true)
    private Long id;

    @Column(name = "hero_name", notNull = true)
    private String name;

    public Hero() {
    }

    // getter and setter
		...
}
  1. 创建、初始化数据库。 下面的createDatabase方法调用DatabaseHelper的getOrmContext方法创建数据库。如果数据库已经存在,不会重复创建。initDatabase()方法从strarray.json中读取数据初始化数据库。
package io.itrunner.heroes.data;

import io.itrunner.heroes.ResourceTable;
import ohos.app.Context;
import ohos.data.DatabaseHelper;
import ohos.data.orm.OrmContext;
import ohos.global.resource.ResourceManager;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

public class DBUtils {
    public static final String DATABASE_NAME = "HeroStore.db";
    public static final String DATABASE_NAME_ALIAS = "HeroStore";

    private static final HiLogLabel LOG_LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00101, "Hero Database");

    public static void createDatabase(Context context) {
        HiLog.info(LOG_LABEL, "create database: %{public}s", DATABASE_NAME_ALIAS);

        DatabaseHelper helper = new DatabaseHelper(context);
        helper.getOrmContext(DATABASE_NAME_ALIAS, DATABASE_NAME, HeroStore.class);

        HiLog.info(LOG_LABEL, "local database path: %{public}s", context.getDatabaseDir().getPath());
    }

    public static void initDatabase(Context context) {
        HiLog.info(LOG_LABEL, "initial database ...");
				
        OrmContext ormContext = getOrmContext(context);
        try {
            ResourceManager resourceManager = context.getResourceManager();
            String[] heroes = resourceManager.getElement(ResourceTable.Strarray_heroes).getStringArray();

            for (String name : heroes) {
                ormContext.insert(new Hero(name));
            }

            ormContext.flush();
        } catch (Exception e) {
            HiLog.error(LOG_LABEL, e.getMessage());
        }
    }

    static OrmContext getOrmContext(Context context) {
        DatabaseHelper helper = new DatabaseHelper(context);
        return helper.getOrmContext(DATABASE_NAME_ALIAS);
    }
}

通过context.getDatabaseDir()方法可以获取数据库文件所在的目录。

最后在系统启动时调用创建、初始化数据库的方法:

public class MainAbility extends Ability {

    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setMainRoute(MainAbilitySlice.class.getName());

        createDatabase(this);
        initDatabase(this);
    }

}

定义Repository

OrmContext提供了CRUD和where方法,操作数据库一般不需要编写SQL语句。下面是本文将使用的HeroRepository:

package io.itrunner.heroes.data;

import ohos.app.Context;
import ohos.data.orm.OrmContext;
import ohos.data.orm.OrmPredicates;
import ohos.data.rdb.ValuesBucket;

import java.util.List;

public class HeroRepository {
    private static final String ID = "id";
    private static final String HERO_NAME = "hero_name";

    private OrmContext ormContext;

    public HeroRepository(Context context) {
        ormContext = DBUtils.getOrmContext(context);
    }

    public List<Hero> queryTop4() {
        OrmPredicates predicates = ormContext.where(Hero.class);
        predicates.orderByAsc(HERO_NAME);
        predicates.limit(4);
        return ormContext.query(predicates);
    }

    public List<Hero> queryAll() {
        OrmPredicates predicates = ormContext.where(Hero.class);
        predicates.orderByAsc(HERO_NAME);
        return ormContext.query(predicates);
    }

    public Hero getOne(Long id) {
        OrmPredicates predicates = ormContext.where(Hero.class);
        predicates.equalTo(ID, id);
        List<Hero> heroes = ormContext.query(predicates);
        return heroes.isEmpty() ? null : heroes.get(0);
    }

    public List<Hero> queryByName(String name) {
        OrmPredicates predicates = ormContext.where(Hero.class);
        predicates.contains(HERO_NAME, name);
        predicates.orderByAsc(HERO_NAME);
        return ormContext.query(predicates);
    }

    public void insert(Hero hero) {
        ormContext.insert(hero);
        ormContext.flush();
    }

    public void update(Hero hero) {
        OrmPredicates predicates = ormContext.where(Hero.class);
        predicates.equalTo(ID, hero.getId());

        ValuesBucket valuesBucket = new ValuesBucket();
        valuesBucket.putString(HERO_NAME, hero.getName());
        ormContext.update(predicates, valuesBucket);
    }

    public void delete(Long id) {
        OrmPredicates predicates = ormContext.where(Hero.class);
        predicates.equalTo(ID, id);
        ormContext.delete(predicates);
        ormContext.flush();
    }
}

备份与恢复

ORM数据库提供了备份、恢复、删除数据库的方法,如下:

public class DBUtils {
    public static final String DATABASE_NAME = "HeroStore.db";
    public static final String DATABASE_NAME_ALIAS = "HeroStore";

    ...

    /**
     * @param destPath the path for backing up the database
     */
    public static void backupDatabase(Context context, String destPath) {
        HiLog.info(LOG_LABEL, "backup database to %{public}s", destPath);
        OrmContext ormContext = getOrmContext(context);
        ormContext.backup(destPath);
        ormContext.close();
    }

    /**
     * @param srcPath the path where the database file is stored
     */
    public static void restoreDatabase(Context context, String srcPath) {
        HiLog.info(LOG_LABEL, "restore database from %{public}s", srcPath);
        OrmContext ormContext = getOrmContext(context);
        ormContext.restore(srcPath);
        ormContext.close();
    }

    /**
     * @param name the database name, for example: HeroStore.db
     */
    public static void deleteDatabase(Context context, String name) {
        HiLog.info(LOG_LABEL, "delete database: %{public}s", name);
        DatabaseHelper helper = new DatabaseHelper(context);
        helper.deleteRdbStore(name);
    }

    static OrmContext getOrmContext(Context context) {
        DatabaseHelper helper = new DatabaseHelper(context);
        return helper.getOrmContext(DATABASE_NAME_ALIAS);
    }
}

说明:备份与恢复需要提供完整的数据库路径;删除只需提供数据库文件名,会自动从DatabaseDir下删除数据库。例如:

public class MainAbility extends Ability {

    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setMainRoute(MainAbilitySlice.class.getName());

        createDatabase(this);
        initDatabase(this);
        backupDatabase(this, getDatabaseDir() + "/HeroStoreBackup.db");
        restoreDatabase(this, getDatabaseDir() + "/HeroStoreBackup.db");
        deleteDatabase(this, "HeroStoreBackup.db");
    }

}

Hero UI

本部分将创建Dashboard、Hero列表、Hero详情三个AbilitySlice页面,采用AbilitySlice导航实现页面切换。

国际化

在资源文件一节,我们介绍过在XML布局和Java代码中引用资源文件的方法。为实现国际化,可以将页面显示的内容配置在资源文件string.json中,内容如下:

{
  "string": [
    {
      "name": "app_name",
      "value": "Tour of Heroes"
    },
    {
      "name": "mainability_description",
      "value": "heroes page"
    },
    {
      "name": "dashboard",
      "value": "Dashboard"
    },
    {
      "name": "heroes",
      "value": "Heroes"
    },
    {
      "name": "top_heroes",
      "value": "Top Heroes"
    },
    {
      "name": "hero_search",
      "value": "Hero Search"
    },
    {
      "name": "my_heroes",
      "value": "My Heroes"
    },
    {
      "name": "hero_name",
      "value": "Hero Name"
    },
    {
      "name": "add",
      "value": "Add"
    },
    {
      "name": "delete",
      "value": "Delete"
    },
    {
      "name": "no",
      "value": "No"
    },
    {
      "name": "name",
      "value": "Name"
    },
    {
      "name": "hero_details",
      "value": "Hero Details"
    },
    {
      "name": "id",
      "value": "ID"
    },
    {
      "name": "back",
      "value": "Back"
    },
    {
      "name": "save",
      "value": "Save"
    }
  ]
}

限于篇幅,这里不再列出中文版。

切换语言 在TV模拟器中,返回Home页面,向左拖动图标,找到【设置】并进入,然后依次点击【通用】 > 【高级设置】 > 【语言】,选择语言。

Dashboard

Dashboard页面显示TOP 4英雄榜,点击hero进入Hero详情页面,可根据名字查询hero。其中,Hero查询列表使用了ListContainer组件,当数据超出组件高度时,上下拖动可以查看所有数据。

定义公共的navigation.xml布局 三个页面顶部均含有导航按钮,可以提取为公共的布局文件,如下:

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_content"
    ohos:width="match_content"
    ohos:orientation="vertical">

    <Text
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:bottom_margin="20vp"
        ohos:text="$string:app_name"
        ohos:text_size="25fp"/>

    <DirectionalLayout
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:orientation="horizontal">

        <Button
            ohos:id="$+id:button_dashboard"
            ohos:height="40vp"
            ohos:width="150vp"
            ohos:background_element="$graphic:gray_button_element"
            ohos:text="$string:dashboard"
            ohos:text_alignment="center"
            ohos:text_size="20fp"/>

        <Button
            ohos:id="$+id:button_heroes"
            ohos:height="40vp"
            ohos:width="150vp"
            ohos:background_element="$graphic:gray_button_element"
            ohos:left_margin="20vp"
            ohos:text="$string:heroes"
            ohos:text_alignment="center"
            ohos:text_size="20fp"/>
    </DirectionalLayout>
</DirectionalLayout>

在其他XML布局文件中引用Navigation布局的方式如下:

<include
    ohos:height="match_content"
    ohos:width="match_content"
    ohos:layout="$layout:navigation"/>

Dashboard布局 main.xml

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_content"
    ohos:width="match_parent"
    ohos:orientation="vertical"
    ohos:padding="20vp">

    <include
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:layout="$layout:navigation"/>

    <Text
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:bottom_margin="20vp"
        ohos:layout_alignment="horizontal_center"
        ohos:text="$string:top_heroes"
        ohos:text_size="25fp"
        ohos:top_margin="20vp"/>

    <TableLayout
        ohos:id="$+id:top_heroes"
        ohos:height="match_content"
        ohos:width="match_parent"
        ohos:column_count="4"
        ohos:layout_alignment="center">
    </TableLayout>

    <Text
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:bottom_margin="20vp"
        ohos:layout_alignment="left"
        ohos:text="$string:hero_search"
        ohos:text_size="20fp"
        ohos:top_margin="20vp"/>

    <TextField
        ohos:id="$+id:search"
        ohos:height="40vp"
        ohos:width="330vp"
        ohos:background_element="$graphic:black_border_element"
        ohos:input_enter_key_type="enter_key_type_search"
        ohos:padding="4vp"
        ohos:text_alignment="vertical_center"
        ohos:text_size="20fp"/>

    <DependentLayout
        ohos:height="175vp"
        ohos:width="match_parent">

        <ListContainer
            ohos:id="$+id:search_list"
            ohos:height="170vp"
            ohos:width="330vp"
            ohos:rebound_effect="true"
            ohos:shader_color="#90EE90"/>

        <Image
            ohos:id="$+id:image_components"
            ohos:height="40vp"
            ohos:width="40vp"
            ohos:align_parent_bottom="true"
            ohos:align_parent_right="true"
            ohos:image_src="$media:icon"
            ohos:scale_mode="inside"/>
    </DependentLayout>
</DirectionalLayout>

MainAbilitySlice

package io.itrunner.heroes.slice;

import io.itrunner.heroes.ResourceTable;
import io.itrunner.heroes.data.Hero;
import io.itrunner.heroes.data.HeroRepository;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.content.Intent;
import ohos.aafwk.content.Operation;
import ohos.agp.components.*;
import ohos.agp.components.ComponentContainer.LayoutConfig;
import ohos.agp.components.element.ShapeElement;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

import java.util.List;

public class MainAbilitySlice extends AbilitySlice {
    private static final String TAG = "MainAbilitySlice";
    private static final String ACTION_COMPONENTS = "action.hero.components";

    private static final HiLogLabel LOG_LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00101, TAG);

    private HeroRepository repository;
    private TextField searchText;

    @Override
    public void onStart(Intent intent) {
        HiLog.info(LOG_LABEL, "onStart");
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_main);

        repository = new HeroRepository(this);

        bindNavListener();
        bindSearchListener();
    }

    @Override
    public void onActive() {
        HiLog.info(LOG_LABEL, "onActive");
        super.onActive();

        queryTopHeroes();
    }

    @Override
    protected void onInactive() {
        HiLog.info(LOG_LABEL, "onInactive");
        super.onInactive();
    }

    @Override
    protected void onBackground() {
        HiLog.info(LOG_LABEL, "onBackground");
        super.onBackground();
    }

    @Override
    public void onForeground(Intent intent) {
        HiLog.info(LOG_LABEL, "onForeground");
        super.onForeground(intent);
    }

    @Override
    protected void onStop() {
        HiLog.info(LOG_LABEL, "onStop");
        super.onStop();
    }

    private void bindNavListener() {
        // to heroes slice
        Button heroesBtn = (Button) findComponentById(ResourceTable.Id_button_heroes);
        heroesBtn.setClickedListener(component -> present(new HeroesAbilitySlice(), new Intent()));

        // to components page
        Image componentsImg = (Image) findComponentById(ResourceTable.Id_image_components);
        componentsImg.setClickedListener(component -> gotoPage(ACTION_COMPONENTS));
    }

    private void bindSearchListener() {
        searchText = (TextField) findComponentById(ResourceTable.Id_search);
        searchText.setEditorActionListener(action -> {
            if (action == 3) {
                fillSearchList(searchText.getText());
                return true;
            }
            return false;
        });
    }

    private void fillSearchList(String name) {
        List<Hero> heroes = repository.queryByName(name);

        ListContainer container = (ListContainer) findComponentById(ResourceTable.Id_search_list);
        ListItemProvider itemProvider = new ListItemProvider(this, heroes);
        container.setItemProvider(itemProvider);
        container.setItemClickedListener((listContainer, component, position, id) -> {
            gotoHeroDetails(itemProvider.getItemId(position));
            clear();
        });
    }

    private void queryTopHeroes() {
        TableLayout tableLayout = (TableLayout) findComponentById(ResourceTable.Id_top_heroes);
        ShapeElement background = new ShapeElement(this, ResourceTable.Graphic_blue_button_element);
        LayoutConfig config = new LayoutConfig(400, 100);
        config.setMargins(0, 0, 40, 0);

        tableLayout.removeAllComponents();

        List<Hero> heroes = repository.queryTop4();
        for (Hero hero : heroes) {
            Button heroBtn = new Button(this);
            heroBtn.setText(hero.getName());
            heroBtn.setTextSize(40);
            heroBtn.setBackground(background);
            heroBtn.setLayoutConfig(config);
            heroBtn.setClickedListener(component -> gotoHeroDetails(hero.getId()));

            tableLayout.addComponent(heroBtn);
        }
    }

    private void gotoHeroDetails(Long id) {
        Intent intent = new Intent();
        intent.setParam("id", id);
        present(new HeroDetailsAbilitySlice(), intent);
    }

    private void gotoPage(String action) {
        Intent intent = new Intent();
        Operation operation = new Intent.OperationBuilder().withAction(action).build();
        intent.setOperation(operation);

        startAbility(intent);
    }

    private void clear() {
        searchText.setText("");
    }

}

在搜索输入框TextField的定义中,我们设置属性input_enter_key_type的值为"enter_key_type_search",软键盘的回车键会显示为“搜索”。input_enter_key_type有四个可选值:enter_key_type_go、enter_key_type_search、enter_key_type_send、enter_key_type_unspecified。设置属性input_enter_key_type后,需要使用EditorActionListener事件,其中参数action取值的对应关系为:2 -> go、3-> search、4 -> send。 ListContainer Hero查询列表使用了ListContainer组件,需要构造ItemProvider为其填充数据。原官方的示例代码继承了BaseItemProvider,现已调整为继承RecycleItemProvider。当数据行超过组件高度时,使用BaseItemProvider不能完整显示数据。ListItemProvider的实现如下:

package io.itrunner.heroes.slice;

import io.itrunner.heroes.ResourceTable;
import io.itrunner.heroes.data.Hero;
import ohos.aafwk.ability.AbilitySlice;
import ohos.agp.components.*;

import java.util.List;

public class ListItemProvider extends RecycleItemProvider {
    private List<Hero> data;
    private AbilitySlice slice;

    ListItemProvider(AbilitySlice abilitySlice, List<Hero> data) {
        slice = abilitySlice;
        this.data = data;
    }

    @Override
    public int getCount() {
        return data.size();
    }
		
    @Override
    public Object getItem(int i) {
        return this.data.get(i);
    }

    @Override
    public long getItemId(int position) {
        return data.get(position).getId();
    }

    @Override
    public Component getComponent(int position, Component convertView, ComponentContainer parent) {
        Component component = LayoutScatter.getInstance(slice).parse(ResourceTable.Layout_list_item, null, false);
        if (!(component instanceof ComponentContainer)) {
            return null;
        }
        ComponentContainer rootLayout = (ComponentContainer) component;
        Text leftText = (Text) rootLayout.findComponentById(ResourceTable.Id_list_content);
        leftText.setText(data.get(position).getName());
        return component;
    }
}

List中行的布局如下(list_item.xml):

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_content"
    ohos:width="match_parent"
    ohos:orientation="horizontal">

    <Text
        ohos:id="$+id:list_content"
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:padding="4vp"
        ohos:text="hero"
        ohos:text_alignment="left"
        ohos:text_size="16fp"/>
</DirectionalLayout>

效果如下:

Hero列表

Hero列表页面可以增加、删除hero,点击hero将进入Hero详情页面。本页面使用了ScrollView组件,上下拖动表格可以查看所有数据。 XML布局 heroes.xml

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_parent"
    ohos:width="match_parent"
    ohos:orientation="vertical"
    ohos:padding="20vp">

    <include
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:layout="$layout:navigation"/>

    <Text
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:bottom_margin="20vp"
        ohos:text="$string:my_heroes"
        ohos:text_size="25fp"
        ohos:top_margin="20vp"/>

    <DirectionalLayout
        ohos:height="50vp"
        ohos:width="match_content"
        ohos:orientation="horizontal">

        <Text
            ohos:height="match_content"
            ohos:width="match_content"
            ohos:text="$string:hero_name"
            ohos:text_size="20fp"/>

        <TextField
            ohos:id="$+id:hero_name"
            ohos:height="40vp"
            ohos:width="300vp"
            ohos:background_element="$graphic:black_border_element"
            ohos:left_margin="20vp"
            ohos:padding="4vp"
            ohos:text_alignment="vertical_center"
            ohos:text_size="20fp"/>

        <Button
            ohos:id="$+id:button_add"
            ohos:height="40vp"
            ohos:width="100vp"
            ohos:background_element="$graphic:gray_button_element"
            ohos:left_margin="20vp"
            ohos:text="$string:add"
            ohos:text_alignment="center"
            ohos:text_size="20fp"/>
    </DirectionalLayout>

    <ScrollView
        ohos:id="$+id:scroll_view"
        ohos:height="240vp"
        ohos:width="match_parent"
        ohos:rebound_effect="true"
        ohos:top_margin="20vp"/>
</DirectionalLayout>

HeroesAbilitySlice

package io.itrunner.heroes.slice;

import io.itrunner.heroes.ResourceTable;
import io.itrunner.heroes.data.Hero;
import io.itrunner.heroes.data.HeroRepository;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.content.Intent;
import ohos.agp.components.*;
import ohos.agp.components.TableLayout.LayoutConfig;
import ohos.agp.components.element.ShapeElement;
import ohos.agp.utils.TextAlignment;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

import java.util.List;

public class HeroesAbilitySlice extends AbilitySlice {
    private static final String TAG = "HeroesAbilitySlice";

    private static final HiLogLabel LOG_LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00101, TAG);

    private HeroRepository repository;
    private TextField heroText;

    @Override
    public void onStart(Intent intent) {
        HiLog.info(LOG_LABEL, "onStart");
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_heroes);

        repository = new HeroRepository(this);

        bindNavListener();
        bindAddListener();
    }

    @Override
    public void onActive() {
        HiLog.info(LOG_LABEL, "onActive");
        super.onActive();

        queryHeroes();
    }

    @Override
    protected void onInactive() {
        HiLog.info(LOG_LABEL, "onInactive");
        super.onInactive();
    }

    @Override
    protected void onBackground() {
        HiLog.info(LOG_LABEL, "onBackground");
        super.onBackground();
    }

    @Override
    public void onForeground(Intent intent) {
        HiLog.info(LOG_LABEL, "onForeground");
        super.onForeground(intent);
    }

    @Override
    protected void onStop() {
        HiLog.info(LOG_LABEL, "onStop");
        super.onStop();
    }

    private void bindNavListener() {
        // goto dashboard page
        Button dashboardBtn = (Button) findComponentById(ResourceTable.Id_button_dashboard);
        dashboardBtn.setClickedListener(component -> present(new MainAbilitySlice(), new Intent()));
    }

    private void bindAddListener() {
        heroText = (TextField) findComponentById(ResourceTable.Id_hero_name);
        Button addBtn = (Button) findComponentById(ResourceTable.Id_button_add);
        addBtn.setClickedListener(component -> addHero());
    }

    private void addHero() {
        String heroName = heroText.getText().trim();
        if (heroName.length() > 2) {
            repository.insert(new Hero(heroName));
            heroText.setText("");
            queryHeroes();
        }
    }

    private void queryHeroes() {
        TableLayout heroesTable = (TableLayout) LayoutScatter.getInstance(this).parse(ResourceTable.Layout_hero_table, null, false);

        LayoutConfig columnConfig = new LayoutConfig(160, 60);
        LayoutConfig buttonConfig = new LayoutConfig(58, LayoutConfig.MATCH_CONTENT);
        buttonConfig.setMargins(50, 4, 0, 4);

        ShapeElement grayButtonElement = new ShapeElement(this, ResourceTable.Graphic_gray_button_element);
        ShapeElement columnElement = new ShapeElement(this, ResourceTable.Graphic_white_column_element);

        List<Hero> heroes = repository.queryAll();

        int i = 1;
        for (Hero hero : heroes) {

            Text no = new Text(this);
            no.setText(i++ + "");
            no.setTextSize(30);
            no.setWidth(80);
            no.setHeight(58);
            no.setPadding(4, 4, 4, 4);
            no.setBackground(columnElement);

            Button heroNameBtn = new Button(this);
            heroNameBtn.setText(hero.getName());
            heroNameBtn.setTextSize(30);
            heroNameBtn.setTextAlignment(TextAlignment.CENTER);
            heroNameBtn.setWidth(800);
            heroNameBtn.setHeight(58);
            heroNameBtn.setPadding(4, 4, 4, 4);
            heroNameBtn.setBackground(columnElement);
            heroNameBtn.setClickedListener(component -> gotoHeroDetails(hero.getId()));

            Button deleteBtn = new Button(this);
            deleteBtn.setText("X");
            deleteBtn.setTextSize(30);
            deleteBtn.setPadding(4, 4, 4, 4);
            deleteBtn.setLayoutConfig(buttonConfig);
            deleteBtn.setBackground(grayButtonElement);
            deleteBtn.setClickedListener(component -> {
                repository.delete(hero.getId());
                queryHeroes();
            });
            DirectionalLayout deleteCol = new DirectionalLayout(this);
            deleteCol.setLayoutConfig(columnConfig);
            deleteCol.setBackground(columnElement);
            deleteCol.addComponent(deleteBtn);

            heroesTable.addComponent(no);
            heroesTable.addComponent(heroNameBtn);
            heroesTable.addComponent(deleteCol);
        }

        ScrollView scrollView = (ScrollView) findComponentById(ResourceTable.Id_scroll_view);
        scrollView.removeAllComponents();
        scrollView.addComponent(heroesTable);
    }

    private void gotoHeroDetails(Long id) {
        Intent intent = new Intent();
        intent.setParam("id", id);
        present(new HeroDetailsAbilitySlice(), intent);
    }

}

Hero Table Dashboard的表格没有表头,而本页面的表格增加了表头,动态添加数据时需要特殊处理一下。这里没有将TableLayout放到页面布局里,而是单独生成一个文件,在代码中通过LayoutScatter的parse()方法加载。

hero_table.xml的内容如下:

<?xml version="1.0" encoding="utf-8"?>
<TableLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:id="$+id:heroes"
    ohos:height="match_content"
    ohos:width="match_parent"
    ohos:column_count="3">

    <Text
        ohos:height="30vp"
        ohos:width="40vp"
        ohos:background_element="$graphic:gray_column_element"
        ohos:text="$string:no"
        ohos:text_alignment="center"
        ohos:text_size="20fp"/>

    <Text
        ohos:height="30vp"
        ohos:width="400vp"
        ohos:background_element="$graphic:gray_column_element"
        ohos:text="$string:name"
        ohos:text_alignment="center"
        ohos:text_size="20fp"/>

    <Text
        ohos:height="30vp"
        ohos:width="80vp"
        ohos:background_element="$graphic:gray_column_element"
        ohos:text="$string:delete"
        ohos:text_alignment="center"
        ohos:text_size="20fp"/>
</TableLayout>

Hero详情

本页面较简单,只提供修改英雄名字的功能。 XML布局 hero_details.xml

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_content"
    ohos:width="match_content"
    ohos:orientation="vertical"
    ohos:padding="20vp">

    <include
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:layout="$layout:navigation"/>

    <Text
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:bottom_margin="20vp"
        ohos:text="$string:hero_details"
        ohos:text_size="25fp"
        ohos:top_margin="20vp"/>

    <DirectionalLayout
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:orientation="horizontal">

        <Text
            ohos:height="match_content"
            ohos:width="70vp"
            ohos:text="$string:id"
            ohos:text_size="20fp"/>

        <Text
            ohos:id="$+id:hero_id"
            ohos:height="40vp"
            ohos:width="match_content"
            ohos:padding="4vp"
            ohos:text_size="20fp"/>
    </DirectionalLayout>

    <DirectionalLayout
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:orientation="horizontal">

        <Text
            ohos:height="match_content"
            ohos:width="70vp"
            ohos:text="$string:name"
            ohos:text_size="20fp"/>

        <TextField
            ohos:id="$+id:hero_name"
            ohos:height="40vp"
            ohos:width="300vp"
            ohos:background_element="$graphic:black_border_element"
            ohos:padding="4vp"
            ohos:text_size="20fp"/>
    </DirectionalLayout>

    <DirectionalLayout
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:orientation="horizontal"
        ohos:padding="10vp">

        <Button
            ohos:id="$+id:button_back"
            ohos:height="40vp"
            ohos:width="150vp"
            ohos:background_element="$graphic:gray_button_element"
            ohos:text="$string:back"
            ohos:text_alignment="center"
            ohos:text_size="20fp"/>

        <Button
            ohos:id="$+id:button_save"
            ohos:height="40vp"
            ohos:width="150vp"
            ohos:background_element="$graphic:gray_button_element"
            ohos:left_margin="20vp"
            ohos:text="$string:save"
            ohos:text_alignment="center"
            ohos:text_size="20fp"/>
    </DirectionalLayout>
</DirectionalLayout>

HeroDetailsAbilitySlice

package io.itrunner.heroes.slice;

import io.itrunner.heroes.ResourceTable;
import io.itrunner.heroes.data.Hero;
import io.itrunner.heroes.data.HeroRepository;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.content.Intent;
import ohos.agp.components.Button;
import ohos.agp.components.Text;
import ohos.agp.components.TextField;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

public class HeroDetailsAbilitySlice extends AbilitySlice {
    private static final String TAG = "HeroDetailsAbilitySlice";

    private static final HiLogLabel LOG_LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00101, TAG);

    private HeroRepository repository;

    @Override
    public void onStart(Intent intent) {
        HiLog.info(LOG_LABEL, "onStart");
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_hero_details);

        repository = new HeroRepository(this);

        bindNavListener();
        bindButtonListener();
        showHeroDetails(intent);
    }

    @Override
    public void onActive() {
        HiLog.info(LOG_LABEL, "onActive");
        super.onActive();
    }

    @Override
    public void onForeground(Intent intent) {
        HiLog.info(LOG_LABEL, "onForeground");
        super.onForeground(intent);
    }

    private void bindNavListener() {
        // Dashboard Button
        Button dashboardBtn = (Button) findComponentById(ResourceTable.Id_button_dashboard);
        dashboardBtn.setClickedListener(component -> present(new MainAbilitySlice(), new Intent()));

        // Heroes Button
        Button heroesBtn = (Button) findComponentById(ResourceTable.Id_button_heroes);
        heroesBtn.setClickedListener(component -> present(new HeroesAbilitySlice(), new Intent()));
    }

    private void bindButtonListener() {
        // Back Button
        Button backBtn = (Button) findComponentById(ResourceTable.Id_button_back);
        backBtn.setClickedListener(component -> back());

        // Save Button
        Button saveBtn = (Button) findComponentById(ResourceTable.Id_button_save);
        saveBtn.setClickedListener(component -> {
            updateHero();
            back();
        });
    }

    private void showHeroDetails(Intent intent) {
        long id = intent.getLongParam("id", 0);
        Hero hero = repository.getOne(id);
        if (hero != null) {
            Text heroId = (Text) findComponentById(ResourceTable.Id_hero_id);
            heroId.setText(id + "");

            TextField heroName = (TextField) findComponentById(ResourceTable.Id_hero_name);
            heroName.setText(hero.getName());
        }
    }

    private void updateHero() {
        Text idText = (Text) findComponentById(ResourceTable.Id_hero_id);
        TextField nameText = (TextField) findComponentById(ResourceTable.Id_hero_name);

        Hero hero = new Hero();
        hero.setId(Long.parseLong(idText.getText()));
        hero.setName(nameText.getText().trim());

        repository.update(hero);
    }

    private void back() {
        terminate();
    }

}

需要说明的一点,导航到此页面时需要通过Intent传入Hero ID参数,如下:

private void gotoHeroDetails(Long id) {
    Intent intent = new Intent();
    intent.setParam("id", id);
    present(new HeroDetailsAbilitySlice(), intent);
}

Wearable

DevEco已支持跨设备运行应用,但需要使用真机。本章仅介绍Wearable Java UI开发的基本方法,与TV应用无关。

创建Wearable Module

在工程根目录点击右键,在弹出的菜单中选择New > Module: 选择Wearable > Empty Feature Ability(Java),然后填写module相关信息,创建module。

HarmonyOS应用有entry和feature两种模块类型,一个APP中,对于同一设备类型必须有且只有一个entry类型的模块,可以包含一个或多个feature类型的模块。开始我们创建的模块只支持TV设备,因此新创建的Wearable模块也为entry类型。

"deviceType": [
  "wearable"
],
"distro": {
  "deliveryWithInstall": true,
  "moduleName": "wearable",
  "moduleType": "entry"
},

Wearable UI

PageSlider支持左右或上下滑动切换页面,这是手表切换页面的主要方式之一。本节演示PageSlider组件的用法,开发如下两个页面: 国际化 string.json 当前手表模拟器不能设置语言,为了测试可以在启动时指定语言。

{
  "string": [
    {
      "name": "app_name",
      "value": "英雄之旅"
    },
    {
      "name": "mainability_description",
      "value": "英雄之旅"
    },
    {
      "name": "sleep",
      "value": "睡眠"
    },
    {
      "name": "hour",
      "value": "小时"
    },
    {
      "name": "minute",
      "value": "分钟"
    },
    {
      "name": "goal",
      "value": "目标"
    }
  ]
}

主布局 main.xml 在主布局中声明PageSlider,其“orientation”属性设定为"horizontal",支持左右滑动切换页面。

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_parent"
    ohos:width="match_parent">

    <PageSlider
        ohos:id="$+id:page_slider"
        ohos:height="match_parent"
        ohos:width="match_parent"
        ohos:orientation="horizontal"/>
</DirectionalLayout>

Hero列表子页面 heroes.xml 使用了ScrollView组件,上下拖动可以查看所有数据。

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_parent"
    ohos:width="match_parent"
    ohos:background_element="#FF000000"
    ohos:bottom_padding="30vp"
    ohos:orientation="vertical"
    ohos:top_padding="30vp">

    <ScrollView
        ohos:height="match_parent"
        ohos:width="match_parent">

        <DirectionalLayout
            ohos:id="$+id:heroes"
            ohos:height="match_content"
            ohos:width="match_parent"
            ohos:orientation="vertical"/>
    </ScrollView>
</DirectionalLayout>

Sleep子页面 sleep.xml

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_parent"
    ohos:width="match_parent"
    ohos:background_element="#FF000000"
    ohos:orientation="vertical">

    <Image
        ohos:height="32vp"
        ohos:width="32vp"
        ohos:image_src="$media:sleep"
        ohos:layout_alignment="horizontal_center"
        ohos:top_margin="30vp"/>

    <Text
        ohos:height="20vp"
        ohos:width="match_parent"
        ohos:alpha="0.66"
        ohos:layout_alignment="horizontal_center"
        ohos:text="$string:sleep"
        ohos:text_alignment="center"
        ohos:text_color="white"
        ohos:text_size="16vp"/>

    <DirectionalLayout
        ohos:height="70vp"
        ohos:width="match_content"
        ohos:layout_alignment="horizontal_center"
        ohos:orientation="horizontal"
        ohos:top_margin="8vp">

        <Text
            ohos:id="$+id:sleep_hour_text"
            ohos:height="match_content"
            ohos:width="match_content"
            ohos:layout_alignment="center"
            ohos:text="6"
            ohos:text_alignment="center"
            ohos:text_color="white"
            ohos:text_size="58vp"/>

        <Text
            ohos:height="match_content"
            ohos:width="match_content"
            ohos:alpha="0.66"
            ohos:bottom_padding="10vp"
            ohos:layout_alignment="bottom"
            ohos:left_margin="2vp"
            ohos:text="$string:hour"
            ohos:text_color="white"
            ohos:text_size="16vp"/>

        <Text
            ohos:id="$+id:sleep_minute_text"
            ohos:height="match_content"
            ohos:width="match_content"
            ohos:layout_alignment="center"
            ohos:left_margin="2vp"
            ohos:text="30"
            ohos:text_alignment="center"
            ohos:text_color="white"
            ohos:text_size="58vp"/>

        <Text
            ohos:height="match_content"
            ohos:width="match_content"
            ohos:alpha="0.66"
            ohos:bottom_padding="10vp"
            ohos:layout_alignment="bottom"
            ohos:left_margin="2vp"
            ohos:text="$string:minute"
            ohos:text_color="white"
            ohos:text_size="16vp"/>
    </DirectionalLayout>

    <DirectionalLayout
        ohos:height="25vp"
        ohos:width="match_content"
        ohos:layout_alignment="horizontal_center"
        ohos:orientation="horizontal"
        ohos:top_margin="20vp">

        <Text
            ohos:height="20vp"
            ohos:width="match_content"
            ohos:alpha="0.66"
            ohos:bottom_margin="1vp"
            ohos:text="$string:goal"
            ohos:text_alignment="bottom"
            ohos:text_color="white"
            ohos:text_size="16vp"/>

        <Text
            ohos:id="$+id:sleep_goal_text"
            ohos:height="match_parent"
            ohos:width="match_content"
            ohos:bottom_padding="2vp"
            ohos:left_margin="2vp"
            ohos:text="8"
            ohos:text_color="white"
            ohos:text_size="21vp"
            ohos:text_weight="600"/>

        <Text
            ohos:height="20vp"
            ohos:width="match_content"
            ohos:alpha="0.66"
            ohos:left_margin="2vp"
            ohos:text="$string:hour"
            ohos:text_color="white"
            ohos:text_size="16vp"/>
    </DirectionalLayout>

    <DirectionalLayout
        ohos:height="25vp"
        ohos:width="match_content"
        ohos:layout_alignment="horizontal_center"
        ohos:orientation="horizontal"
        ohos:top_margin="5vp">

        <Text
            ohos:id="$+id:device_name"
            ohos:height="12vp"
            ohos:width="match_content"
            ohos:alpha="0.66"
            ohos:text_color="white"
            ohos:text_size="10vp"/>
    </DirectionalLayout>
</DirectionalLayout>

MainAbilitySlice MainAbilitySlice加载主布局,为PageSlider构建Provider并添加子页面。

package io.itrunner.heroes.wearable.slice;

import io.itrunner.heroes.wearable.ResourceTable;
import io.itrunner.heroes.wearable.slice.slider.HeroesComponent;
import io.itrunner.heroes.wearable.slice.slider.PageSliderProviderImpl;
import io.itrunner.heroes.wearable.slice.slider.SleepComponent;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.content.Intent;
import ohos.agp.components.PageSlider;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

public class MainAbilitySlice extends AbilitySlice {
    private static final String TAG = "MainAbilitySlice";

    private static final HiLogLabel LOG_LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00102, TAG);

    @Override
    public void onStart(Intent intent) {
        HiLog.info(LOG_LABEL, "onStart");
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_main);

        // 添加子页面
        addComponents();
    }

    @Override
    public void onActive() {
        HiLog.info(LOG_LABEL, "onActive");
        super.onActive();
    }

    @Override
    public void onForeground(Intent intent) {
        HiLog.info(LOG_LABEL, "onForeground");
        super.onForeground(intent);
    }

    private void addComponents() {
        PageSliderProviderImpl provider = new PageSliderProviderImpl();
        provider.addComponent(new HeroesComponent(this));
        provider.addComponent(new SleepComponent(this));

        PageSlider slider = (PageSlider) findComponentById(ResourceTable.Id_page_slider);
        slider.setProvider(provider);
    }

}

PageSliderProviderImpl PageSlider需要使用PageSliderProvider填充数据,其实现如下:

package io.itrunner.heroes.wearable.slice.slider;

import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.PageSliderProvider;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class PageSliderProviderImpl extends PageSliderProvider {
    private static final String TAG = "PageSliderProvider";
    private static final HiLogLabel LOG_LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00102, TAG);

    private List<ComponentOwner> components = new ArrayList<>();

    public void addComponent(ComponentOwner component) {
        components.add(component);
    }
		
    @Override
    public int getCount() {
        return components.size();
    }

    @Override
    public Object createPageInContainer(ComponentContainer componentContainer, int index) {
        HiLog.info(LOG_LABEL, "create page in container, the index is %{public}d", index);
        if (componentContainer == null || index >= components.size()) {
            return Optional.empty();
        }

        components.get(index).init();
        Component component = components.get(index).getComponent();
        componentContainer.addComponent(component);

        return component;
    }

    @Override
    public void destroyPageFromContainer(ComponentContainer componentContainer, int index, Object object) {
        HiLog.info(LOG_LABEL, "destroy page from container, the index is %{public}d", index);
        if (componentContainer == null || index >= components.size()) {
            return;
        }
        Component component = components.get(index).getComponent();
        componentContainer.removeComponent(component);
    }

    @Override
    public boolean isPageMatchToObject(Component component, Object object) {
        return component == object;
    }

}

ComponentOwner 为了能在PageSliderProvider统一处理所有页面,定义了ComponentOwner接口。

package io.itrunner.heroes.wearable.slice.slider;

import ohos.agp.components.Component;

public interface ComponentOwner {
    /*
    获取存放的component
     */
    Component getComponent();

    /*
    当包含的component被添加到容器时回调
     */
    void init();
}

HeroesComponent Hero列表页面读取strarray.json中的数据填充列表。

package io.itrunner.heroes.wearable.slice.slider;

import io.itrunner.heroes.wearable.ResourceTable;
import ohos.agp.components.Component;
import ohos.agp.components.DirectionalLayout;
import ohos.agp.components.LayoutScatter;
import ohos.agp.components.Text;
import ohos.agp.utils.Color;
import ohos.agp.utils.TextAlignment;
import ohos.app.AbilityContext;
import ohos.global.resource.ResourceManager;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

public class HeroesComponent implements ComponentOwner {
    private static final String TAG = "HeroesComponent";
    private static final HiLogLabel LOG_LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00102, TAG);

    private AbilityContext context;
    private Component root;

    public HeroesComponent(AbilityContext context) {
        this.context = context;
        this.root = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_heroes, null, false);
    }

    @Override
    public Component getComponent() {
        return root;
    }

    @Override
    public void init() {
        fillHeroes();
    }

    private void fillHeroes() {
        DirectionalLayout layout = (DirectionalLayout) root.findComponentById(ResourceTable.Id_heroes);
        layout.removeAllComponents();

        ResourceManager resourceManager = context.getResourceManager();
        try {
            String[] heroes = resourceManager.getElement(ResourceTable.Strarray_heroes).getStringArray();
            for (String heroName : heroes) {
                Text hero = new Text(context);
                hero.setText(heroName);
                hero.setTextSize(40);
                hero.setTextAlignment(TextAlignment.CENTER);
                hero.setTextColor(Color.WHITE);
                hero.setWidth(DirectionalLayout.LayoutConfig.MATCH_PARENT);
                hero.setHeight(70);
                layout.addComponent(hero);
            }
        } catch (Exception e) {
            HiLog.error(LOG_LABEL, e.getMessage());
        }
    }
}

SleepComponent 从系统设置中读取设备名称,其他组件填充了固定的数据。

package io.itrunner.heroes.wearable.slice.slider;

import io.itrunner.heroes.wearable.ResourceTable;
import ohos.aafwk.ability.DataAbilityHelper;
import ohos.agp.components.Component;
import ohos.agp.components.LayoutScatter;
import ohos.agp.components.Text;
import ohos.app.AbilityContext;
import ohos.sysappcomponents.settings.SystemSettings;

public class SleepComponent implements ComponentOwner {
    private AbilityContext context;
    private Component root;

    public SleepComponent(AbilityContext context) {
        this.context = context;
        this.root = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_sleep, null, false);
    }

    @Override
    public Component getComponent() {
        return root;
    }

    @Override
    public void init() {
        Text hour = (Text) root.findComponentById(ResourceTable.Id_sleep_hour_text);
        hour.setText("6");

        Text minute = (Text) root.findComponentById(ResourceTable.Id_sleep_minute_text);
        minute.setText("30");

        Text goal = (Text) root.findComponentById(ResourceTable.Id_sleep_goal_text);
        goal.setText("8");

        // 读取设备名称
        DataAbilityHelper dataAbilityHelper = DataAbilityHelper.creator(context);
        String deviceName = SystemSettings.getValue(dataAbilityHelper, SystemSettings.General.DEVICE_NAME);
        Text deviceNameText = (Text) root.findComponentById(ResourceTable.Id_device_name);
        deviceNameText.setText(deviceName);
    }
}

期待HarmonyOS官网提供更完善的文档和示例代码,能够在线展示效果;期待HarmonyOS开放更多源码,提高更新频率,方便与促进开学者学习。

为梦想,千里行,相会在鸿蒙。来吧,朋友,伸出你的手,点燃星星之火。

参考资料

华为鸿蒙HarmonyOS官网