Harmony OS学习笔记——构建更加丰富的页面
icon?当页面信息较多时,为了让用户能够聚焦于当前显示的内容,需要对页面内容进行分类,提高页面空间利用率。Tabs组件可以在一个页面内快速实现视图内容的切换,一方面提升查找信息的效率,另一方面精简用户单次获取到的信息量。Tabs(value?barPosition:设置Tabs的页签位置。index:设置当前显示页签的索引。controller:设置Tabs控制器。
使用ArkWeb构建页面
ArkWeb简介
概念介绍
Web组件用于在应用程序中显示Web页面内容,为开发者提供页面加载、页面交互、页面调试等能力。可以用于实现移动端的混合式开发(HyBrid App):
- 页面加载:Web组件提供基础的前端页面加载的能力,包括加载网络页面、本地页面、HTML格式文本数据
- 页面交互:Web组件提供丰富的页面交互的方式,包括:设置前端页面深色模式,系窗口中加载页面,位置权限管理,Cookie管理,应用侧使用前端页面JavaScript等能力。
- 页面调试:Web组件支持使用Devtools工具调试前端页面。
ArkWeb API参考
官网网址:ArkTS API-ArkWeb(方舟Web)
ArkWeb涉及到的API主要有以下两个:
- Web组件:提供具有网页显示能力的一种组件。
- Webview:提供web控制能力的相关接口。例如控制Web组件加载的内容,控制Web内容后退前进,以及异步执行JavaScript脚本等能力。
页面加载与显示
加载网络页面
开发者可以在Web组件创建时,指定默认加载的网络页面。在默认页面加载完成后,如果开发者需要变更此Web组件显示的网络页面,可以通过调用loadUrl()接口加载指定的网页。
....
struct WebComponent {
webviewController: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
Button('loadUrl')
.onClick(() => {
try{
this.webviewController.loadUrl('www.example1.com')
} catch(error) {
...
}
})
// 组件创建时,加载www.example.com
Web({ src: 'www.example.com', controller: this.webviewController })
}
}
}
加载本地的页面
将本地页面文件放在应用的rawfile目录下,开发者可以在Web组件创建的时候指定默认加载的本地页面,并且加载完成后可通过调用loadUrl()接口变更当前Web组件的页面。
....
struct WebComponent {
webviewController: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
Button('loadUrl')
.onClick(() => {
try{
this.webviewController.loadUrl(${rawfile("local1.html1")})
} catch(error) {
...
}
})
// 组件创建时,加载本地页面
Web({ src: $rawfile("local.html"), controller: this.webviewController })
}
}
}
加载HTML格式数据
Web组件可以通过loadDataO接口实现加载HTML格式的文本数据。当开发者不需要加载整个页面,只需要显示一些页面片段时,可通过此功能来快速加载页面。
....
struct WebComponent {
webviewController: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
Button('loadUrl')
.onClick(() => {
this.webviewController.loadData(
"<html><body bgcolor=\"white\">Source:<pre>source</pre></body></html>",
"text/html",
"UTF-8"
),
})
// 组件创建时,加载www.example.com
Web({ src: 'www.example.com', controller: this.webviewController })
}
}
}
动态创建Web组件
支持命令式创建Web组件,这种方式创建的组件不会立即挂载到组件树,即不会对用户呈现(组件状态为Hidden和InActive),开发者可以在后续使用中按需动态挂载。后台启动的Web实例不建议超过200个。
// 载体Ability
// EntryAbility.ets
import { createNWeb} from "../pages/common"
onWindowStageCreate(windowStaeg: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err,data) => {
// 创建Web动态组件(需传入UIContext),loadContent之后的任意时机均可创建
createNWeb("https://www.example.com", windowStage.getMainWindowSync().getUIContext());
if(err.code){
return;
}
})
}
// 创建NodeController
// common.ets
import {webview} from '@kit.ArkWeb';
import { NodeController, BuilderNode, Size, FrameNode, UIContext} from '@kit.ArkUI'
// @Builder中为动态组件的具体组件内容
// Data为入参封装类
class Data {
url: string = "https://www.example.com";
controller: WebviewController = new webview.WebviewController();
}
@Builder
function WebBuilder(data: Data){
Column() {
Web({src: data.url, controller: data.controller})
.width("100%")
.height("100%")
}
}
let wrap = wrapBuilder<Data[]>(WebBuilder);
// 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用
export class myNodeController extends NodeController {
private rootnode: BuilderNode<Data[]> | null = null;
makeNode(uiContext: UIContext): FrameNode | null {
console.log("uicontext is undefined:" + (uiContext === undefined));
if(this.rootnode != null ){
/// 返回FrameNode节点
return this.rootnode.getFrameNode();
}
// 返回null控制动态组件脱离绑定节点
return null;
}
// 当布局大小发生变化时进行回调
aboutToDisappear() {
console.log("aboutToDisappear")
}
// 当controller对应的NodeContainer在Disappear的时候进行回调
aboutToDisapper() {
console.log("aboutToDisappear")
}
//此函数为自定义函数,可作为初始化函数使用
// 通过UIContext初始化BuilderNode,再通过BuilderNode中的builder接口初始化@Builder中的内容
initWeb(url: string, uiContext: UIContext: UIContext, controller: WebviewController){
if(this.rootnode != null){
return;
}
// 创建节点,需要uiContext
this.rootnode = new BuilderNode(uiContext)
// 创建动态Web组件
this.rootnode.build(wrap, {url: url, controller: controller})
}
}
export const createNWeb = (url: string, uiContext: UIContext) => {
//创建NodeController
let baseNode = new myNodeController();
let controller = new webview.WebviewController();
// 初始化自定义Web组件
baseNode.initWeb(url, uiContext, controller);
controllerMap.set(url, controller)
NodeMap.set(url, baseNode);
}
export const getNWeb = (url: string): myNodeController | undefined => {
return NodeMap.get(url);
}
// 使用NodeController的Page页
// Index.ets
import {getNWeb} from "./common"
@Entry
@Component
struct Index {
build() {
Row() {
Column() {
NodeContainer(getNWeb("https://www.example.com"))
.height("90%")
.width("100%")
}
.width('100%')
}
.height('100%')
}
}
通过结构化数据构建页面
在快速入门案例中, 知识地图页左侧的导航栏以及右侧的知识地图详情页,都是通过结构化数据渲染而来。
数据结构设计
知识地图列表项数据结构设计
NavBar构建
@Component
export struct KnowledgeMap {
// ...
@Builder
NavBar(order: number, title: string) {
Row() {
Text(order < 10 ? '0' + order : '' + order)
.margin({ right: $r('app.float.text_title_span') })
.fontFamily($r("app.string.font_bold"))
.fontSize($r('app.float.order_font_size'))
.fontColor($r('app.color.order_font_color'))
.textAlign(TextAlign.Start)
.lineHeight($r('app.float.order_line_height'))
.fontWeight(Constants.ORDER_FONT_WEIGHT)
Text(title)
.fontFamily($r('app.string.font_medium'))
.fontSize($r('app.float.fs16'))
.fontColor($r('app.color.order_font_color'))
.textAlign(TextAlign.Start)
.lineHeight(Constants.LINE_HEIGHT_22)
.fontWeight(Constants.FONT_WEIGHT_500)
Blank()
Image($r('app.media.ic_arrow'))
.width($r('app.float.right_arrow_width'))
.height($r('app.float.right_arrow_height'))
}
.backgroundColor(this.currentNavBar === order - 1 ? $r('app.color.nav_row_back_color') : Color.Transparent)
.borderRadius($r('app.float.br16'))
.alignItems(VerticalAlign.Center)
.width(Constants.FULL_WIDTH)
.height($r('app.float.nav_bar_height'))
.padding({ left: $r('app.float.nav_bar_span'), right: $r('app.float.nav_bar_span') })
}
build() {
}
}
List循环渲染NavBarItem:
...
@Component
export struct KnowledgeMap {
// ...
build() {
Navigation() {
Scroll() {
Column() {
// ...
List({ space: 12 }) {
ForEach(this.navBarList, (item: NavBarItemType, index: number) => {
ListItem() {
NavBarItem(item.order, item.title)
}.width('100%')
}, (item: NavBarItemType): string => item.title)
}
// ...
}
}
// ...
}
// ...
}
}
知识地图详情页
JSON数据
{
"title": "准备与学习",
"brief": "加入HarmonyOS生态,注册成为开发者,通过HarmonyOS课程了解基本概念和基础知识,轻松开启HarmonyOS的开发旅程。",
"materials": [
{
"subtitle": "HarmonyOS简介",
"knowledgeBase": [
{
"type": "准备",
"title": "注册账号"
},
{
"type": "准备",
"title": "实名认证"
},
{
"type": "学习与获取证书",
"title": "HarmonyOS第一课"
},
{
"type": "学习与获取证书",
"title": "HarmonyOS应用开发者基础认证"
}
]
},
{
"subtitle": "赋能套件介绍",
"knowledgeBase": [
{
"type": "指南",
"title": "开发"
},
{
"type": "指南",
"title": "最佳实践"
},
{
"type": "指南",
"title": "API参考"
}
]
}
]
}
知识地图详情页:页面结构抽象
分析:




