未验证 提交 6b9117ed 编写于 作者: O openharmony_ci 提交者: Gitee

!19118 翻译完成 18007+18495+18180+18719+18043+18284

Merge pull request !19118 from ester.zhou/TR-18007
# Creating a Custom Component # Creating a Custom Component
In ArkUI, components are what's displayed on the UI. They can be classified as built-in components – those directly provided by ArkUI framework, and custom components – those defined by developers. Defining the entire application UI with just built-in components would lead to a monolithic design, low code maintainability, and poor execution performance. A good UI is the result of a well-thought-out development process, with such factors as code reusability, separation of service logic from the UI, and version evolution carefully considered. Creating custom components that encapsulate the UI and some business logic is a critical step in this process. In ArkUI, components are what's displayed on the UI. They can be classified as built-in components – those directly provided by the ArkUI framework, and custom components – those defined by developers. Defining the entire application UI with just built-in components would lead to a monolithic design, low code maintainability, and poor execution performance. A good UI is the result of a well-thought-out development process, with such factors as code reusability, separation of service logic from the UI, and version evolution carefully considered. Creating custom components that encapsulate the UI and some business logic is a critical step in this process.
The custom component has the following features: The custom component has the following features:
......
...@@ -24,9 +24,9 @@ Video(value: {src?: string | Resource, currentProgressRate?: number | string | P ...@@ -24,9 +24,9 @@ Video(value: {src?: string | Resource, currentProgressRate?: number | string | P
| Name | Type | Mandatory| Description | | Name | Type | Mandatory| Description |
| ------------------- | ------------------------------------------------------------ | ---- | ------------------------------------------------------------ | | ------------------- | ------------------------------------------------------------ | ---- | ------------------------------------------------------------ |
| src | string \| [Resource](ts-types.md) | No | Path of the video source, which can be a local path or a URL.<br>The video resources can be stored in the **video** or **rawfile** folder under **resources**.<br>The path can include a **dataability://** prefix, which indicates that the path is provided by a Data ability. For details about the path, see [Data Ability Development](../../application-models/dataability-overview.md).<br>**NOTE**<br>The supported video formats are MP4, MKV, WebM, and TS.| | src | string \| [Resource](ts-types.md) | No | Path of the video source, which can be a local path or a URL.<br>The video resources can be stored in the **video** or **rawfile** folder under **resources**.<br>The path can include a **dataability://** prefix, which indicates that the path is provided by a Data ability. For details about the path, see [Data Ability Development](../../application-models/dataability-overview.md).<br>- Strings with the **file:///data/storage** prefix are supported, which are used to read resources in the application sandbox. Ensure that the application has the read permission to the files in the specified path.<br>**NOTE**<br>The supported video formats are MP4, MKV, WebM, and TS. |
| currentProgressRate | number \| string \| PlaybackSpeed<sup>8+</sup> | No | Video playback speed.<br>**NOTE**<br>The value of the number type can only be **0.75**, **1.0**, **1.25**, **1.75**, or **2.0**.<br>Default value: 1.0 \| PlaybackSpeed.Speed_Forward_1_00_X | | currentProgressRate | number \| string \| PlaybackSpeed<sup>8+</sup> | No | Video playback speed.<br>**NOTE**<br>The value of the number type can only be **0.75**, **1.0**, **1.25**, **1.75**, or **2.0**.<br>Default value: 1.0 \| PlaybackSpeed.Speed_Forward_1_00_X |
| previewUri | string \|PixelMap \| [Resource](ts-types.md) | No | Path of the preview image. (The PixelMap type is not supported currently.) | | previewUri | string \|PixelMap \| [Resource](ts-types.md) | No | Path of the preview image. |
| controller | [VideoController](#videocontroller) | No | Video controller. | | controller | [VideoController](#videocontroller) | No | Video controller. |
## PlaybackSpeed<sup>8+</sup> ## PlaybackSpeed<sup>8+</sup>
......
...@@ -3,12 +3,12 @@ ...@@ -3,12 +3,12 @@
The framework provides four pixel units, with vp as the reference data unit. The framework provides four pixel units, with vp as the reference data unit.
| Name | Description | | Name| Description |
| ---- | ---------------------------------------- | | ---- | ------------------------------------------------------------ |
| px | Physical pixel unit of the screen. | | px | Physical pixel unit of the screen. |
| vp | Pixel unit specific to the screen density. Pixels in this unit are converted into physical pixels of the screen based on the screen pixel density. This unit is used for values whose unit is not specified.| | vp | Pixel unit specific to the screen density. Pixels in this unit are converted into physical pixels of the screen based on the screen pixel density. This unit is used for values whose unit is not specified. On a screen with an actual width of 1440 physical pixels, 1 vp is approximately equal to 3 px.|
| fp | Font pixel, which is similar to vp and varies according to the system font size. | | fp | Font pixel, which is similar to vp and varies according to the system font size.|
| lpx | Logical pixel unit of the window. It is the ratio of the actual screen width to the logical width (configured by **designWidth**). For example, if **designWidth** is set to **720** (default value), then 1lpx is equal to 2px for a screen with an actual width of 1440 physical pixels.| | lpx | Logical pixel unit of the window. It is the ratio of the actual screen width to the logical width (configured by **designWidth**). For example, if **designWidth** is set to **720** (default value), then 1 lpx is equal to 2 px for a screen with an actual width of 1440 physical pixels.|
## Pixel Unit Conversion ## Pixel Unit Conversion
...@@ -37,33 +37,71 @@ struct Example { ...@@ -37,33 +37,71 @@ struct Example {
Flex({ wrap: FlexWrap.Wrap }) { Flex({ wrap: FlexWrap.Wrap }) {
Column() { Column() {
Text("width(220)") Text("width(220)")
.width(220).height(40).backgroundColor(0xF9CF93) .width(220)
.textAlign(TextAlign.Center).fontColor(Color.White).fontSize('12vp') .height(40)
.backgroundColor(0xF9CF93)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.fontSize('12vp')
}.margin(5) }.margin(5)
Column() { Column() {
Text("width('220px')") Text("width('220px')")
.width('220px').height(40).backgroundColor(0xF9CF93) .width('220px')
.textAlign(TextAlign.Center).fontColor(Color.White) .height(40)
.backgroundColor(0xF9CF93)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
}.margin(5) }.margin(5)
Column() { Column() {
Text("width('220vp')") Text("width('220vp')")
.width('220vp').height(40).backgroundColor(0xF9CF93) .width('220vp')
.textAlign(TextAlign.Center).fontColor(Color.White).fontSize('12vp') .height(40)
.backgroundColor(0xF9CF93)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.fontSize('12vp')
}.margin(5) }.margin(5)
Column() { Column() {
Text("width('220lpx') designWidth:720") Text("width('220lpx') designWidth:720")
.width('220lpx').height(40).backgroundColor(0xF9CF93) .width('220lpx')
.textAlign(TextAlign.Center).fontColor(Color.White).fontSize('12vp') .height(40)
.backgroundColor(0xF9CF93)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.fontSize('12vp')
}.margin(5) }.margin(5)
Column() { Column() {
Text("width(vp2px(220) + 'px')") Text("width(vp2px(220) + 'px')")
.width(vp2px(220) + 'px').height(40).backgroundColor(0xF9CF93) .width(vp2px(220) + 'px')
.textAlign(TextAlign.Center).fontColor(Color.White).fontSize('12vp') .height(40)
.backgroundColor(0xF9CF93)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.fontSize('12vp')
}.margin(5) }.margin(5)
Column() { Column() {
Text("fontSize('12fp')") Text("fontSize('12fp')")
.width(220).height(40).backgroundColor(0xF9CF93) .width(220)
.textAlign(TextAlign.Center).fontColor(Color.White).fontSize('12fp') .height(40)
.backgroundColor(0xF9CF93)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.fontSize('12fp')
}.margin(5)
Column() {
Text("width(px2vp(220))")
.width(px2vp(220))
.height(40)
.backgroundColor(0xF9CF93)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.fontSize('12fp')
}.margin(5) }.margin(5)
}.width('100%') }.width('100%')
} }
......
# UI Development # UI Development
- [ArkUI Overview](arkui-overview.md) - [ArkUI Overview](arkui-overview.md)
- ArkTS-based Declarative Development Paradigm - UI Development (ArkTS-based Declarative Development Paradigm)
- [Overview](ui-ts-overview.md) - [UI Development (ArkTS-based Declarative Development Paradigm) Overview](arkts-ui-development-overview.md)
- [Declarative UI Development Guidelines](ui-ts-developing-intro.md) - Layout Development
- Declarative UI Development Examples - [Layout Overview](arkts-layout-development-overview.md)
- [Creating a Simple Page](ui-ts-creating-simple-page.md) - Building a Layout
- Building a Comprehensive Example - [Linear Layout](arkts-layout-development-linear.md)
- [Building a Food Data Model](ui-ts-building-data-model.md) - [Stack Layout](arkts-layout-development-stack-layout.md)
- [Building a Food Category List Layout](ui-ts-building-category-list-layout.md) - [Flex Layout](arkts-layout-development-flex-layout.md)
- [Building a Food Category Grid Layout](ui-ts-building-category-grid-layout.md) - [Relative Layout](arkts-layout-development-relative-layout.md)
- [Implementing Page Redirection and Data Transmission](ui-ts-page-redirection-data-transmission.md) - [Responsive Grid Layout](arkts-layout-development-grid-layout.md)
- Adding a Splash Screen Animation - [Media Query](arkts-layout-development-media-query.md)
- [Using the Drawing Feature](ui-ts-drawing-feature.md) - [Creating a List](arkts-layout-development-create-list.md)
- [Using the Animation Feature](ui-ts-animation-feature.md) - [Creating a Grid](arkts-layout-development-create-grid.md)
- [Common Components](ui-ts-components-intro.md) - [Creating a Swiper](arkts-layout-development-create-looping.md)
- Common Layout Development - [Improving Layout Performance](arkts-layout-development-performance-boost.md)
- Adaptive Layouts - Adding a Component
- [Linear Layout](ui-ts-layout-linear.md) - Adding a Common Component
- [Statck Layout](ui-ts-layout-stack.md) - [Button](arkts-common-components-button.md)
- [Flex Layout](ui-ts-layout-flex.md) - [Radio Button](arkts-common-components-radio-button.md)
- [Grid Layout](ui-ts-layout-grid.md) - [Toggle](arkts-common-components-switch.md)
- Responsive Layouts - [Progress Indicator](arkts-common-components-progress-indicator.md)
- [Grid Layout](ui-ts-layout-grid-container-new.md) - [Text Display](arkts-common-components-text-display.md)
- [Media Query](ui-ts-layout-mediaquery.md) - [Text Input](arkts-common-components-text-input.md)
- [Custom Component Lifecycle Callbacks](ui-ts-custom-component-lifecycle-callbacks.md) - [Custom Dialog Box](arkts-common-components-custom-dialog.md)
- [Web Component Development](ui-ts-components-web.md) - [Video Playback](arkts-common-components-video-player.md)
- [Recommendations for Improving Performance](ui-ts-performance-improvement-recommendation.md) - [XComponent](arkts-common-components-xcomponent.md)
- JavaScript-compatible Web-like Development Paradigm - Adding a Bubble and Menu
- [Overview](ui-js-overview.md) - [Bubble](arkts-popup-and-menu-components-popup.md)
- [Menu](arkts-popup-and-menu-components-menu.md)
- Setting Page Routing and Component Navigation
- [Page Routing](arkts-routing.md)
- Component Navigation
- [Navigation](arkts-navigation-navigation.md)
- [Tabs](arkts-navigation-tabs.md)
- Using Graphics
- [Displaying Images](arkts-graphics-display.md)
- [Drawing Geometric Shapes](arkts-geometric-shape-drawing.md)
- [Drawing Custom Graphics on the Canvas](arkts-drawing-customization-on-canvas.md)
- Using Animation
- [Animation Overview](arkts-animation-overview.md)
- Animation Within a Page
- [Layout Update Animation](arkts-layout-update-animation.md)
- [Transition Animation Within a Component](arkts-transition-animation-within-component.md)
- [Spring Curve Animation](arkts-spring-animation.md)
- Animation Between Pages
- [Zoom Animation](arkts-zoom-animation.md)
- [Page Transition Animation](arkts-page-transition-animation.md)
- Using Interaction Events
- [Interaction Event Overview](arkts-event-overview.md)
- Universal Events
- [Touchscreen Event](arkts-common-events-touch-screen-event.md)
- [Keyboard and Mouse Event](arkts-common-events-device-input-event.md)
- [Focus Event](arkts-common-events-focus-event.md)
- Gesture Events
- [Gesture Binding](arkts-gesture-events-binding.md)
- [Single Gesture](arkts-gesture-events-single-gesture.md)
- [Combined Gestures](arkts-gesture-events-combined-gestures.md)
- [Recommendations for Improving Performance](arkts-performance-improvement-recommendation.md)
- UI Development (JavaScript-compatible Web-like Development Paradigm)
- [UI Development (JavaScript-compatible Web-like Development Paradigm) Overview](ui-js-overview.md)
- Framework Overview - Framework Overview
- [File Organization](js-framework-file.md) - [File Organization](js-framework-file.md)
- ["js" Tag](js-framework-js-tag.md) - ["js" Tag](js-framework-js-tag.md)
......
# Animation Overview
The principle of animation is that the UI appearance is changed for multiple times within a period of time. Because human eyes retain persistence of vision, what you finally see is a continuous animation. A change of the UI is called an animation frame, which corresponds to a screen refresh. An important indicator that determines the animation smoothness is the frame rate (FPS), that is, the number of animation frames per second. The higher the frame rate, the smoother the animation.
In ArkUI, an animation is generated by changing the attribute value and specifying the animation parameters. Animation parameters include animation duration, change rule (that is, curve), and more. After the attribute value changes, the original state is transited to the new state according to the animation parameters. In this way, an animation is formed.
The animation capability provided by ArkUI can be classified into intra-page animation and inter-page animation based on the page classification mode. As shown in the following figure, an animation on a page refers to an animation that can occur on a page, and an animation between pages refers to an animation that occurs only with redirection between pages.
**Figure 1** Animation by page
![en-us_image_0000001562700385](figures/en-us_image_0000001562700385.png)
By capability, the animation can be divided into three parts: attribute animation, explicit animation, and transition animation, as shown in the following figure.
**Figure 2** Animation classified by basic capability
![en-us_image_0000001562820753](figures/en-us_image_0000001562820753.png)
This topic will introduce you to the usage and precautions of animations by the preceding classification and use cases.
# Button
The **\<Button>** component is usually activated by user clicks to perform a specific action. Buttons are classified as capsule, circle, or normal buttons. When used as a container, the **\<Button>** component accepts child components such as text and images. For details, see [Button](../reference/arkui-ts/ts-basic-components-button.md).
## Creating a Button
You can create a button that contains or does not contain child components.
- Create a button that does not contain child components.
```ts
Button(label?: string, options?: { type?: ButtonType, stateEffect?: boolean })
```
Creates a button that does not contain child components. In this API, **label** indicates the button text, **type** indicates the button type, and **stateEffect** specifies whether to set pressed effect on the click of the button.
```ts
Button('Ok', { type: ButtonType.Normal, stateEffect: true })
.borderRadius(8)
.backgroundColor(0x317aff)
.width(90)
.height(40)
```
![en-us_image_0000001562820757](figures/en-us_image_0000001562820757.png)
- Create a button that contains child components.
```ts
Button(options?: {type?: ButtonType, stateEffect?: boolean})
```
Creates a button that contains a single child component, which can either be a [basic component](../reference/arkui-ts/ts-basic-components-blank.md) or a [container component](../reference/arkui-ts/ts-container-ability-component.md).
```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)
```
![en-us_image_0000001511421216](figures/en-us_image_0000001511421216.png)
## Setting the Button Type
Use the **type** parameter to set the button type to **Capsule**, **Circle**, or **Normal**.
- Capsule button (default type)
Buttons of this type have rounded corners whose radius is automatically set to half of the button height. The rounded corners cannot be reset through the **borderRadius** attribute.
```ts
Button('Disable', { type: ButtonType.Capsule, stateEffect: false })
.backgroundColor(0x317aff)
.width(90)
.height(40)
```
![en-us_image_0000001511421208](figures/en-us_image_0000001511421208.png)
- Circle button
Buttons of this type are round. The rounded corners cannot be reset through the **borderRadius** attribute.
```ts
Button('Circle', { type: ButtonType.Circle, stateEffect: false })
.backgroundColor(0x317aff)
.width(90)
.height(90)
```
![en-us_image_0000001511740428](figures/en-us_image_0000001511740428.png)
- Normal button
Buttons of this type have rounded corners set to 0. The rounded corners can be reset through the **borderRadius** attribute.
```ts
Button('Ok', { type: ButtonType.Normal, stateEffect: true })
.borderRadius(8)
.backgroundColor(0x317aff)
.width(90)
.height(40)
```
![en-us_image_0000001563060641](figures/en-us_image_0000001563060641.png)
## Setting Styles
- Set the border radius:
In general cases, you can use universal attributes to define the button styles. For example, you can use the **borderRadius** attribute to set the border radius.
```ts
Button('circle border', { type: ButtonType.Normal })
.borderRadius(20)
.height(40)
```
![en-us_image_0000001511900392](figures/en-us_image_0000001511900392.png)
- The **Font** type is used to set the text style.
Add a font style for text displayed on the button.
```ts
Button('font style', { type: ButtonType.Normal })
.fontSize(20)
.fontColor(Color.Pink)
.fontWeight(800)
```
![en-us_image_0000001511580828](figures/en-us_image_0000001511580828.png)
- Set the background color:
You can do so by adding the **backgroundColor** attribute.
```ts
Button('background color').backgroundColor(0xF55A42)
```
![en-us_image_0000001562940477](figures/en-us_image_0000001562940477.png)
- Assign a function to the button:
In this example, the delete function is assigned to the button.
```ts
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image($r('app.media.ic_public_delete_filled')).width(30).height(30)
}.width(55).height(55).margin({ left: 20 }).backgroundColor(0xF55A42)
```
![en-us_image_0000001511740436](figures/en-us_image_0000001511740436.png)
## Adding Events
The **\<Button>** component is usually used to trigger actions. You can bind the **onClick** event to the button to have it respond with custom behavior after being clicked.
```ts
Button('Ok', { type: ButtonType.Normal, stateEffect: true })
.onClick(()=>{
console.info('Button onClick')
})
```
## Example Scenario
- Using the Button for Startup
You can use the button for any UI element that involves the startup operation. The button triggers the predefined event based on the user's operation. For example, you can use a button in the **\<List>** container to redirect the user to another page.
```ts
// xxx.ets
import router from '@ohos.router';
@Entry
@Component
struct ButtonCase1 {
build() {
List({ space: 4 }) {
ListItem() {
Button("First").onClick(() => {
router.pushUrl({ url: 'pages/first_page' })
})
.width('100%')
}
ListItem() {
Button("Second").onClick(() => {
router.pushUrl({ url: 'pages/second_page' })
})
.width('100%')
}
ListItem() {
Button("Third").onClick(() => {
router.pushUrl({ url: 'pages/third_page' })
})
.width('100%')
}
}
.listDirection(Axis.Vertical)
.backgroundColor(0xDCDCDC).padding(20)
}
}
```
![en-us_image_0000001562700393](figures/en-us_image_0000001562700393.png)
- Use the button for submitting forms:
On the user login/registration page, you can use a button to submit a login or registration request.
```ts
// xxx.ets
@Entry
@Component
struct ButtonCase2 {
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(() => {
// Operation
})
}.padding(20)
}
}
```
![en-us_image_0000001562940473](figures/en-us_image_0000001562940473.png)
- Configure the button to float:
The button can remain floating when the user swipes on the screen.
```ts
// xxx.ets
@Entry
@Component
struct HoverButtonExample {
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
build() {
Stack() {
List({ space: 20, initialIndex: 0 }) {
ForEach(this.arr, (item) => {
ListItem() {
Text('' + item)
.width('100%').height(100).fontSize(16)
.textAlign(TextAlign.Center).borderRadius(10).backgroundColor(0xFFFFFF)
}
}, item => item)
}.width('90%')
Button() {
Image($r('app.media.ic_public_add'))
.width(50)
.height(50)
}
.width(60)
.height(60)
.position({x: '80%', y: 600})
.shadow({radius: 10})
.onClick(() => {
// Operation
})
}
.width('100%')
.height('100%')
.backgroundColor(0xDCDCDC)
.padding({ top: 5 })
}
}
```
![GIF](figures/GIF.gif)
# Custom Dialog Box
A custom dialog box is a dialog box you customize by using APIs of the **CustomDialogController** class. It can be used for user interactions, showing an ad, prize, alert, software update message, and more. For details, see [Custom Dialog Box](../reference/arkui-ts/ts-methods-custom-dialog-box.md).
## Creating a Custom Dialog Box
1. Use the \@CustomDialog decorator to create a custom dialog box.
2. Set the content for the \@CustomDialog decorated dialog box.
```ts
@CustomDialog
struct CustomDialogExample {
controller: CustomDialogController
build() {
Column() {
Text ('I am content')
.fontSize(20)
.margin({ top: 10, bottom: 10 })
}
}
}
```
3. Create a builder that is bound to the decorator.
```ts
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({}),
})
```
4. Click the component bound to the **onClick** event to display the dialog box.
```ts
Flex({justifyContent:FlexAlign.Center}){
Button('click me')
.onClick(() => {
this.dialogController.open()
})
}.width('100%')
```
![en-us_image_0000001562700493](figures/en-us_image_0000001562700493.png)
## Interaction with Custom Dialog Box
Custom dialog boxes can be used for data interactions to complete a series of response operations.
1. Add button operations to the \@CustomDialog decorator and add the creation of data functions.
```ts
@CustomDialog
struct CustomDialogExample {
controller: CustomDialogController
cancel: () => void
confirm: () => void
build() {
Column() {
Text('I am content') .fontSize(20).margin({ top: 10, bottom: 10 })
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Button('cancel')
.onClick(() => {
this.controller.close()
this.cancel()
}).backgroundColor(0xffffff).fontColor(Color.Black)
Button('confirm')
.onClick(() => {
this.controller.close()
this.confirm()
}).backgroundColor(0xffffff).fontColor(Color.Red)
}.margin({ bottom: 10 })
}
}
}
```
2. Receive the page in the builder and create corresponding function operations.
```ts
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
cancel: this.onCancel,
confirm: this.onAccept,
}),
alignment: DialogAlignment.Default, // Set the alignment mode of the dialog box. By default, the dialog box is displayed at the bottom.
})
onCancel() {
console.info('Callback when the first button is clicked')
}
onAccept() {
console.info('Callback when the second button is clicked')
}
```
![en-us_image_0000001511421320](figures/en-us_image_0000001511421320.png)
# Progress Indicator
The **\<Progress>** component is used to provide a progress indicator that displays the progress of an operation. For details, see [Progress](../reference/arkui-ts/ts-basic-components-progress.md).
## Creating a Progress Indicator
You can create a progress indicator by calling the following API:
```ts
Progress(options: {value: number, total?: number, type?: ProgressType})
```
Creates a progress bar indicator. In this API, **value** indicates the initial progress, **total** indicates the total progress, and **type** indicates the style of the progress indicator.
```ts
Progress({ value: 24, total: 100, type: ProgressType.Linear }) // Create a linear progress indicator whose total progress is 100 and initial progress is 24.
```
![create](figures/create.png)
## Setting the Progress Indicator Style
Progress indicators come in five styles. When creating a progress indicator, you can specify its style by setting the **ProgressType** parameter to any of the following: **ProgressType.Linear** (linear style), **ProgressType.Ring** (indeterminate ring style), **ProgressType.ScaleRing** (determinate ring style), **ProgressType.Eclipse** (eclipse style), and **ProgressType.Capsule** (capsule style).
- Linear style (default style)
>**NOTE**
>
> Since API version 9, the progress indicator adaptively switches to the vertical layout if the height is greater than the width and remains the horizontal layout if the height is equal to the width.
```ts
Progress({ value: 20, total: 100, type: ProgressType.Linear }).width(200).height(50)
Progress({ value: 20, total: 100, type: ProgressType.Linear }).width(50).height(200)
```
![en-us_image_0000001562700417](figures/en-us_image_0000001562700417.png)
- Indeterminate ring style
```ts
// The progress indicator in the indeterminate ring style on the left: Retain its default settings for the foreground color (blue) and stroke width (2.0 vp).
Progress({ value: 40, total: 150, type: ProgressType.Ring }).width(100).height(100)
// The right progress indicator in the indeterminate ring style on the right.
Progress({ value: 40, total: 150, type: ProgressType.Ring }).width(100).height(100)
.color(Color.Grey) // Set the foreground color to gray.
.style({ strokeWidth: 15}) // Set the stroke width to 15.0 vp.
```
![progress_ring](figures/progress_ring.png)
- Determinate ring style
```ts
Progress({ value: 20, total: 150, type: ProgressType.ScaleRing }).width(100).height(100)
.backgroundColor(Color.Black)
.style({ scaleCount: 20, scaleWidth: 5 }) // Set the total number of scales to 20 and the scale width to 5 vp.
Progress({ value: 20, total: 150, type: ProgressType.ScaleRing }).width(100).height(100)
.backgroundColor(Color.Black)
.style({ strokeWidth: 15, scaleCount: 20, scaleWidth: 5 }) // Set the stroke width to 15, the total number of scales to 20, and the scale width to 5 vp.
Progress({ value: 20, total: 150, type: ProgressType.ScaleRing }).width(100).height(100)
.backgroundColor(Color.Black)
.style({ strokeWidth: 15, scaleCount: 20, scaleWidth: 3 }) // Set the stroke width to 15, the total number of scales to 20, and the scale width to 3 vp.
```
![progress_scalering](figures/progress_scalering.png)
- Eclipse style
```ts
// The progress indicator in the eclipse style on the left: Retain its default settings for the foreground color (blue).
Progress({ value: 10, total: 150, type: ProgressType.Eclipse }).width(100).height(100)
// The progress indicator in the eclipse style on the right: Set its foreground color to gray.
Progress({ value: 20, total: 150, type: ProgressType.Eclipse }).color(Color.Grey).width(100).height(100)
```
![progress_circle](figures/progress_circle.png)
- Capsule style
>**NOTE**
>
>- At both ends, the progress indicator in the capsule style works in a same manner as that in the eclipse style.
>- In the middle part of the capsule, the progress indicator works in a same manner as the linear style.
>
>- If the height is greater than the width, the progress indicator adaptively switches to the vertical layout.
```ts
Progress({ value: 10, total: 150, type: ProgressType.Capsule }).width(100).height(50)
Progress({ value: 20, total: 150, type: ProgressType.Capsule }).width(50).height(100).color(Color.Grey)
Progress({ value: 50, total: 150, type: ProgressType.Capsule }).width(50).height(100).backgroundColor(Color.Black)
```
![progress_captule](figures/progress_captule.png)
## Example Scenario
In this example, the progress of the **\<Progress>** component is updated by clicking the button. After the button is clicked, the value of **progressValue** is incremented and passed to the **\<Progress>** component, which is then updated accordingly.
```ts
@Entry
@Component
struct ProgressCase1 {
@State progressValue: number = 0 // Set the initial progress of the progress indicator to 0.
build() {
Column() {
Column() {
Progress({value:0, total:100, type:ProgressType.Capsule}).width(200).height(50)
.style({strokeWidth:50}).value(this.progressValue)
Row().width('100%').height(5)
Button ("Progress + 5")
.onClick(()=>{
this.progressValue += 5
if (this.progressValue > 100){
this.progressValue = 0
}
})
}
}.width('100%').height('100%')
}
}
```
![progress](figures/progress.gif)
# Radio Button
The **\<Radio>** component allows users to select from a set of mutually exclusive options. Only one radio button in a given group can be selected at the same time. For details, see [Radio](../reference/arkui-ts/ts-basic-components-radio.md).
## Creating a Radio Button
You can create a radio button by calling the following API:
```ts
Radio(options: {value: string, group: string})
```
Creates a radio button. In this API, **value** indicates the name of the radio button, and **group** indicates the name of the group to which the radio button belongs. You can use the **checked** attribute of the radio button to specify whether it is selected. The value **true** means that the radio button is selected. The color and shape cannot be customized for the radio button.
```ts
Radio({ value: 'Radio1', group: 'radioGroup' })
.checked(false)
Radio({ value: 'Radio2', group: 'radioGroup' })
.checked(true)
```
![en-us_image_0000001562820821](figures/en-us_image_0000001562820821.png)
## Adding Events
The **\<Radio>** component supports the [universal events](../reference/arkui-ts/ts-universal-events-click.md). In addition, it can be bound to the **onChange** event so that it responds with custom behavior after being selected.
```ts
Radio({ value: 'Radio1', group: 'radioGroup' })
.onChange((isChecked: boolean) => {
if(isChecked) {
// Operation
}
})
Radio({ value: 'Radio2', group: 'radioGroup' })
.onChange((isChecked: boolean) => {
if(isChecked) {
// Operation
}
})
```
## Example Scenario
In this example, the **\<Radio>** components are used to switch between sound modes.
```ts
// xxx.ets
import promptAction from '@ohos.promptAction';
@Entry
@Component
struct RadioExample {
build() {
Row() {
Column() {
Radio({ value: 'Radio1', group: 'radioGroup' }).checked(true)
.height(50)
.width(50)
.onChange((isChecked: boolean) => {
if(isChecked) {
// Switch to the ringing mode.
promptAction.showToast({ message: 'Ringing mode.' })
}
})
Text('Ringing')
}
Column() {
Radio({ value: 'Radio2', group: 'radioGroup' })
.height(50)
.width(50)
.onChange((isChecked: boolean) => {
if(isChecked) {
// Switch to the vibration mode.
promptAction.showToast({ message: 'Vibration mode.' })
}
})
Text('Vibration')
}
Column() {
Radio({ value: 'Radio3', group: 'radioGroup' })
.height(50)
.width(50)
.onChange((isChecked: boolean) => {
if(isChecked) {
// Switch to the silent mode.
promptAction.showToast({ message: 'Silent mode.' })
}
})
Text('Silent')
}
}.height('100%').width('100%').justifyContent(FlexAlign.Center)
}
}
```
![en-us_image_0000001562700457](figures/en-us_image_0000001562700457.png)
# Text Display
The **\<Text>** component is used to display a piece of textual information. For details, see [Text](../reference/arkui-ts/ts-basic-components-text.md).
## Creating Text
You can create text in either of the following ways:
- Entering strings
```ts
Text ('I am a piece of text')
```
![en-us_image_0000001563060685](figures/en-us_image_0000001563060685.png)
- Referencing Resource objects
You can use **$r** to create a **Resource** object to reference resources in **/resources/base/element/string.json**.
```ts
Text($r('app.string.module_desc'))
.baselineOffset(0)
.fontSize(30)
.border({ width: 1 })
.padding(10)
.width(300)
```
![en-us_image_0000001511580872](figures/en-us_image_0000001511580872.png)
## Adding Child Components
The **\<Text>** component accepts \<[Span](../reference/arkui-ts/ts-basic-components-span.md)> as its child component. You can add one or more **\<Span>** child components to a **\<Text>** component to display a piece of information, such as the product description and statement of commitment.
- Creating a \<Span> Component
The **\<Span>** component works only when included in a **\<Text>** component. If both the **\<Span>** and **\<Text>** components have text configured, the text of the **\<Span>** overwrites that of the **\<Text>** component.
```ts
Text (' I'm Text') {
Span (' I'm Span')
}
.padding(10)
.borderWidth(1)
```
![en-us_image_0000001562700441](figures/en-us_image_0000001562700441.png)
- Set the text decorative line.
Use the **decoration** attribute to set the style and color of the text decorative line.
```ts
Text() {
Span('I'm Span1,') .fontSize (16).fontColor (Color.Grey)
.decoration({ type: TextDecorationType.LineThrough, color: Color.Red })
Span('I'm Span2').fontColor (Color.Blue).fontSize (16)
.fontStyle(FontStyle.Italic)
.decoration({ type: TextDecorationType.Underline, color: Color.Black })
Span('I'm Span3').fontSize(16).fontColor(Color.Grey)
.decoration({ type: TextDecorationType.Overline, color: Color.Green })
}
.borderWidth(1)
.padding(10)
```
![en-us_image_0000001562700437](figures/en-us_image_0000001562700437.png)
- Use the **textCase** attribute to set the text case.
```ts
Text() {
Span('I am Upper-span').fontSize(12)
.textCase(TextCase.UpperCase)
}
.borderWidth(1)
.padding(10)
```
![en-us_image_0000001562940525](figures/en-us_image_0000001562940525.png)
- Adding Events
The **\<Span>** component does not have size information. Therefore, only the **onClick** event is supported.
```ts
Text() {
Span('I am Upper-span').fontSize(12)
.textCase(TextCase.UpperCase)
.onClick(()=>{
console.info (' I'm Span - onClick')
})
}
```
## Setting Styles
- Use the **textAlign** attribute to set the alignment mode of text.
```ts
Text('Left aligned')
.width(300)
.textAlign(TextAlign.Start)
.border({ width: 1 })
.padding(10)
Text ('Center aligned')
.width(300)
.textAlign(TextAlign.Center)
.border({ width: 1 })
.padding(10)
Text('Right aligned')
.width(300)
.textAlign(TextAlign.End)
.border({ width: 1 })
.padding(10)
```
![en-us_image_0000001511421260](figures/en-us_image_0000001511421260.png)
- Use the **textOverflow** attribute to set the display mode for when the text is too long. This attribute must be used together with **maxLines**. By default, the text is automatically folded.
```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 extra long text, with an ellipse displayed for any excess.')
.width(250)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(1)
.fontSize(12)
.border({ width: 1 }).padding(10)
```
![en-us_image_0000001563060693](figures/en-us_image_0000001563060693.png)
![en-us_image_0000001563060701](figures/en-us_image_0000001563060701.png)
- Use the **lineHeight** attribute to set the text line height.
```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)
```
![en-us_image_0000001511740480](figures/en-us_image_0000001511740480.png)
- Use the **decoration** attribute to set the style and color of the text decorative line.
```ts
Text('This is the text')
.decoration({
type: TextDecorationType.LineThrough,
color: Color.Red
})
.borderWidth(1).padding(10).margin(5)
Text('This is the text')
.decoration({
type: TextDecorationType.Overline,
color: Color.Red
})
.borderWidth(1).padding(10).margin(5)
Text('This is the text')
.decoration({
type: TextDecorationType.Underline,
color: Color.Red
})
.borderWidth(1).padding(10).margin(5)
```
![en-us_image_0000001511580888](figures/en-us_image_0000001511580888.png)
- Use the **baselineOffset** attribute to set the baseline offset of the text.
```ts
Text('This is the text content with baselineOffset 0.')
.baselineOffset(0)
.fontSize(12)
.border({ width: 1 })
.padding(10)
.width('100%')
.margin(5)
Text('This is the text content with baselineOffset 30.')
.baselineOffset(30)
.fontSize(12)
.border({ width: 1 })
.padding(10)
.width('100%')
.margin(5)
Text('This is the text content with baselineOffset -20.')
.baselineOffset(-20)
.fontSize(12)
.border({ width: 1 })
.padding(10)
.width('100%')
.margin(5)
```
![en-us_image_0000001562820789](figures/en-us_image_0000001562820789.png)
- Use the **letterSpacing** attribute to set the letter spacing.
```ts
Text('This is the text content with letterSpacing 0.')
.letterSpacing(0)
.fontSize(12)
.border({ width: 1 })
.padding(10)
.width('100%')
.margin(5)
Text('This is the text content with letterSpacing 3.')
.letterSpacing(3)
.fontSize(12)
.border({ width: 1 })
.padding(10)
.width('100%')
.margin(5)
Text('This is the text content with letterSpacing -1.')
.letterSpacing(-1)
.fontSize(12)
.border({ width: 1 })
.padding(10)
.width('100%')
.margin(5)
```
![en-us_image_0000001562940513](figures/en-us_image_0000001562940513.png)
- Use the **minFontSize** and **maxFontSize** attributes to set the minimum and maximum font size, respectively. For the settings to take effect, these attributes must be used together with **maxLines** or layout constraint settings.
```ts
Text('My maximum font size is 30, minimum font size is 5, width is 250, and maximum number of lines is 1')
.width(250)
.maxLines(1)
.maxFontSize(30)
.minFontSize(5)
.border({ width: 1 })
.padding(10)
.margin(5)
Text('My maximum font size is 30, minimum font size is 5, width is 250, and maximum number of lines is 2')
.width(250)
.maxLines(2)
.maxFontSize(30)
.minFontSize(5)
.border({ width: 1 })
.padding(10)
.margin(5)
Text('My maximum font size is 30, minimum font size is 15, width is 250, and line height is 50')
.width(250)
.height(50)
.maxFontSize(30)
.minFontSize(15)
.border({ width: 1 })
.padding(10)
.margin(5)
Text('My maximum font size is 30, minimum font size is 15, width is 250, and line height is 100')
.width(250)
.height(100)
.maxFontSize(30)
.minFontSize(15)
.border({ width: 1 })
.padding(10)
.margin(5)
```
![en-us_image_0000001511740472](figures/en-us_image_0000001511740472.png)
- Use the **textCase** attribute to set the text case.
```ts
Text('This is the text content with textCase set to Normal.')
.textCase(TextCase.Normal)
.padding(10)
.border({ width: 1 })
.padding(10)
.margin(5)
// The text is displayed in lowercase.
Text('This is the text content with textCase set to LowerCase.')
.textCase(TextCase.LowerCase)
.border({ width: 1 })
.padding(10)
.margin(5)
// The text is displayed in uppercase.
Text('This is the text content with textCase set to UpperCase.')
.textCase(TextCase.UpperCase)
.border({ width: 1 })
.padding(10)
.margin(5)
```
![en-us_image_0000001562940529](figures/en-us_image_0000001562940529.png)
- Use the **copyOption** attribute to set whether copy and paste is allowed.
```ts
Text("This text is copyable")
.fontSize(30)
.copyOption(CopyOptions.InApp)
```
![en-us_image_0000001511580868](figures/en-us_image_0000001511580868.png)
## Adding Events
The **\<Text>** component supports the [universal events](../reference/arkui-ts/ts-universal-events-click.md). It can be bound to the **onClick**, **onTouch**, or other events to respond to user operations.
```ts
Text ('Click Me')
.onClick(()=>{
console.info('I am the response to the click event');
})
```
## Example Scenario
```ts
// xxx.ets
@Entry
@Component
struct TextExample {
build() {
Column() {
Row() {
Text("1").fontSize(14).fontColor(Color.Red).margin({ left: 10, right: 10 })
Text("I am entry 1")
.fontSize(12)
.fontColor(Color.Blue)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.fontWeight(300)
Text ("Top Hit")
.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("I am entry 2")
.fontSize(12)
.fontColor(Color.Blue)
.fontWeight(300)
.constraintSize({ maxWidth: 200 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text ("Hot")
.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("I am entry 3")
.fontSize(12)
.fontColor(Color.Blue)
.fontWeight(300)
.maxLines(1)
.constraintSize({ maxWidth: 200 })
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text ("Hot")
.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("I am entry 4")
.fontSize(12)
.fontColor(Color.Blue)
.fontWeight(300)
.constraintSize({ maxWidth: 200 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}.width('100%').margin(5)
}.width('100%')
}
}
```
![en-us_image_0000001562820805](figures/en-us_image_0000001562820805.png)
# Text Input
The **\<TextInput>** and **\<TextArea>** components are input components typically used to accept input from the user, such as comments, chat messages, and table content. They can be used in combination with other components to meet more diversified purposes, for example, login and registration. For details, see [TextInput](../reference/arkui-ts/ts-basic-components-textinput.md) and [TextArea](../reference/arkui-ts/ts-basic-components-textarea.md).
## Creating a Text Box
The **\<TextInput>** component provides single-line text input, while the **\<TextArea>** component provides multi-line text input. To create these components, use the following APIs:
```ts
TextArea(value?:{placeholder?: ResourceStr, text?: ResourceStr, controller?: TextAreaController})
```
```ts
TextInput(value?:{placeholder?: ResourceStr, text?: ResourceStr, controller?: TextInputController})
```
- Single-line text box
```ts
TextInput()
```
![en-us_image_0000001511580844](figures/en-us_image_0000001511580844.png)
- Multi-line text box
```ts
TextArea()
```
![en-us_image_0000001562940481](figures/en-us_image_0000001562940481.png)
The **\<TextArea>** component automatically wraps text so that each line does not have more than the width of the component.
```ts
TextArea({text:"I am TextArea I am TextArea I am TextArea"}).width(300)
```
![en-us_image_0000001511580836](figures/en-us_image_0000001511580836.png)
## Setting the Input Box Type
The **\<TextInput>** component comes in five types. You can specify its type by setting the **type** parameter to any of the following: **Normal**, **Password**, **Email**, **Number**, and **PhoneNumber**.
- Normal type (default type)
```ts
TextInput()
.type(InputType.Normal)
```
![en-us_image_0000001562820765](figures/en-us_image_0000001562820765.png)
- Password type
```ts
TextInput()
.type(InputType.Password)
```
![en-us_image_0000001511580840](figures/en-us_image_0000001511580840.png)
## Setting Styles
- Set the placeholder text displayed when there is no input.
TextInput({placeholder:'I am placeholder text'})
```ts
TextInput({placeholder:'I am placeholder text'})
```
![en-us_image_0000001511900400](figures/en-us_image_0000001511900400.png)
- Set the current text input.
```ts
TextInput({placeholder:'I am placeholder text',text:'I am current text input'})
```
![en-us_image_0000001562820761](figures/en-us_image_0000001562820761.png)
- Use **backgroundColor** to set the background color of the text box.
```ts
TextInput({placeholder:'I am placeholder text',text:'I am current text input'})
.backgroundColor(Color.Pink)
```
![en-us_image_0000001511740444](figures/en-us_image_0000001511740444.png)
More styles can be implemented by leveraging the [universal attributes](../reference/arkui-ts/ts-universal-attributes-size.md).
## Adding Events
You can add the **onChange** event for the text box to obtain its content changes. You can also add the universal events to implement user interactions.
```ts
TextInput()
.onChange((value: string) => {
console.info(value);
})
.onFocus(() => {
console.info ('Get Focus');
})
```
## Example Scenario
In this example, the text box is used to submit forms on the user login or registration page.
```ts
@Entry
@Component
struct TextInputSample {
build() {
Column() {
TextInput({ placeholder: 'input your username' }).margin({ top: 20 })
.onSubmit((EnterKeyType)=>{
console.info(EnterKeyType+'Enter key type')
})
TextInput({ placeholder: 'input your password' }).type(InputType.Password).margin({ top: 20 })
.onSubmit((EnterKeyType)=>{
console.info(EnterKeyType+'Enter key type')
})
Button('Sign in').width(150).margin({ top: 20 })
}.padding(20)
}
}
```
![en-us_image_0000001563060653](figures/en-us_image_0000001563060653.png)
# Video Playback
The **\<Video>** component is used to play a video and control its playback. It is usually used in video players and video list pages within applications. A video automatically plays once fully loaded. When the user clicks the video area, the video is paused and the playback progress bar is displayed. The user can drag the progress bar to the desired position. For details, see [Video](../reference/arkui-ts/ts-media-components-video.md).
## Creating a \<Video> Component
You can create a **\<Video>** component by calling the following API:
```ts
Video(value: {src?: string | Resource, currentProgressRate?: number | string | PlaybackSpeed, previewUri?: string | PixelMap | Resource, controller?: VideoController})
```
Creates a **\<Video>** component. In this API, **src** indicates the path of the video source, **currentProgressRate** indicates the video playback speed, **previewUri** indicates the path of the preview image, and **controller** indicates the video controller . For details about how to load a video, see [Loading Video](#loading-video).
## Loading Video
The **\<Video>** component supports both local and online videos.
### Loading a Local Video
- Common local video
To load a local video, specify the corresponding video file in the local **rawfile** directory, as shown in the following figure.
![en-us_image_0000001562700409](figures/en-us_image_0000001562700409.png)
Use **$rawfile()** to reference the video resource.
```ts
@Component
export struct VideoPlayer{
private controller:VideoController;
private previewUris: Resource = $r ('app.media.preview');
private innerResource: Resource = $rawfile('videoTest.mp4');
build(){
Column() {
Video({
src: this.innerResource,
previewUri: this.previewUris,
controller: this.controller
})
}
}
}
```
- Video provided by a [DataAbility](../application-models/dataability-overview.md), whose path contains the **dataability://** prefix<br>Ensure that the corresponding video resource exists.
```ts
@Component
export struct VideoPlayer{
private controller:VideoController;
private previewUris: Resource = $r ('app.media.preview');
private videosrc: string= 'dataability://device_id/com.domainname.dataability.videodata/video/10'
build(){
Column() {
Video({
src: this.videosrc,
previewUri: this.previewUris,
controller: this.controller
})
}
}
}
```
### Loading a Video in the Application Sandbox
To load a video in the application sandbox, use a string with the **file:///data/storage** prefix. Ensure that the application has the read permission to the files in the specified path.
```ts
@Component
export struct VideoPlayer {
private controller: VideoController;
private videosrc: string = 'file:///data/storage/el2/base/haps/entry/files/show.mp4'
build() {
Column() {
Video({
src: this.videosrc,
controller: this.controller
})
}
}
}
```
### Loading an Online Video
To load online videos, you must apply for the **ohos.permission.INTERNET** permission. For details about how to apply for the permission, see [Declaring Permissions](../security/accesstoken-guidelines.md). In this scenario, the **src** attribute indicates the URL of the online video.
```ts
@Component
export struct VideoPlayer{
private controller:VideoController;
private previewUris: Resource = $r ('app.media.preview');
private videosrc: string= 'https://www.example.com/example.mp4' // Replace the URL with that of the actual video to load.
build(){
Column() {
Video({
src: this.videosrc,
previewUri: this.previewUris,
controller: this.controller
})
}
}
}
```
## Adding Attributes
Use the [attributes](../reference/arkui-ts/ts-media-components-video.md#attributes) of the **\<Video>** component to control video playback. For example, you can set whether to mute the video and whether to display the video playback control bar.
```ts
@Component
export struct VideoPlayer {
private controller: VideoController;
build() {
Column() {
Video({
controller: this.controller
})
.muted(false) // Set whether to mute the video.
.controls(false) // Set whether to display the video playback control bar.
.autoPlay(false) // Set whether to enable auto play.
.loop(false) // Set whether to repeat the video.
.objectFit(ImageFit.Contain) // Set the video scale type.
}
}
}
```
## Adding Events
The **\<Video>** component supports various callback events in addition to the universal events. For details, see [Events](../reference/arkui-ts/ts-media-components-video.md#events).
```ts
@Entry
@Component
struct VideoPlayer{
private controller:VideoController;
private previewUris: Resource = $r ('app.media.preview');
private innerResource: Resource = $rawfile('videoTest.mp4');
build(){
Column() {
Video({
src: this.innerResource,
previewUri: this.previewUris,
controller: this.controller
})
.onUpdate((event) => { // Triggered when the playback progress changes.
console.info("Video update.");
})
.onPrepared((event) => { // Triggered when video preparation is complete.
console.info("Video prepared.");
})
.onError(() => { // Triggered when the video playback fails.
console.info("Video error.");
})
}
}
}
```
## Using the Video Controller
The video controller is used to control video playback. For details, see [VideoController](../reference/arkui-ts/ts-media-components-video.md#videocontroller).
- Default controller
The default controller supports four basic features: start playback, pause playback, set the video playback position, and play the video in full screen.
```ts
@Entry
@Component
struct VideoGuide {
@State videoSrc: Resource = $rawfile('videoTest.mp4')
@State previewUri: string = 'common/videoIcon.png'
@State curRate: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X
build() {
Row() {
Column() {
Video({
src: this.videoSrc,
previewUri: this.previewUri,
currentProgressRate: this.curRate
})
}
.width('100%')
}
.height('100%')
}
}
```
- Custom controller
To use a custom controller, disable the default controller, and then use components such as \<Button> and \<Slider> to customize the control and display. This type of controller is applicable to scenarios where customization requirements are involved.
```ts
@Entry
@Component
struct VideoGuide {
@State videoSrc: Resource = $rawfile('videoTest.mp4')
@State previewUri: string = 'common/videoIcon.png'
@State curRate: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X
@State isAutoPlay: boolean = false
@State showControls: boolean = true
@State sliderStartTime: string = '';
@State currentTime: number = 0;
@State durationTime: number = 0;
@State durationStringTime: string ='';
controller: VideoController = new VideoController()
build() {
Row() {
Column() {
Video({
src: this.videoSrc,
previewUri: this.previewUri,
currentProgressRate: this.curRate,
controller: this.controller
}).controls(false).autoPlay(true)
.onPrepared((event)=>{
this.durationTime = event.duration
})
.onUpdate((event)=>{
this.currentTime =event.time
})
Row() {
Text(JSON.stringify(this.currentTime) + 's')
Slider({
value: this.currentTime,
min: 0,
max: this.durationTime
})
.onChange((value: number, mode: SliderChangeMode) => {
this.controller.setCurrentTime(value);
}).width("90%")
Text(JSON.stringify(this.durationTime) + 's')
}
.opacity(0.8)
.width("100%")
}
.width('100%')
}
.height('40%')
}
}
```
## Remarks
The **\<Video>** component has encapsulated the basic capabilities of video playback. You do not need to create video instances or set and obtain video information. Simply set the data source and basic information to play videos. To customize video playback, see [Video Playback](../media/video-playback.md).
# Keyboard and Mouse Event
Keyboard and mouse events refer to the input events of the peripheral keyboard and mouse.
## Mouse Event
The supported mouse events include the events triggered by the peripheral mouse and touchpad.
Mouse events can trigger the following callbacks.
| Name | Description |
| ---------------------------------------- | ---------------------------------------- |
| onHover(event:&nbsp;(isHover:&nbsp;boolean)&nbsp;=&gt;&nbsp;void) | Triggered when the mouse cursor enters or leaves the component.<br>**isHover**: whether the mouse cursor hovers over the component. The value **true** means that the mouse cursor enters the component, and the value **false** means that the mouse cursor leaves the component.|
| onMouse(event:&nbsp;(event?:&nbsp;MouseEvent)&nbsp;=&gt;&nbsp;void) | Triggered when the component is clicked by a mouse button or the mouse cursor moves on the component. The **event** parameter indicates the timestamp, mouse button, action, coordinates of the clicked point on the entire screen, and coordinates of the clicked point relative to the component when the event is triggered.|
When the component is bound to the **onHover** callback, you can use the [hoverEffect](../reference/arkui-ts/ts-universal-attributes-hover-effect.md) attribute to set the hover effect of the component in hover state.
**Figure 1** Mouse event data flow
![en-us_image_0000001511900504](figures/en-us_image_0000001511900504.png)
When ArkUI receives the mouse event, it checks whether the mouse event concerns pressing, lifting, or moving of the left mouse button, and then responds accordingly.
- Yes: The mouse event is first converted into a touch event in the same position, and a collision test, gesture judgment, and callback response of the touch event are performed. The collision test and callback response of the mouse event are then performed.
- No: Only the collision test and callback response of the mouse event are performed.
>**NOTE**
>
>All touch events and gesture events that can be responded to by a single finger may be operated and responded by using the left mouse button. For example, to implement page redirection invoked by clicking a button with support for finger touches and left-clicks, you just need to bind one click event (**onClick**). If you want to implement different effects for the finger touch and the left-click, you can use the **source** parameter in the **onClick** callback to determine whether the current event is triggered by a finger or a mouse.
### onHover
```ts
onHover(event: (isHover?: boolean) => void)
```
Triggered when the mouse cursor enters or leaves the component. The **isHover** parameter indicates whether the mouse cursor hovers over the component. This event does not support custom bubbling settings. By default, event bubbling occurs between parent and child components.
If this API is bound to a component, it is triggered when the mouse cursor enters the component from outside and the value of **isHover** is **true**, or when the mouse cursor leaves the component and the value of **isHover** is **false**.
>**NOTE**
>
>Event bubbling is an event propagation in the document object model (DOM) when an event is first handled by an element and then bubbles up to its parent element.
```ts
// xxx.ets
@Entry
@Component
struct MouseExample {
@State isHovered: boolean = false;
build() {
Column() {
Button(this.isHovered ? 'Hovered!' : 'Not Hover')
.width(200).height(100)
.backgroundColor(this.isHovered ? Color.Green : Color.Gray)
.onHover((isHover: boolean) => { // Use the onHover API to listen for whether the mouse cursor is hovered over the component.
this.isHovered = isHover;
})
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
```
In this example, a **\<Button component>** is created, with the initial background color of gray and the content of **Not Hover**. The component is bound to the **onHover** callback. In the callback, **this.isHovered** is set to the callback parameter **isHover**.
When the cursor moves from outside the button to inside the button, the callback is invoked, the value of **isHover** changes to **true**, the value of **isHovered** changes to **true**, the background color of the component changes to **Color.Green**, and the content changes to **Hovered!**.
When the cursor moves from inside the button to outside the button, the callback is invoked, the value of **isHover** changes to **false**, and the component restores to its initial style.
![onHover](figures/onHover.gif)
### onMouse
```ts
onMouse(event: (event?: MouseEvent) => void)
```
Triggered when a mouse event occurs. It is triggered each time an action by the mouse cursor (**MouseAction**) is detected in the component. The parameter is a [MouseEvent](../reference/arkui-ts/ts-universal-mouse-key.md) object, which indicates the mouse event that triggers the callback. This event supports custom bubbling settings. By default, event bubbling occurs between parent and child components. It is commonly used for customized mouse behavior logic processing.
You can use the **MouseEvent** object in the callback to obtain information about the triggered event, including the coordinates (**screenX**/**screenY**/**x**/**y**), button ([MouseButton](../reference/arkui-ts/ts-appendix-enums.md#mousebutton)), action ([MouseAction](../reference/arkui-ts/ts-appendix-enums.md#mouseaction)), timestamp (**timestamp**), display area of the object that triggers the event ([EventTarget](../reference/arkui-ts/ts-universal-events-click.md)), and event source ([SourceType](../reference/arkui-ts/ts-gesture-settings.md)). The **stopPropagation** callback of **MouseEvent** is used to set whether the current event blocks bubbling.
>**NOTE**
>
>**MouseButton** indicates the physical mouse button being pressed or released that triggers the mouse event. The values are **Left**, **Right**, **Middle**, **Back**, **Forward**, and **None**. **None** indicates that no button is pressed or released, which means that the event is triggered by the mouse cursor moving on the component.
```ts
// xxx.ets
@Entry
@Component
struct MouseExample {
@State isHovered: boolean = false;
@State buttonText: string = '';
@State columnText: string = '';
build() {
Column() {
Button(this.isHovered ? 'Hovered!' : 'Not Hover')
.width(200)
.height(100)
.backgroundColor(this.isHovered ? Color.Green : Color.Gray)
.onHover((isHover: boolean) => {
this.isHovered = isHover
})
.onMouse((event: MouseEvent) => { // Set the onMouse callback for the button.
this.buttonText = 'Button onMouse:\n' + '' +
'button = ' + event.button + '\n' +
'action = ' + event.action + '\n' +
'x,y = (' + event.x + ',' + event.y + ')' + '\n' +
'screenXY=(' + event.screenX + ',' + event.screenY + ')';
})
Divider()
Text(this.buttonText).fontColor(Color.Green)
Divider()
Text(this.columnText).fontColor(Color.Red)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Red)
.onMouse((event: MouseEvent) => { // Set the onMouse callback for the column.
this.columnText = 'Column onMouse:\n' + '' +
'button = ' + event.button + '\n' +
'action = ' + event.action + '\n' +
'x,y = (' + event.x + ',' + event.y + ')' + '\n' +
'screenXY=(' + event.screenX + ',' + event.screenY + ')';
})
}
}
```
Bind the **onMouse** API to the button based on the **onHover** example. In the callback, the values of the callback parameters, such as **button** and **action**, are displayed. The same settings are performed on the outer **\<Column>** container. The entire process can be divided into the following two actions:
1. Moving the mouse cursor: When the mouse cursor is moved from outside the button to inside the button, only the **onMouse** callback of the **\<Column>** is triggered. When the mouse cursor is moved to the button, as the **onMouse** event bubbles up by default, both the **onMouse** callbacks of the **\<Column>** and **\<Button>** components are invoked. In this process, the mouse cursor moves, but no mouse button is clicked. Therefore, in the displayed information, the value of **button** is 0 (enumerated value of **MouseButton.None**) and the value of **action** is **3** (enumerated value of **MouseAction.Move**).
2. Clicking the mouse button: After the mouse cursor enters the **\<Button>** component, the **\<Button>** component is clicked twice, namely, left-click and right-click.
Left-clicked: button = 1 (enumerated value of **MouseButton.Left**); action = 1 (enumerated value of **MouseAction.Press**); action = 2 (enumerated value of **MouseAction.Release**).
Right-click: button = 2 (enumerated value of **MouseButton.Right**); action = 1 (enumerated value of **MouseAction.Press**); action = 2 (enumerated value of **MouseAction.Release**)
![onMouse1](figures/onMouse1.gif)
To prevent the mouse event from bubbling, call the **stopPropagation()** API.
```ts
Button(this.isHovered ? 'Hovered!' : 'Not Hover')
.width(200)
.height(100)
.backgroundColor(this.isHovered ? Color.Green : Color.Gray)
.onHover((isHover: boolean) => {
this.isHovered = isHover;
})
.onMouse((event: MouseEvent) => {
event.stopPropagation(); // Prevent the mouse event from bubbling.
this.buttonText = 'Button onMouse:\n' + '' +
'button = ' + event.button + '\n' +
'action = ' + event.action + '\n' +
'x,y = (' + event.x + ',' + event.y + ')' + '\n' +
'screenXY=(' + event.screenX + ',' + event.screenY + ')';
})
```
To prevent the mouse event of the child component (**\<Button>**) from bubbling up to its parent component (**\<Column>**), use the **event** parameter in the **onMouse** callback of **\<Button>** to call the **stopPropagation** API.
```ts
event.stopPropagation()
```
With bubbling prevented, the mouse event on the **\<Button>** component will trigger the **onMouse** callback of the **\<Button>** component, but not the **onMouse** callback of the **\<Column>** component.
### hoverEffect
```ts
hoverEffect(value: HoverEffect)
```
Sets the hover effect of the component in hover state. The parameter value type is **HoverEffect**. The **Auto**, **Scale**, and **Highlight** effects are preset and do not support customization.
**Table 1** HoverEffect
| Enum| Description |
| -------------- | ---------------------------------------- |
| Auto | Default hover effect, which varies by component. |
| Scale | Scale effect. When the mouse cursor is placed over the component, the component is scaled up from 100% to 105%. When the mouse cursor is moved away, the component is scaled down from 105% to 100%.|
| Highlight | Background fade-in and fade-out effect. When the mouse cursor is placed over the component, a white layer with 5% opacity is applied to the background color of the component, resulting in a dimmed background. When the mouse cursor is moved away, the background color of the component is restored to the original style.|
| None | No effect. |
```ts
// xxx.ets
@Entry
@Component
struct HoverExample {
build() {
Column({ space: 10 }) {
Button('Auto')
.width(170).height(70)
Button('Scale')
.width(170).height(70)
.hoverEffect(HoverEffect.Scale)
Button('Highlight')
.width(170).height(70)
.hoverEffect(HoverEffect.Highlight)
Button('None')
.width(170).height(70)
.hoverEffect(HoverEffect.None)
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
```
![hoverEffect](figures/hoverEffect.gif)
For the **\<Button>** component, **Auto** creates the same effect as **Scale**.
## Key Event
**Figure 2** Key event data flow
![en-us_image_0000001511580944](figures/en-us_image_0000001511580944.png)
The key event is triggered by a device such as a peripheral keyboard, and is sent to the currently focused window after being converted by the driver and multi-mode processing. After obtaining the event, the window distributes the event to the input method (which consumes the key as the input). If the input method does not consume the key event, the window sends the event to the ArkUI framework. Therefore, when an input box component has focus and an input method is enabled, most key events are consumed by the input method. For example, a letter key is used by the input method to enter a letter in the input box, and an arrow key is used by the input method to switch to the desired candidate word.
After the key event is sent to the ArkUI framework, it first identifies the complete focus chain, and then sends the key event one by one from the leaf node to the root node.
### onKeyEvent
```ts
onKeyEvent(event: (event?: KeyEvent) => void)
```
Triggered when the bound component has [focus](arkts-common-events-focus-event.md) and a key event occurs on the component. The callback parameter [KeyEvent](../reference/arkui-ts/ts-universal-events-key.md) can be used to obtain the information about the key event, including [KeyType](../reference/arkui-ts/ts-appendix-enums.md#keytype), [keyCode](../reference/apis/js-apis-keycode.md), keyText, [KeySource](../reference/arkui-ts/ts-appendix-enums.md#keysource), **deviceId**, **metaKey**, **timestamp**, and **stopPropagation**.
```ts
// xxx.ets
@Entry
@Component
struct KeyEventExample {
@State buttonText: string = '';
@State buttonType: string = '';
@State columnText: string = '';
@State columnType: string = '';
build() {
Column() {
Button('onKeyEvent')
.width(140).height(70)
.onKeyEvent((event: KeyEvent) => {// Set the onKeyEvent event for the <Button> component.
if (event.type === KeyType.Down) {
this.buttonType = 'Down';
}
if (event.type === KeyType.Up) {
this.buttonType = 'Up';
}
this.buttonText = 'Button: \n' +
'KeyType:' + this.buttonType + '\n' +
'KeyCode:' + event.keyCode + '\n' +
'KeyText:' + event.keyText;
})
Divider()
Text(this.buttonText).fontColor(Color.Green)
Divider()
Text(this.columnText).fontColor(Color.Red)
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
.onKeyEvent((event: KeyEvent) => {// Set the onKeyEvent event for the parent container <Column>.
if (event.type === KeyType.Down) {
this.columnType = 'Down';
}
if (event.type === KeyType.Up) {
this.columnType = 'Up';
}
this.columnText = 'Column: \n' +
'KeyType:' + this.buttonType + '\n' +
'KeyCode:' + event.keyCode + '\n' +
'KeyText:' + event.keyText;
})
}
}
```
In the preceding example, **onKeyEvent** is bound to the **\<Button>** component and its parent container **\<Column>**. After the application opens and loads a page, the first focusable non-container component in the component tree automatically obtains focus. As the application has only one **\<Button>** component, the component automatically obtains focus. Because the **\<Button>** component is a child node of the **\<Column>** component, the **\<Column>** component also obtains focus. For details about the focus obtaining mechanism, see [Focus Event](arkts-common-events-focus-event.md).
![en-us_image_0000001511421324](figures/en-us_image_0000001511421324.gif)
After the application is opened, press the following keys in sequence: Space, Enter, Left Ctrl, Left Shift, Letter A, and Letter Z.
1. Because the **onKeyEvent** event bubbles by default, the **onKeyEvent** callbacks of both **\<Button>** and **\<Column>** are invoked.
2. Each key has two callbacks, which correspond to **KeyType.Down** and **KeyType.Up** respectively, indicating that the key is pressed and then lifted.
To prevent the key event of the **\<Button>** component from bubbling up to its parent container **\<Column>**, add the **event.stopPropagation()** API to the **onKeyEvent** callback of **\<Button>**.
```ts
Button('onKeyEvent')
.width(140).height(70)
.onKeyEvent((event: KeyEvent) => {
// Use stopPropagation to prevent the key event from bubbling up.
event.stopPropagation();
if (event.type === KeyType.Down) {
this.buttonType = 'Down';
}
if (event.type === KeyType.Up) {
this.buttonType = 'Up';
}
this.buttonText = 'Button: \n' +
'KeyType:' + this.buttonType + '\n' +
'KeyCode:' + event.keyCode + '\n' +
'KeyText:' + event.keyText;
})
```
![en-us_image_0000001511900508](figures/en-us_image_0000001511900508.gif)
# Focus Event
## Basic Concepts
- Focus
Focus points to a unique interactive element in the current window. When a user indirectly interacts with an application by using a non-directional input device such as a keyboard, a television remote control, or a vehicle-mounted joystick/knob, focus-based interaction is an important input means.
- Default focus
After an application opens or switches to a page, the first focusable component (if any) in the component tree of the page is the default focus. You can [customize the default focus](#setting-default-focus) as needed.
- Focused
A focused component is one that has focus. Only one end-point component in the application can receive focus at one time, and all the component's ancestor components along the focus chain are focused. If you want a component to be focused, ensure that the component and all its ancestor components are focusable (the [focusable](#setting-whether-a-component-is-focusable) attribute is set to **true**).
- Not focused
A component is not focused when it loses focus. In this case, all its ancestor components and the components not on the same focus chain as the focused component are not focused.
- Focus navigation
Focus navigation refers to a process in which the focus is transferred in the current application. It causes the original focused component to lose focus and a previously not focused component to receive focus. Focus navigation in applications can be classified into the following types by behavior:
- Active navigation: A component is assigned focus due to subjective actions by developers or users, such as pressing the Tab or arrow keys on the external keyboard, using the [requestFocus](#focuscontrolrequestfocus) API, or clicking the component when the [focusOnTouch](#focusontouch) attribute is set to **true**.
- Passive navigation: A component is assigned focus due to logical operations by the system. This focus navigation mode cannot be set by developers. For example, the system may assign focus to a component when the **if-else** statement is used to delete the focused component or set the focused component (or its parent component) to be unfocusable or when the page is switched.
- Focused state
The focused state refers to the style of the focused component. It is similar among different components and is not visible by default. The focused state is visible only when the Tab or arrow keys on the external keyboard are pressed to move focus. The Tab key or arrow key that triggers the focused state for the first time does not trigger focus navigation. When the application receives a touch event (including a finger press event on the screen and a press event of a left mouse button), the focused state style is automatically hidden. The focused state style is defined by the backend component and cannot be modified by developers.
## Rules of Focus Navigation
Focus navigation follows the set rules regardless of whether it is active or passive focus navigation. By default, these rules are defined by the focus system and subject to the container where focus is located.
- Linear navigation: used in components where child components are arranged linearly, such as the **\<Flex>**, **\<Row>**, **\<Column>**, and **\<List>** components. The focus navigation direction is the same as the direction of the arrow keys.
**Figure 1** Linear navigation
![en-us_image_0000001562700537](figures/en-us_image_0000001562700537.png)
For example, in the **\<Row>** container, you can use the left and right arrow keys (←/→) to move focus between two adjacent focusable components.
- Cross navigation: used when the up (↑), down (↓), left (←), and right (→) arrow keys are pressed to move focus. The following figure shows a **\<Grid>** container where cross focus navigation is frequently seen.
**Figure 2** Cross focus navigation in the \<Grid> component
![en-us_image_0000001511740580](figures/en-us_image_0000001511740580.png)
>**NOTE**
> - With the previous focus navigation rules, the functions of the Tab/Shift+Tab keys are the same as those of the arrow keys. Pressing the Tab key is equivalent to pressing the right arrow key and then, if the focus cannot be moved, the down arrow key. Pressing the Shift+Tab key is equivalent to pressing the left arrow key and then, if the focus cannot be moved, the up arrow key.
>
> - The key that triggers focus navigation is the press event (Down event).
>
> - After a component is deleted or set to be unfocusable, the linear navigation rule is followed. The focus automatically moves to the sibling component in front of the deleted or unfocusable component. If that component cannot receive focus, the focus is then moved to the sibling component on the rear.
- tabIndex-based navigation: Focus navigation with the Tab/Shift+Tab keys becomes sequential when the [tabIndex](../reference/arkui-ts/ts-universal-attributes-focus.md) attribute is set for the components.
- Area-based focus: You can define the order of sequential focus navigation and the default focused component, by setting the **tabIndex** attribute for a container component and the [groupDefaultFocus](#groupdefaultfocus) attribute.
- Rule for focusing on a container component: When a container component (for which **groupDefaultFocus** is not set) receives focus for the first time, the positions of its child components are calculated to identify the child component closest to the center of the container. The focus moves to this identified child component. If the container is not focused for the first time, the focus automatically moves to the child component that is focused last time in the container.
- Focus interaction: When a component is focused, the inherent click task of the component or the **onClick** callback task bound is automatically mounted to the space or carriage return key. When the key is pressed, the task is executed, just as in the case of a finger or mouse click.
>**NOTE**
>
>The focus involved in this topic refers to component focus. In real-world applications, the focus can also be window focus, which points to the currently focused window. When a window loses focus, all focused components in the window lose focus.
## Listening for Focus Changes
```ts
onFocus(event: () => void)
```
Triggered when the bound component obtains focus.
```ts
onBlur(event:() => void)
```
Triggered when the bound component loses focus.
The **onFocus** and **onBlur** APIs are usually used in pairs to listen for the focus changes of the component.
The following sample code shows how to use these APIs:
```ts
// xxx.ets
@Entry
@Component
struct FocusEventExample {
@State oneButtonColor: Color = Color.Gray;
@State twoButtonColor: Color = Color.Gray;
@State threeButtonColor: Color = Color.Gray;
build() {
Column({ space: 20 }) {
// You can use the up and down arrow keys on an external keyboard to move the focus between the three buttons. When a button gains focus, its color changes. When it loses focus, its color changes back.
Button('First Button')
.width(260)
.height(70)
.backgroundColor(this.oneButtonColor)
.fontColor(Color.Black)
// Listen for the focus obtaining event of the first component and change its color when it obtains focus.
.onFocus(() => {
this.oneButtonColor = Color.Green;
})
// Listen for the focus loss event of the first component and change its color when it loses focus.
.onBlur(() => {
this.oneButtonColor = Color.Gray;
})
Button('Second Button')
.width(260)
.height(70)
.backgroundColor(this.twoButtonColor)
.fontColor(Color.Black)
// Listen for the focus obtaining event of the second component and change its color when it obtains focus.
.onFocus(() => {
this.twoButtonColor = Color.Green;
})
// Listen for the focus loss event of the second component and change its color when it loses focus.
.onBlur(() => {
this.twoButtonColor = Color.Grey;
})
Button('Third Button')
.width(260)
.height(70)
.backgroundColor(this.threeButtonColor)
.fontColor(Color.Black)
// Listen for the focus obtaining event of the third component and change its color when it obtains focus.
.onFocus(() => {
this.threeButtonColor = Color.Green;
})
// Listen for the focus loss event of the third component and change its color when it loses focus.
.onBlur(() => {
this.threeButtonColor = Color.Gray ;
})
}.width('100%').margin({ top: 20 })
}
}
```
![en-us_image_0000001511740584](figures/en-us_image_0000001511740584.gif)
The preceding example includes four steps:
1. When the application is opened, the **First Button** component obtains the focus by default, its **onFocus** callback is triggered, and its background color turns green.
2. When the Tab key (or the down arrow key) is pressed, **First Button** is in focused state, that is, there is a blue closed box outside the component. If no focus navigation is triggered, the focus remains on **First Button**.
3. When the Tab key (or the down arrow key) is pressed, the **Second Button** component is focused, its **onFocus** callback is triggered, and its background color turns green. **First Button** loses focus, its **onBlur** callback is triggered, and its background color turns gray.
4. When the Tab key (or the down arrow key) is pressed, the **Third Button** component is focused, its **onFocus** callback is triggered, and its background color turns green. **Second Button** loses focus, its **onBlur** callback is triggered, and its background color turns gray.
## Setting Whether a Component Is focusable
Use the **focusable** API to set whether a component is focusable.
```ts
focusable(value: boolean)
```
Components can be classified into the following types based on their focusability:
- Components that are focusable by default: These components are usually interactive components, such as **\<Button>**, **\<Checkbox>**, and **\<TextInput>**.
- Components that can be focused but are unfocusable by default: Typical examples are **\<Text>** and **\<Image>**. To enable them to be focusable, use the **focusable(true)** attribute.
- Components that cannot be focused: These components usually do not allow for interactions, such as **\<Blank>** and **\<Circle>**, and cannot be focused even if they use the **focusable** attribute.
>**NOTE**
> - If **focusable** is set to **false**, the component is unfocusable. The universal attribute [enabled](../reference/arkui-ts/ts-universal-attributes-enable.md) can also be used to make the component unfocusable.
>
> - When a component is in the focused state, if its **focusable** or **enabled** attribute is set to **false**, the component automatically loses focus. Then, the focus moves to other components based on the [Rules of Focus Navigation](#rules-of-focus-navigation).
**Table 1** Focusability of basic components
| Basic Component | Focusable| Default Value of focusable| Rules of Focus Navigation |
| ---------------------------------------- | ------- | ------------ | -------- |
| [AlphabetIndexer](../reference/arkui-ts/ts-container-alphabet-indexer.md) | Yes | true | Linear navigation |
| [Blank](../reference/arkui-ts/ts-basic-components-blank.md) | No | false | / |
| [Button](../reference/arkui-ts/ts-basic-components-button.md) | Yes | true | / |
| [Checkbox](../reference/arkui-ts/ts-basic-components-checkbox.md) | Yes | true | / |
| [CheckboxGroup](../reference/arkui-ts/ts-basic-components-checkboxgroup.md) | Yes | true | / |
| [DataPanel](../reference/arkui-ts/ts-basic-components-datapanel.md) | No | false | / |
| [DatePicker](../reference/arkui-ts/ts-basic-components-datepicker.md) | Yes | true | Linear navigation |
| [Divider](../reference/arkui-ts/ts-basic-components-divider.md) | No | false | / |
| [Formcomponent](../reference/arkui-ts/ts-basic-components-formcomponent.md) | No | false | / |
| [Gauge](../reference/arkui-ts/ts-basic-components-gauge.md) | No | false | / |
| [Image](../reference/arkui-ts/ts-basic-components-image.md) | Yes | false | / |
| [ImageAnimator](../reference/arkui-ts/ts-basic-components-imageanimator.md) | Yes | false | / |
| [LoadingProgress](../reference/arkui-ts/ts-basic-components-loadingprogress.md) | No | false | / |
| [Marquee](../reference/arkui-ts/ts-basic-components-marquee.md) | No | false | / |
| [Menu](../reference/arkui-ts/ts-basic-components-menu.md) | Yes | true | Linear navigation |
| [MenuItem](../reference/arkui-ts/ts-basic-components-menuitem.md) | Yes | true | / |
| [MenuItemGroup](../reference/arkui-ts/ts-basic-components-menuitemgroup.md) | Yes | true | Linear navigation |
| [Navigation](../reference/arkui-ts/ts-basic-components-navigation.md) | No | false | Customized |
| [NavRouter](../reference/arkui-ts/ts-basic-components-navrouter.md) | No | false | Follows the child container |
| [NavDestination](../reference/arkui-ts/ts-basic-components-navdestination.md) | No | false | Linear navigation |
| [PatternLock](../reference/arkui-ts/ts-basic-components-patternlock.md) | No | false | / |
| [PluginComponent](../reference/arkui-ts/ts-basic-components-plugincomponent.md) | No | false | / |
| [Progress](../reference/arkui-ts/ts-basic-components-progress.md) | No | false | / |
| [QRCode](../reference/arkui-ts/ts-basic-components-qrcode.md) | No | false | / |
| [Radio](../reference/arkui-ts/ts-basic-components-radio.md) | Yes | true | / |
| [Rating](../reference/arkui-ts/ts-basic-components-rating.md) | Yes | true | / |
| [RemoteWindow](../reference/arkui-ts/ts-basic-components-remotewindow.md) | No | false | / |
| [RichText](../reference/arkui-ts/ts-basic-components-richtext.md) | No | false | / |
| [ScrollBar](../reference/arkui-ts/ts-basic-components-scrollbar.md) | No | false | / |
| [Search](../reference/arkui-ts/ts-basic-components-search.md) | Yes | true | / |
| [Select](../reference/arkui-ts/ts-basic-components-select.md) | Yes | true | Linear navigation |
| [Slider](../reference/arkui-ts/ts-basic-components-slider.md) | Yes | true | / |
| [Span](../reference/arkui-ts/ts-basic-components-span.md) | No | false | / |
| [Stepper](../reference/arkui-ts/ts-basic-components-stepper.md) | Yes | true | / |
| [StepperItem](../reference/arkui-ts/ts-basic-components-stepperitem.md) | Yes | true | / |
| [Text](../reference/arkui-ts/ts-basic-components-text.md) | Yes | false | / |
| [TextArea](../reference/arkui-ts/ts-basic-components-textarea.md) | Yes | true | / |
| [TextClock](../reference/arkui-ts/ts-basic-components-textclock.md) | No | false | / |
| [TextInput](../reference/arkui-ts/ts-basic-components-textinput.md) | Yes | true | / |
| [TextPicker](../reference/arkui-ts/ts-basic-components-textpicker.md) | Yes | true | Linear navigation |
| [TextTimer](../reference/arkui-ts/ts-basic-components-texttimer.md) | No | false | / |
| [TimePicker](../reference/arkui-ts/ts-basic-components-timepicker.md) | Yes | true | Linear navigation |
| [Toggle](../reference/arkui-ts/ts-basic-components-toggle.md) | Yes | true | / |
| [Web](../reference/arkui-ts/ts-basic-components-web.md) | Yes | true | Customized|
| [XComponent](../reference/arkui-ts/ts-basic-components-xcomponent.md) | No | false | / |
**Table 2** Focusability of container components
| Container Component | Focusable| Default Value of focusable| Rules of Focus Navigation |
| ---------------------------------------- | ----- | ------------ | -------- |
| [AbilityComponent](../reference/arkui-ts/ts-container-ability-component.md) | No | false | / |
| [Badge](../reference/arkui-ts/ts-container-badge.md) | No | false | / |
| [Column](../reference/arkui-ts/ts-container-column.md) | Yes | true | Linear navigation |
| [ColumnSplit](../reference/arkui-ts/ts-container-columnsplit.md) | Yes | true | / |
| [Counter](../reference/arkui-ts/ts-container-counter.md) | Yes | true | Linear navigation |
| [Flex](../reference/arkui-ts/ts-container-flex.md) | Yes | true | Linear navigation |
| [GridCol](../reference/arkui-ts/ts-container-gridcol.md) | Yes | true | Customized |
| [GridRow](../reference/arkui-ts/ts-container-gridrow.md) | Yes | true | Customized |
| [Grid](../reference/arkui-ts/ts-container-grid.md) | Yes | true | Customized |
| [GridItem](../reference/arkui-ts/ts-container-griditem.md) | Yes | true | Follows the child component |
| [List](../reference/arkui-ts/ts-container-list.md) | Yes | true | Linear navigation |
| [ListItem](../reference/arkui-ts/ts-container-listitem.md) | Yes | true | Follows the child component |
| [ListItemGroup](../reference/arkui-ts/ts-container-listitemgroup.md) | Yes | true | Follows the **\<List>** component|
| [Navigator](../reference/arkui-ts/ts-container-navigator.md) | No | true | Customized |
| [Panel](../reference/arkui-ts/ts-container-panel.md) | No | true | Follows the child component |
| [Refresh](../reference/arkui-ts/ts-container-refresh.md) | No | false | / |
| [RelativeContainer](../reference/arkui-ts/ts-container-relativecontainer.md) | No | true | Customized |
| [Row](../reference/arkui-ts/ts-container-row.md) | Yes | true | Linear navigation |
| [RowSplit](../reference/arkui-ts/ts-container-rowsplit.md) | Yes | true | / |
| [Scroll](../reference/arkui-ts/ts-container-scroll.md) | Yes | true | Linear navigation |
| [SideBarContainer](../reference/arkui-ts/ts-container-sidebarcontainer.md) | Yes | true | Linear navigation |
| [Stack](../reference/arkui-ts/ts-container-stack.md) | Yes | true | Linear navigation |
| [Swiper](../reference/arkui-ts/ts-container-swiper.md) | Yes | true | Customized |
| [Tabs](../reference/arkui-ts/ts-container-tabs.md) | Yes | true | Customized |
| [TabContent](../reference/arkui-ts/ts-container-tabcontent.md) | Yes | true | Follows the child component |
**Table 3** Focusability of media components
| Media Component | Focusable| Default Value of focusable| Rules of Focus Navigation|
| ---------------------------------------- | ----- | ------------ | ---- |
| [Video](../reference/arkui-ts/ts-media-components-video.md) | Yes | true | / |
**Table 4** Focusability of canvas components
| Canvas Component | Focusable| Default Value of focusable| Rules of Focus Navigation|
| ---------------------------------------- | ----- | ------------ | ---- |
| [Canvas](../reference/arkui-ts/ts-components-canvas-canvas.md) | No | false | / |
The following example shows how to use the **focusable** API:
```ts
// xxx.ets
@Entry
@Component
struct FocusableExample {
@State textFocusable: boolean = true;
@State color1: Color = Color.Yellow;
@State color2: Color = Color.Yellow;
build() {
Column({ space: 5 }) {
Text('Default Text') // The focusable attribute is not set for the first <Text> component. By default, the component is unfocusable.
.borderColor(this.color1)
.borderWidth(2)
.width(300)
.height(70)
.onFocus(() => {
this.color1 = Color.Blue;
})
.onBlur(() => {
this.color1 = Color.Yellow;
})
Divider()
Text('focusable: ' + this.textFocusable) // The focusable attribute is set for the second <Text> component. The initial value is true.
.borderColor(this.color2)
.borderWidth(2)
.width(300)
.height(70)
.focusable(this.textFocusable)
.onFocus(() => {
this.color2 = Color.Blue;
})
.onBlur(() => {
this.color2 = Color.Yellow;
})
Divider()
Row() {
Button('Button1')
.width(140).height(70)
Button('Button2')
.width(160).height(70)
}
Divider()
Button('Button3')
.width(300).height(70)
Divider()
}.width('100%').justifyContent(FlexAlign.Center)
.onKeyEvent((e) => { // Bind onKeyEvent. When the <Column> component is focused, pressing F can reverse the focusable attribute of the second <Text> component.
if (e.keyCode === 2022 && e.type === KeyType.Down) {
this.textFocusable = !this.textFocusable;
}
})
}
}
```
Operation result:
![en-us_image_0000001511900540](figures/en-us_image_0000001511900540.gif)
The preceding example includes two parts: default focus and active navigation.
**Default focus:**
- According to the definition of the default focus, after the application is opened, the first focusable element is focused by default.
- As the **focusable** attribute is not set for the first **\<Text>** component, it cannot be focused.
- The **focusable** attribute of the second **\<Text>** component is explicitly set to **true**. In this case, the default focus is placed on the component.
**Active navigation:**
Pressing **F** on the keyboard triggers **onKeyEvent**, which sets **focusable** to **false** and makes the **\<Text>** component unfocusable. In this case, the focus automatically shifts. According to the description in passive focus, the system automatically searches for the immediate focusable component above the **\<Text>** component, which is an unfocusable **\<Text>** component. Therefore, the system searches for the next focusable component, finds and moves the focus to the **\<Row>** container, and calculates the positions of **Button1** and **Button2** based on the [rule for focusing on a container component](#rules-of-focus-navigation). Because **Button2** is larger than **Button1**, the focus automatically moves to **Button2**.
## Setting Default Focus
```ts
defaultFocus(value: boolean)
```
When the page is constructed for the first time, the focus system searches for all components on the current page, finds the first component bound to **defaultFocus(true)**, and sets the component as the default focus. If no component is bound to **defaultFocus(true)**, the first focusable component is set as the default focus.
Below is an application layout.
![en-us_image_0000001563060793](figures/en-us_image_0000001563060793.png)
The following is the sample code for implementing the application layout, and **defaultFocus** is not set in the sample code:
```ts
// xxx.ets
import promptAction from '@ohos.promptAction';
class MyDataSource implements IDataSource {
private list: number[] = [];
private listener: DataChangeListener;
constructor(list: number[]) {
this.list = list;
}
totalCount(): number {
return this.list.length;
}
getData(index: number): any {
return this.list[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener;
}
unregisterDataChangeListener() {
}
}
@Entry
@Component
struct SwiperExample {
private swiperController: SwiperController = new SwiperController()
private data: MyDataSource = new MyDataSource([])
aboutToAppear(): void {
let list = []
for (let i = 1; i <= 4; i++) {
list.push(i.toString());
}
this.data = new MyDataSource(list);
}
build() {
Column({ space: 5 }) {
Swiper(this.swiperController) {
LazyForEach(this.data, (item: string) => {
Row({ space: 20 }) {
Column() {
Button('1').width(200).height(200)
.fontSize(40)
.backgroundColor('#dadbd9')
}
Column({ space: 20 }) {
Row({ space: 20 }) {
Button('2')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
Button('3')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
}
Row({ space: 20 }) {
Button('4')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
Button('5')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
}
Row({ space: 20 }) {
Button('6')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
Button('7')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
}
}
}
.width(480)
.height(380)
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Gray)
.backgroundColor(Color.White)
}, item => item)
}
.cachedCount(2)
.index(0)
.interval(4000)
.indicator(true)
.loop(true)
.duration(1000)
.itemSpace(0)
.curve(Curve.Linear)
.onChange((index: number) => {
console.info(index.toString());
})
.margin({ left: 20, top: 20, right: 20 })
Row({ space: 40 }) {
Button('')
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.swiperController.showPrevious();
})
Button('')
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.swiperController.showNext();
})
}
.width(480)
.height(50)
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Gray)
.backgroundColor('#f7f6dc')
Row({ space: 40 }) {
Button('Cancel')
.fontSize(30)
.fontColor('#787878')
.type(ButtonType.Normal)
.width(140)
.height(50)
.backgroundColor('#dadbd9')
Button('OK')
.fontSize(30)
.fontColor('#787878')
.type(ButtonType.Normal)
.width(140)
.height(50)
.backgroundColor('#dadbd9')
.onClick(() => {
promptAction.showToast({ message: 'Button OK on clicked' });
})
}
.width(480)
.height(80)
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Gray)
.backgroundColor('#dff2e4')
.margin({ left: 20, bottom: 20, right: 20 })
}.backgroundColor('#f2f2f2')
.margin({ left: 50, top: 50, right: 20 })
}
}
```
As **defaultFocus** is not set in the application, the first focusable component obtains the focus by default. Pressing the Tab key or arrow keys can set the focused component to enter the focused state.
![en-us_image_0000001511421360](figures/en-us_image_0000001511421360.gif)
Assume that you want to perform the **onClick** callback of the **OK** button without switching the focus when opening the application. In this case, you can bind **defaultFocus(true)** to the button, make it the default focus on the page.
```ts
Button('OK')
.defaultFocus(true) // Bind defaultFocus to the OK button.
.fontSize(30)
.fontColor('#787878')
.type(ButtonType.Normal)
.width(140).height(50).backgroundColor('#dadbd9')
.onClick(() => {
promptAction.showToast({ message: 'Button OK on clicked' });
})
```
![en-us_image_0000001562940617](figures/en-us_image_0000001562940617.gif)
When the application is opened, pressing the Tab key switches the **OK** button to the focused state, indicating that the default focus is changed to the button. After the space key is pressed, the **onClick **event of the **OK** button is triggered.
## Setting the Order for Sequential Tab Navigation
```ts
tabIndex(index: number)
```
Use **tabIndex** to set the order for sequential Tab navigation. The default value is **0**. In Tab navigation, where Tab/Shift+Tab is used (the arrow keys do not affect the navigation), the focus system automatically obtains all components whose **tabIndex** is greater than 0 and moves focus in ascending or descending order.
With the example provided in [Setting Default Focus](#setting-default-focus), the default order for sequential focus navigation is as follows:
![en-us_image_0000001511421364](figures/en-us_image_0000001511421364.gif)
The default order for sequential Tab navigation is from the first focusable component to the last focusable component, and the process goes through Button1 -> Button4 -> Button5 -> Button7 -> Left arrow -> Right arrow -> ButtonOK. This focus navigation queue is relatively complete and traverses most of the components. However, the disadvantage is that the path from the first to the last is long.
If you want to quickly go from the first to the last without sacrificing too much traversal integrity, you can use the **tabIndex** attribute.
For example, take the white area, the yellow area, and the green area each as a unit. To implement the focus navigation queue of Button1 -> Left arrow -> Button-OK, you only need to add **tabIndex(1)**, **tabIndex(2)**, and **tabIndex(3)** to the Button1, left arrow, and ButtonOK components in sequence. The **tabIndex** attribute indicates how a component participates in sequential Tab navigation. A component with a larger value gains focus later than one with a smaller value.
```ts
Button('1').width(200).height(200)
.fontSize(40)
.backgroundColor('#dadbd9')
.tabIndex(1) // Set Button1 as the first tabIndex node.
```
```ts
Button('')
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.swiperController.showPrevious();
})
.tabIndex(2) // Set Button-left arrow as the second tabIndex node.
```
```ts
Button('OK')
.fontSize(30)
.fontColor('#787878')
.type(ButtonType.Normal)
.width(140).height(50).backgroundColor('#dadbd9')
.onClick(() => {
promptAction.showToast({ message: 'Button OK on clicked' });
})
.tabIndex(3) // Set Button-OK as the third tabIndex node.
```
![en-us_image_0000001511580976](figures/en-us_image_0000001511580976.gif)
>**NOTE**
> - When the focus is on a tabIndex (greater than 0) node, after Tab/Shift+Tab is pressed, the focus system preferentially searches for the rear/front node in the tabIndex (greater than 0) queue. If the rear/front node exists, the focus system moves the focus to that node. If the node does not exist, the default focus logic is used to move the focus backward or forward.
>
> - When the focus is on the tabIndex (equal to 0) node, the focus system uses the default focus navigation logic. During the navigation, the tabIndex (greater than 0) and tabIndex (less than 0) nodes are skipped.
>
> - When the focus is on a tabIndex (less than 0) node, pressing Tab/Shift+Tab does not move the focus.
### groupDefaultFocus
```ts
groupDefaultFocus(value: boolean)
```
Using **tabIndex** to [set the order for sequential Tab navigation](#setting-the-order-for-sequential-tab-navigation) has the following issues:
While a component is set as a tabIndex node (white-Button1, yellow-left arrow, and green-ButtonOK) in each area (white, yellow, and green), focus moves quicly only within these components in Tab navigation.
The solution is to set **tabIndex** for the container of each area. However, when a container receives focus for the first time, the focused child component is the first focusable component by default, not the desired component (Button1, left arrow, and ButtonOK).
To address this issue, the **groupDefaultFocus** attribute is introduced, whose value type is boolean and default value is **false**.
This attribute must be used together with **tabIndex**. Use **tabIndex** to bind the focus sequence to the areas (containers), and then bind **groupDefaultFocus(true)** to Button1, left arrow, and ButtonOK. In this way, when the target area (container) is focused for the first time, its child components bound to **groupDefaultFocus(true)** get the focus at the same time.
```ts
// xxx.ets
import promptAction from '@ohos.promptAction';
class MyDataSource implements IDataSource {
private list: number[] = [];
private listener: DataChangeListener;
constructor(list: number[]) {
this.list = list;
}
totalCount(): number {
return this.list.length;
}
getData(index: number): any {
return this.list[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener;
}
unregisterDataChangeListener() {
}
}
@Entry
@Component
struct SwiperExample {
private swiperController: SwiperController = new SwiperController()
private data: MyDataSource = new MyDataSource([])
aboutToAppear(): void {
let list = []
for (let i = 1; i <= 4; i++) {
list.push(i.toString());
}
this.data = new MyDataSource(list);
}
build() {
Column({ space: 5 }) {
Swiper(this.swiperController) {
LazyForEach(this.data, (item: string) => {
Row({ space: 20 }) { // Set the <Row> component as the first tabIndex node.
Column() {
Button('1').width(200).height(200)
.fontSize(40)
.backgroundColor('#dadbd9')
.groupDefaultFocus(true) // Set Button-1 as the default focus of the first tabIndex node.
}
Column({ space: 20 }) {
Row({ space: 20 }) {
Button('2')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
Button('3')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
}
Row({ space: 20 }) {
Button('4')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
Button('5')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
}
Row({ space: 20 }) {
Button('6')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
Button('7')
.width(100)
.height(100)
.fontSize(40)
.type(ButtonType.Normal)
.borderRadius(20)
.backgroundColor('#dadbd9')
}
}
}
.width(480)
.height(380)
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Gray)
.backgroundColor(Color.White)
.tabIndex(1)
}, item => item)
}
.cachedCount(2)
.index(0)
.interval(4000)
.indicator(true)
.loop(true)
.duration(1000)
.itemSpace(0)
.curve(Curve.Linear)
.onChange((index: number) => {
console.info(index.toString());
})
.margin({ left: 20, top: 20, right: 20 })
Row({ space: 40 }) { // Set the <Row> component as the second tabIndex node.
Button('')
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.swiperController.showPrevious();
})
.groupDefaultFocus(true) // Set the Button-left arrow as the default focus of the second tabIndex node.
Button('')
.fontSize(40)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.swiperController.showNext();
})
}
.width(480)
.height(50)
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Gray)
.backgroundColor('#f7f6dc')
.tabIndex(2)
Row({ space: 40 }) { // Set the <Row> component as the third tabIndex node.
Button('Cancel')
.fontSize(30)
.fontColor('#787878')
.type(ButtonType.Normal)
.width(140)
.height(50)
.backgroundColor('#dadbd9')
Button('OK')
.fontSize(30)
.fontColor('#787878')
.type(ButtonType.Normal)
.width(140)
.height(50)
.backgroundColor('#dadbd9')
.defaultFocus(true)
.onClick(() => {
promptAction.showToast({ message: 'Button OK on clicked' });
})
.groupDefaultFocus(true) // Set Button-OK as the default focus of the third tabIndex node.
}
.width(480)
.height(80)
.justifyContent(FlexAlign.Center)
.borderWidth(2)
.borderColor(Color.Gray)
.backgroundColor('#dff2e4')
.margin({ left: 20, bottom: 20, right: 20 })
.tabIndex(3)
}.backgroundColor('#f2f2f2')
.margin({ left: 50, top: 50, right: 20 })
}
}
```
![en-us_image_0000001562700533](figures/en-us_image_0000001562700533.gif)
### focusOnTouch
```ts
focusOnTouch(value: boolean)
```
Sets whether a component is focusable on touch (touching or left-clicking). The parameter value type is boolean and the default value is **false**. The default value is **true** for input components: TextInput, TextArea, Search, and Web.
By binding **focusOnTouch(true)** to a component whose default value is **false**, such as **\<Button>**, you enable the component to become focused on touch.
When **focusOnTouch(true)** is bound to a container and the container area is clicked, the first focusable component of the container is immediately focused.
The sample code is as follows:
```ts
// requestFocus.ets
import promptAction from '@ohos.promptAction';
@Entry
@Component
struct RequestFocusExample {
@State idList: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'N']
build() {
Column({ space:20 }){
Button("id: " + this.idList[0] + " focusOnTouch(true) + focusable(false)")
.width(400).height(70).fontColor(Color.White).focusOnTouch(true)
.focusable(false)
Button("id: " + this.idList[1] + " default")
.width(400).height(70).fontColor(Color.White)
Button("id: " + this.idList[2] + " focusOnTouch(false)")
.width(400).height(70).fontColor(Color.White).focusOnTouch(false)
Button("id: " + this.idList[3] + " focusOnTouch(true)")
.width(400).height(70).fontColor(Color.White).focusOnTouch(true)
}.width('100%').margin({ top:20 })
}
}
```
![en-us_image_0000001511580980](figures/en-us_image_0000001511580980.gif)
Interpretation:
Because **focusOnTouch(true)** and **focusable(false)** are both set for Button-A, the component is unfocusable and cannot be focused on touch.
No related attributes are set for Button-B, and therefore it cannot be focused on touch.
**focusOnTouch(false)** is set for Button-C, and therefore it cannot be focused on touch, just as Button-B.
**focusOnTouch(true)** is set for Button-D, and therefore it is focused on touch.
>**NOTE**
>
>Due to the feature of the focused state, the focused state is cleared immediately after the screen receives a touch event. Therefore, each time a component is clicked, you need to press the Tab key again to display the focused state again. In this way, you can know the component where the focus is located.
### focusControl.requestFocus
```ts
focusControl.requestFocus(id: string)
```
Requests the focus to move to the specified component. This API can be used in global method statements. The parameter **id** indicates the target component to focus, which is the string bound to the component using the universal attribute **id**.
The usage method is as follows: Invoke the API in any execution statement and specify the ID of the target component as the input parameter. When the program executes the statement, it immediately requests focus for the specified target component.
Sample code:
```ts
// requestFocus.ets
import promptAction from '@ohos.promptAction';
@Entry
@Component
struct RequestFocusExample {
@State idList: string[] = ['A', 'B', 'C', 'D', 'E', 'F', 'N']
@State requestId: number = 0
build() {
Column({ space:20 }){
Row({space: 5}) {
Button("id: " + this.idList[0] + " focusable(false)")
.width(200).height(70).fontColor(Color.White)
.id(this.idList[0])
.focusable(false)
Button("id: " + this.idList[1])
.width(200).height(70).fontColor(Color.White)
.id(this.idList[1])
}
Row({space: 5}) {
Button("id: " + this.idList[2])
.width(200).height(70).fontColor(Color.White)
.id(this.idList[2])
Button("id: " + this.idList[3])
.width(200).height(70).fontColor(Color.White)
.id(this.idList[3])
}
Row({space: 5}) {
Button("id: " + this.idList[4])
.width(200).height(70).fontColor(Color.White)
.id(this.idList[4])
Button("id: " + this.idList[5])
.width(200).height(70).fontColor(Color.White)
.id(this.idList[5])
}
}.width('100%').margin({ top:20 })
.onKeyEvent((e) => {
if (e.keyCode >= 2017 && e.keyCode <= 2022) {
this.requestId = e.keyCode - 2017;
} else if (e.keyCode === 2030) {
this.requestId = 6;
} else {
return;
}
if (e.type !== KeyType.Down) {
return;
}
let res = focusControl.requestFocus(this.idList[this.requestId]);
if (res) {
promptAction.showToast({message: 'Request success'});
} else {
promptAction.showToast({message: 'Request failed'});
}
})
}
}
```
![en-us_image_0000001562820905](figures/en-us_image_0000001562820905.gif)
Interpretation: There are six **\<Button>** components on the page. **Focusable(false)** is set for Button-A, indicating that Button-A cannot be focused. In **onKeyEvent** of the external container, key events are listened. When A to F keys are pressed, the focus is requested for Buttons A to F. If you press N, the focus is requested the component whose ID does not exist on the current page.
1. Press the Tab key. Because the first component Button-A cannot be focused, the second component Button-B is focused by default, and Button-B is displayed in the focused state.
2. Press A on the keyboard to request the focus for Button-A. The message "Request failed" is displayed, indicating that the focus cannot be obtained. The focus position remains unchanged.
3. Press B on the keyboard to request the focus for Button-B. The message "Request success" is displayed, indicating that the focus is on Button-B. The focus position remains unchanged.
4. Press C on the keyboard to request the focus for Button-C. The message "Request success" is displayed, indicating that the focus is on Button-C. The focus position changes from Button-B to Button-C.
5. Press D on the keyboard to request the focus for Button-D. The message "Request success" is displayed, indicating that the focus is on Button-D. The focus position changes from Button-C to Button-D.
6. Press E on the keyboard to request the focus for Button-E. The message "Request success" is displayed, indicating that the focus is on Button-E. The focus position changes from Button-D to Button-E.
7. Press F on the keyboard to request the focus for Button-F. The message "Request success" is displayed, indicating that the focus is on Button-F. The focus position changes from Button-E to Button-F.
8. Press N on the keyboard to request the focus for an unknown component. The message "Request failed" is displayed, indicating that the focus cannot be obtained and the focus position remains unchanged.
# Touchscreen Event
Touchscreen events are events triggered when a finger or stylus is placed on, moved along, or lifted from a component. They can be classified as [click event](#click-event), [drag event](#drag-event), or [touch event](#touch-event).
**Figure 1** Touchscreen event principles
![en-us_image_0000001562700461](figures/en-us_image_0000001562700461.png)
## Click Event
A click event is triggered when a complete press and lift action performed by using a finger or a stylus. When a click event occurs, the following callback is triggered:
```ts
onClick(event: (event?: ClickEvent) => void)
```
The **event** parameter provides the coordinates of the click relative to the window or component as well as the event source where the click occurs, for example, a button, a click on which shows or hides an image.
```ts
@Entry
@Component
struct IfElseTransition {
@State flag: boolean = true;
@State btnMsg: string = 'show';
build() {
Column() {
Button(this.btnMsg).width(80).height(30).margin(30)
.onClick(() => {
if (this.flag) {
this.btnMsg = 'hide';
} else {
this.btnMsg = 'show';
}
// Click the button to show or hide the image.
this.flag = !this.flag;
})
if (this.flag) {
Image($r('app.media.icon')).width(200).height(200)
}
}.height('100%').width('100%')
}
}
```
## Drag Event
A drag event is triggered when a user long presses a component (&gt;=500 ms) using a finger or stylus and drags the component to the drop target. The following figure illustrates the process of triggering a drag event.
![en-us_image_0000001562820825](figures/en-us_image_0000001562820825.png)
Whether a drag event can be triggered depends on the distance of long-pressing and dragging with the finger or stylus on the screen. The drag event is triggered when this distance reaches 5 vp. ArkUI supports intra-application and cross-application drag events.
The drag event provides the following [APIs](../reference/arkui-ts/ts-universal-events-drag-drop.md).
| API | Description |
| ---------------------------------------- | ---------------------------------------- |
| onDragStart(event: (event?: DragEvent, extraParams?: string) =&gt; CustomBuilder \| DragItemInfo) | Triggered when dragging starts. Currently, only custom **pixelmap** objects and custom components are supported. |
| onDragEnter(event: (event?: DragEvent, extraParams?: string) =&gt; void) | Triggered when the dragged item enters a valid drop target.<br/>**DragEvent**: position where the drag occurs.<br>**extraParmas**: custom information about the drag event. |
| onDragLeave(event: (event?: DragEvent, extraParams?: string) =&gt; void) | Triggered when the dragged item leaves a valid drop target.<br/>**DragEvent**: position where the drag occurs.<br>**extraParmas**: custom information about the drag event. |
| onDragMove(event: (event?: DragEvent, extraParams?: string) =&gt; void) | Triggered when the dragged item moves in a valid drop target.<br/>**DragEvent**: position where the drag occurs.<br>**extraParmas**: custom information about the drag event. |
| onDrop(event: (event?: DragEvent, extraParams?: string) =&gt; void) | Triggered when the dragged item is dropped on a valid drop target.<br/>**DragEvent**: position where the drag occurs.<br>**extraParmas**: custom information about the drag event. |
The following is an example of dragging a component out of a window in cross-window dragging:
```ts
import image from '@ohos.multimedia.image';
@Entry
@Component
struct Index {
@State visible: Visibility = Visibility.Visible
private pixelMapReader = undefined
aboutToAppear() {
console.info('begin to create pixmap has info message: ')
this.createPixelMap()
}
createPixelMap() {
let color = new ArrayBuffer(4 * 96 * 96);
var buffer = new Uint8Array(color);
for (var i = 0; i < buffer.length; i++) {
buffer[i] = (i + 1) % 255;
}
let opts = {
alphaType: 0,
editable: true,
pixelFormat: 4,
scaleMode: 1,
size: { height: 96, width: 96 }
}
const promise = image.createPixelMap(color, opts);
promise.then((data) => {
console.info('create pixmap has info message: ' + JSON.stringify(data))
this.pixelMapReader = data;
})
}
@Builder pixelMapBuilder() {
Text('drag item')
.width('100%')
.height(100)
.fontSize(16)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text('App1')
.width('40%')
.height(80)
.fontSize(20)
.margin(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Pink)
.visibility(Visibility.Visible)
Text('Across Window Drag This')
.width('80%')
.height(80)
.fontSize(16)
.margin(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Pink)
.visibility(this.visible)
.onDragStart(() => { // Triggered when cross-window dragging starts.
console.info('Text onDrag start')
return { pixelMap: this.pixelMapReader, extraInfo: 'custom extra info.' }
})
.onDrop((event: DragEvent, extraParams: string) => {
console.info('Text onDragDrop, ')
this.visible = Visibility.None // Make the source invisible after the dragging is complete.
})
}
.width('100%')
.height('100%')
}
}
```
The following is an example of dragging a component into a window in cross-window dragging:
```ts
@Entry
@Component
struct Index {
@State number: string[] = ['drag here']
@State text: string = ''
@State bool1: boolean = false
@State bool2: boolean = false
@State visible: Visibility = Visibility.Visible
@State visible2: Visibility = Visibility.None
scroller: Scroller = new Scroller()
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text('App2')
.width('40%')
.height(80)
.fontSize(20)
.margin(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Pink)
.visibility(Visibility.Visible)
List({ space: 20, initialIndex: 0 }) {
ForEach(this.number, (item) => {
ListItem() {
Text('' + item)
.width('100%')
.height(80)
.fontSize(16)
.borderRadius(10)
.textAlign(TextAlign.Center)
.backgroundColor(0xFFFFFF)
}
}, item => item)
ListItem() {
Text('Across Window Drag This')
.width('80%')
.height(80)
.fontSize(16)
.margin(30)
.textAlign(TextAlign.Center)
.backgroundColor(Color.Pink)
.visibility(this.visible2)
}
}
.height('50%')
.width('90%')
.border({ width: 1 })
.divider({ strokeWidth: 2, color: 0xFFFFFF, startMargin: 20, endMargin: 20 })
.onDragEnter((event: DragEvent, extraParams: string) => { // Drag the component into the window.
console.info('List onDragEnter, ' + extraParams)
})
.onDragMove((event: DragEvent, extraParams: string) => { // Move the component during dragging.
console.info('List onDragMove, ' + extraParams)
})
.onDragLeave((event: DragEvent, extraParams: string) => { // Drag the component out of the window.
console.info('List onDragLeave, ' + extraParams)
})
.onDrop((event: DragEvent, extraParams: string) => { // Release the component.
console.info('List onDragDrop, ' + extraParams)
this.visible2 = Visibility.Visible // Make the dragged object visible.
})
}
.width('100%')
.height('100%')
}
}
```
## Touch Event
A touch event is triggered when a finger or stylus is placed on, moved along, or lifted from a component.
```ts
onTouch(event: (event?: TouchEvent) => void)
```
- If **event.type** is **TouchType.Down**, the finger or stylus is placed on the component.
- If **event.type** is **TouchType.Up**, the finger or stylus is lifted from the component.
- If **event.type** is **TouchType.Move**, the finger or stylus is moved along the component.
The touch event supports single and multi-touch interactions. Information about the touch event can be obtained using the **event** parameter, such as the location of the finger that triggers the event, unique identifier of the finger, finger information changed, and the input device source.
```ts
// xxx.ets
@Entry
@Component
struct TouchExample {
@State text: string = '';
@State eventType: string = '';
build() {
Column() {
Button('Touch').height(40).width(100)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.eventType = 'Down';
}
if (event.type === TouchType.Up) {
this.eventType = 'Up';
}
if (event.type === TouchType.Move) {
this.eventType = 'Move';
}
this.text = 'TouchType:' + this.eventType + '\nDistance between touch point and touch element:\nx: '
+ event.touches[0].x + '\n' + 'y: ' + event.touches[0].y + '\nComponent globalPos:('
+ event.target.area.globalPosition.x + ',' + event.target.area.globalPosition.y + ')\nwidth:'
+ event.target.area.width + '\nheight:' + event.target.area.height
})
Button('Touch').height(50).width(200).margin(20)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.eventType = 'Down';
}
if (event.type === TouchType.Up) {
this.eventType = 'Up';
}
if (event.type === TouchType.Move) {
this.eventType = 'Move';
}
this.text = 'TouchType:' + this.eventType + '\nDistance between touch point and touch element:\nx: '
+ event.touches[0].x + '\n' + 'y: ' + event.touches[0].y + '\nComponent globalPos:('
+ event.target.area.globalPosition.x + ',' + event.target.area.globalPosition.y + ')\nwidth:'
+ event.target.area.width + '\nheight:' + event.target.area.height
})
Text(this.text)
}.width('100%').padding(30)
}
}
```
![en-us_image_0000001511900468](figures/en-us_image_0000001511900468.gif)
# Event Overview
Interaction events are classified into touchscreen events, keyboard and mouse events, and focus events based on trigger types.
- [Touchscreen event](arkts-common-events-touch-screen-event.md): single-finger or single-stroke operation performed by a finger or stylus on the touchscreen.
- [Keyboard and mouse event](arkts-common-events-device-input-event.md): includes operation events of the peripheral mouse or touchpad and key events of the peripheral keyboard.
- The mouse event refers to an event responded to when an operation is performed with a peripheral mouse/touchpad.
- The key event refer to an event responded to when an operation is performed with a peripheral keyboard.
- [Focus event](arkts-common-events-focus-event.md): controls the focusability and response events of the component in the preceding ways.
The gesture event includes the gesture binding method and the bound gesture. The bound gesture may be classified into two types: a single gesture and a combined gesture, and is distinguished according to complexity of the gesture.
- [Gesture binding](arkts-gesture-events-binding.md): binds a single gesture or a combination of gestures to a component and declares the response priority of the bound gesture.
- [Single gesture](arkts-gesture-events-single-gesture.md): basic unit of a gesture, which is a part of all complex gestures.
- [Combined gesture](arkts-gesture-events-combined-gestures.md): a combination of multiple single gestures. Multiple single gestures can be combined into a combined gesture according to a declared type and a certain rule, and the combined gesture can be used.
# Drawing Geometric Shapes
The drawing components are used to draw graphs on the page. The **\<Shape>** component is the parent component of the drawing components. The attributes of **\<Shape>** are universal attributes supported by all the drawing components. For details, see [Shape](../reference/arkui-ts/ts-drawing-components-shape.md).
## Creating a Drawing Component
A drawing component can be created in either of the following ways:
- Create a drawing component with **\<Shape>** as their parent to implement the effect similar to SVG. The API used is as follows:
```ts
Shape(value?: PixelMap)
```
In the API, the **value** parameter sets the drawing target. You can draw a graph in the specified **PixelMap** object. If the **value** parameter is not set, the graph is drawn in the current drawing target.
```ts
Shape() {
Rect().width(300).height(50)
}
```
- Create an independent drawing component to draw a specific shape. Seven shapes are supported: [Circle](../reference/arkui-ts/ts-drawing-components-circle.md), [Ellipse](../reference/arkui-ts/ts-drawing-components-ellipse.md), [Line](../reference/arkui-ts/ts-drawing-components-line.md), [Polyine](../reference/arkui-ts/ts-drawing-components-polyline.md), [Polygon](../reference/arkui-ts/ts-drawing-components-polygon.md), [Path](../reference/arkui-ts/ts-drawing-components-path.md), and [Rect](../reference/arkui-ts/ts-drawing-components-rect.md). The following uses the **Circle** API as an example:
```ts
Circle(options?: {width?: string | number, height?: string | number}
```
This API draws a circle on a page. The **width** parameter indicates the width of the circle, and the **height** parameter indicates the height of the circle. The diameter of the circle is determined by the minimum width and height.
```ts
Circle({ width: 150, height: 150 })
```
![creation-2](figures/creation-2.jpg)
## Viewport
```ts
viewPort{ x?: number | string, y?: number | string, width?: number | string, height?: number | string }
```
Creates a viewport, which is a rectangle in the user space that maps to the view boundary established for the associated SVG element. The value of the **viewport** attribute contains four optional parameters: **x**, **y**, **width**, and **height**. **x** and **y** indicate the coordinates of the upper left corner of the viewport, and **width** and **height** indicate the size of the viewport.
The following three examples describe how to use the viewport:
- Zoom in or zoom out a graph through the shape viewport.
```ts
// Draw a circle whose width and height are both 150.
Text ('Original Size Circle')
Circle({width: 75, height: 75}).fill('#E87361')
Row({space:10}) {
Column() {
// Create a shape component whose width and height are both 150, the background color is yellow, and a viewport whose width and height are both 75. Fill the viewport with a blue rectangle and draw a circle with a diameter of 75 in the viewport.
// The drawing is complete. The viewport is zoomed in twice based on the width and height of the component.
Text ('Enlarged Circle')
Shape() {
Rect().width('100%').height('100%').fill('#0097D4')
Circle({width: 75, height: 75}).fill('#E87361')
}
.viewPort({x: 0, y: 0, width: 75, height: 75})
.width(150)
.height(150)
.backgroundColor('#F5DC62')
}
Column() {
// Create a shape component whose width and height are both 150, the background color is yellow, and a viewport whose width and height are both 300. Fill the viewport with a green rectangle and draw a circle with a diameter of 75 in the viewport.
// After the drawing is complete, the viewport is zoomed out by twice based on the width and height of the component.
Text ('Shrunk Circle')
Shape() {
Rect().width('100%').height('100%').fill('#BDDB69')
Circle({width: 75, height: 75}).fill('#E87361')
}
.viewPort({x: 0, y: 0, width: 300, height: 300})
.width(150)
.height(150)
.backgroundColor('#F5DC62')
}
}
```
![2023032401632](figures/2023032401632.jpg)
- Create a shape component whose width and height are both 300, with a yellow background and a viewport whose width and height are both 300. Fill the viewport with a blue rectangle and draw a circle with a radius of 75 in the viewport.
```ts
Shape() {
Rect().width("100%").height("100%").fill("#0097D4")
Circle({ width: 150, height: 150 }).fill("#E87361")
}
.viewPort({ x: 0, y: 0, width: 300, height: 300 })
.width(300)
.height(300)
.backgroundColor("#F5DC62")
```
![viewport-2](figures/viewport-2.jpg)
- Create a shape component whose width and height are both 300, with a yellow background and a viewport whose width and height are both 300. Fill the viewport with a blue rectangle, draw a circle with a radius of 75 in the viewport, and move the viewport 150 to the right and below respectively.
```ts
Shape() {
Rect().width("100%").height("100%").fill("#0097D4")
Circle({ width: 150, height: 150 }).fill("#E87361")
}
.viewPort({ x: -150, y: -150, width: 300, height: 300 })
.width(300)
.height(300)
.backgroundColor("#F5DC62")
```
![viewport-3](figures/viewport-3.jpg)
## Setting Styles
The drawing component allows you to change the component style through various attributes.
- You can use **fill** to set the color of the filling area of the component.
```ts
Path()
.width(100)
.height(100)
.commands('M150 0 L300 300 L0 300 Z')
.fill("#E87361")
```
![2023022792216(1)](figures/2023022792216(1).jpg)
- You can use **stroke** to set the stroke color of a component.
```ts
Path()
.width(100)
.height(100)
.fillOpacity(0)
.commands('M150 0 L300 300 L0 300 Z')
.stroke(Color.Red)
```
![stroke](figures/stroke.jpg)
- You can use **strokeOpacity** to set the stroke opacity.
```ts
Path()
.width(100)
.height(100)
.fillOpacity(0)
.commands('M150 0 L300 300 L0 300 Z')
.stroke(Color.Red)
.strokeWidth(10)
.strokeOpacity(0.2)
```
![strokeopacity](figures/strokeopacity.jpg)
- You can use **strokeLineJoin** to set the join style of the stroke. Options include **Bevel**, **Miter**, and **Round**.
```ts
Polyline()
.width(100)
.height(100)
.fillOpacity(0)
.stroke(Color.Red)
.strokeWidth(8)
.points([[20, 0], [0, 100], [100, 90]])
// Set the join style of the stroke to Round.
.strokeLineJoin(LineJoinStyle.Round)
```
![strokeLineJoin](figures/strokeLineJoin.jpg)
- **strokeMiterLimit** places a limit on the ratio of the miter length to the value of **strokeWidth** used to draw a miter join.
The miter length indicates the distance from the outer tip to the inner corner of the miter. This attribute must be set to a value greater than or equal to 1 and takes effect when **strokeLineJoin** is set to **LineJoinStyle.Miter**.
```ts
Polyline()
.width(100)
.height(100)
.fillOpacity(0)
.stroke(Color.Red)
.strokeWidth(10)
.points([[20, 0], [20, 100], [100, 100]])
// Set the join style of the stroke to Miter.
.strokeLineJoin(LineJoinStyle.Miter)
// Set the limit on the ratio of the miter length to the value of strokeWidth used to draw a miter join.
.strokeMiterLimit(1/Math.sin(45))
Polyline()
.width(100)
.height(100)
.fillOpacity(0)
.stroke(Color.Red)
.strokeWidth(10)
.points([[20, 0], [20, 100], [100, 100]])
.strokeLineJoin(LineJoinStyle.Miter)
.strokeMiterLimit(1.42)
```
![2023032405917](figures/2023032405917.jpg)
- Use the **antiAlias** attribute to set whether to enable anti-aliasing. The default value is true, indicating that anti-aliasing is enabled.
```ts
// Enable anti-aliasing.
Circle()
.width(150)
.height(200)
.fillOpacity(0)
.strokeWidth(5)
.stroke(Color.Black)
```
![untitled](figures/untitled.png)
```ts
// Disable anti-aliasing.
Circle()
.width(150)
.height(200)
.fillOpacity(0)
.strokeWidth(5)
.stroke(Color.Black)
.antiAlias(false)
```
![2023032411518](figures/2023032411518.jpg)
## Example Scenario
- Draw a closed path at (-80, -5). The fill color is 0x317AF7, the stroke width is 10, the stroke color is red, and the Join style of the stroke is miter (default value).
```ts
@Entry
@Component
struct ShapeExample {
build() {
Column({ space: 10 }) {
Shape() {
Path().width(200).height(60).commands('M0 0 L400 0 L400 150 Z')
}
.viewPort({ x: -80, y: -5, width: 500, height: 300 })
.fill(0x317AF7)
.stroke(Color.Red)
.strokeWidth(3)
.strokeLineJoin(LineJoinStyle.Miter)
.strokeMiterLimit(5)
}.width('100%').margin({ top: 15 })
}
}
```
![scenario-1](figures/scenario-1.jpg)
- Draw a circle with a diameter of 150 mm and a ring with a diameter of 150 mm and a red dotted line (use the shorter side as the diameter if the width and height are different).
```ts
@Entry
@Component
struct CircleExample {
build() {
Column({ space: 10 }) {
// Draw a circle whose diameter is 150.
Circle({ width: 150, height: 150 })
// Draw a ring with a diameter of 150 mm and a red dotted line.
Circle()
.width(150)
.height(200)
.fillOpacity(0)
.strokeWidth(3)
.stroke(Color.Red)
.strokeDashArray([1, 2])
}.width('100%')
}
}
```
![scenario-2](figures/scenario-2.jpg)
# Gesture Binding
You can bind to each component different gesture events and design the logic for responding to these events. When a gesture is successfully recognized, the ArkUI framework notifies the component of the gesture recognition result through event callback.
## gesture (Common Gesture Binding Method)
```ts
.gesture(gesture: GestureType, mask?: GestureMask)
```
**gesture** is a frequently used API for binding a gesture to a component.
For example, you can use it to bind the tap gesture to the **\<Text>** component.
```ts
// xxx.ets
@Entry
@Component
struct Index {
build() {
Column() {
Text('Gesture').fontSize(28)
// Use the gesture API to bind the tap gesture.
.gesture(
TapGesture()
.onAction(() => {
console.info('TapGesture is onAction');
}))
}
.height(200)
.width(250)
}
}
```
## priorityGesture (Gesture Binding Method with Priority)
```ts
.priorityGesture(gesture: GestureType, mask?: GestureMask)
```
The **priorityGesture** API binds gestures that are preferentially recognized to a component.
By default, the child component preferentially recognizes the gesture specified by **gesture**, and the parent component preferentially recognizes the gesture specified by **priorityGesture** (if set).
In the following example, the parent component **\<Column>** and child component **\<Text>** are both bound to the tap gesture. As the **\<Column>** is bound to the gesture through **priorityGesture**, the tap gesture recognized by the parent component is preferentially responded to.
```ts
// xxx.ets
@Entry
@Component
struct Index {
build() {
Column() {
Text('Gesture').fontSize(28)
.gesture(
TapGesture()
.onAction(() => {
console.info('Text TapGesture is onAction');
}))
}
.height(200)
.width(250)
// When the tap gesture is bound to the parent <Column> component through priorityGesture, the tap gesture event of the <Text> component is ignored when the text area is tapped, and the tap gesture event of the<Column> component is preferentially responded to.
.priorityGesture(
TapGesture()
.onAction(() => {
console.info('Column TapGesture is onAction');
}), GestureMask.IgnoreInternal)
}
}
```
## parallelGesture (Parallel Gesture Binding Method)
```ts
.parallelGesture(gesture: GestureType, mask?: GestureMask)
```
The **parallelGesture** API binds to a component the gesture that can be triggered together with the child component gesture.
By default, the gesture event does not bubble up. When a parent component and a child component are bound to a same gesture, the gesture events bound to the parent component and the child component compete with each other, and a gesture event of at most one component can be responded to. When **parallelGesture** is set, the same gesture events can be triggered for the parent and child components, thereby implementing a bubbling effect.
```ts
// xxx.ets
@Entry
@Component
struct Index {
build() {
Column() {
Text('Gesture').fontSize(28)
.gesture(
TapGesture()
.onAction(() => {
console.info('Text TapGesture is onAction');
}))
}
.height(200)
.width(250)
// When parallelGesture is set, the tap gestures on the <Column> component and on the child <Text> component are both recognized.
.parallelGesture(
TapGesture()
.onAction(() => {
console.info('Column TapGesture is onAction');
}), GestureMask.IgnoreInternal)
}
}
```
>**NOTE**
>
>When the parent component and the child component are bound to both the click gesture and the double-click gesture, both the parent component and the child component respond only to the click gesture.
# Combined Gestures
A combined gesture consists of multiple single gestures. Different GestureModes are used in GestureGroup to declare the type of the combined gesture. [Continuous recognition](#continuous-recognition), [parallel recognition](#parallel-recognition), and [exclusive recognition](#exclusive-recognition) are supported for a group of gestures.
```ts
GestureGroup(mode:GestureMode, ...gesture:GestureType[])
```
- **mode**: recognition mode of combined gestures. This parameter is mandatory and belongs to the **GestureMode** enumeration class.
- **gesture**: array consisting of multiple gestures. This parameter is mandatory. .
## Continuous Recognition
For continuous recognition, the value of **GestureMode** is **Sequence**. In this gesture mode, gestures registered in the combined gestures will be recognized according to the registration sequence until they are all recognized successfully. If any of the registered gestures fails to be recognized, all gestures fail to be recognized.
In the following example, the combined gestures for continuous recognition are the long press gesture and pan gesture.
The **translate** attribute is bound to a **\<Column>** component. You can set the attribute to translate the component. Then, bind **LongPressGesture** and **PanGesture** to the component in the **Sequence** gesture mode. When a long press gesture is recognized, the displayed number is updated. When the user drags the component after the long press gesture, the component is dragged based on the callback function of the pan gesture.
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State offsetX: number = 0;
@State offsetY: number = 0;
@State count: number = 0;
@State positionX: number = 0;
@State positionY: number = 0;
@State borderStyles: BorderStyle = BorderStyle.Solid
build() {
Column() {
Text('sequence gesture\n' + 'LongPress onAction:' + this.count + '\nPanGesture offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY)
.fontSize(28)
}
// Bind the translate attribute to translate the component.
.translate({ x: this.offsetX, y: this.offsetY, z: 0 })
.height(250)
.width(300)
// The following combined gestures are recognized in sequence. When the long press gesture event is not triggered correctly, the pan gesture event is not triggered.
.gesture(
// Set the gesture mode to Sequence.
GestureGroup(GestureMode.Sequence,
// The first gesture recognized in the combined gestures is the long press gesture, which can be responded to for multiple times.
LongPressGesture({ repeat: true })
// When the long press gesture is successfully recognized, the value of count displayed on the <Text> component is increased.
.onAction((event: GestureEvent) => {
if (event.repeat) {
this.count++;
}
console.info('LongPress onAction');
})
.onActionEnd(() => {
console.info('LongPress end');
}),
// The pan gesture is triggered when the component is dragged after the long press gesture is recognized.
PanGesture()
.onActionStart(() => {
this.borderStyles = BorderStyle.Dashed;
console.info('pan start');
})
// When the gesture is triggered, the pan distance is obtained based on the callback, and the displacement distance of the component is modified. In this way, the component is translated.
.onActionUpdate((event: GestureEvent) => {
this.offsetX = this.positionX + event.offsetX;
this.offsetY = this.positionY + event.offsetY;
console.info('pan update');
})
.onActionEnd(() => {
this.positionX = this.offsetX;
this.positionY = this.offsetY;
this.borderStyles = BorderStyle.Solid;
})
)
)
}
}
```
![sequence](figures/sequence.gif)
>**NOTE**
>
>The drag event is a typical use case of continuous recognition with the long press gesture and pan gesture combined. It is triggered only when the user performs the pan gesture within the preset time frame after a long press gesture is recognized. If the long press gesture is not recognized or the pan gesture is not performed within the preset time frame, the drag event will not be triggered.
## Parallel Recognition
For parallel recognition, the value of **GestureMode** is **Parallel**. In this gesture mode, gestures registered in the combined gestures will be recognized at the same time until they are all recognized successfully. The gestures are recognized in parallel without affecting each other.
For example, if the tap gesture and the double-tap gesture are bound to the \**<Column>** component in parallel recognition mode, they can be recognized at the same time, and the recognition of these two gestures does not interfere with each other.
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State count1: number = 0;
@State count2: number = 0;
build() {
Column() {
Text('parallel gesture\n' + 'tapGesture count is 1:' + this.count1 + '\ntapGesture count is 2:' + this.count2 + '\n')
.fontSize(28)
}
.height(200)
.width(250)
// The following combined gestures are recognized in parallel mode. After a tap gesture is recognized successfully, if another tap gesture is recognized within the specified time frame, a double-tap gesture will also be recognized.
.gesture(
GestureGroup(GestureMode.Parallel,
TapGesture({ count: 1 })
.onAction(() => {
this.count1++;
}),
TapGesture({ count: 2 })
.onAction(() => {
this.count2++;
})
)
)
}
}
```
![parallel](figures/parallel.gif)
>**NOTE**
>
>After a tap gesture and a double-tap gesture are combined for parallel recognition, when taps are performed in an area, the tap gesture and the double-tap gesture are recognized at the same time.
>
>When there is only a single tap, the tap gesture is recognized, but the double-tap gesture fails to be recognized.
>
>When there are two taps and the interval between the two taps is within a specified period (300 ms by default), two tap events and one double-tap event are triggered.
>
>When there are two taps, but the interval between the two taps exceeds the specified time, two tap events are triggered but the double-tap event is not triggered.
## Exclusive Recognition
For exclusive recognition, the value of **GestureMode** is **Exclusive**. In this gesture mode, gestures registered in the combined gesture are recognized at the same time. If one gesture is recognized successfully, the gesture recognition ends, and all other gestures fail to be recognized.
For example, if the tap gesture and the double-tap gesture are bound to the \**<Column>** component in exclusive recognition mode, only a tap gesture event can be triggered. This is because a tap gesture requires a single tap to be triggered, and a double-tap gesture event requires two taps to be triggered; each tap event is consumed by the tap gesture and cannot be accumulated into a double-tap gesture.
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State count1: number = 0;
@State count2: number = 0;
build() {
Column() {
Text('parallel gesture\n' + 'tapGesture count is 1:' + this.count1 + '\ntapGesture count is 2:' + this.count2 + '\n')
.fontSize(28)
}
.height(200)
.width(250)
// The following combined gestures are mutually exclusive. After the tap gesture is recognized successfully, the double-tap gesture fails to be recognized.
.gesture(
GestureGroup(GestureMode.Exclusive,
TapGesture({ count: 1 })
.onAction(() => {
this.count1++;
}),
TapGesture({ count: 2 })
.onAction(() => {
this.count2++;
})
)
)
}
}
```
![exclusive](figures/exclusive.gif)
>**NOTE**
>
>After a tap gesture and a double-tap gesture are combined for exclusive recognition, when taps are performed in an area, the tap gesture and the double-tap gesture are recognized at the same time.
>
>When there is only a single tap, the tap gesture is recognized, but the double-tap gesture fails to be recognized.
>
>When there are two taps, the gesture recognition is declared as successful when the first tap gesture is recognized. In this case, the double-tap gesture fails to be recognized. Even if the second tap is performed within the specified time, the double-tap gesture event is not responded to. Instead, another tap gesture event is triggered.
# Single Gesture
## TapGesture
```ts
TapGesture(value?:{count?:number; fingers?:number})
```
Triggers a tap gesture with one or more taps. This API has two optional parameters:
- **count**: number of consecutive taps required for gesture recognition. The default value is 1. A value less than 1 evaluates to the default value **1**. If multi-tap is configured, the timeout interval between a lift and the next tap is 300 ms.
- **fingers**: number of fingers required for gesture recognition. The value ranges from 1 to 10. The default value is **1**. If the number of fingers used for the tap is less than the specified one within 300 ms after the first finger is tapped, the gesture fails to be recognized. Gesture recognition also fails if the number of fingers used for the tap exceeds the value of **fingers**.
The following example binds a double-tap gesture (a tap gesture whose **count** value is **2**) to the **\<Text>** component:
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State value: string = "";
build() {
Column() {
Text('Click twice').fontSize(28)
.gesture(
// Bind a tap gesture whose count value is 2.
TapGesture({ count: 2 })
.onAction((event: GestureEvent) => {
this.value = JSON.stringify(event.fingerList[0]);
}))
Text(this.value)
}
.height(200)
.width(250)
.padding(20)
.border({ width: 3 })
.margin(30)
}
}
```
![tap](figures/tap.gif)
## LongPressGesture
```ts
LongPressGesture(value?:{fingers?:number; repeat?:boolean; duration?:number})
```
Triggers a long press gesture, which requires one or more fingers with a minimum 500 ms hold-down time. This API has three optional parameters:
- **fingers**: minimum number of fingers required for gesture recognition. The value ranges from 1 to 10. The default value is **1**.
- **repeat**: whether to continuously trigger the event callback. The default value is **false**.
- **duration**: minimum hold-down time, in ms. The default value is **500**.
The following exemplifies how to bind a long press gesture that can be repeatedly triggered to the **\<Text>** component:
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State count: number = 0;
build() {
Column() {
Text('LongPress OnAction:' + this.count).fontSize(28)
.gesture(
// Bind the long press gesture that can be triggered repeatedly.
LongPressGesture({ repeat: true })
.onAction((event: GestureEvent) => {
if (event.repeat) {
this.count++;
}
})
.onActionEnd(() => {
this.count = 0;
})
)
}
.height(200)
.width(250)
.padding(20)
.border({ width: 3 })
.margin(30)
}
}
```
![longPress](figures/longPress.gif)
## PanGesture
```ts
PanGestureOptions(value?:{ fingers?:number; direction?:PanDirection; distance?:number})
```
Triggers a pan gesture, which requires the minimum movement distance (5 vp by default) of a finger on the screen. This API has three optional parameters:
- **fingers**: minimum number of fingers required for gesture recognition. The value ranges from 1 to 10. The default value is **1**.
- **direction**: pan direction. The enumerated value supports the AND (&amp;) and OR (\|) operations. The default value is **Pandirection.All.**
- **distance**: minimum pan distance required for gesture recognition, in vp. The default value is **5**.
The following exemplifies how to bind a pan gesture to the **\<Text>** component. You can pan a component by modifying the layout and position information of the component in the **PanGesture** callback.
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State offsetX: number = 0;
@State offsetY: number = 0;
@State positionX: number = 0;
@State positionY: number = 0;
build() {
Column() {
Text('PanGesture Offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY)
.fontSize(28)
.height(200)
.width(300)
.padding(20)
.border({ width: 3 })
// Bind the layout and position information to the component.
.translate({ x: this.offsetX, y: this.offsetY, z: 0 })
.gesture(
// Bind the pan gesture to the component.
PanGesture()
.onActionStart((event: GestureEvent) => {
console.info('Pan start');
})
// When the drag gesture is triggered, modify the layout and position information of the component based on the callback.
.onActionUpdate((event: GestureEvent) => {
this.offsetX = this.positionX + event.offsetX;
this.offsetY = this.positionY + event.offsetY;
})
.onActionEnd(() => {
this.positionX = this.offsetX;
this.positionY = this.offsetY;
})
)
}
.height(200)
.width(250)
}
}
```
![pan](figures/pan.gif)
>**NOTE**
>
>Most slidable components, such as **\<List>**, **\<Grid>**, **\<Scroll>**, and **\<Tab>**, slide through the pan gesture. Therefore, binding the [pan gesture](#pangesture) or [swipe gesture](#swipegesture) to child components will cause gesture competition.
>
>When a child component is bound to the pan gesture, sliding in the child component area triggers only the pan gesture of the child component. If the parent component needs to respond, you need to modify the gesture binding method or transfer messages from the child component to the parent component, or modify the **PanGesture** parameter distance of the parent and child components to make the panning more sensitive. When a child component is bound to the swipe gesture, you need to modify the parameters of **PanGesture** and **SwipeGesture** to achieve the required effect because the triggering conditions of **PanGesture** and **SwipeGesture** are different.
## PinchGesture
```ts
PinchGesture(value?:{fingers?:number; distance?:number})
```
The pinch gesture is used to trigger a pinch gesture event. A minimum quantity of fingers that trigger the pinch gesture is two fingers, a maximum quantity of fingers that trigger the pinch gesture is five fingers, a minimum recognition distance is 3vp, and there are two optional parameters:
- fingers: specifies the minimum number of fingers required to trigger a pinch gesture. This parameter is optional. The minimum value is 2 and the maximum value is 5. The default value is 2.
- distance: specifies the minimum distance for triggering the pinch gesture. This parameter is optional. The unit is vp. The default value is 3.
For example, to bind a three-finger pinch gesture to the Column component, you can obtain the zoom ratio from the function callback of the pinch gesture to zoom out or zoom in the component.
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State scaleValue: number = 1;
@State pinchValue: number = 1;
@State pinchX: number = 0;
@State pinchY: number = 0;
build() {
Column() {
Column() {
Text('PinchGesture scale:\n' + this.scaleValue)
Text('PinchGesture center:\n(' + this.pinchX + ',' + this.pinchY + ')')
}
.height(200)
.width(300)
.border({ width: 3 })
.margin({ top: 100 })
// Bind the zoom ratio to the component. You can change the zoom ratio to zoom out or zoom in the component.
.scale({ x: this.scaleValue, y: this.scaleValue, z: 1 })
.gesture(
// Bind the pinch gesture triggered by three fingers to the widget.
PinchGesture({ fingers: 3 })
.onActionStart((event: GestureEvent) => {
console.info('Pinch start');
})
// When the pinch gesture is triggered, the callback function can be used to obtain the zoom ratio to change the zoom ratio of the component.
.onActionUpdate((event: GestureEvent) => {
this.scaleValue = this.pinchValue * event.scale;
this.pinchX = event.pinchCenterX;
this.pinchY = event.pinchCenterY;
})
.onActionEnd(() => {
this.pinchValue = this.scaleValue;
console.info('Pinch end');
})
)
}
}
}
```
![pinch](figures/pinch.png)
## RotationGesture
```ts
RotationGesture(value?:{fingers?:number; angle?:number})
```
The rotation gesture is used to trigger a rotation gesture event. A minimum quantity of fingers that trigger the rotation gesture is two fingers, a maximum quantity of fingers that trigger the rotation gesture is five fingers, a minimum change degree is one degree, and there are two optional parameters:
- **fingers**: minimum number of fingers required to trigger a rotation gesture. This parameter is optional. The minimum value is 2 and the maximum value is 5. The default value is 2.
- **angle**: minimum change degree for triggering the rotation gesture. This parameter is optional. The unit is deg. The default value is 1.
For example, a rotation gesture is bound to a **\<Text>** component to implement rotation of the component. A rotation angle may be obtained from a callback function of the rotation gesture, so as to implement rotation of the component:
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State angle: number = 0;
@State rotateValue: number = 0;
build() {
Column() {
Text('RotationGesture angle:' + this.angle).fontSize(28)
// Bind the rotation layout to the component. You can change the rotation angle to rotate the component.
.rotate({ angle: this.angle })
.gesture(
RotationGesture()
.onActionStart((event: GestureEvent) => {
console.info('RotationGesture is onActionStart');
})
// When the rotation gesture takes effect, the rotation angle is obtained by using the callback function of the rotation gesture, so as to modify the rotation angle of the component.
.onActionUpdate((event: GestureEvent) => {
this.angle = this.rotateValue + event.angle;
console.info('RotationGesture is onActionEnd');
})
// Angle of the fixed component at the end of the rotation when the rotation ends and the handle is raised
.onActionEnd(() => {
this.rotateValue = this.angle;
console.info('RotationGesture is onActionEnd');
})
.onActionCancel(() => {
console.info('RotationGesture is onActionCancel');
})
)
}
.height(200)
.width(250)
}
}
```
![rotation](figures/rotation.png)
## SwipeGesture
```ts
SwipeGesture(value?:{fingers?:number; direction?:SwipeDirection; speed?:number})
```
Swipe gestures are used to trigger swipe events. A swipe gesture is recognized when the swipe speed is 100 vp/s or higher. There are three optional parameters:
- **fingers**: minimum number of fingers required to trigger a swipe gesture. TThe minimum value is 1 and the maximum value is 10. The default value is 1.
- **direction**: swipe direction. The enumerated values support the AND and OR operations. The default value is **SwipeDirection.All**.
- **speed**: minimum speed of the swipe gesture, in vp/s. The default value is **100**.
The following describes how to bind a sliding gesture to the Column component to rotate the component:
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State rotateAngle: number = 0;
@State speed: number = 1;
build() {
Column() {
Column() {
Text("SwipeGesture speed\n" + this.speed)
Text("SwipeGesture angle\n" + this.rotateAngle)
}
.border({ width: 3 })
.width(300)
.height(200)
.margin(100)
// Bind rotation to the Column component and change the rotation angle based on the sliding speed and angle of the sliding gesture.
.rotate({ angle: this.rotateAngle })
.gesture(
// Bind the sliding gesture and restrict it to be triggered only when the user slides in the vertical direction.
SwipeGesture({ direction: SwipeDirection.Vertical })
// When the swipe gesture is triggered, the swipe speed and angle are obtained, which can be used to modify the layout parameters.
.onAction((event: GestureEvent) => {
this.speed = event.speed;
this.rotateAngle = event.angle;
})
)
}
}
}
```
![swipe](figures/swipe.gif)
>**NOTE**
>
>When SwipeGesture and PanGesture are bound at the same time, competition occurs if they are bound in default mode or mutually exclusive mode. The trigger condition of SwipeGesture is that the sliding speed reaches 100 vp/s. The trigger condition of PanGesture is that the sliding distance reaches 5 vp and the trigger condition is met first. You can modify the parameters of SwipeGesture and PanGesture to achieve different effects.
# Creating a Grid
## Overview
The grid layout consists of cells formed by rows and columns. You can specify the cells where items are located to form various layouts. The grid layout excels at dividing a page into regions and defining the proportion of child components. It is a key adaptive layout and applies to scenarios such as photo gallery, calendar, and calculator.
ArkUI provides the \<[Grid](../reference/arkui-ts/ts-container-grid.md)> container component and \<[GridItem](../reference/arkui-ts/ts-container-griditem.md)> child component for building grid layouts. The former is used to set parameters related to the grid layout, while the latter is used to define features related to child components. The **\<Grid>** component allows creation of child components with conditional rendering, rendering of repeated content, and [lazy data loading](../quick-start/arkts-rendering-control-lazyforeach.md).
## Layout and Constraints
Each item in the **\<Grid>** container corresponds to a **\<GridItem>** component, as shown below.
**Figure 1** Relationship between \<Grid> and \<GridItem> components
![en-us_image_0000001511900472](figures/en-us_image_0000001511900472.png)
>**NOTE**
>
>The **\<Grid>** component accepts only **\<GridItem>** as its child components.
The grid layout is a two-dimensional layout. The **\<Grid>** component allows you to define the number of rows and columns, proportion of each row and column, number of rows or columns that child components span, and the horizontal and vertical alignment. When the **\<Grid>** container size changes, the child components and spacing are adjusted proportionally. By leveraging these layout capabilities, you can build grid layouts of different styles, as shown below.
**Figure 2** Grid layout
![en-us_image_0000001562700473](figures/en-us_image_0000001562700473.png)
The size of the **\<Grid>** component follows its width and height settings (if configured) or adapts to the size of its parent component.
Depending on the number of rows and columns and the proportion, the **\<Grid>** component behaves as follows:
- If both the number and proportion are set for rows or columns, the **\<Grid>** component displays only elements in the fixed number of rows or columns. Other elements are not displayed, and the component cannot be scrolled.
- If only the number or proportion is set for rows or columns, elements are arranged in the specified direction, and excess elements can be displayed in scrolling mode.
- If neither the number nor the proportion is set for rows or columns, elements are arranged in the layout direction. The number of rows and columns is determined by the layout direction and the width and height of a single grid. Elements that exceed the range of rows and columns are not displayed, and the **\<Grid>** component cannot be scrolled.
>**NOTE**
>
>Whenever possible, set the number or proportion for rows or columns for layout.
## Setting the Arrangement Mode
### Setting the Number and Proportion of Rows and Columns
You can set the number and proportion of rows and columns to determine the overall arrangement mode of the grid layout. To do so, use the **rowsTemplate** and **columnsTemplate** attributes of the **\<Grid>** component.
The values of **rowsTemplate** and **columnsTemplate** are a string consisting of 'number+fr' segments, separated by spaces. Wherein **frs** indicates the number of rows or columns in the grid layout, and the number before **fr** is used to calculate the proportion of the row or column in the grid width, thereby determining the width of the row or column.
**Figure 3** Example of the proportion of rows and columns
![en-us_image_0000001562820833](figures/en-us_image_0000001562820833.png)
The preceding figure shows a grid layout with three rows and three columns. The grid layout is divided into three parts in the vertical direction with each row taking up 1/3, and four parts in the horizontal direction with the first column taking up 1/4, the second column 2/4, and the third column 1/4.
This layout can be implemented by setting **rowsTemplate** to **'1fr 1fr 1fr'** and **columnsTemplate** to **'1fr 2fr 1fr'**.
```ts
Grid() {
...
}
.rowsTemplate('1fr 1fr 1fr')
.columnsTemplate('1fr 2fr 1fr')
```
>**NOTE**
>
>When **rowsTemplate** or **columnsTemplate** is set for the **\<Grid>** component, its **layoutDirection**, **maxCount**, **minCount**, and **cellLength** attributes do not take effect. For details about the attributes, see [Grid Attributes](../reference/arkui-ts/ts-container-grid.md#attributes).
### Setting the Number of Rows and Columns Occupied by a Child Component
In real-world applications, an uneven grid layout, where grid cells span a varying number of cells and rows, is as common as its even counterpart. To implement an uneven grid layout, as shown below, you can set **rowStart**, **rowEnd**, **columnStart**, and **columnEnd** of **\<GridItem>**.
**Figure 4** Uneven grid layout
![en-us_image_0000001511900480](figures/en-us_image_0000001511900480.png)
A common application with an uneven grid layout is the calculator. As shown in the following figure, the **0** key spans the first and second columns, and the **=** key spans the fifth and sixth rows. For a grid layout created using the **\<Grid>** component, the row and column numbers start from 1 and increase continuously.
**Figure 5** Calculator
![en-us_image_0000001511421292](figures/en-us_image_0000001511421292.png)
For a single grid cell, the **rowStart** and** rowEnd** attributes indicate the start and end row numbers of the current element, and the **columnStart** and **columnEnd** attributes indicate the start and end column numbers of the current element.
Therefore, for the **0** key to span the first and second columns, just set **columnStart** and **columnEnd** of the corresponding **\<GridItem>** component to **1** and **2**.
```ts
GridItem() {
Text(key)
...
}
.columnStart(1)
.columnEnd(2)
```
For the **=** key to span the fifth and sixth rows, set **rowStart** and **rowEnd** of the corresponding **\<GridItem>** component to **5** and **6**
```ts
GridItem() {
Text(key)
...
}
.rowStart(5)
.rowEnd(6)
```
### Setting the Main Axis Direction
When neither the number nor proportion is set for rows and columns in a grid layout, you can use the **layoutDirection** attribute to set the main axis direction and thereby specify the arrangement mode of child components. In addition, you can use the **minCount** and **maxCount** attributes to restrict the number of grid cells along the main axis.
**Figure 6** Main axis direction
![en-us_image_0000001562700469](figures/en-us_image_0000001562700469.png)
When **layoutDirection** is set to **Row**, child components are arranged from left to right. When a row is full, a new row will be added. When **layoutDirection** is set to **Column**, child components are arranged from top to bottom. When a column is full, a new column will be added. In this example, the **maxCount** attribute is set to **3**, indicating that the maximum number of grid cells displayed along the main axis is 3.
```ts
Grid() {
...
}
.maxCount(3)
.layoutDirection(GridDirection.Row)
```
>**NOTE**
>
>- The **layoutDirection** attribute takes effect only when **rowsTemplate** and **columnsTemplate** are not set. In this case, child components are arranged in the direction set by **layoutDirection**.
>- When only **rowsTemplate** is set, the main axis of the grid runs in the horizontal direction, and the cross axis runs in the vertical direction.
>- When only **columnsTemplate** is set, the main axis of the grid runs in the vertical direction, and the cross axis runs in the horizontal direction.
## Displaying Data in a Grid Layout
The grid layout organizes its internal elements in two-dimensional layout mode, as shown in the following figure.
**Figure 7** General office services
![en-us_image_0000001563060729](figures/en-us_image_0000001563060729.png)
The **\<Grid>** component can display a group of **\<GridItem>** child components in two-dimensional layout mode.
```ts
Grid() {
GridItem() {
Text('Conference')
...
}
GridItem() {
Text('Sign-in')
...
}
GridItem() {
Text ('Vote')
...
}
GridItem() {
Text ('Print')
...
}
}
.rowsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
```
For multiple **\<GridItem>** components with similar content structures, you are advised to nest them in **ForEach** statements to reduce repeated code.
```ts
@Component
struct OfficeService {
@State services: Array<string> = ['Conference', 'Vote','Sign-in', 'Print']
...
build() {
Column() {
Grid() {
ForEach(this.services, service => {
GridItem() {
Text(service)
...
}
}, service => service)
}
.rowsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
...
}
...
}
}
```
## Setting the Gap Between Rows and Columns
The horizontal spacing between two grid cells is called row spacing, and the vertical spacing is called column spacing, as shown in the following figure.
**Figure 8** Row spacing and column spacing
![en-us_image_0000001511580908](figures/en-us_image_0000001511580908.png)
You can use **rowsGap** and **columnsGap** to set the row spacing and column spacing of the grid layout. In the calculator shown in Figure 5, the row spacing is 15 vp, and the column spacing is 10vp.
```ts
Grid() {
...
}
.columnsGap(10)
.rowsGap(15)
```
## Building a Scrollable Grid Layout
The scrollable grid layout is often used on the file list, product list, video list, and similar pages, as shown in the following figure. When only the number or proportion is set for rows and columns, that is, only the **rowsTemplate** or **columnsTemplate** attribute is set, the elements in the grid are arranged in the configured direction. When the content goes beyond the display area, the grid can be scrolled.
**Figure 9** Horizontal scrollable grid layout
![en-us_image_0000001511740512](figures/en-us_image_0000001511740512.gif)
If **columnsTemplate** is set, the grid scrolls vertically. If **rowsTemplate** is set, the grid scrolls horizontally.
In the horizontal scrollable grid layout shown in the preceding figure, **rowsTemplate** is set but **columnsTemplate** is not. When the content exceeds the width of the grid, the grid can scroll horizontally to display the content outside of the display area.
```ts
@Component
struct Shopping {
@State services: Array<string> = ['Live', 'Premium', ...]
...
build() {
Column({ space: 5 }) {
Grid() {
ForEach(this.services, (service: string, index) => {
GridItem() {
...
}
.width('25%')
}, service => service)
}
.rowsTemplate('1fr 1fr') // Set only the rowsTemplate attribute. When the content exceeds the display area of the grid, the grid can be scrolled horizontally.
.rowsGap(15)
...
}
...
}
}
```
## Controlling the Scrolling Position
Similar to the Back to top button in a list layout, the feature of controlling the scrolling position is commonly used in the grid layout, for example, page turning in the calendar application, as shown below.
**Figure 10** Page turning in the calendar application
![en-us_image_0000001562940549](figures/en-us_image_0000001562940549.gif)
When the **\<Grid>** component is initialized, it can be bound to a [Scroller](../reference/arkui-ts/ts-container-scroll.md/#scroller) object for scrolling control. In this example, the [scrollPage](../reference/arkui-ts/ts-container-scroll.md/#scrollpage) API of the **Scroller** object is used to turn pages.
```ts
private scroller: Scroller = new Scroller()
```
On the calendar page, when a user clicks the **Next** button, the application responds to the click event by setting the **next** parameter in the **scrollPage** API to **true** to scroll to the next page.
```ts
Column({ space: 5 }) {
Grid(this.scroller) {
...
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
...
Row({space: 20}) {
Button ('Previous')
.onClick(() => {
this.scroller.scrollPage({
next: false
})
})
Button ('Next')
.onClick(() => {
this.scroller.scrollPage({
next: true
})
})
}
}
...
```
## Performance Optimization
Just as [LazyForEach](../quick-start/arkts-rendering-control-lazyforeach.md) is recommended for [handling a long list](arkts-layout-development-create-list.md#handling-a-long-list), it is also recommended for a scrolling grid layout when a large number of grid items is involved.
For details about the implementation, see the example in [LazyForEach: Lazy Data Loading](../quick-start/arkts-rendering-control-lazyforeach.md).
When the grid is rendered in lazy loading mode, to improve the grid scrolling experience and minimize white blocks during grid scrolling, you can use the **cachedCount** parameter of the **\<Grid>** component. This parameter sets the number of grid items preloaded outside of the screen and is valid only in **LazyForEach**.
Specifically, the number of the grid items to cache before and after the currently displayed one equals the value of **cachedCount** multiplied by the number of columns. Grid items that exceed the display and cache range are released.
```ts
Grid() {
LazyForEach(this.dataSource, item => {
GridItem() {
...
}
})
}
.cachedCount(3)
```
> **NOTE**
>
> A greater **cachedCount** value may result in higher CPU and memory overhead of the UI. Adjust the value by taking into account both the comprehensive performance and user experience.
<!--no_check-->
\ No newline at end of file
# Flex Layout
## Overview
The flex layout, implemented using the [\<Flex>](../reference/arkui-ts/ts-container-flex.md) container component, provides simple and powerful tools for flexibly distributing space and aligning items. By default, the flex container has a main axis and a cross axis. Child elements are arranged along the main axis by default. The size of a child element along the main axis is referred to as its main axis size. Similarly, the size of a child element along the cross axis is referred to as its cross axis size. The flex layout is widely used in scenarios such as the navigation bar distribution on the page header, page framework setup, and arrangement of multiple lines of data.
**Figure 1** Flex container whose main axis runs in the horizontal direction
![flex-layout](figures/flex-layout.png)
## Basic Concepts
- Main axis: axis along which child elements are placed in the **\<Flex>** component. Child elements are laid out along the main axis by default. The start point of the main axis is referred to as main-start, and the end point is referred to as main-end.
- Cross axis: axis that runs perpendicular to the main axis. The start point of the cross axis is referred to as cross-start, and the end point is referred to as cross-end.
## Layout Direction
In the flex layout, the child elements can be arranged in any direction. You can set the **direction** parameter to define the direction of the main axis and thereby control the arrangement of child elements.
**Figure 2** Flex layout direction
![flex-layout-direction](figures/flex-layout-direction.png)
- **FlexDirection.Row** (default value): The main axis runs along the row horizontally, and the child components are laid out from the start edge of the main axis.
```ts
Flex({ direction: FlexDirection.Row }) {
Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001562820817](figures/en-us_image_0000001562820817.png)
- **FlexDirection.RowReverse**: The main axis runs along the row horizontally, and the child components are laid out from the end edge of the main axis, in a direction opposite to **FlexDirection.Row**.
```ts
Flex({ direction: FlexDirection.RowReverse }) {
Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511900464](figures/en-us_image_0000001511900464.png)
- **FlexDirection.Column**: The main axis runs along the column vertically, and the child components are laid out from the start edge of the main axis.
```ts
Flex({ direction: FlexDirection.Column }) {
Text('1').width('100%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('100%').height(50).backgroundColor(0xD2B48C)
Text('3').width('100%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511580884](figures/en-us_image_0000001511580884.png)
- **FlexDirection.ColumnReverse**: The main axis runs along the column vertically, and the child components are laid out from the end edge of the main axis, in a direction opposite to **FlexDirection.Column**.
```ts
Flex({ direction: FlexDirection.ColumnReverse }) {
Text('1').width('100%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('100%').height(50).backgroundColor(0xD2B48C)
Text('3').width('100%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001562940541](figures/en-us_image_0000001562940541.png)
## Wrapping in the Flex Layout
In the flex layout, child elements can be laid on a single line (also called an axis) or on multiple lines. By default, child elements are laid out on a single line in the flex container. You can use the **wrap** attribute to set whether child elements can wrap onto multiple lines when the total main axis size of the child elements is greater than the main axis size of the container. When wrapped onto multiple lines, the child elements on the new line are stacked in the direction based on the cross axis direction.
- **FlexWrap.NoWrap** (default value): Child components are laid out on a single line. This may cause the child components to shrink to fit the container when the total width of the child components is greater than the width of the container.
```ts
Flex({ wrap: FlexWrap.NoWrap }) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001562700425](figures/en-us_image_0000001562700425.png)
- **FlexWrap.Wrap**: Child components can break into multiple lines along the main axis.
```ts
Flex({ wrap: FlexWrap.Wrap }) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xD2B48C)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511740468](figures/en-us_image_0000001511740468.png)
- **FlexWrap.WrapReverse**: Child components can break into multiple lines in the reverse direction to the main axis.
```ts
Flex({ wrap: FlexWrap.WrapReverse}) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001562940521](figures/en-us_image_0000001562940521.png)
## Alignment on the Main Axis
Use the **justifyContent** parameter to set alignment of child components on the main axis.
![flex-spindle-alignment](figures/flex-spindle-alignment.png)
- **FlexAlign.Start** (default value): The child components are aligned with each other toward the start edge of the container along the main axis.
```ts
Flex({ justifyContent: FlexAlign.Start }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511421280](figures/en-us_image_0000001511421280.png)
- **FlexAlign.Center**: The child components are aligned with each other toward the center of the container along the main axis.
```ts
Flex({ justifyContent: FlexAlign.Center }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001563060681](figures/en-us_image_0000001563060681.png)
- **FlexAlign.End**: The child components are aligned with each other toward the end edge of the container along the main axis.
```ts
Flex({ justifyContent: FlexAlign.End }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001562820809](figures/en-us_image_0000001562820809.png)
- **FlexAlign.SpaceBetween**: The child components are evenly distributed within the container along the main axis. The first and last child components are aligned with the edges of the container.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511421288](figures/en-us_image_0000001511421288.png)
- **FlexAlign.SpaceAround**: The child components are evenly distributed in the container along the main axis. The space between the first child component and main-start, and that between the last child component and main-end are both half of the space between two adjacent child components.
```ts
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511900436](figures/en-us_image_0000001511900436.png)
- **FlexAlign.SpaceEvenly**: The child components are equally distributed along the main axis. The space between the first child component and main-start, the space between the last child component and main-end, and the space between two adjacent child components are the same.
```ts
Flex({ justifyContent: FlexAlign.SpaceEvenly }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001563060713](figures/en-us_image_0000001563060713.png)
## Alignment on the Cross Axis
Alignment on the cross axis can be set for both the container and child elements, with that set for child elements having a higher priority.
### Setting Alignment on the Cross Axis for the Container
Use the **alignItems** parameter of the **\<Flex>** component to set alignment of child elements on the cross axis.
- **ItemAlign.Auto**: The child elements are automatically aligned in the flex container.
```ts
Flex({ alignItems: ItemAlign.Auto }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({ width: '90%', height: 80 })
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001563060677](figures/en-us_image_0000001563060677.png)
- **ItemAlign.Start**: The child elements are aligned with the start edge of the container along the cross axis.
```ts
Flex({ alignItems: ItemAlign.Start }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({ width: '90%', height: 80 })
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001562700453](figures/en-us_image_0000001562700453.png)
- **ItemAlign.Start**: The child elements are aligned with the center of the container along the cross axis.
```ts
Flex({ alignItems: ItemAlign.Center }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({ width: '90%', height: 80 })
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511740484](figures/en-us_image_0000001511740484.png)
- **ItemAlign.End**: The child elements are aligned with the end edge of the container along the cross axis.
```ts
Flex({ alignItems: ItemAlign.End }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({ width: '90%', height: 80 })
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511580876](figures/en-us_image_0000001511580876.png)
- **ItemAlign.Stretch**: The child elements are stretched along the cross axis. If no constraints are set, the child elements are stretched to fill the size of the container on the cross axis.
```ts
Flex({ alignItems: ItemAlign.Stretch }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({ width: '90%', height: 80 })
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511421252](figures/en-us_image_0000001511421252.png)
- **ItemAlign.Baseline**: The child elements are aligned at the baseline of the cross axis.
```ts
Flex({ alignItems: ItemAlign.Baseline }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({ width: '90%', height: 80 })
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511900440](figures/en-us_image_0000001511900440.png)
### Setting Alignment on the Cross Axis for Child Components
Use the [alignSelf](../reference/arkui-ts/ts-universal-attributes-flex-layout.md) attribute of child components to set their alignment in the container on the cross axis. The settings overwrite the default **alignItems** settings in the flex container. The sample code is as follows:
```ts
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) { // The child components are aligned with the center of the container.
Text('alignSelf Start').width('25%').height(80)
.alignSelf(ItemAlign.Start)
.backgroundColor(0xF5DEB3)
Text('alignSelf Baseline')
.alignSelf(ItemAlign.Baseline)
.width('25%')
.height(80)
.backgroundColor(0xD2B48C)
Text('alignSelf Baseline').width('25%').height(100)
.backgroundColor(0xF5DEB3)
.alignSelf(ItemAlign.Baseline)
Text('no alignSelf').width('25%').height(100)
.backgroundColor(0xD2B48C)
Text('no alignSelf').width('25%').height(100)
.backgroundColor(0xF5DEB3)
}.width('90%').height(220).backgroundColor(0xAFEEEE)
```
![en-us_image_0000001562940533](figures/en-us_image_0000001562940533.png)
In the preceding example, both **alignItems** of the **\<Flex>** component and the **alignSelf** attribute of the child component are both set. In this case, the **alignSelf** settings take effect.
### Content Alignment
Use the [alignContent](../reference/arkui-ts/ts-container-flex.md) parameter to set how space is distributed between and around child components along the cross axis. This parameter is valid only for a flex layout that contains multiple lines. The available options are as follows:
- **FlexAlign.Start**: The child components are aligned toward the start edge of the cross axis in the container.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Start }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511900460](figures/en-us_image_0000001511900460.png)
- **FlexAlign.Center**: The child components are aligned toward the center of the cross axis in the container.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Center }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511421256](figures/en-us_image_0000001511421256.png)
- **FlexAlign.End**: The child components are aligned toward the end edge of the cross axis in the container.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.End }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001562820801](figures/en-us_image_0000001562820801.png)
- **FlexAlign.SpaceBetween**: The child components are evenly distributed in the container along the cross axis, with the first and last child components aligned with the edges of the cross axis.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceBetween }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511900448](figures/en-us_image_0000001511900448.png)
- **FlexAlign.SpaceAround**: The child components are evenly distributed in the container along the cross axis. The spacing before the first child component and after the last child component is half of the spacing between two adjacent child components.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceAround }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001562700445](figures/en-us_image_0000001562700445.png)
- **FlexAlign.SpaceEvenly**: The child components are evenly distributed in the container along the cross axis. The spacing between each two adjacent child components, the spacing between the start edge and the first child component, and the spacing between the end edge and the last child component, are the same.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceEvenly }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001511580864](figures/en-us_image_0000001511580864.png)
## Adaptive Stretching
When the size of the container in the flex layout is not large enough, the following attributes of the child component can be used to achieve adaptive layout.
- **flexBasis**: base size of the child component in the container along the main axis. It sets the space occupied by the child component. If this attribute is not set, the space occupied by the child component is the value of width/height.
```ts
Flex() {
Text('flexBasis("auto")')
.flexBasis('auto') // When width is not set and flexBasis is set to auto, the content is loose.
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexBasis("auto")'+' width("40%")')
.width('40%')
.flexBasis('auto') // When width is set and flexBasis is set to auto, the value of width is used.
.height(100)
.backgroundColor(0xD2B48C)
Text('flexBasis(100)') // When width is not set and flexBasis is set to 100, the width is 100 vp.
.flexBasis(100)
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexBasis(100)')
.flexBasis(100)
.width(200) // When width is set to 200 and flexBasis 100, the width is 100 vp, which means that the settings of flexBasis take precedence.
.height(100)
.backgroundColor(0xD2B48C)
}.width('90%').height(120).padding(10).backgroundColor(0xAFEEEE)
```
![en-us_image_0000001562940505](figures/en-us_image_0000001562940505.png)
- **flexGrow**: percentage of the flex layout's remaining space that is allocated to the child component. In other words, it is the grow factor of the child component.
```ts
Flex() {
Text('flexGrow(1)')
.flexGrow(2)
.width(100)
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexGrow(2)')
.flexGrow(2)
.width(100)
.height(100)
.backgroundColor(0xD2B48C)
Text('no flexGrow')
.width(100)
.height(100)
.backgroundColor(0xF5DEB3)
}.width(400).height(120).padding(10).backgroundColor(0xAFEEEE)
```
![en-us_image_0000001562700449](figures/en-us_image_0000001562700449.png)
In the preceding figure, the width of the parent container is 400 vp, the original width of the three child components is 100 vp each, which adds up to the total width of 300 vp. The remaining space 100 vp is allocated to the child components based on their **flexGrow** settings. Child components that do not have **flexGrow** set are not involved in the allocation of remaining space.
The first child component and the second child component receive their share of remaining space at the 2:3 ratio. The width of the first child component is 100 vp + 100 vp x 2/5 = 140 vp, and the width of the second child component is 100 vp + 100 vp x 3/5 = 160 vp.
- **flexShrink**: shrink factor of the child component when the size of all child components is larger than the flex container.
```ts
Flex({ direction: FlexDirection.Row }) {
Text('flexShrink(3)')
.flexShrink(3)
.width(200)
.height(100)
.backgroundColor(0xF5DEB3)
Text('no flexShrink')
.width(200)
.height(100)
.backgroundColor(0xD2B48C)
Text('flexShrink(2)')
.flexShrink(2)
.width(200)
.height(100)
.backgroundColor(0xF5DEB3)
}.width(400).height(120).padding(10).backgroundColor(0xAFEEEE)
```
![en-us_image_0000001562820813](figures/en-us_image_0000001562820813.png)
## Example
In this example, child components can be arranged horizontally in the flex layout, aligned at both edges, evenly spaced, and centered in the vertical direction.
```ts
@Entry
@Component
struct FlexExample {
build() {
Column() {
Column({ space: 5 }) {
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
Text('1').width('30%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('30%').height(50).backgroundColor(0xD2B48C)
Text('3').width('30%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.backgroundColor(0xAFEEEE)
}.width('100%').margin({ top: 5 })
}.width('100%')
}
}
```
![en-us_image_0000001511900452](figures/en-us_image_0000001511900452.png)
# Media Query
## Overview
[Media queries](../reference/apis/js-apis-mediaquery.md) are at the core of responsive design and widely used on mobile devices. You can use media queries to apply application styles based on the device type or device state. Specifically, media queries allow you to:
1. Design a layout style based on the device and application attributes (such as display area, dark light color, and resolution).
2. Update the page layout to adapt to dynamic screen changes (for example, screen splitting or switching between landscape and portrait modes).
## Usage
Invoke the API in the **mediaquery** module to set the media query condition and the callback, and change the page layout or implement service logic in the callback corresponding to the condition. The procedure is as follows:
Import the **mediaquery** module, as shown below:
```ts
import mediaquery from '@ohos.mediaquery';
```
Use the **matchMediaSync** API to set the media query condition and save the returned listener. The following is the example for listening for landscape events:
```ts
let listener = mediaquery.matchMediaSync('(orientation: landscape)');
```
Register the **onPortrait** callback using the saved listener, and change the page layout or implement service logic in the callback. When the media query condition is matched, the callback is triggered. The sample code is as follows:
```ts
onPortrait(mediaQueryResult) {
if (mediaQueryResult.matches) {
// do something here
} else {
// do something here
}
}
listener.on('change', onPortrait);
```
## Media Query Conditions
The media query condition consists of the media type (optional), logical operator, and media feature. The logical operator is used to connect different media types and media features. A media feature must be enclosed in parentheses (). There may be multiple media features. The specific rules are as follows:
### Syntax
Syntax rules include [media-type](#media-type), [media-logic-operations](#media-logic-operations), and [media-feature](#media-feature).
```ts
[media-type] [media-logic-operations] [(media-feature)]
```
Examples are as follows:
- **screen and (round-screen: true)**: The query is valid when the device screen is round.
- **(max-height: 800)**: The query is valid when the height is less than or equal to 800.
- **(height &lt;= 800)**: The query is valid when the height is less than or equal to 800.
- **screen and (device-type: tv) or (resolution < 2)**: The query is valid when the device type is TV or the device resolution is less than 2. This is a multi-condition query that contains multiple media features.
### media-type
| **Type**| **Description** |
| ------ | -------------- |
| screen | Media query based on screen-related parameters.|
### media-logic-operations
You can use logical operators (**and**, **or**, **not**, and **only**) to compose complex media queries. You can also combine them using comma (,). The following table describes the operators.
**Table 1** Media logical operators
| Type | Description |
| -------------- | ---------------------------------------- |
| and | The **and** operator is used to combine multiple media features into one media query, in a logical AND operation. The query is valid only when all media features are true. It can also combine media types and media functions. For example, **screen and (device-type: wearable) and (max-height: 600)** evaluates to **true** when the device type is wearable and the maximum height of the application is 600 pixel units.|
| or | The **or** operator is used to combine multiple media features into one media query, in a logical OR operation. The query is valid if a media feature is true. For example, **screen and (max-height: 1000) or (round-screen: true)** indicates that the query is valid when the maximum height of the application is 1000 pixel units or the device screen is round.|
| not | The **not** operator is used to perform a logical negation for a media query. **true** is returned if the query condition is not met. Otherwise, **false** is returned. For example, **not screen and (min-height: 50) and (max-height: 600)** evaluates to **true** when the height of the application is less than 50 pixel units or greater than 600 pixel units.<br>You must specify the media type when using the **not** operator.|
| only | The **only** operator applies the selected style only when the entire expression is matched. It can be used to prevent ambiguity on browsers of earlier versions. The statements that contain both media types and media features produce ambiguity when they are received by some browsers of earlier versions. For example, regarding **screen and (min-height: 50)**, the browsers of earlier versions would mislead this sentence into **screen**, causing the fact that the specified style is applied when only the media type is matched. In this case, the **only** operator can be used to avoid this issue.<br>You must specify the media type when using the **only** operator.|
| comma (, ) | The **or** operator is used to combine multiple media features into one media query, in a logical OR operation. The query is valid if a media feature is true. The effect of a comma operator is equivalent to that of the **or** operator. For example, **screen and (min-height: 1000), (round-screen: true)** indicates that the query is valid when the minimum height of the application is 1000 pixel units or the device screen is round.|
Media range operators include <=, >=, <, and >. For details, see the following table.
**Table 2** Logical operators for range query
| Type | Description |
| ----- | ---------------------------------------- |
| &lt;= | Less than or equal to, for example, **screen and (50 &lt;= height)**.|
| &gt;= | Greater than or equal to, for example, **screen and (600 &gt;= height)**.|
| &lt; | Less than, for example, **screen and (50 &lt; height)**.|
| &gt; | Greater than, for example, **screen and (height > 600)**.|
### media-feature
The media features include the width and height of the application display area, device resolution, and device width and height. For details, see the following table.
**Table 3** Media features
| Type | Description |
| ----------------- | ---------------------------------------- |
| height | Height of the drawing area of the application. |
| min-height | Minimum height of the drawing area of the application. |
| max-height | Maximum height of the drawing area of the application. |
| width | Width of the drawing area of the application. |
| min-width | Minimum width of the drawing area of the application. |
| max-width | Maximum width of the drawing area of the application. |
| resolution | Resolution of the device. The unit can be dpi, dppx, or dpcm. <br>- **dpi** indicates the number of physical pixels per inch. 1 dpi ≈ 0.39 dpcm.<br>- **dpcm** indicates the number of physical pixels per centimeter. 1 dpcm ≈ 2.54 dpi.<br>- **dppx** indicates the number of physical pixels in each pixel. (This unit is calculated based on this formula: 96 px = 1 inch, which is different from the calculation method of the px unit on the page.) 1 dppx = 96 dpi.|
| min-resolution | Minimum device resolution. |
| max-resolution | Maximum device resolution. |
| orientation | Screen orientation.<br>Options are as follows:<br>- orientation: portrait<br>- orientation: landscape|
| device-height | Height of the device. |
| min-device-height | Minimum height of the device. |
| max-device-height | Maximum height of the device. |
| device-width | Width of the device. |
| device-type | Type of the device.<br>Available options: **default** and **tablet** |
| min-device-width | Minimum width of the device. |
| max-device-width | Maximum width of the device. |
| round-screen | Screen type. The value **true** indicates a circular screen, and **false** indicates a non-circular screen. |
| dark-mode | Whether the device is in dark mode. The value **true** means that the device is in dark mode, and **false** means the opposite. |
## Example Scenario
In the following examples, media queries are used to apply different content and styles to the page text when the screen is switched between landscape and portrait modes.
Stage model:
```ts
import mediaquery from '@ohos.mediaquery';
import window from '@ohos.window';
import common from '@ohos.app.ability.common';
let portraitFunc = null;
@Entry
@Component
struct MediaQueryExample {
@State color: string = '#DB7093';
@State text: string = 'Portrait';
// The query is valid when the device is in landscape mode.
listener = mediaquery.matchMediaSync('(orientation: landscape)');
// The callback is triggered when the query is valid.
onPortrait(mediaQueryResult) {
if (mediaQueryResult.matches) {// If the device is in landscape mode, the page layout is changed accordingly.
this.color = '#FFD700';
this.text = 'Landscape';
} else {
this.color = '#DB7093';
this.text = 'Portrait';
}
}
aboutToAppear() {
// Bind to the current application instance.
portraitFunc = this.onPortrait.bind(this);
// Register the callback.
this.listener.on('change', portraitFunc);
}
// Change the landscape/portrait mode of the device in the callback.
private changeOrientation(isLandscape: boolean) {
// Obtain the context information of the UIAbility instance.
let context = getContext(this) as common.UIAbilityContext;
// Invoke this API to manually change the landscape/portrait mode of the device.
window.getLastWindow(context).then((lastWindow) => {
lastWindow.setPreferredOrientation(isLandscape ? window.Orientation.LANDSCAPE : window.Orientation.PORTRAIT)
});
}
build() {
Column({ space: 50 }) {
Text(this.text).fontSize(50).fontColor(this.color)
Text('Landscape').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange)
.onClick(() => {
this.changeOrientation(true);
})
Text('Portrait').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange)
.onClick(() => {
this.changeOrientation(false);
})
}
.width('100%').height('100%')
}
}
```
FA model:
```ts
import mediaquery from '@ohos.mediaquery';
import featureAbility from '@ohos.ability.featureAbility';
let portraitFunc = null;
@Entry
@Component
struct MediaQueryExample {
@State color: string = '#DB7093';
@State text: string = 'Portrait';
listener = mediaquery.matchMediaSync('(orientation: landscape)'); // The query is valid when the device is in landscape mode.
onPortrait(mediaQueryResult) {// The callback is triggered when the query is valid.
if (mediaQueryResult.matches) { // If the device is in landscape mode, the page layout is changed accordingly.
this.color = '#FFD700';
this.text = 'Landscape';
} else {
this.color = '#DB7093';
this.text = 'Portrait';
}
}
aboutToAppear() {
portraitFunc = this.onPortrait.bind(this) // Bind to the current application instance.
this.listener.on('change', portraitFunc); // Register the callback.
}
build() {
Column({ space: 50 }) {
Text(this.text).fontSize(50).fontColor(this.color)
Text('Landscape').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange)
.onClick(() => {
let context = featureAbility.getContext();
context.setDisplayOrientation(0); // Invoke this API to manually change the landscape/portrait mode of the device.
})
Text('Portrait').fontSize(50).fontColor(this.color).backgroundColor(Color.Orange)
.onClick(() => {
let context = featureAbility.getContext();
context.setDisplayOrientation(1); // Invoke this API to manually change the landscape/portrait mode of the device.
})
}
.width('100%').height('100%')
}
}
```
**Figure 1** Portrait mode
![portralit](figures/portralit.jpg)
**Figure 2** Landscape mode
![landscape](figures/landscape.jpg)
# Layout Overview
In declarative UI, page layout is the art of organizing custom components sequentially over a page. A well-designed layout can help you present information intuitively and effectively.
You design page layout by leveraging specific components or attributes to control the size and position of components on pages. The design process involves the following steps:
- Determine the layout structure of the page.
- Analyze the elements on the page.
- Select container components and attributes to control the position and size constraints of each element on the page.
## Layout Structure
A layout is generally in a hierarchical structure, which represents the overall architecture in the UI. Below is a common page structure.
**Figure 1** Common page structure
![common-page-structure](figures/common-page-structure.png)
To achieve the preceding effect, you need to declare the corresponding elements on the page. **Page** indicates the root node of the page, and other elements such as **Column** and **Row** are built-in components. ArkUI provides a wide variety of layout components, which you can draw on to implement different layouts. For example, you can use **Row** to implement a linear layout.
## Layout Elements
You can leverage layout-related container components to create a specific layout. For example, the **List** component can form a linear layout.
**Figure 2** Layout elements
![layout-element-omposition](figures/layout-element-omposition.png)
- Component area (blue block): size of the component and is determined by the width and height attributes.
- Component content area (yellow block): size of the component area minus the paddings of the component. It is used as the layout calculation constraint for component content (or child component) size calculation.
- Component content (green block): size of the component content, for example, size of the text content. The component content may not match the component content area. For example, if fixed width and height values are set, the component content area is the size obtained with the width and height minus the paddings. The component content is the size calculated by the layout engine, which may be less than the component content area. When the component content and component content area do not match, the **align** attribute takes effect, which defines the alignment mode of the component content in the component content area, for example, center aligned.
- Component layout bounds (dotted lines): component area plus the margins (if supplied).
## Layout Selection
The declarative UI provides eight common layouts. Choose a layout that best suits the use case.
| Layout | Description |
| ---------------------------------------- | ---------------------------------------- |
| [Linear layout](arkts-layout-development-linear.md) (Row and Column)| Use this layout when there are multiple sub-elements and they can be arranged linearly. |
| [Stack layout](arkts-layout-development-stack-layout.md) (Stack)| Use this layout when you want to stack elements. The stacking does not occupy or affect the layout space of other child components in the same container. For example, when the [\<Panel>](../reference/arkui-ts/ts-container-panel.md) component is displayed as a child, superimposing it over other components makes more sense. In this case, the stack layout is preferred at the outer layer.|
| [Flex layout](arkts-layout-development-flex-layout.md) (Flex)| The flex layout is similar to the linear layout. However, it empowers the container to adjust the size of its child components to best fill the available space. Use this layout when you need elements to stretch or shrink to fit into the container.|
| [Relative layout](arkts-layout-development-relative-layout.md) (RelativeContainer)| The relative layout is a two-dimensional layout system. It does not need to comply with linear layout rules, and therefore exhibits more flexibility. By setting anchor rules (**AlignRules**) on a child component, you enable the component to position itself on the horizontal axis and vertical axis as relative to other child component in the container. Anchor rules support compression, stretching, stacking, and wrapping of child components. Use this layout when the distribution of elements is complex or when a linear layout may result in deeply nested components in the container.|
| [Responsive grid layout](arkts-layout-development-grid-layout.md) (GridRow and GridCol)| The responsive grid is an auxiliary positioning tool for a multi-device application, dividing space into rows and columns. Unlike the regular grid, the responsive grid is not allocating fixed-size space. Instead, it allows a layout to dynamically change based on the screen size. In this way, the design and development costs for adapting to different screen sizes are significantly reduced, and the overall design and development process is more orderly and rhythmic. In addition, the responsive grid offers a consistent display experience across devices. Use this layout when you are presenting the same content on different screen sizes.|
| [Media query](arkts-layout-development-media-query.md) (\@ohos.mediaquery)| You can use media queries to apply application styles based on the device type or device state. For example, you can apply specific layouts based on the attribute information of the target device and application, and update a page layout to reflect the dynamic screen changes.|
| [List](arkts-layout-development-create-list.md) (List)| Use lists to easily and efficiently display structured, scrollable information. In ArkUI, the list allows you to lay out elements in the horizontal or vertical layout and is able to adapt to the number of elements in the cross axis direction. It can scroll when the content overflows. Use this layout when you are presenting similar data types or data type sets, such as images and text.|
| [Grid](arkts-layout-development-create-grid.md) (Grid)| As an important adaptive layout, the grid excels at spacing elements evenly and defining the relationship between the elements. The grid layout controls the number of cells occupied by child components, number of rows or columns that child components span, and how the child components and spacing are adjusted proportionally when the grid container size changes. Use this layout in scenarios where space needs to be allocated evenly or based on a fixed proportion, such as calculators, albums, and calendars.|
| [Looping](arkts-layout-development-create-looping.md) (Swiper)| Use this layout when you want to implement ad rotation, image preview, or scrollable content. |
## Layout Position
Attributes such as **position** and **offset** affect the position of the layout container relative to itself or other components.
| Positioning Capability| Description | Implementation |
| ---- | ---------------------------------------- | ---------------------------------------- |
| Absolute positioning| Absolute positioning is poor in adaptability to devices of different sizes, and is prone to screen adaptation errors. | Use [position](../reference/arkui-ts/ts-universal-attributes-location.md) to implement absolute positioning and set the offset position of the upper left corner of an element relative to the upper left corner of the parent container. When laying out components, this attribute does not affect the layout of the parent component. It only adjusts the component position during drawing.|
| Relative positioning| Relative positioning keeps an element in the normal document flow, while allowing you to move it around relative to its original location.| You can use [offset](../reference/arkui-ts/ts-universal-attributes-location.md) to implement relative positioning and set the offset of an element relative to itself. This attribute does not affect the layout of the parent component. It only adjusts the component position during drawing.|
## Constraints on Child Components
| Constraint| Description | Implementation |
| --------- | ---------------------------------------- | ---------------------------------------- |
| Stretching | When the size of a container component changes, the increased or decreased amount of space is allocated to the specified area in the container component. | [flexGrow](../reference/arkui-ts/ts-universal-attributes-flex-layout.md) and [flexShrink](../reference/arkui-ts/ts-universal-attributes-flex-layout.md) attributes:<br>1. **flexGrow** defines the grow factor of a flex item.<br>2. **flexShrink** defines the shrink factor of a flex item.|
| Scaling | The width and height of a child component change with the container component, with its aspect ratio fixed at the preset value.| The [aspectRatio](../reference/arkui-ts/ts-universal-attributes-layout-constraints.md) attribute specifies the aspect ratio of the current component. The formula is as follows: aspectRatio = width/height.|
| Proportion | The proportion capability indicates that the width and height of child components change with the parent container component based on the preset proportion. | Two implementation modes are available with the universal attributes:<br>1. Set the width and height of the child components to a percentage of the width and height of the parent component.<br>2. Use the [layoutWeight](../reference/arkui-ts/ts-universal-attributes-size.md) attribute to enable the child components to adaptively occupy the remaining space.|
| Hiding | The hiding capability refers to that the visibility of a child component in a container component is subject to its preset display priority and the size of the container component. Child components with the same display priority are displayed or hidden at the same time.| The [displayPriority](../reference/arkui-ts/ts-universal-attributes-layout-constraints.md) attribute is used to control the visibility.|
# Improving Layout Performance
When you are using the **\<Flex>** component, note that its layout performance may drop in certain scenarios. The **\<Flex>** component is a container whose child components are automatically laid out to create a flexible layout. By default, the child components are arranged along the main axis. The size of a child component along the main axis is called its main axis size.
In the single-row layout scenario, if the total main axis length of child components is not equal to the main axis length of the container, some child components are laid out twice to fill the container, that is, secondary layout is required. As a result, the layout efficiency decreases.
## Scenario 1
The **DisplayPriority** or **LayoutWeight** attribute of all child components is set to their default value or is not set. In this scenario, the child components are all laid out in sequence.
- If the total main axis length of the child components is equal to the main axis length of the container in the first layout, no secondary layout is required.
![layout-performance-1](figures/layout-performance-1.png)
- If the total main axis length of the child components is less than the main axis length of the container in the first layout, the child components with valid **flexGrow** values will trigger secondary layout and be stretched to fill the container.
![layout-performace-2](figures/layout-performace-2.gif)
- If the total main axis length of the child components is greater than the main axis length of the container in the first layout, the child components with valid **flexShrink** (whose default value **1** is valid) values will trigger secondary layout and shrink to fill the container.
![layout-performace-3](figures/layout-performace-3.gif)
## Scenario 2
The **DisplayPriority** attribute is set for the child component, but not the **LayoutWeight** attribute.
In this scenario, the child components with the same **DisplayPriority** value are laid out in groups, in descending order of the **DisplayPriority** values. When the total main axis length of the child components reaches the maximum and does not exceed the main axis length of the container, child components that have not been laid out and have the smallest **DisplayPriority** value will be discarded (a set of child components with critical **DisplayPriority** values may be laid out but still be discarded).
- If the total main axis length of the child components is equal to the main axis length of the container in the first layout, no secondary layout is required.
![layout-performance-4](figures/layout-performance-4.png)
- If the total main axis length of the child components is less than the main axis length of the container in the first layout, the child components with valid **flexGrow** values will trigger secondary layout and be stretched to fill the container.
![layout-performace-5](figures/layout-performace-5.gif)
## Scenario 3
The **LayoutWeight** attribute is set for the child component.
In this scenario, the child components with the same **DisplayPriority** value and without the **LayoutWeight** attribute set are laid out in groups, in descending order of the **DisplayPriority** values. When the total main axis length of the child components reaches the maximum and does not exceed the main axis length of the container, child components that have not been laid out and have the smallest **DisplayPriority** value will be discarded (a set of child components with critical **DisplayPriority** values may be laid out but still be discarded).
The remaining space in the container is filled by child components for which the **LayoutWeight** attribute is set.
- The child components are laid out only once in this process, and secondary layout is not triggered.
![layout-performace-6](figures/layout-performace-6.gif)
## How to Optimize Flex Layout Performance
- Use **\<Column>** and **\<Row>** instead of **\<Flex>**.
- Set the **flexShrink** attribute of child components whose size does not need to be changed to **0**.
- Prioritize the **LayoutWeight** attribute over the **flexGrow** and **flexShrink** attributes.
- Use the most common layout result so that the total main axis length of child components is equal to the main axis length of the **\<Flex>** container.
# Relative Layout
## Overview
The relative layout, implemented using the [\<RelativeContainer>](../reference/arkui-ts/ts-container-relativecontainer.md) container component, is used to lay out child elements in relative positions. A child element can set the container or another child element as the anchor, based on which its relative position is determined. Below shows a relative layout. The dotted lines in the figure indicate the position dependency.
**Figure 1** Relative layout
![relative-layout](figures/relative-layout.png)
A child element does not necessarily adopt the dependency shown above to determine its relative position. For example, Item4 may use Item2 or the **\<RelativeContainer>** parent container as a dependency anchor.
## Basic Concepts
- Anchor: element relative to which an element's position is specified.
- Alignment mode: how the current element is aligned with the anchor, which can be top-, center-, or bottom-aligned in the vertical direction or left-, center-, and right-aligned in the horizontal direction.
## Setting the Dependency
### Setting the Anchor
By setting the anchor, you set a position dependency relationship between a child element and its parent element or sibling elements. In the horizontal direction, you can set the left, middle, and right anchors. In the vertical direction, you can set the top, center, and bottom anchors. To specify anchors, you must set IDs for the **\<RelativeContainer>** component and its child elements. The default ID is **container**. The ID is set through the **id** attribute. Child elements whose IDs are not set are not displayed in the **\<RelativeContainer>** component.
>**NOTE**
>
>When using anchors, pay attention to the relative positions of child elements to avoid misplacement or blocking.
- The ID of the **\<RelativeContainer>** parent component is **container**.
```ts
RelativeContainer() {
Row()
// Add other attributes.
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})
.id("row1")
Row()
...
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
right: { anchor: '__container__', align: HorizontalAlign.End }
})
.id("row2")
}
...
```
![en-us_image_0000001562820901](figures/en-us_image_0000001562820901.png)
- A child element is used as the anchor.
```ts
RelativeContainer() {
...
top: { anchor: 'row1', align: VerticalAlign.Bottom },
...
}
.width(300).height(300)
.margin({ left: 20 })
.border({ width: 2, color: '#6699FF' })
```
![en-us_image_0000001562940613](figures/en-us_image_0000001562940613.png)
### Setting Alignment Relative to the Anchor
After an anchor is set, you can use **align** to set the alignment mode relative to the anchor.
Alignment modes in the horizontal direction can be left, center, or right, achieved by the **HorizontalAlign.Start**, **HorizontalAlign.Center**, and **HorizontalAlign.End** attributes, respectively.
![alignment-relative-anchor-horizontal](figures/alignment-relative-anchor-horizontal.png)
Alignment modes in the vertical direction can be top, center, or bottom, achieved by the **HorizontalAlign.Top**, **HorizontalAlign.Center**, and **HorizontalAlign.Bottom** attributes, respectively.
![alignment-relative-anchor-vertical](figures/alignment-relative-anchor-vertical.png)
## Example
Child elements in the relative layout are flexible. You can use **alignRules** to move child elements in the **\<RelativeContainer>** container.
```ts
@Entry
@Component
struct Index {
build() {
Row() {
RelativeContainer() {
Row()
.width(100)
.height(100)
.backgroundColor('#FF3333')
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top }, // Use the parent container as the anchor and align with its top vertically.
middle: { anchor: '__container__', align: HorizontalAlign.Center } // Use the parent container as the anchor and align with its center horizontally.
})
.id('row1') // Set the anchor to row1.
Row() {
Image($r('app.media.icon'))
}
.height(100).width(100)
.alignRules({
top: { anchor: 'row1', align: VerticalAlign.Bottom }, // Use row1 as the anchor and align with its bottom vertically.
left: { anchor: 'row1', align: HorizontalAlign.Start } // Use row1 as the anchor and align with its left horizontally.
})
.id('row2') // Set the anchor to row2.
Row()
.width(100)
.height(100)
.backgroundColor('#FFCC00')
.alignRules({
top: { anchor: 'row2', align: VerticalAlign.Top }
})
.id('row3') // Set the anchor to row3.
Row()
.width(100)
.height(100)
.backgroundColor('#FF9966')
.alignRules({
top: { anchor: 'row2', align: VerticalAlign.Top },
left: { anchor: 'row2', align: HorizontalAlign.End },
})
.id('row4') // Set the anchor to row4.
Row()
.width(100)
.height(100)
.backgroundColor('#FF66FF')
.alignRules({
top: { anchor: 'row2', align: VerticalAlign.Bottom },
middle: { anchor: 'row2', align: HorizontalAlign.Center }
})
.id('row5') // Set the anchor to row5.
}
.width(300).height(300)
.border({ width: 2, color: '#6699FF' })
}
.height('100%').margin({ left: 30 })
}
}
```
![en-us_image_0000001562700529](figures/en-us_image_0000001562700529.png)
# Stack Layout
## Overview
The stack layout reserves an area on the screen to display elements in a component and allows the elements to be stacked. You can implement a stack layout through the [\<Stack>](../reference/arkui-ts/ts-container-stack.md) component, which provides a stack container where positioned or non-positioned child components are pushed successively and the latter one sits on top of the previous one.
The stack layout excels at page stacking and positioning, and is widely used in ads and widget arrangement.
In the **\<Stack>** component shown in Figure 1, the sequence of child elements (child components) is Item1 -> Item2 -> Item3.
**Figure 1** Stack layout
![stack-layout](figures/stack-layout.png)
## How to Develop
The **\<Stack>** component can contain a wide variety of child components, which are stacked in the center by default based on their sizes. While respecting the constraints of **\<Stack>**, child components are laid out in their respective style.
```ts
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({ top: 50 })
}
```
![stack-layout-sample](figures/stack-layout-sample.png)
## Alignment
Alignment of child components in the **\<Stack>** component is set through the [alignContent](../reference/arkui-ts/ts-appendix-enums.md#alignment) parameter. As shown in Figure 2, nine alignment modes are supported.
**Figure 2** Alignment modes in the \<Stack> component
![en-us_image_0000001562940621](figures/en-us_image_0000001562940621.png)
## Z-order Control
The stacking order of child components in the **\<Stack>** component is set through the **[zIndex](../reference/arkui-ts/ts-universal-attributes-z-order.md)** attribute. A larger **zIndex** value indicates a higher display level.
In the stack layout, if the size of a component is greater than that of the one before it, the one before it is hidden.
```ts
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text ('Stacked component 1').textAlign (TextAlign.End).fontSize (20)
}.width(100).height(100).backgroundColor(0xffd306)
Column() {
Text ('Stacked component 2').fontSize (20)
}.width(150).height(150).backgroundColor(Color.Pink)
Column() {
Text ('Stacked component 3').fontSize (20)
}.width(200).height(200).backgroundColor(Color.Grey)
}.margin({ top: 100 }).width(350).height(350).backgroundColor(0xe0e0e0)
```
![en-us_image_0000001511900544](figures/en-us_image_0000001511900544.png)
In the following figure, the size of the stacked component 3 is greater than that of all the components before it. Therefore, the first two components are completely hidden. To show these components, modify their **zIndex** attribute settings.
```ts
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text ('Stacked component 1').fontSize (20)
}.width(100).height(100).backgroundColor(0xffd306).zIndex(2)
Column() {
Text ('Stacked component 2').fontSize (20)
}.width(150).height(150).backgroundColor(Color.Pink).zIndex(1)
Column() {
Text ('Stacked component 3').fontSize (20)
}.width(200).height(200).backgroundColor(Color.Grey)
}.margin({ top: 100 }).width(350).height(350).backgroundColor(0xe0e0e0)
```
![en-us_image_0000001563060797](figures/en-us_image_0000001563060797.png)
## Example Scenario
In this example, the stack layout is used to quickly set up a page display model.
```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) => {
Text(item)
.width(100)
.height(100)
.fontSize(16)
.margin(10)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}, item => item)
}.width('100%').height('100%')
Flex({ justifyContent: FlexAlign.SpaceAround, alignItems: ItemAlign.Center }) {
Text ('Contacts').fontSize (16)
Text ('Settings').fontSize (16)
Text ('Messaging').fontSize (16)
}
.width('50%')
.height(50)
.backgroundColor('#16302e2e')
.margin({ bottom: 15 })
.borderRadius(15)
}.width('100%').height('100%').backgroundColor('#CFD0CF')
}
}
```
![en-us_image_0000001511421368](figures/en-us_image_0000001511421368.png)
# Navigation
Generally, the [\<Navigation>](../reference/arkui-ts/ts-basic-components-navigation.md) component functions as the root container of a page and supports three display modes: single-page, column, and adaptive. In addition, **\<Navigation>** provides attributes to set the title bar, toolbar, and navigation bar of a page.
The pages of the Navigation component include the home page and content page. The home page consists of the title bar, content area, and toolbar. You can use the [\<NavRouter>](../reference/arkui-ts/ts-basic-components-navrouter.md) child component in the content area to implement the navigation bar function. The content page displays the content of the [\<NavDestination>](../reference/arkui-ts/ts-basic-components-navdestination.md) child component.
**\<NavRouter>** is a special child component used together with **\<Navigation>**. It provides default processing logic for responding to clicks, eliminating the need for manual logic definition. **\<NavRouter>** has only two root nodes. The second root node is **\<NavDestination>**. **\<NavDestination>** is a special child component used together with **\<NavRouter>** to display the content page of the **\<Navigation>** component. When the user clicks the **\<NavRouter>** component, the corresponding **\<NavDestination>** content area is displayed.
## Setting the Page Display Mode
The **\<Navigation>** component uses the **mode** attribute to set the page display mode.
- Adaptive Mode
By default, the **\<Navigation>** component is in adaptive mode. In this case, the **mode** attribute is **NavigationMode.Auto**. In adaptive mode, when the device width is greater than 520 vp, the **\<Navigation>** component uses the column mode. Otherwise, the **\<Navigation>** component uses the single-page mode.
```
Navigation() {
...
}
.mode(NavigationMode.Auto)
```
- Single-page mode
**Figure 1** Single-page mode
![en-us_image_0000001511740532](figures/en-us_image_0000001511740532.png)
Set **mode** to **NavigationMode.Stack** so that the **\<Navigation>** component is displayed on a single page.
```ts
Navigation() {
...
}
.mode(NavigationMode.Stack)
```
![single-page-1](figures/single-page-1.jpg)
- Column mode
**Figure 2** Column mode
![en-us_image_0000001562820845](figures/en-us_image_0000001562820845.png)
Set **mode** to **NavigationMode.Split** so that the **\<Navigation>** component is displayed in columns.
```ts
@Entry
@Component
struct NavigationExample {
private arr: number[] = [1, 2, 3];
build() {
Column() {
Navigation() {
TextInput({ placeholder: 'search...' })
.width("90%")
.height(40)
.backgroundColor('#FFFFFF')
List({ space: 12 }) {
ForEach(this.arr, (item) => {
ListItem() {
NavRouter() {
Text("NavRouter" + item)
.width("100%")
.height(72)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.fontSize(16)
.fontWeight(500)
.textAlign(TextAlign.Center)
NavDestination() {
Text("NavDestinationContent" + item)
}
.title("NavDestinationTitle" + item)
}
}
}, item => item)
}
.width("90%")
.margin({ top: 12 })
}
.title ("Main Title")
.mode(NavigationMode.Split)
.menus([
{value: "", icon: "./image/ic_public_search.svg", action: ()=> {}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=> {}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=> {}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=> {}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=> {}}
])
.toolBar({items: [
{value: "func", icon: "./image/ic_public_highlights.svg", action: ()=> {}},
{value: "func", icon: "./image/ic_public_highlights.svg", action: ()=> {}},
{value: "func", icon: "./image/ic_public_highlights.svg", action: ()=> {}}
]})
}
.height('100%')
.width('100%')
.backgroundColor('#F1F3F5')
}
}
```
![column](figures/column.jpg)
## Setting the Title Bar Mode
The title bar is on the top of the page and is used to display the page name and operation entry. The **\<Navigation>** component uses the **titleMode** attribute to set the title bar mode.
- Mini mode
Applicable when the title of a level-1 page does not need to be highlighted.
**Figure 3** Title bar in Mini mode
![mini](figures/mini.jpg)
```ts
Navigation() {
...
}
.titleMode(NavigationTitleMode.Mini)
```
- Full mode
Applicable when the title of a level-1 page needs to be highlighted.
**Figure 4** Title bar in Full mode
![free1](figures/free1.jpg)
```ts
Navigation() {
...
}
.titleMode(NavigationTitleMode.Full)
```
## Setting the Menu Bar
The menu bar is in the upper right corner of the **\<Navigation>** component. You can set the menu bar through the **menus** attribute, which supports two parameter types: Array&lt;[NavigationMenuItem](../reference/arkui-ts/ts-basic-components-navigation.md#navigationmenuitem)&gt and CustomBuilder. When the Array\<NavigationMenuItem> type is used, a maximum of three icons can be displayed in portrait mode and a maximum of five icons can be displayed in landscape mode. Extra icons will be placed in the automatically generated More icons.
**Figure 5** Menu bar with three icons
![menu-bar-2](figures/menu-bar-2.jpg)
```ts
Navigation() {
...
}
.menus([{value: "", icon: "./image/ic_public_search.svg", action: ()=>{}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=>{}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=>{}}])
```
**Figure 6** Menu bar with four icons
![menu-bar](figures/menu-bar.jpg)
```ts
Navigation() {
...
}
.menus([{value: "", icon: "./image/ic_public_search.svg", action: ()=>{}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=>{}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=>{}},
{value: "", icon: "./image/ic_public_add.svg", action: ()=>{}}])
```
## Setting the Toolbar
The toolbar is located at the bottom of the **\<Navigation>** component. You can set the toolbar through the **toolBar** attribute.
**Figure 7** Toolbar
![free3](figures/free3.jpg)
```ts
Navigation() {
...
}
.toolBar({items:[
{value: "func", icon: "./image/ic_public_highlights.svg", action: ()=>{}},
{value: "func", icon: "./image/ic_public_highlights.svg", action: ()=>{}},
{value: "func", icon: "./image/ic_public_highlights.svg", action: ()=>{}}]})
```
# Tabs
When there is a large amount of page information, to enable the user to focus on the currently displayed content, the page content needs to be classified to improve the page space utilization. The [\<Tabs>](../reference/arkui-ts/ts-container-tabs.md) component can quickly switch between views on a page, improving information search efficiency and reducing the amount of information that users can obtain at a time.
## Basic Layout
The **\<Tabs>** component consists of two parts: **\<TabContent>** and **\<TabBar>**. **\<TabContent>** is the content page, and **\<TabBar>** is the navigation tab bar. The following figure shows the page structure. The layout varies according to the navigation type. In bottom navigation, top navigation, and side navigation, the navigation tab bar is located at the bottom, top, and edge, respectively.
**Figure 1** \<Tabs> component layout
![tabs-layout](figures/tabs-layout.png)
>**NOTE**
>
> - The **\<TabContent>** component does not support setting of the common width attribute. By default, its width is the same as that of the parent **\<Tabs>** component.
>
> - The **\<TabContent>** component does not support setting of the common height attribute. Its height is determined by the height of the parent **\<Tabs>** component and the **\<TabBar>** component.
**\<Tabs>** use braces to enclose the tab content, as shown in Figure 2.
**Figure 2** Using \<Tabs> and \<TabContent>
![tabs-tabscontent](figures/tabs-tabscontent.png)
Each **\<TabContent>** component should be mapped to a tab page, which can be configured through the **tabBar** attribute. The following is an example.
```ts
TabContent() {
Text('Content of the Home tab').fontSize(30)
}
.tabBar ('Home')
```
When setting multiple **\<TabContent>** components, place them in sequence in the **\<Tabs>** component.
```ts
Tabs() {
TabContent() {
Text('Content of the Home tab').fontSize(30)
}
.tabBar ('Home')
TabContent() {
Text('Content of the Recommended tab').fontSize(30)
}
.tabBar ('Recommended')
TabContent() {
Text ('Content of the Discover tab').fontSize (30)
}
.tabBar ('Discover')
TabContent() {
Text ('Content of the Me tab').fontSize (30)
}
.tabBar ("Me")
}
```
## Bottom Navigation
Bottom navigation is the most common navigation mode in applications. The bottom navigation bar is located at the bottom of the level-1 page of the application. It enables the user to quickly have a picture of the feature categories the moment they open the application. In addition, it facilitates one-hand operations of the user. Bottom navigation generally exists as a main navigation form of an application, in that it provides convenient access to primary destinations anywhere in the application.
**Figure 3** Bottom navigation bar
![bottom-navigation](figures/bottom-navigation.gif)
You set the position of the navigation bar through the **barPosition** parameter of the **\<Tabs>** component. The default value of this parameter is **Start**, which means that the navigation bar is located on the top. To set the navigation bar to the bottom, set **barPosition** to **End**.
```ts
Tabs({ barPosition: BarPosition.End }) {
// TabContent: Home, Discover, Recommended, and Me
...
}
```
## Top Navigation
Top navigation comes in handy when there are many content categories and users need to frequently switch between them. It is usually a further subdivision of the categories in the bottom navigation bar. For example, a theme application may provide a top navigation bar that classifies themes into image, video, and font.
**Figure 4** Top navigation bar
![top-navigation](figures/top-navigation.gif)
The default barPosition parameter of the Tabs component is Start, indicating the top navigation mode.
```ts
Tabs({ barPosition: BarPosition.Start }) {
// TabContent: Following, Video, Game, Digital, Technology, Sports, Movie
...
}
```
## Side Navigation
Side navigation is seldom used in applications. It is more applicable to landscape screens and is used to perform navigation operations on applications. Because the natural eye movement pattern is from left to right, the side navigation bar is located on the left side by default.
**Figure 5** Side navigation bar
![side-navigation](figures/side-navigation.png)
To implement the side navigation bar, set the **vertical** attribute of the **\<Tabs>** component to **true**. In the bottom navigation and top navigation implementations, the default value **false** of the **vertical** attribute is used, indicating that the content page and navigation bar are aligned vertically.
```ts
Tabs({ barPosition: BarPosition.Start }) {
// TabContent: Home, Discover, Recommended, and Me
...
}
.vertical(true)
.barWidth(100)
.barHeight(200)
```
>**NOTE**
>
> - When the **vertical** attribute is set to **true**, the tab bar takes up the whole screen width by default. You need to set **barWidth** to a proper value.
>
> - When the **vertical** attribute is set to **true**, the tab bar takes up the actual content height by default. You need to set **barWidth** to a proper value.
## Restricting the Scrolling of the Navigation Bar
By default, the navigation bar is scrollable. On some pages that require multi-level classification of content, for example, when both bottom navigation and top navigation are used, the scroll effect of the bottom navigation bar may conflict with that of the top navigation bar. In this case, the scrolling of the bottom navigation bar needs to be restricted to improve user experience.
**Figure 6** Restricting the scrolling of the bottom navigation bar
![restricted-navigation](figures/restricted-navigation.gif)
The attribute that enables or disables the scrolling is **scrollable**. Its default value is **true**, indicating that scrolling is enabled. To disable the scrolling, set the attribute to **false**.
```ts
Tabs({ barPosition: BarPosition.End }) {
TabContent(){
Column(){
Tabs(){
// Content on the top navigation bar
...
}
}
.backgroundColor('#ff08a8f1')
.width('100%')
}
.tabBar ('Home')
// Other TabContent content: Discover, Recommended, and Me
...
}
.scrollable(false)
```
## Fixed Navigation Bar
When the content categories are relatively fixed and not scalable, a fixed navigation bar can be used. For example, it can be used for the bottom navigation bar, which generally contains 3 to 5 categories. The fixed navigation bar cannot be scrolled or dragged. The tab bar width is evenly distributed among the categories.
**Figure 7** Fixed navigation bar
![fixed-navigation](figures/fixed-navigation.gif)
To use a fixed navigation bar, set the **barMode** attribute of the **\<Tabs>** component to **Fixed** (default).
```ts
Tabs({ barPosition: BarPosition.End }) {
// TabContent: Home, Discover, Recommended, and Me
...
}
.barMode(BarMode.Fixed)
```
## Scrollable Navigation Bar
The top navigation bar or side navigation bar can be set to be scrollable if the screen width cannot fully accommodate all the tabs. With a scrollable navigation bar, users can reveal tabs beyond the visible area by touching or swiping on the navigation bar.
**Figure 8** Scrollable navigation bar
![scrollable-navigation](figures/scrollable-navigation.gif)
To use a scrollable navigation bar, set the **barMode** attribute of the **\<Tabs>** component to **Scrollable**.
```ts
Tabs({ barPosition: BarPosition.Start }) {
// TabContent: follow, video, game, digital, technology, sports, movie, humanities, art, nature, and military
...
}
.barMode(BarMode.Scrollable)
```
## Customizing the Navigation Bar
The bottom navigation bar is generally used on the home page of an application. To deliver a more vibrant experience, you can customize the style of the navigation bar, combining use of text and icons to signify the tab content.
**Figure 9** Custom navigation bar
![custom-navigation-bar](figures/custom-navigation-bar.png)
By default, the system uses an underscore (_) to indicate the active tab. For a custom navigation bar, you need to implement the corresponding style to distinguish active tabs from inactive tabs.
To customize the navigation bar, use the **tabBar** parameter and pass in to it custom function component styles in **CustomBuilder** mode. In this example, a custom function component **TabBuilder** is declared, and the input parameters include **title** (tab title), **targetIndex** (target index of the tab), **selectedImg** (image for the selected state), and **normalImg** (image for the unselected state). The UI display style is determined based on whether the value of **currentIndex** (index of the active tab) matches that of **targetIndex** (target index of the tab).
```ts
@Builder TabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
Column() {
Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
.size({ width: 25, height: 25 })
Text(title)
.fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
}
```
Pass the custom function component to the **tabBar** attribute corresponding to the tab content and transfer the corresponding parameters.
```ts
TabContent() {
Column(){
Text('Content of the Me tab')
}
.width('100%')
.height('100%')
.backgroundColor('#007DFF')
}
.tabBar(this.TabBuilder('Me', 0, $r('app.media.mine_selected'), $r('app.media.mine_normal')))
```
## Switching to a Specified Tab
Non-custom navigation bars follow the default system switching logic. If you are using a custom navigation bar, you must manually implement the logic for switching tabs so that when the user touches a tab, the application displays the corresponding tab page.
**Figure 10** Switching to a specified tab in a custom navigation bar
![switching-to-a-specified-tab](figures/switching-to-a-specified-tab.gif)
To switch to a specified tab page, use **TabsController**, which is the controller of the **\<Tabs>** component. By using the **changeIndex** API of **TabsController**, you can set your application to display the tab content corresponding to the specified index.
```ts
private tabsController : TabsController = new TabsController()
@State currentIndex:number = 0;
@Builder TabBuilder(title: string, targetIndex: number) {
Column() {
Text(title)
.fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
}
...
.onClick(() => {
this.currentIndex = targetIndex;
this.tabsController.changeIndex(this.currentIndex);
})
}
```
When using a custom navigation bar, pass the corresponding \@Builder in the **tabBar** attribute and transfer the corresponding parameters.
```ts
Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
TabContent(){
...
}.tabBar (this.TabBuilder ('Home', 0))
TabContent(){
...
}.tabBar (this.TabBuilder ('Discover', 1))
TabContent(){
...
}.tabBar (this.TabBuilder ('Recommended', 2))
TabContent(){
...
}
.tabBar (this.TabBuilder ('Me',3))
}
```
## Swiping to Switch Between Tabs
For non-custom navigation bars, tabs and tab content are linked by default. For custom navigation bars, however, tabs and tab content are linked when tab switching is initiated by **TabsController**, but not when tab switching is initiated by a swipe gesture. This means that, when the user swipes on the screen to switch between tab content, the tabs do not switch automatically. In this case, manual switching is required.
**Figure 11** Lack of linkage between tabs and tab content
![lack-of-linkage](figures /lack-of-linkage.gif)
To manually switch between the tabs, use the **onChange** API provided by the **\<Tabs>** component to listen for the index change and pass the index of the active tab to **currentIndex**.
```ts
Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
TabContent() {
...
}.tabBar (this.TabBuilder ('Home', 0))
TabContent() {
...
}.tabBar (this.TabBuilder ('Found', 1))
TabContent() {
...
}.tabBar (this.TabBuilder ('Recommended', 2))
TabContent() {
...
}
.tabBar (this.TabBuilder ('Me', 3))
}.onChange((index) => {
this.currentIndex = index
})
```
**Figure 12** Linkage between tabs and tab content
![final-effect](figures/final-effect.gif)
# Page Transition Animation
During page redirection, one page disappears and the other page appears. You can customize the [page transition effects](../reference/arkui-ts/ts-page-transition-animation.md) for these pages through the **pageTransition** API. Specifically, **PageTransitionEnter** defines the page entrance animation, while **PageTransitionExit** defines the page exit animation.
API of **PageTransitionEnter**:
```ts
PageTransitionEnter({type?: RouteType,duration?: number,curve?: Curve | string,delay?: number})
```
API of **PageTransitionExit**:
```ts
PageTransitionExit({type?: RouteType,duration?: number,curve?: Curve | string,delay?: number})
```
Both **PageTransitionEnter** and **PageTransitionExit** contain the **slide**, **translate**, **scale**, and **opacity** attributes. For **PageTransitionEnter**, these attributes indicate the start values for page entrance. For **PageTransitionExit**, these attributes indicate the end values for page exit. In this sense, configuration of page transition is similar to that of component transition. **PageTransitionEnter** provides the **onEnter** callback, and **PageTransitionExit** provides the **onExit** callback.
In the preceding APIs, the **type** parameter indicates the route type used in page navigation. Each page transition involves exit of one page and entrance of the other. If you switch from page A to page B through the **router.pushUrl** operation, page A exits, with the exit animation applied; and page B enters, with the entrance animation applied. If you switch from page B back to page A through the **router.back** operation, page B exits, , with the exit animation applied; and page A enters, with the entrance animation applied. That is, **PageTransitionEnter** of a page may be an entrance animation of a new page (pushed to the stack) or of an existing page (popped from the stack). To distinguish these two types of entrance animations, the **type** parameter is provided.
## Setting type to RouteType.None
When **type** is set to **RouteType.None** (default value), the page transition animations work for both the push and pop operations in the page stack.
```ts
// pageA
pageTransition() {
// Configure the page entrance animation to sliding in from the left, with the duration of 1200 ms. The settings take effect no matter whether the push or pop operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.None, duration: 1200 })
.slide(SlideEffect.Left)
// Configure the page exit animation to sliding out from the left, with the duration of 1000 ms. The settings take effect no matter whether the push or pop operation is performed on the page stack.
PageTransitionExit({ type: RouteType.None, duration: 1000 })
.slide(SlideEffect.Left)
}
```
```ts
// pageB
pageTransition() {
// Configure the page entrance animation to sliding in from the right, with the duration of 1000 ms. The settings take effect no matter whether the push or pop operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.None, duration: 1000 })
.slide(SlideEffect.Right)
// Configure the page exit animation to sliding out from the right, with the duration of 1200 ms. The settings take effect no matter whether the push or pop operation is performed on the page stack.
PageTransitionExit({ type: RouteType.None, duration: 1200 })
.slide(SlideEffect.Right)
}
```
Assume that the page stack is in the multi-instance mode, that is, duplicate pages are allowed in the page stack. There may be four scenarios. The following table lists the page transition effects.
| Route Operation | Page A Transition Effect | Page B Transition Effect |
| ---------------------------- | ---------------------------------- | ---------------------------------- |
| **router.pushUrl** – redirection from page A to new page B.| The page exits. The animation defined by **PageTransitionExit** is applied. In the example, the page slides out from the left of the screen. | The page enters. The animation defined by **PageTransitionEnter** is applied. In the example, the page slides in from the right of the screen.|
| **router.back** – redirection from page B back to page A. | The page enters. The animation defined by **PageTransitionEnter** is applied. In the example, the page slides in from the left of the screen.| The page exits. The animation defined by **PageTransitionExit** is applied. In the example, the page slides out from the right of the screen. |
| **router.pushUrl** – redirection from page B to new page A.| The page enters. The animation defined by **PageTransitionEnter** is applied. In the example, the page slides in from the left of the screen.| The page exits. The animation defined by **PageTransitionExit** is applied. In the example, the page slides out from the right of the screen. |
| **router.back** – redirection from page A back to page B. | The page exits. The animation defined by **PageTransitionExit** is applied. In the example, the page slides out from the left of the screen. | The page enters. The animation defined by **PageTransitionEnter** is applied. In the example, the page slides in from the right of the screen.|
If you want the page accessed by **router.pushUrl** to always slide in from the right and the page exited by **router.back** to always slide out from the right, the third and fourth cases in the preceding table do not meet the requirements. In this case, you need to define four page transition effects.
## Setting type to RouteType.Push or RouteType.Pop
When **type** is set to **RouteType.Push**, the page transition animations work for only both the push operations in the page stack. When **type** is set to **RouteType.Pop**, the page transition animations work for only both the pop operations in the page stack.
```ts
// pageA
pageTransition() {
// Configure the page entrance animation to sliding in from the right, with the duration of 1200 ms. The settings take effect only when the push operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Push, duration: 1200 })
.slide(SlideEffect.Right)
// Configure the page entrance animation to sliding in from the left, with the duration of 1200 ms. The settings take effect only when the pop operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Pop, duration: 1200 })
.slide(SlideEffect.Left)
// Configure the page exit animation to sliding out from the left, with the duration of 1000 ms. The settings take effect only when the push operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Left)
// Configure the page exit animation to sliding out from the right, with the duration of 1000 ms. The settings take effect only when the pop operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Right)
}
```
```ts
// pageB
pageTransition() {
// Configure the page entrance animation to sliding in from the right, with the duration of 1000 ms. The settings take effect only when the push operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Right)
// Configure the page entrance animation to sliding in from the left, with the duration of 1000 ms. The settings take effect only when the pop operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Left)
// Configure the page exit animation to sliding out from the left, with the duration of 1200 ms. The settings take effect only when the push operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Push, duration: 1200 })
.slide(SlideEffect.Left)
// Configure the page exit animation to sliding out from the right, with the duration of 1200 ms. The settings take effect only when the pop operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Pop, duration: 1200 })
.slide(SlideEffect.Right)
}
```
The preceding code defines page transition effects for all possibles scenarios. Assume that the page stack is in the multi-instance mode, that is, duplicate pages are allowed in the page stack. There may be four scenarios. The following table lists the page transition effects.
| Route Operation | Page A Transition Effect | Page B Transition Effect |
| ---------------------------- | ---------------------------------------- | ---------------------------------------- |
| **router.pushUrl** – redirection from page A to new page B.| The page exits. The transition style of **PageTransitionExit** whose **type** is **RouteType.Push** takes effect. The page slides out from the left of the screen.| The page enters. The transition style of **PageTransitionEnter** whose **type** is **RouteType.Push** takes effect. The page slides in from the right of the screen.|
| **router.back** – redirection from page B back to page A. | The page enters. The transition style of **PageTransitionEnter** whose **type** is **RouteType.Pop** takes effect. The page slides in from the left of the screen.| The page exits. The transition style of **PageTransitionExit** whose **type** is **RouteType.Pop** takes effect. The page slides out from the right of the screen.|
| **router.pushUrl** – redirection from page B to new page A.| The page enters. The transition style of **PageTransitionEnter** whose **type** is **RouteType.Push** takes effect. The page slides in from the right of the screen.| The page exits. The transition style of **PageTransitionExit** whose **type** is **RouteType.Push** takes effect. The page slides out from the left of the screen.|
| **router.back** – redirection from page A back to page B. | The page exits. The transition style of **PageTransitionExit** whose **type** is **RouteType.Pop** takes effect. The page slides out from the right of the screen.| The page enters. The transition style of **PageTransitionEnter** whose **type** is **RouteType.Pop** takes effect. The page slides in from the left of the screen.|
>**NOTE**
>
> 1. The transition style of each page can be independently configured. However, as each transition involves two pages, take into account the smoothness between page transitions, for example, the transition duration.
>
> 2. If no page transition style is defined, a page uses the default page transition style.
## Disabling Page Transition
```ts
pageTransition() {
PageTransitionEnter({ type: RouteType.None, duration: 0 })
PageTransitionExit({ type: RouteType.None, duration: 0 })
}
```
You can disable the transition animation of a page by setting the page transition duration to 0.
## Example
In the following example, page transition animations are defined for all four page transition scenarios.
```ts
// PageTransitionSrc1
import router from '@ohos.router';
@Entry
@Component
struct PageTransitionSrc1 {
build() {
Column() {
Image($r('app.media.mountain'))
.width('90%')
.height('80%')
.objectFit(ImageFit.Fill)
.syncLoad(true) // Load the image synchronously so that the image has been loaded when the page is displayed.
.margin(30)
Row({ space: 10 }) {
Button("pushUrl")
.onClick(() => {
// Navigate to the next page, which is a push operation.
router.pushUrl({ url: 'pages/myTest/PageTransitionDst1' });
})
Button("back")
.onClick(() => {
// Return to the previous page, which is equivalent to the pop operation.
router.back();
})
}.justifyContent(FlexAlign.Center)
}
.width("100%").height("100%")
.alignItems(HorizontalAlign.Center)
}
pageTransition() {
// Configure the page entrance animation to sliding in from the right, with the duration of 1000 ms. The settings take effect only when the push operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Right)
// Configure the page entrance animation to sliding in from the left, with the duration of 1000 ms. The settings take effect only when the pop operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Left)
// Configure the page exit animation to sliding out from the left, with the duration of 1000 ms. The settings take effect only when the push operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Left)
// Configure the page exit animation to sliding out from the right, with the duration of 1000 ms. The settings take effect only when the pop operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Right)
}
}
```
```ts
// PageTransitionDst1
import router from '@ohos.router';
@Entry
@Component
struct PageTransitionDst1 {
build() {
Column() {
Image($r('app.media.forest'))
.width('90%')
.height('80%')
.objectFit(ImageFit.Fill)
.syncLoad(true) // Load the image synchronously so that the image has been loaded when the page is displayed.
.margin(30)
Row({ space: 10 }) {
Button("pushUrl")
.onClick(() => {
// Navigate to the next page, which is a push operation.
router.pushUrl({ url: 'pages/myTest/PageTransitionSrc1' });
})
Button("back")
.onClick(() => {
// Return to the previous page, which is equivalent to the pop operation.
router.back();
})
}.justifyContent(FlexAlign.Center)
}
.width("100%").height("100%")
.alignItems(HorizontalAlign.Center)
}
pageTransition() {
// Configure the page entrance animation to sliding in from the right, with the duration of 1000 ms. The settings take effect only when the push operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Right)
// Configure the page entrance animation to sliding in from the left, with the duration of 1000 ms. The settings take effect only when the pop operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Left)
// Configure the page exit animation to sliding out from the left, with the duration of 1000 ms. The settings take effect only when the push operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Left)
// Configure the page exit animation to sliding out from the right, with the duration of 1000 ms. The settings take effect only when the pop operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Right)
}
}
```
![pageTransition_PushPop](figures/pageTransition_PushPop.gif)
In the following example, **type** is set to **RouteType.None**.
```ts
// PageTransitionSrc2
import router from '@ohos.router';
@Entry
@Component
struct PageTransitionSrc2 {
build() {
Column() {
Image($r('app.media.mountain'))
.width('90%')
.height('80%')
.objectFit(ImageFit.Fill)
.syncLoad(true) // Load the image synchronously so that the image has been loaded when the page is displayed.
.margin(30)
Row({ space: 10 }) {
Button("pushUrl")
.onClick(() => {
// Navigate to the next page, which is a push operation.
router.pushUrl({ url: 'pages/myTest/pageTransitionDst2' });
})
Button("back")
.onClick(() => {
// Return to the previous page, which is equivalent to the pop operation.
router.back();
})
}.justifyContent(FlexAlign.Center)
}
.width("100%").height("100%")
.alignItems(HorizontalAlign.Center)
}
pageTransition() {
// Configure the page entrance animation to sliding in from the left, with the duration of 1000 ms. The settings take effect no matter whether the push or pop operation is performed on the page stack.
PageTransitionEnter({ duration: 1000 })
.slide(SlideEffect.Left)
// Configure the page exit animation to translating by 100 vp along the x- and y-axes and changing the opacity to 0, with the duration of 1200 ms. The settings take effect no matter whether the push or pop operation is performed on the page stack.
PageTransitionExit({ duration: 1200 })
.translate({ x: 100.0, y: 100.0 })
.opacity(0)
}
}
```
```ts
// PageTransitionDst2
import router from '@ohos.router';
@Entry
@Component
struct PageTransitionDst2 {
build() {
Column() {
Image($r('app.media.forest'))
.width('90%')
.height('80%')
.objectFit(ImageFit.Fill)
.syncLoad(true) // Load the image synchronously so that the image has been loaded when the page is displayed.
.margin(30)
Row({ space: 10 }) {
Button("pushUrl")
.onClick(() => {
// Navigate to the next page, which is a push operation.
router.pushUrl({ url: 'pages/myTest/pageTransitionSrc2' });
})
Button("back")
.onClick(() => {
// Return to the previous page, which is equivalent to the pop operation.
router.back();
})
}.justifyContent(FlexAlign.Center)
}
.width("100%").height("100%")
.alignItems(HorizontalAlign.Center)
}
pageTransition() {
// Configure the page entrance animation to sliding in from the left, with the duration of 1200 ms. The settings take effect no matter whether the push or pop operation is performed on the page stack.
PageTransitionEnter({ duration: 1200 })
.slide(SlideEffect.Left)
// Configure the page exit animation to translating by 100 vp along the x- and y-axes and changing the opacity to 0, with the duration of 1000 ms. The settings take effect no matter whether the push or pop operation is performed on the page stack.
PageTransitionExit({ duration: 1000 })
.translate({ x: 100.0, y: 100.0 })
.opacity(0)
}
}
```
![pageTransition_None](figures/pageTransition_None.gif)
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Poor-performing code may work, but will take away from your application performance. This topic presents a line-up of recommendations that you can take to improve your implementation, thereby avoiding possible performance drop. Poor-performing code may work, but will take away from your application performance. This topic presents a line-up of recommendations that you can take to improve your implementation, thereby avoiding possible performance drop.
## Lazy Loading ## Using Lazy Loading
When developing a long list, use of loop rendering, as in the code snippet below, can greatly slow down page loading and increase server load. When developing a long list, use of loop rendering, as in the code snippet below, can greatly slow down page loading and increase server load.
...@@ -131,6 +131,226 @@ struct MyComponent { ...@@ -131,6 +131,226 @@ struct MyComponent {
The preceding code initializes only three list elements during page loading and loads a new list item each time a list element is clicked. The preceding code initializes only three list elements during page loading and loads a new list item each time a list element is clicked.
## Setting Width and Height for \<List> Components
When a **\<List>** component is nested within a **\<Scroll>** component, all of its content will be loaded if its width and height is not specified, which may result in performance drop.
> **NOTE**
>
> When a **\<List>** component is nested within a **\<Scroll>** component:
>
> - If the width and height of the **\<List>** component are not set, all its child components are laid out.
>
> - If the width and height of the **\<List>** component are set, only child components within its display area are laid out.
>
> - When [ForEach](../quick-start/arkts-rendering-control-foreach.md) is used to load child components in the **\<List>** component, all child components are laid out, regardless of whether the width and height are set.
>
> - When [LazyForEach](../quick-start/arkts-rendering-control-lazyforeach.md) is used to load child components in the **\<List>** component, all child components are laid out if the component does not have its width and height specified; and only child components within its display area are laid out if the component has its width and height specified.
```ts
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = []
public totalCount(): number {
return 0
}
public getData(index: number): any {
return undefined
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener')
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener')
this.listeners.splice(pos, 1)
}
}
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded()
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index)
})
}
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index)
})
}
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to)
})
}
}
class MyDataSource extends BasicDataSource {
private dataArray: Array<string> = new Array(100).fill('test')
public totalCount(): number {
return this.dataArray.length
}
public getData(index: number): any {
return this.dataArray[index]
}
public addData(index: number, data: string): void {
this.dataArray.splice(index, 0, data)
this.notifyDataAdd(index)
}
public pushData(data: string): void {
this.dataArray.push(data)
this.notifyDataAdd(this.dataArray.length - 1)
}
}
@Entry
@Component
struct MyComponent {
private data: MyDataSource = new MyDataSource()
build() {
Scroll() {
List() {
LazyForEach(this.data, (item: string, index: number) => {
ListItem() {
Row() {
Text('item value: ' + item + (index + 1)).fontSize(20).margin(10)
}
}
})
}
}
}
}
```
In the above scenario, you are advised to set the width and height for the **\<List>** component.
```ts
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = []
public totalCount(): number {
return 0
}
public getData(index: number): any {
return undefined
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener')
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener')
this.listeners.splice(pos, 1)
}
}
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded()
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index)
})
}
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index)
})
}
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to)
})
}
}
class MyDataSource extends BasicDataSource {
private dataArray: Array<string> = new Array(100).fill('test')
public totalCount(): number {
return this.dataArray.length
}
public getData(index: number): any {
return this.dataArray[index]
}
public addData(index: number, data: string): void {
this.dataArray.splice(index, 0, data)
this.notifyDataAdd(index)
}
public pushData(data: string): void {
this.dataArray.push(data)
this.notifyDataAdd(this.dataArray.length - 1)
}
}
@Entry
@Component
struct MyComponent {
private data: MyDataSource = new MyDataSource()
build() {
Scroll() {
List() {
LazyForEach(this.data, (item: string, index: number) => {
ListItem() {
Text('item value: ' + item + (index + 1)).fontSize(20).margin(10)
}.width('100%')
})
}.width('100%').height(500)
}.backgroundColor(Color.Pink)
}
}
```
![list1](figures/list1.gif)
## Prioritizing Conditional Rendering over Visibility Control ## Prioritizing Conditional Rendering over Visibility Control
Use of the visibility attribute to hide or show a component, as in the code snippet below, results in re-creation of the component, leading to performance drop. Use of the visibility attribute to hide or show a component, as in the code snippet below, results in re-creation of the component, leading to performance drop.
......
# Menu
You can use menu APIs to display a context menu – a vertical list of items displayed by long pressing, clicking, or right-clicking a component. For details, see [Menu Control](../reference/arkui-ts/ts-universal-attributes-menu.md).
## Creating a Menu in the Default Style
Use the **bindMenu** API to implement a menu. **bindMenu** responds to the click event of the bound component. When the bound component is clicked, the menu is displayed.
```ts
Button('click for Menu')
.bindMenu([
{
value: 'Menu1',
action: () => {
console.info('handle Menu1 select')
}
}
])
```
![en-us_image_0000001562940565](figures/en-us_image_0000001562940565.png)
## Creating a Menu in a Custom Style
If the default style does not meet requirements, you can use \@CustomBuilder to customize menu content. Menus can also be customized through the **bindMenu** API.
### \@Builder: Customizing Menu Content
```ts
@State select: boolean = true
private iconStr: ResourceStr = $r("app.media.view_list_filled")
private iconStr2: ResourceStr = $r("app.media.view_list_filled")
@Builder
SubMenu() {
Menu() {
MenuItem({ content: "Copy", labelInfo: "Ctrl+C" })
MenuItem({ content: "Paste", labelInfo: "Ctrl+V" })
}
}
@Builder
MyMenu(){
Menu() {
MenuItem({ startIcon: $r("app.media.icon"), content: "Menu option" })
MenuItem({ startIcon: $r("app.media.icon"), content: "Menu option" }).enabled(false)
MenuItem({
startIcon: this.iconStr,
content: "Menu option",
endIcon: $r("app.media.arrow_right_filled"),
// When the builder parameter is set, it indicates that a submenu is bound to a menu item. When the user hovers the cursor over the menu item, the submenu is displayed.
builder: this.SubMenu.bind(this),
})
MenuItemGroup ({ header: 'Subtitle' }) {
MenuItem ({ content: "Menu option" })
.selectIcon(true)
.selected(this.select)
.onChange((selected) => {
console.info("menuItem select" + selected);
this.iconStr2 = $r("app.media.icon");
})
MenuItem({
startIcon: $r("app.media.view_list_filled"),
content: "Menu option",
endIcon: $r("app.media.arrow_right_filled"),
builder: this.SubMenu.bind(this)
})
}
MenuItem({
startIcon: this.iconStr2,
content: "Menu option",
endIcon: $r("app.media.arrow_right_filled")
})
}
}
```
### Using the bindMenu Attribute to Bind a Component
```ts
Button('click for Menu')
.bindMenu(this.MyMenu)
```
![en-us_image_0000001511580924](figures/en-us_image_0000001511580924.png)
## Creating a Context Menu Displayed Upon Right-clicking or Long Pressing
Use the **bindContextMenu** API to customize the menu content and menu popup mode: right-click or long press. The menu items that are displayed using **bindContextMenu** are in an independent child window and can be displayed outside the application window.
- The content in the @Builder is the same as that in the preceding section.
- Check the menu popup mode and bind the component through the **bindContextMenu** attribute. In the example, the menu is displayed upon right-clicking.
```ts
Button('click for Menu')
.bindContextMenu(this.MyMenu, ResponseType.RightClick)
```
# Page Routing
Page routing refers to the redirection and data transfer between different pages in an application. In OpenHarmony, page routing can be implemented through APIs of the **Router** module. Through different URLs, you can easily navigate users through pages. This document describes the functions provided by the **Router** module from the following aspects: [Page Redirection](#page-redirection), [Page Return](#page-return), and [Adding a Confirmation Dialog Box Before Page Return](#adding-a-confirmation-dialog-box-before-page-return).
## Page Redirection
Page redirection is an important part of the development process. When using an application, you usually need to jump between different pages, and sometimes you need to pass data from one page to another.
**Figure 1** Page redirection
![router-jump-to-detail](figures/router-jump-to-detail.gif)
The **Router** module provides two redirection modes: [router.pushUrl()](../reference/apis/js-apis-router.md#routerpushurl9) and [router.replaceUrl()](../reference/apis/js-apis-router.md#routerreplaceurl9). The two modes determine whether the target page will replace the current page.
- **router.pushUrl()**: The target page does not replace the current page. Instead, it is pushed into the [page stack](../application-models/page-mission-stack.md). In this way, the state of the current page can be retained, and users can return to the current page by pressing the back button or calling the [router.back()](../reference/apis/js-apis-router.md#routerback) API.
- **router.replaceUrl()**: The target page replaces the current page and destroys the current page. In this way, the resources of the current page can be released, and users cannot return to the current page.
>**NOTE**
>
>The maximum capacity of a page stack is 32 pages. If this limit is exceeded, the [router.clear()](../reference/apis/js-apis-router.md#routerclear) API can be called to clear the historical page stack and free the memory.
The **Router** module also provides two instance modes: **Standard** and **Single**. The two modes determine whether the target URL corresponds to multiple instances.
- **Standard**: standard instance mode, which is the default instance mode. Each time this API is called, a target page is created and pushed to the top of the stack.
- **Single**: singleton mode. If the URL of the target page already exists in the page stack, the page with the same URL closest to the top of the stack is moved to the top of the stack and reloaded. If the URL of the target page does not exist in the page stack, the page is redirected in standard mode.
Before using the **Router** module, you need to import it to the code.
```ts
import router from '@ohos.router';
```
- Scenario 1: There is a home page (**Home**) and a details page (**Detail**). You want to click an offering on the home page to go to the details page. In addition, the home page needs to be retained in the page stack so that the status can be restored when the page is returned. In this scenario, you can use the **pushUrl()** API and use the **Standard** instance mode (which can also be omitted).
```ts
// On the Home page
function onJumpClick(): void {
router.pushUrl({
url: 'pages/Detail' // Target URL.
}, router.RouterMode.Standard, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke pushUrl succeeded.');
});
}
```
>**NOTE**
>
>In **Standard** instance mode, the **router.RouterMode.Standard** parameter can be omitted.
- Scenario 2: There is a login page (**Login**) and a personal center page (**Profile**). After a user successfully logs in from the **Login** page, the **Profile** page is displayed. At the same time, the **Login** page is destroyed, and the application exits when the back button is pressed. In this scenario, you can use the **replaceUrl()** API and use the Standard instance mode (which can also be omitted).
```ts
// On the Login page
function onJumpClick(): void {
router.replaceUrl({
url: 'pages/Profile' // Target URL.
}, router.RouterMode.Standard, (err) => {
if (err) {
console.error(`Invoke replaceUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke replaceUrl succeeded.');
})
}
```
>**NOTE**
>
>In **Standard** instance mode, the **router.RouterMode.Standard** parameter can be omitted.
- Scenario 3: There is a setting page (**Setting**) and a theme switching page (**Theme**). You want to click a theme option on the **Setting** page to go to the **Theme** page. In addition, you want to ensure that only one **Theme** page exists in the page stack at a time. When the back button is clicked on the **Theme** page, the **Setting** page is displayed. In this scenario, you can use the **pushUrl()** API and use the **Single** instance mode.
```ts
// On the Setting page
function onJumpClick(): void {
router.pushUrl({
url: 'pages/Theme' // Target URL.
}, router.RouterMode.Single, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke pushUrl succeeded.');
});
}
```
- Scenario 4: There is a search result list page (**SearchResult**) and a search result details page (**SearchDetail**). You want to click a result on the **SearchResult** page to go to the **SearchDetail** page. In addition, if the result has been viewed before, clicking the result displays the existing details page, instead of creating a new one. In this scenario, you can use the **replaceUrl()** API and use the **Single** instance mode.
```ts
// On the SearchResult page
function onJumpClick(): void {
router.replaceUrl({
url: 'pages/SearchDetail' // Target URL.
}, router.RouterMode.Single, (err) => {
if (err) {
console.error(`Invoke replaceUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke replaceUrl succeeded.');})
}
```
The preceding scenarios do not involve parameter transfer.
If you need to transfer some data to the target page during redirection, you can add a **params** attribute and specify an object as a parameter when invoking an API of the **Router** module. Example:
```ts
class DataModelInfo {
age: number;
}
class DataModel {
id: number;
info: DataModelInfo;
}
function onJumpClick(): void {
// On the Home page
let paramsInfo: DataModel = {
id: 123,
info: {
age: 20
}
};
router.pushUrl({
url: 'pages/Detail', // Target URL.
params: paramsInfo // Add the params attribute to transfer custom parameters.
}, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke pushUrl succeeded.');
})
}
```
On the target page, you can call the [getParams()](../reference/apis/js-apis-router.md#routergetparams) API of the **Router** module to obtain the transferred parameters. Example:
```ts
const params = router.getParams(); // Obtain the transferred parameter object.
const id = params['id']; // Obtain the value of the id attribute.
const age = params['info'].age; // Obtain the value of the age attribute.
```
## Page Return
After a user completes an operation on a page, the user usually needs to return to the previous page or a specified page. In this case, the page return function is required. During the return process, the data may need to be transferred to the target page, which requires the data transfer function.
**Figure 2** Page return
![router-back-to-home](figures/router-back-to-home.gif)
Before using the **Router** module, you need to import it to the code.
```ts
import router from '@ohos.router';
```
You can use any of the following methods to return to a page:
- Method 1: Return to the previous page.
```ts
router.back();
```
This method allows you to return to the position of the previous page in the page stack. For this method to work, the previous page must exist in the page stack.
- Method 2: Return to the specified page.
```ts
router.back({
url: 'pages/Home'
});
```
This method allows you to return to a specified page. You need to specify the path of the target page. For this method to work, the target page must it exist in the page stack.
- Method 3: Return to the specified page and transfer custom parameter information.
```ts
router.back({
url: 'pages/Home',
params: {
info:'From Home Page'
}
});
```
This method not only allows you to return to the specified page, but also transfer custom parameter information when returning. The parameter information can be obtained and parsed by invoking the **router.getParams()** API on the target page.
On the target page, call the **router.getParams()** API at the position where parameters need to be obtained, for example, in the **onPageShow()** lifecycle callback:
```ts
onPageShow() {
const params = router.getParams(); // Obtain the transferred parameter object.
const info = params['info']; // Obtain the value of the info attribute.
}
```
>**NOTE**
>
>When the **router.back()** API is used to return to a specified page, the page is pushed to the top of the stack again, and all page stacks between the original top page (included) and the specified page (excluded) are destroyed.
>
> If the **router.back()** method is used to return to the original page, the original page will not be created repeatedly. Therefore, the variable declared using \@State will not be declared repeatedly, and the **aboutToAppear()** lifecycle callback of the page will not be triggered. If you want to use the custom parameters transferred from the returned page on the original page, you can parse the parameters in the required position. For example, parameter parsing can be performed in the **onPageShow()** lifecycle callback.
## Adding a Confirmation Dialog Box Before Page Return
During application development, to prevent misoperations or data loss, a dialog box needs to be displayed before a user returns from one page to another, asking the user whether to perform the operation.
Such a dialog box can be in the [default style](#default-confirmation-dialog-box) or [custom style](#custom-confirmation-dialog-box).
**Figure 3** Adding a confirmation dialog box before page return
![router-add-query-box-before-back](figures/router-add-query-box-before-back.gif)
### Default Confirmation Dialog Box
To implement this function, you can use the [router.showAlertBeforeBackPage()](../reference/apis/js-apis-router.md#routershowalertbeforebackpage9) and [router.back()](../reference/apis/js-apis-router.md#routerback) APIs provided by the **Router** module.
Before using the **Router** module, you need to import it to the code.
```ts
import router from '@ohos.router';
```
To enable the confirmation dialog box for page return, call the [router.showAlertBeforeBackPage()](../reference/apis/js-apis-router.md#routershowalertbeforebackpage9) API (for setting the information about the dialog box), then the [router.back()](../reference/apis/js-apis-router.md#routerback) API. For example, define a click event processing function for the back button on the payment page:
```ts
// Define a click event processing function for the back button.
function onBackClick(): void {
// Invoke the router.showAlertBeforeBackPage() API to set the information about the confirmation dialog box.
try {
router.showAlertBeforeBackPage({
message: 'Payment not completed yet. Are you sure you want to return?' // Set the content of the confirmation dialog box.
});
} catch (err) {
console.error(`Invoke showAlertBeforeBackPage failed, code is ${err.code}, message is ${err.message}`);
}
// Invoke the router.back() API to return to the previous page.
router.back();
}
```
The **router.showAlertBeforeBackPage()** API receives an object as a parameter. The object contains the following attributes:
- **message**: content of the dialog box. The value is of the string type.
If the API is successfully called, the confirmation dialog box is displayed on the target page. Otherwise, an exception is thrown and the error code and error information is obtained through **err.code** and **err.message**.
When the user clicks the back button, a confirmation dialog box is displayed, prompting the user to confirm their operation. If the user selects Cancel, the application stays on the current page. If the user selects OK, the **router.back()** API is triggered and the redirection is performed based on the parameters.
### Custom Confirmation Dialog Box
To implement a custom confirmation dialog box, use APIs in the [PromptAction](../reference/apis/js-apis-promptAction.md#promptactionshowdialog) module or customize a popup window. This topic uses the APIs in the **PromptAction** module an example to describe how to implement a custom confirmation dialog box.
Before using the **Router** module, you need to import it to the code.
```ts
import router from '@ohos.router';
```
In the event callback, call the [promptAction.showDialog()](../reference/apis/js-apis-promptAction.md#promptactionshowdialog) API of the **PromptAction** module.
```ts
function onBackClick() {
// Display a custom confirmation dialog box.
promptAction.showDialog({
message:'Payment not completed yet. Are you sure you want to return?',
buttons: [
{
text: 'Cancel',
color: '#FF0000'
},
{
text: 'OK',
color: '#0099FF'
}
]
}).then((result) => {
if (result.index === 0) {
// The user selects Cancel.
console.info('User canceled the operation.');
} else if (result.index === 1) {
// The user selects OK.
console.info('User confirmed the operation.');
// Invoke the router.back() API to return to the previous page.
router.back();
}
}).catch((err) => {
console.error(`Invoke showDialog failed, code is ${err.code}, message is ${err.message}`);
})
}
```
When the user clicks the back button, the custom confirmation dialog box is displayed, prompting the user to confirm their operation. If the user selects Cancel, the application stays on the current page. If the user selects OK, the **router.back()** API is triggered and the redirection is performed based on the parameters.
# Spring Curve Animation
ArkUI provides [preset animation curves](../reference/arkui-ts/ts-appendix-enums.md#curve), which specifies the change rule of animation attributes from the start value to the end value, such as Linear, Ease, and EaseIn. At the same time, ArkUI also provides spring curves generated by the physical model of spring oscillators. Through the spring curve, developers can set the end value to be exceeded and oscillate around the end value until the end value stops. The animation effect of spring curve is more interactive and playable than other curves.
There are two types of interfaces for spring curves: [springCurve](../reference/apis/js-apis-curve.md#curvesspringcurve9) and [springMotion](../reference/apis/js-apis-curve.md#curvesspringmotion9) and [responsiveSpringMotion](../reference/apis/js-apis-curve.md#curvesresponsivespringmotion9). Spring curves can be generated in both modes.
## Using springCurve
The **springCurve** API is as follows:
```ts
springCurve(velocity: number, mass: number, stiffness: number, damping: number)
```
The structural parameters include the initial velocity, the mass, stiffness and damping of the spring system. When building springCurve, you can set the quality to 1. Adjust the stiffness and damping parameters based on the parameter description in springCurve to achieve the desired oscillation effect.
```ts
import curves from '@ohos.curves';
@Entry
@Component
struct SpringTest {
@State translateX: number = 0;
private jumpWithSpeed(speed: number) {
this.translateX = -1;
animateTo({ duration: 2000, curve: curves.springCurve(speed, 1, 1, 1.2) }, () => {
//Spring animation for translation in the x direction at the specified initial speed
this.translateX = 0;
})
}
build() {
Column() {
Button("button")
.fontSize(14)
.width(100)
.height(50)
.margin(30)
.translate({ x: this.translateX })
Row({space:50}) {
Button("jump 50").fontSize(14)
.onClick(() => {
//Translate the spring curve with an initial speed of 50.
this.jumpWithSpeed(50);
})
Button("jump 200").fontSize(14)
.onClick(() => {
//Translate the spring curve with an initial speed of 200.
this.jumpWithSpeed(200);
})
}.margin(30)
}.height('100%').width('100%')
}
}
```
![springCurve](figures/springCurve.gif)
In the preceding example, when different buttons are clicked and different initial speeds of springCurve are given, the buttons reach the specified positions elastically, and the amplitude of the buttons increases with the speed. In addition, you can modify the mass, stiffness, and damping parameters of springCurve to achieve the desired elasticity.
>**NOTE**
>
>The speed only amplifies the oscillation effect, but whether the system can produce the oscillation effect depends on the physical parameters of the spring oscillator, that is, mass, stiffness, and damping. The smaller the stiffness and the larger the damping, the weaker the "elasticity" of springCurve and the weaker the oscillation effect. With the decrease of stiffness or the increase of damping, no matter how large the velocity is, there will be no oscillation near the end value.
## Using springMotion and responsiveSpringMotion
The interfaces of the [springMotion](../reference/apis/js-apis-curve.md#curvesspringmotion9) are as follows:
```ts
springMotion(response?: number, dampingFraction?: number, overlapDuration?: number)
```
The interfaces of the [responsiveSpringMotion](../reference/apis/js-apis-curve.md#curvesresponsivespringmotion9) are as follows:
```ts
responsiveSpringMotion(response?: number, dampingFraction?: number, overlapDuration?: number)
```
Their construction parameters include three optional parameters: spring natural vibration period, damping coefficient, and elastic animation connection duration. For details about the parameters, see their documents.
When the springMotion and responsiveSpringMotion curves are used, the duration parameter does not take effect. This parameter is applicable to hand animation.
```ts
import curves from '@ohos.curves';
@Entry
@Component
struct SpringMotionTest {
@State positionX: number = 100;
@State positionY: number = 100;
diameter: number = 50;
build() {
Column() {
Row() {
Circle({ width: this.diameter, height: this.diameter })
.fill(Color.Blue)
.position({ x: this.positionX, y: this.positionY })
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Move) {
// Follow-up process. Use the responsiveSpringMotion curve.
animateTo({ curve: curves.responsiveSpringMotion() }, () => {
//subtract the radius so that the center of the ball moves to the finger position
this.positionX = event.touches[0].screenX - this.diameter / 2;
this.positionY = event.touches[0].screenY - this.diameter / 2;
console.info(`move, animateTo x:${this.positionX}, y:${this.positionY}`);
})
} else if (event.type === TouchType.Up) {
//Use the springMotion curve when you leave your hand.
animateTo({ curve: curves.springMotion() }, () => {
this.positionX = 100;
this.positionY = 100;
console.info(`touchUp, animateTo x:100, y:100`);
})
}
})
}
.width("100%").height("80%")
.clip(true) //If the ball exceeds the range of the parent component, the ball is invisible.
.backgroundColor(Color.Orange)
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Start, justifyContent: FlexAlign.Center }) {
Text("Drag the ball").fontSize(16)
}
.width("100%")
Row() {
Text('Click position: [x:' + Math.round(this.positionX) +', y:' + Math.round(this.positionY) + ']').fontSize(16)
}
.padding(10)
.width("100%")
}.height('100%').width('100%')
}
}
```
The preceding code is an example of hand animation. In the onTouch event, the touch position is captured and the translate or position attribute of the component is changed so that the component moves to the touch position when following the hand. After the hand is released, the component returns to the original position. The following figure shows the effect of the follower animation.
![springMotion](figures/springMotion.gif)
The responsiveSpringMotion curve is recommended for the follow-up process, and the springMotion curve is recommended for the release process. The follow-up process is triggered for multiple times as the position of the hand changes. Therefore, the responsiveSpringMotion animation is started for multiple times. When the hand is released, the springMotion animation is started once. During the follow-up and release processes, an animation is executed on the same attribute of the same object, and the springMotion or responsiveSpringMotion curve is used. Each time the animation is started, the speed used by the last animation is inherited, implementing smooth transition.
>**NOTE**
>
> 1. SpringCurve can be used to set the initial speed. If a single attribute has multiple animations, the animations do not affect each other. The effects of multiple animations are superimposed.
>
> 2. Although springMotion has an internal speed mechanism, it cannot be set by developers. When multiple animations exist for a single attribute, the later animation replaces the previous animation and inherits the speed of the previous animation.
# Transition Animation Within the Component
The process of inserting and deleting a component is the transition process of the component. The animation of inserting and deleting a component is called the transition animation in the component. You can define the appearance and disappearance effects of a component through the transition animation in the component.
The interface of the transition animation in the component is as follows:
```ts
transition(value: TransitionOptions)
```
The input parameter of the [transition](../reference/arkui-ts/ts-transition-animation-component.md) API defines the transition effect in the component. You can define the transition effect of one or more transition styles such as translation, opacity, rotation, and scaling. **transition** must be used together with [animateTo](arkts-layout-update-animation.md#using-explicit-animation-to-create-layout-update-animation).
## Common Usage of Transition
The type parameter is used to specify the gadget change scenario where the current transition dynamic effect takes effect. The type is [TransitionType](../reference/arkui-ts/ts-appendix-enums.md#transitiontype).
- The same animation effect is used for inserting and deleting components.
```ts
Button()
.transition({ type: TransitionType.All, scale: { x: 0, y: 0 } })
```
When the type attribute is TransitionType.All, the specified transition effect takes effect in all change (insertion and deletion) scenarios of the component. In this case, deleting the animation and inserting the animation are reverse processes, and deleting the animation is reverse playing of inserting the animation. For example, the preceding code defines a Button control. When a component is inserted, the component changes from the state where x and y of the scale are both 0 to the default state where x and y of the scale are both 1 (complete display). The component is gradually zoomed in. When a component is deleted, the component changes from the default state where x and y of the scale are 1 to the state where x and y of the specified scale are 0 and gradually shrinks to 0.
- Different animation effects are used for inserting and deleting components.
```ts
Button()
.transition({ type: TransitionType.Insert, translate: { x: 200, y: -200 }, opacity: 0 })
.transition({ type: TransitionType.Delete, rotate: { x: 0, y: 0, z: 1, angle: 360 } })
```
When different transition animation effects need to be implemented for component insertion and deletion, you can call the transition function twice to set the type attribute to TransitionType.Insert and TransitionType.Delete respectively. For example, the preceding code defines a Button control. When the component is inserted, the component changes from an initial state in which the position is translated by 200 vp in the x direction and the position is translated by –200 vp in the y direction relative to the normal layout position of the component, and the transparency is 0 in the x and y directions to a default state in which the translation amount is 0 and the transparency is 1. The inserted animation is a combination of a translation animation and a transparency animation. When the component is deleted, the component changes from the default state in which the rotation angle is 0 to the end state in which the component rotates 360 degrees around the z axis, that is, rotates around the z axis for one week.
- Define only one of the animation effects of inserting or deleting a component.
```ts
Button()
.transition({ type: TransitionType.Delete, translate: { x: 200, y: -200 } })
```
If only the transition animation effect of component insertion or deletion is required, you only need to set the transition effect whose **type** attribute is **TransitionType.Insert** or **TransitionType.Delete**. For example, the preceding code defines a Button control. When a component is deleted, the component is moved from the normal position without translation to the position of 200 vp in the x direction and -200 vp in the y direction relative to the normal layout position. Inserting the component does not generate a transition animation for the component.
## if/else: generates transition animations in components.
The if/else statement can control the insertion and deletion of components. The following code can be used to control whether the if condition is met through the button click event to control whether to display the Image component under if.
```ts
@Entry
@Component
struct IfElseTransition {
@State flag: boolean = true;
@State show: string = 'show';
build() {
Column() {
Button(this.show).width(80).height(30).margin(30)
.onClick(() => {
if (this.flag) {
this.show = 'hide';
} else {
this.show = 'show';
}
//Click the button to control the display and disappearance of the image.
this.flag = !this.flag;
})
if (this.flag) {
Image($r('app.media.mountain')).width(200).height(200)
}
}.height('100%').width('100%')
}
}
```
No animation is configured in the preceding code. Next, we'll add the in-component transition effect to the above code. The Image component is controlled by if. You need to add the transition parameter to the Image component to specify the transition effect in the component. For example, you can add the translation effect when inserting the file, and add the scaling and transparency effects when deleting the file.
```ts
if (this.flag) {
Image($r('app.media.mountain')).width(200).height(200)
.transition({ type: TransitionType.Insert, translate: { x: 200, y: -200 } })
.transition({ type: TransitionType.Delete, opacity: 0, scale: { x: 0, y: 0 } })
}
```
Although the preceding code specifies the animation style, the animation parameters are not specified. Therefore, you do not know how long and how to use the curve to complete the animation. Transition must be used together with animateTo. In the closure of animateTo, transition controls the insertion and deletion of components. The preceding sample code is used to change the value of flag in the animateTo closure. The code is as follows: Set the animation duration to 1000 ms, use the default curve of the animateTo function, and change the value of flag. Animation is generated based on the animation parameter for all changes caused by flag changes. Here, the flag affects the appearance and disappearance of the image.
```ts
animateTo({ duration: 1000 }, () => {
this.flag = !this.flag;
})
```
After the preceding process, when animateTo and transition are used together, the transition animation in the component is generated. The complete sample code is as follows:
```ts
@Entry
@Component
struct IfElseTransition {
@State flag: boolean = true;
@State show: string = 'show';
build() {
Column() {
Button(this.show).width(80).height(30).margin(30)
.onClick(() => {
if (this.flag) {
this.show = 'hide';
} else {
this.show = 'show';
}
animateTo({ duration: 1000 }, () => {
//Control the appearance and disappearance of the Image component in the animation closure.
this.flag = !this.flag;
})
})
if (this.flag) {
//The appearance and disappearance of the image are configured as different transition effects.
Image($r('app.media.mountain')).width(200).height(200)
.transition({ type: TransitionType.Insert, translate: { x: 200, y: -200 } })
.transition({ type: TransitionType.Delete, opacity: 0, scale: { x: 0, y: 0 } })
}
}.height('100%').width('100%')
}
}
```
![ifElseTransition](figures/ifElseTransition.gif)
>**NOTE**
>
>When the transition effect is set to translate or scale, the animation process may exceed the range of the parent component after the translation or magnification is overlaid on the position. If you want the child component to be completely displayed when the parent component is beyond the range of the parent component, you can set the clip attribute of the parent component to false so that the parent component does not tailor the child component. If you want the excess child components not to be displayed when the parent component is exceeded, you can set the clip attribute of the parent component to true to tailor the excess child components.
## Forach generates transition animations in components.
Similar to if/else, Forach can control the insertion and deletion of components by controlling the number of elements in an array. To use Forach to generate an intra-component transition animation, the following conditions must be met:
- The transition effect is configured for the component in Forach.
- Controls the insertion or deletion of components in the closure of animateTo, that is, controls the addition and deletion of array elements.
The following code is an example of using Forach to generate an in-component transition animation.
```ts
@Entry
@Component
struct ForEachTransition {
@State numbers: string[] = ["1", "2", "3", "4", "5"]
startNumber: number = 6;
build() {
Column({ space: 10 }) {
Column() {
ForEach(this.numbers, (item) => {
// The transition effect needs to be configured for the direct component under Forach.
Text(item)
.width(240)
.height(60)
.fontSize(18)
.borderWidth(1)
.backgroundColor(Color.Orange)
.textAlign(TextAlign.Center)
.transition({ type: TransitionType.All, translate: { x: 200 }, scale: { x: 0, y: 0 } })
}, item => item)
}
.margin(10)
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Center)
.width("90%")
.height("70%")
Button ('Add element to header')
.fontSize(16)
.width(160)
.onClick(() => {
animateTo({ duration: 1000 }, () => {
//Insert an element to the array header. As a result, Forach adds the corresponding component to the header.
this.numbers.unshift(this.startNumber.toString());
this.startNumber++;
})
})
Button ('Add element to tail')
.width(160)
.fontSize(16)
.onClick(() => {
animateTo({ duration: 1000 }, () => {
//Insert an element to the end of the array. As a result, Forach adds the corresponding component to the end.
this.numbers.push(this.startNumber.toString());
this.startNumber++;
})
})
Button ('Delete Header Element')
.width(160)
.fontSize(16)
.onClick(() => {
animateTo({ duration: 1000 }, () => {
//Delete the header element of the array. As a result, Forach deletes the header component.
this.numbers.shift();
})
})
Button ('Delete Tail Element')
.width(160)
.fontSize(16)
.onClick(() => {
animateTo({ duration: 1000 }, () => {
//Delete the tail element of the array. As a result, Forach deletes the header component.
this.numbers.pop();
})
})
}
.width('100%')
.height('100%')
}
}
```
The display effect is shown below.
![forEachTransition2](figures/forEachTransition2.gif)
The column layout mode is set to FlexAlign.Start, that is, the vertical layout starts from the head. Therefore, when an element is added to the end of an array, the position of the component corresponding to the existing element in the array is not affected, and only the insertion animation of the new component is triggered. When an element is added to the array header, the subscripts of all elements in the original array are added. Although the addition or deletion of the element is not triggered, the location of the corresponding component is affected. Therefore, in addition to the transition animation for new components, the position animation is also performed for components in Forach.
>**NOTE**
>
>If/else and ForEach are syntax nodes. The component for configuring the transition effect in the component should be directly used as the child of the syntax node. If a component is added or deleted due to the addition or deletion of a syntax node, only the intra-component transition animation of the direct child component can be triggered. Developers should not expect the component transition animation to be generated for deeper components.
# UI Development (ArkTS-based Declarative Development Paradigm) Overview
Powered by the ArkTS-based declarative development paradigm, ArkUI is a simplified, high-performance UI development framework for cross-device applications. It provides the capabilities required for building the OpenHarmony application UI, including:
- **ArkTS**
ArkTS is a UI development language. As a superset of TypeScript, ArkTS contains all TS features and added features, including a wide array of decorators, custom components, and the UI description mechanism. State data management provides clear page update and rendering processes and pipes through decorators with different functions. State management covers UI component states and application states. With these features, you are able to build an application-wide data update and UI rendering process. To learn more about ArkTS, see [Getting Started with ArkTS](../quick-start/arkts-get-started.md).
- **Layout**
The layout defines how components are laid out in the UI. ArkUI offers a diverse array of layouts. Besides the basic layouts – linear, stack, flex, relative, and grid, you also have access to the advanced list, grid, and swiper layouts.
- **Component**
Components are essential elements of the UI, working together to shape the UI. They can be classified as built-in components – those directly provided by the ArkUI framework, and custom components – those defined by developers. The built-in components include buttons, radio buttons, progress indicators, and text. You can set the rendering effect of these components in method chaining mode. You can combine built-in components to form custom components. In this way, page components are divided into independent UI units to implement independent creation, development, and reuse of different units on pages, making pages more engineering-oriented.
- **Page routing and component navigation**
An application may contain a good many pages, and each page may come with multiple components. You can implement page routing to navigate users between pages and use navigation component to navigate them between components.
- **Graphics**
ArkUI offers diversified graphics capabilities, including capabilities to display images in various formats and custom drawing capabilities. By leveraging these capabilities, you can easily bring your custom drawing ideas into reality.
- **Animation**
Apart from animations embedded in components, ArkUI offers additional animation features: attribute animation, explicit animation, transition animation, and animation APIs. You can customize animation tracks by calling the provided animation APIs in addition to using the encapsulated physical models.
- **Interaction event**
Interaction events are important for interactions between the UI and users. ArkUI allows users to interact with your application UI, with support for various universal events and gesture events. Universal events include touch events, mouse events, key events, and focus events. Gesture events accept single gestures (tap, long press, pan, pinch, rotation, and swipe), and a combination of gestures.
## Highlights
- Simplified and efficient development
- Simple code: You can describe the UI in pseudo-natural language, without caring about how the framework implements UI drawing and rendering.
- Data-driven UI change: This allows you to better focus on your service logic processing. When the UI changes, you do not need to write code for switching between different UIs. Instead, you only need to write the data that causes the UI change and let the framework take over the rest.
- Improved development experience: Just code to get the UI developed.
- High performance
- Declarative UI frontend and backend layering: The UI backend, constructed using the C++ programming language, provides basic components, layout, animations, interaction events, component state management, and rendering pipelines for the frontend.
- Language compiler and runtime optimization: The productivity punch includes unified bytecode, efficient Foreign Function Interface (FFI), ahead-of-time (AOT), engine minimization, and type optimization.
- Promising ecosystem
ArkUI can gain traction with its relatively neutral and friendly programming language. It can tap on the ecosystems of mainstream languages ecosystem and pushed toward a steady revolutionary path with standards organizations.
## Architecture
**Figure 1** Overall architecture
![arkui-arkts-framework](figures/arkui-arkts-framework.png)
- **Declarative UI frontend**
Provides basic language specifications of the UI development paradigm, built-in UI components, layouts, and animations, and multiple state management mechanisms, with a wide array of APIs for you to call as required.
- **Language runtime**
Provides the parsing capability for the UI paradigm syntax and allows for cross-language API calls for a high-performance operating environment of the TS language.
- **Declarative UI backend engine**
Provides UI rendering pipelines that are compatible with different development paradigms, multiple basic components, layout calculation, dynamic effects, and interaction events, with state management and drawing capabilities.
- **Render engine**
Provides efficient drawing capabilities, which enable rendering instructions collected by the rendering pipeline to be drawn to the screen.
- **Platform adaptation layer**
Provides abstract APIs to connect to different systems, such as system rendering pipelines and lifecycle scheduling.
## Development Process
The table below lists the main tasks involved in UI development with ArkUI. You can familiarize yourself with the UI development process by [getting started with a simple project](../quick-start/start-with-ets-stage.md).
| Task | Description | Guide |
| ----------- | ----------------------------------- | ---------------------------------------- |
| Get started with ArkTS | Learn the basic syntax, state management, and rendering control scenarios of ArkTS. | - [Basic Syntax Overview](../quick-start/arkts-basic-syntax-overview.md)<br>- [State Management](../quick-start/arkts-state-management-overview.md)<br>- [Rendering Control](../quick-start/arkts-rendering-control-overview.md) |
| Develop the layout | Understand the common layouts and how to improve layout performance. | - [Layout Overview](arkts-layout-development-overview.md)<br>- [Improving Layout Performance](arkts-layout-development-performance-boost.md)|
| Add components | Learn the usage of common built-in components, custom components, and GUI elements supported by APIs.| - [Common Components](arkts-common-components-button.md)<br>- [Custom Components](../quick-start/arkts-create-custom-components.md)<br>- [Popup and Menu](arkts-popup-and-menu-components-popup.md)|
| Set page routing and component navigation| Learn how to set page routes and navigation between components. | - [Page Routing](arkts-routing.md)<br>- [Navigation](arkts-navigation-navigation.md)|
| Use graphics | Understand how to display images, draw custom geometry, and make custom graphics on the canvas. | - [Displaying Images](arkts-graphics-display.md)<br>- [Drawing Geometric Shapes](arkts-geometric-shape-drawing.md)<br>- [Drawing Custom Graphics Using the Canvas](arkts-drawing-customization-on-canvas.md) |
| Apply animations | Learn the typical scenarios of applying animations on components and pages. | - [Animation Within a Page](arkts-layout-update-animation.md)<br>- [Animation Between Pages](arkts-zoom-animation.md)|
| Bind events | Learn the basic concepts of events and how to use common events and gesture events. | - [Universal Events](arkts-common-events-touch-screen-event.md)<br>- [Gesture Events](arkts-gesture-events-binding.md)|
##
# Zoom Animation
If the same element (for example, the same image) is used on different pages, you can use the Shared Element Transition (../reference/arkui-ts/ts-transition-animation-shared-elements.md) animation to connect the elements. To highlight the association of the same elements between different pages, you can add shared element transition animations for them. If the sizes of the same element on different pages are obviously different, the view can be zoomed in or out.
The interface for shared element transition is as follows:
```ts
sharedTransition(id: string, options?: sharedTransitionOptions)
```
Based on the type parameter in sharedTransitionOptions, shared element transitions are classified into Exchange shared element transitions and Static shared element transitions.
## Transition of Shared Elements of the Exchange Type
The transition of shared elements of the exchange type requires that components with the same ID configured through the sharedTransition function exist on two pages. These components are called shared elements. This type of shared element transition is applicable to the connection of the same element between two pages, and transits from the position and size of the shared element on the start page to the position and size of the shared element on the target page. If type is not specified, the shared element transition of the Exchange type is used by default, which is the most common shared element transition mode. When the shared element transition of the Exchange type is used, the animation parameters of the shared element transition are determined by the animation parameters in options on the target page.
## Transition of Shared Elements of the Static Type
Static shared element transition is usually used in the scenario where the title gradually appears or is hidden during page switching. Static shared elements only need to exist on one page. Static shared elements with the same ID cannot exist on two pages. When the page (target page) is displayed, the transparency of the sharedTransition component of the Static type is changed from 0 to the transparency set by the component, and the position remains unchanged. When the page (that is, the start page) disappears, an animation in which the transparency gradually changes to 0 is performed, and the position remains unchanged.
The animation parameters of the shared element transition are determined by the animation parameters in the sharedTransition attribute of the component.
## Example Scenario
The following describes an example of using a shared element transition to zoom in or zoom out an image.
```ts
// src page
import router from '@ohos.router';
@Entry
@Component
struct SharedTransitionSrc {
build() {
Column() {
//Configure the transition of the shared element of the Exchange type. The ID of the shared element is sharedImage1.
Image($r('app.media.mountain')).width(50).height(50)
.sharedTransition('sharedImage1', { duration: 1000, curve: Curve.Linear })
.onClick(() => {
//When a small image is clicked, the route jumps to the next page.
router.pushUrl({ url: 'pages/myTest/sharedTransitionDst' });
})
}
.padding(10)
.width("100%")
.alignItems(HorizontalAlign.Start)
}
}
```
```ts
// dest page
import router from '@ohos.router';
@Entry
@Component
struct SharedTransitionDest {
build() {
Column() {
//Configure the transition of shared elements of the Static type.
Text("SharedTransition dest page")
.fontSize(16)
.sharedTransition('text', { duration: 500, curve: Curve.Linear, type: SharedTransitionEffectType.Static })
.margin({ top: 10 })
//Configure the transition of the shared element of the Exchange type. The ID of the shared element is sharedImage1.
Image($r('app.media.mountain'))
.width(150)
.height(150)
.sharedTransition('sharedImage1', { duration: 500, curve: Curve.Linear })
.onClick(() => {
// Return to the previous page when a user clicks an image.
router.back();
})
}
.width("100%")
.alignItems(HorizontalAlign.Center)
}
}
```
In the preceding example, the shared element transition whose ID is sharedImage1 is configured on both the first page (src page) and the second page (dest page) so that the two pages can match the group of shared elements. When you jump from the first page to the second page, the first page is the start page, and the second page is the target page. Configure the component whose ID is sharedImage1 to perform shared element transition based on the duration of 500 ms on the target page to zoom in the view. Configure the component whose ID is text to perform shared element transition based on the duration of 500 ms in the sharedTransition parameter of the Static type, the headline gradually appears. When you return from the second page to the first page, the second page is the start page, and the first page is the target page. Configure the component whose ID is sharedImage1 to perform shared element transition based on the duration of 1000 ms on the target page and zoom out to the original view. Configure the component whose ID is text to perform shared element transition based on the duration of 500 ms in the sharedTransition parameter of the Static type, the title is gradually hidden.
![sharedTransition](figures/sharedTransition.gif)
# Using the Animation Feature
The animation feature provides component animation and inter-page animation, and opens the animation capability APIs for [interpolation calculation](../reference/apis/js-apis-curve.md) and [matrix transformation](../reference/apis/js-apis-matrix4.md), allowing you to design animation effects to a great extent.
This section describes the following animation effects:
1. Animation on the splash screen: fade-in and fade-out of the logo.
2. Transition animation of shared elements on the food list page and food details page.
## AnimateTo for Splash Screen Animation
Component animations include attribute animations and **animateTo** explicit animations.
1. Attribute animation: animation for the universal attribute changes of a component.
2. Explicit animation: animation for a component changing from state A to state B, including the style, location information, and node addition and deletion. You only need to specify the start and end states, without the need for paying attention to the change process. The **animateTo** class also provides the callback for the playback state change events, which is an enhancement and encapsulation of the attribute animation.
The splash screen animation refers to the fade-in and fade-out of the logo. After the animation is complete, the food list page is displayed. The following describes how to use **animateTo** to implement the splash screen animation.
1. Configure the splash screen animation to automatically play. The expected effect is as follows: **animateTo** automatically starts to play the animation once the logo page is displayed. Call the **onAppear** API of **Shape** to set the explicit animation.
```ts
Shape() {
...
}
.onAppear(() => {
animateTo()
})
```
2. Create member variables of **opacity** and **scale** values and decorate them using the **@State** decorator, which indicates that the data is stateful and its change will trigger page refresh.
```ts
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
build() {
Shape() {
...
}
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo()
})
}
}
```
3. Sets the **animateTo** animation curve. The acceleration curve of the logo is slow first and then fast. The Bezier curve cubicBezier, **cubicBezier(0.4, 0, 1, 1)** is used.
Before using interpolation calculation in the **animateTo**, you must import the **Curves** module.
```ts
import Curves from '@ohos.curves'
```
The **@ohos.curves** module provides the linear curve. For initialization function of the linear, step, cubicBezier, and spring interpolation curves, an interpolation curve object can be created based on the input parameters.
```ts
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
build() {
Shape() {
...
}
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo({
curve: this.curve1
})
})
}
}
```
4. Set the animation duration to 1s, set the delay to 0.1s, and set the closure function for displaying animation events. That is, set **opacityValue** and **scaleValue** to change from 0 to 1 from the start point to the end point. In this way, the logo fades in and out.
```ts
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
build() {
Shape() {
...
}
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo({
duration: 1000,
curve: this.curve1,
delay: 100,
}, () => {
this.opacityValue = 1
this.scaleValue = 1
})
})
}
}
```
5. After the splash screen animation plays for 1 second, the **FoodCategoryList** page is displayed. Set the **onFinish** callback of **animateTo**. Invoke the **setTimeout** API of the timer. After a delay of 1s, call **router.replaceUrl** to display the **FoodCategoryList** page.
```ts
import router from '@ohos.router'
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
build() {
Shape() {
...
}
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo({
duration: 1000,
curve: this.curve1,
delay: 100,
onFinish: () => {
setTimeout(() => {
router.replaceUrl({ url: "pages/FoodCategoryList" })
}, 1000);
}
}, () => {
this.opacityValue = 1
this.scaleValue = 1
})
})
}
}
```
The code is as follows:
```ts
import Curves from '@ohos.curves'
import router from '@ohos.router'
@Entry
@Component
struct Logo {
@State private opacityValue: number = 0
@State private scaleValue: number = 0
private curve1 = Curves.cubicBezier(0.4, 0, 1, 1)
private pathCommands1: string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
private pathCommands2: string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z'
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
.fill(Color.White)
.stroke(Color.Transparent)
Path()
.commands(this.pathCommands1)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands(this.pathCommands1))
Path()
.commands(this.pathCommands2)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 50,
colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]]
})
.clip(new Path().commands(this.pathCommands2))
}
.height('630px')
.width('630px')
.scale({ x: this.scaleValue, y: this.scaleValue })
.opacity(this.opacityValue)
.onAppear(() => {
animateTo({
duration: 1000,
curve: this.curve1,
delay: 100,
onFinish: () => {
setTimeout(() => {
router.replaceUrl({ url: "pages/FoodCategoryList" })
}, 1000);
}
}, () => {
this.opacityValue = 1
this.scaleValue = 1
})
})
Text('Healthy Diet')
.fontSize(26)
.fontColor(Color.White)
.margin({ top: 300 })
Text('Healthy life comes from a balanced diet')
.fontSize(17)
.fontColor(Color.White)
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![animation-feature](figures/animation-feature.gif)
## Page Transition Animation
Implement the shared element transition between the food list page and the food details page. That is, after you click **FoodListItem** or **FoodGridItem**, the food thumbnail is zoomed in, and then you are redirected to the large image on the food details page.
1. Set the **sharedTransition** method for the **<Image>** component of **FoodListItem** and **FoodGridItem**. The transition ID is **foodItem.id**, the duration of the transition animation is 1s, and the delay is 0.1s. The change curve is **Curves.cubicBezier(0.2, 0.2, 0.1, 1.0)**. You need to import the **Curves** module first.
During the shared element transition, the attributes of the current element are carried. Therefore, create a **<Row>** component as the parent component of the **<Image>** component, and set the background color on the **<Row>** component.
Set **autoResize** to **false** for the **<Image>** component of **FoodListItem**. The **<Image>** component adjusts the size of the image source based on the final display area by default to optimize the image rendering performance. In the transition animation, the image will be reloaded during the zoom-in process. Therefore, to ensure the smoothness of the transition animation, set **autoResize** to **false**.
```ts
// FoodList.ets
import Curves from '@ohos.curves'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Navigator({ target: 'pages/FoodDetail' }) {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Row() {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.autoResize(false)
.height(40)
.width(40)
.sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 })
}
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
}
.params({ foodData: this.foodItem })
.margin({ right: 24, left:32 })
}
}
@Component
struct FoodGridItem {
private foodItem: FoodData
build() {
Column() {
Row() {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.autoResize(false)
.height(152)
.width('100%')
.sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 })
}
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
.padding({ left: 8 })
Text(this.foodItem.calories + 'kcal')
.fontSize(14)
.margin({ right: 6 })
}
.height(32)
.width('100%')
.backgroundColor('#FFe5e5e5')
}
.height(184)
.width('100%')
.onClick(() => {
router.pushUrl({ url: 'pages/FoodDetail', params: { foodData: this.foodItem } })
})
}
}
```
2. Sets the **sharedTransition** method for the **<Image>** component of **FoodImageDisplay** on the **FoodDetail** page. The setting method is the same as that mentioned above.
```ts
import Curves from '@ohos.curves'
@Component
struct FoodImageDisplay {
private foodItem: FoodData
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.sharedTransition(this.foodItem.id, { duration: 1000, curve: Curves.cubicBezier(0.2, 0.2, 0.1, 1.0), delay: 100 })
Text(this.foodItem.name)
.fontSize(26)
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
.height(357)
}
}
```
![animation-feature1](figures/animation-feature1.gif)
Now we have completed the drawing of the startup logo, splash screen animation, and transition animation between pages. By applying and combining the various animation APIs in the declarative development framework, you can create a more immersive experience for your applications.
# Building a Food Category Grid Layout
The diet application allows food on the home page to display in list or grid mode. You can implement switching between food categories through tabs in grid mode.
1. Import the **Category** enumeration type to the **FoodCategoryList** page.
```ts
import { Category, FoodData } from '../model/FoodData'
```
2. Create the **FoodCategoryList** and **FoodCategory** components. The **FoodCategoryList** component is used as the entry component of the new page, and the **initializeOnStartup** method is invoked in the entry component.
```ts
@Component
struct FoodList {
private foodItems: FoodData[]
build() {
......
}
}
@Component
struct FoodCategory {
private foodItems: FoodData[]
build() {
......
}
}
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
......
}
}
```
3. Create the **showList** member variable in the **FoodCategoryList** component to control the rendering switchover between the list layout and grid layout. The conditional rendering statement **if...else...** is required.
```ts
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
private showList: boolean = false
build() {
Stack() {
if (this.showList) {
FoodList({ foodItems: this.foodItems })
} else {
FoodCategory({ foodItems: this.foodItems })
}
}
}
}
```
4. In the upper right corner of the page, create an icon for switching between the list and grid layouts. Set the stack alignment mode to **TopEnd**, top-bottom alignment. Create an image component, and set the click event, that is, negation of **showList**.
```ts
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
private showList: boolean = false
build() {
Stack({ alignContent: Alignment.TopEnd }) {
if (this.showList) {
FoodList({ foodItems: this.foodItems })
} else {
FoodCategory({ foodItems: this.foodItems })
}
Image($r('app.media.Switch'))
.height(24)
.width(24)
.margin({ top: 15, right: 10 })
.onClick(() => {
this.showList = !this.showList
})
}.height('100%')
}
}
```
5. Add the **@State** decorator. After you click the switch tab in the upper right corner, the page does not change. This is because the **showList** does not have state data and its change does not trigger the page refresh. You need to add the **@State** decorator to make it state data. The change of the **@State** decorator will cause re-rendering of the component where the decorator is located.
```ts
@Entry
@Component
struct FoodCategoryList {
private foodItems: FoodData[] = initializeOnStartup()
@State private showList: boolean = false
build() {
Stack({ alignContent: Alignment.TopEnd }) {
if (this.showList) {
FoodList({ foodItems: this.foodItems })
} else {
FoodCategory({ foodItems: this.foodItems })
}
Image($r('app.media.Switch'))
.height(24)
.width(24)
.margin({ top: 15, right: 10 })
.onClick(() => {
this.showList = !this.showList
})
}.height('100%')
}
}
```
When you click the switch icon, the **FoodList** component is displayed. When you click the switch icon again, the **FoodList** component is hidden.
![en-us_image_0000001170411978](figures/en-us_image_0000001170411978.gif)
6. Create a tab to display all food categories (**All**). Create the **\<Tabs>** component and its child component **TabContent** in the **FoodCategory** component, and set **tabBar** to **All**. Set the width of the **TabBars** to 280 and the layout mode to **Scrollable**. This means that the **TabBars** can be scrolled when the total length exceeds 280. The **\<Tabs>** component is a container component that allows users to switch between content views through tabs. Each tab page corresponds to a **TabContent**.
```ts
@Component
struct FoodCategory {
private foodItems: FoodData[]
build() {
Stack() {
Tabs() {
TabContent() {}.tabBar('All')
}
.barWidth(280)
.barMode(BarMode.Scrollable)
}
}
}
```
![en-us_image_0000001204538065](figures/en-us_image_0000001204538065.png)
7. Create the **FoodGrid** component to function as a child component of the **TabContent** component.
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
build() {}
}
@Component
struct FoodCategory {
private foodItems: FoodData[]
build() {
Stack() {
Tabs() {
TabContent() {
FoodGrid({ foodItems: this.foodItems })
}.tabBar('All')
}
.barWidth(280)
.barMode(BarMode.Scrollable)
}
}
}
```
8. Implement a 2 x 6 grid layout (12 food data resources in total). Create a **Grid** component, and set **columnsTemplate** to **('1fr 1fr')**, **rowsTemplate** to **('1fr 1fr 1fr 1fr 1fr 1fr')**, and both **rowsGap** and **columnsGap** to **8**. Create a **Scroll** component so that it can be slid.
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
build() {
Scroll() {
Grid() {
ForEach(this.foodItems, (item: FoodData) => {
GridItem() {}
}, (item: FoodData) => item.id.toString())
}
.rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr')
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
}
.scrollBar(BarState.Off)
.padding({left: 16, right: 16})
}
}
```
9. Create a **FoodGridItem** component to display the food image, name, and calories and implement the UI layout. The **FoodGridItem** component is a child component of the **GridItem** component. The height of each **FoodGridItem** is **184**, and the line spacing is **8**. The total height of the **Grid** component is calculated as follows: (184 + 8) x 6 – 8 = 1144.
```ts
@Component
struct FoodGridItem {
private foodItem: FoodData
build() {
Column() {
Row() {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(152)
.width('100%')
}
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
.padding({ left: 8 })
Text(this.foodItem.calories + 'kcal')
.fontSize(14)
.margin({ right: 6 })
}
.height(32)
.width('100%')
.backgroundColor('#FFe5e5e5')
}
.height(184)
.width('100%')
}
}
@Component
struct FoodGrid {
private foodItems: FoodData[]
build() {
Scroll() {
Grid() {
ForEach(this.foodItems, (item: FoodData) => {
GridItem() {
FoodGridItem({foodItem: item})
}
}, (item: FoodData) => item.id.toString())
}
.rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr')
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.height(1144)
}
.scrollBar(BarState.Off)
.padding({ left: 16, right: 16 })
}
}
```
![en-us_image_0000001170167520](figures/en-us_image_0000001170167520.png)
10. Create the **Category.Vegetable**, **Category.Fruit**, **Category.Nut**, **Category.SeaFood**, and **Category.Dessert** tabs.
```ts
@Component
struct FoodCategory {
private foodItems: FoodData[]
build() {
Stack() {
Tabs() {
TabContent() {
FoodGrid({ foodItems: this.foodItems })
}.tabBar('All')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Vegetable)) })
}.tabBar('Vegetable')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Fruit)) })
}.tabBar('Fruit')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Nut)) })
}.tabBar('Nut')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Seafood)) })
}.tabBar('Seafood')
TabContent() {
FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Dessert)) })
}.tabBar('Dessert')
}
.barWidth(280)
.barMode(BarMode.Scrollable)
}
}
}
```
11. Set the number of rows and height of grids for different food categories. Because the number of foods varies according to the category, the **''1fr 1fr 1fr 1fr 1fr 1fr '** constant cannot be used to set the number of rows to 6.
Create member variables **gridRowTemplate** and **HeightValue**, and set the number of grid rows and height by using these member variables.
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
private gridRowTemplate: string = ''
private heightValue: number
build() {
Scroll() {
Grid() {
ForEach(this.foodItems, (item: FoodData) => {
GridItem() {
FoodGridItem({ foodItem: item })
}
}, (item: FoodData) => item.id.toString())
}
.rowsTemplate(this.gridRowTemplate)
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.height(this.heightValue)
}
.scrollBar(BarState.Off)
.padding({ left: 16, right: 16 })
}
}
```
Invoke the **aboutToAppear** API to calculate the number of rows (**gridRowTemplate**) and height (**heightValue**).
```ts
aboutToAppear() {
var rows = Math.round(this.foodItems.length / 2);
this.gridRowTemplate = '1fr '.repeat(rows);
this.heightValue = rows * 192 - 8;
}
```
The custom component provides two lifecycle callbacks: **aboutToAppear** and **aboutToDisappear**. **aboutToAppear** is executed after the custom component is created and before the **build** method of the custom component is executed. **aboutToDisappear** is executed before the custom component is destroyed.
![en-us_image_0000001215113569](figures/en-us_image_0000001215113569.png)
```ts
@Component
struct FoodGrid {
private foodItems: FoodData[]
private gridRowTemplate: string = ''
private heightValue: number
aboutToAppear() {
var rows = Math.round(this.foodItems.length / 2);
this.gridRowTemplate = '1fr '.repeat(rows);
this.heightValue = rows * 192 - 8;
}
build() {
Scroll() {
Grid() {
ForEach(this.foodItems, (item: FoodData) => {
GridItem() {
FoodGridItem({ foodItem: item })
}
}, (item: FoodData) => item.id.toString())
}
.rowsTemplate(this.gridRowTemplate)
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.height(this.heightValue)
}
.scrollBar(BarState.Off)
.padding({ left: 16, right: 16 })
}
}
```
![en-us_image_0000001170008198](figures/en-us_image_0000001170008198.gif)
# Building a Food Category List Layout
Use the **\<List>** component and **ForEach** loop to build the food category list layout.
1. Create a page file named **FoodCategoryList.ets** in the **pages** directory and rename the **index.ets** file **FoodDetail.ets**.
2. Create a **\<List>** component named **FoodList** as the page entry point. Then, add a **\<ListItem>** component named **FoodListItem** as its child component. The **\<List>** component is used to display data of the same type. Its child component **\<ListItem>** is used to display specific items in the list.
```ts
@Component
struct FoodListItem {
build() {}
}
@Entry
@Component
struct FoodList {
build() {
List() {
ListItem() {
FoodListItem()
}
}
}
}
```
3. Import the **FoodData** class and **initializeOnStartup** method.
There are two file access methods in application code:
- Use a relative path to reference the code file: Use **"../"** for referencing the parent directory and **"./"** (which can also be omitted) for referencing the current directory.
- Use the absolute path, which is the root path of the current module, to reference the code file, for example, **common/utils/utils**.
In this example, a relative path is used for access.
```
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
```
4. Configure the **FoodList** and **FoodListItem** components to pass values. Create a member variable named **foodItems** of the **FoodData[]** type in the **FoodList** component and invoke the **initializeOnStartup** method to assign a value to the variable. Create a member variable **foodItem** of the **FoodData** type in the **FoodListItem** component. Pass the **foodItems[0]** of the first element in the parent **foodItems** array as a parameter to **FoodListItem**.
```ts
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {}
}
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
List() {
ListItem() {
FoodListItem({ foodItem: this.foodItems[0] })
}
}
}
}
```
5. Declare the UI layout of the **FoodListItem** child component. Create a **\<Flex>** component, including the food image thumbnail, food name, and calories in the food.
```ts
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
.margin({ right: 24, left:32 })
}
}
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
List() {
ListItem() {
FoodListItem({ foodItem: this.foodItems[0] })
}
}
}
}
```
![en-us_image_0000001204776353](figures/en-us_image_0000001204776353.png)
6. Create two **FoodListItem** objects. Create two **FoodListItem** objects in the **List** component and pass the first element **this.foodItems[0]** and the second element **foodItem: this.foodItems[1]** to the **FoodListItem**.
```ts
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
.margin({ right: 24, left: 32 })
}
}
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
List() {
ListItem() {
FoodListItem({ foodItem: this.foodItems[0] })
}
ListItem() {
FoodListItem({ foodItem: this.foodItems[1] })
}
}
}
}
```
![en-us_image1_0000001204776353](figures/en-us_image1_0000001204776353.png)
7. Import [ForEach](../quick-start/arkts-rendering-control.md#loop-rendering) so that you do not need to create **FoodListItem** objects one by one.
```ts
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
.margin({ right: 24, left:32 })
}
}
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
List() {
ForEach(this.foodItems, item => {
ListItem() {
FoodListItem({ foodItem: item })
}
}, item => item.id.toString())
}
}
}
```
8. Add a title for the **FoodList**.
```
@Entry
@Component
struct FoodList {
private foodItems: FoodData[] = initializeOnStartup()
build() {
Column() {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text('Food List')
.fontSize(20)
.margin({ left: 20 })
}
.height('7%')
.backgroundColor('#FFf1f3f5')
List() {
ForEach(this.foodItems, item => {
ListItem() {
FoodListItem({ foodItem: item })
}
}, item => item.id.toString())
}
.height('93%')
}
}
}
```
![en-us_image_0000001169678922](figures/en-us_image_0000001169678922.png)
# Building a Food Data Model
In real-world development, it is impractical to describe all aspects of food in code, including the food name and nutrition facts. This is where the food data model comes into the picture. With the food data model, you can store and manage data in a unified manner.
![en-us_image_0000001267767897](figures/en-us_image_0000001267767897.png)
1. Create a folder named **model** and create a file named **FoodData.ets** therein.
![en-us_image_0000001223127760](figures/en-us_image_0000001223127760.png)
2. Define a food data storage model, **FoodData**, and an enum variable, **Category**. The **FoodData** class contains **id**, **name**, **category**, **image**, **calories**, **protein**, **fat**, **carbohydrates**, and **vitaminC** attributes.
The ArkTS programming language is an extension of the TS language and also supports the TS syntax.
```ts
enum Category {
Fruit,
Vegetable,
Nut,
Seafood,
Dessert
}
let NextId = 0;
class FoodData {
id: string;
name: string;
image: Resource;
category: Category;
calories: number;
protein: number;
fat: number;
carbohydrates: number;
vitaminC: number;
constructor(name: string, image: Resource, category: Category, calories: number, protein: number, fat: number, carbohydrates: number, vitaminC: number) {
this.id = `${ NextId++ }`;
this.name = name;
this.image = image;
this.category = category;
this.calories = calories;
this.protein = protein;
this.fat = fat;
this.carbohydrates = carbohydrates;
this.vitaminC = vitaminC;
}
}
```
3. Store food images in the **resources** > **base** > **media** directory. Use food names as the image names.
4. Create food resource data. Create **FoodDataModels.ets** in the **model** folder and declare the food composition array **FoodComposition** on the page. The following example creates two pieces of food data.
```ts
const FoodComposition: any[] = [
{ 'name': 'Tomato', 'image': $r('app.media.Tomato'), 'category': Category.Vegetable, 'calories': 17, 'protein': 0.9, 'fat': 0.2, 'carbohydrates': 3.9, 'vitaminC': 17.8 },
{ 'name': 'Walnut', 'image': $r('app.media.Walnut'), 'category': Category.Nut, 'calories': 654 , 'protein': 15, 'fat': 65, 'carbohydrates': 14, 'vitaminC': 1.3 }
]
```
In real-world development, you can customize more data resources when needed. Use [Lazy Loading](../quick-start/arkts-rendering-control.md#lazy-loading) to load data if a large amount of food data is involved.
5. Create the **initializeOnStartUp** method to initialize the **FoodData** array. Export the **FoodData** class from **FoodData.ets**, and import **FoodData** and **Category** in **FoodDataModels.ets**.
```ts
// FoodData.ets
export enum Category {
......
}
export class FoodData {
......
}
// FoodDataModels.ets
import { Category, FoodData } from './FoodData'
export function initializeOnStartup(): Array<FoodData> {
let FoodDataArray: Array<FoodData> = []
FoodComposition.forEach(item => {
FoodDataArray.push(new FoodData(item.name, item.image, item.category, item.calories, item.protein, item.fat, item.carbohydrates, item.vitaminC ));
})
return FoodDataArray;
}
```
The data resources for the diet application are now ready. You can continue to create a food category list by loading the data.
# Common Components
Components are the core of a UI page. Each component can provide visible and interactive functional units that are independent from each other. This is achieved by data and method encapsulation. You can use and reuse any component anywhere as needed.
The table below lists the components available in the declarative development paradigm.
| Component Type | Components |
| ---------------------------- | ---------------------------------------------------------- |
| [Basic components](../reference/arkui-ts/ts-basic-components-blank.md) | Blank, Button, Checkbox, CheckboxGroup, DataPanel, DatePicker, Divider, Gauge, Image, ImageAnimator, LoadingProgress, Marquee, Navigation, PatternLock, PluginComponent, Progress, QRCode, Radio, Rating, RemoteWindow, RichText, ScrollBar, Search, Select, Slider, Span, Stepper, StepperItem, Text, TextArea, TextClock, TextInput, TextPicker, TextTimer, TimePicker, Toggle, Web, XComponent |
| [Container components](../reference/arkui-ts/ts-container-ability-component.md) | AbilityComponent, AlphabetIndexer, Badge, Column, ColumnSplit, Counter, Flex, FlowItem, GridContainer, GridCol, GridRow, Grid, GridItem, List, ListItem, ListItemGroup, Navigator, Panel, Refresh, RelativeContainer, Row, RowSplit, Scroll, SideBarContainer, Stack, Swiper, Tabs, TabContent, WaterFlow |
| [Media components](../reference/arkui-ts/ts-media-components-video.md)| Video |
| [Drawing components](../reference/arkui-ts/ts-drawing-components-circle.md)| Circle, Ellipse, Line, Polyline, Polygon, Path, Rect, Shape |
| [Canvas components](../reference/arkui-ts/ts-components-canvas-canvas.md)| Canvas |
# Web Component Development
The **\<Web>** component can be used to display web pages. For details, see [Web](../reference/arkui-ts/ts-basic-components-web.md).
## Creating a Component
Create a **\<Web>** component in the .ets file under **main/ets/entryability/pages**. Then, in the created component, use **src** to specify the web page URI to be referenced, and bind a controller to the component to call the component APIs.
```ts
// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Web({ src: 'https://www.example.com', controller: this.controller })
}
}
}
```
## Setting Styles and Attributes
When using the **\<Web>** component, you must specify the styles and attributes. The sample code is as follows.
```ts
// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
fileAccess: boolean = true;
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Text('Hello world!')
.fontSize(20)
Web({ src: 'https://www.example.com', controller: this.controller })
// Set the height and padding.
.height(500)
.padding(20)
// Set the file access permission and script execution permission.
.fileAccess(this.fileAccess)
.javaScriptAccess(true)
Text('End')
.fontSize(20)
}
}
}
```
## Adding Events and Methods
Add the **onProgressChange** event for the **\<Web>** component, which is triggered when the loading progress changes and returns the progress value in its callback. Assign the progress value to the **\<Progress>** component to control the status of the component. When the progress reaches 100%, the **\<Progress>** component is hidden, and the web page loading is complete.
```ts
// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
@State progress: number = 0;
@State hideProgress: boolean = true;
fileAccess: boolean = true;
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Text('Hello world!')
.fontSize(20)
Progress({value: this.progress, total: 100})
.color('#0000ff')
.visibility(this.hideProgress ? Visibility.None : Visibility.Visible)
Web({ src: 'https://www.example.com', controller: this.controller })
.fileAccess(this.fileAccess)
.javaScriptAccess(true)
.height(500)
.padding(20)
// Add an onProgressChange event.
.onProgressChange(e => {
this.progress = e.newProgress;
// When the progress reaches 100%, the progress bar disappears.
if (this.progress === 100) {
this.hideProgress = true;
} else {
this.hideProgress = false;
}
})
Text('End')
.fontSize(20)
}
}
}
```
Add the **runJavaScript** method to the **onPageEnd** event. The **onPageEnd** event is triggered when the web page finishes loading. In this case, the **runJavaScript** method can be used to execute JavaScript scripts in the HTML file. In the sample code below, when the web page finishes loading, the **onPageEnd** event is triggered to call the **test** method in the HTML file and print information on the console.
```ts
// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
@State progress: number = 0;
@State hideProgress: boolean = true;
@State webResult: string = ''
fileAccess: boolean = true;
// Define the controller for the \<Web> component.
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Text('Hello world!')
.fontSize(20)
Progress({value: this.progress, total: 100})
.color('#0000ff')
.visibility(this.hideProgress ? Visibility.None : Visibility.Visible)
// Initialize the \<Web> component and bind it to the controller.
Web({ src: $rawfile('index.html'), controller: this.controller })
.fileAccess(this.fileAccess)
.javaScriptAccess(true)
.height(500)
.padding(20)
.onProgressChange(e => {
this.progress = e.newProgress;
if (this.progress === 100) {
this.hideProgress = true;
} else {
this.hideProgress = false;
}
})
.onPageEnd(e => {
// test() is defined in index.html.
try {
this.controller.runJavaScript(
'test()',
(error, result) => {
if (error) {
console.info(`run JavaScript error: ` + JSON.stringify(error))
return;
}
if (result) {
this.webResult = result
console.info(`The test() return value is: ${result}`)
}
});
console.info('url: ', e.url);
} catch (error) {
console.error(`ErrorCode: ${error.code}, Message: ${error.message}`);
}
})
Text('End')
.fontSize(20)
}
}
}
```
Create an HTML file in **main/resources/rawfile**.
```html
<!-- index.html -->
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<body>
Hello world!
</body>
<script type="text/javascript">
function test() {
console.info('Ark Web Component');
}
</script>
</html>
```
## Enabling Web Debugging
To debug web pages in the **\<Web>** component on a device, connect the device to a PC through the USB port, and then set the **webDebuggingAccess** attribute of the **\<Web>** component to **true** and enable port forwarding on the PC.
The configuration procedure is as follows:
1. Set the **webDebuggingAccess** attribute of the **\<Web>** component to **true**.
```ts
// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Web({ src: 'www.example.com', controller: this.controller })
.webDebuggingAccess(true) // true means to enable web debugging.
}
}
}
```
2. Enable port forwarding on the PC and add a mapping for TCP port 9222.
```ts
hdc fport tcp:9222 tcp:9222
```
You can run the following command to view the existing mapping table:
```ts
hdc fport ls
```
When you are done, you can debug a web page as follows: On the target device, open the **\<Web>** component in the application and access the web page to debug; on the PC, use the Chrome browser to access **http://localhost:9222** and start to debug the web page.
## Scenario Example
In this example, you'll implement a **\<Web>** component where videos can be played dynamically. Embed a video resource into an HTML page, and then use the **\<Web>** component controller to invoke the **onActive** and **onInactive** methods to activate and pause page rendering, respectively. Upon clicking of the **onInactive** button, the **\<Web>** component stops rendering and the video playback pauses. Upon clicking of the **onActive** button, the **\<Web>** component is activated and the video playback resumes.
```ts
// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Row() {
Button('onActive').onClick(() => {
console.info("Web Component onActive");
try {
this.controller.onActive();
} catch (error) {
console.error(`Errorcode: ${error.code}, Message: ${error.message}`);
}
})
Button('onInactive').onClick(() => {
console.info("Web Component onInactive");
try {
this.controller.onInactive();
} catch (error) {
console.error(`Errorcode: ${error.code}, Message: ${error.message}`);
}
})
}
Web({ src: $rawfile('index.html'), controller: this.controller })
.fileAccess(true)
}
}
}
```
```html
<!-- index.html -->
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<body>
<video width="320" height="240" controls="controls" muted="muted" autoplay="autoplay">
<!-- The test.mp4 video file is stored in main/resources/rawfile. -->
<source src="test.mp4" type="video/mp4">
</video>
</body>
</html>
```
# Creating a Simple Page
In this section, we will develop an infographic food details page, by building custom components through the container components **\<Stack>** and **\<Flex>** as well as basic components **\<Image>** and **\<Text>**.
Before creating a page, create an ArkTS project. For details, see [Creating an ArkTS Project in Stage Model](../quick-start/start-with-ets-stage.md#creating-an-arkts-project) or [Creating an ArkTS Project in FA Model](../quick-start/start-with-ets-fa.md#creating-an-arkts-project).
## Building the Stack Layout
1. Create a food name.
In the **index.ets** file, create a **\<Stack>** component, and place the **\<Text>** component in the braces of the **\<Stack>** component. When the **\<Stack>** component contains multiple child components, the latter child component overwrites the former one.
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack() {
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
![en-us_image_0000001214128687](figures/en-us_image_0000001214128687.png)
2. Display food pictures.
Create an **\<Image>** component and specify a URL for it. To display the **\<Text>** component above the **\<Image>** component, declare the **\<Image>** component first. Image resources are stored in the **rawfile** folder in **resources**. When referencing the resources in the **rawfile** folder, use the `$rawfile('filename')` format, where **filename** indicates the relative path of the file in the **rawfile** folder. `$rawfile` only allows the **\<Image>** component to reference image resources.
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack() {
Image($rawfile('Tomato.png'))
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
![en-us_image_0000001168410342](figures/en-us_image_0000001168410342.png)
3. Access images through resources.
In addition to specifying the image path, you can also use the media resource symbol **$r** to reference resources based on the resource qualifier rules in the **resources** folder. Right-click the **resources** folder, choose **New** > **Resource Directory** from the shortcut menu, and set **Resource Type** to **Media** (image resource).
Place **Tomato.png** in the **media** folder. You can then reference the application resources in the `$r('app.type.name')` format, which is `$r('app.media.Tomato')` in this example.
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack() {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
4. Set the width and height of the image, and set the **objectFit** attribute of the image to **ImageFit.Contain**, which means to keep the aspect ratio of the image to ensure that the image is completely displayed within the boundary.
If the image fills the entire screen, the possible causes are as follows:
1. The width and height of the image are not set.
2. The default attribute of **objectFit** of the image is **ImageFit.Cover**, that is, the image is zoomed in or zoomed out to fill the entire display boundary with the aspect ratio locked.
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack() {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
![en-us_image_0000001214210217](figures/en-us_image_0000001214210217.png)
5. Set the food image and name layout. Set the alignment mode of the stack to bottom alignment. By default, the stack is center aligned. Set **alignContent** to **Alignment.BottomStart**. Similar to **FontWeight**, **Alignment** is a built-in enumeration type provided by the framework.
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
}
}
```
![en-us_image_0000001168728872](figures/en-us_image_0000001168728872.png)
6. Adjust the left and bottom margin of the **\<Text>** component. **margin** is a shorthand attribute. You can specify the margins of the four edges in a unified manner or separately.
1. To set the margins of the four edges in a unified manner, use the **Margin(Length)** format. For example, **margin(20)** indicates that the margins of the top, right, bottom, and left edges are all 20.
2. To set the margins of the four edges separately, use the **{top?: Length, right?: Length, bottom?: Length, left?:Length}** format. For example, **margin({ left: 26, bottom: 17.4 })** indicates that the left margin is 26 and the bottom margin is 17.4.
```ts
@Entry
@Component
struct MyComponent {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
.margin({left: 26, bottom: 17.4})
}
}
}
```
![en-us_image_0000001213968747](figures/en-us_image_0000001213968747.png)
7. Adjust the structure between components and semanticize component names. Create the **FoodDetail** page entry component, create a **\<Column>** component in **FoodDetail**, and set the alignment to **alignItems(HorizontalAlign.Center)**. Change the name of the **MyComponent** component to **FoodImageDisplay**, which is a child component of the **FoodDetail** component.
The **\<Column>** component is a container whose child components are vertically arranged. It is in linear layout in essence. Therefore, only the alignment in the cross axis direction can be set.
```ts
@Component
struct FoodImageDisplay {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
.height(357)
}
}
@Entry
@Component
struct FoodDetail {
build() {
Column() {
FoodImageDisplay()
}
.alignItems(HorizontalAlign.Center)
}
}
```
## Building the Flex Layout
Use the **Flex** layout to build a food composition table. In this way, cell sizes are flexibly set based on the proportion, eliminating the need for width and height calculation.
1. Create a **ContentTable** component as a child component of the **FoodDetail** component.
```ts
@Component
struct FoodImageDisplay {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
}
}
@Component
struct ContentTable {
build() {
}
}
@Entry
@Component
struct FoodDetail {
build() {
Column() {
FoodImageDisplay()
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
```
2. Create a **\<Flex>** component to display two food composition categories in the tomato: **Calories** and **Nutrition**.
**Calories** contains information about calories. **Nutrition** contains information about protein, fat, carbohydrates, and vitamin C.
Create the **Calories** class. Create a **\<Flex>** component and set its height to **280**, and the top, right, and left margins to **30**. The **Flex** component contains three **\<Text>** child components, which represent the category name (**Calories**), content name (**Calories**), and contain value (**17 kcal**), respectively. By default, child components in the **Flex** component are arranged horizontally.
In the following example, code of **FoodImageDisplay** is omitted, and only code of **ContentTable** is provided.
```ts
@Component
struct ContentTable {
build() {
Flex() {
Text('Calories')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
Text('Calories')
.fontSize(17.4)
Text('17kcal')
.fontSize(17.4)
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
build() {
Column() {
FoodImageDisplay()
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
```
![en-us_image_0000001169759552](figures/en-us_image_0000001169759552.png)
3. Adjust the layout and set the proportion (**layoutWeight**) of each part. Set **layoutWeight** of the category name to **1**, and **layoutWeight** of the content name and content value to **2**. The content name and content value are in a same **Flex**, and the content name occupies all remaining space **flexGrow(1)**.
```ts
@Component
struct FoodImageDisplay {
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.Tomato'))
.objectFit(ImageFit.Contain)
.height(357)
Text('Tomato')
.fontSize(26)
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
}
}
@Component
struct ContentTable {
build() {
Flex() {
Text('Calories')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text('Calories')
.fontSize(17.4)
.flexGrow(1)
Text('17kcal')
.fontSize(17.4)
}
.layoutWeight(2)
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
build() {
Column() {
FoodImageDisplay()
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
```
![en-us_image_0000001215079443](figures/en-us_image_0000001215079443.png)
4. Create the **Nutrient** class in a similar process. **Nutrition** consists of four parts: **Protein**, **Fat**, **Carbohydrates**, and **VitaminC**. The names of the last three parts are omitted in the table and represented by spaces.
Set **FlexDirection.Column**, **FlexAlign.SpaceBetween**, and **ItemAlign.Start**.
```ts
@Component
struct ContentTable {
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
Flex() {
Text('Calories')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text('Calories')
.fontSize(17.4)
.flexGrow(1)
Text('17kcal')
.fontSize(17.4)
}
.layoutWeight(2)
}
Flex() {
Text('Nutrition')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text('Protein')
.fontSize(17.4)
.flexGrow(1)
Text('0.9g')
.fontSize(17.4)
}
.layoutWeight(2)
}
Flex() {
Text(' ')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text('Fat')
.fontSize(17.4)
.flexGrow(1)
Text('0.2g')
.fontSize(17.4)
}
.layoutWeight(2)
}
Flex() {
Text(' ')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text('Carbohydrates')
.fontSize(17.4)
.flexGrow(1)
Text('3.9g')
.fontSize(17.4)
}
.layoutWeight(2)
}
Flex() {
Text(' ')
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text('vitaminC')
.fontSize(17.4)
.flexGrow(1)
Text('17.8mg')
.fontSize(17.4)
}
.layoutWeight(2)
}
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
build() {
Column() {
FoodImageDisplay()
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
```
![en-us_image_0000001215199399](figures/en-us_image_0000001215199399.png)
5. Use the custom constructor **\@Builder** to simplify the code. It can be found that the food groups in each food composition table are actually of the same UI structure.
![en-us_image_0000001169599582](figures/en-us_image_0000001169599582.png)
Currently, all food groups are declared, resulting in code duplication and redundancy. You can use **\@Builder** to build a custom method and abstract the same UI structure declaration. The **\@Builder** decorated method and the **build** method for the **@Component** decorated component are used to declare some UI rendering structures and comply with the same ArkTS syntax. You can define one or more methods decorated by **\@Builder**, but a component decorated by **@Component** can have only one **build** method.
Declare the **IngredientItem** method decorated by **\@Builder** in **ContentTable** to declare the UI descriptions for the category name, content name, and content value.
```ts
@Component
struct ContentTable {
@Builder IngredientItem(title:string, name: string, value: string) {
Flex() {
Text(title)
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex({ alignItems: ItemAlign.Center }) {
Text(name)
.fontSize(17.4)
.flexGrow(1)
Text(value)
.fontSize(17.4)
}
.layoutWeight(2)
}
}
}
```
When the **IngredientItem** API is called in the **build** method of **ContentTable**, **this** needs to be used to invoke the method in the scope of the component to distinguish the global method call.
```ts
@Component
struct ContentTable {
......
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
this.IngredientItem('Calories', 'Calories', '17kcal')
this.IngredientItem('Nutrition', 'Protein', '0.9g')
this.IngredientItem('', 'Fat', '0.2g')
this.IngredientItem('', 'Carbohydrates', '3.9g')
this.IngredientItem('', 'VitaminC', '17.8mg')
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
```
The overall code of the **ContentTable** component is as follows:
```ts
@Component
struct ContentTable {
@Builder IngredientItem(title:string, name: string, value: string) {
Flex() {
Text(title)
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text(name)
.fontSize(17.4)
.flexGrow(1)
Text(value)
.fontSize(17.4)
}
.layoutWeight(2)
}
}
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
this.IngredientItem('Calories', 'Calories', '17kcal')
this.IngredientItem('Nutrition', 'Protein', '0.9g')
this.IngredientItem('', 'Fat', '0.2g')
this.IngredientItem('', 'Carbohydrates', '3.9g')
this.IngredientItem('', 'VitaminC', '17.8mg')
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
build() {
Column() {
FoodImageDisplay()
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
```
![en-us_image_0000001215199399](figures/en-us_image_0000001215199399.png)
You've learned how to build a simple food details page. Read on to learn how to define the page layout and connection.
# Custom Component Lifecycle Callbacks
The lifecycle callbacks of a custom component are used to notify users of the lifecycle of the component. These callbacks are private and are invoked by the development framework at a specified time at runtime. They cannot be manually invoked from applications.
> **NOTE**
>
> - Promise and asynchronous callback functions can be used in lifecycle functions, for example, network resource getters and timer setters.
>
> - Do not use **async await** in lifecycle functions.
## aboutToAppear
aboutToAppear?(): void
Invoked after a new instance of the custom component is created and before its **build** function is executed. You can change state variables in the **aboutToAppear** function. The change will take effect when you execute the **build** function next time.
Since API version 9, this API is supported in ArkTS widgets.
## aboutToDisappear
aboutToDisappear?(): void
Invoked before the destructor of the custom component is consumed. Do not change state variables in the **aboutToDisappear** function as doing this can cause unexpected errors. For example, the modification of the **@Link** decorated variable may cause unstable application running.
Since API version 9, this API is supported in ArkTS widgets.
**Example 1:**
```ts
// xxx.ets
@Entry
@Component
struct CountDownTimerComponent {
@State countDownFrom: number = 10
private timerId: number = -1
aboutToAppear(): void {
this.timerId = setInterval(() => {
if (this.countDownFrom <= 1) {
clearTimeout(this.timerId) // Clear the timer when the value of countDownFrom is less than or equal to 1.
}
this.countDownFrom -= 1
}, 1000) // The value of countDownFrom decreases by 1 every second.
}
aboutToDisappear(): void {
if (this.timerId > 0) {
clearTimeout(this.timerId)
this.timerId = -1
}
}
build() {
Column() {
Text(`${this.countDownFrom} sec left`)
.fontSize(30)
.margin(30)
}.width('100%')
}
}
```
![aboutToAppear](figures/aboutToAppear.gif)
The example above shows that lifecycle functions are critical for **CountDownTimerComponent** to manage its timer resources. Similar functions include loading resources asynchronously from the network.
## onPageShow
onPageShow?(): void
Invoked each time when a page is displayed. This callback is used in the routing process or scenarios where the application is switched to the foreground or background. Only the custom components decorated by **@Entry** take effect.
## onPageHide
onPageHide?(): void
Invoked each time when a page is hidden. This callback is used in the routing process or scenarios where the application is switched to the foreground or background. Only the custom components decorated by **@Entry** take effect.
## onBackPress
onBackPress?(): void
Invoked when a user clicks the back button. This callback takes effect only for the custom components decorated by **@Entry**.
**Example 2:**
```ts
// xxx.ets
@Entry
@Component
struct IndexComponent {
@State textColor: Color = Color.Black
onPageShow() {
this.textColor = Color.Blue
console.info('IndexComponent onPageShow')
}
onPageHide() {
this.textColor = Color.Transparent
console.info('IndexComponent onPageHide')
}
onBackPress() {
this.textColor = Color.Red
console.info('IndexComponent onBackPress')
}
build() {
Column() {
Text('Hello World')
.fontColor(this.textColor)
.fontSize(30)
.margin(30)
}.width('100%')
}
}
```
![lifecycle](figures/lifecycle.PNG)
## onLayout<sup>9+</sup>
onLayout?(children: Array\<LayoutChild\>, constraint: ConstraintSizeOptions): void
Invoked when the custom component lays out its child components. Through this callback the component receives its child component layout information and size constraint from the framework. The state variable cannot be changed in the **onLayout** function.
This API is supported in ArkTS widgets.
**Parameters**
| Name | Type | Description |
| ---------- | -------------------------------------------------------------------------------- | ---------------------- |
| children | Array\<[LayoutChild](#layoutchild9)\> | Child component layout information. |
| constraint | [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions) | Size constraint information of the parent component.|
## onMeasure<sup>9+</sup>
onMeasure?(children: Array\<LayoutChild\>, constraint: ConstraintSizeOptions): void
Invoked when the custom component needs to determine its size. Through this callback the component receives its child component layout information and size constraint from the framework. The state variable cannot be changed in the onMeasure function.
This API is supported in ArkTS widgets.
**Parameters**
| Name | Type | Description |
| ---------- | -------------------------------------------------------------------------------- | ---------------------- |
| children | Array\<[LayoutChild](#layoutchild9)\> | Child component layout information. |
| constraint | [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions) | Size constraint information of the parent component.|
## LayoutChild<sup>9+</sup>
Provides the child component layout information.
This API is supported in ArkTS widgets.
| Name | Type | Description |
| ---------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| name | string | Name of the child component. |
| id | string | ID of the child component. |
| constraint | [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions) | Size constraint of the child component. |
| borderInfo | [LayoutBorderInfo](#layoutborderinfo9) | Border information of the child component. |
| position | [Position](../reference/arkui-ts/ts-types.md#position) | Position coordinates of the child component. |
| measure | (childConstraint: [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions)) => void | Method called to apply the size constraint to the child component.|
| layout | (LayoutInfo: [LayoutInfo](#layoutinfo9)) => void | Method called to apply the layout information to the child component.|
## LayoutBorderInfo<sup>9+</sup>
Provides the border information of the child component.
This API is supported in ArkTS widgets.
| Name | Type | Description |
| ----------- | ---------------------------------------------------------- | ---------------------------------------------- |
| borderWidth | [EdgeWidths](../reference/arkui-ts/ts-types.md#edgewidths) | Edge widths in different directions of the child component.|
| margin | [Margin](../reference/arkui-ts/ts-types.md#margin) | Margin areas in different directions of the child component. |
| padding | [Padding](../reference/arkui-ts/ts-types.md#padding) | Padding areas in different directions of the child component. |
## LayoutInfo<sup>9+</sup>
Provides the layout information of the child component.
This API is supported in ArkTS widgets.
| Name | Type | Description |
| ---------- | -------------------------------------------------------------------------------- | ---------------- |
| position | [Position](../reference/arkui-ts/ts-types.md#position) | Position coordinates of the child component.|
| constraint | [ConstraintSizeOptions](../reference/arkui-ts/ts-types.md#constraintsizeoptions) | Size constraint of the child component.|
Example 3:
```ts
// xxx.ets
@Entry
@Component
struct Index {
build() {
Column() {
CustomLayout() {
ForEach([1, 2, 3], (index) => {
Text('Sub' + index)
.fontSize(30)
.borderWidth(2)
})
}
}
}
}
@Component
struct CustomLayout {
@BuilderParam builder: () => {}
onLayout(children: Array<LayoutChild>, constraint: ConstraintSizeOptions) {
let pos = 0
children.forEach((child) => {
child.layout({ position: { x: pos, y: pos }, constraint: constraint })
pos += 100
})
}
onMeasure(children: Array<LayoutChild>, constraint: ConstraintSizeOptions) {
let size = 100
children.forEach((child) => {
child.measure({ minHeight: size, minWidth: size, maxWidth: size, maxHeight: size })
size += 50
})
}
build() {
this.builder()
}
}
```
![customlayout](figures/customLayout.png)
# Declarative UI Development Guidelines
## Overview
| Task | Description | Related Resources |
| ----------- | ---------------------------------------- | ---------------------------------------- |
| Set up the development environment. | Understand the project structure of the declarative UI.<br>Learn the resource categories and how to access resources. | [OpenHarmony Project Overview](https://developer.harmonyos.com/en/docs/documentation/doc-guides/ohos-project-overview-0000001218440650)<br>[Resource Categories and Access](../quick-start/resource-categories-and-access.md)|
| Learn ArkTS. | As its name implies, ArkTS is a superset of TypeScript. It is the preferred, primary programming language for application development in OpenHarmony.| [Learning ArkTS](../quick-start/arkts-get-started.md)|
| Develop a page. | Select an appropriate layout based on the use case.<br>Add built-in components and set the component style based on the page content to present.<br>Update and diversify the page content.| [Creating a Page](#creating-a-page)<br> [Common Layout Development](ui-ts-layout-linear.md)<br> [Common Components](ui-ts-components-intro.md)<br>[Setting the Component Style](#setting-the-component-styles)<br>[Updating Page Content](#updating-page-content)|
| (Optional) Diversify the page content. | Leverage the drawing and animation features to effectively increase user engagement. | [Drawing Components](../reference/arkui-ts/ts-drawing-components-circle.md)<br>[Canvas Components](../reference/arkui-ts/ts-components-canvas-canvas.md)<br>[Animation](../reference/arkui-ts/ts-animatorproperty.md)|
| (Optional) Implement page redirection.| Use page routing to implement redirection between pages. | [Page Routing](../reference/apis/js-apis-router.md)|
| (Optional) Improve performance. | Learn how you can improve your implementation, thereby avoiding possible performance drop. | [Recommendations for Improving Performance](ui-ts-performance-improvement-recommendation.md)|
## Creating a Page
Select a layout to create a page and add basic built-in components to the page. In this example, the [flex layout](ui-ts-layout-flex.md) is used, and the **\<Text>** component is centered horizontally and vertically on the page.
```ts
// xxx.ets
@Entry
@Component
struct MyComponent {
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text('Hello')
}
.width('100%')
.height('100%')
}
}
```
## Setting the Component Style
If a built-in component does not have attribute methods set, it takes the default style. To change the style of a component and thereby how it looks on the UI, modify the settings of styles, including component-specific styles and [universal styles](../reference/arkui-ts/ts-universal-attributes-size.md).
1. Change the display content of the **\<Text>** component to **Tomato** by modifying its constructor parameters.
2. Set the **fontSize** attribute to **26** and the **fontWeight** attribute to **500**.
```ts
// xxx.ets
@Entry
@Component
struct MyComponent {
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text('Tomato')
.fontSize(26)
.fontWeight(500)
}
.width('100%')
.height('100%')
}
}
```
![en-us_image_0000001168888224](figures/en-us_image_0000001168888224.png)
## Updating Page Content
You can update page content by updating component status. Below is a simple example.
> **NOTE**
>
> Before updating the status of a component, initialize its member variables. The member variables of a custom component can be initialized in either of the following methods: [local initialization](../quick-start/arkts-restrictions-and-extensions.md#initialization-and-restrictions-of-custom-components-member-variables) or [initialization using constructor parameters](../quick-start/arkts-restrictions-and-extensions.md#initialization-and-restrictions-of-custom-components-member-variables). Select the appropriate method based on the decorator used by the variable.
**Example**
```ts
// xxx.ets
@Entry
@Component
struct ParentComp {
@State isCountDown: boolean = true
build() {
Column() {
Text(this.isCountDown ? 'Count Down' : 'Stopwatch').fontSize(20).margin(20)
if (this.isCountDown) {
// The image resources are stored in the media directory.
Image($r("app.media.countdown")).width(120).height(120)
TimerComponent({ counter: 10, changePerSec: -1, showInColor: Color.Red })
} else {
// The image resources are stored in the media directory.
Image($r("app.media.stopwatch")).width(120).height(120)
TimerComponent({ counter: 0, changePerSec: +1, showInColor: Color.Black })
}
Button(this.isCountDown ? 'Switch to Stopwatch' : 'Switch to Count Down')
.onClick(() => {
this.isCountDown = !this.isCountDown
})
}.width('100%')
}
}
// Custom timer/countdown component.
@Component
struct TimerComponent {
@State counter: number = 0
private changePerSec: number = -1
private showInColor: Color = Color.Black
private timerId: number = -1
build() {
Text(`${this.counter}sec`)
.fontColor(this.showInColor)
.fontSize(20)
.margin(20)
}
aboutToAppear() {
this.timerId = setInterval(() => {
this.counter += this.changePerSec
}, 1000)
}
aboutToDisappear() {
if (this.timerId > 0) {
clearTimeout(this.timerId)
this.timerId = -1
}
}
}
```
![component](figures/component.gif)
**Initial creation and rendering:**
1. Create the parent component **ParentComp**.
2. Locally initialize the state variable **isCountDown** of **ParentComp**.
3. Execute the **build** function of **ParentComp**.
4. Create a **\<Column>** component.
1. Create a **\<Text>** component, set the text content, and add the **\<Text>** component instance to the **\<Column>** component.
2. With the **if** statement, create a component under the **true** condition.
1. Create an **\<Image>** component and set the image source address.
2. Create a **TimerComponent** using the given constructor.
3. Create a **\<Button>** component and set the corresponding content.
**Status update:**
When the user clicks a button:
1. The value of the **isCountDown** state variable of **ParentComp** is changed to **false**.
2. The **build** function of **ParentComp** is executed.
3. The **\<Column>** component is reused and reinitialized.
4. The child components of **\<Column>** reuse and reinitialize the objects in the memory.
1. The **\<Text>** component is reused after being re-initialized using the new text content.
2. With the **if** statement, the components under the **false** condition are used.
1. The component instances created under the **true** condition are destroyed.
2. Components under the **false** condition are created.
3. The **\<Button>** component is reused with the new image source.
# Using the Drawing Feature
The drawing feature is provided by the drawing component in the framework, with support for the SVG standard drawing commands.
This section describes how to use the drawing component to draw food composition labels (basic geometric shapes) and apply logos (custom graphics) on the details page.
## Drawing Basic Geometric Shapes
The drawing component encapsulates some common basic geometric shapes, such as rectangles, circles, and ellipses, eliminating the need to calculate routes.
In the food component table on the **FoodDetail** page, each food group is labeled with a round icon.
1. Create a **Circle** component and add a round icon before each food group as a label. Set the circle diameter to 6vp. Modify the **IngredientItem** method in the **ContentTable** component on the **FoodDetail** page by adding **Circle** before the food group name.
```ts
// FoodDetail.ets
@Component
struct ContentTable {
private foodItem: FoodData
@Builder IngredientItem(title:string, colorValue: string, name: string, value: string) {
Flex() {
Text(title)
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex({ alignItems: ItemAlign.Center }) {
Circle({width: 6, height: 6})
.margin({right: 12})
.fill(colorValue)
Text(name)
.fontSize(17.4)
.flexGrow(1)
Text(value)
.fontSize(17.4)
}
.layoutWeight(2)
}
}
build() {
......
}
}
```
2. To use different label colors for different food groups, call **IngredientItem** in the **build** method to fill the circles with different colors.
```ts
// FoodDetail.ets
@Component
struct ContentTable {
private foodItem: FoodData
@Builder IngredientItem(title:string, colorValue: string, name: string, value: string) {
Flex() {
Text(title)
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex({ alignItems: ItemAlign.Center }) {
Circle({width: 6, height: 6})
.margin({right: 12})
.fill(colorValue)
Text(name)
.fontSize(17.4)
.flexGrow(1)
Text(value)
.fontSize(17.4)
}
.layoutWeight(2)
}
}
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
this.IngredientItem('Calories', '#FFf54040', 'Calories', this.foodItem.calories + 'kcal')
this.IngredientItem('Nutrition', '#FFcccccc', 'Protein', this.foodItem.protein + 'g')
this.IngredientItem(' ', '#FFf5d640', 'Fat', this.foodItem.fat + 'g')
this.IngredientItem(' ', '#FF9e9eff', 'Carbohydrates', this.foodItem.carbohydrates + 'g')
this.IngredientItem(' ', '#FF53f540', 'VitaminC', this.foodItem.vitaminC + 'mg')
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
```
![drawing-feature](figures/drawing-feature.png)
## Drawing Custom Graphs
In addition to drawing basic geometric shapes, you can also use the **\<Path>** component to draw a custom path. The following describes how to draw an application logo.
1. Create the page **Logo.ets** in the **pages** folder.
![drawing-feature1](figures/drawing-feature1.png)
2. Delete the template code from **Logo.ets** and create a logo component.
```ts
@Entry
@Component
struct Logo {
build() {
}
}
```
3. Create a **\<Flex>** component as the root node, set the width and height to 100%, set the alignment mode on the main axis and cross axis to **Center**, and create a **\<Shape>** component as a child component of **\<Flex>**.
The **\<Shape>** component is the parent component of all drawing components. To combine multiple drawing components into a whole, create a **\<Shape>** as their parent component.
Draw a logo with the size of 630 px x 630 px. The declarative development paradigm supports multiple length units. In the preceding sections, **number** is used as a parameter, which means that the default length unit – virtual pixel (vp) is used. The virtual pixel count is related to the device resolution and screen density. For example, if the device resolution is 1176 x 2400 and the reference screen resolution is 3, with the formula of vp = px/resolution, the width of the device screen is 392 vp.
The drawing components follow the SVG standard and use px as the unit by default. Therefore, to facilitate consistency, px is also used as the unit of the logo. The declarative development framework also supports the px unit, with the input parameter of the string type. For example, to set the width to 630px (210vp), use **width('*630px*')** or **width(*210*)**.
```ts
@Entry
@Component
struct Logo {
build() {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
}
.height('630px')
.width('630px')
}
.width('100%')
.height('100%')
}
}
```
4. Fill the page with gradient colors. Set the gradient mode to linear gradient, the offset angle to 180 degrees, and the color stops to #BDE895 -> 95DE7F -> #7AB967 in the ranges of [0, 0.1], (0.1, 0.6], and (0.6, 1], respectively.
```ts
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
```
![drawing-feature2](figures/drawing-feature2.png)
```ts
@Entry
@Component
struct Logo {
build() {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
}
.height('630px')
.width('630px')
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![drawing-feature3](figures/drawing-feature3.png)
5. Set the drawing command to draw the first path.
```ts
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
```
The **Path** drawing commands comply with the SVG standard. The preceding command can be divided into the following:
```ts
M162 128.7
```
Start a new subpath at point (162, 128.7).
```ts
a222 222 0 0 1 100.8 374.4
```
Draw an elliptical arc, setting **rx** and **ry** to **222**, **x-axis-rotation** to **0**, **large-arc-flag** to **0**, and **sweep-flag** to **1** (which means to draw the arc counterclockwise). The lowercase **a** indicates a relative position. In this example, the end point coordinate is (162 + 100.8 = 262.8, 128.7 + 374.4 = 503.1).
```ts
H198
```
Draw a horizontal line from point (262.8, 503.1) to point (198, 503.1).
```ts
a36 36 0 0 3 -36 -36
```
Draw a circular arc. The input parameters are the same as those in drawing an elliptical arc. The end point is (198 - 36 = 162, 503.1 - 36 = 467.1).
```ts
V128.7
```
Draw a vertical line from point (162, 467.1) to point (162, 128.7).
```ts
z
```
Close the current subpath.
![drawing-feature4](figures/drawing-feature4.png)
In the sample code, the fill color is white and the stroke is transparent.
```ts
.fill(Color.White)
.stroke(Color.Transparent)
```
```ts
@Entry
@Component
struct Logo {
build() {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
.fill(Color.White)
.stroke(Color.Transparent)
}
.height('630px')
.width('630px')
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![drawing-feature5](figures/drawing-feature5.png)
6. Draw the second path. Fill the shape background with gradient colors. Because the gradient colors fill the entire box, you must use the **clip** method with the **Shape** as the input parameter so that the path is clipped.
```ts
Path()
.commands('M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z')
.fill('none')
.stroke(Corlor.Transparent)
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands('M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'))
```
The path drawing command is long. You can call the command by using it as a member variable of the component through **this**.
```ts
@Entry
@Component
struct Logo {
private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
build() {
......
Path()
.commands(this.pathCommands1)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands(this.pathCommands1))
......
}
}
```
![drawing-feature6](figures/drawing-feature6.png)
7. Draw the third path.
```ts
@Entry
@Component
struct Logo {
private pathCommands1:string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
private pathCommands2:string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z'
build() {
Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
.fill(Color.White)
.stroke(Color.Transparent)
Path()
.commands(this.pathCommands1)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands(this.pathCommands1))
Path()
.commands(this.pathCommands2)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 50,
colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]]
})
.clip(new Path().commands(this.pathCommands2))
}
.height('630px')
.width('630px')
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![drawing-feature7](figures/drawing-feature7.png)
The drawing of the application logo is now completed. This logo combines three **\<Path>** components through SVG commands and takes the shape of an artistic leaf.
8. Add the application title and slogan.
```ts
@Entry
@Component
struct Logo {
private pathCommands1: string = 'M319.5 128.1 c103.5 0 187.5 84 187.5 187.5 v15 a172.5 172.5 0 0 3 -172.5 172.5 H198 a36 36 0 0 3 -13.8 -1 207 207 0 0 0 87 -372 h48.3 z'
private pathCommands2: string = 'M270.6 128.1 h48.6 c51.6 0 98.4 21 132.3 54.6 a411 411 0 0 3 -45.6 123 c-25.2 45.6 -56.4 84 -87.6 110.4 a206.1 206.1 0 0 0 -47.7 -288 z'
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Shape() {
Path()
.commands('M162 128.7 a222 222 0 0 1 100.8 374.4 H198 a36 36 0 0 3 -36 -36')
.fill(Color.White)
.stroke(Color.Transparent)
Path()
.commands(this.pathCommands1)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 30,
colors: [["#C4FFA0", 0], ["#ffffff", 1]]
})
.clip(new Path().commands(this.pathCommands1))
Path()
.commands(this.pathCommands2)
.fill('none')
.stroke(Color.Transparent)
.linearGradient(
{
angle: 50,
colors: [['#8CC36A', 0.1], ["#B3EB90", 0.4], ["#ffffff", 0.7]]
})
.clip(new Path().commands(this.pathCommands2))
}
.height('630px')
.width('630px')
Text('Healthy Diet')
.fontSize(26)
.fontColor(Color.White)
.margin({ top: 300 })
Text('Healthy life comes from a balanced diet')
.fontSize(17)
.fontColor(Color.White)
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.linearGradient(
{
angle: 180,
colors: [['#BDE895', 0.1], ["#95DE7F", 0.6], ["#7AB967", 1]]
})
}
}
```
![drawing-feature8](figures/drawing-feature8.png)
# Flex Layout
The flex layout is the most flexible layout used in adaptive layout. It provides simple and powerful tools for distributing space and aligning items.
- Container: [\<Flex>](../reference/arkui-ts/ts-container-flex.md) component, used to set layout attributes.
- Child component: any of the child items in the **\<Flex>** component.
- Main axis: axis along which items are placed in the **\<Flex>** component. Child components are laid out along the main axis by default. The start point of the main axis is referred to as main-start, and the end point is referred to as main-end.
- Cross axis: axis that runs perpendicular to the main axis. The start point of the cross axis is referred to as cross-start, and the end point is referred to as cross-end.
The following uses **FlexDirection.Row** as an example.
![](figures/flex.png)
## Container Attributes
Create a flex layout through the **Flex** API provided by the **\<Flex>** component. The sample code is as follows:
`Flex(options?: { direction?: FlexDirection, wrap?: FlexWrap, justifyContent?: FlexAlign, alignItems?: ItemAlign, alignContent?: FlexAlign })`
### Flex Layout Direction
The **direction** parameter sets the direction in which items are laid out in the **\<Flex>** component and thereby defines the main axis. Available values are as follows:
![](figures/direction.png)
- **FlexDirection.Row** (default value): The main axis runs along the row horizontally, and the child components are laid out from the start edge of the main axis.
```ts
Flex({ direction: FlexDirection.Row }) {
Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001218579606](figures/en-us_image_0000001218579606.png)
- **FlexDirection.RowReverse**: The main axis runs along the row horizontally, and the child components are laid out from the end edge of the main axis, in a direction opposite to **FlexDirection.Row**.
```ts
Flex({ direction: FlexDirection.RowReverse }) {
Text('1').width('33%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(50).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001218739566](figures/en-us_image_0000001218739566.png)
- **FlexDirection.Column**: The main axis runs along the column vertically, and the child components are laid out from the start edge of the main axis.
```ts
Flex({ direction: FlexDirection.Column }) {
Text('1').width('100%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('100%').height(50).backgroundColor(0xD2B48C)
Text('3').width('100%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001263019457](figures/en-us_image_0000001263019457.png)
- **FlexDirection.ColumnReverse**: The main axis runs along the column vertically, and the child components are laid out from the end edge of the main axis, in a direction opposite to **FlexDirection.Column**.
```ts
Flex({ direction: FlexDirection.ColumnReverse }) {
Text('1').width('100%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('100%').height(50).backgroundColor(0xD2B48C)
Text('3').width('100%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001263339459](figures/en-us_image_0000001263339459.png)
### Wrapping in the Flex Layout
By default, child components are laid out on a single line (also called an axis) in the flex container. You can use the **wrap** parameter to set whether child components can wrap onto multiple lines. Available values are as follows:
- **FlexWrap.NoWrap** (default value): Child components are laid out on a single line. This may cause the child components to shrink to fit the container when the total width of the child components is greater than the width of the container.
```ts
Flex({ wrap: FlexWrap.NoWrap }) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001263139409](figures/en-us_image_0000001263139409.png)
- **FlexWrap.Wrap**: Child components can break into multiple lines along the main axis.
```ts
Flex({ wrap: FlexWrap.Wrap }) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xD2B48C)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001218419614](figures/en-us_image_0000001218419614.png)
- **FlexWrap.WrapReverse**: Child components can break into multiple lines in the reverse direction to the main axis.
```ts
Flex({ wrap: FlexWrap.WrapReverse}) {
Text('1').width('50%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('50%').height(50).backgroundColor(0xD2B48C)
Text('3').width('50%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001263259399](figures/en-us_image_0000001263259399.png)
### Alignment in the Flex Layout
#### Alignment on the Main Axis
Use the **justifyContent** parameter to set alignment of items on the main axis. The available options are as follows:
![](figures/justifyContent.png)
- **FlexAlign.Start** (default value): The items are aligned with each other toward the start edge of the container along the main axis.
```ts
Flex({ justifyContent: FlexAlign.Start }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001218259634](figures/mainStart.png)
- **FlexAlign.Center**: The items are aligned with each other toward the center of the container along the main axis.
```ts
Flex({ justifyContent: FlexAlign.Center }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001218579608](figures/mainCenter.png)
- **FlexAlign.End**: The items are aligned with each other toward the end edge of the container along the main axis.
```ts
Flex({ justifyContent: FlexAlign.End }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001218739568](figures/mainEnd.png)
- **FlexAlign.SpaceBetween**: The items are evenly distributed within the container along the main axis. The first and last items are aligned with the edges of the container.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001263019461](figures/mainSpacebetween.png)
- **FlexAlign.SpaceAround**: The items are evenly distributed in the container along the main axis. The space between the first item and main-start, and that between the last item and main-end are both half of the space between two adjacent items.
```ts
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001263339461](figures/mainSpacearound.png)
- **FlexAlign.SpaceEvenly**: The items are equally distributed along the main axis. The space between the first item and main-start, the space between the last item and main-end, and the space between two adjacent items are the same.
```ts
Flex({ justifyContent: FlexAlign.SpaceEvenly }) {
Text('1').width('20%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('20%').height(50).backgroundColor(0xD2B48C)
Text('3').width('20%').height(50).backgroundColor(0xF5DEB3)
}
.width('90%')
.padding({ top: 10, bottom: 10 })
.backgroundColor(0xAFEEEE)
```
![zh-cn_image_0000001263139411](figures/mainSpaceevenly.png)
#### Alignment on the Cross Axis
Alignment on the cross axis can be set for both the container and child components, with that set for child components having a higher priority.
##### Setting Alignment on the Cross Axis for the Container
Use the **alignItems** parameter of the **\<Flex>** component to set alignment of items on the cross axis. The available options are as follows:
- **ItemAlign.Auto**: The items are automatically aligned in the flex container.
```ts
Flex({ alignItems: ItemAlign.Auto }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({width: '90%', height: 80})
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001218419616](figures/en-us_image_0000001218419616.png)
- **ItemAlign.Start**: The items are aligned with the start edge of the container along the cross axis.
```ts
Flex({ alignItems: ItemAlign.Start }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({width: '90%', height: 80})
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001263259401](figures/en-us_image_0000001263259401.png)
- **ItemAlign.Start**: The items are aligned with the center of the container along the cross axis.
```ts
Flex({ alignItems: ItemAlign.Center }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({width: '90%', height: 80})
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001218259636](figures/en-us_image_0000001218259636.png)
- **ItemAlign.End**: The items are aligned with the end edge of the container along the cross axis.
```ts
Flex({ alignItems: ItemAlign.End }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({width: '90%', height: 80})
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001218579610](figures/en-us_image_0000001218579610.png)
- **ItemAlign.Stretch**: The items are stretched along the cross axis. If no constraints are set, the items are stretched to fill the size of the container on the cross axis.
```ts
Flex({ alignItems: ItemAlign.Stretch }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({width: '90%', height: 80})
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001218739570](figures/itemalignstretch.png)
- **ItemAlign.Baseline**: The items are aligned at the baseline of the cross axis.
```ts
Flex({ alignItems: ItemAlign.Baseline }) {
Text('1').width('33%').height(30).backgroundColor(0xF5DEB3)
Text('2').width('33%').height(40).backgroundColor(0xD2B48C)
Text('3').width('33%').height(50).backgroundColor(0xF5DEB3)
}
.size({width: '90%', height: 80})
.padding(10)
.backgroundColor(0xAFEEEE)
```
![en-us_image_0000001263019463](figures/en-us_image_0000001263019463.png)
##### Setting Alignment on the Cross Axis for Child Components
Use the **alignSelf** attribute of child components to set their alignment in the container on the cross axis. The settings overwrite the default **alignItems** settings in the flex layout container. The sample code is as follows:
```ts
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) { // The items are aligned with the center of the container.
Text('alignSelf Start').width('25%').height(80)
.alignSelf(ItemAlign.Start)
.backgroundColor(0xF5DEB3)
Text('alignSelf Baseline')
.alignSelf(ItemAlign.Baseline)
.width('25%')
.height(80)
.backgroundColor(0xD2B48C)
Text('alignSelf Baseline').width('25%').height(100)
.backgroundColor(0xF5DEB3)
.alignSelf(ItemAlign.Baseline)
Text('no alignSelf').width('25%').height(100)
.backgroundColor(0xD2B48C)
Text('no alignSelf').width('25%').height(100)
.backgroundColor(0xF5DEB3)
}.width('90%').height(220).backgroundColor(0xAFEEEE)
```
![alignself](figures/alignself.png)
In the preceding example, both **alignItems** of the **\<Flex>** component and the **alignSelf** attribute of the child component are set. In this case, the **alignSelf** settings take effect.
#### Content Alignment
Use the **alignContent** parameter to set how space is distributed between and around content items along the cross axis. This parameter is valid only for a flex layout that contains multiple lines. The available options are as follows:
- **FlexAlign.Start**: The items are aligned toward the start edge of the cross axis in the container.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Start }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![crossStart.png](figures/crossStart.png)
- **FlexAlign.Center**: The items are aligned toward the center of the cross axis in the container.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.Center }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![crossCenter.png](figures/crossCenter.png)
- **FlexAlign.End**: The items are aligned toward the end edge of the cross axis in the container.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.End }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![crossEnd.png](figures/crossEnd.png)
- **FlexAlign.SpaceBetween**: The items are evenly distributed in the container along the cross axis, with the first and last items aligned with the edges of the cross axis.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceBetween }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![crossSpacebetween.png](figures/crossSpacebetween.png)
- **FlexAlign.SpaceAround**: The items are evenly distributed in the container along the cross axis. The spacing before the first item and after the last item is half of the spacing between two adjacent items.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceAround }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![crossSpacearound.png](figures/crossSpacearound.png)
- **FlexAlign.SpaceEvenly**: The items are evenly distributed in the container along the cross axis. The spacing between each two adjacent items, the spacing between the start edge and the first item, and the spacing between the end edge and the last item, are the same.
```ts
Flex({ justifyContent: FlexAlign.SpaceBetween, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceEvenly }) {
Text('1').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('2').width('60%').height(20).backgroundColor(0xD2B48C)
Text('3').width('40%').height(20).backgroundColor(0xD2B48C)
Text('4').width('30%').height(20).backgroundColor(0xF5DEB3)
Text('5').width('20%').height(20).backgroundColor(0xD2B48C)
}
.width('90%')
.height(100)
.backgroundColor(0xAFEEEE)
```
![crossSpaceevenly.png](figures/crossSpaceevenly.png)
### Adaptive Stretching of Flex Layout
When the size of the container in the flex layout is not large enough, the following attributes of the child component can be used to achieve adaptive layout.
- **flexBasis**: base size of the child component in the container along the main axis. It sets the space occupied by the child component. If this attribute is not set or is set to **auto**, the space occupied by the child component is the value of width/height.
```ts
Flex() {
Text('flexBasis("auto")')
.flexBasis('auto') // When width is not set and flexBasis is set to auto, the content is loose.
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexBasis("auto")'+' width("40%")')
.width('40%')
.flexBasis('auto') // When width is set and flexBasis is set to auto, the value of width is used.
.height(100)
.backgroundColor(0xD2B48C)
Text('flexBasis(100)') // When width is not set and flexBasis is set to 100, the width is 100 vp.
.flexBasis(100)
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexBasis(100)')
.flexBasis(100)
.width(200) // When width is set to 200 and flexBasis 100, the width is 100 vp, which means that the settings of flexBasis take precedence.
.height(100)
.backgroundColor(0xD2B48C)
}.width('90%').height(120).padding(10).backgroundColor(0xAFEEEE)
```
![flexbasis](figures/flexbasis.png)
- **flexGrow**: percentage of the flex layout's remaining space that is allocated to the child component. In other words, it is the grow factor of the child component.
```ts
Flex() {
Text('flexGrow(1)')
.flexGrow(2)
.width(100)
.height(100)
.backgroundColor(0xF5DEB3)
Text('flexGrow(2)')
.flexGrow(2)
.width(100)
.height(100)
.backgroundColor(0xD2B48C)
Text('no flexGrow')
.width(100)
.height(100)
.backgroundColor(0xF5DEB3)
}.width(400).height(120).padding(10).backgroundColor(0xAFEEEE)
```
![flexgrow](figures/flexgrow.png)
In the preceding figure, the width of the parent container is 400 vp, the original width of the three child components is 100 vp, which adds up to the total width of 300 vp. The remaining space 100 vp is allocated to the child components based on their **flexGrow** settings. Child components that do not have **flexGrow** set are not involved in the allocation of remaining space.
The first child component and the second child component receive their share of remaining space at the 2:3 ratio. The width of the first child component is 100 vp + 100 vp x 2/5 = 140 vp, and the width of the second child component is 100 vp + 100 vp x 3/5 = 160 vp.
- **flexShrink**: shrink factor of the child component when the size of all child components is larger than the flex container.
```ts
Flex({ direction: FlexDirection.Row }) {
Text('flexShrink(3)')
.flexShrink(3)
.width(200)
.height(100)
.backgroundColor(0xF5DEB3)
Text('no flexShrink')
.width(200)
.height(100)
.backgroundColor(0xD2B48C)
Text('flexShrink(2)')
.flexShrink(2)
.width(200)
.height(100)
.backgroundColor(0xF5DEB3)
}.width(400).height(120).padding(10).backgroundColor(0xAFEEEE)
```
![flexshrink](figures/flexshrink.png)
## Example Scenario
With the flex layout, child components can be arranged horizontally, aligned at both edges, evenly spaced, and centered in the vertical direction. The sample code is as follows:
```ts
@Entry
@Component
struct FlexExample {
build() {
Column() {
Column({ space: 5 }) {
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.NoWrap, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
Text('1').width('30%').height(50).backgroundColor(0xF5DEB3)
Text('2').width('30%').height(50).backgroundColor(0xD2B48C)
Text('3').width('30%').height(50).backgroundColor(0xF5DEB3)
}
.height(70)
.width('90%')
.backgroundColor(0xAFEEEE)
}.width('100%').margin({ top: 5 })
}.width('100%')
}
}
```
![en-us_image_0000001261605867](figures/flexExample.png)
# Grid Layout
As a tool to provide layout auxiliary lines, the grid system is handy in graphic and website design. When employed in the UI design of mobile devices, the grid system exhibits the following advantages:
1. Provides rules for layout design and resolves issues of dynamic layout across devices with different sizes.
2. Provides a unified positioning method for the system to ensure layout consistency across layouts on different devices.
3. Provides a flexible spacing adjustment method for applications to keep up with layout in special scenarios.
To implement a grid layout, **[GridRow](../reference/arkui-ts/ts-container-gridrow.md)** and **[GridCol](../reference/arkui-ts/ts-container-gridcol.md)** are prioritized
over the obsolete **[GridContainer](../reference/arkui-ts/ts-container-gridcontainer.md)**, because the former provides more flexibility and functionality. **GridRow** is a grid container item and must be used only with **GridCol** in the grid container.
## GridRow
The grid container works in terms of **columns**, **gutter**, **direction**, and **breakpoints**.
- **columns**: total number of columns in the grid container. This attribute is the main tool for placing items in the grid layout.
- **gutter**: spacing between elements. This attribute determines how close the content tracks are with each other.
- **direction**: alignment of child components in the grid container.
- **breakpoints**: ranges of application window widths, which are set based on the device screen widths. You can use the breakpoints to meet specific layout requirements.
After you set the breakpoints, the layout listens for changes in the application window size, determines which breakpoint range the application window is located, and adjusts to match the window size accordingly.
### Grid Breakpoints
The grid system defines breakpoints, which are screen width types in effect, based on the horizontal width (screen density pixels, in vp) of the screens. You can use the breakpoints to meet specific layout requirements.
By default, the grid system provides four breakpoints: xs, sm, md, and lg.
| Breakpoint | Value Range (vp)|
| --------| ------ |
| xs | [0, 320) |
| sm | [320, 600) |
| md | [600, 840) |
| lg | [840, +∞) |
In the **\<GridRow>** component, you can use **breakpoints** to customize the value range of breakpoints. A maximum of six breakpoints are supported.
In addition to the four default breakpoints, you can also enable the xl and xxl breakpoints for your application window layout.
| Breakpoint| Device Description |
| ----- | ---------------------------------------- |
| xs | Device of the minimum size. |
| sm | Small-sized device. |
| md | Medium-sized device. |
| lg | Large-sized device. |
| xl | Extra-large-sized device. |
| xxl | Ultra-large-sized device. |
- Set **breakpoints** with a monotonically increasing array based on the use case. Because **breakpoints** supports a maximum of six breakpoints, the maximum length of the monotonically increasing array is 5.
```ts
breakpoints: {value: ["100vp", "200vp"]}
```
Enables three breakpoints: xs, sm, and md. If the value is less than 100 vp, the breakpoint is xs. If the value is 100–200 vp, the breakpoint is sm. If the value is greater than 200 vp, the breakpoint is md.
```ts
breakpoints: {value: ["320vp", "600vp", "840vp", "1080vp"]}
```
Enables five breakpoints: xs, sm, md, lg, and xl. If the value is less than 320 vp, the breakpoint is xs. If the value is 320–600 vp, the breakpoint is sm. If the value is 600–840 vp, the breakpoint is md. If the value is 840–1080vp, the breakpoint is lg. If the value is greater than 1080 vp, the breakpoint is xl.
- The grid system implements breakpoints by listening for the changes in the window or container size, and sets the breakpoint references through **reference**. Considering that the application may be displayed in non-full-screen mode, design the breakpoints with the application window width as the reference.
In the following example, the default number of columns of a grid is 12. Breakpoints are used to divide the application window width into six ranges, where different grid items occupy a different number of columns. The following figure shows the effect.
```ts
GridRow({
breakpoints: {
value: ['200vp', '300vp', '400vp', '500vp', '600vp'],
reference: BreakpointsReference.WindowSize
}
}) {
ForEach(this.bgColors, (color, index) => {
GridCol({
span: {
xs: 2,
sm: 3,
md: 4,
lg: 6,
xl: 8,
xxl: 12
}
}) {
Row() {
Text(`${index}`)
}.width("100%").height("50vp")
}.backgroundColor(color)
})
}
```
![](figures/breakpoints.gif)
### Columns
In the **\<GridRow>**, **columns** is used to set the total number of columns in the grid layout.
- The default value of **columns** is 12. If **columns** is not set, the grid layout is divided into 12 columns at any breakpoint.
```ts
GridRow() {
ForEach(this.bgColors, (item, index) => {
GridCol() {
Row() {
Text(`${index + 1}`)
}.width("100%").height("50")
}.backgroundColor(item)
})
}
```
![](figures/columns1.png)
- When **columns** is set to a number, the grid layout is divided into the specified number of columns regardless of the screen size. The following example sets the number of grid layout columns to 4 and 8 in sequence, where a child component occupies one column by default.
```ts
Row() {
GridRow({ columns: 4 }) {
ForEach(this.bgColors, (item, index) => {
GridCol() {
Row() {
Text(`${index + 1}`)
}.width("100%").height("50")
}.backgroundColor(item)
})
}
.width("100%").height("100%")
.onBreakpointChange((breakpoint) => {
this.currentBp = breakpoint
})
}
.height(160)
.border({ color: Color.Blue, width: 2 })
.width('90%')
Row() {
GridRow({ columns: 8 }) {
ForEach(this.bgColors, (item, index) => {
GridCol() {
Row() {
Text(`${index + 1}`)
}.width("100%").height("50")
}.backgroundColor(item)
})
}
.width("100%").height("100%")
.onBreakpointChange((breakpoint) => {
this.currentBp = breakpoint
})
}
.height(160)
.border({ color: Color.Blue, width: 2 })
.width('90%')
```
![](figures/columns2.png)
- When **columns** is set to a value of the **GridRowColumnOption** type, you can assign values specific to the screen size (xs, sm, md, lg, xl, xxl).
```ts
GridRow({ columns: { sm: 4, md: 8 }, breakpoints: { value: ['200vp', '300vp', '400vp', '500vp', '600vp'] } }) {
ForEach(this.bgColors, (item, index) => {
GridCol() {
Row() {
Text(`${index + 1}`)
}.width("100%").height("50")
}.backgroundColor(item)
})
}
```
![](figures/columns3.gif)
As shown above, if **columns** is only set for the sm and md screen size types, screen sizes smaller than sm use the default value **12**, and screen sizes larger than md (lg, xl, and xxl) use the value of **columns** of the md type.
### Gutters
In the **\<GridRow>**, **gutter** is used to set the spacing between adjacent child components in the horizontal and vertical directions.
- When **gutter** is set to a number, the number applies to both the horizontal and vertical directions. In the following example, the horizontal and vertical spacing between adjacent child components are set to **10**.
```ts
GridRow({ gutter: 10 }){}
```
![](figures/gutter1.png)
- When **gutter** is set to a value of the **GutterOption** type, the **x** attribute of the value indicates the horizontal gutter, and the **y** attribute indicates the vertical gutter.
```ts
GridRow({ gutter: { x: 20, y: 50 } }){}
```
![](figures/gutter2.png)
### Alignment
In the **\<GridRow>**, **direction** is used to set the alignment of child components in the grid container.
- Child components are arranged from left to right by default.
```ts
GridRow({ direction: GridRowDirection.Row }){}
```
![](figures/direction1.png)
- Child components are arranged from right to left.
```ts
GridRow({ direction: GridRowDirection.RowReverse }){}
```
![](figures/direction2.png)
## GridCol
The **\<GridCol>** component is a child component of the **\<GridRow>** component. You can set the **span**, **offset**, and **order** attributes of this component by passing parameters or using setters.
- Setting **span**
```ts
GridCol({ span: 2 }){}
GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }){}
GridCol(){}.span(2)
GridCol(){}.span({ xs: 1, sm: 2, md: 3, lg: 4 })
```
- Setting **offset**
```ts
GridCol({ offset: 2 }){}
GridCol({ offset: { xs: 2, sm: 2, md: 2, lg: 2 } }){}
GridCol(){}.offset(2)
GridCol(){}.offset({ xs: 1, sm: 2, md: 3, lg: 4 })
```
- Setting **order**
```ts
GridCol({ order: 2 }){}
GridCol({ order: { xs: 1, sm: 2, md: 3, lg: 4 } }){}
GridCol(){}.order(2)
GridCol(){}.order({ xs: 1, sm: 2, md: 3, lg: 4 })
```
The following describes how to set the attributes by passing parameters.
### span
Sets the number of columns occupied by a child component in the grid layout, which determines the child component width. The default value is **1**.
- When the value type is number, the number of columns occupied by the child component is the same across screen sizes.
```ts
GridRow({ columns: 8 }) {
ForEach(this.bgColors, (color, index) => {
GridCol({ span: 2 }) {
Row() {
Text(`${index}`)
}.width("100%").height("50vp")
}
.backgroundColor(color)
})
}
```
![](figures/span1.png)
- When the value type is **GridColColumnOption**, you can assign values specific to the screen size (xs, sm, md, lg, xl, xxl).
```ts
GridRow({ columns: 8 }) {
ForEach(this.bgColors, (color, index) => {
GridCol({ span: { xs: 1, sm: 2, md: 3, lg: 4 } }) {
Row() {
Text(`${index}`)
}.width("100%").height("50vp")
}
.backgroundColor(color)
})
}
```
![](figures/span2.gif)
### offset
Sets the column offset of a child component relative to the previous child component. The default value is **0**.
- When the value type is number, the column offset of the child component is the same across screen sizes.
```ts
GridRow() {
ForEach(this.bgColors, (color, index) => {
GridCol({ offset: 2 }) {
Row() {
Text("" + index)
}.width("100%").height("50vp")
}
.backgroundColor(color)
})
}
```
![](figures/offset1.png)
By default, a grid is divided into 12 columns and each child component occupies one column with an offset of two columns. Each row holds four child components, with three columns per child component plus the gutter.
- When the value type is **GridColColumnOption**, you can assign values specific to the screen size (xs, sm, md, lg, xl, xxl).
```ts
GridRow() {
ForEach(this.bgColors, (color, index) => {
GridCol({ offset: { xs: 1, sm: 2, md: 3, lg: 4 } }) {
Row() {
Text("" + index)
}.width("100%").height("50vp")
}
.backgroundColor(color)
})
}
```
![](figures/offset2.gif)
### order
Sets the sequence number of a child component in the grid layout. If a child component shares an **order** value with another child component or does not have **order** set, it is displayed based on its code sequence number. A child components with a smaller **order** value is placed before the one with a larger **order** value.
If **order** is not set for all child components, those that have **order** set are displayed after those that do not have **order** set and are sorted in ascending order based on the value.
- When the value type is number, child components are sorted in the same order across screen sizes.
```ts
GridRow() {
GridCol({ order: 5 }) {
Row() {
Text("1")
}.width("100%").height("50vp")
}.backgroundColor(Color.Red)
GridCol({ order: 4 }) {
Row() {
Text("2")
}.width("100%").height("50vp")
}.backgroundColor(Color.Orange)
GridCol({ order: 3 }) {
Row() {
Text("3")
}.width("100%").height("50vp")
}.backgroundColor(Color.Yellow)
GridCol({ order: 2 }) {
Row() {
Text("4")
}.width("100%").height("50vp")
}.backgroundColor(Color.Green)
}
```
![](figures/order1.png)
- When the value type is **GridColColumnOption**, you can assign values specific to the screen size (xs, sm, md, lg, xl, xxl).
```ts
GridRow() {
GridCol({ order: { xs:1, sm:5, md:3, lg:7}}) {
Row() {
Text("1")
}.width("100%").height("50vp")
}.backgroundColor(Color.Red)
GridCol({ order: { xs:2, sm:2, md:6, lg:1} }) {
Row() {
Text("2")
}.width("100%").height("50vp")
}.backgroundColor(Color.Orange)
GridCol({ order: { xs:3, sm:3, md:1, lg:6} }) {
Row() {
Text("3")
}.width("100%").height("50vp")
}.backgroundColor(Color.Yellow)
GridCol({ order: { xs:4, sm:4, md:2, lg:5} }) {
Row() {
Text("4")
}.width("100%").height("50vp")
}.backgroundColor(Color.Green)
}
```
![](figures/order2.gif)
# Grid Layout
The grid layout is an important layout in adaptive layout. It excels at dividing a page and defining proportion.
You can implement a grid layout through the **[\<Grid>](../reference/arkui-ts/ts-container-grid.md)** container and the **[\<GridItem>](../reference/arkui-ts/ts-container-griditem.md)** child component.
**\<Grid>** sets grid-wide parameters, and **\<GridItem>** defines parameters specific to grid items. The grid layout has the following features:
1. When the size of the container changes, all child components and their spacings are adjusted proportionally.
2. The numbers of rows and columns in the grid layout as well as the proportion of each row and column are customizable.
3. The row and column spacing of child components in the grid layout can be set.
4. A child component can span the specified number of rows or columns.
## Grid Settings
### Number and Proportion of Rows and Columns
The number and proportion of rows and columns are set through the **columnsTemplate** and **rowTemplate** attributes.
The following describes how to set **columnsTemplate**. The value of this attribute is a string consisting of multiple 'number+fr' entries separated by spaces. The number of entries is the number of columns in the grid layout. The number before **fr** is used to calculate the proportion of the column to the grid layout width, thereby determining the column width.
```ts
struct GridExample {
@State Number: Array<string> = ['1', '2', '3', '4']
build() {
Column({ space: 5 }) {
Grid() {
ForEach(this.Number, (num: string) => {
GridItem() {
Text(`Column ${num}`)
.fontSize(16)
.textAlign(TextAlign.Center)
.backgroundColor(0xd0d0d0)
.width('100%')
.height('100%')
.borderRadius(5)
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr')
.columnsGap(10)
.rowsGap(20)
.width('90%')
.backgroundColor(0xF0F0F0)
.height(100)
}.width('100%')
}
}
```
Create a four-column grid, where each column has an equal width.
```ts
struct GridExample {
@State Number: Array<string> = ['1', '2', '3', '4']
build() {
Column({ space: 5 }) {
Grid() {
ForEach(this.Number, (num: string) => {
GridItem() {
Text(`Column ${num}`)
.fontSize(16)
.textAlign(TextAlign.Center)
.backgroundColor(0xd0d0d0)
.width('100%')
.height('100%')
.borderRadius(5)
}
})
}
.columnsTemplate('1fr 2fr 3fr 4fr')
.rowsTemplate('1fr')
.columnsGap(10)
.rowsGap(20)
.width('90%')
.backgroundColor(0xF0F0F0)
.height(100)
}.width('100%')
}
}
```
Create a four-column grid, where the proportions of the columns are 1:2:3:4.
```ts
struct GridExample {
@State Number: Array<string> = ['1', '2', '3']
build() {
Column({ space: 5 }) {
Grid() {
ForEach(this.Number, (num: string) => {
GridItem() {
Text(`Column ${num}`)
.fontSize(16)
.textAlign(TextAlign.Center)
.backgroundColor(0xd0d0d0)
.width('100%')
.height('100%')
.borderRadius(5)
}
})
}
.columnsTemplate('4fr 2fr 3fr')
.rowsTemplate('1fr')
.columnsGap(10)
.rowsGap(20)
.width('90%')
.backgroundColor(0xF0F0F0)
.height(100)
}.width('100%')
}
}
```
Create a three-column grid, where the proportions of the columns are 4:2:3.
The effect is as follows:
![](figures/columnTemplate.png)
### Alignment
Set **layoutDirection** to define how grid items are laid out along the main axis.
The layout options are **Row**, **RowReverse**, **Column**, and **ColumnReverse**.
The effect is as follows:
![](figures/gridlayout.png)
### Spacing Between Rows and Columns
Set **columnsGap** to define the vertical spacing of the grid items and **rowsGap** to define the horizontal spacing.
```ts
Grid()
.columnsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(20)
```
![](figures/columnGap.png)
In the preceding example, the horizontal and vertical spacings between grid items are 10 and 20, respectively.
## GridItem Settings
### Setting the Number of Rows and Columns per Grid Item
The rows and columns of a grid layout are numbered in sequence, starting from 1.
If a grid item spans multiple rows, use **rowStart** to set the start row number and **rowEnd** to set the end row number. If the value of **rowStart** is the same as that of **rowEnd**, the grid item occupies only one grid. The sample code is as follows:
```ts
Grid() {
GridItem() {
Text('5')
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.rowStart(2).rowEnd(3) // The grid item spans the second and third rows.
GridItem() {
Text('4')
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.columnStart(4).columnEnd(5) // The grid item spans the fourth and fifth columns.
GridItem() {
Text('6')
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.columnStart(2).columnEnd(4) // The grid item spans the second to fourth columns.
GridItem() {
Text('9')
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.columnStart(3).columnEnd(4) // The grid item spans the third and fourth columns.
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(20)
.width('90%')
.backgroundColor(0xF0F0F0)
.height('200vp')
.layoutDirection(GridDirection.Column)
```
![](figures/griditem.png)
## Example Scenario
Use the grid layout to implement a calculator. The sample code is as follows:
```ts
@Entry
@Component
struct GridExample {
@State Number: Array<string> = ['1', '2', '3', '+', '4', '5', '6', '-', '7', '8', '9', '*', '0', '.', '/']
@Styles textStyle(){
.backgroundColor(0xd0d0d0)
.width('100%')
.height('100%')
.borderRadius(5)
}
build() {
Column({ space: 5 }) {
Grid() {
GridItem() {
Text('0')
.fontSize(30)
.textStyle()
}.columnStart(1).columnEnd(4)
GridItem() {
Text ('Clear')
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.columnStart(1).columnEnd(2)
GridItem() {
Text ('Back')
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.columnStart(3).columnEnd(4)
ForEach(this.Number, (day: string) => {
if (day === '0') {
GridItem() {
Text(day)
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}.columnStart(1).columnEnd(2)
} else {
GridItem() {
Text(day)
.fontSize(16)
.textAlign(TextAlign.Center)
.textStyle()
}
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('2fr 1fr 1fr 1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(15)
.width('90%')
.backgroundColor(0xF0F0F0)
.height('70%')
}.width('100%').margin({ top: 5 })
}
}
```
Below you can see how the calculator displays on a large-screen device.
![](figures/gridExp1.png)
Below you can see how the calculator displays on a small-screen device.
![](figures/gridExp2.png)
# Linear Layout
The linear layout is the most commonly used layout in development. The child components in a linear layout are arranged one by one in a linear direction, either vertically or horizontally.
You can implement a linear layout through the **[\<Row>](../reference/arkui-ts/ts-container-row.md)** and **[\<Column>](../reference/arkui-ts/ts-container-column.md)** containers. In the **\<Column>** container, child components are arranged vertically. In the **\<Row>** container, child components are arranged horizontally.
## Linear Layout Orientation
The orientation of a linear layout is subject to the container: **\<Row>** or **\<Column>**. You can use the container attributes to adjust the spacing between child components and the horizontal and vertical alignment modes.
1. The **space** attribute sets the spacing between child components so that they are evenly spaced along the main axis.
2. The **alignItems** attribute sets the alignment mode of the child components along the cross axis, which is consistent across screens of various sizes. When the cross axis is vertical, the value type is [VerticalAlign](../reference/arkui-ts/ts-appendix-enums.md#verticalalign). When the cross axis is horizontal, the value type is [HorizontalAlign](../reference/arkui-ts/ts-appendix-enums.md#horizontalalign).
3. The **justifyContent** attribute sets the alignment mode of the child components along the main axis, implementing adaptive layout. The value type is [FlexAlign](../reference/arkui-ts/ts-appendix-enums.md#flexalign).
The table below provides the usage and demo effects.
|Attribute|Description|Row Effect|Column Effect|
|------|---------------------------|----------------------------|---------------------------|
|space |- Horizontal spacing between child components in the horizontal layout<br> - Vertical spacing between child components in the vertical layout| ![](figures/rowspace.png) | ![](figures/columnspace.png) |
|alignItems |Alignment mode of child components along the cross axis of the container.|![](figures/rowalign.png) |![](figures/columnalign.png)|
|justifyContent |Alignment mode of child components along the main axis of the container.|![](figures/rowjustify.png) |![](figures/columnjustify.png)|
## Adaptive Stretching
In linear layout, the **[\<Blank>](../reference/arkui-ts/ts-basic-components-blank.md)** component is commonly used to automatically fill blank space along the main axis of the container, so as to achieve adaptive stretching.
```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%')
}
}
```
![](figures/blank.gif)
## Adaptive Scaling
Adaptive scaling means that child components scale according to the preset ratio to adapt to the container size on devices of various sizes. Below are the methods to implement adaptive scaling in a linear layout:
1. When the size of the parent container is determined, child components are laid out along the main axis based on the **layoutWeight** attribute settings, with the size settings of the child components ignored. In this way, the child components are adaptively scaled to fill up remaining space, regardless of the screen size.
```ts
@Entry
@Component
struct layoutWeightExample {
build() {
Column() {
Text('1:2:3').width('100%')
Row() {
Column() {
Text('layoutWeight(1)')
.textAlign(TextAlign.Center)
}.layoutWeight(2).backgroundColor(0xffd306).height('100%')
Column() {
Text('layoutWeight(2)')
.textAlign(TextAlign.Center)
}.layoutWeight(4).backgroundColor(0xffed97).height('100%')
Column() {
Text('layoutWeight(6)')
.textAlign(TextAlign.Center)
}.layoutWeight(6).backgroundColor(0xffd306).height('100%')
}.backgroundColor(0xffd306).height('30%')
Text('2:5:3').width('100%')
Row() {
Column() {
Text('layoutWeight(2)')
.textAlign(TextAlign.Center)
}.layoutWeight(2).backgroundColor(0xffd306).height('100%')
Column() {
Text('layoutWeight(5)')
.textAlign(TextAlign.Center)
}.layoutWeight(5).backgroundColor(0xffed97).height('100%')
Column() {
Text('layoutWeight(3)')
.textAlign(TextAlign.Center)
}.layoutWeight(3).backgroundColor(0xffd306).height('100%')
}.backgroundColor(0xffd306).height('30%')
}
}
}
```
![](figures/layoutWeight.gif)
3. When the size of the parent container is determined, set the widths of the child component and sibling components in percentage.
```ts
@Entry
@Component
struct WidthExample {
build() {
Column() {
Row() {
Column() {
Text('left width 20%')
.textAlign(TextAlign.Center)
}.width('20%').backgroundColor(0xffd306).height('100%')
Column() {
Text('center width 50%')
.textAlign(TextAlign.Center)
}.width('50%').backgroundColor(0xffed97).height('100%')
Column() {
Text('right width 30%')
.textAlign(TextAlign.Center)
}.width('30%').backgroundColor(0xffd306).height('100%')
}.backgroundColor(0xffd306).height('30%')
}
}
}
```
![](figures/width.gif)
In the preceding example, the proportion of child components remains unchanged across devices of different sizes.
## Positioning
- Relative layout
You can use the **[offset](../reference/arkui-ts/ts-universal-attributes-location.md)** attribute to set the offset of a component relative to itself, thereby implementing relative layout. Setting this attribute does not affect the layout of the parent container. It only adjusts the component position during drawing. The linear layout and offset can work together to meet most layout development requirements.
```ts
@Entry
@Component
struct OffsetExample {
@Styles eleStyle() {
.size({ width: 120, height: '50' })
.backgroundColor(0xbbb2cb)
.border({ width: 1 })
}
build() {
Column({ space: 20 }) {
Row() {
Text('1').size({ width: '15%', height: '50' }).backgroundColor(0xdeb887).border({ width: 1 }).fontSize(16)
Text('2 offset(15, 30)')
.eleStyle()
.fontSize(16)
.align(Alignment.Start)
.offset({ x: 15, y: 30 })
Text('3').size({ width: '15%', height: '50' }).backgroundColor(0xdeb887).border({ width: 1 }).fontSize(16)
Text('4 offset(-10%, 20%)')
.eleStyle()
.fontSize(16)
.offset({ x: '-5%', y: '20%' })
}.width('90%').height(150).border({ width: 1, style: BorderStyle.Dashed })
}
.width('100%')
.margin({ top: 25 })
}
}
```
![](figures/offset.gif)
- Absolute layout
In linear layout, you can use the **[position](../reference/arkui-ts/ts-universal-attributes-location.md)** attribute to set the offset of the upper left corner of a component relative to the upper left corner of the parent container, thereby implementing absolute layout. Absolute layout offers poorer adaptability to screen sizes than relative layout.
```ts
@Entry
@Component
struct PositionExample {
@Styles eleStyle(){
.backgroundColor(0xbbb2cb)
.border({ width: 1 })
.size({ width: 120, height: 50 })
}
build() {
Column({ space: 20 }) {
// Set the offset of the upper left corner of the child component relative to the upper left corner of the parent container.
Row() {
Text('position(30, 10)')
.eleStyle()
.fontSize(16)
.position({ x: 10, y: 10 })
Text('position(50%, 70%)')
.eleStyle()
.fontSize(16)
.position({ x: '50%', y: '70%' })
Text('position(10%, 90%)')
.eleStyle()
.fontSize(16)
.position({ x: '10%', y: '80%' })
}.width('90%').height('100%').border({ width: 1, style: BorderStyle.Dashed })
}
.width('90%').margin(25)
}
}
```
![](figures/position.gif)
## Adaptive Extension
Adaptive extension allows users to drag the scrollbar to display content when the content amount displayed on a page is subject to the screen size and the content extends beyond the viewport. Below are the methods to implement adaptive extension in a linear layout:
- **\<List>** component
If the list items cannot be fully displayed on one screen, you can use the scrollbar to let users view the list items in full. Use the **scrollBar** attribute to set the scrollbar status, and the **edgeEffect** to set the rebound effect when the scrollbar has reached the edge.
Vertical list:
```ts
@Entry
@Component
struct ListExample1 {
@State arr: string[] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"]
@State alignListItem: ListItemAlign = ListItemAlign.Start
build() {
Column() {
List({ space: 20, initialIndex: 0 }) {
ForEach(this.arr, (item) => {
ListItem() {
Text('' + item)
.width('100%')
.height(100)
.fontSize(16)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}
.border({ width: 2, color: Color.Green })
}, item => item)
}
.border({ width: 2, color: Color.Red, style: BorderStyle.Dashed })
.scrollBar (BarState.On) // ScrollBar status
.edgeEffect(EdgeEffect.Spring) // Rebound effect when the scrollbar has reached the edge
}.width('100%').height('100%').backgroundColor(0xDCDCDC).padding(20)
}
}
```
![](figures/listcolumn.gif)
Horizontal List:
```ts
@Entry
@Component
struct ListExample2 {
@State arr: string[] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"]
@State alignListItem: ListItemAlign = ListItemAlign.Start
build() {
Column() {
List({ space: 20, initialIndex: 0 }) {
ForEach(this.arr, (item) => {
ListItem() {
Text('' + item)
.height('100%')
.width(100)
.fontSize(16)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}
.border({ width: 2, color: Color.Green })
}, item => item)
}
.border({ width: 2, color: Color.Red, style: BorderStyle.Dashed })
.scrollBar (BarState.On) // Scrollbar status.
.edgeEffect(EdgeEffect.Spring) // Rebound effect when the scrollbar has reached the edge.
.listDirection(Axis.Horizontal) // Horizontal layout.
}.width('100%').height('100%').backgroundColor(0xDCDCDC).padding(20)
}
}
```
![](figures/listrow.gif)
- **\<Scroll>** component
In linear layout, the **\<Scroll>** component scrolls the content when the layout size of a component exceeds the size of its parent component. You can wrap a **\<Scroll>** component at the outer layer of the **\<Column>** or **\<Row>** component.
Vertical 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) => {
Text(item.toString())
.width('90%')
.height(150)
.backgroundColor(0xFFFFFF)
.borderRadius(15)
.fontSize(16)
.textAlign(TextAlign.Center)
.margin({ top: 10 })
}, item => item)
}.width('100%')
}
.backgroundColor(0xDCDCDC)
.scrollable(ScrollDirection.Vertical) // Vertical scrolling.
.scrollBar(BarState.On) // Scrollbar status.
.scrollBarColor(Color.Gray) // Scrollbar color.
.scrollBarWidth(30) // Scrollbar width.
.edgeEffect(EdgeEffect.Spring) // Rebound effect when the scrollbar has reached the edge.
}
}
```
![](figures/scrollcolumn.gif)
Horizontal scrolling:
```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) => {
Text(item.toString())
.height('90%')
.width(150)
.backgroundColor(0xFFFFFF)
.borderRadius(15)
.fontSize(16)
.textAlign(TextAlign.Center)
.margin({ left: 10 })
}, item => item)
}.height('100%')
}
.backgroundColor(0xDCDCDC)
.scrollable(ScrollDirection.Horizontal) // Horizontal scrolling.
.scrollBar(BarState.On) // Scrollbar status.
.scrollBarColor(Color.Gray) // Scrollbar color
.scrollBarWidth(30) // Scrollbar width.
.edgeEffect(EdgeEffect.Spring) // Rebound effect when the scrollbar has reached the edge.
}
}
```
![](figures/scrollrow.gif)
# Media Query
[Media queries](../reference/apis/js-apis-mediaquery.md) are at the core of responsive design and widely used on mobile devices. You can use them to create different application styles depending on the device type or device state. Specifically, media queries allow you to:
1. Design a layout style based on the device and application attributes (such as display area, dark light color, and resolution).
2. Update the page layout to adapt to dynamic screen changes (for example, screen splitting or switching between landscape and portrait modes).
## Usage
Invoke the **mediaquery** API to set the media query condition and the callback, and change the page layout or implement service logic in the callback corresponding to the condition. The procedure is as follows:
1. Import the **mediaquery** module, as shown below:
```ts
import mediaquery from '@ohos.mediaquery'
```
2. Use the **matchMediaSync** API to set the media query condition and save the returned listener, as shown below:
```ts
listener = mediaquery.matchMediaSync('(orientation: landscape)')
```
3. Register the **onPortrait** callback using the saved listener, and change the page layout or implement service logic in the callback. When the media query condition is matched, the callback is triggered. The sample code is as follows:
```ts
onPortrait(mediaQueryResult) {
if (mediaQueryResult.matches) {
// do something here
} else {
// do something here
}
}
listener.on('change', onPortrait)
```
## Media Query Conditions
The media query condition consists of the media type (optional), logical operator, and media feature. The logical operator is used to connect different media types and media features. A media feature must be enclosed in parentheses (). There may be multiple media features. The specific rules are as follows:
### Syntax
```
[media-type] [and|not|only] [(media-feature)]
```
Examples are as follows:
**screen and (round-screen: true)**: The query is valid when the device screen is round.
**(max-height: 800)**: The query is valid when the height is less than or equal to 800.
**(height <= 800)**: The query is valid when the height is less than or equal to 800.
**screen and (device-type: tv) or (resolution < 2)**: The query is valid when the device type is TV or the device resolution is less than 2. This is a multi-condition query that contains multiple media features.
### media-type
| Type | Description |
| ------ | -------------- |
| screen | Media query based on screen-related parameters.|
### Media Logic Operation (and|or|not|only)
You can use logical operators (**and**, **or**, **not**, and **only**) to compose complex media queries. You can also combine them using comma (,). The following table describes the operators.
**Table 1** Media logical operators
| Type | Description |
| -------- | ---------------------------------------- |
| and | The **and** operator is used to combine multiple media features into one media query, in a logical AND operation. The query is valid only when all media features are true. It can also combine media types and media functions.<br>For example, **screen and (device-type: wearable) and (max-height: 600)** indicates that the query is valid when the device type is wearable and the maximum height of the application is 600 pixel units.|
| or | The **or** operator is used to combine multiple media features into one media query, in a logical OR operation. The query is valid if a media feature is true.<br>For example, **screen and (max-height: 1000) or (round-screen: true)** indicates that the query is valid when the maximum height of the application is 1000 pixel units or the device screen is round.|
| not | The **not** operator is used to perform a logical negation for a media query. **true** is returned if the query condition is not met. Otherwise, **false** is returned.<br>For example, **not screen and (min-height: 50) and (max-height: 600)** indicates that the query is valid when the height of the application is less than 50 pixel units or greater than 600 pixel units.<br>You must specify the media type when using the **not** operator.|
| only | The **only** operator applies the selected style only when the entire expression is matched. It can be used to prevent ambiguity on browsers of earlier versions. The statements that contain both media types and media features produce ambiguity when they are received by some browsers of earlier versions. For example:<br>screen and (min-height: 50)<br>The browsers of earlier versions would mislead this sentence into screen, causing the fact that the specified style is applied when only the media type is matched. In this case, the **only** operator can be used to avoid this problem.<br>You must specify the media type when using the **only** operator.|
| , (comma) | The **or** operator is used to combine multiple media features into one media query, in a logical OR operation. The query is valid if a media feature is true. The effect of a comma operator is equivalent to that of the **or** operator.<br>For example, **screen and (min-height: 1000), (round-screen: true)** indicates that the query is valid when the minimum height of the application is 1000 pixel units or the device screen is round.|
At MediaQuery Level 4, range query is imported so that you can use the operators including &lt;=, &gt;=, &lt;, and &gt; besides the max- and min-operators.
**Table 2** Logical operators for range query
| Type | Description |
| ----- | ---------------------------------------- |
| &lt; = | Less than or equal to, for example, **screen and (50 &lt;= height)**.|
| &gt; = | Greater than or equal to, for example, **screen and (600 &gt;= height)**.|
| &lt; | Less than, for example, **screen and (50 &lt; height)**.|
| &gt; | Greater than, for example, **screen and (600 &gt; height)**.|
### media-feature
| Type | Description |
| ----------------- | ------------------------------------------------------------ |
| height | Height of the display area on the application page. |
| min-height | Minimum height of the display area on the application page. |
| max-height | Maximum height of the display area on the application page. |
| width | Width of the display area on the application page. |
| min-width | Minimum width of the display area on the application page. |
| max-width | Maximum width of the display area on the application page. |
| resolution | Resolution of the device. The unit can be dpi, dppx, or dpcm. <br>- **dpi** indicates the number of physical pixels per inch. 1 dpi ≈ 0.39 dpcm.<br>- **dpcm** indicates the number of physical pixels per centimeter. 1 dpcm ≈ 2.54 dpi.<br>- **dppx** indicates the number of physical pixels in each pixel. (This unit is calculated based on this formula: 96 px = 1 inch, which is different from the calculation method of the px unit on the page.) 1 dppx = 96 dpi.|
| min-resolution | Minimum device resolution. |
| max-resolution | Maximum device resolution. |
| orientation | Screen orientation.<br>Options are as follows:<br>- orientation: portrait<br>- orientation: landscape|
| device-height | Height of the device. |
| min-device-height | Minimum height of the device. |
| max-device-height | Maximum height of the device. |
| device-width | Width of the device. |
| device-type | Type of the device.<br>Options: **default** and tablet |
| min-device-width | Minimum width of the device. |
| max-device-width | Maximum width of the device. |
| round-screen | Screen type. The value **true** means that the screen is round, and **false** means the opposite. |
| dark-mode | Whether the device is in dark mode. The value **true** means that the device is in dark mode, and **false** means the opposite. |
## Example Scenario
In the following example, media queries are used to apply different content and styles to the page text when the screen is switched between landscape and portrait modes.
```ts
import mediaquery from '@ohos.mediaquery'
let portraitFunc = null
@Entry
@Component
struct MediaQueryExample {
@State color: string = '#DB7093'
@State text: string = 'Portrait'
listener = mediaquery.matchMediaSync('(orientation: landscape)') // The query is valid when the device is in landscape mode.
onPortrait(mediaQueryResult) {
if (mediaQueryResult.matches) {
this.color = '#FFD700'
this.text = 'Landscape'
} else {
this.color = '#DB7093'
this.text = 'Portrait'
}
}
aboutToAppear() {
portraitFunc = this.onPortrait.bind(this) // Bind to the current application instance.
this.listener.on('change', portraitFunc)
}
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text(this.text).fontSize(50).fontColor(this.color)
}
.width('100%').height('100%')
}
}
```
When the device is in landscape orientation, the text content is displayed in landscape mode in the color of #FFD700.
![en-us_image_0000001262954829](figures/en-us_image_0000001262954829.png)
When the device is not in landscape orientation, the text content is displayed in portrait mode in the color of #DB7093.
![en-us_image_0000001263074739](figures/en-us_image_0000001263074739.png)
# Statck Layout
The stack layout reserves an area on the screen to display elements in a component and allows the elements to be stacked.
You can implement a stack layout through the **[\<Stack>](../reference/arkui-ts/ts-container-stack.md)** component, which provides a stack container where child components are successively stacked and the latter one overwrites the previous one.
## Alignment
Child components in the container can be aligned in any of the alignment modes described in the table below.
|Name| Description| Image|
|---|---|---|
|TopStart| Top start.|![](figures/stacktopstart.png)|
|Top |Horizontally centered on the top.|![](figures/stacktop.png)|
|TopEnd| Top end.|![](figures/stacktopend.png)|
|Start| Vertically centered start.|![](figures/stackstart.png)|
|Center| Horizontally and vertically centered.|![](figures/stackcenter.png)|
|End| Vertically centered end.|![](figures/stackend.png)|
|BottomStart |Bottom start.|![](figures/stackbottomstart.png)|
|Bottom| Horizontally centered on the bottom.|![](figures/stackbottom.png)|
|BottomEnd| Bottom end.|![](figures/stackbottomend.png)|
## Z-order Control
You can use the **[zIndex](../reference/arkui-ts/ts-universal-attributes-z-order.md)** attribute to set the z-order of a component in the stacking context,
so as to create a custom stacking order of the child components. A larger **zIndex** value indicates a higher display level.
- In the statck layout, if the size of a component is greater than that of the one before it, the one before it is hidden.
```ts
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text ('Stacked component 1').textAlign (TextAlign.End).fontSize (20)
}.width(100).height(100).backgroundColor(0xffd306)
Column() {
Text ('Stacked component 2').fontSize (20)
}.width(150).height(150).backgroundColor(Color.Pink)
Column() {
Text ('Stacked component 3').fontSize (20)
}.width(200).height(200).backgroundColor(Color.Grey)
}.margin({ top: 100 }).width(350).height(350).backgroundColor(0xe0e0e0)
```
![](figures/stack2.png)
In the preceding figure, the size of the stacked component 3 is greater than that of all the components before it. Therefore, the first two components are completely hidden. To show these components, modify their **zIndex** attribute settings.
```ts
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
Text ('Stacked component 1').textAlign (TextAlign.End).fontSize (20)
}.width(100).height(100).backgroundColor(0xffd306).zIndex(2)
Column() {
Text ('Stacked component 2').fontSize (20)
}.width(150).height(150).backgroundColor(Color.Pink).zIndex(1)
Column() {
Text ('Stacked component 3').fontSize (20)
}.width(200).height(200).backgroundColor(Color.Grey)
}.margin({ top: 100 }).width(350).height(350).backgroundColor(0xe0e0e0)
```
![](figures/stack1.png)
# Overview
The ArkTS-based declarative development paradigm of ArkUI is a simplified, high-performance UI development framework for cross-device applications.
## Basic Capabilities
In ArkUI that uses the ArkTS-based declarative development paradigm, the programming mode is closer to natural semantics. You can intuitively describe the UI without caring about how the framework implements UI drawing and rendering, leading to simplified and efficient development. The UI capabilities are provided from three dimensions: component, animation, and state management. System capability APIs are also provided to allow for effortless invocation of system capabilities.
To accelerate your journey with ArkUI development, take time to learn the [ArkTS programming language](../quick-start/arkts-get-started.md) and the [built-in components](../reference/arkui-ts/ts-universal-events-click.md) provided in ArkUI.
- **Out-of-the-box components**
A wide range of preset system components are provided. You can set the rendering effect of these components in method chaining mode. You can combine system components to form custom components. In this way, page components are divided into independent UI units to implement independent creation, development, and reuse of different units on pages, making pages more engineering-oriented.
- **A diverse array of animation APIs**
By drawing from the standard SVG drawing capability and various open animation APIs, you can customize animation tracks by encapsulating physical models or calling the provided APIs.
- **State and data management**
State data management provides clear page update and rendering processes and pipes through decorators with different functions. State management covers UI component states and application states. With these features, you are able to build an application-wide data update and UI rendering process.
- **System capability APIs**
Development has never been so easy, with a diverse array of encapsulated system capability APIs, from UI design to system capability invoking.
## Overall Architecture
![en-us_image_0000001223287712](figures/en-us_image_0000001223287712.png)
- **Declarative UI frontend**
Provides basic language specifications of the UI development paradigm, built-in UI components, layouts, and animations, and multiple state management mechanisms, with a wide array of APIs for you to call as required.
- **Language runtime**
Provides parsing for the UI paradigm syntax, cross-language invoking, and a TS-based high-performance running environment.
- **Declarative UI backend engine**
Provides UI rendering pipelines that are compatible with different development paradigms, multiple basic components, layout calculation, dynamic effects, and interaction events, with state management and drawing capabilities.
- **Render engine**
Provides efficient drawing capabilities, which enable rendering instructions collected by the rendering pipeline to be drawn to the screen.
- **Porting layer**
Provides abstract APIs to connect to different systems, such as system rendering pipelines and lifecycle scheduling.
# Implementing Page Redirection and Data Transmission
This section describes how to implement page redirection and data transmission between pages:
1. Page redirection: Click a food item on the food list page to go to the food details page. Click the back button on the food details page to go back to the food list page.
2. Data transmission between pages: After you click a food item, **FoodDetail** receives data from the previous page and renders the corresponding food details page.
## Page Redirection
The declarative UI paradigm provides two mechanisms for page redirection:
1. **Navigator**: encapsulates the page routing capability. After the page target is specified, all child components in the page target have the routing capability.
2. Router APIs: called to implement various operations of page routing. You'll need to import **router** before calling the router APIs.
The procedure below uses these two mechanisms for redirection between the page list page and food details page.
1. Click **FoodListItem**. The **FoodDetail** page is displayed. Create a **Navigator** component in **FoodListItem** to enable its child components to have the routing function. The target page is **'pages/FoodDetail'**.
```ts
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Navigator({ target: 'pages/FoodDetail' }) {
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
.height(40)
.width(40)
.backgroundColor('#FFf1f3f5')
.margin({ right: 16 })
Text(this.foodItem.name)
.fontSize(14)
.flexGrow(1)
Text(this.foodItem.calories + ' kcal')
.fontSize(14)
}
.height(64)
}
.margin({ right: 24, left:32 })
}
}
```
![en-us_image_0000001223127744](figures/en-us_image_0000001223127744.gif)
2. Click **FoodGridItem**. The **FoodDetail** page is displayed. Import the **router** module, and then call the **push** API of this module to push the **FoodDetail** page to the route stack to implement page redirection.
```ts
import router from '@ohos.router'
@Component
struct FoodGridItem {
private foodItem: FoodData
build() {
Column() {
......
}
.height(184)
.width('100%')
.onClick(() => {
router.pushUrl({ url: 'pages/FoodDetail' })
})
}
}
```
![en-us_image_0000001267607909](figures/en-us_image_0000001267607909.gif)
3. Add the icon for returning from the **FoodDetail** page to the food list page. Save the **Back.png** file to the **resources** > **base** > **media** directory. Create a custom component **PageTitle**, which contains the back icon and Food Detail text. Call the **router.back()** API of the router to display the top page of the route stack, that is, the upper-level page.
```ts
// FoodDetail.ets
import router from '@ohos.router'
@Component
struct PageTitle {
build() {
Flex({ alignItems: ItemAlign.Start }) {
Image($r('app.media.Back'))
.width(21.8)
.height(19.6)
Text('Food Detail')
.fontSize(21.8)
.margin({left: 17.4})
}
.height(61)
.backgroundColor('#FFedf2f5')
.padding({ top: 13, bottom: 15, left: 28.3 })
.onClick(() => {
router.back()
})
}
}
```
4. Create the Stack component in the **FoodDetail** component, including the **FoodImageDisplay** and **PageTitle** child components. Set the alignment mode to **TopStart**.
```ts
@Entry
@Component
struct FoodDetail {
build() {
Column() {
Stack( { alignContent: Alignment.TopStart }) {
FoodImageDisplay()
PageTitle()
}
ContentTable()
}
.alignItems(HorizontalAlign.Center)
}
}
```
![en-us_image_0000001267767881](figures/en-us_image_0000001267767881.png)
## Data Transmission Between Pages
We have implemented the redirection and going back of the **FoodCategoryList** and **FoodDetail** pages. At this point, the tomato details page is displayed no matter which **FoodListItem**/**FoodGridItem** is clicked. This is because the data transmission between pages is not yet configured. To configure data transmission between pages, set the routing with parameters as follows:
1. Set the **params** attribute, which accepts the key-value object, in the **Navigator** of the **FoodListItem** component.
```ts
// FoodList.ets
@Component
struct FoodListItem {
private foodItem: FoodData
build() {
Navigator({ target: 'pages/FoodDetail' }) {
......
}
.params({ foodData: this.foodItem })
}
}
```
The router API called by **FoodGridItem** also has supports redirection with parameters. The method of using the router API is similar to that of using the **Navigator**.
```ts
router.pushUrl({
url: 'pages/FoodDetail',
params: { foodData: this.foodItem }
})
```
2. Import the **FoodData** class to the **FoodDetail** page and add the **foodItem** member variable to the **FoodDetail** component.
```ts
// FoodDetail.ets
import { FoodData } from '../model/FoodData'
@Entry
@Component
struct FoodDetail {
private foodItem: FoodData
build() {
......
}
}
```
3. Obtain the value of **foodData**. Call **router.getParams()['foodData']** to obtain the data corresponding to **foodData** carried when the **FoodCategoryList** page is displayed.
```ts
@Entry
@Component
struct FoodDetail {
private foodItem: FoodData = router.getParams()['foodData']
build() {
......
}
}
```
4. Re-build the components on the **FoodDetail** page. During building, the food information on the **FoodDetail** page is all directly declared constants. You need to use the passed **FoodData** data to assign a new value to the constants. The sample code is as follows:
```ts
@Component
struct PageTitle {
build() {
Flex({ alignItems: ItemAlign.Start }) {
Image($r('app.media.Back'))
.width(21.8)
.height(19.6)
Text('Food Detail')
.fontSize(21.8)
.margin({left: 17.4})
}
.height(61)
.backgroundColor('#FFedf2f5')
.padding({ top: 13, bottom: 15, left: 28.3 })
.onClick(() => {
router.back()
})
}
}
@Component
struct FoodImageDisplay {
private foodItem: FoodData
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Image(this.foodItem.image)
.objectFit(ImageFit.Contain)
Text(this.foodItem.name)
.fontSize(26)
.fontWeight(500)
.margin({ left: 26, bottom: 17.4 })
}
.height(357)
.backgroundColor('#FFedf2f5')
}
}
@Component
struct ContentTable {
private foodItem: FoodData
@Builder IngredientItem(title:string, name: string, value: string) {
Flex() {
Text(title)
.fontSize(17.4)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Flex() {
Text(name)
.fontSize(17.4)
.flexGrow(1)
Text(value)
.fontSize(17.4)
}
.layoutWeight(2)
}
}
build() {
Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
this.IngredientItem('Calories', 'Calories', this.foodItem.calories + 'kcal')
this.IngredientItem('Nutrition', 'Protein', this.foodItem.protein + 'g')
this.IngredientItem('', 'Fat', this.foodItem.fat + 'g')
this.IngredientItem('', 'Carbohydrates', this.foodItem.carbohydrates + 'g')
this.IngredientItem('', 'VitaminC', this.foodItem.vitaminC + 'mg')
}
.height(280)
.padding({ top: 30, right: 30, left: 30 })
}
}
@Entry
@Component
struct FoodDetail {
private foodItem: FoodData = router.getParams()['foodData']
build() {
Column() {
Stack( { alignContent: Alignment.TopStart }) {
FoodImageDisplay({ foodItem: this.foodItem })
PageTitle()
}
ContentTable({ foodItem: this.foodItem })
}
.alignItems(HorizontalAlign.Center)
}
}
```
# Recommendations for Improving Performance
Poor-performing code may work, but will take away from your application performance. This topic presents a line-up of recommendations that you can take to improve your implementation, thereby avoiding possible performance drop.
## Lazy Loading
When developing a long list, use of loop rendering, as in the code snippet below, can greatly slow down page loading and increase server load.
```ts
@Entry
@Component
struct MyComponent {
@State arr: number[] = Array.from(Array(100), (v,k) =>k); // Construct an array of 0 to 99.
build() {
List() {
ForEach(this.arr, (item: number) => {
ListItem() {
Text(`item value: ${item}`)
}
}, (item: number) => item.toString())
}
}
}
```
The preceding code snippet loads all of the 100 list elements at a time during page loading. This is generally not desirable. Instead, what we need is to load data from the data source and create corresponding components on demand. This can be achieved through lazy loading. The sample code is as follows:
```ts
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = []
public totalCount(): number {
return 0
}
public getData(index: number): any {
return undefined
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener')
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener')
this.listeners.splice(pos, 1)
}
}
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded()
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index)
})
}
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index)
})
}
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to)
})
}
}
class MyDataSource extends BasicDataSource {
private dataArray: string[] = ['item value: 0', 'item value: 1', 'item value: 2']
public totalCount(): number {
return this.dataArray.length
}
public getData(index: number): any {
return this.dataArray[index]
}
public addData(index: number, data: string): void {
this.dataArray.splice(index, 0, data)
this.notifyDataAdd(index)
}
public pushData(data: string): void {
this.dataArray.push(data)
this.notifyDataAdd(this.dataArray.length - 1)
}
}
@Entry
@Component
struct MyComponent {
private data: MyDataSource = new MyDataSource()
build() {
List() {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item).fontSize(20).margin({ left: 10 })
}
}
.onClick(() => {
this.data.pushData('item value: ' + this.data.totalCount())
})
}, item => item)
}
}
}
```
![LazyForEach1](figures/LazyForEach1.gif)
The preceding code initializes only three list elements during page loading and loads a new list item each time a list element is clicked.
## Prioritizing Conditional Rendering over Visibility Control
Use of the visibility attribute to hide or show a component, as in the code snippet below, results in re-creation of the component, leading to performance drop.
```ts
@Entry
@Component
struct MyComponent {
@State isVisible: Visibility = Visibility.Visible;
build() {
Column() {
Button ("Show/Hide")
.onClick(() => {
if (this.isVisible == Visibility.Visible) {
this.isVisible = Visibility.None
} else {
this.isVisible = Visibility.Visible
}
})
Row().visibility(this.isVisible)
.width(300).height(300).backgroundColor(Color.Pink)
}.width('100%')
}
}
```
To avoid the preceding issue, use the **if** statement instead. The sample code is as follows:
```ts
@Entry
@Component
struct MyComponent {
@State isVisible: boolean = true;
build() {
Column() {
Button ("Show/Hide")
.onClick(() => {
this.isVisible = !this.isVisible
})
if (this.isVisible) {
Row()
.width(300).height(300).backgroundColor(Color.Pink)
}
}.width('100%')
}
}
```
![isVisible](figures/isVisible.gif)
## Prioritizing Flex over Column/Row
By default, the flex container needs to re-lay out flex items to comply with the **flexShrink** and **flexGrow** settings. This may result in drop in rendering performance.
```ts
@Entry
@Component
struct MyComponent {
build() {
Flex({ direction: FlexDirection.Column }) {
Flex().width(300).height(200).backgroundColor(Color.Pink)
Flex().width(300).height(200).backgroundColor(Color.Yellow)
Flex().width(300).height(200).backgroundColor(Color.Grey)
}
}
}
```
To avoid the preceding issue, replace **Flex** with **Column** and **Row**, which can create the same page layout as **Flex** does.
```ts
@Entry
@Component
struct MyComponent {
build() {
Column() {
Row().width(300).height(200).backgroundColor(Color.Pink)
Row().width(300).height(200).backgroundColor(Color.Yellow)
Row().width(300).height(200).backgroundColor(Color.Grey)
}
}
}
```
![flex1](figures/flex1.PNG)
## Setting Width and Height for \<List> Components
When a **\<List>** component is nested within a **\<Scroll>** component, all of its content will be loaded if its width and height is not specified, which may result in performance drop.
```ts
@Entry
@Component
struct MyComponent {
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
build() {
Scroll() {
List() {
ForEach(this.arr, (item) => {
ListItem() {
Text(`item value: ${item}`).fontSize(30).margin({ left: 10 })
}.height(100)
}, (item) => item.toString())
}
}.backgroundColor(Color.Pink)
}
}
```
Therefore, in the above scenario, you are advised to set the width and height for the **\<List>** component as follows:
```ts
@Entry
@Component
struct MyComponent {
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
build() {
Scroll() {
List() {
ForEach(this.arr, (item) => {
ListItem() {
Text(`item value: ${item}`).fontSize(30).margin({ left: 10 })
}.height(100)
}, (item) => item.toString())
}.width('100%').height(500)
}.backgroundColor(Color.Pink)
}
}
```
![list1](figures/list1.gif)
## Minimizing White Blocks During Swiping
To minimize white blocks during swiping, expand the UI loading range by increasing the value of **cachedCount** for the **\<List>** and **\<Grid>** components. **cachedCount** indicates the number of list or grid items preloaded outside of the screen.
If an item needs to request an online image, set **cachedCount** as appropriate so that the image is downloaded in advance before the item comes into view on the screen, thereby reducing the number of white blocks.
The following is an example of using **cachedCount**:
```ts
@Entry
@Component
struct MyComponent {
private source: MyDataSource = new MyDataSource();
build() {
List() {
LazyForEach(this.source, item => {
ListItem() {
Text("Hello" + item)
.fontSize(50)
.onAppear(() => {
console.log("appear:" + item)
})
}
})
}.cachedCount(3) // Increase the number of list or grid items preloaded outside of the screen.
}
}
class MyDataSource implements IDataSource {
data: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
public totalCount(): number {
return this.data.length
}
public getData(index: number): any {
return this.data[index]
}
registerDataChangeListener(listener: DataChangeListener): void {
}
unregisterDataChangeListener(listener: DataChangeListener): void {
}
}
```
![list2](figures/list2.gif)
**Instructions**
A greater **cachedCount** value may result in higher CPU and memory overhead of the UI. Adjust the value by taking into account both the comprehensive performance and user experience.
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册