本章聚焦HarmonyOS应用开发中ArkUI的页面布局。开篇介绍ArkTS与ArkUI,对比ArkTS和标准TS的差异,阐述 ArkUI的开发范式与架构。接着深入讲解UI范式基本语法,包括组件创建、属性配置等。随后介绍像素单位、各类组件(文本、布局、图片、按钮等)的使用方法。最后通过仿猫眼电影M站首页布局案例,巩固知识。助力读者快速掌握ArkUI页面布局开发技巧,为HarmonyOS应用开发筑牢基础。
2.1 ArKTS与ArkUI介绍
本节介绍了ArkTS和ArkUI。ArkTS是HarmonyOS的优选开发语言,在TypeScript的基础上进行了扩展,并强化了静态检查。ArkUI提供UI开发基础设施,支持声明式和类Web两种开发范式。此外,本节还阐述了 ArkUI的架构及声明式UI语法特点,为后续学习奠定基础。
2.1.1 ArkTS简介
ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript(简称TS)生态基础上做了进一步扩展,保持了TS的基本风格,同时通过规范定义强化开发期静态检查和分析,提升程序执行稳定性和性能。
从API version 10开始,ArkTS进一步通过规范强化静态检查和分析,对比标准TS的差异例如:
- 强制使用静态类型:静态类型是ArkTS最重要的特性之一。如果使用静态类型,那么程序中变量的类型就是确定的。同时,由于所有类型在程序实际运行前都是已知的,编译器可以验证代码的正确性,从而减少运行时的类型检查,有助于性能提升。
- 禁止在运行时改变对象布局:为实现最大性能,ArkTS要求在程序执行期间不能更改对象布局。
- 限制运算符语义:为获得更好的性能并鼓励开发者编写更清晰的代码,ArkTS限制了一些运算符的语义。比如,一元加法运算符只能作用于数字,不能用于其他类型的变量。
- 不支持Structural typing:对Structural typing的支持需要在语言、编译器和运行时进行大量的考虑和仔细的实现,当前ArkTS不支持该特性。
当前,在UI开发框架中,ArkTS主要扩展了如下能力:
- 基本语法:ArkTS定义了声明式UI描述、自定义组件和动态扩展UI元素的能力,再配合ArkUI开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了UI开发的主体。
- 状态管理:ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活地利用这些能力来实现数据和UI的联动。
- 渲染控制:ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。
ArkTS兼容TS/JavaScript(简称JS)生态,开发者可以使用TS/JS进行开发或复用已有代码。
未来,ArkTS会结合应用开发/运行的需求持续演进,逐步提供并行和并发能力增强、系统类型增强、分布式开发范式等更多特性。
2.1.2 ArkUI简介
ArkUI(方舟UI框架)为应用的UI开发提供了完整的基础设施,包括简洁的UI语法、丰富的UI功能(组件、布局、动画以及交互事件),以及实时界面预览工具等,可以支持开发者进行可视化界面开发。
2.1.2.1 基本概念
- UI: 即用户界面。开发者可以将应用的用户界面设计为多个功能页面,每个页面进行单独的文件管理,并通过页面路由API完成页面间的调度管理如跳转、回退等操作,以实现应用内的功能解耦。
- 组件: UI构建与显示的最小单位,如列表、网格、按钮、单选框、进度条、文本等。开发者通过多种组件的组合,构建出满足自身应用诉求的完整界面。
2.1.2.2 两种开发范式
针对不同的应用场景及技术背景,方舟UI框架提供了两种开发范式,分别是基于ArkTS的声明式开发范式(简称"声明式开发范式")和兼容JS的类Web开发范式(简称"类Web开发范式")。
- 声明式开发范式:采用基于TypeScript声明式UI语法扩展而来的ArkTS语言,从组件、动画和状态管理三个维度提供UI绘制能力。
- 类Web开发范式:采用经典的HML、CSS、JavaScript三段式开发方式,即使用HML标签文件搭建布局、使用CSS文件描述样式、使用JavaScript文件处理逻辑。该范式更符合于Web前端开发者的使用习惯,便于快速将已有的Web应用改造成方舟UI框架应用。
在开发一款新应用时,推荐采用声明式开发范式来构建UI,主要基于以下几点考虑:
- 开发效率: 声明式开发范式更接近自然语义的编程方式,开发者可以直观地描述UI,无需关心如何实现UI绘制和渲染,开发高效简洁。
- 应用性能: 如下图所示,两种开发范式的UI后端引擎和语言运行时是共用的,但是相比类Web开发范式,声明式开发范式无需JS框架进行页面DOM管理,渲染更新链路更为精简,占用内存更少,应用性能更佳。
- 发展趋势:声明式开发范式后续会作为主推的开发范式持续演进,为开发者提供更丰富、更强大的能力。
2.1.2.3 不同应用类型支持的开发范式
根据所选用应用模型(Stage模型、FA模型)和页面形态(应用或服务的普通页面、卡片)的不同,对应支持的UI开发范式也有所差异,详见下表。
应用模型 | 页面形态 | 支持的UI开发范式 |
---|---|---|
Stage模型(推荐) | 应用或服务的页面 | 声明式开发范式(推荐) |
卡片 | 声明式开发范式(推荐)类Web开发范式 | |
FA模型 | 应用或服务的页面 | 声明式开发范式类Web开发范式 |
卡片 | 类Web开发范式 |
2.1.3 ArkUI整体架构
下图为ArkUI的整体架构图。
- 声明式UI前端
提供了UI开发范式的基础语言规范,并提供内置的UI组件、布局和动画,提供了多种状态管理机制,为应用开发者提供一系列接口支持。
- 语言运行时
选用方舟语言运行时,提供了针对UI范式语法的解析能力、跨语言调用支持的能力和TS语言高性能运行环境。
- 声明式UI后端引擎
后端引擎提供了兼容不同开发范式的UI渲染管线,提供多种基础组件、布局计算、动效、交互事件,提供了状态管理和绘制能力。
- 渲染引擎
提供了高效的绘制能力,将渲染管线收集的渲染指令,绘制到屏幕的能力。
- 平台适配层
提供了对系统平台的抽象接口,具备接入不同系统的能力,如系统渲染管线、生命周期调度等。
2.1.4 声明式UI语法
- 命令式UI操作
以下是传统的Web前端原生代码的语法:
html
<!-- mycomponent.html -->
<div class="column">
<text class="title">
Hello HarmonyOS
</text>
</div>
css
/* mycomponent.css */
.column {
flex-direction: column;
justify-content: center;
}
.title {
font-size: 38px;
color: "red";
}
js
// mycomponent.js
var b = findViewById(title)
b.setOnClickListener(() => {
b.setColor(Color.RED)
});
- 数据驱动的声明式UI
以下是ArkUI的数据驱动声明式UI的语法:
ts
@State mText = 'Hello HarmonyOS'
// ...
Column() {
Text(this.mText)
.fontSize(this.mFontSize)
.color(Color.red)
.onClick(() => {
this.mText = 'Hello ArkUI'
})
}
- 声明式UI和命令式UI的特点
- 从命令式到声明式的转变
声明式UI更注重对界面的抽象描述,而非具体的实现方案,强调数据与状态的定义,采用声明式的数据绑定,并融入组件化与组合的设计思想,使界面构建更加直观、高效、可维护。如下代码所示:
ts
@Entry
@Component
struct Page {
@State title: string = 'World' // 强调数据和状态的定义
build() { // 关注UI的抽象描述而非解决方案
Row() {
Column() {
Text(`Hello ${this.title}`) // 声明式的数据绑定
.fontSize(50)
.fontWeight(FontWeight.Bold)
Divider()
Button('click me') // 组件化和组合思想
.onClick(() => {
this.title = 'ArkUI'
})
.height(50)
.width(100)
.margin({ top: 20 })
}
.width('100%')
}
.height('100%')
}
}
2.4.5 数据驱动UI原理
View(UI):UI渲染,一般指自定义组件的build方法和@Builder装饰的方法内的UI描述。
State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。
下图展示了状态(State)与用户界面(UI)之间的关系,体现了数据驱动UI的核心概念。UI的呈现形式是由当前的状态数据决定的。当状态数据发生变化时,通过这个函数映射关系,UI会相应地更新展示内容,反映最新的状态。这种理念在现代前端开发框架中广泛应用,有助于实现高效、可维护的UI更新机制。
下图清晰地阐述了ArkUI框架中"数据驱动UI"的核心工作原理。下面将从几个关键方面进行详细解析:
- 首次渲染过程
当组件首次渲染时,框架会执行以下步骤:
- 调用组件的build()方法渲染系统组件
- 如果存在自定义子组件,会创建这些子组件的实例
- 在build()执行过程中,框架会观察所有状态变量的读取状态,通过Proxy/响应式系统拦截状态变量的get和set操作
- 数据与UI的绑定机制
从图中可以看到三个核心部分:
- 数据层:
-
- 包含实际数据(如aVar=7)
- 通过Proxy代理(getter/setter)实现对数据的拦截
- UI层:
-
- UI结构(如Text('(aVar)')显示变量值)
- UI逻辑(如点击事件click () => aVar = 10修改数据)
- 依赖收集系统:
-
- 维护一个"组件数据依赖表"
- 记录状态变量与UI组件之间的依赖关系
- 依赖关系的建立
框架会建立两种关键映射关系:
- 状态变量 → UI组件:记录哪些UI组件(包括ForEach和if条件块)依赖了哪些状态变量
- UI组件 → 更新函数:每个UI组件对应一个lambda方法(可以看作是build()函数的子集),专门用于创建和更新该组件
- 更新流程
当数据变化时(如aVar从7变为10),系统会:
- 通过setter拦截到变化
- 查询"组件数据依赖表"找到所有依赖这个变量的UI组件
- 执行这些组件对应的更新函数(而非整个build方法)
- 实现最小化更新 - 只更新真正需要变化的部分
- 关键优势
这种架构带来了几个重要优势:
- 高效更新:避免了整个组件树的重新渲染
- 精确更新:只更新数据变化影响的UI部分
- 声明式编程:开发者只需声明UI应该如何响应数据变化,而不需要手动操作UI
2.2 UI范式基本语法
本节讲解UI范式的基本语法。首先概述ArkTS的核心组成,包括装饰器和UI描述等。随后,从创建组件、配置属性、配置事件和配置子组件四个方面,详细解析声明式UI描述,帮助你熟练掌握其使用方法。
2.2.1 基本语法概述
在初步了解了ArkTS语言之后,我们以一个具体的示例来说明ArkTS的基本组成。如下图所示,当开发者点击按钮时,文本内容从"Hello World"变为"Hello ArkUI"。
本示例中,ArkTS的基本组成如下所示。
- 装饰器: 用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如上述示例中@Entry、@Component和@State都是装饰器,@Component表示自定义组件,@Entry表示该自定义组件为入口组件,@State表示组件中的状态变量,状态变量变化会触发UI刷新。
- UI描述:以声明式的方式来描述UI的结构,例如build()方法中的代码块。
- 自定义组件:可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello。
- 系统组件:ArkUI框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的Column、Text、Divider、Button。
- 属性方法:组件可以通过链式调用配置多项属性,如fontSize()、width()、height()、backgroundColor()等。
- 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,如跟随在Button后面的onClick()。
2.2.2 声明式UI描述
ArkTS以声明方式组合和扩展组件来描述应用程序的UI,同时还提供了基本的属性、事件和子组件配置方法,帮助开发者实现应用交互逻辑。
2.2.2.1 创建组件
根据组件构造方法的不同,创建组件包含有参数和无参数两种方式。
- 无参数
如果组件的接口定义没有包含必选构造参数,则组件后面的"()"不需要配置任何内容。例如,Divider组件不包含构造参数。
ts
Column() {
Text('item 1')
Divider()
Text('item 2')
}
- 有参数
如果组件的接口定义包含构造参数,则在组件后面的"()"需要配置相应参数。
- Image组件的必选参数src。
ts
Image('https://xyz/test.jpg')
- Text组件的非必选参数content。
ts
// string类型的参数
Text('test')
// $r形式引入应用资源,可应用于多语言场景
Text($r('app.string.title_value'))
// 无参数形式
Text()
- 变量或表达式也可以用于参数赋值,其中表达式返回的结果类型必须满足参数类型要求。
例如,设置变量或表达式来构造Image和Text组件的参数。
kotlin
Image(this.imagePath)
Image('https://' + this.imageUrl)
Text(`count: ${this.count}`)
2.2.2.2 配置属性
属性方法以"."链式调用的方式配置系统组件的样式和其他属性,建议每个属性方法单独写一行。
- 配置Text组件的字体大小。
ts
Text('test')
.fontSize(12)
- 配置组件的多个属性。
ts
Image('test.jpg')
.alt('error.jpg')
.width(100)
.height(100)
- 除了直接传递常量参数外,还可以传递变量或表达式。
ts
Text('hello')
.fontSize(this.size)
Image('test.jpg')
.width(this.count % 2 === 0 ? 100 : 200)
.height(this.offset + 100)
- 对于系统组件,ArkUI还为其属性预定义了一些枚举类型供开发者调用,枚举类型可以作为参数传递,但必须满足参数类型要求。
例如,可以按以下方式配置Text组件的颜色和字体样式。
ts
Text('hello')
.fontSize(20)
.fontColor(Color.Red)
.fontWeight(FontWeight.Bold)
2.2.2.3 配置事件
事件方法以"."链式调用的方式配置系统组件支持的事件,建议每个事件方法单独写一行。
- 使用箭头函数配置组件的事件方法。
ts
Button('Click me')
.onClick(() => {
this.myText = 'ArkUI'
})
- 使用箭头函数表达式配置组件的事件方法,要求使用"() => {...}",以确保函数与组件绑定,同时符合ArkTS语法规范。
ts
Button('add counter')
.onClick(() => {
this.counter += 2
})
- 使用组件的成员函数配置组件的事件方法,需要bind this。ArkTS语法不推荐使用成员函数配合bind this去配置组件的事件方法。
ts
myClickHandler(): void {
this.counter += 2
}
// ...
Button('add counter')
.onClick(this.myClickHandler.bind(this))
- 使用声明的箭头函数,可以直接调用,不需要bind this。
ts
fn = () => {
console.info(`counter: ${this.counter}`)
this.counter++
}
// ...
Button('add counter')
.onClick(this.fn)
2.2.2.4 配置子组件
如果组件支持子组件配置,则需在尾随闭包"{...}"中为组件添加子组件的UI描述。Column、Row、Stack、Grid、List等组件都是容器组件。
- 以下是简单的Column组件配置子组件的示例。
ts
Column() {
Text('Hello')
.fontSize(100)
Divider()
Text(this.myText)
.fontSize(100)
.fontColor(Color.Red)
}
- 容器组件均支持子组件配置,可以实现相对复杂的多级嵌套。
ts
Column() {
Row() {
Image('test1.jpg')
.width(100)
.height(100)
Button('click +1')
.onClick(() => {
console.info('+1 clicked!')
})
}
}
2.3 虚拟像素单位(vp)
ArkUI为开发者提供4种像素单位,采用vp为基准数据单位。
名称 | 描述 |
---|---|
px | 屏幕物理像素单位。 |
vp | 屏幕密度相关像素,根据屏幕像素密度转换为屏幕物理像素,当数值不带单位时,默认单位vp。 |
fp | 字体像素,与vp类似适用屏幕密度变化,随系统字体大小设置变化。 |
lpx | 视窗逻辑像素单位,lpx单位为实际屏幕宽度与逻辑宽度(通过module.json5的designWidth配置)的比值,designWidth默认值为720。当designWidth为720时,在实际宽度为1440物理像素的屏幕上,1lpx为2px大小。 |
在实际布局时使用最多的单位是vp,它提供了一种灵活的方式来适应不同屏幕密度的显示效果。如下图所示:使用虚拟像素vp,使元素在不同密度的设备上具有一致的视觉体验。
2.3.1 像素单位转换
下表列出了其他单位与px单位之间的转换方法。
接口 | 描述 |
---|---|
vp2px(value : number) : number | 将vp单位的数值转换为以px为单位的数值。 |
px2vp(value : number) : number | 将px单位的数值转换为以vp为单位的数值。 |
fp2px(value : number) : number | 将fp单位的数值转换为以px为单位的数值。 |
px2fp(value : number) : number | 将px单位的数值转换为以fp为单位的数值。 |
lpx2px(value : number) : number | 将lpx单位的数值转换为以px为单位的数值。 |
px2lpx(value : number) : number | 将px单位的数值转换为以lpx为单位的数值。 |
2.3.2 使用示例
以下示例代码展示了像素单位的转换过程。
ts
@Entry
@Component
struct Index {
build() {
Column({space: 10}) {
Column() {
Text('width(220)')
.width(220)
.height(40)
.backgroundColor(0x6435c9)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.fontSize('12vp')
}
Column() {
Text('width(220vp)')
.width('220vp')
.height(40)
.backgroundColor(0x6435c9)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.fontSize('12vp')
}
Column() {
Text('width("220lpx") designWidth:720')
.width('220lpx')
.height(40)
.backgroundColor(0x6435c9)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.fontSize('12vp')
}
Column() {
Text('fontSize(12fp)')
.width(220)
.height(40)
.backgroundColor(0x6435c9)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.fontSize('12fp')
}
Column() {
Text('width (vp2px(220) px)')
// 建议使用this.getUIContext().vp2px()
.width(vp2px(220) + 'px')
.height(40)
.backgroundColor(0x6435c9)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.fontSize('12fp')
}
Column() {
Text('width (px2vp(220) vp)')
.width((this.getUIContext()).px2vp(220))
.height(40)
.backgroundColor(0x6435c9)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.fontSize('12fp')
}
}
.alignItems(HorizontalAlign.Start)
.width('100%')
}
}
这里,直接使用vp2px/px2vp/fp2px/px2fp/lpx2px/px2lpx可能存在UI上下文不明确的问题,建议使用getUIContext获取UIContext实例,再使用UIContext下的vp2px/px2vp/fp2px/px2fp/lpx2px/px2lpx调用绑定实例的接口。
各终端设备的运行效果如下所示。
2.4 文本显示组件(Text/Span)
Text是文本组件,通常用于展示用户视图,如显示文章的文字内容。Span则用于呈现显示行内文本。
2.4.1 创建文本
Text可通过以下两种方式来创建:
- string字符串。
scss
Text('我是一段文本')
- 引用Resource资源。
资源引用类型可以通过$r创建Resource类型对象,文件位置为/resources/base/element/string.json。
ts
Text($r('app.string.module_desc'))
.baselineOffset(0)
.fontSize(30)
.border({ width: 1 })
.padding(10)
.width(300)
2.4.2 添加子组件
Span只能作为Text的子组件显示文本内容。可以在一个Text内添加多个Span来显示一段信息,例如产品说明书、承诺书等。
- 创建Span。
Span组件必须嵌入在Text组件中才能显示,单独的Span组件不会呈现任何内容。Text与Span同时配置文本内容时,Span内容覆盖Text内容。
ts
Text('我是Text') {
Span('我是Span')
}
.padding(10)
.borderWidth(1)
- 添加事件。
由于Span组件无尺寸信息,事件仅支持添加点击事件onClick。
ts
Text() {
Span('I am Upper-span').fontSize(12)
.textCase(TextCase.UpperCase)
.onClick(() => {
console.info('我是Span------onClick')
})
}
2.4.3 自定义文本样式
- 通过textAlign属性设置文本对齐样式。
ts
Text('左对齐')
.width(300)
.textAlign(TextAlign.Start)
.border({ width: 1 })
.padding(10)
Text('中间对齐')
.width(300)
.textAlign(TextAlign.Center)
.border({ width: 1 })
.padding(10)
Text('右对齐')
.width(300)
.textAlign(TextAlign.End)
.border({ width: 1 })
.padding(10)
- 通过textOverflow属性控制文本超长处理,textOverflow需配合maxLines一起使用(默认情况下文本自动折行)。
ts
Text('This is the setting of textOverflow to Clip text content ' +
'This is the setting of textOverflow to None text content. ' +
'This is the setting of textOverflow to Clip text content ' +
'This is the setting of textOverflow to None text content.')
.width(250)
.textOverflow({ overflow: TextOverflow.None })
.maxLines(1)
.fontSize(12)
.border({ width: 1 })
.padding(10)
Text('我是超长文本,超出的部分显示省略号。I am an extra long text, ' +
'with ellipses displayed for any excess。')
.width(250)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(1)
.fontSize(12)
.border({ width: 1 })
.padding(10)
Text('当文本溢出其尺寸时,文本将滚动显示。When the text overflows ' +
'its dimensions, the text will scroll for displaying.')
.width(250)
.textOverflow({ overflow: TextOverflow.MARQUEE })
.maxLines(1)
.fontSize(12)
.border({ width: 1 })
.padding(10)
- 通过lineHeight属性设置文本行高。
ts
Text('This is the text with the line height set. ' +
'This is the text with the line height set.')
.width(300).fontSize(12).border({ width: 1 }).padding(10)
Text('This is the text with the line height set. ' +
'This is the text with the line height set.')
.width(300).fontSize(12).border({ width: 1 }).padding(10)
.lineHeight(20)
2.4.4 添加事件
Text组件可以添加通用事件,可以绑定onClick、onTouch等事件来响应操作。
ts
Text('点我')
.onClick(() => {
console.info('我是Text的点击响应事件')
})
2.4.5 场景示例
该示例通过maxLines、textOverflow、textAlign、constraintSize属性展示了热搜榜的效果。
ts
@Entry
@Component
struct TextExample {
build() {
Column() {
Row() {
Text("1")
.fontSize(14).fontColor(Color.Red)
.margin({ left: 10, right: 10 })
Text("我是热搜词条1")
.fontSize(12)
.fontColor(Color.Blue)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.fontWeight(300)
Text("爆")
.margin({ left: 6 })
.textAlign(TextAlign.Center)
.fontSize(10)
.fontColor(Color.White)
.fontWeight(600)
.backgroundColor(0x770100)
.borderRadius(5)
.width(15)
.height(14)
}.width('100%').margin(5)
Row() {
Text("2").fontSize(14).fontColor(Color.Red)
.margin({ left: 10, right: 10 })
Text("我是热搜词条2 我是热搜词条2 我是热搜词条2 我是热搜词条2 我是热搜词条2")
.fontSize(12)
.fontColor(Color.Blue)
.fontWeight(300)
.constraintSize({ maxWidth: 200 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text("热")
.margin({ left: 6 })
.textAlign(TextAlign.Center)
.fontSize(10)
.fontColor(Color.White)
.fontWeight(600)
.backgroundColor(0xCC5500)
.borderRadius(5)
.width(15)
.height(14)
}.width('100%').margin(5)
Row() {
Text("3").fontSize(14).fontColor(Color.Orange)
.margin({ left: 10, right: 10 })
Text("我是热搜词条3")
.fontSize(12)
.fontColor(Color.Blue)
.fontWeight(300)
.maxLines(1)
.constraintSize({ maxWidth: 200 })
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text("热")
.margin({ left: 6 })
.textAlign(TextAlign.Center)
.fontSize(10)
.fontColor(Color.White)
.fontWeight(600)
.backgroundColor(0xCC5500)
.borderRadius(5)
.width(15)
.height(14)
}.width('100%').margin(5)
Row() {
Text("4").fontSize(14).fontColor(Color.Grey)
.margin({ left: 10, right: 10 })
Text("我是热搜词条4 我是热搜词条4 我是热搜词条4 我是热搜词条4 我是热搜词条4")
.fontSize(12)
.fontColor(Color.Blue)
.fontWeight(300)
.constraintSize({ maxWidth: 200 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}.width('100%').margin(5)
}.width('100%')
}
}
2.5 线性布局(Row/Column)
线性布局(LinearLayout)是开发中最常用的布局,通过线性容器Row和Column构建。线性布局是其他布局的基础,其子元素在线性方向上(水平方向和垂直方向)依次排列。线性布局的排列方向由所选容器组件决定,Column容器内子元素按照垂直方向排列,Row容器内子元素按照水平方向排列。根据不同的排列方向,开发者可选择使用Row或Column容器创建线性布局。
2.5.1 基本概念
- 布局容器:具有布局能力的容器组件,可以承载其他元素作为其子元素,布局容器会对其子元素进行尺寸计算和布局排列。
- 布局子元素:布局容器内部的元素。
- 主轴:线性布局容器在布局方向上的轴线,子元素默认沿主轴排列。Row容器主轴为水平方向,Column容器主轴为垂直方向。
- 交叉轴:垂直于主轴方向的轴线。Row容器交叉轴为垂直方向,Column容器交叉轴为水平方向。
- 间距:布局子元素的间距。
2.5.2 布局子元素在排列方向上的间距
在布局容器内,可以通过space属性设置排列方向上子元素的间距,使各子元素在排列方向上有等间距效果。
2.5.2.1 Column容器内排列方向上的间距
scss
Column({ space: 20 }) {
Text('space: 20').fontSize(15).fontColor(Color.Gray).width('90%')
Row().width('90%').height(50).backgroundColor(0xF5DEB3)
Row().width('90%').height(50).backgroundColor(0xD2B48C)
Row().width('90%').height(50).backgroundColor(0xF5DEB3)
}.width('100%')
2.5.2.2 Row容器内排列方向上的间距
scss
Row({ space: 35 }) {
Text('space: 35').fontSize(15).fontColor(Color.Gray)
Row().width('10%').height(150).backgroundColor(0xF5DEB3)
Row().width('10%').height(150).backgroundColor(0xD2B48C)
Row().width('10%').height(150).backgroundColor(0xF5DEB3)
}.width('90%')
2.5.3 布局子元素在交叉轴上的对齐方式
在布局容器内,可以通过alignItems属性设置子元素在交叉轴(排列方向的垂直方向)上的对齐方式。且在各类尺寸屏幕中,表现一致。其中,交叉轴为垂直方向时,取值为VerticalAlign类型,水平方向取值为HorizontalAlign类型。
alignSelf属性用于控制单个子元素在容器交叉轴上的对齐方式,其优先级高于alignItems属性,如果设置了alignSelf属性,则在单个子元素上会覆盖alignItems属性。
2.5.3.1 Column容器内子元素在水平方向上的排列
限于篇幅,以下仅举一例,更多内容请读者自行实验或观看配套视频:
HorizontalAlign.Start:子元素在水平方向左对齐。
ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.backgroundColor('rgb(242,242,242)')
2.5.3.2 Row容器内子元素在垂直方向上的排列
限于篇幅,以下仅举一例,更多内容请读者自行实验或观看配套视频:
VerticalAlign.Top:子元素在垂直方向顶部对齐。
ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}
.width('100%')
.height(200)
.alignItems(VerticalAlign.Top)
.backgroundColor('rgb(242,242,242)')
2.5.4 布局子元素在主轴上的排列方式
在布局容器内,可以通过justifyContent属性设置子元素在容器主轴上的排列方式。可以从主轴起始位置开始排布,也可以从主轴结束位置开始排布,或者均匀分割主轴的空间。
2.5.4.1 Column容器内子元素在垂直方向上的排列
限于篇幅,以下仅举一例,更多内容请读者自行实验或观看配套视频:
justifyContent(FlexAlign.Start): 元素在垂直方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。
ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}
.width('100%')
.height(300)
.backgroundColor('rgb(242,242,242)')
.justifyContent(FlexAlign.Start)
2.5.4.2 Row容器内子元素在水平方向上的排列
限于篇幅,以下仅举一例,更多内容请读者自行实验或观看配套视频:
justifyContent(FlexAlign.Start): 元素在水平方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。
ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}
.width('100%')
.height(200)
.backgroundColor('rgb(242,242,242)')
.justifyContent(FlexAlign.Start)
2.5.5 自适应拉伸
在线性布局下,常用空白填充组件Blank,在容器主轴方向自动填充空白空间,达到自适应拉伸效果。Row和Column作为容器,只需要添加宽高为百分比,当屏幕宽高发生变化时,会产生自适应效果。
ts
@Entry
@Component
struct BlankExample {
build() {
Column() {
Row() {
Text('Bluetooth').fontSize(18)
Blank()
Toggle({ type: ToggleType.Switch, isOn: true })
}
.backgroundColor(0xFFFFFF)
.borderRadius(15)
.padding({ left: 12 }).width('100%')
}.backgroundColor(0xEFEFEF).padding(20).width('100%')
}
}
- 竖屏效果图
- 横屏效果图
2.5.6 自适应缩放
自适应缩放是指子元素随容器尺寸的变化而按照预设的比例自动调整尺寸,适应各种不同大小的设备。在线性布局中,可以使用以下两种方法实现自适应缩放。
- 父容器尺寸确定时,使用layoutWeight属性设置子元素和兄弟元素在主轴上的权重,忽略元素本身尺寸设置,使它们在任意尺寸的设备下自适应占满剩余空间。
ts
@Entry
@Component
struct layoutWeightExample {
build() {
Column() {
Text('1:2:3').width('100%')
Row() {
Column() {
Text('layoutWeight(1)')
.textAlign(TextAlign.Center)
}.layoutWeight(1).backgroundColor(0xF5DEB3).height('100%')
Column() {
Text('layoutWeight(2)')
.textAlign(TextAlign.Center)
}.layoutWeight(2).backgroundColor(0xD2B48C).height('100%')
Column() {
Text('layoutWeight(3)')
.textAlign(TextAlign.Center)
}.layoutWeight(3).backgroundColor(0xF5DEB3).height('100%')
}.backgroundColor(0xffd306).height('30%')
Text('2:5:3').width('100%')
Row() {
Column() {
Text('layoutWeight(2)')
.textAlign(TextAlign.Center)
}.layoutWeight(2).backgroundColor(0xF5DEB3).height('100%')
Column() {
Text('layoutWeight(5)')
.textAlign(TextAlign.Center)
}.layoutWeight(5).backgroundColor(0xD2B48C).height('100%')
Column() {
Text('layoutWeight(3)')
.textAlign(TextAlign.Center)
}.layoutWeight(3).backgroundColor(0xF5DEB3).height('100%')
}.backgroundColor(0xffd306).height('30%')
}
}
}
图11 横屏效果图
- 竖屏效果图
- 父容器尺寸确定时,使用百分比设置子元素和兄弟元素的宽度,使他们在任意尺寸的设备下保持固定的自适应占比。
ts
@Entry
@Component
struct WidthExample {
build() {
Column() {
Row() {
Column() {
Text('left width 20%')
.textAlign(TextAlign.Center)
}.width('20%').backgroundColor(0xF5DEB3).height('100%')
Column() {
Text('center width 50%')
.textAlign(TextAlign.Center)
}.width('50%').backgroundColor(0xD2B48C).height('100%')
Column() {
Text('right width 30%')
.textAlign(TextAlign.Center)
}.width('30%').backgroundColor(0xF5DEB3).height('100%')
}.backgroundColor(0xffd306).height('30%')
}
}
}
- 横屏效果图
- 竖屏效果图
2.5.7 自适应延伸
自适应延伸是指在不同尺寸设备下,当页面的内容超出屏幕大小而无法完全显示时,可以通过滚动条进行拖动展示。这种方法适用于线性布局中内容无法一屏展示的场景。通常有以下两种实现方式。
- 在List中添加滚动条:当List子项过多一屏放不下时,可以将每一项子元素放置在不同的组件中,通过滚动条进行拖动展示。可以通过scrollBar属性设置滚动条的常驻状态,edgeEffect属性设置拖动到内容最末端的回弹效果。
- 使用Scroll组件:在线性布局中,开发者可以进行垂直方向或者水平方向的布局。当一屏无法完全显示时,可以在Column或Row组件的外层包裹一个可滚动的容器组件Scroll来实现可滑动的线性布局。
垂直方向布局中使用Scroll组件:
ts
@Entry
@Component
struct ScrollExample {
scroller: Scroller = new Scroller();
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
build() {
Scroll(this.scroller) {
Column() {
ForEach(this.arr, (item?:number|undefined) => {
if(item){
Text(item.toString())
.width('90%')
.height(150)
.backgroundColor(0xFFFFFF)
.borderRadius(15)
.fontSize(16)
.textAlign(TextAlign.Center)
.margin({ top: 10 })
}
}, (item:number) => item.toString())
}.width('100%')
}
.backgroundColor(0xDCDCDC)
.scrollable(ScrollDirection.Vertical) // 滚动方向为垂直方向
.scrollBar(BarState.On) // 滚动条常驻显示
.scrollBarColor(Color.Gray) // 滚动条颜色
.scrollBarWidth(10) // 滚动条宽度
.edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹
}
}
水平方向布局中使用Scroll组件:
ts
@Entry
@Component
struct ScrollExample {
scroller: Scroller = new Scroller();
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
build() {
Scroll(this.scroller) {
Row() {
ForEach(this.arr, (item?:number|undefined) => {
if(item){
Text(item.toString())
.height('90%')
.width(150)
.backgroundColor(0xFFFFFF)
.borderRadius(15)
.fontSize(16)
.textAlign(TextAlign.Center)
.margin({ left: 10 })
}
})
}.height('100%')
}
.backgroundColor(0xDCDCDC)
.scrollable(ScrollDirection.Horizontal) // 滚动方向为水平方向
.scrollBar(BarState.On) // 滚动条常驻显示
.scrollBarColor(Color.Gray) // 滚动条颜色
.scrollBarWidth(10) // 滚动条宽度
.edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹
}
}
2.6 层叠布局(Stack)
层叠布局(StackLayout)用于在屏幕上预留一块区域来显示组件中的元素,提供元素可以重叠的布局。层叠布局通过Stack容器组件实现位置的固定定位与层叠,容器中的子元素依次入栈,后一个子元素覆盖前一个子元素,子元素可以叠加,也可以设置位置。
层叠布局具有较强的页面层叠、位置定位能力,其使用场景有广告、卡片层叠效果等。
如下图,Stack作为容器,容器内的子元素的顺序为Item1->Item2->Item3。
2.6.1 开发布局
Stack组件为容器组件,容器内可包含各种子元素。其中子元素默认进行居中堆叠。子元素被约束在Stack下,进行自己的样式定义以及排列。
ts
let MTop: Record<string, number> = { 'top': 50 }
@Entry
@Component
struct StackExample {
build() {
Column() {
Stack({}) {
Column() {}
.width('90%').height('100%')
.backgroundColor('#ff58b87c')
Text('text')
.width('60%').height('60%')
.backgroundColor('#ffc3f6aa')
Button('button')
.width('30%').height('30%')
.backgroundColor('#ff8ff3eb').fontColor('#000')
}.width('100%').height(150).margin(MTop)
}
}
}
2.6.2 对齐方式
Stack组件通过alignContent参数实现位置的相对移动。如下图所示,支持九种对齐方式。
使用方法如下:
ts
@Entry
@Component
struct StackExample {
build() {
Stack({ alignContent: Alignment.TopStart }) {
Text('Stack')
.width('90%').height('100%')
.backgroundColor('#e1dede').align(Alignment.BottomEnd)
Text('Item 1')
.width('70%').height('80%')
.backgroundColor(0xd2cab3).align(Alignment.BottomEnd)
Text('Item 2')
.width('50%').height('60%')
.backgroundColor(0xc1cbac).align(Alignment.BottomEnd)
}.width('100%').height(150).margin({ top: 5 })
}
}
2.6.3 Z序控制
Stack容器中兄弟组件显示层级关系可以通过Z序控制的zIndex属性改变。zIndex值越大,显示层级越高,即zIndex值大的组件会覆盖在zIndex值小的组件上方。
在层叠布局中,如果后面子元素尺寸大于前面子元素尺寸,则前面子元素完全隐藏。
ts
@Entry
@Component
struct StackExample {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text('Stack子元素1').textAlign(TextAlign.End).fontSize(20)
}.width(100).height(100).backgroundColor(0xffd306)
Column() {
Text('Stack子元素2').fontSize(20)
}.width(150).height(150).backgroundColor(Color.Pink)
Column() {
Text('Stack子元素3').fontSize(20)
}.width(200).height(200).backgroundColor(Color.Grey)
}.width(350).height(350).backgroundColor(0xe0e0e0)
}
}
上图中,最后的子元素3的尺寸大于前面的所有子元素,所以,前面两个元素完全隐藏。改变子元素1,子元素2的zIndex属性后,可以将元素展示出来。
ts
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text('Stack子元素1').fontSize(20)
}.width(100).height(100).backgroundColor(0xffd306).zIndex(2)
Column() {
Text('Stack子元素2').fontSize(20)
}.width(150).height(150).backgroundColor(Color.Pink).zIndex(1)
Column() {
Text('Stack子元素3').fontSize(20)
}.width(200).height(200).backgroundColor(Color.Grey)
}.width(350).height(350).backgroundColor(0xe0e0e0)
2.6.4 场景示例
使用层叠布局快速搭建页面。
ts
@Entry
@Component
struct StackSample {
private arr: string[] = [
'APP1', 'APP2', 'APP3', 'APP4', 'APP5', 'APP6', 'APP7', 'APP8'
]
build() {
Stack({ alignContent: Alignment.Bottom }) {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.arr, (item: string) => {
Text(item)
.width(100)
.height(100)
.fontSize(16)
.margin(10)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}, (item: string): string => item)
}.width('100%').height('100%')
Flex({
justifyContent: FlexAlign.SpaceAround,
alignItems: ItemAlign.Center
}) {
Text('联系人').fontSize(16)
Text('设置').fontSize(16)
Text('短信').fontSize(16)
}
.width('50%')
.height(50)
.backgroundColor('#16302e2e')
.margin({ bottom: 15 })
.borderRadius(15)
}.width('100%').height('100%').backgroundColor('#CFD0CF')
}
}
2.7 显示图片(Image)
开发者经常需要在应用中显示一些图片,例如:按钮中的icon、网络图片、本地图片等。在应用中显示图片需要使用Image组件实现,Image支持多种图片格式,包括png、jpg、bmp、svg、gif和heif。
Image通过调用接口来创建,接口调用形式如下:
ts
Image(src: PixelMap | ResourceStr | DrawableDescriptor)
该接口通过图片数据源获取图片,支持本地图片和网络图片的渲染展示。其中,src是图片的数据源。
2.7.1 加载图片资源
Image支持加载存档图、多媒体像素图两种类型。
2.7.1.1 存档图类型数据源
存档图类型的数据源可以分为本地资源、网络资源、Resource资源和base64。
- 本地资源
创建文件夹,将本地图片放入ets文件夹下的任意位置。
Image组件引入本地图片路径,即可显示图片(根目录为ets文件夹)。
ts
Image('images/view.jpg')
.width(200)
- 网络资源
引入网络图片需申请权限ohos.permission.INTERNET,请在module.json5中配置网络访问权限:
ts
"requestPermissions": [
{
"name" : "ohos.permission.INTERNET"
}
]
此时,Image组件的src参数为网络图片的链接。当前Image组件仅支持加载简单网络图片。
Image组件首次加载网络图片时,需要请求网络资源,非首次加载时,默认从缓存中直接读取图片。
ts
Image('https://www.example.com/example.JPG') // 实际使用时请替换为真实地址
- Resource资源
使用资源格式可以跨包/跨模块引入图片,resources文件夹下的图片都可以通过$r资源接口读取到并转换到Resource格式。
调用方式:
ts
Image($r('app.media.heart'))
还可以将图片放在rawfile文件夹下。
调用方式:
ts
Image($rawfile('flower.png'))
- base64
路径格式为data:image/[png|jpeg|bmp|webp|heif];base64,[base64 data],其中[base64 data]为Base64字符串数据。Base64格式字符串可用于存储图片的像素数据,在网页上使用较为广泛。
调用方式:
ts
Image('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAAAdVBMVEVHcEz0SkDwQznvQjjwQzvxRDnxQjv/Tjv/VVX/SUn/VUD/VVXwQjjyQzzvQzjvQznvQzjvQznvQznwQzjwQzjwQzjvQjjwQjj/gIDvQjnwQzjxRjz0Qz3xQzrxQznvQzjwQzjwQzr0RDnvQjrvQjnvQjrvQjjZmN5IAAAAJnRSTlMAGPzjV0dJDQYHDANkJvGC9JTllb/ZsZYC88c3LlhIkdqFLZLysetr6xgAAAEYSURBVHgBtZRpf4MgDMbtodaragUp7Y52B9//Iy5hGxMI/cUXy7uH/EkgkGT/a4d8ElJrKab88ADbqKNxdlSbBFb1g/Fs6Csy3MVEdiGCPu2sazcWXV13xfgj36J41nFqm9+Fpj1ZMohZ2bxzuVwrZ5vdP2ePa69nf/f5HVd7LzHedw44IDHmsEyu8HxlfMMSz6kW74F1bqmatVj5vzfK8XoNBTZYjNzJCdRIP9cIrskpAaqgwQJcwikJqqPBDlzSKQ2qpsEaXJoLfqxPzb4MuzzsgrOfkP0p+N8s8XE/o4/7oBVuQcummstsn4Pz0O1KkdQAuG2R3AckNVL2JEkNqQRJjL0UGds3eeWS94xJvnDA7HrncWvtCxqTNWpxQUE/AAAAAElFTkSuQmCC')
2.7.1.2 显示矢量图
Image组件可显示矢量图(svg格式的图片),svg格式的图片可以使用fillColor属性改变图片的绘制颜色。
ts
Image($r('app.media.mine'))
.width('100vp')
.fillColor(Color.Blue)
以上代码加载了media目录下的mine.svg资源:
显示效果如下:
- 原始图片
- 设置绘制颜色后的svg图片
2.7.2 添加属性
给Image组件设置属性可以使图片显示更灵活,达到一些自定义的效果。给图片添加属性的方式包括设置缩放类型、插值方式、重复样式、渲染模式、解码尺寸、滤镜效果以及同步加载等。
限于篇幅,这里只给出设置图片缩放类型的用法。更多图片属性的添加方法,请参阅随书配套电子书或观看配套视频。
通过objectFit属性使图片缩放到高度和宽度确定的框内。
ts
@Entry
@Component
struct MyComponent {
scroller: Scroller = new Scroller()
build() {
Scroll(this.scroller) {
Column() {
Row() {
Image($r('app.media.cloud'))
.width(200)
.height(150)
.border({ width: 1 })
// 保持宽高比进行缩小或者放大,使得图片完全显示在显示边界内。
.objectFit(ImageFit.Contain)
.margin(15)
.overlay('Contain', {
align: Alignment.Bottom,
offset: { x: 0, y: 20 }
})
Image($r('app.media.cloud'))
.width(200)
.height(150)
.border({ width: 1 })
// 保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界。
.objectFit(ImageFit.Cover)
.margin(15)
.overlay('Cover', {
align: Alignment.Bottom,
offset: { x: 0, y: 20 }
})
Image($r('app.media.cloud'))
.width(200)
.height(150)
.border({ width: 1 })
// 自适应显示。
.objectFit(ImageFit.Auto)
.margin(15)
.overlay('Auto', {
align: Alignment.Bottom,
offset: { x: 0, y: 20 }
})
}
Row() {
Image($r('app.media.cloud'))
.width(200)
.height(150)
.border({ width: 1 })
// 不保持宽高比进行放大缩小,使得图片充满显示边界。
.objectFit(ImageFit.Fill)
.margin(15)
.overlay('Fill', {
align: Alignment.Bottom,
offset: { x: 0, y: 20 }
})
Image($r('app.media.cloud'))
.width(200)
.height(150)
.border({ width: 1 })
// 保持宽高比显示,图片缩小或者保持不变。
.objectFit(ImageFit.ScaleDown)
.margin(15)
.overlay('ScaleDown', {
align: Alignment.Bottom,
offset: { x: 0, y: 20 }
})
Image($r('app.media.cloud'))
.width(200)
.height(150)
.border({ width: 1 })
// 保持原有尺寸显示。
.objectFit(ImageFit.None)
.margin(15)
.overlay('None', {
align: Alignment.Bottom,
offset: { x: 0, y: 20 }
})
}
}
}
}
}
2.7.3 事件调用
通过在Image组件上绑定onComplete事件,图片加载成功后可以获取图片的必要信息。如果图片加载失败,也可以通过绑定onError回调来获得结果。
ts
@Entry
@Component
struct ImageComponent {
@State widthValue: number = 0
@State heightValue: number = 0
build() {
Column() {
Image('images/view.jpg')
.width(200)
.height(150)
.margin(15)
.onComplete((msg) => {
if (msg) {
console.log(msg.width.toString())
console.log(msg.height.toString())
this.widthValue = msg.width
this.heightValue = msg.height
}
})
.onError((err) => {
console.log(err + '')
})
.overlay(`width: ${this.widthValue.toString()}, `
+ `height: ${this.heightValue.toString()}`, {
align: Alignment.Bottom,
offset: {x: 0, y: 40}
})
}
}
}
2.8 按钮(Button)
Button是按钮组件,通常用于响应用户的点击操作,其类型包括胶囊按钮、圆形按钮、普通按钮。Button做为容器使用时可以通过添加子组件实现包含文字、图片等元素的按钮。
2.8.1 创建按钮
Button通过调用接口来创建,接口调用有以下两种形式:
- 通过label和ButtonOptions创建不包含子组件的按钮。以ButtonOptions中的type和stateEffect为例。
ts
Button(label?: ResourceStr, options?: { type?: ButtonType, stateEffect?: boolean })
其中,label用来设置按钮文字,type用于设置Button类型,stateEffect属性设置Button是否开启点击效果。
ts
Button('Ok', { type: ButtonType.Normal, stateEffect: true })
.borderRadius(8)
.backgroundColor(0x317aff)
.width(90)
.height(40)
- 通过ButtonOptions创建包含子组件的按钮。以ButtonOptions中的type和stateEffect为例。
ts
Button(options?: {type?: ButtonType, stateEffect?: boolean})
只支持包含一个子组件,子组件可以是基础组件或者容器组件。
ts
Button({ type: ButtonType.Normal, stateEffect: true }) {
Row() {
Image($r('app.media.loading')).width(20).height(40).margin({ left: 12 })
Text('loading')
.fontSize(12)
.fontColor(0xffffff)
.margin({ left: 5, right: 12 })
}
.alignItems(VerticalAlign.Center)
}
.borderRadius(8)
.backgroundColor(0x317aff)
.width(90).height(40)
2.8.2 设置按钮类型
Button有三种可选类型,分别为胶囊类型(Capsule)、圆形按钮(Circle)和普通按钮(Normal),通过type进行设置。
- 胶囊按钮(默认类型)。
此类型按钮的圆角自动设置为高度的一半,不支持通过borderRadius属性重新设置圆角。
ts
Button('Disable', { type: ButtonType.Capsule, stateEffect: false })
.backgroundColor(0x317aff)
.width(90)
.height(40)
- 圆形按钮。
此类型按钮为圆形,不支持通过borderRadius属性重新设置圆角。
ts
Button('Circle', { type: ButtonType.Circle, stateEffect: false })
.backgroundColor(0x317aff)
.width(90)
.height(90)
- 普通按钮。
此类型的按钮默认圆角为0,支持通过borderRadius属性重新设置圆角。
ts
Button('Ok', { type: ButtonType.Normal, stateEffect: true })
.borderRadius(8)
.backgroundColor(0x317aff)
.width(90)
.height(40)
2.8.3 自定义样式
- 设置边框弧度。
使用通用属性来自定义按钮样式。例如通过borderRadius属性设置按钮的边框弧度。
ts
Button('circle border', { type: ButtonType.Normal })
.borderRadius(20)
.height(40)
- 设置文本样式。
通过添加文本样式设置按钮文本的展示样式。
ts
Button('font style', { type: ButtonType.Normal })
.fontSize(20)
.fontColor(Color.Pink)
.fontWeight(800)
- 设置背景颜色。
添加backgroundColor属性设置按钮的背景颜色。
ts
Button('background color').backgroundColor(0xF55A42)
- 创建功能型按钮。
为删除操作创建一个按钮。
scss
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image($r('app.media.garbage')).width(30).height(30)
}.width(55).height(55).margin({ 'left': 20 }).backgroundColor(0xF55A42)
2.8.4 添加事件
Button组件通常用于触发某些操作,可以绑定onClick事件来响应点击操作后的自定义行为。
ts
Button('Ok', { type: ButtonType.Normal, stateEffect: true })
.onClick(()=>{
console.info('Button onClick')
})
2.8.5 场景示例
- 用于提交表单。
在用户登录/注册页面,使用按钮进行登录或注册操作。
ts
@Entry
@Component
struct ButtonCase {
build() {
Column() {
TextInput({ placeholder: 'input your username' })
.margin({ top: 20 })
TextInput({ placeholder: 'input your password' })
.type(InputType.Password).margin({ top: 20 })
Button('Register').width(300).margin({ top: 20 })
.onClick(() => {
// 需要执行的操作
})
}.padding(20)
}
}
更多场景示例请参阅随书配套电子书或观看配套视频。
2.9 案例实战
本节以仿猫眼电影M站首页布局为案例,展示ArkUI在实际开发中的应用。内容包括案例效果及相关知识点,深入解析布局框架以及头部、脚部、内容区域的构建思路与代码实现,最后提供完整代码和教程资源,助力你强化实践能力。
2.9.1 案例效果截图
2.9.2 案例运用到的知识点
- 核心知识点
- UI 范式基本语法
- 文本显示Text、Span组件
- 线性布局Column、Row组件
- 层叠布局Stack组件
- 按钮Button组件
- 显示图片Image组件
- 其他知识点
- DevEco Studio的基本使用
- 简单的资源分类访问
- 移动端APP布局基本技巧
2.9.1 布局框架
可以按照下图来思考布局的框架:
框架的代码如下:
ts
@Entry
@Component
struct Index {
build() {
Column() {
Stack() {
}
.width('100%')
.height(50)
.backgroundColor('#e54847')
Column() {
Text('content')
}
.width('100%')
.layoutWeight(1)
Row() {
}
.width('100%')
.height(50)
.backgroundColor('#fff')
.border({width: { top: 1}, color: '#eee'})
}
}
}
2.9.2 头部区域
可以按照下图思路来构建布局:
代码如下:
ts
/**
* 头部区域
*/
Stack({ alignContent: Alignment.End }) {
Text('猫眼电影')
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
.fontColor('#fff')
.fontSize(18)
Image($rawfile('menu.png'))
.width(17)
.height(16)
.margin({ right: 10 })
}
.width('100%')
.height(50)
.backgroundColor('#e54847')
2.9.3 脚部区域
可以按照下图思路来构建布局:
代码如下:
ts
/**
* 脚部区域
*/
Row() {
Column() {
Image($rawfile('movie.svg'))
.width(25)
.height(25)
.fillColor('#e54847')
Text('电影/影院')
.fontSize(10)
.fontColor('#e54847')
}
.layoutWeight(1)
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
Column() {
Image($rawfile('video.png'))
.width(25)
.height(25)
.fillColor('#696969')
Text('视频')
.fontSize(10)
.fontColor('#696969')
}
.layoutWeight(1)
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
Column() {
Image($rawfile('perform.svg'))
.width(25)
.height(25)
.fillColor('#696969')
Text('演出')
.fontSize(10)
.fontColor('#696969')
}
.layoutWeight(1)
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
Column() {
Image($rawfile('mine.svg'))
.width(25)
.height(25)
.fillColor('#696969')
Text('我的')
.fontSize(10)
.fontColor('#696969')
}
.layoutWeight(1)
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
}
.width('100%')
.height(50)
.border({ width: { top: 1 }, color: '#eee' })
.backgroundColor('#fff')
2.9.4 内容区域
可以参照下图思考内容区域的整体框架布局:
代码如下:
ts
/**
* 内容区域
*/
Column() {
Row() {
}
.width('100%')
.height(44)
.border({width: {bottom: 1}, color: '#e6e6e6'})
Scroll() {
}
.layoutWeight(1)
}
.width('100%')
.layoutWeight(1)
2.9.4.1 导航区
内容区域的导航区可以参照下图思考布局:
代码如下:
ts
// 导航区
Row() {
Row() {
Text('北京').fontColor('#666')
Text('')
.width(0)
.height(0)
.border({
width: 5,
color: {
top: '#b0b0b0',
left: Color.Transparent,
right: Color.Transparent,
bottom: Color.Transparent
}
})
.margin({top: 6, left: 4})
}
.offset({x: 15})
.width(60)
Row() {
Stack() {
Text('热映')
.fontSize(17)
.fontWeight(FontWeight.Bold)
Text('')
.width(24)
.border({width: {bottom: 3}, color: '#e54847'})
.offset({y: 18})
}
Text('影院')
Text('待映')
Text('经典电影')
}
.justifyContent(FlexAlign.SpaceEvenly)
.layoutWeight(1)
Row() {
Image($rawfile('search-red.png'))
.width(20)
.height(20)
}
.justifyContent(FlexAlign.Center)
.width(50)
}
.width('100%')
.height(44)
.border({width: {bottom: 1}, color: '#e6e6e6'})
2.9.4.2 最受好评区
可以参照下面示意图考虑整体布局:
代码如下:
ts
// 好评和列表区内容
Column() {
// 好评区
Column() {
Text('最受好评电影')
.width('100%')
.fontSize(14)
.fontColor('#333')
.textAlign(TextAlign.Start)
.margin({ bottom: 12 })
Scroll() {
Row() {
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdeddf2a92339dd2c95022e99e5fe27091.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('出走的决心')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdedd51b51b0fafb53588b9711782eda09.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('里斯本丸沉没')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdedd807ddd21f00e13a48cbecf74136a6.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('荒野机器人')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdeddb12c7e537c537ca593367280c0deb.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.5')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('变形金刚')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdeddf2a92339dd2c95022e99e5fe27091.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('出走的决心')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdedd51b51b0fafb53588b9711782eda09.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('里斯本丸沉没')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdedd807ddd21f00e13a48cbecf74136a6.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('荒野机器人')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdeddb12c7e537c537ca593367280c0deb.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.5')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('变形金刚')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
.width('100%')
.padding({
top: 12,
bottom: 12,
left: 15,
right: 15
})
}
.height('100%')
2.9.4.3 列表区
列表区整体布局参照下图:
代码如下:
ts
// 列表区
Column() {
Row() {
Image('https://p0.pipi.cn/basicdata/54ecdedd5377e187a9e7aa5ee9ec15a184b18.jpg?imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180')
.width(64)
.height(90)
Stack({ alignContent: Alignment.End }) {
Column() {
Row() {
Text('志愿军:存亡之战')
.fontSize(17)
.fontWeight(FontWeight.Bold)
Image($rawfile('v2dimax.png'))
.width(43)
.height(14)
.margin({ left: 4 })
}
Text() {
Span('274337').fontColor('#faaf00')
Span('人想看').fontColor('#666').fontSize(13)
}
Text('主演: 朱一龙,辛柏青,张子枫').fontColor('#666').fontSize(13)
Text('2024-09-30 下周一上映').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start)
.height('100%')
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ right: 53 })
Button() {
Text('预售')
.fontColor('#fff')
.fontSize(13)
.fontWeight(500)
}
.width(54)
.height(28)
.backgroundColor('#3C9FE6')
}
.height('100%')
.layoutWeight(1)
.padding({
top: 12,
right: 14,
bottom: 12,
left: 0
})
.margin({ left: 12 })
.border({ width: { bottom: 1 }, color: '#eee' })
}
.height(144)
Row() {
Image('https://p0.pipi.cn/basicdata/54ecdeddddd338e7aad7c30f0886bd5fc417a.jpg?imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180')
.width(64).height(90)
Stack({alignContent: Alignment.End}) {
Column() {
Row() {
Text('危机航线').fontSize(17).fontWeight(FontWeight.Bold)
Image($rawfile('v2dimax.png')).width(43).height(14).margin({ left: 4 })
}
Text() {
Span('点映评 ').fontColor('#666').fontSize(13)
Span('9.5').fontColor('#faaf00')
}
Text('主演: 刘德华,张子枫,屈楚萧').fontColor('#666').fontSize(13)
Text('今天241家影院放映960场').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start)
.height('100%')
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ right: 53 })
Button() {
Text('购票').fontColor('#fff').fontSize(13).fontWeight(500)
}.width(54).height(28).backgroundColor('#f03d37')
}
.padding({
top: 12,
right: 14,
bottom: 12,
left: 0
})
.margin({ left: 12 })
.layoutWeight(1)
.height('100%')
.border({ width: { bottom: 1 }, color: '#eee' })
}
.height(114)
Row() {
Image('https://p0.pipi.cn/basicdata/54ecdedd2c9b12395b50c8273d157513f2ebb.jpg?imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180')
.width(64).height(90)
Stack({alignContent: Alignment.End}) {
Column() {
Row() {
Text('变形金刚:起源').fontSize(17).fontWeight(FontWeight.Bold)
Image($rawfile('v3dimax.png')).width(43).height(14).margin({ left: 4 })
}
Text() {
Span('暂无评分').fontColor('#666').fontSize(13)
}
Text('主演: 克里斯·海姆斯沃斯,布莱恩·泰里·亨利,斯嘉丽·约翰逊')
.fontColor('#666').fontSize(13)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('今天245家影院放映1898场').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start)
.height('100%')
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ right: 53 })
Button() {
Text('购票').fontColor('#fff').fontSize(13).fontWeight(500)
}.width(54).height(28).backgroundColor('#f03d37')
}
.padding({
top: 12,
right: 14,
bottom: 12,
left: 0
})
.margin({ left: 12 })
.layoutWeight(1)
.height('100%')
.border({ width: { bottom: 1 }, color: '#eee' })
}
.height(114)
Row() {
Image('https://p0.pipi.cn/basicdata/54ecdedd5377e187a9e7aa5ee9ec15a184b18.jpg?imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180')
.width(64).height(90)
Stack({alignContent: Alignment.End}) {
Column() {
Row() {
Text('志愿军:存亡之战').fontSize(17).fontWeight(FontWeight.Bold)
Image($rawfile('v2dimax.png')).width(43).height(14).margin({ left: 4 })
}
Text() {
Span('274337').fontColor('#faaf00')
Span('人想看').fontColor('#666').fontSize(13)
}
Text('主演: 朱一龙,辛柏青,张子枫').fontColor('#666').fontSize(13)
Text('2024-09-30 下周一上映').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start)
.height('100%')
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ right: 53 })
Button() {
Text('预售').fontColor('#fff').fontSize(13).fontWeight(500)
}.width(54).height(28).backgroundColor('#3C9FE6')
}
.padding({
top: 12,
right: 14,
bottom: 12,
left: 0
})
.margin({ left: 12 })
.layoutWeight(1)
.height('100%')
.border({ width: { bottom: 1 }, color: '#eee' })
}
.height(114)
Row() {
Image('https://p0.pipi.cn/basicdata/54ecdeddddd338e7aad7c30f0886bd5fc417a.jpg?imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180')
.width(64).height(90)
Stack({alignContent: Alignment.End}) {
Column() {
Row() {
Text('危机航线').fontSize(17).fontWeight(FontWeight.Bold)
Image($rawfile('v2dimax.png')).width(43).height(14).margin({ left: 4 })
}
Text() {
Span('点映评 ').fontColor('#666').fontSize(13)
Span('9.5').fontColor('#faaf00')
}
Text('主演: 刘德华,张子枫,屈楚萧').fontColor('#666').fontSize(13)
Text('今天241家影院放映960场').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start)
.height('100%')
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ right: 53 })
Button() {
Text('购票').fontColor('#fff').fontSize(13).fontWeight(500)
}.width(54).height(28).backgroundColor('#f03d37')
}
.padding({
top: 12,
right: 14,
bottom: 12,
left: 0
})
.margin({ left: 12 })
.layoutWeight(1)
.height('100%')
.border({ width: { bottom: 1 }, color: '#eee' })
}
.height(114)
Row() {
Image('https://p0.pipi.cn/basicdata/54ecdedd2c9b12395b50c8273d157513f2ebb.jpg?imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180')
.width(64).height(90)
Stack({alignContent: Alignment.End}) {
Column() {
Row() {
Text('变形金刚:起源').fontSize(17).fontWeight(FontWeight.Bold)
Image($rawfile('v3dimax.png')).width(43).height(14).margin({ left: 4 })
}
Text() {
Span('暂无评分').fontColor('#666').fontSize(13)
}
Text('主演: 克里斯·海姆斯沃斯,布莱恩·泰里·亨利,斯嘉丽·约翰逊')
.fontColor('#666').fontSize(13)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('今天245家影院放映1898场').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start)
.height('100%')
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ right: 53 })
Button() {
Text('购票').fontColor('#fff').fontSize(13).fontWeight(500)
}.width(54).height(28).backgroundColor('#f03d37')
}
.padding({
top: 12,
right: 14,
bottom: 12,
left: 0
})
.margin({ left: 12 })
.layoutWeight(1)
.height('100%')
.border({ width: { bottom: 1 }, color: '#eee' })
}
.height(114)
}
.backgroundColor('#fff')
.padding({ left: 15 })
2.9.5 全部代码
ts
@Entry
@Component
struct PageABC {
build() {
Column() {
/**
* 头部区域
*/
Stack({ alignContent: Alignment.End }) {
Text('猫眼电影')
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
.fontColor('#fff')
.fontSize(18)
Image($rawfile('menu.png'))
.width(17)
.height(16)
.margin({ right: 10 })
}
.width('100%')
.height(50)
.backgroundColor('#e54847')
/**
* 内容区域
*/
Column() {
// 导航区
Row() {
Row() {
Text('北京').fontColor('#666')
Text('')
.width(0)
.height(0)
.border({
width: 5,
color: {
top: '#b0b0b0',
left: Color.Transparent,
right: Color.Transparent,
bottom: Color.Transparent
}
})
.margin({ top: 6, left: 4 })
}
.offset({ x: 15 })
.width(60)
Row() {
Stack() {
Text('热映')
.fontSize(17)
.fontWeight(FontWeight.Bold)
Text('')
.width(24)
.border({ width: { bottom: 3 }, color: '#e54847' })
.offset({ y: 18 })
}
Text('影院')
Text('待映')
Text('经典电影')
}
.justifyContent(FlexAlign.SpaceEvenly)
.layoutWeight(1)
Row() {
Image($rawfile('search-red.png'))
.width(20)
.height(20)
}
.justifyContent(FlexAlign.Center)
.width(50)
}
.width('100%')
.height(44)
.border({ width: { bottom: 1 }, color: '#e6e6e6' })
.backgroundColor('#fff')
// 好评和列表区
Scroll() {
Column() {
// 好评区
Column() {
Text('最受好评电影')
.width('100%')
.fontSize(14)
.fontColor('#333')
.textAlign(TextAlign.Start)
.margin({ bottom: 12 })
Scroll() {
Row() {
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdeddf2a92339dd2c95022e99e5fe27091.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('出走的决心')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdedd51b51b0fafb53588b9711782eda09.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('里斯本丸沉没')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdedd807ddd21f00e13a48cbecf74136a6.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('荒野机器人')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdeddb12c7e537c537ca593367280c0deb.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.5')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('变形金刚')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdeddf2a92339dd2c95022e99e5fe27091.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('出走的决心')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdedd51b51b0fafb53588b9711782eda09.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('里斯本丸沉没')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdedd807ddd21f00e13a48cbecf74136a6.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.6')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('荒野机器人')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image('https://p0.pipi.cn/basicdata/54ecdeddb12c7e537c537ca593367280c0deb.jpg?imageMogr2/thumbnail/2500x2500%3E')
.width(85)
.height(115)
Text('')
.width('100%')
.height(35)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['rgba(0,0,0,0)', 0], [0x000000, 1]]
})
Text('观众评分:9.5')
.fontColor('#faaf00')
.fontSize(11)
.fontWeight(700)
.offset({ x: 4, y: -4 })
}
.height(115)
.margin({ bottom: 6 })
Text('变形金刚')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.width(85)
.textAlign(TextAlign.Start)
.margin({ bottom: 3 })
}
.width(85)
.margin({ right: 10 })
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
.width('100%')
.backgroundColor('#fff')
.margin({bottom: 10})
.padding({
top: 12,
bottom: 12,
left: 15,
right: 15
})
// 列表区
Column() {
Row() {
Image('https://p0.pipi.cn/basicdata/54ecdedd5377e187a9e7aa5ee9ec15a184b18.jpg?imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180')
.width(64)
.height(90)
Stack({ alignContent: Alignment.End }) {
Column() {
Row() {
Text('志愿军:存亡之战')
.fontSize(17)
.fontWeight(FontWeight.Bold)
Image($rawfile('v2dimax.png'))
.width(43)
.height(14)
.margin({ left: 4 })
}
Text() {
Span('274337').fontColor('#faaf00')
Span('人想看').fontColor('#666').fontSize(13)
}
Text('主演: 朱一龙,辛柏青,张子枫').fontColor('#666').fontSize(13)
Text('2024-09-30 下周一上映').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start)
.height('100%')
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ right: 53 })
Button() {
Text('预售')
.fontColor('#fff')
.fontSize(13)
.fontWeight(500)
}
.width(54)
.height(28)
.backgroundColor('#3C9FE6')
}
.height('100%')
.layoutWeight(1)
.padding({
top: 12,
right: 14,
bottom: 12,
left: 0
})
.margin({ left: 12 })
.border({ width: { bottom: 1 }, color: '#eee' })
}
.height(144)
Row() {
Image('https://p0.pipi.cn/basicdata/54ecdeddddd338e7aad7c30f0886bd5fc417a.jpg?imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180')
.width(64).height(90)
Stack({alignContent: Alignment.End}) {
Column() {
Row() {
Text('危机航线').fontSize(17).fontWeight(FontWeight.Bold)
Image($rawfile('v2dimax.png')).width(43).height(14).margin({ left: 4 })
}
Text() {
Span('点映评 ').fontColor('#666').fontSize(13)
Span('9.5').fontColor('#faaf00')
}
Text('主演: 刘德华,张子枫,屈楚萧').fontColor('#666').fontSize(13)
Text('今天241家影院放映960场').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start)
.height('100%')
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ right: 53 })
Button() {
Text('购票').fontColor('#fff').fontSize(13).fontWeight(500)
}.width(54).height(28).backgroundColor('#f03d37')
}
.padding({
top: 12,
right: 14,
bottom: 12,
left: 0
})
.margin({ left: 12 })
.layoutWeight(1)
.height('100%')
.border({ width: { bottom: 1 }, color: '#eee' })
}
.height(114)
Row() {
Image('https://p0.pipi.cn/basicdata/54ecdedd2c9b12395b50c8273d157513f2ebb.jpg?imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180')
.width(64).height(90)
Stack({alignContent: Alignment.End}) {
Column() {
Row() {
Text('变形金刚:起源').fontSize(17).fontWeight(FontWeight.Bold)
Image($rawfile('v3dimax.png')).width(43).height(14).margin({ left: 4 })
}
Text() {
Span('暂无评分').fontColor('#666').fontSize(13)
}
Text('主演: 克里斯·海姆斯沃斯,布莱恩·泰里·亨利,斯嘉丽·约翰逊')
.fontColor('#666').fontSize(13)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('今天245家影院放映1898场').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start)
.height('100%')
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ right: 53 })
Button() {
Text('购票').fontColor('#fff').fontSize(13).fontWeight(500)
}.width(54).height(28).backgroundColor('#f03d37')
}
.padding({
top: 12,
right: 14,
bottom: 12,
left: 0
})
.margin({ left: 12 })
.layoutWeight(1)
.height('100%')
.border({ width: { bottom: 1 }, color: '#eee' })
}
.height(114)
Row() {
Image('https://p0.pipi.cn/basicdata/54ecdedd5377e187a9e7aa5ee9ec15a184b18.jpg?imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180')
.width(64).height(90)
Stack({alignContent: Alignment.End}) {
Column() {
Row() {
Text('志愿军:存亡之战').fontSize(17).fontWeight(FontWeight.Bold)
Image($rawfile('v2dimax.png')).width(43).height(14).margin({ left: 4 })
}
Text() {
Span('274337').fontColor('#faaf00')
Span('人想看').fontColor('#666').fontSize(13)
}
Text('主演: 朱一龙,辛柏青,张子枫').fontColor('#666').fontSize(13)
Text('2024-09-30 下周一上映').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start)
.height('100%')
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ right: 53 })
Button() {
Text('预售').fontColor('#fff').fontSize(13).fontWeight(500)
}.width(54).height(28).backgroundColor('#3C9FE6')
}
.padding({
top: 12,
right: 14,
bottom: 12,
left: 0
})
.margin({ left: 12 })
.layoutWeight(1)
.height('100%')
.border({ width: { bottom: 1 }, color: '#eee' })
}
.height(114)
Row() {
Image('https://p0.pipi.cn/basicdata/54ecdeddddd338e7aad7c30f0886bd5fc417a.jpg?imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180')
.width(64).height(90)
Stack({alignContent: Alignment.End}) {
Column() {
Row() {
Text('危机航线').fontSize(17).fontWeight(FontWeight.Bold)
Image($rawfile('v2dimax.png')).width(43).height(14).margin({ left: 4 })
}
Text() {
Span('点映评 ').fontColor('#666').fontSize(13)
Span('9.5').fontColor('#faaf00')
}
Text('主演: 刘德华,张子枫,屈楚萧').fontColor('#666').fontSize(13)
Text('今天241家影院放映960场').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start)
.height('100%')
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ right: 53 })
Button() {
Text('购票').fontColor('#fff').fontSize(13).fontWeight(500)
}.width(54).height(28).backgroundColor('#f03d37')
}
.padding({
top: 12,
right: 14,
bottom: 12,
left: 0
})
.margin({ left: 12 })
.layoutWeight(1)
.height('100%')
.border({ width: { bottom: 1 }, color: '#eee' })
}
.height(114)
Row() {
Image('https://p0.pipi.cn/basicdata/54ecdedd2c9b12395b50c8273d157513f2ebb.jpg?imageMogr2/thumbnail/2500x2500%3E?imageView2/1/w/128/h/180')
.width(64).height(90)
Stack({alignContent: Alignment.End}) {
Column() {
Row() {
Text('变形金刚:起源').fontSize(17).fontWeight(FontWeight.Bold)
Image($rawfile('v3dimax.png')).width(43).height(14).margin({ left: 4 })
}
Text() {
Span('暂无评分').fontColor('#666').fontSize(13)
}
Text('主演: 克里斯·海姆斯沃斯,布莱恩·泰里·亨利,斯嘉丽·约翰逊')
.fontColor('#666').fontSize(13)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('今天245家影院放映1898场').fontColor('#666').fontSize(13)
}
.alignItems(HorizontalAlign.Start)
.height('100%')
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({ right: 53 })
Button() {
Text('购票').fontColor('#fff').fontSize(13).fontWeight(500)
}.width(54).height(28).backgroundColor('#f03d37')
}
.padding({
top: 12,
right: 14,
bottom: 12,
left: 0
})
.margin({ left: 12 })
.layoutWeight(1)
.height('100%')
.border({ width: { bottom: 1 }, color: '#eee' })
}
.height(114)
}
.backgroundColor('#fff')
.padding({ left: 15 })
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
.layoutWeight(1)
Row() {
Column() {
Image($rawfile('movie.svg'))
.width(25)
.height(25)
.fillColor('#e54847')
Text('电影/影院')
.fontSize(10)
.fontColor('#e54847')
}
.layoutWeight(1)
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
Column() {
Image($rawfile('video.png'))
.width(25)
.height(25)
.fillColor('#696969')
Text('视频')
.fontSize(10)
.fontColor('#696969')
}
.layoutWeight(1)
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
Column() {
Image($rawfile('perform.svg'))
.width(25)
.height(25)
.fillColor('#696969')
Text('演出')
.fontSize(10)
.fontColor('#696969')
}
.layoutWeight(1)
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
Column() {
Image($rawfile('mine.svg'))
.width(25)
.height(25)
.fillColor('#696969')
Text('我的')
.fontSize(10)
.fontColor('#696969')
}
.layoutWeight(1)
.height('100%')
.justifyContent(FlexAlign.SpaceEvenly)
}
.width('100%')
.height(50)
.backgroundColor('#fff')
.border({ width: { top: 1 }, color: '#eee' })
}
.backgroundColor('#f5f5f5')
}
}
2.9.6 代码与视频教程
完整案例代码与视频教程请参见:
代码:Code-02-01.zip。
视频:《仿猫眼电影M站首页布局》。
2.10 本章小结
本章聚焦HarmonyOS的ArkUI页面布局。首先介绍ArkTS与ArkUI,ArkTS作为核心开发语言,在TypeScript基础上增强了静态检查等特性,而ArkUI则提供多种开发范式与完整的UI开发基础设施。
随后,深入讲解UI范式的基本语法,包括组件创建、属性与事件配置等内容,同时介绍虚拟像素单位,并详细解析文本、线性布局、层叠布局、图片、按钮等组件的使用方法。
最后,通过仿猫眼电影M站首页布局案例,综合运用所学知识,展示从布局框架搭建到各区域实现的完整过程。通过本章学习,读者将全面理解ArkUI页面布局,掌握基本开发技巧,为HarmonyOS应用开发打下坚实基础。