数据接口定义
interface Section {
title: string,
brief: string,
materials: Material[]
}
interface Material {
subtitle: string,
knowledgeBase: KnowledgeBaseItem[]
}
interface KnowledgeBaseItem {
icon?: Resource | string,
title: string,
type: string
}
代码实现
@Builder
KnowledgeBlockLine(knowledgeBaseItem: KnowledgeBaseItem) {
Row() {
Image($r(TYPE_MAP_ICON[knowledgeBaseItem.type]))
.width($r('app.float.type_icon_size'))
.height($r('app.float.type_icon_size'))
Column() {
Text(knowledgeBaseItem.title)
.fontFamily($r('app.string.font_medium'))
.fontSize($r('app.float.fs16'))
.fontWeight(CommonConstants.FONT_WEIGHT_500)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(1)
Text(knowledgeBaseItem.type)
.fontFamily($r('app.string.font_normal'))
.fontSize($r('app.float.fs14'))
.fontWeight(CommonConstants.FONT_WEIGHT_400)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: $r('app.float.title_type_margin_left') })
Image($r('app.media.ic_arrow'))
.width($r('app.float.right_arrow_width'))
.height($r('app.float.right_arrow_height'))
}
.width(CommonConstants.FULL_SCREEN)
.height($r('app.float.content_line_height'))
.alignItems(VerticalAlign.Center)
.onClick(() => {
const now = Date.now();
if (now - this.lastCall < 3000) {
return;
}
this.lastCall = now;
promptAction.showToast({
message: '当前知识地图仅供参考,具体内容请查阅官网',
duration: 2000
})
})
}
@Builder
KnowledgeBlock(material: Material) {
Column() {
Text(material.subtitle)
.fontFamily($r('app.string.font_medium'))
.fontSize($r('app.float.fs14'))
.fontWeight(CommonConstants.FONT_WEIGHT_500)
.margin({ bottom: $r('app.float.back_icon_margin_right') })
List() {
ForEach(material.knowledgeBase, (item: KnowledgeBaseItem, index: number) => {
this.KnowledgeBlockLine(item)
}, (item: KnowledgeBaseItem, index: number) => item.title + index)
}
.lanes(new BreakpointType<number>({
sm: Constants.LANES_ONE,
md: Constants.LANES_ONE,
lg: Constants.LANES_TWO,
xl: Constants.LANES_TWO
}).getValue(this.currentBreakpoint),
$r('app.float.nav_bar_span'),
)
.backgroundColor(Color.White)
.borderRadius($r('app.float.br16'))
.padding({ left: $r('app.float.nav_bar_span'), right: $r('app.float.nav_bar_span') })
.divider({
strokeWidth: Constants.STROKE_WIDTH,
startMargin: Constants.START_MARGIN,
endMargin: Constants.END_MARGIN,
color: $r('app.color.divider_color')
})
}
.width(CommonConstants.FULL_SCREEN)
.alignItems(HorizontalAlign.Start)
}
@Builder
KnowledgeBlock(material: Material) {
Column() {
Text(material.subtitle)
.fontFamily($r('app.string.font_medium'))
.fontSize($r('app.float.fs14'))
.fontWeight(CommonConstants.FONT_WEIGHT_500)
.margin({ bottom: $r('app.float.back_icon_margin_right') })
List() {
ForEach(material.knowledgeBase, (item: KnowledgeBaseItem, index: number) => {
this.KnowledgeBlockLine(item)
}, (item: KnowledgeBaseItem, index: number) => item.title + index)
}
.lanes(new BreakpointType<number>({
sm: Constants.LANES_ONE,
md: Constants.LANES_ONE,
lg: Constants.LANES_TWO,
xl: Constants.LANES_TWO
}).getValue(this.currentBreakpoint),
$r('app.float.nav_bar_span'),
)
.backgroundColor(Color.White)
.borderRadius($r('app.float.br16'))
.padding({ left: $r('app.float.nav_bar_span'), right: $r('app.float.nav_bar_span') })
.divider({
strokeWidth: Constants.STROKE_WIDTH,
startMargin: Constants.START_MARGIN,
endMargin: Constants.END_MARGIN,
color: $r('app.color.divider_color')
})
}
.width(CommonConstants.FULL_SCREEN)
.alignItems(HorizontalAlign.Start)
}
设置组件导航
Navigation组件
基础概念
Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,可以实现路由进行切换。
知识地图首页切换到知识地图详情页由Navigation实现
Navigation组件的组成
单页面模式

首页 非首页
- 首页页面结构
- 标题栏:用于标识出整个页面的主题,支持两种类型的标题栏,也可以用Navigation的hideTitleBar属性隐藏标题栏
- 菜单栏:支持将一些常用功能都放入菜单栏,开发者可以根据menus属性进行设置
- 内容区:主要用于显示一些界面内容以及导航栏,点击导航栏会跳转到该导航栏所对应的内容区,实现页面的切换
- 导航栏:最重要的部分
- 工具栏:可以使用toolbarConfiguration属性进行设置
- 非首页页面结构(NavDestination)
- 标题栏:同样用于标识整内容区页面的主题,开发者可以使用NavDestination的title属性来控制标题栏的显示内容,也可以用Navigation的hideTitleBar属性控制标题栏的显示
- 菜单栏:与首页的功能定义一致
- 内容区:显示首页中当前导航栏对应的内容
分栏模式
使用Navigation组件时,若设置Navigation组件的mode属性为Navigation.Auto,那么当设备的宽度大于520vp时,Navigation组件会采用分栏模式。
标题栏
标题栏在界面顶部,用于呈现界面名称和操作入口,Navigation组件通过titleMode属性设置标题栏模式。支持两种显示模式:
- Mini模式:普通型标题栏,用于一级页面不需要突出标题的场景
Naviagtion() {
.....
}
.titleMode(NavigationTitleMode.Mini)
- Full模式:强调型标题栏,用于一级页面需要突出标题的场景。
Naviagtion() {
.....
}
.titleMode(NavigationTitleMode.Full)
菜单栏
菜单栏位于Navigation组件的右上角,开发者可以通过menus属性进行设置。
menus支持Array<NavigationMenuItem>和CustonBuilder两种参数类型。
在使用Array<NavigationMenuItem>类型时,竖屏最多支持显示3个图标,横屏最多支持显示5个图标,多余的图标会被放入自动生成的更多图标。
let menuItem1: NavigationMenuItem = {'value': '', 'icon': './image/ic_search.svg', 'action': ()=> {} };
let menuItem1: NavigationMenuItem = {'value': '', 'icon': './image/ic_add.svg', 'action': ()=> {} };
Navigation(){
......
}
.menus([meunItem1, menuItem2, menuItem2])

工具栏
工具栏位于Navigation组件的底部开发者可以通过toolbarConfiguration属性进行设置。
该属性需要传入一个ToolbarItem构成的数组。
let toolItem: ToolbarItem = {'value': 'func', 'icon': './image/ic_public_highlights.svg', 'action': ()=> {} };
let toolBar: ToolbarItem[] = [toolItem, toolItem, toolItem]
Navigation() {
...
}
.toolbarConfiguration(toolbar)

模块内页面切换
在使用Navigation组件中,非常重要的一个点就是依靠Navigation组件提供的组件级路由能力实现更加自然流畅的转场体验,而这就需要依靠路由栈提供的系列方法。
官方文档:NavPathStack-Navigation路由栈
- push系列方法:将指定的页面栈数据信息入栈
pushPath(info: NavPathInfo, animated?: boolean): voidpushPathByName(name: string, param: unknown, animated?: boolean): voidpushPathByName(name: string, param: Object,...)pushDestination(info: NavPathInfo, animated?: boolean):Promise<void>
- replace系列方法:将当前页面栈栈顶退出,再将指定的
NavDestination页面信息入栈。replacePath(info: NavPathInfo, animated?: boolean): voidreplacePathByName(name: string, param: Object, animated?: boolean): void
以pushPathByName为例
interface Param{
name: string
}
const param5: Param = { name: 'test'};
this.pageInfos.pushPathByName('Page5', param5)
→ 
以replacePathByName为例
interface Param{
name: string
}
const param1: Param = { name: 'test'};
this.pageInfos.replacePathByName('Page1', param5)
→ 
路由传参与路由参数获取

getParamByIndex(index: number): unknown | undefined- 获取index指定的NavDestination页面的参数信息。

getParamByName(name: string): Array<unknown>- 获取全部名为name的NavDestination页面的参数信息

Tabs组件
简介
当页面信息较多时,为了让用户能够聚焦于当前显示的内容,需要对页面内容进行分类,提高页面空间利用率。Tabs组件可以在一个页面内快速实现视图内容的切换,一方面提升查找信息的效率,另一方面精简用户单次获取到的信息量。Tabs(value?: {barPosition?: BarPosition, index?: number, controller?: TabsController})
- barPosition:设置Tabs的页签位置。
- index:设置当前显示页签的索引。
- controller:设置Tabs控制器。
Tabs组件的使用
界面结构
TabBar按照不同的导航栏类型,可以分为底部导航、顶部导航、侧边导航。其导航栏分别位于底部、顶部和侧边。
controller
Tabs参数所传入的TabsController实例可以对Tabs显示的TabContent区域进行控制,控制方式是使用实例的changeIndex方法,该方法需要传入一个数值,能控制Tabs显示到对应的索引。
- 关联controller与Tabs
struct Index {
private tabsController: TabsController = new TabsController();
// 关联controller
Tabs({ controller: tabsController}) {
...
}
}
- 控制Tabs的TabContent区域显示的内容
this.tabsController.chanageIndex(1)
自定义TabBar
对于底部导航栏,一般作为应用主要页面功能区分,为了更好的用户体验,会组合文字以及对应语义图标标识页签内容,这种情况下,需要自定义当行列页签的样式
@Builder
tabBarBuilder(
title:string,
targetIndex:number,
selectedIcon: Resource,
unselectIcon: Resource
) {
Column() {
Image(this.currentIndexX === targetIndex? selectedIcon : unselectIcon)
.width(24)
.height(24)
Text(title)
.fontFamily('HarmonyHeiTi-Medium')
.fontsize(10)
.fontColor(this.currentIndex === targetIndex? '#0A59F7' : '#99000000')
.textAlign(TextAlign.Center)
.lineHeight(14)
.fontWeight(500)
}
}
实操案例
Navigation实操
创建相关数据
@Component
export struct KnowledgeMap {
// 创建路由栈
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();
// 创建NavDestination
@Builder
PageMap(name: string, param: Section) {
if (name === 'KnowledgeMapContent') {
KnowledgeMapContent();
}
}
build() {
}
}
绑定到Navigation,并添加点击事件实现路由跳转
@Component
export struct KnowledgeMap {
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();
@Builder
PageMap(name: string, param: Section) {
if (name === 'KnowledgeMapContent') {
KnowledgeMapContent();
}
}
@Builder
NavBarItem(order: string, title: string) {
// ...
}
// 添加点击事件实现路由跳转
.onClick(() => {
const index = Number(order) - 1;
this.pageInfos.pushPathByName('KnowledgeMapContent', this.sections[index]);
})
// ...
build() {
// 绑定路由栈
Navigation(this.pageInfos) {
//...
List({ space: 12 }) {
ForEach(this.navBarList, (item: NavBarItemType, index: number) => {
ListItem() {
this.NavBarItem(item.order, item.title)
}
}, (item: NavBarItemType): string => item.title)
}.width('100%')
// ...
}
// 绑定NavDestination
.navDestination(this.PageMap)
}
}
路由子组件获取数据
export struct KnowledgeMapContent {
@Consume('pageInfos') pageInfos: NavPathStack;
@State section: Section | null = null
aboutToAppear(): void {
const size = this.pageInfos.size();
this.section = this.pageInfos.getParamByIndex(size - 1) as Section;
}
}
Tabbs实操-使用自定义tabBar
@Entry
@Component
struct Index {
private tabsController: TabsController = new TabsController();
@Builder
tabBarBuilder(
title: string,
targetIndex: number,
selectedIcon: Resource,
unselectIcon: Resource
) {
Column() {
Image(this.currentIndex === targetIndex ? selectedIcon : unselectIcon).width(24).height(24)
Text(title)
.fontFamily('HarmonyHeiTi-Medium')
.fontSize(10)
.fontColor(this.currentIndex === targetIndex ? '#0A59F7' : '#99000000')
.textAlign(TextAlign.Center)
.lineHeight(14)
.fontWeight(500)
}
.onClick(() => {
this.currentIndex = targetIndex;
this.tabsController.changeIndex(targetIndex);
})
}
build() {
Tabs({ barPosition: BarPosition.End, controller: this.controller }) {
TabContent() {
QuickStartPage()
}.tabBar(this.tabBarBuilder('快速入门', 0, $r('app.media.ic_01_on'), $r('app.media.ic_01_off')))
TabContent() {
CourseLearning()
}.tabBar(this.tabBarBuilder('课程学习', 1, $r('app.media.ic_01_on'), $r('app.media.ic_01_off')))
TabContent() {
KnowledgeMap()
}.tabBar(this.tabBarBuilder('知识地图', 2, $r('app.media.ic_01_on'), $r('app.media.ic_01_off')))
}
}
}
更多推荐


所有评论(0)