提交 c616179e 编写于 作者: E ester.zhou

Update docs (17050)

Signed-off-by: Nester.zhou <ester.zhou@huawei.com>
上级 cd29a802
...@@ -39,17 +39,37 @@ ...@@ -39,17 +39,37 @@
- [Resource Categories and Access](resource-categories-and-access.md) - [Resource Categories and Access](resource-categories-and-access.md)
- Learning ArkTS - Learning ArkTS
- [Getting Started with ArkTS](arkts-get-started.md) - [Getting Started with ArkTS](arkts-get-started.md)
- ArkTS Syntax (Declarative UI) - Basic Syntax
- [Basic UI Description](arkts-basic-ui-description.md) - [Basic Syntax Overview](arkts-basic-syntax-overview.md)
- [Declarative UI Description](arkts-declarative-ui-description.md)
- Custom Component
- [Creating a Custom Component](arkts-create-custom-components.md)
- [Page and Custom Component Lifecycle](arkts-page-custom-components-lifecycle.md)
- [\@Builder: Custom Builder Function](arkts-builder.md)
- [\@BuilderParam: @Builder Function Reference](arkts-builderparam.md)
- [\@Styles: Definition of Resusable Styles](arkts-style.md)
- [\@Extend: Extension of Built-in Components](arkts-extend.md)
- [stateStyles: Polymorphic Style](arkts-statestyles.md)
- State Management - State Management
- [Basic Concepts](arkts-state-mgmt-concepts.md) - [State Management Overview](arkts-state-management-overview.md)
- [State Management with Page-level Variables](arkts-state-mgmt-page-level.md) - Component State Management
- [State Management with Application-level Variables](arkts-state-mgmt-application-level.md) - [\@State: State Owned by Component](arkts-state.md)
- [Dynamic UI Element Building](arkts-dynamic-ui-elememt-building.md) - [\@Prop: One-Way Synchronization from Parent to Child Components](arkts-prop.md)
- [Rendering Control](arkts-rendering-control.md) - [\@Link: Two-Way Synchronization Between Parent and Child Components](arkts-link.md)
- [Restrictions and Extensions](arkts-restrictions-and-extensions.md) - [\@Provide and \@Consume: Two-Way Synchronization with Descendant Components](arkts-provide-and-consume.md)
- FAQs - [\@Observed and \@ObjectLink: Observing Attribute Changes in Nested Class Objects](arkts-observed-and-objectlink.md)
- [Full SDK Compilation Guide](full-sdk-compile-guide.md) - Application State Management
- [Guide to Switching to Full SDK](full-sdk-switch-guide.md) - [Application State Management Overview](arkts-application-state-management-overview.md)
- Tools - [LocalStorage: UI State Storage](arkts-localstorage.md)
- [DevEco Studio (OpenHarmony) User Guide](deveco-studio-user-guide-for-openharmony.md) - [AppStorage: Application-wide UI State Storage](arkts-appstorage.md)
\ No newline at end of file - [PersistentStorage: Application State Persistence](arkts-persiststorage.md)
- [Environment: Device Environment Query](arkts-environment.md)
- Other State Management Features
- [Overview of Other State Management Features](arkts-other-state-mgmt-functions-overview.md)
- [\@Watch: Getting Notified of State Variable Changes](arkts-watch.md)
- [$$ Syntax: Two-Way Synchronization of Built-in Components](arkts-two-way-sync.md)
- Rendering Control
- [Rendering Control Overview](arkts-rendering-control-overview.md)
- [if/else: Conditional Rendering](arkts-rendering-control-ifelse.md)
- [ForEach: Rendering of Repeated Content](arkts-rendering-control-foreach.md)
- [LazyForEach: Lazy Data Loading](arkts-rendering-control-lazyforeach.md)
# Application State Management Overview
The decorators described in the previous topics are used to share state variables within a page, that is, within a component tree. If you want to share state data at the application level or across multiple pages, you would need to apply application-level state management. ArkTS provides a wide variety of application state management capabilities:
- [LocalStorage](arkts-localstorage.md): API for storing the UI state, usually used for state sharing within a [UIAbility](https://gitee.com/openharmony/docs/blob/master/en/application-dev/reference/apis/js-apis-app-ability-uiAbility.md) or between pages.
- [AppStorage](arkts-appstorage.md): special, singleton LocalStorage object within the application, which is created by the UI framework at application startup and provides the central storage for application UI state attributes.
- [PersistentStorage](arkts-persiststorage.md): API for persisting application attributes. It is usually used together with AppStorage to persist selected AppStorage attributes to the disk so that their values are the same upon application re-start as they were when the application was closed.
- [Environment](arkts-environment.md): a range of environment parameters regarding the device where the application runs. The environment parameters are synchronized to the AppStorage and can be used together with the AppStorage.
# AppStorage: Application-wide UI State Storage
AppStorage provides the central storage for mutable application UI state attributes. It is bound to the application process and is created by the UI framework at application startup.
Unlike LocalStorage, which is usually used for page-level state sharing, AppStorage enables application-wide UI state sharing. AppStorage is equivalent to the hub of the entire application. [PersistentStorage](arkts-persiststorage.md) and [Environment](arkts-environment.md) data is passed first to AppStorage and then from AppStorage to the UI component.
This topic describes only the AppStorage application scenarios and related decorators: \@StorageProp and \@StorageLink.
## Overview
AppStorage is a singleton LocalStorage object that is created by the UI framework at application startup. Its purpose is to provide the central storage for mutable application UI state attributes. AppStorage retains all those attributes and their values as long as the application remains running. Attributes are accessed using a unique key string value.
UI components synchronize application state attributes with the AppStorage. Implementation of application business logic can access AppStorage as well.
Selected state attributes of AppStorage can be synched with different data sources or data sinks. Those data sources and sinks can be on a local or remote device, and have different capabilities, such as data persistence (see [PersistentStorage](arkts-persiststorage.md)). These data sources and sinks are implemented in the business logic, separate from the UI. Link those AppStorage attributes to [@StorageProp](#storageprop) and [@StorageLink](#storagelink) whose values should be kept until application re-start.
## \@StorageProp
As mentioned above, if you want to establish a binding between AppStorage and a custom component, you need to use the \@StorageProp and \@StorageLink decorators. Use \@StorageProp(key) or \@StorageLink(key) to decorate variables in the component. **key** identifies the attribute in AppStorage.
When a custom component is initialized, the \@StorageProp(key)/\@StorageLink(key) decorated variable is initialized with the value of the attribute with the given key in AppStorage. Local initialization is mandatory. If an attribute with the given key is missing from AppStorage, it will be added with the stated initializing value. (Whether the attribute with the given key exists in AppStorage depends on the application logic.)
By decorating a variable with \@StorageProp(key), a one-way data synchronization is established with the attribute with the given key in AppStorage. A local change can be made, but it will not be synchronized to AppStorage. An update to the attribute with the given key in AppStorage will overwrite local changes.
### Rules of Use
| \@StorageProp Decorator| Description |
| ------------------ | ---------------------------------------- |
| Decorator parameters | **key**: constant string, mandatory (note, the string is quoted) |
| Allowed variable types | Object, class, string, number, Boolean, enum, and array of these types. For details about the scenarios of nested objects, see [Observed Changes and Behavior](#observed-changes-and-behavior).<br>The type must be specified and must be the same as the corresponding attribute in LocalStorage. **any** is not supported. The **undefined** and **null** values are not allowed.|
| Synchronization type | One-way: from the attribute in AppStorage to the component variable.<br>The component variable can be changed locally, but an update from AppStorage will overwrite local changes.|
| Initial value for the decorated variable | Mandatory. It is used as the default value for initialization if the attribute does not exist in AppStorage.|
### Variable Transfer/Access Rules
| Transfer/Access | Description |
| ---------- | ---------------------------------------- |
| Initialization and update from the parent component| Forbidden.|
| Subnode initialization | Supported; can be used to initialize a n \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.|
| Access | None. |
**Figure 1** \@StorageProp initialization rule
![en-us_image_0000001552978157](figures/en-us_image_0000001552978157.png)
### Observed Changes and Behavior
**Observed Changes**
- When the decorated variable is of the Boolean, string, or number type, its value change can be observed.
- When the decorated variable is of the class or Object type, its value change and value changes of all its attributes, that is, the attributes that **Object.keys(observedObject)** returns.
- When the decorated variable is of the array type, the addition, deletion, and updates of array items can be observed.
**Framework Behavior**
- When the value change of the \@StorageProp(key) decorated variable is observed, the change is not synchronized to the attribute with the give key value in AppStorage.
- The value change of the \@StorageProp(key) decorated variable only applies to the private member variables of the current component, but not other variables bound to the key.
- When the data decorated by \@StorageProp(key) is a state variable, the change of the data is not synchronized to AppStorage, but the owning custom component is re-rendered.
- When the attribute with the given key in AppStorage is updated, the change is synchronized to all the \@StorageProp(key) decorated data, and the local changes of the data are overwritten.
## \@StorageLink
\@StorageLink(key) creates a two-way data synchronization with the attribute with the given key in AppStorage.
1. If a local change occurs, it is synchronized to AppStorage.
2. Changes in AppStorage are synchronized to all attributes with the given key, including one-way bound variables (\@StorageProp decorated variables and one-way bound variables created through \@Prop), two-way bound variables (\@StorageLink decorated variables and two-way bound variables created through \@Link), and other instances (such as PersistentStorage).
### Rules of Use
| \@StorageLink Decorator| Description |
| ------------------ | ---------------------------------------- |
| Decorator parameters | **key**: constant string, mandatory (note, the string is quoted) |
| Allowed variable types | Object, class, string, number, Boolean, enum, and array of these types. For details about the scenarios of nested objects, see [Observed Changes and Behavior](#observed-changes-and-behavior).<br>The type must be specified and must be the same as the corresponding attribute in AppStorage. **any** is not supported. The **undefined** and **null** values are not allowed.|
| Synchronization type | Two-way: from the attribute in AppStorage to the custom component variable and back|
| Initial value for the decorated variable | Mandatory. It is used as the default value for initialization if the attribute does not exist in AppStorage.|
### Variable Transfer/Access Rules
| Transfer/Access | Description |
| ---------- | ---------------------------------------- |
| Initialization and update from the parent component| Forbidden. |
| Subnode initialization | Supported; can be used to initialize a regular variable or \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.|
| Access | None. |
**Figure 2** \@StorageLink initialization rule
![en-us_image_0000001501938718](figures/en-us_image_0000001501938718.png)
### Observed Changes and Behavior
**Observed Changes**
- When the decorated variable is of the Boolean, string, or number type, its value change can be observed.
- When the decorated variable is of the class or Object type, its value change and value changes of all its attributes, that is, the attributes that **Object.keys(observedObject)** returns.
- When the decorated variable is of the array type, the addition, deletion, and updates of array items can be observed.
**Framework Behavior**
1. When the value change of the \@StorageLink(key) decorated variable is observed, the change is synchronized to the attribute with the give key value in AppStorage.
2. Once the attribute with the given key in AppStorage is updated, all the data (including \@StorageLink and \@StorageProp decorated variables) bound to the attribute key is changed synchronously.
3. When the data decorated by \@StorageLink(key) is a state variable, the change of the data is synchronized to AppStorage, and the owning custom component is re-rendered.
## Application Scenarios
### Example of Using AppStorage and LocalStorage from Application Logic
Since AppStorage is a singleton, its APIs are all static ones. How these APIs work resembles the non-static APIs of LocalStorage.
```ts
AppStorage.SetOrCreate('PropA', 47);
let storage: LocalStorage = new LocalStorage({ 'PropA': 17 });
let propA: number = AppStorage.Get('PropA') // propA in AppStorage == 47, propA in LocalStorage == 17
var link1: SubscribedAbstractProperty<number> = AppStorage.Link('PropA'); // link1.get() == 47
var link2: SubscribedAbstractProperty<number> = AppStorage.Link('PropA'); // link2.get() == 47
var prop: SubscribedAbstractProperty<number> = AppStorage.Prop('PropA'); // prop.get() = 47
link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get()=1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49
storage.get('PropA') // == 17
storage.set('PropA', 101);
storage.get('PropA') // == 101
AppStorage.Get('PropA') // == 49
link1.get() // == 49
link2.get() // == 49
prop.get() // == 49
```
### Example of Using AppStorage and LocalStorage from Inside the UI
\@StorageLink works together with the AppStorage in the same way as \@LocalStorageLink works together with LocalStorage. It creates two-way data synchronization with an attribute in AppStorage.
```ts
AppStorage.SetOrCreate('PropA', 47);
let storage = new LocalStorage({ 'PropA': 48 });
@Entry(storage)
@Component
struct CompA {
@StorageLink('PropA') storLink: number = 1;
@LocalStorageLink('PropA') localStorLink: number = 1;
build() {
Column({ space: 20 }) {
Text(`From AppStorage ${this.storLink}`)
.onClick(() => this.storLink += 1)
Text(`From LocalStorage ${this.localStorLink}`)
.onClick(() => this.localStorLink += 1)
}
}
}
```
## Restrictions
When using AppStorage together with [PersistentStorage](arkts-persiststorage.md) and [Environment](arkts-environment.md), pay attention to the following:
- A call to **PersistentStorage.PersistProp()** after creating the attribute in AppStorage uses the type and value in AppStorage and overwrites any attribute with the same name in PersistentStorage. In light of this, the opposite order of calls is recommended. For an example of incorrect usage, see [Accessing Attribute in AppStorage Before PersistentStorage](arkts-persiststorage.md#accessing-attribute-in-appstorage-before-persistentstorage).
- A call to **Environment.EnvProp()** after creating the attribute in AppStorage will fail. This is because AppStorage already has an attribute with the same name, and the environment variable will not be written into AppStorage. Therefore, you are advised not to use the preset environment variable name in AppStorage.
# Basic Syntax Overview
With a basic understanding of the ArkTS language, let's look into the basic composition of ArkTS through an example. As shown below, when the user clicks the button, the text content changes from **Hello World** to **Hello ArkUI**.
**Figure 1** Example effect drawing
![Video_2023-03-06_152548](figures/Video_2023-03-06_152548.gif)
In this example, the basic composition of ArkTS is as follows.
**Figure 2** Basic composition of ArkTS
![arkts-basic-grammar](figures/arkts-basic-grammar.png)
- Decorator: design pattern used to decorate classes, structures, methods, and variables to assign special meanings to them. In the preceding sample code, \@Entry, \@Component, and \@State are decorators. \@Component indicates a custom component, \@Entry indicates that the custom component is an entry component, and \@State indicates a state variable in the component, whose change will trigger the UI to re-render.
- [UI description](arkts-declarative-ui-description.md): declarative description of the UI structure, such as the code block of the **build()** method.
- [Custom component](arkts-create-custom-components.md): reusable UI unit, which can be combined with other components, such as the struct **Hello** decorated by @Component.
- Built-in component: default basic or container component preset in ArkTS, which can be directly invoked, such as** \<Column>**,** \<Text>**, **\<Divider>**, and **\<Button>** components in the sample code.
- Attribute method: method used to configure component attributes, such as **fontSize()**, **width()**, **height()**, and **color()**. You can configure multiple attributes of a component in method chaining mode.
- Event method: method used to add the logic for a component to respond to an event. In the sample code, **onClick()** following **Button** is an event method. You can configure response logic for multiple events in method chaining mode.
ArkTS extends multiple syntax paradigms to make development a more enjoyable experience.
- [@Builder](arkts-builder.md)/[@BuilderParam](arkts-builderparam.md): special method for encapsulating UI descriptions. It enables UI descriptions to be encapsulated and reused in a fine-grained manner.
- [@Extend](arkts-extend.md)/[@Style](arkts-style.md): decorator that extends built-in components and encapsulates attribute styles to combine built-in components more flexibly.
- [stateStyles](arkts-statestyles.md): polymorphic style, which can be set based on the internal state of the component.
# Basic UI Description
In ArkTS, you define a custom component by using decorators **@Component** and **@Entry** to decorate a data structure declared with the **struct** keyword. A custom component provides a **build** function, where you must write the basic UI description in chain call mode. For details about the UI description, see [UI Description Specifications](#ui-description-specifications).
## Basic Concepts
- struct: a data structure that can be used to implement custom components and cannot have inheritance. The **new** keyword can be omitted when initializing a struct.
- Decorator: a special type of declaration that can be applied to classes, structures, or class attributes to add new functionality to them. Multiple decorators can be applied to the same target element and defined on a single line or multiple lines. It is recommended that the decorators be defined on multiple lines.
```ts
@Entry
@Component
struct MyComponent {
}
```
- **build** function: A custom component must implement the **build** function and must implement no constructor. The **build** function meets the definition of the **Builder** API and is used to define the declarative UI description of components.
```ts
interface Builder {
build: () => void
}
```
- **@Component**: a decorator applied to a struct to equip it with the component-based capability. The **build** method must be implemented for UI creation.
- **@Entry**: a decorator applied to a struct to make it the entry to a page, which is rendered and displayed when the page is loaded.
- **@Preview**: a decorator applied to struct to make it previewable in the DevEco Studio Previewer. The decorated component is created and displayed when the residing page is loaded.
> **NOTE**
>
> In a single source file, you can use up to 10 **@Preview** decorators to decorate custom components. For details, see [Previewing ArkTS Components](https://developer.harmonyos.com/en/docs/documentation/doc-guides/ohos-previewing-app-service-0000001218760596#section146052489820).
- Chain call: a syntax for configuring the attribute methods, event methods, and more of UI components by using the dot notation.
## UI Description Specifications
### Structs Without Parameters
A struct without parameters is a component whose API definition has empty parentheses. No parameter needs to be passed to this type of component, for example, the **Divider** component in the following snippet:
```ts
Column() {
Text('item 1')
Divider()
Text('item 2')
}
```
### Structs with Parameters
A struct with parameters is a component whose API definition expects parameters enclosed in the parentheses. You can use constants to assign values to the parameters.
Sample code:
- Set the mandatory parameter **src** of the **\<Image>** component as follows:
```ts
Image('https://xyz/test.jpg')
```
- Set the optional parameter **content** of the **\<Text>** component as follows:
```ts
Text('test')
```
You can use variables or expressions to assign values to parameters. The result type returned by an expression must meet the parameter type requirements. For details about the variables, see [State Management with Page-level Variables](arkts-state-mgmt-page-level.md) and [State Management with Application-level Variables](arkts-state-mgmt-application-level.md). For example, set a variable or expression to construct the **\<Image>** and **\<Text>** components:
```ts
Image(this.imagePath)
Image('https://' + this.imageUrl)
Text(`count: ${this.count}`)
```
### Attribute Configuration
Component attributes are configured using an attribute method, which follows the corresponding component and is bound to the component using the "**.**" operator.
- Example of configuring the font size attribute of the **\<Text>** component:
```ts
Text('test')
.fontSize(12)
```
- Example of configuring multiple attributes at the same time by using the "**.**" operator to implement chain call:
```ts
Image('test.jpg')
.alt('error.jpg')
.width(100)
.height(100)
```
- Example of passing variables or expressions in addition to constants:
```ts
Text('hello')
.fontSize(this.size)
Image('test.jpg')
.width(this.count % 2 === 0 ? 100 : 200)
.height(this.offset + 100)
```
- For attributes of built-in components, ArkUI also provides some predefined [enumeration types](../reference/arkui-ts/ts-appendix-enums.md), which you can pass as parameters to methods if they meet the parameter type requirements. For example, you can configure the font color and weight attributes of the **\<Text>** component as follows:
```ts
Text('hello')
.fontSize(20)
.fontColor(Color.Red)
.fontWeight(FontWeight.Bold)
```
### Event Configuration
Events supported by components are configured using event methods, which each follow the corresponding component and are bound to the component using the "**.**" operator.
- Example of using a lambda expression to configure the event of a component:
```ts
Button('add counter')
.onClick(() => {
this.counter += 2;
})
```
- Example of using an anonymous function expression to configure the event of a component (**bind** must be used to ensure that the contained components are referenced by **this** in the function body):
```ts
Button('add counter')
.onClick(function () {
this.counter += 2;
}.bind(this))
```
- Example of using a component's member function to configure the event of the component:
```ts
myClickHandler(): void {
this.counter += 2;
}
...
Button('add counter')
.onClick(this.myClickHandler.bind(this))
```
### Child Component Configuration
For a component that supports child components, for example, a container component, add the UI descriptions of the child components inside parentheses. The **\<Column>**, **\<Row>**, **\<Stack>**, **\<Grid>**, and **\<List>** components are all container components.
- Simple example of the **\<Column>** component:
```ts
Column() {
Text('Hello')
.fontSize(100)
Divider()
Text(this.myText)
.fontSize(100)
.fontColor(Color.Red)
}
```
- Example of nesting multiple child components in the **\<Column>** component:
```ts
Column() {
Row() {
Image('test1.jpg')
.width(100)
.height(100)
Button('click +1')
.onClick(() => {
console.info('+1 clicked!');
})
}
Divider()
Row() {
Image('test2.jpg')
.width(100)
.height(100)
Button('click +2')
.onClick(() => {
console.info('+2 clicked!');
})
}
Divider()
Row() {
Image('test3.jpg')
.width(100)
.height(100)
Button('click +3')
.onClick(() => {
console.info('+3 clicked!');
})
}
}
```
# \@Builder: Custom Builder Function
After a custom component is created, its internal UI structure is fixed and allows only data passing with its caller. ArkUI also provides a more lightweight mechanism for reusing UI elements: \@Builder. An \@Builder decorated function is a special function that serves similar purpose as the build function. The \@Builder function body follows the same syntax rules as the **build** function. You can abstract reusable UI elements into a method and call the method in **build**.
To simplify language, here we refer to an \@Builder decorated function also as a custom builder function.
> **NOTE**
>
> Since API version 9, this decorator is supported in ArkTS widgets.
## Rules of Use
### Custom Builder Function
Syntax:
```ts
@Builder myBuilderFunction({ ... })
```
Usage:
```ts
this.myBuilderFunction({ ... })
```
- Defining one or more custom builder (\@Builder decorated) functions inside a custom component is allowed. Such a custom builder function can be considered as a private, special type of member functions of that component.
- The custom builder function can be called from the owning component's** build** or another custom builder (within that custom component) function only.
- Inside the custom builder function body, **this** refers to the owning component. Component state variables are accessible from within the custom builder function implementation. Using the custom components' state variables is recommended over parameter passing.
### Global Custom Builder Function
Syntax:
```ts
@Builder function MyGlobalBuilderFunction({ ... })
```
Usage:
```ts
MyGlobalBuilderFunction()
```
- A global custom builder function is accessible from the entire application. **this** and the **bind** method are not allowed.
- Use of a global custom builder function is recommended if no own state is required.
## Parameter Passing Rules
There are two types of parameter passing for custom builder functions: [by-value parameter passing](#by-value-parameter-passing) and [by-reference parameter passing](#by-reference-parameter-passing). Both of them must comply with the following rules:
- The parameter type must be the same as the declared parameter type. The **undefined** or **null** constants as well as expressions evaluating to these values are not allowed.
- All parameters are immutable inside the custom builder function. If mutability and synchronization of the mutation is required, the custom builder should be replaced by a custom component with a [@Link](arkts-link.md) decorated variable.
- The \@Builder function body follows the same [syntax rules](arkts-create-custom-components.md#build-function) as the **build** function.
### By-Reference Parameter Passing
In by-reference parameter passing, the passed parameters can be state variables, and the change of these state variables causes the UI re-rendering in the \@Builder method. ArkUI provides $$ as a paradigm for by-reference parameter passing.
```ts
ABuilder( $$ : { paramA1: string, paramB1 : string } );
```
```ts
@Builder function ABuilder($$: { paramA1: string }) {
Row() {
Text(`UseStateVarByReference: ${$$.paramA1} `)
}
}
@Entry
@Component
struct Parent {
@State label: string = 'Hello';
build() {
Column() {
// Pass the this.label reference to the ABuilder component when the ABuilder component is called in the Parent component.
ABuilder({ paramA1: this.label })
Button('Click me').onClick(() => {
// After Click me is clicked, the UI text changes from Hello to ArkUI.
this.label = 'ArkUI';
})
}
}
}
```
### By-Value Parameter Passing
By default, parameters in the \@Builder decorated functions are passed by value. When the passed parameter is a state variable, the change of the state variable does not cause the UI re-rendering in the \@Builder method. Therefore, when using state variables, you are advised to use [by-reference parameter passing](#by-reference-parameter-passing).
```ts
@Builder function ABuilder(paramA1: string) {
Row() {
Text(`UseStateVarByValue: ${paramA1} `)
}
}
@Entry
@Component
struct Parent {
label: string = 'Hello';
build() {
Column() {
ABuilder(this.label)
}
}
}
```
# \@BuilderParam: @Builder Function Reference
In certain circumstances, you may need to add a specific function, such as a click-to-jump action, to a custom component. However, embedding an event method directly inside of the component will add the function to all places where the component is imported. This is where the \@BuilderParam decorator comes into the picture. \@BuilderParam is used to decorate a custom component member variable of type reference to an \@Builder method. When initializing a custom component, you can assign a value to the variable, thereby adding the specific function to the custom component. This decorator can be used to declare an element of any UI description, similar to a slot placeholder.
> **NOTE**
>
> Since API version 9, this decorator is supported in ArkTS widgets.
## Rules of Use
### Initializing \@BuilderParam Decorated Method
An \@BuildParam decorated method can be initialized only by an \@Builder function reference.
- Local initialization with the owning component's custom \@Builder function or a global \@Builder function reference
```ts
@Builder function GlobalBuilder0() {}
@Component
struct Child {
@Builder doNothingBuilder() {};
@BuilderParam aBuilder0: () => void = this.doNothingBuilder;
@BuilderParam aBuilder1: () => void = GlobalBuilder0;
build(){}
}
```
- Initialization from the parent component
```ts
@Component
struct Child {
@BuilderParam aBuilder0: () => void;
build() {
Column() {
this.aBuilder0()
}
}
}
@Entry
@Component
struct Parent {
@Builder componentBuilder() {
Text(`Parent builder `)
}
build() {
Column() {
Child({ aBuilder0: this.componentBuilder })
}
}
}
```
- **this** in the function body points to the correct object.
In the following example, when the **Parent** component calls **this.componentBuilder()**, **this.label** points to the owning component, that is, **Parent**. With **\@BuilderParam aBuilder0** passed to the **Child** component from **\@Builder componentBuilder()**, when the **Child** component calls **this.aBuilder0()**, **this.label** points to the label of the **Child** component, that is, **Child**.
> **NOTE**
>
> Exercise caution when using **bind** to change the context of function invoking, which may cause **this** to point to an incorrect object.
```ts
@Component
struct Child {
label: string = `Child`
@BuilderParam aBuilder0: () => void;
build() {
Column() {
this.aBuilder0()
}
}
}
@Entry
@Component
struct Parent {
label: string = `Parent`
@Builder componentBuilder() {
Text(`${this.label}`)
}
build() {
Column() {
this.componentBuilder()
Child({ aBuilder0: this.componentBuilder })
}
}
}
```
## Application Scenarios
### Component Initialization Through Parameters
An \@BuilderParam decorated method can be a method with or without parameters. Whether it contains parameters should match that of the assigned \@Builder method. The type of the \@BuilderParam decorated method must also match that of the assigned \@Builder method.
```ts
@Builder function GlobalBuilder1($$ : {label: string }) {
Text($$.label)
.width(400)
.height(50)
.backgroundColor(Color.Blue)
}
@Component
struct Child {
label: string = 'Child'
// Without parameters. The pointed componentBuilder is also without parameters.
@BuilderParam aBuilder0: () => void;
// With parameters. The pointed GlobalBuilder1 is also with parameters.
@BuilderParam aBuilder1: ($$ : { label : string}) => void;
build() {
Column() {
this.aBuilder0()
this.aBuilder1({label: 'global Builder label' } )
}
}
}
@Entry
@Component
struct Parent {
label: string = 'Parent'
@Builder componentBuilder() {
Text(`${this.label}`)
}
build() {
Column() {
this.componentBuilder()
Child({ aBuilder0: this.componentBuilder, aBuilder1: GlobalBuilder1 })
}
}
}
```
### Example of Component Initialization Through Trailing Closure
In a custom component, the \@BuilderParam decorated attribute can be initialized using a trailing closure. During initialization, the component name is followed by a pair of braces ({}) to form a trailing closure.
> **NOTE**
> In this scenario, the custom component has one and only one \@BuilderParam decorated attribute.
You can pass the content in the trailing closure to \@BuilderParam as an \@Builder decorated method. Example:
```ts
// xxx.ets
@Component
struct CustomContainer {
@Prop header: string;
@BuilderParam closer: () => void
build() {
Column() {
Text(this.header)
.fontSize(30)
this.closer()
}
}
}
@Builder function specificParam(label1: string, label2: string) {
Column() {
Text(label1)
.fontSize(30)
Text(label2)
.fontSize(30)
}
}
@Entry
@Component
struct CustomContainerUser {
@State text: string = 'header';
build() {
Column() {
// Create the CustomContainer component. During initialization, append a pair of braces ({}) to the component name to form a trailing closure.
// Used as the parameter passed to CustomContainer @BuilderParam closer: () => void.
CustomContainer({ header: this.text }) {
Column() {
specificParam('testA', 'testB')
}.backgroundColor(Color.Yellow)
.onClick(() => {
this.text = 'changeHeader';
})
}
}
}
}
```
# 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.
The custom component has the following features:
- Combinable: allows you to combine built-in components and other components, as well as their attributes and methods.
- Reusable: can be reused by other components and used as different instances in different parent components or containers.
- Data-driven update: holds some state and triggers UI re-rendering with the change of state variables.
The following example shows the basic usage of a custom component.
```ts
@Component
struct HelloComponent {
@State message: string = 'Hello, World!';
build() {
// The HelloComponent custom component combines the <Row> and <Text> built-in components.
Row() {
Text(this.message)
.onClick(() => {
// The change of the state variable message drives the UI to be re-rendered. As a result, the text changes from "Hello, World!" to "Hello, ArkUI!".
this.message = 'Hello, ArkUI!';
})
}
}
}
```
Multiple **HelloComponent** instances can be created in the **build()** function of other custom components. In this way, **HelloComponent** is reused by those custom components.
```ts
@Entry
@Component
struct ParentComponent {
build() {
Column() {
Text('ArkUI message')
HelloComponent({ message: 'Hello, World!' });
Divider()
HelloComponent ({ message: 'Hello!' });
}
}
}
```
To fully understand the preceding example, a knowledge of the following concepts is essential:
- [Basic Structure of a Custom Component](#basic-structure-of-a-custom-component)
- [Member Functions/Variables](#member-functionsvariables)
- [Rules of for Custom Component Parameters](#rules-of-for-custom-component-parameters)
- [build Function](#build-function)
- [Universal Style of a Custom Component](#universal-style-of-a-custom-component)
- [Custom Attribute Methods](#custom-attribute-methods)
## Basic Structure of a Custom Component
- struct: The definition of a custom component must start with the \@Component struct followed by the component name, and then component body enclosed by curly brackets {....}. No inheritance is allowed. You can omit the **new** operator when instantiating a struct.
> **NOTE**
>
> The name or its class or function name of a custom component must be different from that of any built-in components.
- \@Component: The \@Component decorator can decorate only the data structures declared by the **struct** keyword. After being decorated by \@Component, a struct has the componentization capability. It must implement the **build** function to describe the UI. One struct can be decorated by only one \@Component.
> **NOTE**
>
> Since API version 9, this decorator is supported in ArkTS widgets.
```ts
@Component
struct MyComponent {
}
```
- build(): The **build()** function is used to define the declarative UI description of a custom component. Every custom component must define a **build()** function.
```ts
@Component
struct MyComponent {
build() {
}
}
```
- \@Entry: A custom component decorated with \@Entry is used as the default entry component of the page. At most one component can be decorated with \@Entry in a single source file. The \@Entry decorator accepts an optional parameter of type [LocalStorage](arkts-localstorage.md).
> **NOTE**
>
> Since API version 9, this decorator is supported in ArkTS widgets.
```ts
@Entry
@Component
struct MyComponent {
}
```
## Member Functions/Variables
In addition to the mandatory** build()** function, a custom component may implement other member functions with the following restrictions:
- Static functions are not supported.
- Access to the member functions is always private. Defining **private** access is optional. Defining access other than **private** is a syntax error.
A custom component can also implement member variables with the following restrictions:
- Static member variables are not supported.
- Access to the member variables is always private.The access rules of member variables are the same as those of member functions.
- Local initialization is optional for some member variables and mandatory for others. For details about whether local initialization or initialization from the parent component is required, see [State Management](arkts-state-management-overview.md).
## Rules of for Custom Component Parameters
As can be learnt from preceding examples, a custom component can be created from a **build** or [@Builder](arkts-builder.md) function, and during the creation, parameters can be supplied to the component.
```ts
@Component
struct MyComponent {
private countDownFrom: number = 0;
private color: Color = Color.Blue;
build() {
}
}
@Entry
@Component
struct ParentComponent {
private someColor: Color = Color.Pink;
build() {
Column() {
// Create an instance of MyComponent and initialize its countDownFrom variable with the value 10 and its color variable with the value this.someColor.
MyComponent({ countDownFrom: 10, color: this.someColor })
}
}
}
```
## build Function
All languages declared in the **build** function are called UI description languages. The UI description languages must comply with the following rules:
- For an \@Entry decorated custom component, exactly one root component is required under the **build** function. This root component must be a container component. **ForEach** is not allowed at the top level.
For an \@Component decorated custom component, exactly one root component is required under the **build** function. This root component is not necessarily a container component. **ForEach** is not allowed at the top level.
```ts
@Entry
@Component
struct MyComponent {
build() {
// Exactly one root component is required, and it must be a container component.
Row() {
ChildComponent()
}
}
}
@Component
struct ChildComponent {
build() {
// Exactly one root component is required, and it is not necessarily a container component.
Image('test.jpg')
}
}
```
- Local variable declaration is not allowed. The following example is invalid:
```ts
build() {
// Invalid: Local variable declaration is not allowed.
let a: number = 1;
}
```
- **console.info** cannot be directly used in the UI description, but can be used in methods or functions. The following is an example:
```ts
build() {
// Invalid: Use of console.info is not allowed.
console.info('print debug log');
}
```
- Creation of a local scope is not allowed. The following example is invalid:
```ts
build() {
// Invalid: Creation of local scope is not allowed.
{
...
}
}
```
- Calling a function other than the \@Builder decorated is not allowed. The parameters of built-in components can be the return values of TS methods.
```ts
@Component
struct ParentComponent {
doSomeCalculations() {
}
calcTextValue(): string {
return 'Hello World';
}
@Builder doSomeRender() {
Text(`Hello World`)
}
build() {
Column() {
// Invalid: No function calls except @Builder functions.
this.doSomeCalculations();
// Valid: The function can be called.
this.doSomeRender();
// Valid: The parameter can be the return value of a TS method.
Text(this.calcTextValue())
}
}
}
```
- The **switch** syntax is not allowed. Use **if** instead. The following example is invalid:
```ts
build() {
Column() {
// Invalid: The switch syntax is not allowed.
switch (expression) {
case 1:
Text('...')
break;
case 2:
Image('...')
break;
default:
Text('...')
break;
}
}
}
```
- Expressions are not allowed. The following example is invalid:
```ts
build() {
Column() {
// Invalid: Expressions are not allowed.
(this.aVar > 10) ? Text('...') : Image('...')
}
}
```
## Universal Style of a Custom Component
The universal style of a custom component is configured by invoking chainable attribute methods.
```ts
@Component
struct MyComponent2 {
build() {
Button(`Hello World`)
}
}
@Entry
@Component
struct MyComponent {
build() {
Row() {
MyComponent2()
.width(200)
.height(300)
.backgroundColor(Color.Red)
}
}
}
```
> **NOTE**
>
> When ArkUI sets styles for custom components, an invisible container component is set for **MyComponent2**. These styles are set on the container component instead of the **\<Button>** component of **MyComponent2**. As seen from the rendering result, the red background color is not directly applied to the button. Instead, it is applied to the container component that is invisible to users where the button is located.
## Custom Attribute Methods
Custom components do not support custom attribute methods. You can use the Controller capability to implement custom APIs.
```ts
// Custom controller
export class MyComponentController {
item: MyComponent = null;
setItem(item: MyComponent) {
this.item = item;
}
changeText(value: string) {
this.item.value = value;
}
}
// Custom component
@Component
export default struct MyComponent {
public controller: MyComponentController = null;
@State value: string = 'Hello World';
build() {
Column() {
Text(this.value)
.fontSize(50)
}
}
aboutToAppear() {
if (this.controller)
this.controller.setItem (this); // Link to the controller.
}
}
// Processing logic
@Entry
@Component
struct StyleExample {
controller = new MyComponentController();
build() {
Column() {
MyComponent({ controller: this.controller })
}
.onClick(() => {
this.controller.changeText('Text');
})
}
}
```
In the preceding example:
1. The **aboutToAppear** method of the **MyComponent** child component passes the current **this** pointer to the **item** member variable of **MyComponentController**.
2. The **StyleExample** parent component holds a **Controller** instance and with which calls the **changeText** API of **Controller**. That is, the value of the state variable **value** of **MyComponent** is changed through the **this** pointer of the **MyComponent** child component held by the controller.
Through the encapsulation of the controller, **MyComponent** exposes the **changeText** API. All instances that hold the controller can call the **changeText** API to change the value of the **MyComponent** state variable **value**.
# Declarative UI Description
ArkTS declaratively combines and extends components to describe the UI of an application. It also provides basic methods for configuring attributes, events, and child components to help you implement application interaction logic.
## Creating a Component
Depending on the builder, you can create components with or without mandatory parameters.
> **NOTE**
>
> The **new** operator is not required when you create a component.
### Without Mandatory Parameters
A struct without mandatory parameters is a component whose API definition has empty parentheses. No parameter needs to be passed to this type of component, for example, the **Divider** component in the following snippet:
```ts
Column() {
Text('item 1')
Divider()
Text('item 2')
}
```
### With Mandatory Parameters
A struct with mandatory parameters is a component whose API definition expects parameters enclosed in the parentheses.
- Set the mandatory parameter **src** of the **\<Image>** component as follows:
```ts
Image('https://xyz/test.jpg')
```
- Set the optional parameter **content** of the **\<Text>** component.
```ts
// Parameter of the string type
Text('test')
// Add application resources in $r format, which can be used in multi-language scenarios.
Text($r('app.string.title_value'))
// No mandatory parameters
Text()
```
- You can also use variables or expressions to assign values to parameters. The result type returned by an expression must meet the parameter type requirements.
For example, to set a variable or expression to construct the **\<Image>** and **\<Text>** components:
```ts
Image(this.imagePath)
Image('https://' + this.imageUrl)
Text(`count: ${this.count}`)
```
## Configuring Attributes
Use chainable attribute methods to configure the style and other attributes of built-in components. It is recommended that a separate line be used for each attribute method.
- Example of configuring the **fontSize** attribute for the **\<Text>** component:
```ts
Text('test')
.fontSize(12)
```
- Example of configuring multiple attributes for the **\<Image>** component:
```ts
Image('test.jpg')
.alt('error.jpg')
.width(100)
.height(100)
```
- Attribute methods accept expressions and variables as well constant parameters.
```ts
Text('hello')
.fontSize(this.size)
Image('test.jpg')
.width(this.count % 2 === 0 ? 100 : 200)
.height(this.offset + 100)
```
- For built-in components, ArkUI also predefines some enumeration types. These enumeration types can be passed as parameters, as long as they meet the parameter type requirements.
Example of configuring the font color and style of the **\<Text>** component:
```ts
Text('hello')
.fontSize(20)
.fontColor(Color.Red)
.fontWeight(FontWeight.Bold)
```
## Handling Events
Use chainable event methods to configure events supported by built-in components. It is recommended that a separate line be used for each event method.
- Example of using a lambda expression to configure the event of a component:
```ts
Button('Click me')
.onClick(() => {
this.myText = 'ArkUI';
})
```
- Example of using an anonymous function expression to configure the event of a component (**bind** must be used to ensure that the current components are referenced by **this **in the function body):
```ts
Button('add counter')
.onClick(function(){
this.counter += 2;
}.bind(this))
```
- Example of using a component's member function to configure the event of the component:
```ts
myClickHandler(): void {
this.counter += 2;
}
...
Button('add counter')
.onClick(this.myClickHandler.bind(this))
```
## Configuring Child Components
For a component that supports child components, for example, a container component, add the UI descriptions of the child components inside parentheses. The **\<Column>**, **\<Row>**, **\<Stack>**, **\<Grid>**, and **\<List>** components are all container components.
- Simple example of configuring child components for the **\<Column>** component:
```ts
Column() {
Text('Hello')
.fontSize(100)
Divider()
Text(this.myText)
.fontSize(100)
.fontColor(Color.Red)
}
```
- Example of nesting multiple child components in the **\<Column>** component:.
```ts
Column() {
Row() {
Image('test1.jpg')
.width(100)
.height(100)
Button('click +1')
.onClick(() => {
console.info('+1 clicked!');
})
}
}
```
# Dynamic UI Element Building
After you've created a custom component (as described in [Basic UI Description](arkts-basic-ui-description.md)), you can customize the internal UI structure for the component, by drawing on the capability of dynamic UI element building.
## @Builder
The **@Builder** decorator is used to decorate a function for quickly generating multiple layouts in a custom component. This function can be declared outside the **build** function and used in the **build** function or other **@Builder** decorated functions. The following example shows how to use **@Builder**.
```ts
// xxx.ets
@Component
struct CompB {
@State CompValue: string = ''
aboutToAppear() {
console.info('CompB aboutToAppear.')
}
aboutToDisappear() {
console.info('CompB aboutToDisappear.')
}
build() {
Column() {
Button(this.CompValue)
.margin(5)
}
}
}
@Entry
@Component
struct CompA {
size1: number = 100
@State CompValue1: string = "Hello,CompValue1"
@State CompValue2: string = "Hello,CompValue2"
@State CompValue3: string = "Hello,CompValue3"
// Use the custom component CompB in the @Builder decorated function CompC.
@Builder CompC(value: string) {
CompB({ CompValue: value })
}
@Builder SquareText(label: string) {
Text(label)
.fontSize(18)
.width(1 * this.size1)
.height(1 * this.size1)
}
// Use the @Builder decorated function SquareText in the @Builder decorated function RowOfSquareTexts.
@Builder RowOfSquareTexts(label1: string, label2: string) {
Row() {
this.SquareText(label1)
this.SquareText(label2)
}
.width(2 * this.size1)
.height(1 * this.size1)
}
build() {
Column() {
Row() {
this.SquareText("A")
this.SquareText("B")
}
.width(2 * this.size1)
.height(1 * this.size1)
this.RowOfSquareTexts("C", "D")
Column() {
// Use the @Builder decorated custom components three times.
this.CompC(this.CompValue1)
this.CompC(this.CompValue2)
this.CompC(this.CompValue3)
}
.width(2 * this.size1)
.height(2 * this.size1)
}
.width(2 * this.size1)
.height(2 * this.size1)
}
}
```
![builder](figures/builder.PNG)
## @BuilderParam<sup>8+</sup>
The **@BuilderParam** decorator is used to decorate the function type attributes (for example, **@BuilderParam noParam: () => void**) in a custom component. When the custom component is initialized, the attributes decorated by **@BuilderParam** must be assigned values.
### Background
In certain circumstances, you may need to add a specific function, such as a click-to-jump action, to a custom component. However, embedding an event method directly inside of the component will add the function to all places where the component is imported. This is where the **@BuilderParam** decorator comes into the picture. When initializing a custom component, you can assign a **@Builder** decorated method to the **@BuilderParam** decorated attribute, thereby adding the specific function to the custom component.
### Component Initialization Through Parameters
When initializing a custom component through parameters, assign a **@Builder** decorated method to the **@BuilderParam** decorated attribute — **content**, and call the value of **content** in the custom component. If no parameter is passed when assigning a value to the **@BuilderParam** decorated attribute (for example, **noParam: this.specificNoParam**), define the type of the attribute as a function without a return value (for example, **@BuilderParam noParam: () => void**). If any parameter is passed when assigning a value to the **@BuilderParam** decorated attribute (for example, **withParam: this.SpecificWithParam('WithParamA')**), define the type of the attribute as **any** (for example, **@BuilderParam withParam: any**).
```ts
// xxx.ets
@Component
struct CustomContainer {
header: string = ''
@BuilderParam noParam: () => void
@BuilderParam withParam: any
footer: string = ''
build() {
Column() {
Text(this.header)
.fontSize(30)
this.noParam()
this.withParam()
Text(this.footer)
.fontSize(30)
}
}
}
@Entry
@Component
struct CustomContainerUser {
@Builder specificNoParam() {
Column() {
Text('noParam').fontSize(30)
}
}
@Builder SpecificWithParam(label: string) {
Column() {
Text(label).fontSize(30)
}
}
build() {
Column() {
CustomContainer({
header: 'HeaderA',
noParam: this.specificNoParam,
withParam: this.SpecificWithParam('WithParamA'),
footer: 'FooterA'
})
Divider()
.strokeWidth(3)
.margin(10)
CustomContainer({
header: 'HeaderB',
noParam: this.specificNoParam,
withParam: this.SpecificWithParam('WithParamB'),
footer: 'FooterB'
})
}
}
}
```
![builder1](figures/builder1.PNG)
### Component Initialization Through Trailing Closure
In a custom component, the **@BuilderParam** decorated attribute can be initialized using a trailing closure. During initialization, the component name is followed by a pair of braces ({}) to form a trailing closure (**CustomContainer(){}**). You can consider a trailing closure as a container and add content to it. For example, you can add a component (**{Column(){...}**) to the closure. The syntax of the closure is the same as that of **build**. In this scenario, the custom component has one and only one **@BuilderParam** decorated attribute.
Example: Add a **\<Column>** component and a click event to the closure, and call the **specificParam** method decorated by **@Builder** in the new **\<Column>** component. After the **\<Column>** component is clicked, the value of the **CustomContainer** component's **header** attribute will change from **header** to **changeHeader**. When the component is initialized, the content of the trailing closure will be assigned to the **closer** attribute decorated by **@BuilderParam**.
```ts
// xxx.ets
@Component
struct CustomContainer {
header: string = ''
@BuilderParam closer: () => void
build() {
Column() {
Text(this.header)
.fontSize(30)
this.closer()
}
}
}
@Builder function specificParam(label1: string, label2: string) {
Column() {
Text(label1)
.fontSize(30)
Text(label2)
.fontSize(30)
}
}
@Entry
@Component
struct CustomContainerUser {
@State text: string = 'header'
build() {
Column() {
CustomContainer({
header: this.text,
}) {
Column() {
specificParam('testA', 'testB')
}.backgroundColor(Color.Yellow)
.onClick(() => {
this.text = 'changeHeader'
})
}
}
}
}
```
![builder2](figures/builder2.gif)
## @Styles
The **@Styles** decorator helps avoid repeated style setting, by extracting multiple style settings into one method. When declaring a component, you can invoke this method and use the **@Styles** decorator to quickly define and reuse the custom styles of a component. **@Styles** supports only universal attributes.
**@Styles** can be defined inside or outside a component declaration. When it is defined outside a component declaration, the component name must be preceded by the keyword **function**.
```ts
// xxx.ets
@Styles function globalFancy () {
.width(150)
.height(100)
.backgroundColor(Color.Pink)
}
@Entry
@Component
struct FancyUse {
@Styles componentFancy() {
.width(100)
.height(200)
.backgroundColor(Color.Yellow)
}
build() {
Column({ space: 10 }) {
Text('FancyA')
.globalFancy()
.fontSize(30)
Text('FancyB')
.globalFancy()
.fontSize(20)
Text('FancyC')
.componentFancy()
.fontSize(30)
Text('FancyD')
.componentFancy()
.fontSize(20)
}
}
}
```
![styles](figures/styles.PNG)
**@Styles** can also be used inside the **[StateStyles](../reference/arkui-ts/ts-universal-attributes-polymorphic-style.md)** attribute declaration of a component, to assign state-specific attributes to the component.
In **StateStyles**, **@Styles** decorated methods defined outside the component can be directly called, while those defined inside can be called only with the keyword **this**.
```ts
// xxx.ets
@Styles function globalFancy () {
.width(120)
.height(120)
.backgroundColor(Color.Green)
}
@Entry
@Component
struct FancyUse {
@Styles componentFancy() {
.width(80)
.height(80)
.backgroundColor(Color.Red)
}
build() {
Row({ space: 10 }) {
Button('Fancy')
.stateStyles({
normal: {
.width(100)
.height(100)
.backgroundColor(Color.Blue)
},
disabled: this.componentFancy,
pressed: globalFancy
})
}
}
}
```
![styles1](figures/styles1.gif)
## @Extend
The **@Extend** decorator adds new attribute methods to built-in components, such as **\<Text>**, **\<Column>**, and **\<Button>**. In this way, the built-in components are extended instantly.
```ts
// xxx.ets
@Extend(Text) function fancy (fontSize: number) {
.fontColor(Color.Red)
.fontSize(fontSize)
.fontStyle(FontStyle.Italic)
.fontWeight(600)
}
@Entry
@Component
struct FancyUse {
build() {
Row({ space: 10 }) {
Text("Fancy")
.fancy(16)
Text("Fancy")
.fancy(24)
Text("Fancy")
.fancy(32)
}
}
}
```
> **NOTE**
>
> - The **@Extend** decorator cannot be defined inside the struct of a custom component.
> - The **@Extend** decorator supports only attribute methods.
![extend](figures/extend.PNG)
## @CustomDialog
The **@CustomDialog** decorator is used to decorate custom dialog boxes, enabling their content and styles to be dynamically set.
```ts
// xxx.ets
@CustomDialog
struct DialogExample {
controller: CustomDialogController
action: () => void
build() {
Row() {
Button('Close CustomDialog')
.onClick(() => {
this.controller.close()
this.action()
})
}.padding(20)
}
}
@Entry
@Component
struct CustomDialogUser {
dialogController: CustomDialogController = new CustomDialogController({
builder: DialogExample({ action: this.onAccept }),
cancel: this.existApp,
autoCancel: true
});
onAccept() {
console.info('onAccept');
}
existApp() {
console.info('Cancel dialog!');
}
build() {
Column() {
Button('Click to open Dialog')
.onClick(() => {
this.dialogController.open()
})
}
}
}
```
![customdialog](figures/customDialog.gif)
# Environment: Device Environment Query
You may want your application to behave differently based on the device environment where the application is running, for example, switching to dark mode or a specific language. In this case, you need Environment for device environment query.
Environment is a singleton object created by the ArkUI framework at application start. It provides a range of application state attributes to AppStorage that describe the device environment in which the application is running. Environment and its attributes are immutable. All property values are of simple type only.
## Application Scenarios
### Accessing Environment Parameters from UI
- Use **Environment.EnvProp** to save the environment variables of the device to AppStorage.
```ts
// Save the language code of the device to AppStorage. The default value is en.
// Whenever its value changes in the device environment, it will update its value in AppStorage.
Environment.EnvProp('languageCode', 'en');
```
- To keep a component variable updated with changes in the device environment, this variable should be decorated with \@StorageProp.
```ts
@StorageProp('languageCode') lang : string = 'en';
```
The chain of updates is as follows: Environment > AppStorage > Component.
> **NOTE**
>
> An \@StorageProp decorated variable can be locally modified, but the change will not be updated to AppStorage. This is because the environment variable parameters are read-only to the application.
```ts
// Save the device language code to AppStorage.
Environment.EnvProp('languageCode', 'en');
let enable = AppStorage.Get('languageCode');
@Entry
@Component
struct Index {
@StorageProp('languageCode') languageCode: string = 'en';
build() {
Row() {
Column() {
// Output the current device language code.
Text(this.languageCode)
}
}
}
}
```
### Using Environment in Application Logic
```ts
// Use Environment.EnvProp to save the device language code to AppStorage.
Environment.EnvProp('languageCode', 'en');
// Obtain the one-way bound languageCode variable from AppStorage.
const lang: SubscribedAbstractProperty<string> = AppStorage.Prop('languageCode');
if (lang.get() === 'en') {
console.info('Hi');
} else {
console.info('Hello!');
}
```
# \@Extend: Extension of Built-in Components
Apart from\@Styles used to extend styles, AkrUI also provides \@Extend, which allows you to add a new attribute feature to a built-in component.
> **NOTE**
>
> Since API version 9, this decorator is supported in ArkTS widgets.
## Rules of Use
### Syntax
```ts
@Extend(UIComponentName) function functionName { ... }
```
### Rules of Use
- Unlike \@Styles, \@Extend can be defined only globally, that is, outside a component declaration.
- Unlike \@Styles, \@Extend can encapsulate private attributes and events of specified components and predefine \@Extend decorated methods of the same component.
```ts
// @Extend(Text) supports the private attribute fontColor of the <Text> component.
@Extend(Text) function fancy () {
.fontColor(Color.Red)
}
// superFancyText can call the predefined fancy method.
@Extend(Text) function superFancyText(size:number) {
.fontSize(size)
.fancy()
}
```
- Unlike \@Styles, \@Extend decorated methods support parameters. You can pass parameters when calling such methods. Regular TypeScript provisions for method parameters apply.
```ts
// xxx.ets
@Extend(Text) function fancy (fontSize: number) {
.fontColor(Color.Red)
.fontSize(fontSize)
}
@Entry
@Component
struct FancyUse {
build() {
Row({ space: 10 }) {
Text('Fancy')
.fancy(16)
Text('Fancy')
.fancy(24)
}
}
}
```
- A function can be passed as a parameter in an \@Extend decorated method to be used as the handler of the event.
```ts
@Extend(Text) function makeMeClick(onClick: () => void) {
.backgroundColor(Color.Blue)
.onClick(onClick)
}
@Entry
@Component
struct FancyUse {
@State label: string = 'Hello World';
onClickHandler() {
this.label = 'Hello ArkUI';
}
build() {
Row({ space: 10 }) {
Text(`${this.label}`)
.makeMeClick(this.onClickHandler.bind(this))
}
}
}
```
- A [state variable](arkts-state-management-overview.md) can be passed as a parameter in an \@Extend decorated method. When the state variable changes, the UI is updated and re-rendered.
```ts
@Extend(Text) function fancy (fontSize: number) {
.fontColor(Color.Red)
.fontSize(fontSize)
}
@Entry
@Component
struct FancyUse {
@State fontSizeValue: number = 20
build() {
Row({ space: 10 }) {
Text('Fancy')
.fancy(this.fontSizeValue)
.onClick(() => {
this.fontSizeValue = 30
})
}
}
}
```
## Application Scenarios
The following example declares three **\<Text>** components. The **fontStyle**, **fontWeight**, and **backgroundColor** styles are set for each **\<Text>** component.
```ts
@Entry
@Component
struct FancyUse {
@State label: string = 'Hello World'
build() {
Row({ space: 10 }) {
Text(`${this.label}`)
.fontStyle(FontStyle.Italic)
.fontWeight(100)
.backgroundColor(Color.Blue)
Text(`${this.label}`)
.fontStyle(FontStyle.Italic)
.fontWeight(200)
.backgroundColor(Color.Pink)
Text(`${this.label}`)
.fontStyle(FontStyle.Italic)
.fontWeight(300)
.backgroundColor(Color.Orange)
}.margin('20%')
}
}
```
\@Extend combines and reuses styles. The following is an example:
```ts
@Extend(Text) function fancyText(weightValue: number, color: Color) {
.fontStyle(FontStyle.Italic)
.fontWeight(weightValue)
.backgroundColor(color)
}
```
With the use of \@Extend, the code readability is enhanced.
```ts
@Entry
@Component
struct FancyUse {
@State label: string = 'Hello World'
build() {
Row({ space: 10 }) {
Text(`${this.label}`)
.fancyText(100, Color.Blue)
Text(`${this.label}`)
.fancyText(200, Color.Pink)
Text(`${this.label}`)
.fancyText(200, Color.Orange)
}.margin('20%')
}
}
```
# Getting Started with ArkTS # Getting Started with ArkTS
As its name implies, ArkTS is a superset of TypeScript. It is the preferred, primary programming language for application development in OpenHarmony.
- ArkTS offers all the features of TS. ArkTS is the preferred main programming language for application development in OpenHarmony. ArkTS is a superset of [TypeScript](https://www.typescriptlang.org/) (TS for short). It contains all TS features and added features. Before getting started with ArkTS, it would be helpful if you have experience with TS development.
- ArkTS extends TS mainly by adding [declarative UI](arkts-basic-ui-description.md) capabilities, which allow you to develop high-performance applications in a more natural and intuitive manner.
The declarative UI capabilities offered by ArkTS include the following: The added features offered by ArkTS include the following:
- [Basic UI description](arkts-basic-ui-description.md): A wide variety of decorators, custom components, and UI description mechanisms work with the built-in components, event methods, and attribute methods in ArkUI, jointly underpinning UI development.
- [State management](arkts-state-mgmt-page-level.md): In the multi-dimensional state management mechanism for ArkUI, UI-related data can be used not only within a component, but also be transferred between different component levels (for example, between parent and child components, between grandparent and grandchild components, or globally) in a device or even across devices. In addition, data transfer can be classified as one-way (read-only) or two-way (mutable). You can use these capabilities at your disposal to implement linkage between data and the UI.
- [Dynamic UI element building](arkts-dynamic-ui-elememt-building.md): In ArkTS, you can dynamically build UI elements, customizing the internal UI structure of components or extending the native components with custom component styles.
- [Rendering control](arkts-rendering-control.md): ArkTS provides conditional rendering and loop rendering. Conditional rendering can render state-specific content based on the application status. Loop rendering iteratively obtains data from the data source and creates the corresponding component during each iteration.
- [Restrictions and extensions](arkts-restrictions-and-extensions.md): ArkTS provides extensions, such as two-way binding. However, it is not without its restrictions.
- ArkTS will continue to evolve to accommodate changing application development and running requirements, and gradually adds more features, such as parallelism and concurrency enhancement, typed system enhancement, and distributed development paradigm. - [Basic syntax](arkts-basic-syntax-overview.md): ArkTS defines declarative UI description, custom components, and dynamic extension of UI elements. All these, together with built-in components, event methods, and attribute methods in ArkUI, jointly underpin UI development.
Below is sample code to illustrate the building blocks of ArkTS. It implements a simple UI with two text segments, one divider, and one button. When the user clicks the button, the text content changes from "Hello World" to "Hello ArkUI". - [State management](arkts-state-management-overview.md): In the multi-dimensional state management mechanism for ArkUI, UI-related data can be used not only within a component, but also be transferred between different component levels (for example, between parent and child components, between grandparent and grandchild components, or globally) in a device or even across devices. In addition, data transfer can be classified as one-way (read-only) or two-way (mutable). You can use these capabilities at your disposal to implement linkage between data and the UI.
![arkts-get-started](figures/arkts-get-started.png) - [Rendering control](arkts-rendering-control-overview.md): ArkTS provides rendering control. Conditional rendering can render state-specific content based on the application status. **ForEach** iteratively obtains data from the data source and creates the corresponding component during each iteration. **LazyForEach** iterates over provided data sources and creates corresponding components during each iteration.
As shown above, ArkTS has the following building blocks:
- Decorators: used to decorate classes, structures, methods, and variables for custom definitions. In the preceding sample code, **@Entry**, **@Component**, and **@State** are decorators. Specifically, **@Component** indicates a custom component, **@Entry** indicates an entry component, and **@State** indicates a state variable in the component, whose change will trigger re-rendering of the UI. ArkTS will continue to evolve to accommodate changing application development and running requirements, and gradually adds more features, such as parallelism and concurrency enhancement, typed system enhancement, and distributed development paradigm.
- Custom components: reusable UI units that can be used in flexible combinations. In the preceding sample code, the structure **Hello** decorated by **@Component** is a custom component.
- UI description: declarative description of the UI structure, such as the code block of the **build()** method.
- Built-in components: basic, container, media, drawing, and canvas components preset in ArkTS. You can directly invoke such components as **\<Column>**, **\<Text>**, **\<Divider>**, and **\<Button>** components in the sample code.
- Attribute methods: methods used to configure component attributes, such as **fontSize()**, **width()**, **height()**, and **color()**. You can configure multiple attributes of a component in method chaining mode.
- Event methods: methods used to add the logic for a component to respond to an event. In the sample code, **onClick()** following **Button** is an event method. You can configure response logic for multiple events in method chaining mode.
# \@Link: Two-Way Synchronization Between Parent and Child Components
An \@Link decorated variable can create two-way synchronization with a variable of its parent component.
> **NOTE**
>
> Since API version 9, this decorator is supported in ArkTS widgets.
## Overview
An \@Link decorated variable in a child component shares the same value with a variable in its parent component.
## Rules of Use
| \@Link Decorator| Description |
| ----------- | ---------------------------------------- |
| Decorator parameters | None. |
| Synchronization type | Two-way:<br>from an \@State, \@StorageLink, or \@Link decorated variable in the parent component to this variable; and the other way around.|
| Allowed variable types | Object, class, string, number, Boolean, enum, and array of these types. For details about the scenarios of nested types, see [Observed Changes](#observed-changes).<br>The type must be specified and must be the same as that of the counterpart variable of the parent component.<br>**any** is not supported. A combination of simple and complex types is not supported. The **undefined** and **null** values are not allowed.<br>**NOTE**<br>The Length, ResourceStr, and ResourceColor types are a combination of simple and complex types and therefore not supported.|
| Initial value for the decorated variable | Forbidden. |
## Variable Transfer/Access Rules
| Transfer/Access | Description |
| ---------- | ---------------------------------------- |
| Initialization and update from the parent component| Mandatory. A two-way synchronization relationship can be established with the @State, @StorageLink, or \@Link decorated variable in the parent component. An \@Link decorated variable can be initialized from an \@State, \@Link, \@Prop, \@Provide, \@Consume, \@ObjectLink, \@StorageLink, \@StorageProp, \@LocalStorageLink, or \@LocalStorageProp decorated variable in the parent component.<br>Since API version 9, the syntax is **Comp({&nbsp;aLink:&nbsp;this.aState&nbsp;})** for initializing an \@Link decorated variable in the child component from an @State decorated variable in its parent component. The **Comp({aLink:&nbsp;$aState})** syntax is also supported|
| Subnode initialization | Supported; can be used to initialize a regular variable or \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.|
| Access | Private, accessible only within the component. |
**Figure 1** Initialization rule
![en-us_image_0000001502092556](figures/en-us_image_0000001502092556.png)
## Observed Changes and Behavior
### Observed Changes
- When the decorated variable is of the Boolean, string, or number type, its value change can be observed. For details, see [Example for @Link with Simple and Class Types](#example-for-link-with-simple-and-class-types).
- When the decorated variable is of the class or Object type, its value change and value changes of all its attributes, that is, the attributes that **Object.keys(observedObject)** returns. For details, see [Example for @Link with Simple and Class Types](#example-for-link-with-simple-and-class-types).
- When the decorated variable is of the array type, the addition, deletion, and updates of array items can be observed. For details, see [Array Type \@Link](#array-type-link).
### Framework Behavior
An \@Link decorated variable shares the lifecycle of its owning component.
To understand the value initialization and update mechanism of the \@Link decorated variable, it is necessary to consider the parent component and the initial render and update process of the child component that owns the \@Link decorated variable (in this example, the \@State decorated variable in the parent component is used).
1. Initial render: The execution of the parent component's **build()** function creates a new instance of the child component. The initialization process is as follows:
1. An \@State decorated variable of the parent component must be specified to initialize the child component's \@Link decorated variable. The child component's \@Link decorated variable value and its source variable are kept in sync (two-way data synchronization).
2. The \@State state variable wrapper class of the parent component is passed to the child component through the build function. After obtaining the \@State state variable of the parent component, the \@Link wrapper class of the child component registers the **this** pointer to the current \@Link wrapper class with the \@State variable of the parent component.
2. Update of the \@Link source: When the state variable in the parent component is updated, the \@Link decorated variable in the related child component is updated. Processing steps:
1. As indicated in the initial rendering step, the child component's \@Link wrapper class registers the current **this** pointer with the parent component. When the \@State decorated variable in the parent component is changed, all system components (**elementid**) and state variables (such as the \@Link wrapper class) that depend on the parent component are traversed and updated.
2. After the \@Link wrapper class is updated, all system components (**elementId**) that depend on the \@Link decorated variable in the child component are notified of the update. In this way, the parent component has the state data of the child components synchronized.
3. Update of \@Link: After the \@Link decorated variable in the child component is updated, the following steps are performed (the \@State decorated variable in the parent component is used):
1. After the \@Link decorated variable is updated, the **set** method of the \@State wrapper class in the parent component is called to synchronize the updated value back to the parent component.
2. The \@Link in the child component and \@State in the parent component traverse the dependent system components and update the corresponding UI. In this way, the \@Link decorated variable in the child component is synchronized back to the \@State decorated variable in the parent component.
## Application Scenarios
### Example for @Link with Simple and Class Types
The following example is for \@Link of both the simple type and class type. After **Parent View: Set yellowButton** and **Parent View: Set GreenButton** in the parent component **ShufflingContainer** are clicked, the change in the parent component is synchronized to the child components. The change of the \@Link decorated variable in the child components **GreenButton** and **YellowButton** is also synchronized to the parent component.
```ts
class GreenButtonState {
width: number = 0;
constructor(width: number) {
this.width = width;
}
}
@Component
struct GreenButton {
@Link greenButtonState: GreenButtonState;
build() {
Button('Green Button')
.width(this.greenButtonState.width)
.height(150.0)
.backgroundColor('#00ff00')
.onClick(() => {
if (this.greenButtonState.width < 700) {
// Update the attribute of the class. The change can be observed and synchronized back to the parent component.
this.greenButtonState.width += 125;
} else {
// Update the class. The change can be observed and synchronized back to the parent component.
this.greenButtonState = new GreenButtonState(100);
}
})
}
}
@Component
struct YellowButton {
@Link yellowButtonState: number;
build() {
Button('Yellow Button')
.width(this.yellowButtonState)
.height(150.0)
.backgroundColor('#ffff00')
.onClick(() => {
// The change of the decorated variable of a simple type in the child component can be synchronized back to the parent component.
this.yellowButtonState += 50.0;
})
}
}
@Entry
@Component
struct ShufflingContainer {
@State greenButtonState: GreenButtonState = new GreenButtonState(300);
@State yellowButtonProp: number = 100;
build() {
Column() {
// Simple type @Link in the child component synchronized from @State in the parent component.
Button('Parent View: Set yellowButton')
.onClick(() => {
this.yellowButtonProp = (this.yellowButtonProp < 700) ? this.yellowButtonProp + 100 : 100;
})
// Class type @Link in the child component synchronized from @State in the parent component.
Button('Parent View: Set GreenButton')
.onClick(() => {
this.greenButtonState.width = (this.greenButtonState.width < 700) ? this.greenButtonState.width + 100 : 100;
})
// Initialize the class type @Link.
GreenButton({ greenButtonState: $greenButtonState })
// Initialize the simple type @Link.
YellowButton({ yellowButtonState: $yellowButtonProp })
}
}
}
```
### Array Type \@Link
```ts
@Component
struct Child {
@Link items: number[];
build() {
Column() {
Button(`Button1: push`).onClick(() => {
this.items.push(this.items.length + 1);
})
Button(`Button2: replace whole item`).onClick(() => {
this.items = [100, 200, 300];
})
}
}
}
@Entry
@Component
struct Parent {
@State arr: number[] = [1, 2, 3];
build() {
Column() {
Child({ items: $arr })
ForEach(this.arr,
item => {
Text(`${item}`)
},
item => item.toString()
)
}
}
}
```
As described above, the ArkUI framework can observe the addition, deletion, and replacement of array items. It should be noted that, in the preceding example, the type of the \@Link and \@State decorated variables is the same: number[]. It is not allowed to define the \@Link decorated variable in the child component as type number (**\@Link item: number**), and create child components for each array item in the \@State decorated array in the parent component. [\@Prop](arkts-prop.md) or \@Observed should be used depending on application semantics.
# LocalStorage: UI State Storage
LocalStorage provides storage for the page-level UI state. The parameters of the LocalStorage type accepted through the \@Entry decorator share the same LocalStorage instance on the page. LocalStorage also allows for state sharing between pages within a UIAbility.
This topic describes only the LocalStorage application scenarios and related decorators: \@LocalStorageProp and \@LocalStorageLink.
> **NOTE**
>
> This module is supported since API version 9.
## Overview
LocalStorage is an in-memory "database" that ArkTS provides for storing state variables that are required to build pages of the application UI.
- An application can create multiple LocalStorage instances. These instances can be shared on a page or, by using the **GetShared** API from the UIAbility, across pages in a UIAbility.
- The root node of a component tree, that is, the \@Component decorated by \@Entry, can be assigned to a LocalStorage instance. All child instances of this custom component automatically gain access to the same LocalStorage instance.
- An \@Component decorated component has access to at most one LocalStorage instance and to [AppStorage](arkts-appstorage.md). A component not decorated with \@Entry cannot be assigned a LocalStorage instance. It can only accept a LocalStorage instance passed from its parent component through \@Entry. A LocalStorage instance can be assigned to multiple components in the component tree.
- All attributes in LocalStorage are mutable.
The application determines the lifecycle of a LocalStorage object. The JS Engine will garbage collect a LocalStorage object when the application releases the last reference to it, which includes deleting the last custom component.
LocalStorage provides two decorators based on the synchronization type of the component decorated with \@Component:
- [@LocalStorageProp](#localstorageprop): \@LocalStorageProp creates a one-way data synchronization from the named attribute in LocalStorage to the \@LocalStorageProp decorated variable.
- [@LocalStorageLink](#localstoragelink): \@LocalStorageLink creates a two-way data synchronization with the named attribute in the \@Component's LocalStorage.
## Restrictions
Once created, the type of a named attribute cannot be changed. Subsequent calls to **Set** must set a value of same type.
## \@LocalStorageProp
As mentioned above, if you want to establish a binding between LocalStorage and a custom component, you need to use the \@LocalStorageProp and \@LocalStorageLink decorators. Use \@LocalStorageProp(key) or \@LocalStorageLink(key) to decorate variables in the component. **key** identifies the attribute in LocalStorage.
When a custom component is initialized, the \@LocalStorageProp(key)/\@LocalStorageLink(key) decorated variable is initialized with the value of the attribute with the given key in LocalStorage. Local initialization is mandatory. If an attribute with the given key is missing from LocalStorage, it will be added with the stated initializing value. (Whether the attribute with the given key exists in LocalStorage depends on the application logic.)
> **NOTE**
>
> Since API version 9, this decorator is supported in ArkTS widgets.
By decorating a variable with \@LocalStorageProp(key), a one-way data synchronization is established with the attribute with the given key in LocalStorage. A local change can be made, but it will not besynchronized to LocalStorage. An update to the attribute with the given key in LocalStorage will overwrite local changes.
### Rules of Use
| \@LocalStorageProp Decorator| Description |
| ----------------------- | ---------------------------------------- |
| Decorator parameters | **key**: constant string, mandatory (note, the string is quoted) |
| Allowed variable types | Object, class, string, number, Boolean, enum, and array of these types. For details about the scenarios of nested objects, see [Observed Changes and Behavior](#observed-changes-and-behavior).<br>The type must be specified and must be the same as the corresponding attribute in LocalStorage. **any** is not supported. The **undefined** and **null** values are not allowed.|
| Synchronization type | One-way: from the attribute in LocalStorage to the component variable. The component variable can be changed locally, but an update from LocalStorage will overwrite local changes.|
| Initial value for the decorated variable | Mandatory. It is used as the default value for initialization if the attribute does not exist in LocalStorage.|
### Variable Transfer/Access Rules
| Transfer/Access | Description |
| ---------- | ---------------------------------------- |
| Initialization and update from the parent component| Forbidden.|
| Subnode initialization | Supported; can be used to initialize an \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.|
| Access | None. |
**Figure 1** \@LocalStorageProp initialization rule
![en-us_image_0000001501936014](figures/en-us_image_0000001501936014.png)
### Observed Changes and Behavior
**Observed Changes**
- When the decorated variable is of the Boolean, string, or number type, its value change can be observed.
- When the decorated variable is of the class or Object type, its value change and value changes of all its attributes, that is, the attributes that **Object.keys(observedObject)** returns.
- When the decorated variable is of the array type, the addition, deletion, and updates of array items can be observed.
**Framework Behavior**
- When the value change of the \@LocalStorageProp(key) decorated variable is observed, the change is not synchronized to the attribute with the give key value in LocalStorage.
- The value change of the \@LocalStorageProp(key) decorated variable only applies to the private member variables of the current component, but not other variables bound to the key.
- When the data decorated by \@LocalStorageProp(key) is a state variable, the change of the data is not synchronized to LocalStorage, but the owning custom component is re-rendered.
- When the attribute with the given key in LocalStorage is updated, the change is synchronized to all the \@LocalStorageProp(key) decorated data, and the local changes of the data are overwritten.
## \@LocalStorageLink
\@LocalStorageLink is required if you need to synchronize the changes of the state variables in a custom component back to LocalStorage.
\@LocalStorageLink(key) creates a two-way data synchronization with the attribute with the given key in LocalStorage.
1. If a local change occurs, it is synchronized to LocalStorage.
2. Changes in LocalStorage are synchronized to all attributes with the given key, including one-way bound variables (\@LocalStorageProp decorated variables and one-way bound variables created through \@Prop) and two-way bound variables (\@LocalStorageLink decorated variables and two-way bound variables created through \@Link).
### Rules of Use
| \@LocalStorageLink Decorator| Description |
| ----------------------- | ---------------------------------------- |
| Decorator parameters | **key**: constant string, mandatory (note, the string is quoted) |
| Allowed variable types | Object, class, string, number, Boolean, enum, and array of these types. For details about the scenarios of nested objects, see [Observed Changes and Behavior](#observed-changes-and-behavior).<br>The type must be specified and must be the same as the corresponding attribute in LocalStorage. **any** is not supported. The **undefined** and **null** values are not allowed.|
| Synchronization type | Two-way: from the attribute in LocalStorage to the custom component variable and back|
| Initial value for the decorated variable | Mandatory. It is used as the default value for initialization if the attribute does not exist in LocalStorage.|
### Variable Transfer/Access Rules
| Transfer/Access | Description |
| ---------- | ---------------------------------------- |
| Initialization and update from the parent component| Forbidden.|
| Subnode initialization | Supported; can be used to initialize a n \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.|
| Access | None. |
**Figure 2** \@LocalStorageLink initialization rule
![en-us_image_0000001552855957](figures/en-us_image_0000001552855957.png)
### Observed Changes and Behavior
**Observed Changes**
- When the decorated variable is of the Boolean, string, or number type, its value change can be observed.
- When the decorated variable is of the class or Object type, its value change and value changes of all its attributes, that is, the attributes that **Object.keys(observedObject)** returns.
- When the decorated variable is of the array type, the addition, deletion, and updates of array items can be observed.
**Framework Behavior**
1. When the value change of the \@LocalStorageLink(key) decorated variable is observed, the change is synchronized to the attribute with the give key value in LocalStorage.
2. Once the attribute with the given key in LocalStorage is updated, all the data (including \@LocalStorageLink and \@LocalStorageProp decorated variables) bound to the attribute key is changed synchronously.
3. When the data decorated by \@LocalStorageProp(key) is a state variable, the change of the data is synchronized to LocalStorage, and the owning custom component is re-rendered.
## Application Scenarios
### Example of Using LocalStorage in Application Logic
```ts
let storage = new LocalStorage({ 'PropA': 47 }); // Create a new instance and initialize it with the given object.
let propA = storage.get('PropA') // propA == 47
let link1 = storage.link('PropA'); // link1.get() == 47
let link2 = storage.link('PropA'); // link2.get() == 47
let prop = storage.prop('PropA'); // prop.get() = 47
link1.set(48); // two-way sync: link1.get() == link2.get() == prop.get() == 48
prop.set(1); // one-way sync: prop.get()=1; but link1.get() == link2.get() == 48
link1.set(49); // two-way sync: link1.get() == link2.get() == prop.get() == 49
```
### Example for Using LocalStorage from Inside the UI
The two decorators \@LocalStorageProp and \@LocalStorageLink can work together to obtain the state variable stored in a LocalStorage instance in the UI component.
This example uses \@LocalStorage as an example to show how to:
- Use the **build** function to create a LocalStorage instance named **storage**.
- Use the \@Entry decorator to add **storage** to the top-level component **CompA**.
- Use \@LocalStorageLink to create a two-way data synchronization with the given attribute in LocalStorage.
```ts
// Create a new instance and initialize it with the given object.
let storage = new LocalStorage({ 'PropA': 47 });
@Component
struct Child {
// @LocalStorageLink creates a two-way data synchronization with the ProA attribute in LocalStorage.
@LocalStorageLink('PropA') storLink2: number = 1;
build() {
Button(`Child from LocalStorage ${this.storLink2}`)
// The changes will be synchronized to ProA in LocalStorage and with Parent.storLink1.
.onClick(() => this.storLink2 += 1)
}
}
// Make LocalStorage accessible from the @Component decorated component.
@Entry(storage)
@Component
struct CompA {
// @LocalStorageLink creates a two-way data synchronization with the ProA attribute in LocalStorage.
@LocalStorageLink('PropA') storLink1: number = 1;
build() {
Column({ space: 15 }) {
Button(`Parent from LocalStorage ${this.storLink1}`) // initial value from LocalStorage will be 47, because 'PropA' initialized already
.onClick(() => this.storLink1 += 1)
// The @Component decorated child component automatically obtains access to the CompA LocalStorage instance.
Child()
}
}
}
```
### Simple Example of Using \@LocalStorageProp with LocalStorage
In this example, the **CompA** and **Child** components create local data that is one-way synced with the PropA attribute in the LocalStorage instance **storage**.
- The change of **this.storProp1** in **CompA** takes effect only in **CompA** and is not synchronized to **storage**.
- In the **Child** component, the value of **storProp2** bound to **Text** is still 47.
```ts
// Create a new instance and initialize it with the given object.
let storage = new LocalStorage({ 'PropA': 47 });
// Make LocalStorage accessible from the @Component decorated component.
@Entry(storage)
@Component
struct CompA {
// @LocalStorageProp creates a one-way data synchronization with the ProA attribute in LocalStorage.
@LocalStorageProp('PropA') storProp1: number = 1;
build() {
Column({ space: 15 }) {
// The initial value is 47. After the button is clicked, the value is incremented by 1. The change takes effect only in storProp1 in the current component and is not synchronized to LocalStorage.
Button(`Parent from LocalStorage ${this.storProp1}`)
.onClick(() => this.storProp1 += 1)
Child()
}
}
}
@Component
struct Child {
// @LocalStorageProp creates a one-way data synchronization with the ProA attribute in LocalStorage.
@LocalStorageProp('PropA') storProp2: number = 2;
build() {
Column({ space: 15 }) {
// When CompA changes, the current storProp2 does not change, and 47 is displayed.
Text(`Parent from LocalStorage ${this.storProp2}`)
}
}
}
```
### Simple Example of Using \@LocalStorageLink and LocalStorage
This example shows how to create a two-way data synchronization between an \@LocalStorageLink decorated variable and LocalStorage.
```ts
// Create a LocalStorage instance.
let storage = new LocalStorage({ 'PropA': 47 });
// Invoke the link9+ API to create a two-way data synchronization with PropA. linkToPropA is a global variable.
let linkToPropA = storage.link('PropA');
@Entry(storage)
@Component
struct CompA {
// @LocalStorageLink('PropA') creates a two-way synchronization with PropA in the CompA custom component. The initial value is 47, because PropA has been set to 47 during LocalStorage construction.
@LocalStorageLink('PropA') storLink: number = 1;
build() {
Column() {
Text(`incr @LocalStorageLink variable`)
// Clicking incr @LocalStorageLink variable increases the value of this.storLink by 1. The change is synchronized back to the storage. The global variable linkToPropA also changes.
.onClick(() => this.storLink += 1)
// You are not advised to use the global variable linkToPropA.get() in the component because errors may occur due to different life cycles.
Text(`@LocalStorageLink: ${this.storLink} - linkToPropA: ${linkToPropA.get()}`)
}
}
}
```
### State Variable Synchronization Between Sibling Nodes
This example shows how to use \@LocalStorageLink to create a two-way synchronization for the state between sibling nodes.
Check the changes in the **Parent** custom component.
1. Clicking **countStorage ${this.playCount} incr by 1** decreases the value of **this.playCount** by 1. This change is synchronized to LocalStorage and to the components bound to **playCountLink** in the **Child** component.
2. Click **countStorage ${this.playCount} incr by 1** to call the **set** API in LocalStorage to update the attributes corresponding to **countStorage** in LocalStorage. The components bound to** playCountLink** in the **Child** component are updated synchronously.
3. The **playCount in LocalStorage for debug ${storage.get&lt;number&gt;('countStorage')}** **\<Text>** component is not updated synchronously, because **storage.get<number>('countStorage')** returns a regular variable. The update of a regular variable does not cause the **\<Text>** component to be re-rendered.
Changes in the **Child** custom component:
1. The update of **playCountLink** is synchronized to LocalStorage, and the parent and sibling child custom components are re-rendered accordingly.
```ts
let storage = new LocalStorage({ countStorage: 1 });
@Component
struct Child {
// Name the child component instance.
label: string = 'no name';
// Two-way synchronization with countStorage in LocalStorage.
@LocalStorageLink('countStorage') playCountLink: number = 0;
build() {
Row() {
Text(this.label)
.width(50).height(60).fontSize(12)
Text(`playCountLink ${this.playCountLink}: inc by 1`)
.onClick(() => {
this.playCountLink += 1;
})
.width(200).height(60).fontSize(12)
}.width(300).height(60)
}
}
@Entry(storage)
@Component
struct Parent {
@LocalStorageLink('countStorage') playCount: number = 0;
build() {
Column() {
Row() {
Text('Parent')
.width(50).height(60).fontSize(12)
Text(`playCount ${this.playCount} dec by 1`)
.onClick(() => {
this.playCount -= 1;
})
.width(250).height(60).fontSize(12)
}.width(300).height(60)
Row() {
Text('LocalStorage')
.width(50).height(60).fontSize(12)
Text(`countStorage ${this.playCount} incr by 1`)
.onClick(() => {
storage.set<number>('countStorage', 1 + storage.get<number>('countStorage'));
})
.width(250).height(60).fontSize(12)
}.width(300).height(60)
Child({ label: 'ChildA' })
Child({ label: 'ChildB' })
Text(`playCount in LocalStorage for debug ${storage.get<number>('countStorage')}`)
.width(300).height(60).fontSize(12)
}
}
}
```
### Sharing a LocalStorage Instance from UIAbility to One or More Pages
In the preceding examples, the LocalStorage instance is shared only in an \@Entry decorated component and its owning child component (a page). To enable a LocalStorage instance to be shared across pages, you can create a LocalStorage instance in the owning UIAbility and call windowStage.[loadContent](https://gitee.com/openharmony/docs/blob/master/en/application-dev/reference/apis/js-apis-window.md#loadcontent9).
```ts
// EntryAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
export default class EntryAbility extends UIAbility {
storage: LocalStorage = new LocalStorage({
'PropA': 47
});
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', this.storage);
}
}
```
On the page, call the **GetShared** API to obtain the LocalStorage instance shared through **loadContent**.
```ts
// Use the GetShared API to obtain the Storage instance shared by stage.
let storage = LocalStorage.GetShared()
@Entry(storage)
@Component
struct CompA {
// can access LocalStorage instance using
// @LocalStorageLink/Prop decorated variables
@LocalStorageLink('PropA') varA: number = 1;
build() {
Column() {
Text(`${this.varA}`).fontSize(50)
}
}
}
```
> **NOTE**
>
> It is good practice to always create a LocalStorage instance with meaningful default values, which serve as a backup when execution exceptions occur and are also useful for unit testing of pages.
# \@Observed and \@ObjectLink: Observing Attribute Changes in Nested Class Objects
The decorators described above can observe only the changes of the first layer. However, in real-world application development, the application may encapsulate its own data model based on development requirements. In the case of multi-layer nesting, for example, a two-dimensional array, an array item class, or a class insider another class as an attribute, the attribute changes at the second layer cannot be observed. This is where the \@Observed and \@ObjectLink decorators come in handy.
> **NOTE**
>
> Since API version 9, these two decorators are supported in ArkTS widgets.
## Overview
\@ObjectLink and \@Observed class decorators are used for two-way data synchronization in scenarios involving nested objects or arrays:
- Regarding classes decorated by \@Observed, the attribute changes can be observed.
- The \@ObjectLink decorated state variable in the child component is used to accept the instance of the \@Observed decorated class and establish two-way data binding with the corresponding state variable in the parent component. The instance can be an \@Observed decorated item in the array or an \@Observeddecorated attribute in the class object.
- Using \@Observed alone has no effect. Combined use with \@ObjectLink for two-way synchronization or with [\@Prop](arkts-prop.md) for one-way synchronization is required.
## Decorator Description
| \@Observed Decorator| Description |
| -------------- | --------------------------------- |
| Decorator parameters | None. |
| Class decorator | Decorates a class. You must use **new** to create a class object before defining the class.|
| \@ObjectLink Decorator| Description |
| ----------------- | ---------------------------------------- |
| Decorator parameters | None. |
| Synchronization type | No synchronization with the parent component. |
| Allowed variable types | Objects of \@Observed decorated classes. The type must be specified.<br>Simple type variables are not supported. Use [\@Prop](arkts-prop.md) instead.<br>An \@ObjectLink decorated variable accepts changes to its attributes, but assignment is not allowed. In other words, an \@ObjectLink decorated variable is read-only and cannot be changed.|
| Initial value for the decorated variable | Not allowed. |
Example of a read-only \@ObjectLink decorated variable:
```ts
// The \@ObjectLink decorated variable accepts changes to its attribute.
this.objLink.a= ...
// Value assignment is not allowed for the \@ObjectLink decorated variable.
this.objLink= ...
```
> **NOTE**
>
> Value assignment is not allowed for the \@ObjectLink decorated variable. To assign a value, use [@Prop](arkts-prop.md) instead.
>
> - \@Prop creates a one-way synchronization from the data source to the decorated variable. It takes a copy of its source tp enable changes to remain local. When \@Prop observes a change to its source, the local value of the \@Prop decorated variable is overwritten.
>
> - \@ObjectLink creates a two-way synchronization between the data source and the decorated variable. An \@ObjectLink decorated variable can be considered as a pointer to the source object inside the parent component. If value assignment of an \@ObjectLink decorated variable occurs, the synchronization chain is interrupted.
## Variable Transfer/Access Rules
| \@ObjectLink Transfer/Access| Description |
| ----------------- | ---------------------------------------- |
| Initialization from the parent component | Mandatory.<br>To initialize an \@ObjectLink decorated variable, a variable in the parent component must meet all the following conditions:<br>- The variable type is an \@Observed decorated class.<br>- The initialized value must be an array item or a class attribute.<br>- The class or array of the synchronization source must be decorated by \@State, \@Link, \@Provide, \@Consume, or \@ObjectLink.<br>For an example where the synchronization source is an array item, see [Object Array](#object-array). For an example of the initialized class, see [Nested Object](#nested-object).|
| Synchronize with the source | Two-way. |
| Subnode initialization | Supported; can be used to initialize a regular variable or \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.|
**Figure 1** Initialization rule
![en-us_image_0000001502255262](figures/en-us_image_0000001502255262.png)
## Observed Changes and Behavior
### Observed Changes
If the attribute of an \@Observed decorated class is not of the simple type, such as class, object, or array, it must be decorated by \@Observed. Otherwise, the attribute changes cannot be observed.
```ts
class ClassA {
public c: number;
constructor(c: number) {
this.c = c;
}
}
@Observed
class ClassB {
public a: ClassA;
public b: number;
constructor(a: ClassA, b: number) {
this.a = a;
this.b = b;
}
}
```
In the preceding example, **ClassB** is decorated by \@Observed, and the value changes of its member variables can be observed. In contrast, **ClassA** is not decorated by \@Observed, and therefore its attribute changes cannot be observed.
```ts
@ObjectLink b: ClassB
// The value assignment can be observed.
this.b.a = new ClassA(5)
this.b.b = 5
// ClassA is not decorated by @Observed, and its attribute changes cannot be observed.
this.b.a.c = 5
```
\@ObjectLink: \@ObjectLink can only accept instances of classes decorated by \@Observed. The following can be observed:
- Value changes of the attributes that **Object.keys(observedObject)** returns. For details, see [Nested Object](#nested-object).
- Replacement of array items for the data source of an array and changes of class attributes for the data source of a class. For details, see [Object Array](#object-array).
### Framework Behavior
1. Initial render:
1. \@Observed causes all instances of the decorated class to be wrapped with an opaque proxy object, which takes over the setter and getter methods of the attributes on the class.
2. The \@ObjectLink decorated variable in the child component is initialized from the parent component and accepts the instance of the \@Observed decorated class. The \@ObjectLink decorated wrapped object registers itself with the \@Observed decorated class.
2. Attribute update: When the attribute of the \@Observed decorated class is updated, the system uses the setter and getter of the proxy, traverses the \@ObjectLink decorated wrapped objects that depend on it, and notifies the data update.
## Application Scenarios
### Nested Object
The following is the data structure of a nested class object.
```ts
// objectLinkNestedObjects.ets
let NextID: number = 1;
@Observed
class ClassA {
public id: number;
public c: number;
constructor(c: number) {
this.id = NextID++;
this.c = c;
}
}
@Observed
class ClassB {
public a: ClassA;
constructor(a: ClassA) {
this.a = a;
}
}
```
The following component hierarchy presents this data structure.
```ts
@Component
struct ViewA {
label: string = 'ViewA1';
@ObjectLink a: ClassA;
build() {
Row() {
Button(`ViewA [${this.label}] this.a.c=${this.a.c} +1`)
.onClick(() => {
this.a.c += 1;
})
}
}
}
@Entry
@Component
struct ViewB {
@State b: ClassB = new ClassB(new ClassA(0));
build() {
Column() {
ViewA({ label: 'ViewA #1', a: this.b.a })
ViewA({ label: 'ViewA #2', a: this.b.a })
Button(`ViewB: this.b.a.c+= 1`)
.onClick(() => {
this.b.a.c += 1;
})
Button(`ViewB: this.b.a = new ClassA(0)`)
.onClick(() => {
this.b.a = new ClassA(0);
})
Button(`ViewB: this.b = new ClassB(ClassA(0))`)
.onClick(() => {
this.b = new ClassB(new ClassA(0));
})
}
}
}
```
Event handlers in **ViewB**:
- this.b.a = new ClassA(0) and this.b = new ClassB(new ClassA(0)): Change to the \@State decorated variable **b** and its attributes.
- this.b.a.c = ... : Second change. [@State](arkts-state.md# observe the change) cannot observe the change of the second layer, but ClassA is decorated by \@Observed, and therefore the change of its attribute c can be observed by \@ObjectLink.
Event handlers in **ViewA**:
- this.a.c += 1: Changes to the \@ObjectLink decorated variable which cause the button label to be updated. Unlike \@Prop, \@ObjectLink does not have a copy of its source. Instead, \@ObjectLink creates a reference to its source.
- The \@ObjectLink decorated variable is read-only. Assigning **this.a = new ClassA(...)** is not allowed. Once value assignment occurs, the reference to the data source is reset and the synchronization is interrupted.
### Object Array
An object array is a frequently used data structure. The following example shows the usage of array objects.
```ts
@Component
struct ViewA {
// The type of @ObjectLink of the child component ViewA is ClassA.
@ObjectLink a: ClassA;
label: string = 'ViewA1';
build() {
Row() {
Button(`ViewA [${this.label}] this.a.c = ${this.a.c} +1`)
.onClick(() => {
this.a.c += 1;
})
}
}
}
@Entry
@Component
struct ViewB {
// ViewB has the @State decorated ClassA[].
@State arrA: ClassA[] = [new ClassA(0), new ClassA(0)];
build() {
Column() {
ForEach(this.arrA,
(item) => {
ViewA({ label: `#${item.id}`, a: item })
},
(item) => item.id.toString()
)
// Initialize the @ObjectLink decorated variable using the array item in the @State decorated array, which is an instance of ClassA decorated by @Observed.
ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] })
ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] })
Button(`ViewB: reset array`)
.onClick(() => {
this.arrA = [new ClassA(0), new ClassA(0)];
})
Button(`ViewB: push`)
.onClick(() => {
this.arrA.push(new ClassA(0))
})
Button(`ViewB: shift`)
.onClick(() => {
this.arrA.shift()
})
Button(`ViewB: chg item property in middle`)
.onClick(() => {
this.arrA[Math.floor(this.arrA.length / 2)].c = 10;
})
Button(`ViewB: chg item property in middle`)
.onClick(() => {
this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11);
})
}
}
}
```
- this.arrA[Math.floor(this.arrA.length/2)] = new ClassA(..): The change of this state variable triggers two updates.
1. ForEach: The value assignment of the array item causes the change of [itemGenerator](arkts-rendering-control-foreach.md#api-description) of **ForEach**. Therefore, the array item is identified as changed, and the item builder of ForEach is executed to create a **ViewA** component instance.
2. ViewA({ label: ViewA this.arrA[first], a: this.arrA[0] }): The preceding update changes the first element in the array. Therefore, the **ViewA** component instance bound to **this.arrA[0]** is updated.
- this.arrA.push(new ClassA(0)): The change of this state variable triggers two updates with different effects.
1. ForEach: The newly added Class A object is unknown to the **ForEach** [itemGenerator](arkts-rendering-control-foreach.md#api-description). The item builder of **ForEach** will be executed to create a **View A** component instance.
2. ViewA({ label: ViewA this.arrA[last], a: this.arrA[this.arrA.length-1] }): The last item of the array is changed. As a result, the second **View A** component instance is changed. For **ViewA({ label: ViewA this.arrA[first], a: this.arrA[0] })**, a change to the array does not trigger a change to the array item, so the first **View A** component instance is not refreshed.
- this.arrA[Math.floor (this.arrA.length/2)].c: [@State] (arkts-state.md#observe-changes) cannot observe changes in the second layer. However, as **ClassA** is decorated by \@Observed, the change of its attributes will be observed by \@ObjectLink.
### Two-Dimensional Array
@Observed class decoration is required for a two-dimensional array. You can declare an \@Observed decorated class that extends from **Array**.
```ts
@Observed
class StringArray extends Array<String> {
}
```
Declare a class that extends from** Array**: **class StringArray extends Array\<String> {}** and create an instance of **StringArray**. The use of the **new** operator is required for the \@Observed class decorator to work properly.
```ts
@Observed
class StringArray extends Array<String> {
}
@Component
struct ItemPage {
@ObjectLink itemArr: StringArray;
build() {
Row() {
Text('ItemPage')
.width(100).height(100)
ForEach(this.itemArr,
item => {
Text(item)
.width(100).height(100)
},
item => item
)
}
}
}
@Entry
@Component
struct IndexPage {
@State arr: Array<StringArray> = [new StringArray(), new StringArray(), new StringArray()];
build() {
Column() {
ItemPage({ itemArr: this.arr[0] })
ItemPage({ itemArr: this.arr[1] })
ItemPage({ itemArr: this.arr[2] })
Divider()
ForEach(this.arr,
itemArr => {
ItemPage({ itemArr: itemArr })
},
itemArr => itemArr[0]
)
Divider()
Button('update')
.onClick(() => {
console.error('Update all items in arr');
if (this.arr[0][0] !== undefined) {
// We should have a real ID to use with ForEach, but we do no.
// Therefore, we need to make sure the pushed strings are unique.
this.arr[0].push(`${this.arr[0].slice(-1).pop()}${this.arr[0].slice(-1).pop()}`);
this.arr[1].push(`${this.arr[1].slice(-1).pop()}${this.arr[1].slice(-1).pop()}`);
this.arr[2].push(`${this.arr[2].slice(-1).pop()}${this.arr[2].slice(-1).pop()}`);
} else {
this.arr[0].push('Hello');
this.arr[1].push('World');
this.arr[2].push('!');
}
})
}
}
}
```
# Overview of Other State Management Features
In addition to the component state and application state management mentioned in previous topics, ArkTS also provides \@Watch and $$ for state management.
- [\@Watch](arkts-watch.md) is used to listen for the changes of state variables.
- [$$operator](arkts-two-way-sync.md) provides a TS variable by-reference to a built-in component so that the variable value and the internal state of that component are kept in sync.
# Page and Custom Component Lifecycle
Before we dive into the page and custom component lifecycle, it would be helpful to learn the relationship between custom components and pages.
- Custom component: \@Component decorated UI unit, which can combine multiple built-in components for component reusability.
- Page: UI page of an application. A page can consist of one or more custom components. A custom component decorated with \@Entry is used as the default entry component of the page. Exactly one component can be decorated with \@Entry in a single source file. Only components decorated by \@Entry can call the lifecycle callbacks of a page.
The following lifecycle callbacks are provided for the lifecycle of a page, that is, the lifecycle of a custom component decorated with \@Entry:
- [onPageShow](../reference/arkui-ts/ts-custom-component-lifecycle.md#onpageshow): Invoked when the page is displayed.
- [onPageHide](../reference/arkui-ts/ts-custom-component-lifecycle.md#onpagehide): Invoked when the page is hidden.
- [onBackPress](../reference/arkui-ts/ts-custom-component-lifecycle.md#onbackpress): Invoked when the user clicks the Back button.
The following lifecycle callbacks are provided for the lifecycle of a custom component, which is one decorated with \@Component:
- [aboutToAppear](../reference/arkui-ts/ts-custom-component-lifecycle.md#abouttoappear): Invoked when the custom component is about to appear. Specifically, it is invoked after a new instance of the custom component is created and before its **build** function is executed.
- [aboutToDisappear](../reference/arkui-ts/ts-custom-component-lifecycle.md#abouttodisappear): Invoked before the destructor of the custom component is consumed.
The following figure shows the lifecycle of a component (home page) decorated with \@Entry.
![en-us_image_0000001502372786](figures/en-us_image_0000001502372786.png)
Based on the preceding figure, let's look into the initial creation, re-rendering, and deletion of a custom component.
## Custom Component Creation and Rendering
1. Custom component creation: An instance of a custom component is created by the ArkUI framework.
2. Initialization of custom component member variables: The member variables are initialized with locally defined default values or component constructor parameters. The initialization happens in the document order, which is the order in which the member variables are defined.
3. If defined, the component's **aboutToAppear** callback is invoked.
4. On initial render, the **build** function of a component is executed for rendering. The rendering creates instances of further child components. While executing the **build** function, the framework observes read access on each state variable and then constructs two mapping tables:
1. State variable -> UI component (including **ForEach** and **if**)
2. UI component -> Update function for this component, which is a lambda. As a subset of the **build** function, the lambda creates one UI component and executes its attribute methods.
```ts
build() {
...
this.observeComponentCreation(() => {
Button.create();
})
this.observeComponentCreation(() => {
Text.create();
})
...
}
```
When the application is started in the background, since the application process is not destroyed, only the **onPageShow** callback is invoked.
## Custom Component Re-rendering
When an event handle is triggered (for example, the click event is triggered), the state variable of the component is changed, or the attribute in LocalStorage or AppStorage is changed, which causes the value of the linked state variable to be changed.
1. The framework observes the variable change and marks the component for re-rendering.
2. Using the two mapping tables created in step 4 of the custom component creation and rendering process, the framework knows which UI components are managed by the state variable and the update functions corresponding to these UI components. With this knowledge, the framework executes only the update functions of these UI components.
## Custom Component Deletion
A custom component is deleted when the branch of the **if** statement or the number of arrays in **ForEach** changes.
1. Before the component is deleted, the **aboutToDisappear** callback is invoked to mark the component for deletion. The component deletion mechanism of ArkUI is as follows: (1) The backend component is directly removed from the component tree and destroyed; (2) The reference to the destroyed component is released from the frontend components; (3) The JS Engine garbage collects the destroyed component.
2. The custom component and all its variables are deleted. Any variables linked to this component, such as [@Link](arkts-link.md), [@Prop](arkts-prop.md), or [@StorageLink](arkts-appstorage.md#storagelink) decorated variables, are unregistered from their [synchronization sources](arkts-state-management-overview.md#basic-concepts).
Use of **async await** is not recommended inside the **aboutToDisappear** callback. In case of an asynchronous operation (a promise or a callback) being started from the **aboutToDisappear** callback, the custom component will remain in the Promise closure until the function has been called, which prevents the component from being garbage collected.
The following example when the lifecycle callbacks are invoked:
```ts
// Index.ets
import router from '@ohos.router';
@Entry
@Component
struct MyComponent {
@State showChild: boolean = true;
// Only components decorated by @Entry can call the lifecycle callbacks of a page.
onPageShow() {
console.info('Index onPageShow');
}
// Only components decorated by @Entry can call the lifecycle callbacks of a page.
onPageHide() {
console.info('Index onPageHide');
}
// Only components decorated by @Entry can call the lifecycle callbacks of a page.
onBackPress() {
console.info('Index onBackPress');
}
// Component lifecycle
aboutToAppear() {
console.info('MyComponent aboutToAppear');
}
// Component lifecycle
aboutToDisappear() {
console.info('MyComponent aboutToDisappear');
}
build() {
Column() {
// When this.showChild is true, the Child child component is created, and Child aboutToAppear is invoked.
if (this.showChild) {
Child()
}
// When this.showChild is false, the Child child component is deleted, and Child aboutToDisappear is invoked.
Button('create or delete Child').onClick(() => {
this.showChild = false;
})
// Because of the pushing from the current page to Page2, onPageHide is invoked.
Button('push to next page')
.onClick(() => {
router.pushUrl({ url: 'pages/Page2' });
})
}
}
}
@Component
struct Child {
@State title: string = 'Hello World';
// Component lifecycle
aboutToDisappear() {
console.info('[lifeCycle] Child aboutToDisappear')
}
// Component lifecycle
aboutToAppear() {
console.info('[lifeCycle] Child aboutToAppear')
}
build() {
Text(this.title).fontSize(50).onClick(() => {
this.title = 'Hello ArkUI';
})
}
}
```
In the preceding example, the **Index** page contains two custom components. One is **MyComponent** decorated with \@Entry, which is also the entry component (root node) of the page. The other is **Child**, which is a child component of **MyComponent**. Only components decorated by \@Entry can call the lifecycle callbacks of a page.Therefore, the page lifecycle callbacks of the **Index** page are declared in **MyComponent**. **MyComponent** and its child components also declare the lifecycle callbacks of the respective component.
- The initialization process of application cold start is as follows: MyComponent aboutToAppear -> MyComponent build -> Child aboutToAppear -> Child build -> Child build execution completed -> MyComponent build execution completed -> Index onPageShow.
- When **delete Child** is clicked, the value of **this.showChild** linked to **if** changes to **false**. As a result, the **Child** component is deleted, and the **Child aboutToDisappear** callback is invoked.
- When **push to next page** is clicked, the **router.pushUrl** API is called to jump to the next page. As a result, the **Index** page is hidden, and the **Index onPageHide** callback is invoked. As the called API is **router.pushUrl**, which results in the Index page being hidden, but not destroyed, only the **onPageHide** callback is invoked. After a new page is displayed, the process of initializing the lifecycle of the new page is executed.
- If **router.replaceUrl** is called, the **Index** page is destroyed. In this case, the execution of lifecycle callbacks changes to: Index onPageHide -> MyComponent aboutToDisappear -> Child aboutToDisappear. As aforementioned, a component is destroyed by directly removing it from the component tree. Therefore, **aboutToDisappear** of the parent component is called first, followed by **aboutToDisAppear** of the child component, and then the process of initializing the lifecycle of the new page is executed.
- When the Back button is clicked, the **Index onBackPress** callback is invoked. When the application is minimized or switched to the background, the **Index onPageHide** callback is invoked. The application is not destroyed in these two states. Therefore, the **aboutToDisappear** callback of the component is not executed. When the application returns to the foreground, the **Index onPageShow** callback is invoked.
- When the application exits, the following callbacks are executed in order: Index onPageHide -> MyComponent aboutToDisappear -> Child aboutToDisappear.
# PersistentStorage: Application State Persistence
During application development, you may want selected attributes to persist even when the application is closed. In this case, you'll need PersistentStorage.
PersistentStorage is an optional singleton object within an application. Its purpose is to persist selected AppStorage attributes so that their values are the same upon application re-start as they were when the application was closed.
## Overview
PersistentStorage retains the selected AppStorage attributes on the device disk. The application uses the API to determine which AppStorage attributes should be persisted with PersistentStorage. The UI and business logic do not directly access attributes in PersistentStorage. All attribute access is to AppStorage. Changes in AppStorage are automatically synchronized to PersistentStorage.
PersistentStorage creates a two-way synchronization with attributes in AppStorage. A frequently used API function is to access AppStorage through PersistentStorage. Additional API functions include managing persisted attributes. The business logic always obtains or sets attributes through AppStorage.
## Restrictions
Persistence of data is a relatively slow operation. Applications should avoid the following situations:
- Persistence of large data sets
- Persistence of variables that change frequently
The preceding situations may overload the change process of persisted data. As a result, the PersistentStorage implementation may limit the change frequency of persisted attributes.
## Application Scenarios
### Accessing PersistentStorage Initialized Attribute from AppStorage
1. Initialize the PersistentStorage instance.
```ts
PersistentStorage.PersistProp('aProp', 47);
```
2. Obtain the corresponding attribute from AppStorage.
```ts
AppStorage.Get('aProp'); // returns 47
```
Alternatively, apply local definition within the component:
```ts
@StorageLink('aProp') aProp: number = 48;
```
The complete code is as follows:
```ts
PersistentStorage.PersistProp('aProp', 47);
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
@StorageLink('aProp') aProp: number = 48
build() {
Row() {
Column() {
Text(this.message)
// The current result is saved when the application exits. After the restart, the last saved result is displayed.
Text(`${this.aProp}`)
.onClick(() => {
this.aProp += 1;
})
}
}
}
}
```
- First running after fresh application installation:
1. **PersistProp** is called to initialize PersistentStorage. A search for the **aProp** attribute on the PersistentStorage disk returns no result, because the application has just been installed.
2. A search for the attribute **aProp** in AppStorage still returns no result.
3. Create the **aProp** attribute of the number type in AppStorge and initialize it with the value 47.
4. PersistentStorage writes the **aProp** attribute and its value **47** to the disk. The value of **aProp** in AppStorage and its subsequent changes are persisted.
5. Create the state variable **\@StorageLink('aProp') aProp** in the **\<Index>** component, which creates a two-way synchronization with the **aProp** attribute in AppStorage. During the creation, the search in AppStorage for the **aProp** attribute succeeds, and therefore, the state variable is initialized with the value **47** found in AppStorage.
**Figure 1** PersistProp initialization process
![en-us_image_0000001553348833](figures/en-us_image_0000001553348833.png)
- After a click event is triggered:
1. The state variable **\@StorageLink('aProp') aProp** is updated, triggering the **\<Text>** component to be re-rendered.
2. The two-way synchronization between the \@StorageLink decorated variable and AppStorage results in the change of the **\@StorageLink('aProp') aProp** being synchronized back to AppStorage.
3. The change of the **aProp** attribute in AppStorage triggers any other one-way or two-way bound variables to be updated. (In this example, there are no such other variables.)
4. Because the attribute corresponding to **aProp** has been persisted, the change of the **aProp** attribute in AppStorage triggers PersistentStorage to write the attribute and its changed value to the device disk.
- Subsequent application running:
1. **PersistentStorage.PersistProp('aProp', 47)** is called. A search for the **aProp** attribute on the PersistentStorage disk succeeds.
2. The attribute is added to AppStorage with the value found on the PersistentStorage disk.
3. In the **\<Index>** component, the value of the @StorageLink decorated **aProp** attribute is the value written by PersistentStorage to AppStorage, that is, the value stored when the application was closed last time.
### Accessing Attribute in AppStorage Before PersistentStorage
This example is an incorrect use. It is incorrect to use the API to access the attributes in AppStorage before calling **PersistentStorage.PersistProp** or **PersistProps**, because such a call sequence will result in loss of the attribute values used in the previous application run:
```ts
let aProp = AppStorage.SetOrCreate('aProp', 47);
PersistentStorage.PersistProp('aProp', 48);
```
**AppStorage.SetOrCreate('aProp', 47)**: The **aProp** attribute is created in AppStorage, its type is number, and its value is set to the specified default value **47**. **aProp** is a persisted attribute. Therefore, it is written back to the PersistentStorage disk, and the value stored in the PersistentStorage disk from the previous run is lost.
**PersistentStorage.PersistProp('aProp', 48)**: An attribute with same name **aProp** is available in PersistentStorage.
# \@Prop: One-Way Synchronization from Parent to Child Components
An \@Prop decorated variable can create one-way synchronization with a variable of its parent component. \@Prop decorated variables are mutable, but changes are not synchronized to the parent component.
> **NOTE**
>
> Since API version 9, this decorator is supported in ArkTS widgets.
## Overview
For an \@Prop decorated variable, the value synchronization is uni-directional from the parent component to the owning component.
- An @Prop variable is allowed to be modified locally, but the change does not propagate back to its parent component.
- Whenever that data source changes, the @Prop decorated variable gets updated, and any locally made changes are overwritten.
## Rules of Use
| \@Prop Decorator| Description |
| ----------- | ---------------------------------------- |
| Decorator parameters | None. |
| Synchronization type | One-way: from the data source provided by the parent component to the @Prop decorated variable.|
| Allowed variable types | string, number, boolean, or enum type.<br>**any** is not supported. The **undefined** and **null** values are not allowed.<br>The type must be specified.<br>Negative examples:<br>CompA&nbsp;({&nbsp;aProp:&nbsp;undefined&nbsp;})<br>CompA&nbsp;({&nbsp;aProp:&nbsp;null&nbsp;})<br>The type must be the same as that of the [data source](arkts-state-management-overview.md#basic-concepts). There are three cases (\@State is used as an example of the data source):<br>- The type of the \@Prop decorated variable is the same as that of the state variable of the parent component, that is, \@Prop: S and \@State: S. For an example, see [Simple Type @Prop Synced from @State in Parent Component](#simple-type-prop-synced-from-state-in-parent-component).<br>- When the state variable of the parent component is an array, the type of the \@Prop decorated variable is the same as that of the array item of the state variable of the parent component, that is, \@Prop: S and \@State: Array\<S>. For examples, see [Simple Type @Prop Synched from @State Array Item in Parent Component](#simple-type-prop-synched-from-state-array-item-in-parent-component).<br>- When the state variable of the parent component is Object or class, the type of the \@Prop decorated variableis the same as the attribute type of the state variable of the parent component, that is, \@Prop: S and \@State: { propA: S }. For examples, see [Class Object Type @Prop Synchedd from @State Class Object Property in Parent Component](#class-object-type-prop-synchedd-from-state-class-object-property-in-parent-component).|
| Initial value for the decorated variable | Local initialization is allowed. |
## Variable Transfer/Access Rules
| Transfer/Access | Description |
| --------- | ---------------------------------------- |
| Initialization from the parent component | Optional. Initialization from the parent component or local initialization can be used. An \@Prop decorated variable can be initialized from a regular variable or an \@State, \@Link, \@Prop, \@Provide, \@Consume, \@ObjectLink, \@StorageLink, \@StorageProp, \@LocalStorageLink, or \@LocalStorageProp decorated variable in its parent component.|
| Subnode initialization | Supported; can be used to initialize a regular variable or \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.|
| Access| Private, accessible only within the component. |
**Figure 1** Initialization rule
![en-us_image_0000001552972029](figures/en-us_image_0000001552972029.png)
## Observed Changes and Behavior
### Observed Changes
\@Prop decorated variables can observe the following changes:
- When the decorated variable is of the string, number, Boolean, or enum type, its value change can be observed.
```ts
// Simple type
@Prop count: number;
// The value assignment can be observed.
this.count = 1;
```
For synchronization between \@State and \@Prop decorated variables:
- The value of an \@State decorated variable in the parent component initializes an \@Prop decorated variable in the child component. The \@State decorated variable also updates the @Prop decorated variable whenever the value of the former changes.
- Changes to the @Prop decorated variable do not affect the value of its source @State decorated variable.
- In addition to \@State, the source can also be decorated with \@Link or \@Prop, where the mechanisms for syncing the \@Prop would be the same.
- The type of the source and the @Prop decorated variable must be the same.
### Framework Behavior
To understand the value initialization and update mechanism of the \@Prop decorated variable, it is necessary to consider the parent component and the initial render and update process of the child component that owns the \@Prop decorated variable.
1. Initial render:
1. The execution of the parent component's **build()** function creates a new instance of the child component, and the parent component provides a source for the @Prop decorated variable.
2. The @Prop decorated variable is initialized.
2. Update:
1. When the @Prop decorated variable is modified locally, the change remains local and does not propagate back to its parent component.
2. When the data source of the parent component is updated, the \@Prop decorated variable in the child component is reset, and its local value changes are overwritten.
## Application Scenarios
### Simple Type @Prop Synced from @State in Parent Component
In this example, the \@Prop decorated **count** variable in the **CountDownComponent** child component is initialized from the \@State decorated **countDownStartValue** variable in the **ParentComponent**. When **Try again** is touched, the value of the **count** variable is modified, but the change remains within the **CountDownComponent** and does not affect the **CountDownComponent**.
Updating **countDownStartValue** in the **ParentComponent** will update the value of the @Prop decorated **count**.
```ts
@Component
struct CountDownComponent {
@Prop count: number;
costOfOneAttempt: number = 1;
build() {
Column() {
if (this.count > 0) {
Text(`You have ${this.count} Nuggets left`)
} else {
Text('Game over!')
}
// Changes to the @Prop decorated variables are not synchronized to the parent component.
Button(`Try again`).onClick(() => {
this.count -= this.costOfOneAttempt;
})
}
}
}
@Entry
@Component
struct ParentComponent {
@State countDownStartValue: number = 10;
build() {
Column() {
Text(`Grant ${this.countDownStartValue} nuggets to play.`)
// Changes to the data source provided by the parent component are synchronized to the child component.
Button(`+1 - Nuggets in New Game`).onClick(() => {
this.countDownStartValue += 1;
})
// Updating the parent component will also update the child component.
Button(`-1 - Nuggets in New Game`).onClick(() => {
this.countDownStartValue -= 1;
})
CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 })
}
}
}
```
In the preceding example:
1. On initial render, when the **CountDownComponent** child component is created, its @Prop decorated **count** variable is initialized from the \@State decorated **countDownStartValue** variable in the **ParentComponent**.
2. When the "+1" or "-1" button is touched, the @State decorated **countDownStartValue** of the **ParentComponent** changes. This will cause the **ParentComponent** to re-render. At the minumum, the **CountDownComponent** will be updated because of the changed **count** variable value.
3. Because of the changed **count** variable value, the **CountDownComponent** child component will re-render. At a minimum, the **if** statement's conditions (**this.counter> 0**) is-evaluated and the **\<Text>** child component inside the **if** would be updated.
4. When **Try again** in the **CountDownComponent** child component is touched, the value of the **count** variable is modified, but the change remains within the child component and does not affect the **countDownStartValue** in the parenet component.
5. Updating **countDownStartValue** will overwrite the local value changes of the @Prop decorated **count** in the **CountDownComponent** child component.
### Simple Type @Prop Synched from @State Array Item in Parent Component
The \@State decorated array an array item in the parent component can be used as data source to initialize and update a @Prop decorated variable. In the following example, the \@State decorated array **arr** in the parent component **Index** initializes the \@Prop decorated **value** variable in the child component **Child**.
```ts
@Component
struct Child {
@Prop value: number;
build() {
Text(`${this.value}`)
.fontSize(50)
.onClick(()=>{this.value++})
}
}
@Entry
@Component
struct Index {
@State arr: number[] = [1,2,3];
build() {
Row() {
Column() {
Child({value: this.arr[0]})
Child({value: this.arr[1]})
Child({value: this.arr[2]})
Divider().height(5)
ForEach(this.arr,
item => {
Child({value: item})
},
item => item.toString()
)
Text('replace entire arr')
.fontSize(50)
.onClick(()=>{
// Both arrays contain item "3".
this.arr = this.arr[0] == 1 ? [3,4,5] : [1,2,3];
})
}
}
}
}
```
Initial render creates six instances of the **Child** component. Each \@Prop decorated variable is initialized with a copy of an array item. The **onclick** event handler of the **Child** component changes the local variable value.
Assume that we clicked so many times that all local values be '7'.
```
7
7
7
----
7
7
7
```
After **replace entire arr** is clicked, the following information is displayed:
```
3
4
5
----
7
4
5
```
- Changes made in the **Child** component are not synchronized to the parent component **Index**. Therefore, even if the values of the six intances of the **Child** component are 7, the value of **this.arr** in the **Index** component is still **[1,2,3]**.
- After **replace entire arr** is clicked, if **this.arr[0] == 1** is true, **this.arr** is set to **[3, 4, 5]**.
- Because **this.arr[0]** has been changed, the **Child({value: this.arr[0]})** component synchronizes the update of **this.arr[0]** to the instance's \@Prop decorated variable. The same happens for **Child({value: this.arr[1]})** and **Child({value: this.arr[2]})**.
- The change of **this.arr** causes **ForEach** to update: The array item with the ID **3** is retained in this update, array items with IDs **1** and **2** are deleted, and array items with IDs **4** and **5** are added. The array before and after the update is **[1, 2, 3]** and **[3, 4, 5]**, respectively. This implies that the **Child** instance generated for item **3** will be moved to the first place, but not updated. In this case, the component value corresponding to **3** is **7**, and the final render result of **ForEach** is **7**, **4**, and **5**.
### Class Object Type @Prop Synchedd from @State Class Object Property in Parent Component
In a library with one book and two users, each user can mark the book as read, but this does not affect the other user reader. Technically speaking, local changes to the \@Prop decorated **book** object do not sync back to the @State decorated **book** in the **Library** component.
```ts
class Book {
public title: string;
public pages: number;
public readIt: boolean = false;
constructor(title: string, pages: number) {
this.title = title;
this.pages = pages;
}
}
@Component
struct ReaderComp {
@Prop title: string;
@Prop readIt: boolean;
build() {
Row() {
Text(this.title)
Text(`... ${this.readIt ? 'I have read' : 'I have bot read it'}`)
.onClick(() => this.readIt = true)
}
}
}
@Entry
@Component
struct Library {
@State book: Book = new Book('100 secrets of C++', 765);
build() {
Column() {
ReaderComp({ title: this.book.title, readIt: this.book.readIt })
ReaderComp({ title: this.book.title, readIt: this.book.readIt })
}
}
}
```
### Simple Type @Prop with Local Initialization and No Sync from Parent Parent
To enable an \@Component decorated component to be reusable, \@Prop allows for optional local initialization. This makes the synchronization with a variable in the parent component a choice, rather than mandatory. Providing a data source in the parent component is optional only when local initialization is provided for the \@Prop decorated variable.
The following example includes two @Prop decorated variables in child component.
- The @Prop decorated variable **customCounter** has no local initialization, and therefore it requires a synchronization source in its parent component. When the source value changes, the @Prop decorated variable is udpated.
- The @Prop decorated variable **customCounter2** has local initialization. In this case, specifying a synchronization source in the parent component is allowed but not mandatory.
```ts
@Component
struct MyComponent {
@Prop customCounter: number;
@Prop customCounter2: number = 5;
build() {
Column() {
Row() {
Text(`From Main: ${this.customCounter}`).width(90).height(40).fontColor('#FF0010')
}
Row() {
Button('Click to change locally !').width(480).height(60).margin({ top: 10 })
.onClick(() => {
this.customCounter2++
})
}.height(100).width(480)
Row() {
Text(`Custom Local: ${this.customCounter2}`).width(90).height(40).fontColor('#FF0010')
}
}
}
}
@Entry
@Component
struct MainProgram {
@State mainCounter: number = 10;
build() {
Column() {
Row() {
Column() {
Button('Click to change number').width(480).height(60).margin({ top: 10, bottom: 10 })
.onClick(() => {
this.mainCounter++
})
}
}
Row() {
Column(
// customCounter must be initialized from the parent component due to lack of local initialization. Here, customCounter2 does not need to be initialized.
MyComponent({ customCounter: this.mainCounter })
// customCounter2 of the child component can also be initialized from the parent component. The value from the parent component overwrites the locally assigned value of customCounter2 during initialization.
MyComponent({ customCounter: this.mainCounter, customCounter2: this.mainCounter })
}.width('40%')
}
Row() {
Text('').width(480).height(10)
}
}
}
}
```
# \@Provide and \@Consume: Two-Way Synchronization with Descendant Components
\@Provide and \@Consume are used for two-way data synchronization with descendant components in scenarios where state data needs to be transferred between multiple levels. They do not involve passing a variable from component to component multiple times.
An \@Provide decorated state variable exists in the ancestor component and is said to be "provided" to descendent components. An \@Consume decorated state variable is used in a descendent component. It is linked to ("consumes") the provided state variable in its ancestor component.
> **NOTE**
>
> Since API version 9, these two decorators are supported in ArkTS widgets.
## Overview
\@Provide/\@Consume decorated state variables have the following features:
- An \@Provide decorated state variable becomes available to all descendent components of the providing component automatically. The variable is said to be "provided" to other components. This means that you do not need to pass a variable from component to component multiple times.
- A descendent component gains access to the provided state variable by decorating a variable with \@Consume. This establishes a two-way data synchronization between the provided and the consumed variable. This synchronization works the same as a combination of \@State and \@Link does. The only difference is that the former allows transfer across multiple levels of the UI parent-child hierarchy.
- \@Provide and \@Consume can be bound using the same variable name or variable alias. The variable types must be the same.
```ts
// Binding through the same variable name
@Provide a: number = 0;
@Consume a: number;
// Binding through the same variable alias
@Provide('a') b: number = 0;
@Consume('a') c: number;
```
When \@Provide and \@Consume are bound through the same variable name or variable alias, the variables decorated by \@Provide and \@Consume are in a one-to-many relationship. A custom component, including its child components, cannot contain multiple \@Provide decorated variables under the same name or alias.
## Decorator Description
The rules of \@State also apply to \@Provide. The difference is that \@Provide also functions as a synchronization source for multi-layer descendants.
| \@Provide Decorator| Description |
| -------------- | ---------------------------------------- |
| Decorator parameters | Alias: constant string, optional.<br>If the alias is specified, the variable is provided under the alias name only. If the alias is not specified, the variable is provided under the variable name.|
| Synchronization type | Two-way:<br>from the \@Provide decorated variable to all \@Consume decorated variables; and the other way around. The two-way synchronization behaviour is the same as that of the combination of \@State and \@Link.|
| Allowed variable types | Object, class, string, number, Boolean, enum, and array of these types. For details about the scenarios of nested types, see [Observed Changes](#observed-changes).<br>**any** is not supported. A combination of simple and complex types is not supported. The **undefined** and **null** values are not allowed.<br>The type must be specified. The type of the provided and the consumed variables must be the same.<br>**NOTE**<br>The Length, ResourceStr, and ResourceColor types are a combination of simple and complex types and therefore not supported.|
| Initial value for the decorated variable | Mandatory. |
| \@Consume Decorator| Description |
| -------------- | ---------------------------------------- |
| Decorator parameters | Alias: constant string, optional.<br>If the alias is specified, the alias name is used for matching with the \@Provide decorated variable. Otherwise, the variable name is used.|
| Synchronization type | from the \@Provide decorated variable to all \@Consume decorated variables; and the other way around. The two-way synchronization behaviour is the same as that of the combination of \@State and \@Link.|
| Allowed variable types | Object, class, string, number, Boolean, enum, and array of these types. For details about the scenarios of nested types, see [Observed Changes](#observed-changes).<br>**any** is not supported. The **undefined** and **null** values are not allowed.<br>The type must be specified. The type of the provided and the consumed variables must be the same.<br>**NOTE**<br>An \@Consume decorated variable must have a matching \@Provide decorated variable with the corresponding attribute and alias on its parent or ancestor node.|
| Initial value for the decorated variable | Forbidden. |
## Variable Transfer/Access Rules
| \@Provide Transfer/Access| Description |
| -------------- | ---------------------------------------- |
| Initialization and update from the parent component | Optional. An \@Provide decorated variable can be initialized from a regular variable or an \@State, \@Link, \@Prop, \@Provide, \@Consume, \@ObjectLink, \@StorageLink, \@StorageProp, \@LocalStorageLink, or \@LocalStorageProp decorated variable in its parent component.|
| Subnode initialization | Supported; can be used to initialize an \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.|
| Synchronization with the parent component | None. |
| Synchronization with descendant components | Two-way with @Consume decorated variables in descendant components. |
| Access | Private, accessible only within the component. |
**Figure 1** \@Provide initialization rule
![en-us_image_0000001552614217](figures/en-us_image_0000001552614217.png)
| \@Consume Transfer/Access| Description |
| -------------- | ---------------------------------------- |
| Initialization and update from the parent component | Forbidden. Initialized from the \@Provide decorated variable with the same name or alias. |
| Subnode initialization | Supported; can be used to initialize an \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.|
| Synchronization with the ancestor component | Two-way with the @Provide decorated variable in the ancestor component. |
| Access | Private, accessible only within the component. |
**Figure 2** \@Consume initialization rule
![en-us_image_0000001502094666](figures/en-us_image_0000001502094666.png)
## Observed Changes and Behavior
### Observed Changes
- When the decorated variable is of the Boolean, string, or number type, its value change can be observed.
- When the decorated variable is of the class or Object type, its value change and value changes of all its attributes, that is, the attributes that **Object.keys(observedObject)** returns.
- When the decorated variable is of the array type, the addition, deletion, and updates of array items can be observed.
### Framework Behavior
1. Initial render:
1. The \@Provide decorated variable is passed to all child components of the owning component in map mode.
2. If an \@Consume decorated variable is used in a child component, the system checks the map for a matching \@Provide decorated variable based on the variable name or alias. If no matching variable is found, the framework throws a JS error.
3. The process of initializing the \@Consume decorated variable is similar to that of initializing the \@State/\@Link decorated variable. The \@Consume decorated variable saves the matching \@Provide decorated variable found in the map and registers itself with the \@Provide decorated variable.
2. When the \@Provide decorated variable is updated:
1. The system traverses and updates all system components (**elementid**) and state variable (\@Consume) that depend on the \@Provide decorated variable, with which the \@Consume decorated variable has registered itself on initial render.
2. After the \@Consume decorated variable is updated in all owning child components, all system components (**elementId**) that depend on the \@Consume decorated variable are updated. In this way, changes to the \@Provide decorated variable are synchronized to the \@Consume decorated variable.
3. When the \@Consume decorated variable is updated:
As can be learned from the initial render procedure, the \@Consume decorated variable holds an instance of \@Provide. After the \@Consume decorated variable is updated, the update method of \@Provide is called to synchronize the changes to \@Provide.
## Application Scenarios
The following example shows the two-way synchronization between \@Provide and \@Consume decorated variables. When the buttons in the **CompA** and **CompD** components are clicked, the changes to **reviewVotes** are synchronized to the **CompA** and **CompD** components.
```ts
@Component
struct CompD {
// The @Consume decorated variable is bound to the @Provide decorated variable in its ancestor component CompA under the same attribute name.
@Consume reviewVotes: number;
build() {
Column() {
Text(`reviewVotes(${this.reviewVotes})`)
Button(`reviewVotes(${this.reviewVotes}), give +1`)
.onClick(() => this.reviewVotes += 1)
}
.width('50%')
}
}
@Component
struct CompC {
build() {
Row({ space: 5 }) {
CompD()
CompD()
}
}
}
@Component
struct CompB {
build() {
CompC()
}
}
@Entry
@Component
struct CompA {
// @Provide decorated variable reviewVotes is provided by the entry component CompA.
@Provide reviewVotes: number = 0;
build() {
Column() {
Button(`reviewVotes(${this.reviewVotes}), give +1`)
.onClick(() => this.reviewVotes += 1)
CompB()
}
}
}
```
# ForEach: Rendering of Repeated Content
**ForEach** enables repeated content based on array-type data.
## API Description
```ts
ForEach(
arr: any[],
itemGenerator: (item: any, index?: number) => void,
keyGenerator?: (item: any, index?: number) => string
)
```
| Name | Type | Mandatory | Description |
| ------------- | ---------------------------------------- | ---- | ---------------------------------------- |
| arr | Array | Yes | An array, which can be empty, in which case no child component is created. The functions that return array-type values are also allowed, for example, **arr.slice (1, 3)**. The set functions cannot change any state variables including the array itself, such as **Array.splice**, **Array.sort**, and **Array.reverse**.|
| itemGenerator | (item:&nbsp;any,&nbsp;index?:&nbsp;number)&nbsp;=&gt;&nbsp;void | Yes | A lambda function used to generate one or more child components for each data item in an array. Each component and its child component list must be contained in parentheses.<br>**NOTE**<br>- The type of the child component must be the one allowed inside the parent container component of **ForEach**. For example, a **\<LitemItem>** child component is allowed only when the parent container component of **ForEach** is **\<List>**.<br>- The child build function is allowed to return an **if** or another **ForEach**. **ForEach** can be placed inside **if**.<br>- The optional **index** parameter should only be specified in the function signature if used in its body.|
| keyGenerator | (item:&nbsp;any,&nbsp;index?:&nbsp;number)&nbsp;=&gt;&nbsp;string | No | An anonymous function used to generate a unique and fixed key value for each data item in an array. This key-value generator is optional. However, for performance reasons, it is strongly recommended that the key-value generator be provided, so that the development framework can better identify array changes. For example, if no key-value generator is provided, a reverse of an array will result in rebuilding of all nodes in **ForEach**.<br>**NOTE**<br>- Two items inside the same array must never work out the same ID.<br>- If **index** is not used, an item's ID must not change when the item's position within the array changes. However, if **index** is used, then the ID must change when the item is moved within the array.<br>- When an item is replaced by a new one (with a different value), the ID of the replaced and the ID of the new item must be different.<br>- When **index** is used in the build function, it should also be used in the ID generation function.<br>- The ID generation function is not allowed to mutate any component state.|
## Restrictions
- **ForEach** must be used in container components.
- The type of the child component must be the one allowed inside the parent container component of **ForEach**.
- The **itemGenerator** function can contain an **if/else** statement, and an **if/else** statement can contain **ForEach**.
- The call sequence of **itemGenerator** functions may be different from that of the data items in the array. During the development, do not assume whether or when the **itemGenerator** and **keyGenerator** functions are executed. For example, the following example may not run properly:
```ts
ForEach(anArray.map((item1, index1) => { return { i: index1 + 1, data: item1 }; }),
item => Text(`${item.i}. item.data.label`),
item => item.data.id.toString())
```
## Recommendations
- Make no assumption on the order of item build functions. The execution order may not be the order of items inside the array.
- Make no assumption either when items are built the first time. Currently initial render of **ForEach** builds all array items when the \@Component decorated component is rendered at the first time, but future framework versions might change this behaviour to a more lazy behaviour.
- Using the **index** parameter has severe negative impact on the UI update performance. Avoid using this parameter whenever possible.
- If the **index** parameter is used in the item generator function, it must also be used in the item index function. Otherwise, the framework counts in the index when generating the ID. By default, the index is concatenated to the end of the ID.
## Application Scenarios
### Simple ForEach Example
This example creates three **\<Text>** and **\<Divide>** components based on the **arr** data.
```ts
@Entry
@Component
struct MyComponent {
@State arr: number[] = [10, 20, 30];
build() {
Column({ space: 5 }) {
Button('Reverse Array')
.onClick(() => {
this.arr.reverse();
})
ForEach(this.arr, (item: number) => {
Text(`item value: ${item}`).fontSize(18)
Divider().strokeWidth(2)
}, (item: number) => item.toString())
}
}
}
```
### Complex ForEach Example
```ts
@Component
struct CounterView {
label: string;
@State count: number = 0;
build() {
Button(`${this.label}-${this.count} click +1`)
.width(300).height(40)
.backgroundColor('#a0ffa0')
.onClick(() => {
this.count++;
})
}
}
@Entry
@Component
struct MainView {
@State arr: number[] = Array.from(Array(10).keys()); // [0.,.9]
nextUnused: number = this.arr.length;
build() {
Column() {
Button(`push new item`)
.onClick(() => {
this.arr.push(this.nextUnused++)
})
.width(300).height(40)
Button(`pop last item`)
.onClick(() => {
this.arr.pop()
})
.width(300).height(40)
Button(`prepend new item (unshift)`)
.onClick(() => {
this.arr.unshift(this.nextUnused++)
})
.width(300).height(40)
Button(`remove first item (shift)`)
.onClick(() => {
this.arr.shift()
})
.width(300).height(40)
Button(`insert at pos ${Math.floor(this.arr.length / 2)}`)
.onClick(() => {
this.arr.splice(Math.floor(this.arr.length / 2), 0, this.nextUnused++);
})
.width(300).height(40)
Button(`remove at pos ${Math.floor(this.arr.length / 2)}`)
.onClick(() => {
this.arr.splice(Math.floor(this.arr.length / 2), 1);
})
.width(300).height(40)
Button(`set at pos ${Math.floor(this.arr.length / 2)} to ${this.nextUnused}`)
.onClick(() => {
this.arr[Math.floor(this.arr.length / 2)] = this.nextUnused++;
})
.width(300).height(40)
ForEach(this.arr,
(item) => {
CounterView({ label: item.toString() })
},
(item) => item.toString()
)
}
}
}
```
**MainView** has an \@State decorated array of numbers. Adding, deleting, and replacing array items are observed mutation events. Whenever one of these events occurs, **ForEach** in **MainView** is updated.
The item index function creates a unique and persistent ID for each array item. The ArkUI framework uses this ID to determine whether an item in the array changes. As long as the ID is the same, the item value is assumed to remain unchanged, but its index position may have changed. For this mechanism to work, different array items cannot have the same ID.
Using the item ID obtained through computation, the framework can distinguish newly created, removed, and retained array items.
1. The framework removes UI components for a removed array item.
2. The framework executes the item build function only for newly added array items.
3. The framework does not execute the item build function for retained array items. If the item index within the array has changed, the framework will just move its UI components according to the new order, but will not update that UI components.
The item index function is recommended, but optional. The generated IDs must be unique. This means that the same ID must not be computed for any two items within the array. The ID must be different even if the two array items have the same value.
If the array item value changes, the ID must be changed.
As mentioned earlier, the ID generation function is optional. The following example shows **ForEach** without the item index function:
```ts
ForEach(this.arr,
(item) => {
CounterView({ label: item.toString() })
}
)
```
If no item ID function is provided, the framework attempts to intelligently detect array changes when updating **ForEach**. However, it might remove child components and re-execute the item build function for array items that have been moved (with indexes changed). In the preceding example, this changes the application behavior in regard to the **counter** state of **CounterView**. When a new **CounterView** instance is created, the value of **counter** is initialized with **0**.
### Example of ForEach Using \@ObjectLink
If your application needs to preserve the state of repeated child components, you can use \@ObjectLink to enable the state to be "pushed up the component tree."
```ts
let NextID: number = 0;
@Observed
class MyCounter {
public id: number;
public c: number;
constructor(c: number) {
this.id = NextID++;
this.c = c;
}
}
@Component
struct CounterView {
@ObjectLink counter: MyCounter;
label: string = 'CounterView';
build() {
Button(`CounterView [${this.label}] this.counter.c=${this.counter.c} +1`)
.width(200).height(50)
.onClick(() => {
this.counter.c += 1;
})
}
}
@Entry
@Component
struct MainView {
@State firstIndex: number = 0;
@State counters: Array<MyCounter> = [new MyCounter(0), new MyCounter(0), new MyCounter(0),
new MyCounter(0), new MyCounter(0)];
build() {
Column() {
ForEach(this.counters.slice(this.firstIndex, this.firstIndex + 3),
(item) => {
CounterView({ label: `Counter item #${item.id}`, counter: item })
},
(item) => item.id.toString()
)
Button(`Counters: shift up`)
.width(200).height(50)
.onClick(() => {
this.firstIndex = Math.min(this.firstIndex + 1, this.counters.length - 3);
})
Button(`counters: shift down`)
.width(200).height(50)
.onClick(() => {
this.firstIndex = Math.max(0, this.firstIndex - 1);
})
}
}
}
```
When the value of **firstIndex** is increased, **ForEach** within **Mainview** is updated, and the **CounterView** child component associated with the item ID **firstIndex-1** is removed. For the array item whose ID is **firstindex + 3**, a new** CounterView** child component instance is created. The value of the state variable **counter** of the **CounterView** child component is preserved by the **Mainview** parent component. Therefore, **counter** is not rebuilt when the **CounterView** child component instance is rebuilt.
> **NOTE**
>
> The most common mistake application developers make in connection with **ForEach** is that the ID generation function returns the same value for two array items, especially in the Array\<number> scenario.
### Nested Use of ForEach
While nesting **ForEach** inside another **ForEach** in the same component is allowed, it is not recommended. It is better to split the component into two and have each build function include just one ForEach. The following is a poor example of nested use of **ForEach**.
```ts
class Month {
year: number;
month: number;
days: number[];
constructor(year: number, month: number, days: number[]) {
this.year = year;
this.month = month;
this.days = days;
}
}
@Component
struct CalendarExample {
// Simulate with six months.
@State calendar : Month[] = [
new Month(2020, 1, [...Array(31).keys()]),
new Month(2020, 2, [...Array(28).keys()]),
new Month(2020, 3, [...Array(31).keys()]),
new Month(2020, 4, [...Array(30).keys()]),
new Month(2020, 5, [...Array(31).keys()]),
new Month(2020, 6, [...Array(30).keys()])
]
build() {
Column() {
Button() {
Text('next month')
}.onClick(() => {
this.calendar.shift()
this.calendar.push(new Month(year: 2020, month: 7, days: [...Array(31).keys()]))
})
ForEach(this.calendar,
(item: Month) => {
ForEach(item.days,
(day : number) => {
// Build a date block.
},
(day : number) => day.toString()
)// Inner ForEach
},
(item: Month) => (item.year * 12 + item.month).toString() // This field is used together with the year and month as the unique ID of the month.
)// Outer ForEach
}
}
}
```
The preceding example has two issues:
1. The code readability is poor.
2. For a data structure of months and days of a year, the framework cannot observe the attribute changes to **Month** objects, including any changes to the **days** array. As a result, the inner **ForEach** will not update the date.
The recommended application design is to split **Calendar** into **Year**, **Month**, and **Day** child components. Define a **Day** model class to hold information about a day and decorate the class with \@Observed. The **DayView** component uses an \@ObjectLink decorated variable to link to the data about a day. Perform the same operations on the **MonthView** and **Month** model classes.
### Example of Using the Optional index Parameter in ForEach
You can use the optional **index** parameter in item build and ID generation functions.
```ts
@Entry
@Component
struct ForEachWithIndex {
@State arr: number[] = [4, 3, 1, 5];
build() {
Column() {
ForEach(this.arr,
(it, indx) => {
Text(`Item: ${indx} - ${it}`)
},
(it, indx) => {
return `${indx} - ${it}`
}
)
}
}
}
```
The correct construction of the ID generation function is essential. When **index** is used in the item generation function, it should also be used in the ID generation function to produce unique IDs and an ID for given source array item that changes when its index position within the array changes.
This example also illustrates that the **index** parameter can cause significant performance degradation. If an item is moved in the source array without modification, the dependent UI still requires rebuilding because of the changed index. For example, with the use of index sorting, the array only requires the unmodified child UI node of **ForEach** to be moved to the correct slot, which is a lightweight operation for the framework. When **index** is used, all child UI nodes need to be rebuilt, which is much more heavy weight.
# if/else: Conditional Rendering
ArkTS provides conditional rendering. Use the **if**, **else**, and **else if** statements to enable your application to display different content based on the condition or state.
> **NOTE**
>
> Since API version 9, this API is supported in ArkTS widgets.
## Rules of Use
- The **if**, **else**, and **else if** statements are supported.
- The conditional statements following **if** and **else if** can use state variables.
- Use of the conditional statements within a container component is allowed for building different child components.
- Conditional statements are "transparent" when it comes to the parent-child relationship of components. Rules about permissible child components must be followed when there is one or more **if** statements between the parent and child components.
- The build function inside each branch must follow the special rules for build functions. Each of such build functions must create one or more components. An empty build function that creates no components will result in a syntax error.
- Some container components impose restrictions on the type or number of child components. When conditional statements are used in such components, these restrictions also apply to the components created in conditional statements. For example, when a conditional statement is used in the **\<Grid>** container component, whose child components can only be **\<GridItem>**, only the **\<GridItem>** component can be used in the conditional statement.
## Update Mechanism
A conditional statement updates whenever a state variable used inside the **if** condition or the **else if** condition changes. Specifically:
1. The conditional statement re-evaluates the conditions. If the evaluation of the conditions changes, steps 2 and 3 are performed. Otherwise, no follow-up operation is required.
2. The framework removes all child components that have been built.
3. The framework executes the build function of the branch again to add the generated child component to its parent component. If an applicable **else** branch is missing, no new build function will be executed.
A condition can include Typescript expressions. As for any expression inside build functions, such an expression must not change any application state.
## Application Scenarios
### Using if for Conditional Rendering
```ts
@Entry
@Component
struct ViewA {
@State count: number = 0;
build() {
Column() {
Text(`count=${this.count}`)
if (this.count > 0) {
Text(`count is positive`)
.fontColor(Color.Green)
}
Button('increase count')
.onClick(() => {
this.count++;
})
Button('decrease count')
.onClick(() => {
this.count--;
})
}
}
}
```
Each branch of the **if** statement includes a build function. Each of such build functions must create one or more components. On initial render, **if** will execute a build function and add the generated child component to its parent component.
**if** updates whenever a state variable used inside the **if** condition or the **else if** condition changes and re-evaluates the conditions. If the evaluation of the conditions changes, it means that another branch of **if** needs to be built. In this case, the ArkUI framework will:
1. Remove all previously rendered components (of the earlier branch).
2. Execute the build function of the branch and add the generated child component to its parent component.
In the preceding example, if **count** increases from 0 to 1, then, **if** updates, the condition **count > 0** is re-evaluated, and the evaluation result changes from **false** to **true**. Therefore, the positive branch build function will be executed, which creates a **\<Text>** component and adds it to the **\<Column>** parent component. If **count** changes back to 0 later, then, the **\<Text>** component will be removed from the **\<Column>** component. Since there is no **else** branch, no new build function will be executed.
### if ... else ... and Child Component State
This example involves **if...** **else...** and a child component with an \@State decorated variable.
```ts
@Component
struct CounterView {
@State counter: number = 0;
label: string = 'unknown';
build() {
Row() {
Text(`${this.label}`)
Button(`counter ${this.counter} +1`)
.onClick(() => {
this.counter += 1;
})
}
}
}
@Entry
@Component
struct MainView {
@State toggle: boolean = true;
build() {
Column() {
if (this.toggle) {
CounterView({ label: 'CounterView #positive' })
} else {
CounterView({ label: 'CounterView #negative' })
}
Button(`toggle ${this.toggle}`)
.onClick(() => {
this.toggle = !this.toggle;
})
}
}
}
```
On first render, the **CounterView** (label: **'CounterView \#positive'**) child component is created. This child component carries the \@State decorated variable **counter**. When the **CounterView.counter** state variable is updated, the **CounterView** (label: **'CounterView \#positive'**) child component is re-rendered, with its state variable value preserved. When the value of the **MainView.toggle** state variable changes to **false**, the **if** statement inside the **MainView** parent component gets updated, and subsequently the **CounterView** (label: **'CounterView \#positive'**) child component will be removed. At the same time, a new **CounterView** (label: **'CounterView \#negative'**) child component will be created. Its own **counter** state variable is set to the initial value **0**.
> **NOTE**
>
> **CounterView** (label: **'CounterView \#positive'**) and **CounterView** (label: **'CounterView \#negative'**) are two distinct instances of the same custom component. When the **if** branch changes, there is no updating of an existing child component and no preservation of state.
The following example shows the required modifications if the value of **counter** be preserved when the **if** condition changes:
```
@Component
struct CounterView {
@Link counter: number;
label: string = 'unknown';
build() {
Row() {
Text(`${this.label}`)
Button(`counter ${this.counter} +1`)
.onClick(() => {
this.counter += 1;
})
}
}
}
@Entry
@Component
struct MainView {
@State toggle: boolean = true;
@State counter: number = 0;
build() {
Column() {
if (this.toggle) {
CounterView({ counter: $counter, label: 'CounterView #positive' })
} else {
CounterView({ counter: $counter, label: 'CounterView #negative' })
}
Button(`toggle ${this.toggle}`)
.onClick(() => {
this.toggle = !this.toggle;
})
}
}
}
```
Here, the \@State decorated variable **counter** is owned by the parent component. Therefore, it is not destroyed when a **CounterView** component instance is removed. The **CounterView** component refers to the state by an \@Link decorator. This technique is sometimes referred to as "pushing up the state in the component tree." The state must be moved from a child to its parent (or parent of parent) to avoid losing it when the conditional content (or repeated content) is destroyed.
### Nested if Statements
The nesting of **if** statements makes no difference to the rule about the parent component.
```ts
@Entry
@Component
struct CompA {
@State toggle: boolean = false;
@State toggleColor: boolean = false;
build() {
Column() {
Text('Before')
.fontSize(15)
if (this.toggle) {
Text('Top True, positive 1 top')
.backgroundColor('#aaffaa').fontSize(20)
// Inner if statement
if (this.toggleColor) {
Text('Top True, Nested True, positive COLOR Nested ')
.backgroundColor('#00aaaa').fontSize(15)
} else {
Text('Top True, Nested False, Negative COLOR Nested ')
.backgroundColor('#aaaaff').fontSize(15)
}
} else {
Text('Top false, negative top level').fontSize(20)
.backgroundColor('#ffaaaa')
if (this.toggleColor) {
Text('positive COLOR Nested ')
.backgroundColor('#00aaaa').fontSize(15)
} else {
Text('Negative COLOR Nested ')
.backgroundColor('#aaaaff').fontSize(15)
}
}
Text('After')
.fontSize(15)
Button('Toggle Outer')
.onClick(() => {
this.toggle = !this.toggle;
})
Button('Toggle Inner')
.onClick(() => {
this.toggleColor = !this.toggleColor;
})
}
}
}
```
# LazyForEach: Lazy Data Loading
**LazyForEach** iterates over provided data sources and creates corresponding components during each iteration. When **LazyForEach** is used in a scrolling container, the framework creates components as required within the visible area of the scrolling container. When a component is out of the visible area, the framework destroys and reclaims the component to reduce memory usage.
## API Description
```ts
LazyForEach(
dataSource: IDataSource, // Data source to iterate over
itemGenerator: (item: any) => void, // Child component generation function
keyGenerator?: (item: any) => string // (Optional). ID generation function
): void
interface IDataSource {
totalCount(): number; // Get total count of data
getData(index: number): any; // Get single data by index
registerDataChangeListener(listener: DataChangeListener): void; // Register listener to listening data changes
unregisterDataChangeListener(listener: DataChangeListener): void; // Unregister listener
}
interface DataChangeListener {
onDataReloaded(): void; // Called while data reloaded
onDataAdd(index: number): void; // Called while single data added
onDataMove(from: number, to: number): void; // Called while single data moved
onDataDelete(index: number): void; // Called while single data deleted
onDataChange(index: number): void; // Called while single data changed
}
```
**Parameters**
| Name | Type | Mandatory | Description |
| ------------- | --------------------------------------- | ---- | ---------------------------------------- |
| dataSource | IDataSource | Yes | **LazyForEach** data source. You need to implement related APIs. |
| itemGenerator | (item:&nbsp;any)&nbsp;=&gt;&nbsp;void | Yes | Child component generation function, which generates a child component for each data item in the array.<br>**NOTE**<br>The function body of **itemGenerator** must be included in braces {...}. **itemGenerator** can and must generate only one child component for each iteration. The **if** statement is allowed in **itemGenerator**, but you must ensure that each branch of the **if** statement creates a child component of the same type. **ForEach** and **LazyForEach** statements are not allowed in **itemGenerator**.|
| keyGenerator | (item:&nbsp;any)&nbsp;=&gt;&nbsp;string | No | ID generation function, which generates a unique and fixed ID for each data item in the data source. This ID must remain unchanged for the data item even when the item is relocated in the array. When the item is replaced by a new item, the ID of the new item must be different from that of the replaced item. This ID generation function is optional. However, for performance reasons, it is strongly recommended that the ID generation function be provided, so that the framework can better identify array changes. For example, if no ID generation function is provided, a reverse of an array will result in rebuilding of all nodes in **LazyForEach**.<br>**NOTE**<br>The ID generated for each data item in the data source must be unique.|
## Description of IDataSource
```ts
interface IDataSource {
totalCount(): number;
getData(index: number): any;
registerDataChangeListener(listener: DataChangeListener): void;
unregisterDataChangeListener(listener: DataChangeListener): void;
}
```
| Declaration | Parameter Type | Description |
| ---------------------------------------- | ------------------ | ------------------------------------- |
| totalCount():&nbsp;number | - | Obtains the total number of data records. |
| getData(index:&nbsp;number):&nbsp;any | number | Obtains the data record corresponding to the specified index.<br>**index**: index of the data record to obtain.|
| registerDataChangeListener(listener:DataChangeListener):&nbsp;void | DataChangeListener | Registers a listener for data changes.<br>**listener**: listener for data changes. |
| unregisterDataChangeListener(listener:DataChangeListener):&nbsp;void | DataChangeListener | Deregisters the listener for data changes.<br>**listener**: listener for data changes. |
## Description of DataChangeListener
```ts
interface DataChangeListener {
onDataReloaded(): void;
onDataAdded(index: number): void;
onDataMoved(from: number, to: number): void;
onDataDeleted(index: number): void;
onDataChanged(index: number): void;
onDataAdd(index: number): void;
onDataMove(from: number, to: number): void;
onDataDelete(index: number): void;
onDataChange(index: number): void;
}
```
| Declaration | Parameter Type | Description |
| ---------------------------------------- | -------------------------------------- | ---------------------------------------- |
| onDataReloaded():&nbsp;void | - | Invoked when all data is reloaded. |
| onDataAdded(index:&nbsp;number):void | number | Invoked when data is added to the position indicated by the specified index.<br>**index**: index of the position where data is added. |
| onDataMoved(from:&nbsp;number,&nbsp;to:&nbsp;number):&nbsp;void | from:&nbsp;number,<br>to:&nbsp;number | Invoked when data is moved.<br>**from**: original position of data; **to**: target position of data.<br>**NOTE**<br>The ID must remain unchanged before and after data movement. If the ID changes, APIs for deleting and adding data must be called.|
| onDataChanged(index:&nbsp;number):&nbsp;void | number | Invoked when data in the position indicated by the specified index is changed.<br>**index**: listener for data changes. |
| onDataAdd(index:&nbsp;number):&nbsp;void | number | Invoked when data is added to the position indicated by the specified index.<br>**index**: index of the position where data is added. |
| onDataMove(from:&nbsp;number,&nbsp;to:&nbsp;number):&nbsp;void | from:&nbsp;number,<br>to:&nbsp;number | Invoked when data is moved.<br>**from**: original position of data; **to**: target position of data.<br>**NOTE**<br>The ID must remain unchanged before and after data movement. If the ID changes, APIs for deleting and adding data must be called.|
| onDataChanged(index:&nbsp;number):&nbsp;void | number | Invoked when data in the position indicated by the specified index is changed.<br>**index**: index of the position where data is changed.|
## Restrictions
- **LazyForEach** must be used in the container component. Only the **\<List>**, **\<Grid>**, and **\<Swiper>** components support lazy loading (that is, only the visible part and a small amount of data before and after the visible part are loaded for caching). For other components, all data is loaded at the same time.
- **LazyForEach** must create one and only one child component in each iteration.
- The generated child components must be allowed in the parent container component of **LazyForEach**.
- **LazyForEach** can be included in an **if/else** statement, and can also contain such a statement.
- The ID generation function must generate a unique value for each piece of data. If the IDs are the same, the framework ignores the UI components with the same key value. As a result, these UI components cannot be displayed in the parent container.
- **LazyForEach** must be updated using a **DataChangeListener** object. When the first parameter **dataSource** uses a state variable, a state variable change does not trigger the UI update of **LazyForEach**.
- For better rendering performance, when the **onDataChange** API of the **DataChangeListener** object is used to update the UI, an ID different from the original one needs to be generated to trigger component re-rendering.
- The call sequence of **itemGenerator** functions may be different from the order of the data items in the data source. Therefore, do not assume whether or when the **itemGenerator** and **keyGenerator** functions are executed. For example, the following example may not run properly:
```ts
LazyForEach(dataSource,
item => Text(`${item.i}. item.data.label`),
item => item.data.id.toString())
```
## Example
```ts
// Basic implementation of IDataSource to handle data listener
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[] = ['/path/image0', '/path/image1', '/path/image2', '/path/image3'];
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({ space: 3 }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Image(item).width('30%').height(50)
Text(item).fontSize(20).margin({ left: 10 })
}.margin({ left: 10, right: 10 })
}
.onClick(() => {
this.data.pushData('/path/image' + this.data.totalCount());
})
}, item => item)
}
}
}
```
# Rendering Control Overview
ArkUI uses the **build()** function of [custom components](arkts-create-custom-components.md) and declarative UI description statements in the [@builder decorator](arkts-builder.md) to build the corresponding UI. In declarative description statements, you can use rendering control statements in addition to system components to accelerate UI construction. These rendering control statements include conditional statements that control whether components are displayed, rendering statements for repeated content that quickly generate components based on array data, and lazy loading statements for scenarios involving a large amount of data.
# Rendering Control
ArkTS provides conditional rendering and loop rendering. Conditional rendering can render state-specific UI content based on the application status. Loop rendering iteratively obtains data from the data source and creates the corresponding component during each iteration.
## Conditional Rendering
Use **if/else** for conditional rendering.
> **NOTE**
>
> - State variables can be used in the **if/else** statement.
>
> - The **if/else** statement can be used to implement rendering of child components.
>
> - The **if/else** statement must be used in container components.
>
> - Some container components limit the type or number of subcomponents. When **if/else** is placed in these components, the limitation applies to components created in **if/else** statements. For example, when **if/else** is used in the **\<Grid>** container component, whose child components can only be **\<GridItem>**, only the **\<GridItem>** component can be used in the **if/else** statement.
```ts
Column() {
if (this.count < 0) {
Text('count is negative').fontSize(14)
} else if (this.count % 2 === 0) {
Text('count is even').fontSize(14)
} else {
Text('count is odd').fontSize(14)
}
}
```
## Loop Rendering
You can use **ForEach** to obtain data from arrays and create components for each data item.
```ts
ForEach(
arr: any[],
itemGenerator: (item: any, index?: number) => void,
keyGenerator?: (item: any, index?: number) => string
)
```
Since API version 9, this API is supported in ArkTS widgets.
**Parameters**
| Name | Type | Mandatory| Description |
| ------------- | ------------------------------------- | ---- | ------------------------------------------------------------ |
| arr | any[] | Yes | An array, which can be empty, in which case no child component is created. The functions that return array-type values are also allowed, for example, **arr.slice (1, 3)**. The set functions cannot change any state variables including the array itself, such as **Array.splice**, **Array.sort**, and **Array.reverse**.|
| itemGenerator | (item: any, index?: number) => void | Yes | A lambda function used to generate one or more child components for each data item in an array. A single child component or a list of child components must be included in parentheses.|
| keyGenerator | (item: any, index?: number) => string | No | An anonymous function used to generate a unique and fixed key value for each data item in an array. This key value must remain unchanged for the data item even when the item is relocated in the array. When the item is replaced by a new item, the key value of the new item must be different from that of the existing item. This key-value generator is optional. However, for performance reasons, it is strongly recommended that the key-value generator be provided, so that the development framework can better identify array changes. For example, if no key-value generator is provided, a reverse of an array will result in rebuilding of all nodes in **ForEach**.|
> **NOTE**
>
> - **ForEach** must be used in container components.
>
> - The generated child components should be allowed in the parent container component of **ForEach**.
>
> - The **itemGenerator** function can contain an **if/else** statement, and an **if/else** statement can contain **ForEach**.
>
> - The call sequence of **itemGenerator** functions may be different from that of the data items in the array. During the development, do not assume whether or when the **itemGenerator** and **keyGenerator** functions are executed. The following is an example of incorrect usage:
>
> ```ts
> ForEach(anArray.map((item1, index1) => { return { i: index1 + 1, data: item1 }; }),
> item => Text(`${item.i}. item.data.label`),
> item => item.data.id.toString())
> ```
## Example
```ts
// xxx.ets
@Entry
@Component
struct MyComponent {
@State arr: number[] = [10, 20, 30]
build() {
Column({ space: 5 }) {
Button('Reverse Array')
.onClick(() => {
this.arr.reverse()
})
ForEach(this.arr, (item: number) => {
Text(`item value: ${item}`).fontSize(18)
Divider().strokeWidth(2)
}, (item: number) => item.toString())
}
}
}
```
![forEach1](figures/forEach1.gif)
## Lazy Loading
You can use **LazyForEach** to iterate over provided data sources and create corresponding components during each iteration.
```ts
LazyForEach(
dataSource: IDataSource,
itemGenerator: (item: any) => void,
keyGenerator?: (item: any) => string
): void
interface IDataSource {
totalCount(): number;
getData(index: number): any;
registerDataChangeListener(listener: DataChangeListener): void;
unregisterDataChangeListener(listener: DataChangeListener): void;
}
interface DataChangeListener {
onDataReloaded(): void;
onDataAdd(index: number): void;
onDataMove(from: number, to: number): void;
onDataDelete(index: number): void;
onDataChange(index: number): void;
}
```
**Parameters**
| Name | Type | Mandatory| Description |
| ------------- | --------------------- | ---- | ------------------------------------------------------------ |
| dataSource | IDataSource | Yes | Object used to implement the **IDataSource** API. You need to implement related APIs. |
| itemGenerator | (item: any, index?: number) => void | Yes | A lambda function used to generate one or more child components for each data item in an array. A single child component or a list of child components must be included in parentheses.|
| keyGenerator | (item: any, index?: number) => string | No | An anonymous function used to generate a unique and fixed key value for each data item in an array. This key value must remain unchanged for the data item even when the item is relocated in the array. When the item is replaced by a new item, the key value of the new item must be different from that of the existing item. This key-value generator is optional. However, for performance reasons, it is strongly recommended that the key-value generator be provided, so that the development framework can better identify array changes. For example, if no key-value generator is provided, a reverse of an array will result in rebuilding of all nodes in **LazyForEach**.|
### Description of IDataSource
| Name | Description |
| ------------------------------------------------------------ | ---------------------- |
| totalCount(): number | Obtains the total number of data records. |
| getData(index: number): any | Obtains the data corresponding to the specified index. |
| registerDataChangeListener(listener:DataChangeListener): void | Registers a listener for data changes.|
| unregisterDataChangeListener(listener:DataChangeListener): void | Deregisters a listener for data changes.|
### Description of DataChangeListener
| Name | Description |
| -------------------------------------------------------- | -------------------------------------- |
| onDataReloaded(): void | Invoked when all data is reloaded. |
| onDataAdded(index: number): void<sup>deprecated</sup> | Invoked when data is added to the position indicated by the specified index. This API is deprecated since API version 8. You are advised to use **onDataAdd**. |
| onDataMoved(from: number, to: number): void<sup>deprecated</sup> | Invoked when data is moved from the **from** position to the **to** position. This API is deprecated since API version 8. You are advised to use **onDataMove**.|
| onDataDeleted(index: number): void<sup>deprecated</sup> | Invoked when data is deleted from the position indicated by the specified index. This API is deprecated since API version 8. You are advised to use **onDataDelete**. |
| onDataChanged(index: number): void<sup>deprecated</sup> | Invoked when data in the position indicated by the specified index is changed. This API is deprecated since API version 8. You are advised to use **onDataChange**. |
| onDataAdd(index: number): void<sup>8+</sup> | Invoked when data is added to the position indicated by the specified index. |
| onDataMove(from: number, to: number): void<sup>8+</sup> | Invoked when data is moved from the **from** position to the **to** position.|
| onDataDelete(index: number): void<sup>8+</sup> | Invoked when data is deleted from the position indicated by the specified index. |
| onDataChange(index: number): void<sup>8+</sup> | Invoked when data in the position indicated by the specified index is changed. |
## Example
```ts
// xxx.ets
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 {
// Initialize the data list.
private dataArray: string[] = ['/path/image0.png', '/path/image1.png', '/path/image2.png', '/path/image3.png']
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({ space: 3 }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Image(item).width(50).height(50)
Text(item).fontSize(20).margin({ left: 10 })
}.margin({ left: 10, right: 10 })
}
.onClick(() => {
// The count increases by one each time the list is clicked.
this.data.pushData('/path/image' + this.data.totalCount() + '.png')
})
}, item => item)
}.height('100%').width('100%')
}
}
```
> **NOTE**
>
> - **LazyForEach** must be used in the container component. Currently, only the **\<List>**, **\<Grid>**, and **\<Swiper>** components support lazy loading (that is, only the visible part and a small amount of data before and after the visible part are loaded for caching). For other components, all data is loaded at a time.
>
> - **LazyForEach** must create one and only one child component in each iteration.
>
> - The generated child components must be the ones allowed in the parent container component of **LazyForEach**.
>
> - **LazyForEach** can be included in an **if/else** statement.
>
> - For the purpose of high-performance rendering, when the **onDataChange** method of the **DataChangeListener** object is used to update the UI, the component update is triggered only when the state variable is used in the child component created by **itemGenerator**.
>
> - The call sequence of **itemGenerator** functions may be different from that of the data items in the data source. During the development, do not assume whether or when the **itemGenerator** and **keyGenerator** functions are executed. The following is an example of incorrect usage:
>
> ```ts
> LazyForEach(dataSource,
> item => Text(`${item.i}. item.data.label`),
> item => item.data.id.toString())
> ```
![lazyForEach](figures/lazyForEach.gif)
# Restrictions and Extensions
## Restrictions on Using ArkTS in Generators
ArkTS has the following restrictions on generators:
- Expressions can be used only in character strings (${expression}), **if/else** statements, **ForEach** parameters, and component parameters.
- No expressions should cause any application state variables (that is, variables decorated by **@State**, **@Link**, and **@Prop**) to change. Otherwise, undefined and potentially unstable framework behavior may occur.
- The generator function cannot contain local variables.
None of the above restrictions applies to anonymous function implementations of event methods (such as **onClick**).
## Two-Way Binding of Variables
ArkTS supports two-way binding through **$$**, which is usually used for variables whose state values change frequently.
- **$$** supports variables of primitive types and variables decorated by **@State**, **@Link**, or **@Prop**.
- **$$** supports only the **show** parameter of the **[bindPopup](../reference/arkui-ts/ts-universal-attributes-popup.md)** attribute method, the **checked** attribute of the **[\<Radio>](../reference/arkui-ts/ts-basic-components-radio.md)** component, and the **refreshing** parameter of the **[\<Refresh>](../reference/arkui-ts/ts-container-refresh.md)** component.
- When the variable bound to **$$** changes, only the current component is rendered, which improves the rendering speed.
```ts
// xxx.ets
@Entry
@Component
struct bindPopupPage {
@State customPopup: boolean = false
build() {
Column() {
Button('Popup')
.margin(20)
.onClick(() => {
this.customPopup = !this.customPopup
})
.bindPopup($$this.customPopup, {
message: "showPopup"
})
}
}
}
```
![popup](figures/popup.gif)
## Restrictions on Data Type Declarations of State Variables
1. The data types of state variables decorated by state decorators must be explicitly declared. They cannot be declared as **any** or **Date**.
Example:
```ts
// xxx.ets
@Entry
@Component
struct DatePickerExample {
// Incorrect: @State isLunar: any = false
@State isLunar: boolean = false
// Incorrect: @State selectedDate: Date = new Date('2021-08-08')
private selectedDate: Date = new Date('2021-08-08')
build() {
Column() {
Button('Switch Calendar')
.margin({ top: 30 })
.onClick(() => {
this.isLunar = !this.isLunar
})
DatePicker({
start: new Date('1970-1-1'),
end: new Date('2100-1-1'),
selected: this.selectedDate
})
.lunar(this.isLunar)
.onChange((value: DatePickerResult) => {
this.selectedDate.setFullYear(value.year, value.month, value.day)
console.info('select current date is: ' + JSON.stringify(value))
})
}.width('100%')
}
}
```
![datePicker](figures/restrictions-data-type-declarations.gif)
2. The data type declaration of the **@State**, **@Provide**, **@Link**, or **@Consume** decorated state variables can consist of only one of the primitive data types or reference data types.
The **Length**, **ResourceStr**, and **ResourceColor** types are combinations of primitive data types or reference data types. Therefore, they cannot be used by the aforementioned types of state variables.
For details about the definitions of **Length**, **ResourceStr**, and **ResourceColor**, see [Types](../../application-dev/reference/arkui-ts/ts-types.md).
Example:
```ts
// xxx.ets
@Entry
@Component
struct IndexPage {
// Incorrect: @State message: string | Resource = 'Hello World'
@State message: string = 'Hello World'
// Incorrect: @State message: ResourceStr = $r('app.string.hello')
@State resourceStr: Resource = $r('app.string.hello')
build() {
Row() {
Column() {
Text(`${this.message}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
```
![hello](figures/hello.PNG)
## Initialization Rules and Restrictions of Custom Components' Member Variables
The member variables of a component can be initialized in either of the following ways:
- Local initialization:
```ts
@State counter: Counter = new Counter()
```
- Initialization using constructor parameters:
```ts
MyComponent({counter: $myCounter})
```
The allowed method depends on the decorator of the state variable, as described in the following table.
| Decorator | Local Initialization| Initialization Using Constructor Parameters|
| ------------ | ----- | ----------- |
| @State | Mandatory | Optional |
| @Prop | Forbidden | Mandatory |
| @Link | Forbidden | Mandatory |
| @StorageLink | Mandatory | Forbidden |
| @StorageProp | Mandatory | Forbidden |
| @LocalStorageLink | Mandatory | Forbidden |
| @LocalStorageProp | Mandatory | Forbidden |
| @Provide | Mandatory | Optional |
| @Consume | Forbidden | Forbidden |
| @ObjectLink | Forbidden | Mandatory |
| Normal member variable | Recommended | Optional |
As indicated by the preceding table:
- The **@State** decorated variables must be initialized locally. Their initial values can be overwritten by the constructor parameters.
- The **@Prop** and **@Link** decorated variables must be initialized only by constructor parameters.
Comply with the following rules when using constructors to initialize member variables:
| **From the Variable in the Parent Component (Right) to the Variable in the Child Component (Below)**| **regular** | **@State** | **@Link** | **@Prop** | **@Provide** | **@Consume** | **@ObjectLink** |
|---------------------------------|----------------------------|------------|-----------|-----------|--------------|--------------|------------------|
| **regular** | Supported | Supported | Supported | Supported | Not supported | Not supported | Supported |
| **@State** | Supported | Supported | Supported | Supported | Supported | Supported | Supported |
| **@Link** | Not supported | Supported (1) | Supported (1) | Supported (1) | Supported (1) | Supported (1) | Supported (1) |
| **@Prop** | Supported | Supported | Supported | Supported | Supported | Supported | Supported |
| **@Provide** | Supported | Supported | Supported | Supported | Supported | Supported | Supported |
| **@Consume** | Not supported | Not supported | Not supported | Not supported | Not supported | Not supported | Not supported |
| **@ObjectLink** | Not supported | Not supported | Not supported | Not supported | Not supported | Not supported | Not supported |
| **From the Variable in the Parent Component (Right) to the Variable in the Child Component (Below)**| **@StorageLink** | **@StorageProp** | **@LocalStorageLink** | **@LocalStorageProp** |
|------------------|------------------|------------------|-----------------------|------------------------|
| **regular** | Supported | Not supported | Not supported | Not supported |
| **@State** | Supported | Supported | Supported | Supported |
| **@Link** | Supported (1) | Supported (1) | Supported (1) | Supported (1) |
| **@Prop** | Supported | Supported | Supported | Supported |
| **@Provide** | Supported | Supported | Supported | Supported |
| **@Consume** | Not supported | Not supported | Not supported | Not supported |
| **@ObjectLink** | Not supported | Not supported | Not supported | Not supported |
> **NOTE**
>
> **Supported (1)**: The dollar sign ($) must be used, for example, **this.$varA**.
>
> **regular**: refers to a regular variable that is not decorated by any decorator.
As indicated by the preceding tables:
- The **@ObjectLink** decorated variable cannot be directly initialized from a decorated variable in the parent component. The source of the parent component must be an array item or object attribute decorated by **@State**, **@Link**, **@Provide**, **@Consume**, or **@ObjectLink**.
- The regular variables of the parent component can be used to initialize the **@State** variable of the child component, but cannot be used to initialize the **@Link**, **@Consume**, and **@ObjectLink** variables.
- The **@State** variable of the parent component can be used to initialize the **@Prop**, **@Link** (through **$**), or regular variables of the child component, but cannot be used to initialize the **@Consume** variable.
- The **@Link** variable of the parent component cannot be used to initialize the **@Consume** and **@ObjectLink** variables of the child component.
- The **@Prop** variable of the parent component cannot be used to initialize the **@Consume** and **@ObjectLink** variables of the child component.
- **@StorageLink**, **@StorageProp**, **@LocalStorageLink**, and **@LocalStorageProp** variables cannot be initialized from the parent component.
- In addition to the preceding rules, the TypeScript strong type rules need to be followed.
Example:
```ts
@Entry
@Component
struct Parent {
message: string = "Hello World"
build() {
Column() {
Child({
stateMessage: this.message,
/* ArkTS:ERROR The regular property 'message' cannot be assigned
to the @Link property 'linkMessage'.*/
linkMessage: this.$message
})
}
.width('100%')
}
}
@Component
struct Child {
@State stateMessage: string = "Hello World"
@Link linkMessage: string
build() {
Column() {
Text(this.stateMessage)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
}
```
## Restrictions on Naming Custom Components, Classes, and Functions
The name of a custom component, class, or function cannot be the same as any system component name.
Example:
```
// Rect.ets
export class Rect {
constructor(){}
}
// Index.ets
// ERROR: The module name 'Rect' can not be the same as the inner component name.
import { Rect } from './Rect';
@Entry
@Component
struct Index {
build() {
}
}
```
# State Management Overview
In previous examples, most of the pages built are static pages, which are delivered to the end user without having to be processed. If you are building dynamic, interactive pages, you need to master state management.
**Figure 1** State managed UI
![Video_2023-03-06_152548](figures/Video_2023-03-06_152548.gif)
In the preceding example, the interaction between the user and the application triggers an update in the text state, which in turn triggers re-rendering of the UI. As a result, the **Hello World** text changes to **Hello ArkUI**.
In the declarative UI framework, the UI is the execution result of the application state. You build a UI model in which the state of the application when running is a parameter. When the parameter is changed, the UI as the return result is updated accordingly. This process of UI re-rendering caused by state changes during application running is called the state management mechanism in ArkUI.
Custom components have variables. A variable must be decorated by a decorator whenever the re-rendering of the UI depends on this variable. Otherwise, the UI is rendered only at initialization and will not be updated. The following figure shows the relationship between the state and view (UI).
![en-us_image_0000001562352677](figures/en-us_image_0000001562352677.png)
- View (UI): UI rendering, which generally refers to the UI description in the **build** method of a custom component and the **\@Builder** decorated method.
- State: data decorated by a decorator. State data is changed by invoking the event method of the component. The change of the state data triggers the re-rendering of the UI.
## Basic Concepts
- State variable: a variable decorated by the state decorator. Its change will trigger the re-rendering of the UI.
- Regular variable: a variable that has no state and is usually used for auxiliary calculation. Its change will not trigger the re-rendering of the UI.
- Data source/Synchronization source: original source of a state variable, which can be synchronized to different state data. Generally, it is the data passed from the parent component to the child component.
- Named parameter mechanism: a mechanism where the parent component passes state variables to the child component by specifying parameters. It is the primary means of passing synchronization parameters from the parent component to the child component. Example: CompA: ({ aProp: this.aProp }).
- Initialization from the parent component: a process where the parent component uses the named parameter mechanism to pass specified parameters to the child component. The default value used in local initialization is overwritten by the value passed from the parent component. Example:
```ts
@Component
struct MyComponent {
@State count: number = 0;
private increaseBy: number = 1;
build() {
}
}
@Component
struct Parent {
build() {
Column() {
// Initialization from the parent component: The named parameter specified here will overwrite the default value defined locally.
MyComponent({ count: 1, increaseBy: 2 })
}
}
}
```
- Subnode initialization: a capability to pass state variables to the child component to initialize the corresponding state variables therein. The example is the same as above.
- Local initialization: a process where a value is assigned to a variable in its declaration as the default value for initialization. Example: \@State count: number = 0.
## Decorator Overview
ArkUI provides a diverse array of decorators. By using these decorators, state variables can not only observe changes within a component, but also pass the changes between different component levels (for example, between parent and child components or grandparent and grandchild components) or globally. According to the scope of the state variable, decorators can be roughly classified into the following types:
- Decorators for managing the state owned by a component: implement state management at the component level by allowing for observation of state changes within a component and changes at different component levels. The observation is limited to state changes on the same component tree, that is, on the same page.
- Decorators for managing the state owned by an application: implement state management at the component level by allowing for observation of state changes on different pages or even different UIAbility components.
According to the data transfer mode and synchronization type, decorators can also be classified into the following types:
- Decorators that allow for one-way (read-only) transfer
- Decorators that allow for two-way (mutable) transfer
The following figure illustrates the decorators. For details, see [Component State Management](arkts-state.md) and [Application State Management](arkts-application-state-management-overview.md). You can use these decorators at your disposal to implement linkage between data and the UI.
![en-us_image_0000001502704640](figures/en-us_image_0000001502704640.png)
In the preceding figure, the decorators in the Components area are used for state management at the component level, while others are used for state management at the application level. You can use \@StorageLink/\@LocalStorageLink and \@StorageProp/\@LocalStorageProp to implement two-way and one-way synchronization of the application and component state. In the preceding figure, the data synchronization direction is indicated by the arrow direction: single-headed arrow for one-way synchronization and double-headed arrow for two-way sync.
Decorators for [managing the state owned by a component](arkts-state.md):
- \@State: An \@State decorated variable holds the state owned by the owning component. It can be the source of one- or two-way synchronization with child components. When the variable changes, the dependent component will be updated.
- \@Prop: An \@Prop decorated variable can create one-way synchronization with a variable of its parent component. \@Prop decorated variables are mutable, but changes are not synchronized to the parent component.
- \@Link: An \@Link decorated variable creates two-way synchronization with a variable of its parent component. When the @Link decorated variable has its value changed, its source is updated as well; when the source updates, the @Link decorated variable will do as well.
- \@Provide/\@Consume: Variables decorated by \@Provide/\@Consume are used for data synchronization across component levels. The components can be bound to the variables through aliases or attribute names. Data does not need to be passed through the named parameter mechanism.
- \@Observed: \@Observed is a class decorator. In scenarios involving multiple levels of nested objects or arrays in a class, the class must be decorated with the @Observed decorator. Note that decorating a class with \@Observed alone has no effect. Combined use with \@ObjectLink for two-way synchronization or with \@Prop for one-way synchronization is required.
- \@ObjectLink: An \@ObjectLink decorated variable, when used with an \@Observed decorated class of the parent component, is for two-way data synchronization in scenarios involving multiple levels of nested objects or arrays in the class.
Decorators for [managing the state owned by an application](arkts-state.md):
- AppStorage: a special LocalStorage singleton instance. It is an application-wide database bound to the application process and can be linked to components through the [@StorageProp](arkts-appstorage.md#storageprop) and [@StorageLink](arkts-appstorage.md#storagelink) decorators.
- AppStorage is the hub for application state. Data that needs to interact with components (UI) is stored in AppStorage, including PersistentStorage and Environment data. The UI accesses the data through the decorators or APIs provided by AppStorage.
- LocalStorage: an in-memory "database" for the application state declared by the application and typically used to share state across pages. It can be linked to the UI through the [@LocalStorageProp](arkts-localstorage.md#localstorageprop) and [@LocalStorageLink](arkts-localstorage.md#localstoragelink) decorators.
### Other State Management Features
\@Watch: listens for the changes of the state variables.
$$ operator: provides TS variable references for built-in components so that the variables are synchronized with the internal state of built-in components.
# State Management with Application-level Variables
This topic covers how to manage the application status with application-level variables. For details about the APIs, see [State Management with Application-level Variables](../reference/arkui-ts/ts-state-management.md).
## AppStorage
The [AppStorage](../reference/arkui-ts/ts-state-management.md#appstorage) is a singleton object in an application that provides central storage for changing state attributes of an application. It is created by the UI framework when the application is started and destroyed when the application exits.
The **AppStorage** contains all the state attributes that need to be accessed throughout the application. It retains all attributes and their values as long as the application remains running, and the attribute values can be accessed through unique key values.
Components can synchronize the application state data with the **AppStorage** through decorators. The application service logic can also be implemented by accessing the **AppStorage** through APIs.
The selection state attribute of the **AppStorage** can be synchronized with different data sources or data sinks. These data sources and data sinks can be local or remote devices and provide different functions, such as data persistence. Such data sources and data sinks can be implemented independently of the UI in the service logic.
By default, the attributes in the **AppStorage** are mutable. If needed, **AppStorage** can also use immutable (read-only) attributes.
> **NOTE**
>
> [Worker](../reference/apis/js-apis-worker.md) can interact with the main thread only through [postMessage](../reference/apis/js-apis-worker.md#postmessage).
### @StorageLink Decorator
Two-way data binding can be established between components and the **AppStorage** through state variables decorated by **@StorageLink(*key*)**. Wherein, **key** is the attribute key value in the **AppStorage**. When a component containing the **@StorageLink** decorated variable is created, the variable is initialized using the value in the **AppStorage**. Changes made to this variable in the component will be first synchronized to the **AppStorage**, and then to other bound instances, such as **PersistentStorage** or other bound UI components.
### @StorageProp Decorator
One-way data binding can be established between components and the **AppStorage** through state variables decorated by **@StorageProp(*key*)**. Wherein, **key** is the attribute key value in the **AppStorage**. When a component containing the **@StorageProp** decorated variable is created, the variable is initialized using the value in the **AppStorage**. Changes made to the value in the **AppStorage** will cause the bound UI component to update the state.
### Example
Each time the user clicks the **Count** button, the value of **this.varA** will increase by 1. This variable is synchronized with **varA** in the **AppStorage**. Each time the user clicks the language button, the value of **languageCode** in the **AppStorage** will be changed, and the change is synchronized to the **this.languageCode** variable.
```ts
// xxx.ets
@Entry
@Component
struct ComponentA {
@StorageLink('varA') varA: number = 2
@StorageProp('languageCode') languageCode: string = 'en'
private label: string = 'count'
aboutToAppear() {
this.label = (this.languageCode === 'en') ? 'Number' : 'Count'
}
build() {
Column() {
Row({ space: 20 }) {
Button(`${this.label}: ${this.varA}`)
.onClick(() => {
AppStorage.Set<number>('varA', AppStorage.Get<number>('varA') + 1)
})
Button(`language: ${this.languageCode}`)
.onClick(() => {
if (AppStorage.Get<string>('languageCode') === 'zh') {
AppStorage.Set<string>('languageCode', 'en')
} else {
AppStorage.Set<string>('languageCode', 'zh')
}
this.label = (this.languageCode === 'en') ? 'Number' : 'Count'
})
}
.margin({ top: 50, bottom: 50 })
Row() {
Button (`Change @StorageLink decorated variable: ${this.varA}`).height(40).fontSize(14)
.onClick(() => {
this.varA++
})
}
}.width('100%')
}
}
```
![appstorage](figures/appstorage.gif)
## LocalStorage
> **NOTE**
>
> This API is supported since API version 9. Updates will be marked with a superscript to indicate their earliest API version.
The **LocalStorage** is a storage unit in an application. Its lifecycle follows its associated ability. In the stage model, the **LocalStorage** provides global data isolation between abilities and applies to where a data sharing scope smaller than that provided by the **AppStorage** is required. The **LocalStorage** also provides storage for application-wide mutable and immutable state attributes, which are used for building part of the application UI, such as an ability UI. The **LocalStorage** resolves the data interference between the application and the abilities and, in multi-instance scenarios, data interference between different **Ability** instances under the same **Ability** class. In distributed migration scenarios, **Ability**, as the minimum unit for the system to schedule applications, allows for easier component data migration when working with the **LocalStorage**.
At the application layer, multiple **LocalStorage** instances can be created for an application, each corresponding to an ability of the application.
An application can have multiple abilities. At most one **LocalStorage** instance can be allocated to the components in an ability. In addition, all components in the ability inherit access to the objects stored in the **LocalStorage** instance.
A component can access a maximum of one **LocalStorage** instance, and one **LocalStorage** instance can be assigned to multiple components.
### @LocalStorageLink Decorator
Two-way data binding can be established between a component and the **LocalStorage** through the component's state variable decorated by **@LocalStorageLink(*key*)**. Wherein, **key** is the attribute key value in the **LocalStorage**. When a component that contains a **@LocalStorageLink** decorated state variable is created, the state variable is initialized with the initial value in the **LocalStorage**. If no initial value is assigned in the **LocalStorage**, the state variable will use the value defined by **@LocalStorageLink**. Changes made to the **@LocalStorageLink** decorated variable in a component will be first synchronized to the **LocalStorage**, and then to other bound UI components under the same ability.
### @LocalStorageProp Decorator
One-way data binding can be established between a component and the **LocalStorage** through the component's state variable decorated by **@LocalStorageProp(*key*)**. Wherein, **key** is the attribute key value in the **LocalStorage**. When a component that contains a **@LocalStorageProp** decorated state variable is created, the state variable is initialized with the initial value in the **LocalStorage**. Changes made to the value in the **LocalStorage** will cause all UI components under the current ability to update the state.
> **NOTE**
>
> If a **LocalStorage** instance does not have an initial value assigned when being created, it can use the initial value defined by **@LocalStorageLink** or **@LocalStorageProp** in the component.
### Example 1: Creating a LocalStorage Instance in an Ability
The **LocalStorage** is loaded through the **loadContent** API. For details, see [loadContent](../reference/apis/js-apis-window.md#loadcontent9-1).
```ts
// MainAbility.ts
import Ability from '@ohos.application.Ability'
export default class MainAbility extends Ability {
storage: LocalStorage
onCreate() {
this.storage = new LocalStorage()
this.storage.setOrCreate('storageSimpleProp', 121)
console.info('[Demo MainAbility onCreate]')
}
onDestroy() {
console.info('[Demo MainAbility onDestroy]')
}
onWindowStageCreate(windowStage) {
// storage is passed to the loadContent API as a parameter.
windowStage.loadContent('pages/Index', this.storage)
}
onWindowStageDestroy() {
console.info('[Demo] MainAbility onWindowStageDestroy')
}
onForeground() {
console.info('[Demo] MainAbility onForeground')
}
onBackground() {
console.info('[Demo] MainAbility onBackground')
}
}
```
The **@Component** decorated component obtains data.
```ts
// Index.ets
let storage = LocalStorage.GetShared()
@Entry(storage)
@Component
struct LocalStorageComponent {
@LocalStorageLink('storageSimpleProp') simpleVarName: number = 0
build() {
Column() {
Button(`LocalStorageLink: ${this.simpleVarName.toString()}`)
.margin(20)
.onClick(() => {
this.simpleVarName += 1
})
Text(JSON.stringify(this.simpleVarName))
.fontSize(50)
LocalStorageComponentProp()
}.width('100%')
}
}
@Component
struct LocalStorageComponentProp {
@LocalStorageProp('storageSimpleProp') simpleVarName: number = 0
build() {
Column() {
Button(`LocalStorageProp: ${this.simpleVarName.toString()}`)
.margin(20)
.onClick(() => {
this.simpleVarName += 1
})
Text(JSON.stringify(this.simpleVarName))
.fontSize(50)
}.width('100%')
}
}
```
![appstorage1](figures/appstorage1.gif)
### Example 2: Defining LocalStorage on the Entry Page
```ts
// xxx.ets
let storage = new LocalStorage({ "PropA": 47 })
@Entry(storage)
@Component
struct ComA {
@LocalStorageLink("PropA") storageLink: number = 1
build() {
Column() {
Text(`Parent from LocalStorage ${this.storageLink}`)
.fontSize(18)
.margin(20)
.onClick(() => this.storageLink += 1)
Child()
}
}
}
@Component
struct Child {
@LocalStorageLink("PropA") storageLink: number = 1
build() {
Text(`Child from LocalStorage ${this.storageLink}`)
.fontSize(18)
.margin(20)
.onClick(() => this.storageLink += 1)
}
}
```
![appstorage2](figures/appstorage2.gif)
## PersistentStorage
[PersistentStorage](../reference/arkui-ts/ts-state-management.md#persistentstorage) provides a set of static methods for managing persistent data of applications. Persistent data with specific tags can be linked to the **AppStorage**, and then the persistent data can be accessed through the **AppStorage** APIs. Alternatively, the **@StorageLink** decorator can be used to access the variable that matches the specific key.
> **NOTE**
>
> - When using the **PersistProp** API in **PersistentStorage**, ensure that the input key exists in the **AppStorage**.
> - The **DeleteProp** API in **PersistentStorage** takes effect only for the data that has been linked during the current application startup.
```ts
// xxx.ets
PersistentStorage.PersistProp('highScore', '0')
@Entry
@Component
struct PersistentComponent {
@StorageLink('highScore') highScore: string = '0'
@State currentScore: number = 0
build() {
Column() {
if (this.currentScore === Number(this.highScore)) {
Text(`new highScore : ${this.highScore}`).fontSize(18)
}
Button(`goal!, currentScore : ${this.currentScore}`)
.margin(20)
.onClick(() => {
this.currentScore++
if (this.currentScore > Number(this.highScore)) {
this.highScore = this.currentScore.toString()
}
})
}.width('100%')
}
}
```
![appstorage3](figures/appstorage3.gif)
## Environment
[Environment](../reference/arkui-ts/ts-state-management.md#environment) is a singleton object created by the framework when the application is started. It provides the **AppStorage** with an array of environment state attributes required by the application. These attributes, such as the system language and color mode, describe the device environment where the application runs. **Environment** and its attributes are immutable, and all attribute values are of simple types. The following example shows how to obtain whether accessibility is enabled from **Environment**:
```ts
Environment.EnvProp('accessibilityEnabled', 'default')
var enable = AppStorage.Get('accessibilityEnabled')
```
**accessibilityEnabled** is the default system variable identifier provided by **Environment**. You need to bind the corresponding system attribute to the **AppStorage**. Then, you can use the methods or decorators in the **AppStorage** to access the corresponding system attribute data.
# Basic Concepts
In the multi-dimensional state management mechanism for ArkUI, UI-related data can be used not only within a component, but also be transferred between different component levels (for example, between parent and child components, between grandparent and grandchild components, or globally). In addition, data transfer can be classified as one-way (read-only) or two-way (mutable). You can use these capabilities at your disposal to implement linkage between data and the UI.
![](figures/CoreSpec_figures_state-mgmt-overview.png)
## State Management with Page-level Variables
| Decorator | Decorates... | Description |
| ----------- | ------------------------- | ------------------------------------------------------------ |
| @State | Primitive data types, classes, and arrays | If the decorated state data is modified, the **build** method of the component will be called to update the UI. |
| @Prop | Primitive data types | This decorator is used to establish one-way data binding between the parent and child components. When the data associated with the parent component is modified, the UI of the current component is re-rendered.|
| @Link | Primitive data types, classes, and arrays | This decorator is used to establish two-way data binding between the parent and child components. The internal state data of the parent component is used as the data source. Any changes made to one component will be reflected to the other.|
| @Observed | Class | This decorator is used to indicate that the data changes in the class will be managed by the UI page. |
| @ObjectLink | Objects of **@Observed** decorated classes| When the decorated state variable is modified, the parent and sibling components that have the state variable will be notified for UI re-rendering.|
| @Provide | Primitive data types, classes, and arrays | As the data provider, **@Provide** can update the data of child nodes and trigger page re-rendering.|
| @Consume | Primitive data types, classes, and arrays | When the **@Consume** decorated variable detects the update of the **@Provide** decorated variable, the re-rendering of the current custom component is triggered.|
## State Management with Application-level Variables
**AppStorage** is the central store of the application states in the entire UI. ArkUI creates a singleton **AppStorage** object for the application and provides the corresponding decorators and APIs for the application.
- **@StorageLink**: works in a way similar to that of **@Consume**. The difference is that the target object is obtained from the **AppStorage** based on the given name. **@StorageLink** establishes two-way binding between the decorated UI component and **AppStorage** to synchronize data.
- **@StorageProp**: synchronizes UI component attributes with the **AppStorage** unidirectionally. That is, the value change in the **AppStorage** will trigger an update of the corresponding UI component, but the change of the UI component will not cause an update of the attribute value in the **AppStorage**.
- Service logic implementation API: adds, reads, modifies, or deletes the state data of applications. The changes made by this API will be synchronized to the UI component for UI update.
- **LocalStorage**: provides ability-specific storage.
- **@LocalStorageLink**: establishes two-way data binding between a component and the **LocalStorage**. Specifically, this is achieved by decorating the component's state variable with **@LocalStorageLink(*key*)**. Wherein, **key** is the attribute key value in the **LocalStorage**.
- **@LocalStorageProp**: establishes one-way data binding between a component and the **LocalStorage**. Specifically, this is achieved by decorating the component's state variable with **@LocalStorageProp(*key*)**. Wherein, **key** is the attribute key value in the **LocalStorage**.
- **PersistentStorage**: provides a set of static methods for managing persistent data of applications. Persistent data with specific tags can be linked to the **AppStorage**, and then the persistent data can be accessed through the **AppStorage** APIs. Alternatively, the **@StorageLink** decorator can be used to access the variable that matches the specific key.
- **Environment**: provides the **AppStorage** with an array of environment state attributes that are required by the application and describe the device environment where the application runs. It is a singleton object created by the framework when the application is started.
For details about how to use state variables, see [Restrictions on Data Type Declarations of State Variables](arkts-restrictions-and-extensions.md).
# State Management with Page-level Variables
This topic covers how to manage the states with page-level variables with the **@State**, **@Prop**, **@Link**, **@Provide**, **@Consume**, **@ObjectLink**, **@Observed**, and **@Watch** decorators.
For details about the constraints of the **@State**, **@Provide**, **@Link**, and **@Consume** decorated state variables, see [Restrictions on Data Type Declarations of State Variables](./arkts-restrictions-and-extensions.md).
## @State
The **@State** decorated variable is the internal state data of the component. When the state data is modified, the **build** method of the component is called to refresh the UI.
The **@State** data has the following features:
- Support for multiple types: The following types are supported: strong types by value and by reference, including **class**, **number**, **boolean**, **string**, as well as arrays of these types, that is, **Array\<class>**, **Array\<number>**, **Array\<boolean>**, and **Array\<string>**. **object** and **any** are not supported.
- Support for multiple instances: Multiple instances can coexist in a component. The internal state data of different instances is independent.
- **Private**: An attribute marked with **@State** can only be accessed within the component.
- Local initialization required: Initial values must be allocated to all **@State** decorated variables. Uninitialized variables may cause undefined framework exceptions.
- Support for setting of initial attribute values based on the state variable name: When creating a component instance, you can explicitly specify the initial value of the **@State** decorated attribute based on the variable name.
**Example**
In the following example:
- Two **@State** decorated variables, **count** and **title**, have been defined for **MyComponent**. If the value of **count** or **title** changes, the **build** method of **MyComponent** needs to be called to render the component again.
- The **EntryComponent** has multiple **MyComponent** instances. The internal status change of the first **MyComponent** instance does not affect the second **MyComponent** instance.
- When creating a **MyComponent** instance, initialize the variables in the component based on the variable name. For example:
```ts
MyComponent({ title: { value: 'Hello World 2' }, count: 7 })
```
```ts
// xxx.ets
class Model {
value: string
constructor(value: string) {
this.value = value
}
}
@Entry
@Component
struct EntryComponent {
build() {
Column() {
MyComponent ({ count: 1,increaseBy:2 }) // First MyComponent instance
MyComponent({ title: { value:'Hello World 2' }, count: 7 }) // Second MyComponent instance
}
}
}
@Component
struct MyComponent {
@State title: Model = { value: 'Hello World' }
@State count: number = 0
private toggle: string = 'Hello World'
private increaseBy: number = 1
build() {
Column() {
Text(`${this.title.value}`).fontSize(30)
Button('Click to change title')
.margin(20)
.onClick(() => {
// Change the value of the internal status variable title.
this.title.value = (this.toggle == this.title.value) ? 'Hello World' : 'Hello ArkUI'
})
Button(`Click to increase count=${this.count}`)
.margin(20)
.onClick(() => {
// Change the value of the internal status variable count.
this.count += this.increaseBy
})
}
}
}
```
## @Prop
**@Prop** and **@State** have the same semantics but different initialization modes. A **@Prop** decorated variable in a component must be initialized using the **@State** decorated variable in its parent component. The **@Prop** decorated variable can be modified in the component, but the modification is not updated to the parent component; the modification to the **@State** decorated variable is synchronized to the **@Prop** decorated variable. That is, **@Prop** establishes one-way data binding.
The **@Prop** decorated state variable has the following features:
- Support for simple types: The number, string, and boolean types are supported.
- Private: Data is accessed only within the component.
- Support for multiple instances: A component can have multiple attributes decorated by **@Prop**.
- Support for initialization with a value passed to the @Prop decorated variable: When a new instance of the component is created, all **@Prop** variables must be initialized. Initialization inside the component is not supported.
**Example**
In the following example, when the user presses **+1** or **-1**, the status of the parent component changes and the **build** method is executed again. In this case, a new **CountDownComponent** instance is created. The **countDownStartValue** attribute of the parent component is used to initialize the **@Prop** decorated variable of the child component. When the **count - costOfOneAttempt** button of the child component is touched, the value of the **@Prop** decorated variable **count** is changed. As a result, the **CountDownComponent** is rendered again. However, the change of the **count** value does not affect the **countDownStartValue** value of the parent component.
```ts
// xxx.ets
@Entry
@Component
struct ParentComponent {
@State countDownStartValue: number = 10 // Initialize countDownStartValue
build() {
Column() {
Text(`Grant ${this.countDownStartValue} nuggets to play.`).fontSize(18)
Button('+1 - Nuggets in New Game')
.margin(15)
.onClick(() => {
this.countDownStartValue += 1
})
Button('-1 - Nuggets in New Game')
.margin(15)
.onClick(() => {
this.countDownStartValue -= 1
})
// When creating a child component, you must provide the initial value of its @Prop decorated variable count in the constructor parameter and initialize the regular variable costOfOneAttempt (not @Prop decorated).
CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 })
}
}
}
@Component
struct CountDownComponent {
@Prop count: number
private costOfOneAttempt: number
build() {
Column() {
if (this.count > 0) {
Text(`You have ${this.count} Nuggets left`).fontSize(18)
} else {
Text('Game over!').fontSize(18)
}
Button('count - costOfOneAttempt')
.margin(15)
.onClick(() => {
this.count -= this.costOfOneAttempt
})
}
}
}
```
## @Link
Two-way binding can be established between the **@Link** decorated variable and the **@State** decorated variable of the parent component. The **@Link** data has the following features:
- Support for multiple types: The **@Link** decorated variables support the data types the same as the **@State** decorated variables; that is, the value can be of the following types: class, number, string, boolean, or arrays of these types.
- Private: Data is accessed only within the component.
- Single data source: The variable used to initialize the **@Link** decorated variable in a component must be a state variable defined in the parent component.
- **Two-way binding**: When a child component changes the **@Link** decorated variable, the **@State** decorated variable of its parent component is also changed.
- Support for initialization with the variable reference passed to the @Link decorated variable: When creating an instance of the component, you must use the naming parameter to initialize all **@Link** decorated variables. **@Link** decorated variables can be initialized by using the reference of the **@State** or **@Link** decorated variable. Wherein, the **@State** decorated variables can be referenced using the **'$'** operator.
> **NOTE**
>
> A **@Link** decorated variable cannot be initialized inside the component.
**Simple Type Example**
The **@Link** semantics are derived from the '**$**' operator. In other words, **$isPlaying** is the two-way binding of the internal state **this.isPlaying**. When the button in the **PlayButton** child component is touched, the value of the **@Link** decorated variable is changed, and **PlayButton** together with the **\<Text>** and **\<Button>** components of the parent component is refreshed. Similarly, when the button in the parent component is touched, the value of **this.isPlaying** is changed, and **PlayButton** together with the **\<Text>** and **\<Button>** components of the parent component is refreshed.
```ts
// xxx.ets
@Entry
@Component
struct Player {
@State isPlaying: boolean = false
build() {
Column() {
PlayButton({ buttonPlaying: $isPlaying })
Text(`Player is ${this.isPlaying ? '' : 'not'} playing`).fontSize(18)
Button('Parent:' + this.isPlaying)
.margin(15)
.onClick(() => {
this.isPlaying = !this.isPlaying
})
}
}
}
@Component
struct PlayButton {
@Link buttonPlaying: boolean
build() {
Column() {
Button(this.buttonPlaying ? 'pause' : 'play')
.margin(20)
.onClick(() => {
this.buttonPlaying = !this.buttonPlaying
})
}
}
}
```
**Complex Type Example**
```ts
// xxx.ets
@Entry
@Component
struct Parent {
@State arr: number[] = [1, 2, 3]
build() {
Column() {
Child({ items: $arr })
Button('Parent Button: splice')
.margin(10)
.onClick(() => {
this.arr.splice(0, 1, 60)
})
ForEach(this.arr, item => {
Text(item.toString()).fontSize(18).margin(10)
}, item => item.toString())
}
}
}
@Component
struct Child {
@Link items: number[]
build() {
Column() {
Button('Child Button1: push')
.margin(15)
.onClick(() => {
this.items.push(100)
})
Button('Child Button2: replace whole item')
.margin(15)
.onClick(() => {
this.items = [100, 200, 300]
})
}
}
}
```
**Example of Using @Link, @State, and @Prop Together**
In the following example, **ParentView** contains two child components: **ChildA** and **ChildB**. The **counter** state variable of **ParentView** is used to initialize the **@Prop** decorated variable of **ChildA** and the **@Link** decorated variable of **ChildB**.
- **@Link** establishes two-way binding between **ChildB** and **ParentView**.Value changes of the **counterRef** state variable in **ChildB** will be synchronized to **ParentView** and **ChildA**.
- **@Prop** establishes one-way binding between **ChildA** and **ParentView**. Value changes of the **counterVal** state variable in **ChildA** will trigger a re-render of **ChildA**, but will not be synchronized to **ParentView** or **ChildB**.
```ts
// xxx.ets
@Entry
@Component
struct ParentView {
@State counter: number = 0
build() {
Column() {
ChildA({ counterVal: this.counter })
ChildB({ counterRef: $counter })
}
}
}
@Component
struct ChildA {
@Prop counterVal: number
build() {
Button(`ChildA: (${this.counterVal}) + 1`)
.margin(15)
.onClick(() => {
this.counterVal += 1
})
}
}
@Component
struct ChildB {
@Link counterRef: number
build() {
Button(`ChildB: (${this.counterRef}) + 1`)
.margin(15)
.onClick(() => {
this.counterRef += 1
})
}
}
```
## @Observed and @ObjectLink
When you need to set up bidirectional synchronization for a parent variable (**parent_a**) between the parent and child components, you can use **@State** to decorate the variable (**parent_a**) in the parent component and use **@Link** to decorate the corresponding variable (**child_a**) in the child component. In this way, data can be synchronized between the parent component and the specific child component, and between the parent component and its other child components. As shown below, bidirectional synchronization is configured for variables of **ClassA** in the parent and child components. If attribute **c** of the variable in child component 1 has its value changed, the parent component will be notified to synchronize the change. If attribute **c** in the parent component has its value changed, all child components will be notified to synchronize the change.
![en-us_image_0000001251090821](figures/en-us_image_0000001251090821.png)
In the preceding example, full synchronization is performed for a data object. If you want to synchronize partial information of a data object in a parent component, and if the information is a class object, use **@ObjectLink** and **@Observed** instead, as shown below.
![en-us_image_0000001206450834](figures/en-us_image_0000001206450834.png)
### Configuration Requirements
- **@Observed** applies to classes, and **@ObjectLink** applies to variables.
- The variables decorated by **@ObjectLink** must be of the class type.
- The classes must be decorated by **@Observed**.
- Parameters of the simple types are not supported. You can use **@Prop** to perform unidirectional synchronization.
- **@ObjectLink** decorated variables are immutable.
- Attribute changes are allowed. If an object is referenced by multiple **@ObjectLink** decorated variables, all custom components that have these variables will be notified for re-rendering.
- Default values cannot be set for **@ObjectLink** decorated variables.
- The parent component must be initialized with a TypeScript expression that involves variables decorated by **@State**, **@Link**, **@StorageLink**, **@Provide**, or **@Consume**.
- **@ObjectLink** decorated variables are private variables and can be accessed only within the component.
### Example
```ts
// xxx.ets
// Use @ObjectLink and @Observed to set up bidirectional synchronization for the class object ClassA between the parent component ViewB and the child component ViewA. In this way, changes made to ClassA in ViewA will be synchronized to ViewB and other child components bound to ClassA.
var nextID: number = 0
@Observed
class ClassA {
public name: string
public c: number
public id: number
constructor(c: number, name: string = 'OK') {
this.name = name
this.c = c
this.id = nextID++
}
}
@Component
struct ViewA {
label: string = 'ViewA1'
@ObjectLink a: ClassA
build() {
Row() {
Button(`ViewA [${this.label}] this.a.c= ${this.a.c} +1`)
.onClick(() => {
this.a.c += 1
})
}.margin({ top: 10 })
}
}
@Entry
@Component
struct ViewB {
@State arrA: ClassA[] = [new ClassA(0), new ClassA(0)]
build() {
Column() {
ForEach(this.arrA, (item) => {
ViewA({ label: `#${item.id}`, a: item })
}, (item) => item.id.toString())
ViewA({ label: `this.arrA[first]`, a: this.arrA[0] })
ViewA({ label: `this.arrA[last]`, a: this.arrA[this.arrA.length - 1] })
Button(`ViewB: reset array`)
.margin({ top: 10 })
.onClick(() => {
this.arrA = [new ClassA(0), new ClassA(0)]
})
Button(`ViewB: push`)
.margin({ top: 10 })
.onClick(() => {
this.arrA.push(new ClassA(0))
})
Button(`ViewB: shift`)
.margin({ top: 10 })
.onClick(() => {
this.arrA.shift()
})
}.width('100%')
}
}
```
## @Provide and @Consume
As the data provider, **@Provide** can update the data of child nodes and trigger page rendering. After **@Consume** detects that the **@Provide** decorated variable is updated, it will initiate re-rendering of the current custom component.
> **NOTE**
>
> When using **@Provide** and **@Consume**, avoid circular reference that may lead to infinite loops.
### @Provide
| Name | Description |
| -------------- | ------------------------------------------------------------ |
| Decorator parameter | A constant of the string type, which is used to set an alias for a decorated variable. If an alias is specified, implement the data update for this alias. If there is no alias, use the variable name as the alias. **@Provide(*'alias'*)** is recommended.|
| Synchronization mechanism | The **@Provide** decorated variable is similar to the **@State** decorated variable. You can change the value of the variable to trigger a re-render. You can also modify the **@Consume** decorated variable to modify the **@State** decorated variable reversely.|
| Initial value | The initial value must be set. |
| Page re-rendering scenarios| Page re-rendering is triggered in the following scenarios:<br>- Changes of variables of simple types (boolean, string, and number)<br>- Changes of the **@Observed** decorated classes or their attributes<br>- Addition, deletion, or updating of elements in an array|
### @Consume
| Type | Description |
| ------ | ---------------- |
| Initial value| The default initial value cannot be set.|
### Example
```ts
// xxx.ets
@Entry
@Component
struct CompA {
@Provide("reviewVote") reviewVotes: number = 0;
build() {
Column() {
CompB()
Button(`CompA: ${this.reviewVotes}`)
.margin(10)
.onClick(() => {
this.reviewVotes += 1;
})
}
}
}
@Component
struct CompB {
build() {
Column() {
CompC()
}
}
}
@Component
struct CompC {
@Consume("reviewVote") reviewVotes: number
build() {
Column() {
Button(`CompC: ${this.reviewVotes}`)
.margin(10)
.onClick(() => {
this.reviewVotes += 1
})
}.width('100%')
}
}
```
## @Watch
**@Watch** is used to listen for changes of state variables. The syntax structure is as follows:
```ts
@State @Watch("onChanged") count : number = 0
```
As shown above, add an **@Watch** decorator to the target state variable to register an **onChanged** callback. When the state variable **count** is changed, the **onChanged** callback will be triggered.
**@Watch** can be used to listen for value changes of variables decorated by **@State**, **@Prop**, **@Link**, **@ObjectLink**, **@Provide**, **@Consume**, **@StorageProp**, and **@StorageLink**.
> **NOTE**
>
> **@Watch** cannot be used to listen for in-depth data modification, such as changes of object values in an array.
```ts
// xxx.ets
@Entry
@Component
struct CompA {
@State @Watch('onBasketUpdated') shopBasket: Array<number> = [7, 12, 47, 3]
@State totalPurchase: number = 0
@State addPurchase: number = 0
aboutToAppear() {
this.updateTotal()
}
updateTotal(): void {
let sum = 0;
this.shopBasket.forEach((i) => {
sum += i
})
// Calculate the total amount of items in the shopping basket. If the amount exceeds 100, the specified discount will be applied.
this.totalPurchase = (sum < 100) ? sum : 0.9 * sum
return this.totalPurchase
}
// This method is triggered when the value of shopBasket is changed.
onBasketUpdated(propName: string): void {
this.updateTotal()
}
build() {
Column() {
Button('add to basket ' + this.addPurchase)
.margin(15)
.onClick(() => {
this.addPurchase = Math.round(100 * Math.random())
this.shopBasket.push(this.addPurchase)
})
Text(`${this.totalPurchase}`)
.fontSize(30)
}
}
}
```
# \@State: State Owned by Component
An \@State decorated variable, also called a state variable, is a variable that holds the state property and is used to render the owning custom component. When it changes, the UI is re-rendered accordingly.
Among the decorators related to state variables, \@State is the most basic decorator, as it is the one that empowers variables to have the state property. It is also the data source of most state variables.
> **NOTE**
>
> Since API version 9, this decorator is supported in ArkTS widgets.
## Overview
An @State decorated variable, like all other decorated variables in the declarative paradigm, are private and only accessible from within the component. Its type and its local initialization must be specified. Initialization from the parent component using the named parameter mechanism is accepted.
\@State decorated variables have the following features:
- A one-way and two-way data synchronization relationship can be set up from an \@State decorated variable to an \@Prop, \@Link, or \@ObjectLink decorated variable in a child component.
- The lifecycle of the \@State decorated variable is the same as that of its owning custom component.
## Rules of Use
| \@State Decorator| Description |
| ------------ | ---------------------------------------- |
| Decorator parameters | None. |
| Synchronization type | Does not synchronize with any type of variable in the parent component. |
| Allowed variable types | Object, class, string, number, Boolean, enum, and array of these types. For details about the scenarios of nested types, see [Observed Changes](#observed-changes).<br>The type must be specified.<br>**any** is not supported. A combination of simple and complex types is not supported. The **undefined** and **null** values are not allowed.<br>**NOTE**<br>Avoid using this decorator to decorate the Date type, as doing so may lead to unexpected behavior of the application.<br>The Length, ResourceStr, and ResourceColor types are a combination of simple and complex types and therefore not supported.|
| Initial value for the decorated variable | Mandatory. |
## Variable Transfer/Access Rules
| Transfer/Access | Description |
| --------- | ---------------------------------------- |
| Initialization from the parent component | Optional. Initialization from the parent component or local initialization can be used.<br>An \@State decorated variable can be initialized from a regular variable or an \@State, \@Link, \@Prop, \@Provide, \@Consume, \@ObjectLink, \@StorageLink, \@StorageProp, \@LocalStorageLink, or \@LocalStorageProp decorated variable in its parent component.|
| Subnode initialization | Supported. An \@State decorated variable can be used to initialize a regular variable or \@State, \@Link, \@Prop, or \@Provide decorated variable in the child component.|
| Access| Private, accessible only within the component. |
**Figure 1** Initialization rule
![en-us_image_0000001502091796](figures/en-us_image_0000001502091796.png)
## Observed Changes and Behavior
Not all changes to state variables cause UI updates. Only changes that can be observed by the framework do. This section describes what changes can be observed and how the framework triggers UI updates after the changes are observed, that is, how the framework behaves.
### Observed Changes
- When the decorated variable is of the Boolean, string, or number type, its value change can be observed.
```ts
// for simple type
@State count: number = 0;
// value changing can be observed
this.count = 1;
```
- When the decorated variable is of the class or Object type, its value change and value changes of all its attributes, that is, the attributes that **Object.keys(observedObject)** returns. Below is an example.
Declare the **ClassA** and **Model** classes.
```ts
class ClassA {
public value: string;
constructor(value: string) {
this.value = value;
}
}
class Model {
public value: string;
public name: ClassA;
constructor(value: string, a: ClassA) {
this.value = value;
this.name = a;
}
}
```
Use \@State to decorate a variable of the Model class object type.
```ts
// Class type
@State title: Model = new Model('Hello', new ClassA('World'));
```
Assign a value to the \@State decorated variable.
```ts
// Assign a value to the class object.
this.title = new Model('Hi', new ClassA('ArkUI'));
```
Assign a value to an attribute of the \@State decorated variable.
```ts
// Assign a value to an attribute of the class object.
this.title.value = 'Hi'
```
The value assignment of the nested attribute cannot be observed.
```ts
// The value assignment of the nested attribute cannot be observed.
this.title.name.value = 'ArkUI'
```
- When the decorated variable is of the array type, the addition, deletion, and updates of array items can be observed. Below is an example.
Declare the **ClassA** and **Model** classes.
```ts
class Model {
public value: number;
constructor(value: number) {
this.value = value;
}
}
```
Use \@State to decorate a variable of the Model class array type.
```ts
@State title: Model[] = [new Model(11), new Model(1)]
```
The value assignment of the array itself can be observed.
```ts
this.title = [new Model(2)]
```
The value assignment of array items can be observed.
```ts
this.title[0] = new Model(2)
```
The deletion of array items can be observed.
```ts
this.title.pop()
```
The addition of array items can be observed.
```ts
this.title.push(new Model(12))
```
### Framework Behavior
- When a state variable is changed, the framework searches for components that depend on this state variable.
- The framework executes an update method of the dependent components, which triggers re-rendering of the components.
- Components or UI descriptions irrelevant to the state variable are not re-rendered, thereby implementing on-demand page updates.
## Application Scenarios
### Decorating Variables of Simple Types
In this example, \@State is used to decorate the **count** variable of the simple type and turns it into a state variable. The change of **count** causes the update of the **\<Button>** component.
- When the state variable **count** changes, the framework searches for components that depend on this state variable, which include only the **\<Button>** component in this example.
- The framework executes the update method of the **\<Button>** component to implement on-demand update.
```ts
@Entry
@Component
struct MyComponent {
@State count: number = 0;
build() {
Button(`click times: ${this.count}`)
.onClick(() => {
this.count += 1;
})
}
}
```
### Decorating Variables of the Class Object Type
- In this example, \@State is used to decorate the variables **count** and **title** in the custom component **MyComponent**. The type of **title** is **Model**, a custom class. If the value of **count** or **title** changes, the framework searches for all **MyComponent** instances that depend on these variables and triggers re-rendering of them.
- The **EntryComponent** has multiple **MyComponent** instances. The internal state change of the first **MyComponent** instance does not affect the second **MyComponent** instance.
```ts
class Model {
public value: string;
constructor(value: string) {
this.value = value;
}
}
@Entry
@Component
struct EntryComponent {
build() {
Column() {
// The parameters specified here will overwrite the default values defined locally during initial render. Not all parameters need to be initialized from the parent component.
MyComponent({ count: 1, increaseBy: 2 })
MyComponent({ title: new Model('Hello, World 2'), count: 7 })
}
}
}
@Component
struct MyComponent {
@State title: Model = new Model('Hello World');
@State count: number = 0;
private increaseBy: number = 1;
build() {
Column() {
Text(`${this.title.value}`)
Button(`Click to change title`).onClick(() => {
// The update of the @State decorated variable triggers the update of the <Text> component.
this.title.value = this.title.value === 'Hello ArkUI' ? 'Hello World' : 'HelloArkUI';
})
Button(`Click to increase count=${this.count}`).onClick(() => {
// The update of the @State decorated variable triggers the update of the <Text> component.
this.count += this.increaseBy;
})
}
}
}
```
From this example, we learn the initialization process of an \@State decorated variable on initial render.
1. Apply the locally defined default value.
```ts
@State title: Model = new Model('Hello World');
@State count: number = 0;
```
2. Apply the named parameter value, if one is provided.
```ts
MyComponent({ count: 1, increaseBy: 2 })
```
# stateStyles: Polymorphic Style
Unlike \@Styles and \@Extend, which are used to reuse styles only on static pages, stateStyles enables you to set state-specific styles.
## Overview
stateStyles is an attribute method that sets the style based on the internal state of a component. It is similar to a CSS pseudo-class, with different syntax. ArkUI provides the following states:
- focused
- normal
- pressed
- disabled
## Application Scenarios
### Common Scenarios
This example shows the most basic application scenario of stateStyles. The **\<Button>** component is the first component and is in the default focused state, where the pink style specified for **focused** takes effect. When the component is pressed, the black style specified for **pressed** takes effect. If you place another component before the **\<Button>** component and have it in the normal state, the yellow style specified for **normal** takes effect.
```ts
@Entry
@Component
struct StateStylesSample {
build() {
Column() {
Button('Click me')
.stateStyles({
focused: {
.backgroundColor(Color.Pink)
},
pressed: {
.backgroundColor(Color.Black)
},
normal: {
.backgroundColor(Color.Yellow)
}
})
}.margin('30%')
}
}
```
**Figure 1** Focused and pressed states
![Video_2023-03-17_120758](figures/Video_2023-03-17_120758.gif)
### Combined Use of \@Styles and stateStyles
The following example uses \@Styles to specify different states of stateStyles.
```ts
@Entry
@Component
struct MyComponent {
@Styles normalStyle() {
.backgroundColor(Color.Gray)
}
@Styles pressedStyle() {
.backgroundColor(Color.Red)
}
build() {
Column() {
Text('Text1')
.fontSize(50)
.fontColor(Color.White)
.stateStyles({
normal: this.normalStyle,
pressed: this.pressedStyle,
})
}
}
}
```
**Figure 2** Normal and pressed states
![Video_2023-03-17_144824](figures/Video_2023-03-17_144824.gif)
### Using Regular Variables and State Variables in stateStyles
stateStyles can use **this** to bind regular variables and state variables in a component.
```ts
@Entry
@Component
struct CompWithInlineStateStyles {
@State focusedColor: Color = Color.Red;
normalColor: Color = Color.Green
build() {
Button('clickMe').height(100).width(100)
.stateStyles({
normal: {
.backgroundColor(this.normalColor)
},
focused: {
.backgroundColor(this.focusedColor)
}
})
.onClick(() => {
this.focusedColor = Color.Pink
})
.margin('30%')
}
}
```
By default, the **\<Button>** component is in red when focused. After a click event is triggered, it turns to pink when focused.
**Figure 3** Change of the styles in focused state by a click
![Video_2023-03-17_144605](figures/Video_2023-03-17_144605.gif)
# \@Styles: Definition of Resusable Styles
If the style of each component needs to be set separately, this will result in a large amount of repeated code during development. Though copy and paste is available, it is inefficient and error-prone. To maximize code efficiency and maintainability, the \@Styles decorator is introduced.
\@Styles helps avoid repeated style setting, by extracting multiple style settings into one method. When declaring a component, you can invoke this method and use the \@Styles decorator to quickly define and reuse the custom styles of a component.
> **NOTE**
>
> Since API version 9, this decorator is supported in ArkTS widgets.
## Rules of Use
- \@Styles supports only [universal attributes](../reference/arkui-ts/ts-universal-attributes-size.md) and [universal events](../reference/arkui-ts/ts-universal-events-click.md).
- An \@Styles decorated method does not support parameters. The following example is invalid:
```ts
// Invalid: @Styles does not support parameters.
@Styles function globalFancy (value: number) {
.width(value)
}
```
- \@Styles can be defined inside or outside a component declaration. When it is defined outside a component declaration, the component name must be preceded by the keyword **function**.
```ts
// Global (outside a component declaration)
@Styles function functionName() { ... }
// Inside a component declaration
@Component
struct FancyUse {
@Styles fancy() {
.height(100)
}
}
```
- \@Styles defined inside a component declaration can access constants and state variables of the component through **this**, and mutate the values of state variables through events in \@Styles. The following is an example:
```ts
@Component
struct FancyUse {
@State heightVlaue: number = 100
@Styles fancy() {
.height(this.heightVlaue)
.backgroundColor(Color.Yellow)
.onClick(() => {
this.heightVlaue = 200
})
}
}
```
- The priority of \@Styles defined inside a component declaration is higher than that of \@Styles defined outside a component declaration.
The framework preferentially searches for \@Styles within the current component.
## Application Scenarios
The following example demonstrates the usage of \@Styles inside and outside a component declaration.
```ts
// Define a \@Styles decorated method outside a component declaration.
@Styles function globalFancy () {
.width(150)
.height(100)
.backgroundColor(Color.Pink)
}
@Entry
@Component
struct FancyUse {
@State heightVlaue: number = 100
// Define a \@Styles decorated method inside a component declaration.
@Styles fancy() {
.width(200)
.height(this.heightVlaue)
.backgroundColor(Color.Yellow)
.onClick(() => {
this.heightVlaue = 200
})
}
build() {
Column({ space: 10 }) {
// Use the \@Styles decorated method defined outside a component declaration.
Text('FancyA')
.globalFancy ()
.fontSize(30)
// Use the \@Styles decorated method defined outside a component declaration.
Text('FancyB')
.fancy()
.fontSize(30)
}
}
}
```
# $$ Syntax: Two-Way Synchronization of Built-in Components
The $$ operator provides a TS variable by-reference to a built-in component so that the variable value and the internal state of that component are kept in sync.
What the internal state is depends on the component. For example, for the [bindPopup](https://gitee.com/openharmony/docs/blob/master/en/application-dev/reference/arkui-ts/ts-universal-attributes-popup.md) attribute method, it is the **show** parameter.
## Rules of Use
- $$ supports variables of simple types and variables decorated by **\@State**, **\@Link**, or **\@Prop**.
- Currently, $$ supports only the **show** parameter of the [bindPopup](https://gitee.com/openharmony/docs/blob/master/en/application-dev/reference/arkui-ts/ts-universal-attributes-popup.md) attribute method, the **checked** attribute of the [\<Radio>](https://gitee.com/openharmony/docs/blob/master/en/application-dev/reference/arkui-ts/ts-basic-components-radio.md) component, and the **refreshing** parameter of the [\<Refresh>](https://gitee.com/openharmony/docs/blob/master/en/application-dev/reference/arkui-ts/ts-container-refresh.md) component.
- When the variable bound to $$ changes, the UI is re-rendered synchronously.
## Example
This example uses the **show** parameter of the [bindPopup](https://gitee.com/openharmony/docs/blob/master/en/application-dev/reference/arkui-ts/ts-universal-attributes-popup.md) attribute method.
```ts
// xxx.ets
@Entry
@Component
struct bindPopupPage {
@State customPopup: boolean = false;
build() {
Column() {
Button('Popup')
.margin(20)
.onClick(() => {
this.customPopup = !this.customPopup
})
.bindPopup($$this.customPopup, {
message: 'showPopup'
})
}
}
}
```
![popup](figures/popup.gif)
# \@Watch: Getting Notified of State Variable Changes
\@Watch is used to listen for state variables. If your application needs watch for value changes of a state variable, you can decorate the variable with \@Watch.
> **NOTE**
>
> Since API version 9, this decorator is supported in ArkTS widgets.
## Overview
An application can request to be notified whenever the value of the \@Watch decorated variable changes. The \@Watch callback is called when the value change has occurred. \@Watch uses strict equality (===) to determine whether a value is updated in the ArkUI framework. If **false** is returned, the \@Watch callback is triggered.
## Decorator Description
| \@Watch Decorator| Description |
| -------------- | ---------------------------------------- |
| Decorator parameters | Mandatory. Constant string, which is quoted. Reference to a (string) => void custom component member function.|
| Custom component variables that can be decorated | All decorated state variables. Regular variables cannot be watched. |
| Order of decorators | It is recommended that the \@State, \@Prop, \@Link, or other decorators precede the \@Watch decorator.|
## Syntax
| Type | Description |
| ---------------------------------------- | ---------------------------------------- |
| (changedPropertyName?&nbsp;:&nbsp;string)&nbsp;=&gt;&nbsp;void | This function is a member function of the custom component. **changedPropertyName** indicates the name of the watched attribute.<br>It is useful when you use the same function as a callback to several watched attributes.<br>It takes the attribute name as a string input parameter and returns nothing.|
## Observed Changes and Behavior
1. When a state variable change (including the change of the named attribute in AppStorage or LocalStorage) is observed, the corresponding \@Watch callback is triggered.
2. \@The Watch callback is executed synchronously after the variable change in the custom component.
3. If the \@Watch callback mutates other watched variables, their variable @Watch callbacks in the same and other custom components as well as state updates are triggered.
4. A \@Watch function is not called upon custom component variable initialization, because initialization is not considered as variable mutation. A \@Watch function is called upon updating of the custom component variable.
## Restrictions
- Pay attention to the risk of infinite loops. Loops can be caused by the \@Watch callback directly or indirectly mutating the same variable. To avoid loops, avoid mutating the \@Watch decorated state variable inside the callback handler.
- Pay attention to performance. The attribute value update function delays component re-render (see the preceding behavior description). The callback should only perform quick computations.
- Calling **async await** from an \@Watch function is not recommended, because asynchronous behavior may cause performance issues of re-rendering.
## Application Scenarios
### Combination of \@Watch and \@Link
This example illustrates how to watch an \@Link decorated variable in a child component.
```ts
class PurchaseItem {
static NextId: number = 0;
public id: number;
public price: number;
constructor(price: number) {
this.id = PurchaseItem.NextId++;
this.price = price;
}
}
@Component
struct BasketViewer {
@Link @Watch('onBasketUpdated') shopBasket: PurchaseItem[];
@State totalPurchase: number = 0;
updateTotal(): number {
let total = this.shopBasket.reduce((sum, i) => sum + i.price, 0);
// A discount is provided when the amount exceeds 100 euros.
if (total >= 100) {
total = 0.9 * total;
}
return total;
}
// @Watch callback
onBasketUpdated(propName: string): void {
this.totalPurchase = this.updateTotal();
}
build() {
Column() {
ForEach(this.shopBasket,
(item) => {
Text(`Price: ${item.price.toFixed(2)} €`)
},
item => item.id.toString()
)
Text(`Total: ${this.totalPurchase.toFixed(2)} €`)
}
}
}
@Entry
@Component
struct BasketModifier {
@State shopBasket: PurchaseItem[] = [];
build() {
Column() {
Button('Add to basket')
.onClick(() => {
this.shopBasket.push(new PurchaseItem(Math.round(100 * Math.random())))
})
BasketViewer({ shopBasket: $shopBasket })
}
}
}
```
The processing procedure is as follows:
1. **Button.onClick** of the **BasketModifier** component adds an item to **BasketModifier shopBasket**.
2. The value of the \@Link decorated variable **BasketViewer shopBasket** changes.
3. The state management framework calls the \@Watch callback **BasketViewer onBasketUpdated** to update the value of **BaketViewer TotalPurchase**.
4. Because \@Link decorated shopBasket changes (a new item is added), the ForEach component executes the item Builder to render and build the new item. Because the @State decorated totalPurchase variables changes, the **Text** component is also re-rendered. Re-rendering happens asynchronously.
### \@Watch and Custom Component Update
This example is used to clarify the processing steps of custom component updates and \@Watch. Note that **count** is @State decorated in both components.
```ts
@Component
struct TotalView {
@Prop @Watch('onCountUpdated') count: number;
@State total: number = 0;
// @Watch cb
onCountUpdated(propName: string): void {
this.total += this.count;
}
build() {
Text(`Total: ${this.total}`)
}
}
@Entry
@Component
struct CountModifier {
@State count: number = 0;
build() {
Column() {
Button('add to basket')
.onClick(() => {
this.count++
})
TotalView({ count: this.count })
}
}
}
```
Processing steps:
1. The click event **Button.onClick** of the **CountModifier** custom component increases the value of **count**.
2. In response to the change of the @State decorated variable **count**, \@Prop in the child component **TotalView** is updated, and its **\@Watch('onCountUpdated')** callback is triggered, which updates the **total** variable in **TotalView**.
3. The **Text** component in the child component **TotalView** is re-rendered.
...@@ -51,12 +51,12 @@ resources ...@@ -51,12 +51,12 @@ resources
| Category | base Subdirectory | Qualifiers Subdirectory | rawfile Subdirectory | | Category | base Subdirectory | Qualifiers Subdirectory | rawfile Subdirectory |
| ---- | ---------------------------------------- | ---------------------------------------- | ---------------------------------------- | | ---- | ---------------------------------------- | ---------------------------------------- | ---------------------------------------- |
| Structure| The **base** subdirectory is a default directory. If no qualifiers subdirectories in the **resources** directory of the application match the device status, the resource file in the **base** subdirectory will be automatically referenced.<br>Resource group subdirectories are located at the second-level subdirectories in the **base** directory to store basic elements such as strings, colors, and boolean values, as well as resource files such as media, animations, and layouts. For details, see [Resource Group Directory](#resource-group-directory).| **en_US** and **zh_CN** are two default qualifiers subdirectories. You need to create other qualifiers subdirectories on your own. Each directory name consists of one or more qualifiers that represent the application scenarios or device characteristics. For details, see [Qualifiers Directory](#qualifiers-directory).<br>Resource group subdirectories are located at the second level of qualifiers subdirectories to store basic elements such as strings, colors, and boolean values, as well as resource files such as media, animations, and layouts. For details, see [Resource Group Directory](#resource-group-directory).| You can create multiple levels of subdirectories with custom directory names. They can be used to store various resource files.<br>However, resource files in the **rawfile** subdirectory will not be matched based on the device status.| | Structure| The **base** subdirectory is a default directory. If no qualifiers subdirectories in the **resources** directory of the application match the device status, the resource file in the **base** subdirectory will be automatically referenced.<br>Resource group subdirectories are located at the second-level subdirectories in the **base** directory to store basic elements such as strings, colors, and boolean values, as well as resource files such as media, animations, and layouts. For details, see [Resource Group Subdirectory](#resource-group-subdirectory).| **en_US** and **zh_CN** are two default qualifiers subdirectories. You need to create other qualifiers subdirectories on your own. Each directory name consists of one or more qualifiers that represent the application scenarios or device characteristics. For details, see [Qualifiers Subdirectory](#qualifiers-subdirectory).<br>Resource group subdirectories are located at the second level of qualifiers subdirectories to store basic elements such as strings, colors, and boolean values, as well as resource files such as media, animations, and layouts. For details, see [Resource Group Subdirectory](#resource-group-subdirectory).| You can create multiple levels of subdirectories with custom directory names. They can be used to store various resource files.<br>However, resource files in the **rawfile** subdirectory will not be matched based on the device status.|
| Compilation| Resource files in the subdirectories are compiled into binary files, and each resource file is assigned an ID. | Resource files in the subdirectories are compiled into binary files, and each resource file is assigned an ID. | Resource files in the subdirectory are directly packed into the application without being compiled, and no IDs will be assigned to the resource files. | | Compilation| Resource files in the subdirectories are compiled into binary files, and each resource file is assigned an ID. | Resource files in the subdirectories are compiled into binary files, and each resource file is assigned an ID. | Resource files in the subdirectory are directly packed into the application without being compiled, and no IDs will be assigned to the resource files. |
| Reference| Resource files in the subdirectory are referenced based on the resource type and resource name. | Resource files in the subdirectory are referenced based on the resource type and resource name. | Resource files in the subdirectory are referenced based on the file path and file name. | | Reference| Resource files in the subdirectory are referenced based on the resource type and resource name. | Resource files in the subdirectory are referenced based on the resource type and resource name. | Resource files in the subdirectory are referenced based on the file path and file name. |
### Qualifiers Subdirectories ### Qualifiers Subdirectory
The name of a qualifiers subdirectory consists of one or more qualifiers that represent the application scenarios or device characteristics, covering the mobile country code (MCC), mobile network code (MNC), language, script, country or region, screen orientation, device type, night mode, and screen density. The qualifiers are separated using underscores (\_) or hyphens (\-). When creating a qualifiers subdirectory, you need to understand the directory naming conventions and the rules for matching qualifiers subdirectories and the device status. The name of a qualifiers subdirectory consists of one or more qualifiers that represent the application scenarios or device characteristics, covering the mobile country code (MCC), mobile network code (MNC), language, script, country or region, screen orientation, device type, night mode, and screen density. The qualifiers are separated using underscores (\_) or hyphens (\-). When creating a qualifiers subdirectory, you need to understand the directory naming conventions and the rules for matching qualifiers subdirectories and the device status.
...@@ -246,7 +246,7 @@ When referencing resources in the **rawfile** subdirectory, use the `"$rawfile(' ...@@ -246,7 +246,7 @@ When referencing resources in the **rawfile** subdirectory, use the `"$rawfile('
> >
> The return value of `$r` is a Resource object. You can obtain the corresponding string by using the [getStringValue](../reference/apis/js-apis-resource-manager.md#getstringvalue9) method. > The return value of `$r` is a Resource object. You can obtain the corresponding string by using the [getStringValue](../reference/apis/js-apis-resource-manager.md#getstringvalue9) method.
In the ***example*.ets** file, you can use the resources defined in the **resources** directory. The following is a resource usage example based on the resource file examples in [Resource Group Subdirectories](#resource-group-subdirectories): In the ***example*.ets** file, you can use the resources defined in the **resources** directory. The following is a resource usage example based on the resource file examples in [Resource Group Subdirectory](#resource-group-subdirectory):
```ts ```ts
Text($r('app.string.string_hello')) Text($r('app.string.string_hello'))
......
# Custom Component Lifecycle
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.
## 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.
## onPageShow
onPageShow?(): void
Invoked 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. It works only for the custom components decorated by **@Entry**.
## onPageHide
onPageHide?(): void
Invoked 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. It works only for the custom components decorated by **@Entry**.
## onBackPress
onBackPress?(): void
Invoked when a user clicks the back button. It works only for the custom components decorated by **@Entry**. The value **true** is returned if the page processes the return logic instead of performing page routing. The value false is returned if the default return logic is used. If the return value is not set, the value **false** is used.
```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%')
}
}
```
![en-us_image_0000001563060749](figures/en-us_image_0000001563060749.png)
## onLayout<sup>9+</sup>
onLayout?(children: Array&lt;LayoutChild&gt;, 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** callback.
Since API version 9, this API is supported in ArkTS widgets.
**Parameters**
| Name | Type | Description |
| ---------- | ---------------------------------------- | ---------------- |
| children | Array&lt;[LayoutChild](#layoutchild9)&gt; | Child component layout information. |
| constraint | [ConstraintSizeOptions](ts-types.md#constraintsizeoptions) | Size constraint information of the parent component.|
## onMeasure<sup>9+</sup>
onMeasure?(children: Array&lt;LayoutChild&gt;, 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 **onMeasure** callback.
Since API version 9, this API is supported in ArkTS widgets.
**Parameters**
| Name | Type | Description |
| ---------- | ---------------------------------------- | ---------------- |
| children | Array&lt;[LayoutChild](#layoutchild9)&gt; | Child component layout information. |
| constraint | [ConstraintSizeOptions](ts-types.md#constraintsizeoptions) | Size constraint information of the parent component.|
## LayoutChild<sup>9+</sup>
Provides the child component layout information.
Since API version 9, this API is supported in ArkTS widgets.
| Parameter | Type | Description |
| ---------- | ---------------------------------------- | ------------------- |
| name | string | Name of the child component. |
| id | string | ID of the child component. |
| constraint | [ConstraintSizeOptions](ts-types.md#constraintsizeoptions) | Constraint size of the child component. |
| borderInfo | [LayoutBorderInfo](#layoutborderinfo9) | Provides the border information of the child component. |
| position | [Position](ts-types.md#position) | Position coordinates of the child component. |
| measure | (childConstraint:) =&gt; void | Method called to apply the size constraint to the child component.|
| layout | (LayoutInfo: [LayoutInfo](#layoutinfo9)) =&gt; void| Method called to apply the layout information to the child component.|
## LayoutBorderInfo<sup>9+</sup>
Provides the border information of the child component.
Since API version 9, this API is supported in ArkTS widgets.
| Parameter | Type | Description |
| ----------- | ------------------------------------ | ----------------------- |
| borderWidth | [EdgeWidths](ts-types.md#edgewidths) | Edge widths in different directions of the component.|
| margin | [Margin](ts-types.md#margin) | Margins in different directions of the component. |
| padding | [Padding](ts-types.md#padding) | Paddings in different directions of the component. |
## LayoutInfo<sup>9+</sup>
Provides the layout information of the child component.
Since API version 9, this API is supported in ArkTS widgets.
| Parameter | Type | Description |
| ---------- | ---------------------------------------- | -------- |
| position | [Position](ts-types.md#position) | Position coordinates of the child component.|
| constraint | [ConstraintSizeOptions](ts-types.md#constraintsizeoptions) | Constraint size of the child component.|
```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()
}
}
```
![en-us_image_0000001511900496](figures/en-us_image_0000001511900496.png)
# UI Development # UI Development
- [ArkUI Overview](arkui-overview.md) - [ArkUI Overview](arkui-overview.md)
- UI Development with 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)
- UI Development with 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 generate visual persistence, 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 parameters such as animation duration and change rule (that is, curve). After the attribute value changes, the original state is transited to the new state according to the animation parameter, that is, an animation is formed.
The animation capability provided by the 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 when two pages jump.
Figure 1 Animation by page
![en-us_image_0000001562700385](figures/en-us_image_0000001562700385.png)
Based on the basic 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)
Based on the page classification mode and application scenarios, this document provides the usage methods and precautions of various animations for developers to quickly learn animations.
# 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)
# Toggle
The **\<Toggle>** component provides a clickable element in the check box, button, or switch type, typically used to switch between two states. For details, see [Toggle](../reference/arkui-ts/ts-basic-components-toggle.md).
## Creating a Toggle
You can create a toggle by calling the following API:
```ts
Toggle(options: { type: ToggleType, isOn?: boolean })
```
Creates a toggle. In this API, **ToggleType** indicates the toggle type, which can be **Button**, **Checkbox**, or **Switch**, and **isOn** specifies whether the toggle is turned on. The API can be called in either of the following ways:
- Create a toggle that does not contain child components.
This can be achieved by calling the API with **ToggleType** set to **Checkbox** or **Switch**.
```ts
Toggle({ type: ToggleType.Checkbox, isOn: false })
Toggle({ type: ToggleType.Checkbox, isOn: true })
```
![en-us_image_0000001562940485](figures/en-us_image_0000001562940485.png)
```ts
Toggle({ type: ToggleType.Switch, isOn: false })
Toggle({ type: ToggleType.Switch, isOn: true })
```
![en-us_image_0000001511421228](figures/en-us_image_0000001511421228.png)
- Create a toggle that contains child components.
This can be achieved by calling the API with **ToggleType** set to **Button**. If the child component has text set, the text content is displayed inside the button.
```ts
Toggle({ type: ToggleType.Button, isOn: false }) {
Text('status button')
.fontColor('#182431')
.fontSize(12)
}.width(100)
Toggle({ type: ToggleType.Button, isOn: true }) {
Text('status button')
.fontColor('#182431')
.fontSize(12)
}.width(100)
```
![en-us_image_0000001511900404](figures/en-us_image_0000001511900404.png)
## Setting Styles
- Use the **selectedColor** attribute to set the background color of the toggle for when it is turned on.
```ts
Toggle({ type: ToggleType.Button, isOn: true }) {
Text('status button')
.fontColor('#182431')
.fontSize(12)
}.width(100).selectedColor(Color.Pink)
Toggle({ type: ToggleType.Checkbox, isOn: true })
.selectedColor(Color.Pink)
Toggle({ type: ToggleType.Switch, isOn: true })
.selectedColor(Color.Pink)
```
![en-us_image_0000001563060657](figures/en-us_image_0000001563060657.png)
- Use the **switchPointColor** attribute to set the color of the circular slider. This attribute is valid only when **type** of the toggle is set to **ToggleType.Switch**.
```ts
Toggle({ type: ToggleType.Switch, isOn: false })
.switchPointColor(Color.Pink)
Toggle({ type: ToggleType.Switch, isOn: true })
.switchPointColor(Color.Pink)
```
![en-us_image_0000001511421232](figures/en-us_image_0000001511421232.png)
## Adding Events
The **\<Toggle>** 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 turned on or off.
```ts
Toggle({ type: ToggleType.Switch, isOn: false })
.onChange((isOn: boolean) => {
if(isOn) {
// Operation
}
})
```
## Example Scenario
In this example, the **\<Toggle>** component is used to enable or disable Bluetooth.
```ts
// xxx.ets
import prompt from '@ohos.promptAction';
@Entry
@Component
struct ToggleExample {
build() {
Column() {
Row() {
Text("Bluetooth Mode")
.height(50)
.fontSize(16)
}
Row() {
Text("Bluetooth")
.height(50)
.padding({left: 10})
.fontSize(16)
.textAlign(TextAlign.Start)
.backgroundColor(0xFFFFFF)
Toggle({ type: ToggleType.Switch })
.margin({left: 200, right: 10})
.onChange((isOn: boolean) => {
if(isOn) {
promptAction.showToast({ message: 'Bluetooth is on.' })
} else {
promptAction.showToast({ message: 'Bluetooth is off.' })
}
})
}
.backgroundColor(0xFFFFFF)
}
.padding(10)
.backgroundColor(0xDCDCDC)
.width('100%')
.height('100%')
}
}
```
![en-us_image_0000001511740448](figures/en-us_image_0000001511740448.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 a Video](#loading-a-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 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).
# XComponent
As a drawing component, the \<[XComponent](../reference/arkui-ts/ts-basic-components-xcomponent.md)> is usually used to meet relatively complex drawing customization requirements, for example, display of a camera preview stream and drawing of a game image.
You can specify the **type** parameter to implement different features. Two options are mainly available for this parameter: **surface** and **component**.
With the **\<XComponent>** of the **surface** type, you can pass data to the surface independently owned by it to render the image.
With the **\<XComponent>** of the **component** type, you can dynamically load the displayed content.
## surface Type
When the **\<XComponent>** is set to the **surface** type, you can write EGL/OpenGL ES and media data and display it on the **\<XComponent>**.
You can also have the **\<XComponent>** laid out and rendered together with other components.
The **\<XComponent>** has an independent surface, which provides a native window for you to create the EGL/OpenGL ES environment on the native (C/C++) side and use the standard OpenGL ES for development.
In addition, media-related applications (such as videos and cameras) can write data to the surface provided by the **\<XComponent>** to present the corresponding image.
## Using EGL/OpenGL ES for Rendering
### Key Points of Native Code Development
OpenHarmony applications use native APIs to implement interactions between JS and C/C++ code. This is also the case with the **\<XComponent>**. For details, see [Using N-APIs in Application Projects](../napi/napi-guidelines.md).
The type of the file for processing the JS logic on the native side is .so.
- Each module has a .so file.
- The .so file is named in the format of lib{moduleName}.so.
In the scenario where the **\<XComponent>** is used for standard OpenGL ES development, the content of the **CMAKELists.txt** file is as follows:
```
cmake_minimum_required(VERSION 3.4.1)
project(XComponent) # Project name
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
# Path for searching for header files
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include
)
# Compile the target .so file. SHARED indicates the dynamic library.
add_library(nativerender SHARED
xxx.cpp
)
# Search for related libraries (including OpenGL ES libraries and NDK APIs provided by the <XComponent>).
find_library( EGL-lib
EGL )
find_library( GLES-lib
GLESv3 )
find_library( libace-lib
ace_ndk.z )
# Dependencies required for compiling .so files
target_link_libraries(nativerender PUBLIC ${EGL-lib} ${GLES-lib} ${libace-lib} libace_napi.z.so libc++.a)
```
### Registering the N-API Module
```c++
static napi_value Init(napi_env env, napi_value exports)
{
// Define the API exposed on the module.
napi_property_descriptor desc[] ={
DECLARE_NAPI_FUNCTION("changeColor", PluginRender::NapiChangeColor),
};
// You can mount the native method (PluginRender::NapiChangeColor) to exports through this API. exports is bound to a JS object at the JS layer through the JS engine.
NAPI_CALL(env, napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc));
return exports;
}
static napi_module nativerenderModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init, // Specify the callback for when the corresponding module is loaded.
.nm_modname = "nativerender", // Specify the module name. For <XComponent>-related development, the name must be the same as the value of libraryname in the <XComponent> on ArkTS.
.nm_priv = ((void*)0),
.reserved = { 0 },
};
extern "C" __attribute__((constructor)) void RegisterModule(void)
{
// Register the SO module.
napi_module_register(&nativerenderModule);c
}
```
### Parsing the NativeXComponent Instance
**NativeXComponent** provides an instance at the native layer for the **\<XComponent>**, which can be used as a bridge for binding with the **\<XComponent>** at the JS layer. The NDK APIs provided by the **\<XComponent>** depend on this instance. For details about the NKD APIs, see [Native XComponent](../reference/native-apis/_o_h___native_x_component.md).
The **NativeXComponent** instance can be obtained by parsing the callback (that is, the **Init** function in [NAPI module registration](#registering-the-n-api-module)) when the module is loaded.
```c++
{
// ...
napi_status status;
napi_value exportInstance = nullptr;
OH_NativeXComponent *nativeXComponent = nullptr;
// Parse the attribute of the wrapped NativeXComponent pointer.
status = napi_get_named_property(env, exports, OH_NATIVE_XCOMPONENT_OBJ, &exportInstance);
if (status != napi_ok) {
return false;
}
// Use the napi_unwrap API to parse the NativeXComponent instance pointer.
status = napi_unwrap(env, exportInstance, reinterpret_cast<void**>(&nativeXComponent));
// ...
}
```
### Registering XComponent Callback
Based on the NativeXComponent pointer obtained by [parsing the NativeXComponent instance](#parsing-the-nativexcomponent-instance), perform callback registration through the **OH_NativeXComponent_RegisterCallback** API.
```c++
{
...
OH_NativeXComponent *nativeXComponent = nullptr;
// Parse the NativeXComponent instance.
OH_NativeXComponent_Callback callback;
callback->OnSurfaceCreated = OnSurfaceCreatedCB; // Invoked when a surface is successfully created. You can obtain the handle to the native window from this event.
callback->OnSurfaceChanged = OnSurfaceChangedCB; // Invoked when the surface changes. You can obtain the native window handle and XComponent change information from this event.
callback->OnSurfaceDestroyed = OnSurfaceDestroyedCB; // Invoked when the surface is destroyed. You can release resources in this event.
callback->DispatchTouchEvent = DispatchTouchEventCB; // Invoked when a touch event occurs. You can obtain the touch event information from this event.
OH_NativeXComponent_RegisterCallback(nativeXComponent, callback);
...
}
```
### Creating the EGL/OpenGL ES Environment
In the registered **OnSurfaceCreated** callback, you can obtain the handle to the native window (which is essentially the surface independently owned by the **\<XComponent>**). Therefore, you can create the EGL/OpenGL ES environment for your application to start the development of the rendering logic.
```c++
EGLCore* eglCore_; // EGLCore is a class that encapsulates OpenGL-related APIs.
uint64_t width_;
uint64_t height_;
void OnSurfaceCreatedCB(OH_NativeXComponent* component, void* window)
{
int32_t ret = OH_NativeXComponent_GetXComponentSize(component, window, &width_, &height_);
if (ret === OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {
eglCore_->GLContextInit(window, width_, height_); // Initialize the OpenGL environment.
}
}
```
### ArkTS Syntax
You can use the **\<XComponent>** to develop EGL/OpenGL ES rendering by using the following code on the ArkTS side:
```ts
XComponent({ id: 'xcomponentId1', type: 'surface', libraryname: 'nativerender' })
.onLoad((context) => {})
.onDestroy(() => {})
```
- **id**: corresponds to an **\<XComponent>** and must be unique. Generally, you can use the **OH_NativeXComponent_GetXComponentId** API on the native side to obtain the corresponding ID and bind the corresponding **\<XComponent>**.
- **libraryname**: name of the loaded module, which must be the same as the value of **nm_modname** used when the Napi module is registered on the native side.
>**NOTE**
>
> An application loads modules to implement cross-language invoking in either of the following ways:
>
> 1. Use the **import** mode of the NAPI.
>
> ```ts
> import nativerender from "libnativerender.so"
> ```
>
> 2. Use the **\<XComponent>**, which, in essence, is to use the NAPI mechanism.
> The difference between this loading mode and the **import** loading mode is that when the dynamic library is loaded, the **NativeXComponent** instance of the **\<XComponent>** is exposed to the native layer of the application so that you can use the NDK APIs of the **\<XComponent>**.
- onLoad event
- Trigger time: when the surface of the **\<XComponent>** is prepared.
- **context** parameter: where the native API exposed on the module is mounted. Its usage is similar to the usage of the **context2** instance obtained after the module is directly loaded using **import context2 from "libnativerender.so"**.
- Time sequence: When the **onLoad** event is subject to the surface. The following figure shows the time sequence of the **onLoad** event and the **OnSurfaceCreated** event on the native side.
![onLoad] (figures /onLoad.png)
- onDestroy event
Trigger time: when the **\<XComponent>** is destroyed, in the same manner as that when an ArkUI component is destroyed. The following figure shows the time sequence of the **onDestroy** event and the **OnSurfaceDestroyed** event on the native side.
![onDestroy](figures /onDestroy.png)
### Writing Media Data
The surface held by the **\<XComponent>** complies with the producer-consumer model.
In OpenHarmony, components that comply with the producer design, such as the camera and video player, can write data to the surface held by the **\<XComponent>** and display the data through the **\<XComponent>**.
![Picture 1](figures /Picture 1.png)
You can bind the **\<XComponent>** to the **XComponentController** to obtain the surface ID (**surfaceId**, which uniquely identifies a surface) and send it to the corresponding component API.
```ts
@State surfaceId:string = "";
mXComponentController: XComponentController = new XComponentController();
XComponent({ id: '', type: 'surface', controller: this.mXComponentController })
.onLoad(() => {
this.surfaceId = this.mXComponentController.getXComponentSurfaceId()
})
```
For details about component APIs, see [AVPlayer](../reference/apis/js-apis-media.md#avplayer9) and [Camera](../reference/apis/js-apis-camera.md).
### component Type
When the **\<XComponent>** is set to the **component** type, you can execute non-UI logic to dynamically load the displayed content.
>**NOTE**
>
> When **type** is set to **component**, the **\<XComponent>** functions as a container, where child components are laid out vertically.
>
> - Vertical alignment: [FlexAlign](../reference/arkui-ts/ts-appendix-enums.md#flexalign).Start
>
> - Horizontal alignment: [FlexAlign](../reference/arkui-ts/ts-appendix-enums.md#flexalign).Center
>
> The component does not respond to any events.
>
> Layout changes and event responses can be set by mounting child components.
>
> The non-UI logic written internally needs to be encapsulated in one or more functions.
### Example Scenario
```ts
@Builder
function addText(label: string): void {
Text(label)
.fontSize(40)
}
@Entry
@Component
struct Index {
@State message: string = 'Hello XComponent'
@State messageCommon: string = 'Hello World'
build() {
Row() {
Column() {
XComponent({ id: 'xcomponentId-container', type: 'component' }) {
addText(this.message)
Divider()
.margin(4)
.strokeWidth(2)
.color('#F1F3F5')
.width("80%")
Column() {
Text(this.messageCommon)
.fontSize(30)
}
}
}
.width('100%')
}
.height('100%')
}
}
```
![en-us_image_0000001511900428](figures/en-us_image_0000001511900428.png)
# 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](#customizing-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.
Take the example provided by [defaultFocus] (#setting-default-focus) as an example. 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
A touchscreen event refer to a callback event triggered when a finger or stylus is pressed, slides, or is lifted from a component. Touchscreen events include the [click event](#click-event), [drag event](#drag-event), and [touch event](#touch-event).
**Figure 1** Touchscreen event principles
![en-us_image_0000001562700461](figures/en-us_image_0000001562700461.png)
## Click Event
A click event refers to 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 event relative to the window or component and the event source where the click occurs.
For example, the click event of a button is used to control the display and hiding of 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 control the display and hiding of 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 presses and holds a component (&gt;=500 ms) using a finger or stylus and drags the component to the drop target. The process of triggering a drag event is as follows:
![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:&nbsp;(event?:&nbsp;DragEvent,&nbsp;extraParams?:&nbsp;string)&nbsp;=&gt;&nbsp;CustomBuilder&nbsp;\|&nbsp;DragItemInfo) | Triggered when dragging starts. Currently, only custom **pixelmap** objects and custom components are supported. |
| onDragEnter(event:&nbsp;(event?:&nbsp;DragEvent,&nbsp;extraParams?:&nbsp;string)&nbsp;=&gt;&nbsp;void) | Triggered when the dragged item enters a valid drop target. **DragEvent** indicates the position where the drag occurs. **extraParmas** indicates the additional information about the drag event.|
| onDragLeave(event:&nbsp;(event?:&nbsp;DragEvent,&nbsp;extraParams?:&nbsp;string)&nbsp;=&gt;&nbsp;void) | Triggered when the dragged item leaves a valid drop target. **DragEvent** indicates the position where the drag occurs. **extraParmas** indicates the additional information about the drag event.|
| onDragMove(event:&nbsp;(event?:&nbsp;DragEvent,&nbsp;extraParams?:&nbsp;string)&nbsp;=&gt;&nbsp;void) | Triggered when the dragged item moves in a valid drop target. **DragEvent** indicates the position where the drag occurs. **extraParmas** indicates the additional information about the drag event.|
| onDrop(event:&nbsp;(event?:&nbsp;DragEvent,&nbsp;extraParams?:&nbsp;string)&nbsp;=&gt;&nbsp;void) | Triggered when the dragged item is dropped on a valid drop target. **DragEvent** indicates the position where the drag occurs. **extraParmas** indicates the additional 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 text: string = ''
@State bool1: boolean = false
@State bool2: boolean = false
@State visible: Visibility = Visibility.Visible
@State pixelMap: PixelMap = undefined
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.pixelMap = 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')
this.bool1 = true
this.text = 'TextDrag'
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
When a finger or stylus touches a component, a touch event corresponding to the action is triggered, such as a press (Down), slide (Move), or lift (Up) event.
```ts
onTouch(event: (event?: TouchEvent) => void)
```
- If **event.type** is **TouchType.Down**, the finger or stylus is pressed.
- If** event.type** is **TouchType.Up**, the finger or stylus is lifted.
- If **event.type** is **TouchType.Move**, the finger or stylus is pressed and moved.
The touch event may be triggered by multiple fingers at the same time. Information such as the location of the finger that triggers the event, unique identifier of the finger, finger information changed, and an input device source may be obtained by using the **event** parameter.
```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)
# Drawing Custom Graphics on the Canvas
**Canvas** provides a canvas component for drawing custom graphics. You can use the **CanvasRenderingContext2D** and **OffscreenCanvasRenderingContext2D** objects to draw graphics on the **Canvas** component. The drawing objects can be basic shapes, text, and images.
## Drawing a Custom Chart Using the Canvas
You can draw a custom chart on the canvas in any of the following ways:
- Use [CanvasRenderingContext2D](../reference/arkui-ts/ts-canvasrenderingcontext2d.md).
```ts
@Entry
@Component
struct CanvasExample1 {
// Configure the parameters of the CanvasRenderingContext2D object, including whether to enable anti-aliasing. The value true indicates that anti-aliasing is enabled.
private settings: RenderingContextSettings = new RenderingContextSettings(true)
// Create a CanvasRenderingContext2D object by calling CanvasRenderingContext2D object in the canvas.
private context: CanvasRenderingContext2D= new CanvasRenderingContext2D(this.settings)
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
// Invoke the CanvasRenderingContext2D object in the canvas.
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
// You can draw content here.
this.context.strokeRect(50, 50, 200, 150);
})
}
.width('100%')
.height('100%')
}
}
```
![2023022793003(1)](figures/2023022793003(1).jpg)
- Drawing offscreen onto a canvas is a process where content to draw onto the canvas is first drawn in the buffer, and then converted into a picture, and finally the picture is drawn on the canvas. This process increases the drawing efficiency. The process is as follows:
1. Use the **transferToImageBitmap** API to create an **ImageBitmap** object for the image that is recently rendered off the screen canvas.
2. Use the **transferFromImageBitmap** API of the **CanvasRenderingContext2D** object to display the given **ImageBitmap** object.
For details, see [OffscreenCanvasRenderingContext2D](../reference/arkui-ts/ts-offscreencanvasrenderingcontext2d.md).
```ts
@Entry
@Component
struct CanvasExample2 {
// Configure the parameters of the CanvasRenderingContext2D and OffscreenCanvasRenderingContext2D objects, including whether to enable anti-aliasing. The value true indicates that anti-aliasing is enabled.
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
// Create the OffscreenCanvasRenderingContext2D object. width indicates the width of the offscreen canvas, and height indicates the height of the offscreen canvas.
private offContext: OffscreenCanvasRenderingContext2D = new OffscreenCanvasRenderingContext2D(600, 600, this.settings)
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
// You can draw content here.
this.offContext.strokeRect(50, 50, 200, 150);
// Display the image rendered by the offscreen drawing value on the common canvas.
let image = this.offContext.transferToImageBitmap();
this.context.transferFromImageBitmap(image);
})
}
.width('100%')
.height('100%')
}
}
```
![2023022793003(1)](figures/2023022793003(1).jpg)
>**NOTE**
>
>The APIs called for drawing on the canvas through the **CanvasRenderingContext2D** and **OffscreenCanvasRenderingContext2D** objects are the same. Unless otherwise specified, the unit of the API parameters is vp.
- Before loading the Lottie animation on the canvas, download the Lottie as follows:
```ts
import lottie from '@ohos/lottie'
```
For details about the APIs, see [Lottie](../reference/arkui-ts/ts-components-canvas-lottie.md).
>**NOTE**
>
>Before using Lottie for the first time, run the **ohpm install \@ohos/lottieETS** command in the Terminal window to download Lottie.
## Initializing the Canvas Component
**onReady(event: () =&gt; void)** is the event callback when the **Canvas** component initialization is complete. After this event is called, the determined width and height of the **Canvas** component can be obtained. The **CanvasRenderingContext2D** and **OffscreenCanvasRenderingContext2D** objects can then be used to call related APIs to draw graphics.
```ts
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() => {
this.context.fillStyle = '#0097D4';
this.context.fillRect(50, 50, 100, 100);
})
```
![2023022793350(1)](figures/2023022793350(1).jpg)
## Canvas Component Drawing Modes
After the **Canvas** component lifecycle callback **onReady()** is invoked, you can use the **Canvas** component for drawing. Alternatively, you can separately define the **Path2d** object to build an ideal path without the **Canvas** component and **onReady** lifecycle callback, and then use the **Canvas** component for drawing after **onReady** is called.
- The **CanvasRenderingContext2D** and **OffscreenCanvasRenderingContext2D** objects are used to directly call related APIs for drawing.
```ts
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
this.context.beginPath();
this.context.moveTo(50, 50);
this.context.lineTo(280, 160);
this.context.stroke();
})
```
![2023022793719(1)](figures/2023022793719(1).jpg)
- Define the **path2d** object to build an ideal path, and then call the **stroke** or **fill** API of the **CanvasRenderingContext2D** and **OffscreenCanvasRenderingContext2D** objects to draw the path. For details, see [Path2D](../reference/arkui-ts/ts-components-canvas-path2d.md).
```ts
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
let region = new Path2D();
region.arc(100, 75, 50, 0, 6.28);
this.context.stroke(region);
})
```
![2023022794031(1)](figures/2023022794031(1).jpg)
## Common Usage of the Canvas Component
**OffscreenCanvasRenderingContext2D** and **CanvasRenderingContext2D** provide a large number of attributes and methods, which can be used to draw text and graphics and process pixels. They are the core of the **Canvas** component. Common APIs include [fill](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#fill), [clip](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#clip), and [stroke](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#stroke). In addition, [fillStyle](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#fillstyle), [globalAlpha](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#globalalpha), and [strokeStyle](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#strokestyle) and more attributes are provided. This topic describes typical usage of the canvas.
- Draw a basic shape.
You can draw a basic shape by calling APIs such as [arc](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#arc), [ellipse](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#ellipse), and [rect](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#rect).
```ts
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
// Draw a rectangle.
this.context.beginPath();
this.context.rect(100, 50, 100, 100);
this.context.stroke();
// Draw a circle on the canvas.
this.context.beginPath();
this.context.arc(150, 250, 50, 0, 6.28);
this.context.stroke();
// Draw an oval on the canvas.
this.context.beginPath();
this.context.ellipse(150, 450, 50, 100, Math.PI * 0.25, Math.PI * 0, Math.PI * 2);
this.context.stroke();
})
```
![2023022794521(1)](figures/2023022794521(1).jpg)
- Draw text.
You can use APIs such as [fillText](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#filltext) and [strokeText](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#stroketext) to draw text.
```ts
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
// Draw filled text on the canvas.
this.context.font = '50px sans-serif';
this.context.fillText("Hello World!", 50, 100);
// Draw a text stroke on the canvas.
this.context.font = '55px sans-serif';
this.context.strokeText("Hello World!", 50, 150);
})
```
![2023022795105(1)](figures/2023022795105(1).jpg)
- Draw images and processes image pixel information.
You can draw an image by calling APIs such as [drawImage](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#drawimage) and [putImageData](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#putimagedata). You can also process image pixel information by calling APIs such as [createImageData](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#createimagedata), [getPixelMap](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#getpixelmap), and [getImageData](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#getimagedata).
```ts
@Entry
@Component
struct GetImageData {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
private offContext: OffscreenCanvasRenderingContext2D = new OffscreenCanvasRenderingContext2D(600, 600, this.settings)
private img:ImageBitmap = new ImageBitmap("/common/images/1234.png")
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
// Use the drawImage API to draw an image in the area with the width and height of 130 starting from (0, 0).
this.offContext.drawImage(this.img,0,0,130,130);
// Use the getImageData API to obtain the drawing content within the range of 130 (width and height) starting from (50, 50) on the canvas.
let imagedata = this.offContext.getImageData(50,50,130,130);
// Use the putImageData API to draw the obtained image data in the area starting from (150, 150).
this.offContext.putImageData(imagedata,150,150);
// Draw the offscreen drawing content to the canvas.
let image = this.offContext.transferToImageBitmap();
this.context.transferFromImageBitmap(image);
})
}
.width('100%')
.height('100%')
}
}
```
![drawimage](figures/drawimage.PNG)
- Other usage
**Canvas** also provides other usage. Usage related to [canvas gradient](../reference/arkui-ts/ts-components-canvas-canvasgradient.md): [createLinearGradient](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#createlineargradient), [createRadialGradient](../reference/arkui-ts/ts-canvasrenderingcontext2d.md#createradialgradient), and more.
```ts
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
// Create a CanvasGradient object with radial gradient colors.
let grad = this.context.createRadialGradient(200,200,50, 200,200,200)
// Set the gradient color stop for the CanvasGradient object, including the offset and color.
grad.addColorStop(0.0, '#E87361');
grad.addColorStop(0.5, '#FFFFF0');
grad.addColorStop(1.0, '#BDDB69');
// Fill the rectangle with the CanvasGradient object.
this.context.fillStyle = grad;
this.context.fillRect(0, 0, 400, 400);
})
```
![2023022700701(1)](figures/2023022700701(1).jpg)
## Example Scenario
- Draw a basic shape.
```ts
@Entry
@Component
struct ClearRect {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
// Set the fill color to blue.
this.context.fillStyle = '#0097D4';
// Take (50, 50) as the upper left vertex and draw a rectangle with the width and height of 200.
this.context.fillRect(50,50,200,200);
// Use (70, 70) as the upper left vertex and clear the area whose width is 150 and height is 100.
this.context.clearRect(70,70,150,100);
})
}
.width('100%')
.height('100%')
}
}
```
![2023022701120(1)](figures/2023022701120(1).jpg)
- Draw an irregular shape.
```ts
@Entry
@Component
struct Path2d {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
build() {
Row() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
// Use the Path2D API to create a pentagon.
let path = new Path2D();
path.moveTo(150, 50);
path.lineTo(50, 150);
path.lineTo(100, 250);
path.lineTo(200, 250);
path.lineTo(250, 150);
path.closePath();
// Set the fill color to blue.
this.context.fillStyle = '#0097D4';
// Draw the pentagon described by Path2D ib the canvas in fill mode.
this.context.fill(path);
})
}
.width('100%')
}
.height('100%')
}
}
```
![2023032422159](figures/2023032422159.jpg)
# Interaction 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:
- Drawing components use **\<Shape>** as their parent to implement the effect similar to SVG. The API is called in the following format:
```ts
Shape(value?: PixelMap)
```
Create a drawing component with a parent component. The **value** parameter is used to set 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)
}
```
- The **\<Shape>** component is used independently 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}
```
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
Different gesture events are bound to each component and the event response mode is designed. When gesture recognition is successful, 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)
```
Binds a gesture to the specified component. **gesture** is a general gesture binding method.
For example, the tap gesture TapGesture can be bound to the **\<Text>** component by using the **gesture** method.
```ts
// xxx.ets
@Entry
@Component
struct Index {
build() {
Column() {
Text('Gesture').fontSize(28)
// Use the gesture method to bind the TapGesture.
.gesture(
TapGesture()
.onAction(() => {
console.info('TapGesture is onAction');
}))
}
.height(200)
.width(250)
}
}
```
## priorityGesture (Gesture Binding Method with Priority)
```ts
.priorityGesture(gesture: GestureType, mask?: GestureMask)
```
Binds gestures that are preferentially recognized to a component.
By default, when a parent component and a child component use gesture to bind gestures of the same type, the child component preferentially identifies gestures bound by using gesture. When the parent component uses priorityGesture to bind gestures of the same type as the child component, the parent component preferentially identifies gestures bound through priorityGesture.
For example, when the parent component Column and child component Text are bound to the TapGesture gesture at the same time, and the parent component is bound in the form of a priority gesture priorityGesture, the TapGesture bound to the parent component is preferentially responded.
```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 this parameter is set to priorityGesture, the TapGesture gesture event of the Text component is ignored when the text area is tapped, and the TapGesture gesture event of the parent component Column is preferentially responded.
.priorityGesture(
TapGesture()
.onAction(() => {
console.info('Column TapGesture is onAction');
}), GestureMask.IgnoreInternal)
}
}
```
## parallelGesture (Parallel Gesture Binding Method)
```ts
.parallelGesture(gesture: GestureType, mask?: GestureMask)
```
Binds the same gesture that can be responded to at the same time to the parent and child components.
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 the parent component is bound to parallel gesture parallelGesture, the same gesture events of the parent and child components can be triggered to implement 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)
// If this parameter is set to parallelGesture, the TapGesture gesture events of the parent component Column and child component Text are responded when the text area is clicked.
.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 event and the double-click gesture event, both the parent component and the child component respond only to the click gesture event.
# 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**: declare a type of the combined gesture. 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, **GestureMode** corresponding to the combined gesture is **Sequence**. Continuous recognition of combined gestures will recognize gestures according to the registration sequence of gestures until all gestures are recognized successfully. When one gesture in the continuously recognized combined gestures fails to be recognized, all gestures fail to be recognized.
A continuous gesture formed by combining a touch-and-hold gesture and a drag gesture is used as an example.
The translate attribute is bound to a Column component. You can modify the attribute to move the component. Then, bind the sequence gesture combined by LongPressGesture and PanGesture to the component. When LongPressGesture is triggered, the displayed number is updated. When a user drags a widget after touching and holding the widget, the widget is dragged based on the callback function of the drag 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 move the component.
.translate({ x: this.offsetX, y: this.offsetY, z: 0 })
.height(250)
.width(300)
// The following combined gestures are identified in sequence. When the touch and hold gesture event is not triggered normally, the drag gesture event is not triggered.
.gesture(
// Declare that the type of the combined gesture is the Sequence type.
GestureGroup(GestureMode.Sequence,
// The first gesture triggered by the combined gesture is a touch-and-hold gesture, and the touch-and-hold gesture can be responded to for multiple times.
LongPressGesture({ repeat: true })
// Increase the count displayed on the Text component when the touch and hold gesture is successfully recognized.
.onAction((event: GestureEvent) => {
if (event.repeat) {
this.count++;
}
console.info('LongPress onAction');
})
.onActionEnd(() => {
console.info('LongPress end');
}),
// The PanGesture gesture is triggered when you drag the slider after touching and holding the slider.
PanGesture()
.onActionStart(() => {
this.borderStyles = BorderStyle.Dashed;
console.info('pan start');
})
// When the gesture is triggered, the dragging distance is obtained according to the callback, and the displacement distance of the component is modified to move the component.
.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 continuous recognition combined gesture event, and is formed by combining a touch and hold gesture event and a slide gesture event. The drag event is triggered only when the user touches and holds the gesture for a preset period of time. If the touch and hold event is not reached or the slider is not performed after the touch and hold event, the drag event fails to be identified.
## Parallel Recognition
For parallel recognition, **GestureMode** corresponding to the combined gesture is **Parallel**. Parallel recognition of gestures registered in the combined gestures will be recognized at the same time until all gestures are recognized. The gestures in the gesture combination are recognized in parallel without affecting each other.
For example, a parallel recognition gesture formed by a tap gesture and a double-tap gesture is bound to a Column component. Because the tap gesture and the double-tap gesture are recognized in parallel, the two gestures may be recognized at the same time, and the two gestures do 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 parallel. After the tap gesture is recognized successfully, if you tap the gesture again within the specified time, the double-tap gesture will also be recognized successfully.
.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 form a parallel recognition combined gesture, when a tap is 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 successfully, but the double-tap gesture fails to be recognized.
>
>When there are two clicks, if the interval between the two clicks is within a specified period (300 ms by default), two click events and one double-click event are triggered.
>
>When there are two clicks, if the interval between the two clicks exceeds the specified time, the two clicks are triggered but the double-click event is not triggered.
## Exclusive Recognition
For exclusive recognition, **GestureMode** corresponding to the combined gesture is **Exclusive**. The gestures registered in the mutually exclusive recognition combination 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.
A mutually exclusive recognition combination gesture formed by binding a click gesture and a double-tap gesture to a Column component is used as an example. Because a click gesture can be triggered only once and a double-tap gesture needs to be triggered twice, each click event is consumed by the click gesture and cannot be accumulated into a double-tap gesture, the double-tap gesture cannot be triggered.
```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 the tapping gesture and the double-tap gesture form a mutually exclusive recognition combination gesture, when tapping is performed in the area, the tapping gesture and the double-tap gesture are recognized at the same time.
>
>When there is only a single tap, the tap gesture is recognized successfully, but the double-tap gesture fails to be recognized.
>
>When there are two taps, the tap gesture declares that the recognition is successful when the tap gesture is tapped for the first time. In this case, the double-tap gesture has failed. Even if the second tap is performed within the specified time, the double-tap gesture event is not responded. In this case, the second recognition success of the tap gesture event is triggered.
# Single Gesture
## TapGesture
```ts
TapGesture(value?:{count?:number; fingers?:number})
```
Tap gestures support single tap and multiple taps. There are two optional parameters:
- **count**: number of consecutive taps recognized by the tap gesture. This parameter is optional. The default value is 1. If this parameter is set to an invalid value less than 1, the default value is used. If multiple clicks are configured, the timeout interval for the previous lift and next press is 300 ms.
- **fingers**: number of fingers that trigger the touch. The minimum value is 1, and the maximum value is 10. The default value is 1. This parameter is optional. When multi-finger is configured, if the number of fingers used for tap does not reach the specified number within 300 ms after the first finger is tapped, the gesture fails to be recognized. Gesture recognition fails if the number of fingers used for tap exceeds the configured number.
For example, to bind a double-tap gesture (a tap gesture whose count value is 2) to the Text component, run the following command:
```ts
// xxx.ets
@Entry
@Component
struct Index {
@State value: string = "";
build() {
Column() {
Text('Click twice').fontSize(28)
.gesture(
//Bind the TapGesture whose count 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})
```
The touch-and-hold gesture is used to trigger a touch-and-hold gesture event. The minimum quantity of fingers that trigger the touch-and-hold gesture is 1, the minimum touch-and-hold event is 500 milliseconds, and has three optional parameters:
- **fingers**: minimum number of fingers required to trigger the touch and hold gesture. The minimum value is 1 and the maximum value is 10. The default value is 1. This parameter is optional.
- **repeat**: whether to continuously trigger event callback. The default value is false. This parameter is optional.
- **duration**: minimum duration (in milliseconds) required for triggering a long press. The default value is 500. This parameter is optional.
The following describes how to bind a touch and hold 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 LongPressGesture 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})
```
Drag gestures are used to trigger drag gesture events. When the sliding distance reaches the minimum sliding distance (5vp by default), drag gestures are successfully identified. There are three optional parameters:
- **fingers**: minimum number of fingers required to trigger a drag gesture. This parameter is optional. The minimum value is 1 and the maximum value is 10. The default value is 1.
- **direction**: direction of the gesture that triggers the drag. This parameter is optional. The enumerated values support the AND and OR operations. The default value is **Pandirection.All.**
- **distance**: specifies the minimum drag recognition distance for triggering drag. This parameter is optional. The unit is vp. The default value is 5.
Binding a drag gesture to a Text component is used as an example. You can drag a component by modifying the layout position information of the component in the callback function of the drag gesture.
```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 position information to the component.
.translate({ x: this.offsetX, y: this.offsetY, z: 0 })
.gesture(
//Bind drag gestures.
PanGesture()
.onActionStart((event: GestureEvent) => {
console.info('Pan start');
})
//When the drag gesture is triggered, modify the layout position information of the component based on the callback function.
.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 sliding components, such as List, Grid, Scroll, and Tab, slide through PanGesture. Bind [Drag gesture (PanGesture)] (#Drag gesture pangesture) to subcomponents in the components. [SwipeGesture](#SwipeGesture swipegesture) will cause gesture competition.
>
>When a subcomponent is bound to PanGesture, sliding in the subcomponent area triggers only PanGesture of the subcomponent. 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 dragging more sensitive. When a subcomponent is bound to SwipeGesture, 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. This parameter is optional. The minimum value is 1 and the maximum value is 10. The default value is 1.
- **direction**: Swipe direction. This parameter is optional. 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.This parameter is optional. 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 sliding gesture is triggered, obtain the sliding speed and angle to modify the layout parameters of the component.
.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.
# Displaying Images
More often than not, you may need to display images in applications, for example, logos in buttons, online images, and local images. To display images in applications, you need to use the **\<Image>** component, which supports multiple image formats, including PNG, JPG, BMP, SVG, and GIF. For details, see [Image](../reference/arkui-ts/ts-basic-components-image.md).
You can call the API in the following format to create an image:
```ts
Image(src: string | Resource | media.PixelMap)
```
Obtains images from the image data source. Local images and online images can be rendered and displayed. In the preceding information, src indicates the data source of the image. For details about how to load the data source, see [Loading Image Resources](#loading-image-resources).
## Loading Image Resources
The **\<Image>** component supports two types of images: archived images and multimedia pixel images.
### Archived Type Data Source
Data sources of the archived type can be classified into local resources, online resources, Resource objects, media library datashare resources, and Base64 resources.
- Local resources
Create a folder and place the local image in any position in the ets folder.
The **\<Image>** component imports the local image path to display the image. The root directory is the ets folder.
```ts
Image('images/view.jpg')
.width(200)
```
- Network resource
To use network images, you need to apply for the ohos.permission.INTERNET permission. For details, see [Applying for Permissions](../security/accesstoken-guidelines.md). In this case, the src parameter of the Image component is the link of the network image.
```ts
Image('https://www.example.com/example.JPG') // Replace the URL with the actual URL.
```
- Resource
The Resource format can be used to import images across bundles or modules. All images in the **resources** folder can be read and converted to the resource format through **$r**.
**Figure 1** Resouces
![image-resource](figures/image-resource.jpg)
Method:
```
Image($r('app.media.icon'))
```
You can also place the images in the **rawfile** folder.
**Figure 2** rawfile
![image-rawfile](figures/image-rawfile.jpg)
Method:
```
Image($rawfile('snap'))
```
- media library datashare
The value is a string that supports the **datashare://** path prefix, which is used to access the image path provided by the media library.
1. Call the API in to obtain the photo URL in the gallery.
```ts
import picker from '@ohos.file.picker';
@Entry
@Component
struct Index {
private imgDatas: string[] = [];
// Obtain the photo URL set.
async getAllImg() {
let photoPicker = new picker.PhotoViewPicker();
let result = new Array<string>();
try {
let PhotoSelectOptions = new picker.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 5;
let photoPicker = new picker.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult) => {
result = PhotoSelectResult.photoUris;
}).catch((err) => {
console.error(`PhotoViewPicker.select failed with. Code: ${err.code}, message: ${err.message}`);
});
} catch (err) {
console.error(`PhotoViewPicker failed with. Code: ${err.code}, message: ${err.message}`); }
return result;
}
// Call the preceding function in aboutToAppear to obtain the URLs of all images in the gallery and store the URLs in imgDatas.
async aboutToAppear() {
this.imgDatas = await this.getAllImg();
}
// Use the URL of imgDatas to load the image.
build() {
Grid() {
ForEach(this.imgDatas, item => {
GridItem() {
Image(item)
.width(200)
}
}, item => JSON.stringify(item))
}
}
}
```
2. The format of the URL obtained from the media library is as follows:
```ts
Image('datashare:///media/5')
.width(200)
```
- base64
The path format is data:image/[png|jpeg|bmp|webp];base64,[base64 data], in which [base64 data] indicates Base64 string data.
Base64 character strings can be used to store pixel data of images and are widely used on web pages.
### Multimedia Pixel Map
PixelMap is the pixel image after image decoding. For details, see [Image](../media/image.md). In the following example, the data returned by the loaded network image is decoded into the PixelMap format and then displayed on the Image component,
1. Creates a PixelMap state variable.
```ts
@State image: PixelMap = undefined;
```
2. Referencing multimedia.
Requests network images and decodes and encodes PixelMap.
1. Reference the network permission and media library permission.
```ts
import http from '@ohos.net.http';
import ResponseCode from '@ohos.net.http';
import image from '@ohos.multimedia.image';
```
2. Enter the network image address.
```ts
http.createHttp().request("https://www.example.com/xxx.png",
(error, data) => {
if (error){
console.error(`http reqeust failed with. Code: ${error.code}, message: ${error.message}`);
} else {
}
}
)
```
3. Encode and transcodes the data returned by the network address to the pixelMap image format.
```ts
let code = data.responseCode;
if(ResponseCode.ResponseCode.OK === code) {
let imageSource = image.createImageSource(data.result);
let options = {
alphaType: 0, // Alpha type
editable: false, // Whether the image is editable
pixelFormat: 3, // Pixel format
scaleMode: 1, // Scale mode
size: {height: 100, width: 100}
} // Image size.
imageSource.createPixelMap(options).then((pixelMap) => {
this.image = pixelMap
})
```
4. Show pictures
```ts
Button ("Get Online Image")
.onClick(() => {
this.httpRequest()
})
Image(this.image).height(100).width(100)
```
## Display Vector Diagram
The **\<Image>** component can display vector images in SVG format. The supported SVG labels are svg, rect, circle, ellipse, path, line, polyline, polygon, and animate.
You can use the fillColor attribute to change the drawing color of an SVG image.
```ts
Image($r('app.media.cloud')).width(50)
.fillColor(Color.Blue)
```
**Figure 3** Original image
![screenshot_20230223_141141](figures/screenshot_20230223_141141.png)
**Figure 4** SVG image after the drawing color is set
![screenshot_20230223_141404](figures/screenshot_20230223_141404.png)
## Adding Attributes
Setting attributes for the **\<Image>** component can make the image display more flexible and achieve some customized effects. The following are examples of common attributes. For details about attributes, see [Image](../reference/arkui-ts/ts-basic-components-image.md).
### Setting the Image Scale Type
The **objectFit** attribute is used to scale an image to a box whose height and width are determined.
```ts
@Entry
@Component
struct MyComponent {
scroller: Scroller = new Scroller()
build() {
Scroll(this.scroller) {
Row() {
Image($r('app.media.img_2')).width(200).height(150)
.border({ width: 1 })
.objectFit(ImageFit.Contain).margin(15) // Zoom out or zoom in the image based on the aspect ratio so that the image is completely displayed within the display boundary.
.overlay('Contain', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.ic_img_2')).width(200).height(150)
.border({ width: 1 })
.objectFit(ImageFit.Cover).margin(15)
// The image is scaled with its aspect ratio retained for both sides to be greater than or equal to the display boundaries.
.overlay('Cover', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.img_2')).width(200).height(150)
.border({ width: 1 })
// The image is scaled automatically to fit the display area.
.objectFit(ImageFit.Auto).margin(15)
.overlay('Auto', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
}
Row() {
Image($r('app.media.img_2')).width(200).height(150)
.border({ width: 1 })
.objectFit(ImageFit.Fill).margin(15)
// The image is scaled to fill the display area, and its aspect ratio is not retained.
.overlay('Fill', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.img_2')).width(200).height(150)
.border({ width: 1 })
// The image content is displayed with its aspect ratio retained. The size is smaller than or equal to the original size.
.objectFit(ImageFit.ScaleDown).margin(15)
.overlay('ScaleDown', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
Image($r('app.media.img_2')).width(200).height(150)
.border({ width: 1 })
// The original size is retained.
.objectFit(ImageFit.None).margin(15)
.overlay('None', { align: Alignment.Bottom, offset: { x: 0, y: 20 } })
}
}
}
}
```
![en-us_image_0000001511421240](figures/en-us_image_0000001511421240.png)
### Synchronously Loading Images
Generally, the image loading process is performed asynchronously to avoid blocking the main thread and affecting UI interaction. In certain cases, however, the picture flickers when it is refreshed. In this case, you can use the **syncLoad** attribute to load the picture synchronously to avoid flickering. You are not advised to use this function when the image loading takes a long time. Otherwise, the page cannot respond.
```ts
Image($r('app.media.icon'))
.syncLoad(true)
```
## Invoking Events
After the **onComplete** event is bound to the Image component, necessary information about the image can be obtained after the image is successfully loaded. If the image fails to be loaded, you can also bind the onError callback function to obtain the result.
```ts
@Entry
@Component
struct MyComponent {
@State widthValue: number = 0
@State heightValue: number = 0
@State componentWidth: number = 0
@State componentHeight: number = 0
build() {
Column() {
Row() {
Image($r('app.media.ic_img_2'))
.width(200)
.height(150)
.margin(15)
.onComplete((msg: {
width: number,
height: number,
componentWidth: number,
componentHeight: number
}) => {
this.widthValue = msg.width
this.heightValue = msg.height
this.componentWidth = msg.componentWidth
this.componentHeight = msg.componentHeight
})
// If the image fails to be obtained, print the result.
.onError(() => {
console.info('load image fail')
})
.overlay('\nwidth: ' + String(this.widthValue) + ', height: ' + String(this.heightValue) + '\ncomponentWidth: ' + String(this.componentWidth) + '\ncomponentHeight: ' + String(this.componentHeight), {
align: Alignment.Bottom,
offset: { x: 0, y: 60 }
})
}
}
}
}
```
![en-us_image_0000001511740460](figures/en-us_image_0000001511740460.png)
# 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.
##
-
# Creating a List
## Overview
A list is a container that displays a collection of items. If the list items go beyond the screen, the list can scroll to reveal the content off the screen. A list is applicable for presenting similar data types or data type sets, such as images and text. For example, it can be used to present a collection of contacts, songs, and items to shop.
Use lists to easily and efficiently display structured, scrollable information. You can provide a single view of rows or columns by arranging the [\<ListItemGroup>](../reference/arkui-ts/ts-container-listitemgroup.md/) or [\<ListItem>](../reference/arkui-ts/ts-container-listitem.md) child components linearly in a vertical or horizontal direction in the [\<List>](../reference/arkui-ts/ts-container-list.md/) component, or use [ForEach](../quick-start/arkts-rendering-control-foreach.md) to iterate over a group of rows or columns, or mix any number of single views and **ForEach** structures to build a list. The **\<List>** component supports the generation of child components in various [rendering](../quick-start/arkts-rendering-control-overview.md) modes, including conditional rendering, rendering of repeated content, and lazy data loading.
## Layout and Constraints
A list automatically arranges child components in the direction it scrolls. Adding or removing child components from the list will trigger re-arrangement of the child components.
As shown in the following figure, in a vertical list, **\<ListItemGroup>** or **\<ListItem>** components are automatically arranged vertically.
**\<ListItemGroup>** is used to display list data by group. Its child component is also **\<ListItem>**. **\<ListItem>** represents a list item, which can contain a single child component.
**Figure 1** Relationships between \<List>, \<ListItemGroup>, and \<ListItem>
![en-us_image_0000001562940589](figures/en-us_image_0000001562940589.png)
>**NOTE**
>
>A **\<List>** component can contain only **\<ListItemGroup>** or **\<ListItem>** as its child components. **\<ListItemGroup>** and **\<ListItem>** must be used together with **\<List>**.
### Layout
Apart from the aforementioned features, the list is also able to adapt to the number of elements in the cross axis direction.
When used in vertical layout, the list can contain one or more scrollable columns, as shown below.
**Figure 2** Vertical scrolling list (left: one column; right: multiple columns)
![en-us_image_0000001511580940](figures/en-us_image_0000001511580940.png)
When used in horizontal layout, the list can contain one or more scrollable rows, as shown below.
**Figure 3** Horizontal scrolling list (left: one column; right: multiple columns)
![en-us_image_0000001511421344](figures/en-us_image_0000001511421344.png)
### Constraints
The main axis direction of a list refers to the direction in which the child component columns are laid out and in which the list scrolls. An axis perpendicular to the main axis is referred to as a cross axis, and the direction of the cross axis is perpendicular to a direction of the main axis.
As shown below, the main axis of a vertical list is in the vertical direction, and the cross axis is in the horizontal direction. The main axis of a horizontal list is in the horizontal direction, and the cross axis is in the horizontal direction.
**Figure 4** Main axis and cross axis of the list
![en-us_image_0000001562940581](figures/en-us_image_0000001562940581.png)
If a size is set for the main axis or cross axis of the **\<List>** component, it is used as the size of the component in the corresponding direction.
If no size is set for the main axis of the **\<List>** component, the size of the **\<List>** component in the main axis direction automatically adapts to the total size of its child components, as long as the total size of the child components in the main axis direction does not exceed the size of the parent component of **\<List>**.
In the example shown below, no height is set for vertical list B, and the height of its parent component A is 200 vp. If the total height of all child components C is 150 vp, the height of list B is 150 vp.
**Figure 5** Main axis height constraint example 1 (A: parent component of \<List>; B: \<List> component; C: all child components of \<List>)
![en-us_image_0000001511580956](figures/en-us_image_0000001511580956.png)
If the total size of the child components in the main axis direction is greater than the size of the parent component of **\<List>**, the size of the **\<List>** component in the main axis direction automatically adapts to the size of its parent component.
In the example shown below, still no height is set for vertical list B, and the height of its parent component A is 200 vp. If the total height of all child components C is 300 vp, the height of list B is 200 vp.
**Figure 6** Main axis height constraint example 2 (A: parent component of \<List>; B: \<List> component; C: all child components of \<List>)
![en-us_image_0000001511740548](figures/en-us_image_0000001511740548.png)
If no size is set for the cross axis of the **\<List>** component, the size of the **\<List>** component in the cross axis direction automatically adapts to the size of its parent component.
## Developing the Layout
### Setting the Main Axis Direction
By default, the main axis of the **\<List>** component runs in the vertical direction. This means that you can create a vertical scrolling list without the need to manually set the list direction.
To create a horizontal scrolling list, set the **listDirection** attribute to **Axis.Horizontal**. The default value of **listDirection** is **Axis.Vertical**.
```ts
List() {
...
}
.listDirection(Axis.Horizontal)
```
### Setting the Cross Axis Layout
The cross axis layout of the **\<List>** component can be set using the **lanes** and **alignListItem** attributes. The **lanes** attribute controls the number of list items along the cross axis, and the **alignListItem** attribute controls the alignment mode of child components along the cross axis.
The **lanes** attribute of the **\<List>** component is useful in building a list that auto-adapts the numbers of rows or columns on devices of different sizes. Its value type is number or [LengthConstrain](../reference/arkui-ts/ts-types.md/#lengthconstrain). If you are building a two-column vertical list shown on the right in Figure 2, set the **lanes** attribute to **2**. The default value of **lanes** is **1**.
```ts
List() {
...
}
.lanes(2)
```
If set to a value of the LengthConstrain type, the **lanes** attribute determines the number of rows or columns based on the LengthConstrain settings and the size of the **\<List>** component.
```ts
List() {
...
}
.lanes({ minLength: 200, maxLength: 300 })
```
For example, if the **lanes** attribute is set to **{ minLength: 200, maxLength: 300 }** for a vertical list, then:
- When the list width is 300 vp, the list contains one column, because **minLength** is 200 vp.
- When the list width changes to 400 vp, which is twice that of the **minLength** value, the list is automatically adapted to two-column.
>**NOTE**
>
>When the **lanes** attribute is set to a value of the LengthConstrain type, the value is used only to calculate the number of rows or columns in the list and does not affect the size of the list items.
With regard to a vertical list, when the **alignListItem** attribute is set to **ListItemAlign.Center**, list items are center-aligned horizontally; when the **alignListItem** attribute is at its default value **ListItemAlign.Start**, list items are aligned toward the start edge of the cross axis in the list.
```ts
List() {
...
}
.alignListItem(ListItemAlign.Center)
```
## Displaying Data in the List
The list displays a collection of items horizontally or vertically and can scroll to reveal content off the screen. In the simplest case, a **\<List>** component is statically made up of **\<ListItem>** components.
**Figure 7** Example of a city list
![en-us_image_0000001563060761](figures/en-us_image_0000001563060761.png)
```ts
@Component
struct CityList {
build() {
List() {
ListItem() {
Text('Beijing').fontSize(24)
}
ListItem() {
Text('Hangzhou').fontSize(24)
}
ListItem() {
Text('Shanghai').fontSize(24)
}
}
.backgroundColor('#FFF1F3F5')
.alignListItem(ListItemAlign.Center)
}
}
```
Each **\<ListItem>** component can contain only one root child component. Therefore, it does not allow use of child components in tile mode. If tile mode is required, you need to encapsulate the child components into a container or create a custom component.
**Figure 8** Example of a contacts list
![en-us_image_0000001511421328](figures/en-us_image_0000001511421328.png)
As shown above, as a list item, each contact has a profile picture and a name. To present it, you can encapsulate **\<Image>** and **\<Text>** components into a **\<Row>** container.
```ts
List() {
ListItem() {
Row() {
Image($r('app.media.iconE'))
.width(40)
.height(40)
.margin(10)
Text ('Tom')
.fontSize(20)
}
}
ListItem() {
Row() {
Image($r('app.media.iconF'))
.width(40)
.height(40)
.margin(10)
Text ('Tracy')
.fontSize(20)
}
}
}
```
## Iterating List Content
Compared with a static list, a dynamic list is more common in applications. You can use [ForEach](../quick-start/arkts-rendering-control-foreach.md) to obtain data from the data source and create components for each data item.
For example, when creating a contacts list, you can store the contact name and profile picture data in a **Contact** class structure to the **contacts** array, and nest **ListItem**s in **ForEach**, thereby reducing repeated code needed for tiling similar list items.
```ts
import util from '@ohos.util';
class Contact {
key: string = util.generateRandomUUID(true);
name: string;
icon: Resource;
constructor(name: string, icon: Resource) {
this.name = name;
this.icon = icon;
}
}
@Entry
@Component
struct SimpleContacts {
private contacts = [
new Contact ('Tom', $r ("app.media.iconA")),
new Contact ('Tracy', $r ("app.media.iconB")),
...
]
build() {
List() {
ForEach(this.contacts, (item: Contact) => {
ListItem() {
Row() {
Image(item.icon)
.width(40)
.height(40)
.margin(10)
Text(item.name).fontSize(20)
}
.width('100%')
.justifyContent(FlexAlign.Start)
}
}, item => item.key)
}
.width('100%')
}
}
```
In the **\<List>** component, **ForEach** can be used to render **\<ListItemGroup>** items as well as **\<ListItem>** items. For details, see [Adding Grouping Support](#adding-grouping-support).
## Customizing the List Style
### Setting the Spacing
When initializing a list, you can use the **space** parameter to add spacing between list items. In the following example, a 10vp spacing is added between list items along the main axis:
```ts
List({ space: 10 }) {
...
}
```
### Adding Dividers
A divider separates UI items to make them easier to identify. In the following figure, a divider is added between the setting items. Note that since the icons are easy to identify in their own right, the divers do not extend below the icons.
**Figure 9** Using dividers between the setting items
![en-us_image_0000001511580960](figures/en-us_image_0000001511580960.png)
To add dividers between items in a **\<List>** component, you can use its **divider** attribute, sprucing up the dividers with the following style attributes:<br> **strokeWidth** and **color**: indicate the stroke width and color of the diver, respectively.
**startMargin** and **endMargin**: indicate the distance between the divider and the start edge and end edge of the list, respectively.
```ts
List() {
...
}
.divider({
strokeWidth: 1,
startMargin: 60,
endMargin: 10,
color: '#ffe9f0f0'
})
```
This example draws a divider with a stroke thickness of 1 vp from a position 60 vp away from the start edge of the list to a position 10 vp away from the end edge of the list. The effect is shown in Figure 8.
>**NOTE**
>
>1. The stroke width of the divider causes some space between list items. If the content spacing set for the list is smaller than the stroke width of the divider, the latter is used instead.
>
>2. When a list contains multiple columns, the **startMargin** and **endMargin** attributes of the divider apply to each column.
>
>3. The divider is drawn between list items. No divider is drawn above the first list item and below the last list item.
### Adding a Scrollbar
When the total height (width) of list items exceeds the screen height (width), the list can scroll vertically (horizontally). The scrollbar of a list enables users to quickly navigate the list content, as shown below.
**Figure 10** Scrollbar of a list
![en-us_image_0000001511740544](figures/en-us_image_0000001511740544.gif)
When using the **\<List>** component, you can use the **scrollBar** attribute to control the display of the list scrollbar. The value type of **scrollBar** is [BarState](../reference/arkui-ts/ts-appendix-enums.md/#barstate). When the value is **BarState.Auto**, the scrollbar is displayed as required: It is displayed when the scrollbar area is touched and becomes thicker when being dragged; it automatically disappears after 2 seconds of inactivity.
```ts
List() {
...
}
.scrollBar(BarState.Auto)
```
## Adding Grouping Support
By allowing data to be displayed in groups in the list, you make the list easier to scan and navigate. Grouping is common in real-world applications. For example, the contacts list below use grouping.
**Figure 11** Contacts list with grouping
![en-us_image_0000001511580948](figures/en-us_image_0000001511580948.png)
You can use **\<ListItemGroup>** to group items in the **\<List>** component to build a two-dimensional list.
A **\<List>** component allows one or more **\<ListItemGroup>** child components. By default, the width of **\<ListItemGroup>** is equal to that of **\<List>**. When initializing **\<ListItemGroup>**, you can use the **header** parameter to set its header.
```ts
@Component
struct ContactsList {
...
@Builder itemHead(text: string) {
// Header of the list group, corresponding to the group A and B locations.
Text(text)
.fontSize(20)
.backgroundColor('#fff1f3f5')
.width('100%')
.padding(5)
}
build() {
List() {
ListItemGroup({ header: this.itemHead('A') }) {
// Render the repeated list items of group A.
...
}
...
ListItemGroup({ header: this.itemHead('B') }) {
// Render the repeated list items of group B.
...
}
...
}
}
}
```
If the structures of multiple **\<ListItemGroup>** components are similar, you can combine the data of these components into an array and use **ForEach** to render them cyclically. For example, in the contacts list, the **contacts** data of each group (for details, see [Iterating List Content](#iterating-list-content)) and the **title** data of the corresponding group are combined and defined as the **contactsGroups** array.
```ts
contactsGroups: object[] = [
{
title: 'A',
contacts: [
new Contact('Alice', $r('app.media.iconA')),
new Contact ('Ann', $r ('app.media.iconB')),
new Contact('Angela', $r('app.media.iconC')),
],
},
{
title: 'B',
contacts: [
new Contact ('Ben', $r ('app.media.iconD')),
new Contact ('Bryan', $r ('app.media.iconE')),
],
},
...
]
```
Then, with rendering of **contactsGroups** in **ForEach**, a contact list with multiple groups is implemented.
```ts
List() {
// Render the <ListItemGroup> components cyclically. contactsGroups is the data set of contacts and titles of multiple groups.
ForEach(this.contactsGroups, item => {
ListItemGroup({ header: this.itemHead(item.title) }) {
// Render <ListItem> components cyclically.
ForEach(item.contacts, (contact) => {
ListItem() {
...
}
}, item => item.key)
}
...
})
}
```
## Adding a Sticky Header
The sticky header is a common pattern for keeping the header in the same place on the screen while the user scrolls down the list. As shown in the following figure, when you scroll through group A in the contacts list, the header of group B is always below group A. When you start scrolling through group B, the header of group B is fixed at the top of the screen. After group B has been scrolled to the bottom, the header of group B is replaced by the header of next group.
Sticky headers not only signify the representation and usage of data in the respective groups, but also help users navigate through a large amount of information, thereby avoiding unnecessary scrolling between the top of the area where the header is located and the area of interest.
**Figure 12** Sticky header
![en-us_image_0000001511740552](figures/en-us_image_0000001511740552.gif)
You can set a sticky header or footer for a **\<ListItemGroup>** component by setting the **sticky** attribute of its parent **\<List>** component.
Setting the **sticky** attribute to **StickyStyle.Header** implements a sticky header. To implement a sticky footer, use the **footer** parameter to initialize the footer of **\<ListItemGroup>** and set the **sticky** attribute to **StickyStyle.Footer**.
```ts
@Component
struct ContactsList {
// Define the contactsGroups array.
...
@Builder itemHead(text: string) {
// Header of the list group, corresponding to the group A and B locations.
Text(text)
.fontSize(20)
.backgroundColor('#fff1f3f5')
.width('100%')
.padding(5)
}
build() {
List() {
// Render the <ListItemGroup> components cyclically. contactsGroups is the data set of contacts and titles of multiple groups.
ForEach(this.contactsGroups, item => {
ListItemGroup({ header: this.itemHead(item.title) }) {
// Render <ListItem> components cyclically.
ForEach(item.contacts, (contact) => {
ListItem() {
...
}
}, item => item.key)
}
...
})
}
.sticky(StickyStyle.Header) // Set a sticky header.
}
}
```
## Controlling the Scrolling Position
In some cases you may want to control the scrolling position of a list. For example, when there are a huge number of items in the news page list, you may want to allow users to quickly jump to the top or bottom of the list after they have scrolled to a certain point. Below is an example.
**Figure 13** Returning to the top of the list
![en-us_image_0000001511900520](figures/en-us_image_0000001511900520.gif)
When the **\<List>** component is initialized, you can use the **scroller** parameter to bind a [Scroller](../reference/arkui-ts/ts-container-scroll.md/#scroller) object to control the scrolling of the list. In this example of a news page list, the **scrollToIndex** API of the **Scroller** object is used to scroll the list to the list item with the specified index. This allows the user to return to the top of the list by clicking a specific button.
First, you need to create a **Scroller** object **listScroller**.
```ts
private listScroller: Scroller = new Scroller();
```
Then, use **listScroller** to initialize the **scroller** parameter to bind it with the **\<List>** component. Set **scrollToIndex** to **0**, meaning to return to the top of the list.
```ts
Stack({ alignContent: Alignment.BottomEnd }) {
// use listScroller to initialize the scroller parameter to bind it with the <List> component.
List({ space: 20, scroller: this.listScroller }) {
...
}
...
Button() {
...
}
.onClick(() => {
// Specify where e to jump when the specific button is clicked, which is the top of the list in this example.
this.listScroller.scrollToIndex(0)
})
...
}
```
## Responding to the Scrolling Position
Many applications need to listen for the scrolling position change of the list and respond. For example, with regard to a contacts list, if scrolling spans more than one group, the alphabetical index bar at one side of the list also needs to be updated to highlight the letter corresponding to the current group.
Another common example is a scrolling list working with a multi-level index bar, as in the case of a product category page in a shopping application.
**Figure 14** Alphabetical index bar's response to contacts list scrolling
![en-us_image_0000001563060769](figures/en-us_image_0000001563060769.gif)
As shown above, when the contacts list scrolls from group A to B, the alphabetical index bar on the right also changes from A to B. This scenario can be implemented by listening for the **onScrollIndex** event of the **\<List>** component. The alphabet index bar is implemented using the [\<AlphabetIndexer>](../reference/arkui-ts/ts-container-alphabet-indexer.md/) component.
When the list scrolls, the **selectedIndex** value of the letter to highlight in the alphabet index bar is recalculated based on the **firstIndex** value of the item to which the list has scrolled. In the **\<AlphabetIndexer>** component, the index of the highlighted item is set through the **selected** attribute. When the value of **selectedIndex** changes, the **\<AlphabetIndexer>** component is re-rendered to highlight the corresponding letter.
```ts
...
const alphabets = ['#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
@Entry
@Component
struct ContactsList {
@State selectedIndex: number = 0;
private listScroller: Scroller = new Scroller();
...
build() {
Stack({ alignContent: Alignment.End }) {
List({ scroller: this.listScroller }) {
...
}
.onScrollIndex((firstIndex: number) => {
// Recalculate the value of this.selectedIndex in the alphabetical index bar based on the index of the item to which the list has scrolled.
...
})
...
// <AlphabetIndexer> component
AlphabetIndexer({ arrayValue: alphabets, selected: 0 })
.selected(this.selectedIndex)
...
}
}
}
```
>**NOTE**
>
>During index calculation, each **\<ListItemGroup>** component is taken as a whole and assigned an index, and the indexes of the list items within are not included in the calculation.
## Responding to Swipe on List Items
Swipe menus are common in many applications. For example, a messaging application generally provides a swipe-to-delete feature for its message list. This feature allows users to delete a message by swiping left on it in the list and touching the delete button, as shown in the following figure.
**Figure 15** Swipe-to-delete feature
![en-us_image_0000001563060773](figures/en-us_image_0000001563060773.gif)
To implement the swipe feature, you can use the **swipeAction** attribute of **\<ListItem>**. In initialization of the **swipeAction** attribute, the **SwipeActionOptions** parameter is mandatory, wherein the **start** parameter indicates the component that appears from the start edge when the list item slides right, and the **end** parameter indicates the component that appears from the end edge when the list item slides left.
In the example of the message list, the **end** parameter is set to a custom delete button. In initialization of the **end** attribute, the index of the sliding list item is passed to the delete button. When the user touches the delete button, the data corresponding to the list item is deleted based on the index.
```ts
@Entry
@Component
struct MessageList {
@State messages: object[] = [
// Initialize the message list data.
...
];
@Builder itemEnd(index: number) {
// Set the component that appears from the end edge when the list item slides left.
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_public_delete_filled'))
.width(20)
.height(20)
}
.onClick(() => {
this.messages.splice(index, 1);
})
...
}
build() {
...
List() {
ForEach(this.messages, (item, index) => {
ListItem() {
...
}
.swipeAction({ end: this.itemEnd.bind(this, index) }) // Set the swipe attributes.
}, item => item.id.toString())
}
...
}
}
```
## Adding a Mark to a List Item
A mark is an intuitive, unintrusive visual indicator to draw attention and convey a specific message. For example, when a new message is received in the message list, a mark is displayed in the upper right corner of the contact's profile picture, indicating that there is a new message from that contact, as shown in the following figure.
**Figure 16** Adding a mark to a list item
![en-us_image_0000001511580952](figures/en-us_image_0000001511580952.png)
To add a mark, you can use the [\<Badge>](../reference/arkui-ts/ts-container-badge.md/) component in **\<ListItem>**. The **\<Badge>** component is a container that can be attached to another component for tagging.
In this example, when implementing the **\<Image>** component for presenting the profile picture of a list item, add it to **\<Badge>** as a child component.
In the **\<Badge>** component, the **count** and **position** parameters are used to set the number of notifications and the position to display the badge, respectively. You can also use the **style** parameter to spruce up the mark.
```ts
Badge({
count: 1,
position: BadgePosition.RightTop,
style: { badgeSize: 16, badgeColor: '#FA2A2D' }
}) {
// The <Image> component implements the contact profile picture.
...
}
...
```
## Implementing Pull-Down-to-Refresh and Pull-Up-to-Load
The pull-down-to-refresh and pull-up-to-load features are widely used in mobile applications, such as news applications. In effect, the implementation of these two features follows the same process: (1) As response to a [touch event](../reference/arkui-ts/ts-universal-events-touch.md/), a refresh or load view is displayed at the top or bottom of the page; (2) when the refresh or load is complete, the refresh or load view is hidden.
The following describes the implementation of the pull-and-refresh feature:
1. Listen for the finger press event and record the value of the initial position.
2. Listen for the finger movement event, and record and calculate the difference between the value of the current position and the initial value. If the difference is greater than 0, the finger moves downward. Set the maximum value for the movement.
3. Listen for the finger lift event. If the movement reaches the maximum value, trigger data loading and display the refresh view. After the loading is complete, hide the view.
You can also use the third-party component [PullToRefresh](https://gitee.com/openharmony-sig/PullToRefresh) to implement this feature.
## Editing a List
The list editing mode is frequently used in various scenarios, such as to-do list management, file management, and note management. In editing mode, adding and deleting list items are the most basic functions. The core is to add and delete data in the data set corresponding to the list items.
The following uses to-do list management as an example to describe how to quickly add and delete list items.
### Adding a List Item
As shown below, when a user touches **Add**, a page is displayed for the user to set options for the new list item. After the user touches **OK**, the corresponding item is added to the list.
**Figure 17** Adding a to-do task
![en-us_image_0000001511740556](figures/en-us_image_0000001511740556.gif)
The process of implementing the addition feature is as follows:
1. Define the list item data structure and initialize the list data to build the overall list layout and list items.
In this example, first define the to-do data structure.
```ts
import util from '@ohos.util';
export class ToDo {
key: string = util.generateRandomUUID(true);
name: string;
constructor(name: string) {
this.name = name;
}
}
```
Then, initialize the to-do list data and options:
```ts
@State toDoData: ToDo[] = [];
private availableThings: string[] = ['Reading', 'Fitness', 'Travel','Music','Movie', 'Singing'];
```
Finally, build the list layout and list items:
```ts
List({ space: 10 }) {
ForEach(this.toDoData, (toDoItem) => {
ListItem() {
...
}
}, toDoItem => toDoItem.key)
}
```
2. Provide the entry for adding a list item, that is, add a click event to the add button.
3. Respond to the user's confirmation of adding and update the list data.
The code snippet for steps 2 and 3 is as follows:
```ts
Text('+')
.onClick(() => {
TextPickerDialog.show({
range: this.availableThings,
onAccept: (value: TextPickerResult) => {
this.toDoData.push(new ToDo(this.availableThings[value.index])); // Add the list item data.
},
})
})
```
### Deleting a List Item
As shown below, when the user long presses a list item to enter the deletion mode, a page is displayed for the user to delete the list item. After the user selects the list item and touches the delete button, the list item is deleted.
**Figure 18** Deleting a to-do task
![en-us_image_0000001562820877](figures/en-us_image_0000001562820877.gif)
The process of implementing the deletion feature is as follows:
1. Generally, the deletion feature is available only after the list enters the editing mode. Therefore, the entry to the editing mode needs to be provided.
In this example, by listening for the long press event of a list item, the list enters the editing mode when the user long presses a list item.
```ts
// ToDoListItem.ets
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
...
}
.gesture(
GestureGroup(GestureMode.Exclusive,
LongPressGesture()
.onAction(() => {
if (!this.isEditMode) {
this.isEditMode = true; // Enter the editing mode.
this.selectedItems.push(this.toDoItem); // Record the list item selected when the user long presses the button.
}
})
)
)
```
2. Respond to the selection by the user and record the list items to be deleted.
In this example of the to-do list, respond to the selection by correctly displaying the check mark and record all the selected list items.
```ts
// ToDoListItem.ets
if (this.isEditMode) {
Checkbox()
.onChange((isSelected) => {
if (isSelected) {
this.selectedItems.push(this.toDoItem) // When an item is selected, record the selected item.
} else {
let index = this.selectedItems.indexOf(this.toDoItem)
if (index !== -1) {
this.selectedItems.splice(index, 1) // When an item is deselected, delete the item from the selectedItems array.
}
}
})
...
}
```
3. Respond to the user's clicking the delete button and delete the corresponding items from the list.
```ts
// ToDoList.ets
Button ('Delete')
.onClick(() => {
// Delete the toDoData data corresponding to the selected list items.
let leftData = this.toDoData.filter((item) => {
return this.selectedItems.find((selectedItem) => selectedItem !== item);
})
this.toDoData = leftData;
this.isEditMode = false;
})
...
```
## Handling a Long List
[ForEach](../quick-start/arkts-rendering-control-foreach.md) is applicable to short lists. With regard to a long list with a large number of list items, using **ForEach** will greatly slow down page loading, as it loads all list items at a time. Therefore, for better list performance, use [LazyForEach](../quick-start/arkts-rendering-control-lazyforeach.md) instead to implement on-demand iterative data loading.
For details about the implementation, see the example in [LazyForEach: Lazy Data Loading](../quick-start/arkts-rendering-control-lazyforeach.md).
When the list is rendered in lazy loading mode, to improve the list scrolling experience and minimize white blocks during list scrolling, you can use the **cachedCount** parameter of the **\<List>** component. This parameter sets the number of list items preloaded outside of the screen and is valid only in **LazyForEach**.
```ts
List() {
LazyForEach(this.dataSource, item => {
ListItem() {
...
}
})
}.cachedCount(3)
```
The following uses a vertical list as an example:
- If lazy loading is used for list items and the list contains only one column, the number of the list items to cache before and after the currently displayed one equals the value of **cachedCount**. If the list contains multiple columns, the number of the list items to cache is the value of **cachedCount** multiplied by the number of columns.
- If lazy loading is used for list item groups, the number of the list item groups to cache before and after the currently displayed one equals the value of **cachedCount**, regardless of the number of columns.
>**NOTE**
>
>1. 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.
>
>2. When a list uses data lazy loading, all list items except the list items in the display area and the cached list items are destroyed.
##
-
-
-
# Creating a Swiper
The \<[Swiper](../reference/arkui-ts/ts-container-swiper.md)> component is a container that is able to display child components in looping mode. It is typically used in scenarios such as display of recommended content on the home page.
## Layout and Constraints
The size of the **\<Swiper>** component follows its own size settings (if configured) or adapts based on the size of its child components.
## Loop Playback
The **loop** attribute sets whether to enable loop playback. Its default value is **true**.
When **loop** is set to **true**, the user can switch to the previous or next page when they are on the first or last page.
Example of setting **loop** to **true**:
```ts
...
private swiperController: SwiperController = new SwiperController()
...
Swiper(this.swiperController) {
Text("0")
.width('90%')
.height('100%')
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width('90%')
.height('100%')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width('90%')
.height('100%')
.backgroundColor(Color.Blue)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.loop(true)
```
![loop_true](figures/loop_true.gif)
Example of setting **loop** to **false**:
```ts
Swiper(this.swiperController) {
Text("0")
.width('90%')
.height('100%')
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width('90%')
.height('100%')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width('90%')
.height('100%')
.backgroundColor(Color.Blue)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.loop(false)
```
![loop_false](figures/loop_false.gif)
## Automatic Playback
The **autoPlay** attribute sets whether to enable automatic playback for child component switching. Its default value is **false**.
When **autoPlay** is set to **true**, automatic playback is enabled for child component switching. The playback interval is specified by the **interval** attribute, which is **3000** by default, in milliseconds.
Example of setting **autoPlay** to **true**:
```ts
Swiper(this.swiperController) {
Text("0")
.width('90%')
.height('100%')
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width('90%')
.height('100%')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width('90%')
.height('100%')
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.loop(true)
.autoPlay(true)
.interval(1000)
```
![autoPlay](figures/autoPlay.gif)
## Navigation Dots Indicator
The **\<Swiper>** component provides a navigation dots indicator, which is displayed in the bottom center of the component. You can customize the position and style of the navigation dots indicator through the **indicatorStyle **attribute.
With the **indicatorStyle** attribute, you can set the position of the navigation dots indicator relative to the edges of the **\<Swiper>** component, in addition to the size, color, and mask of each navigation dot as well as the color of the selected navigation dot.
Example of using the navigation dots indicator in its default style:
```ts
Swiper(this.swiperController) {
Text("0")
.width('90%')
.height('100%')
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width('90%')
.height('100%')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width('90%')
.height('100%')
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
}
```
![indicator](figures/indicator.PNG)
Example of customizing the style of the navigation dots indicator, with the diameter of 30 vp, left margin of 0, and color of red:
```ts
Swiper(this.swiperController) {
Text("0")
.width('90%')
.height('100%')
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width('90%')
.height('100%')
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width('90%')
.height('100%')
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.indicatorStyle({
size: 30,
left: 0,
color: Color.Red
})
```
![ind](figures/ind.PNG)
## Page Switching Mode
The **\<Swiper>** component supports three page switching modes: using the swipe gesture, using the navigation dots indicator, and using the controller.
Switch pages through the controller:
```ts
@Entry
@Component
struct SwiperDemo {
private swiperController: SwiperController = new SwiperController();
build() {
Column({ space: 5 }) {
Swiper(this.swiperController) {
Text("0")
.width(250)
.height(250)
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width(250)
.height(250)
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width(250)
.height(250)
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.indicator(true)
Row({ space: 12 }) {
Button('showNext')
.onClick(() => {
this.swiperController.showNext(); // Switch to the next page through the controller.
})
Button('showPrevious')
.onClick(() => {
this.swiperController.showPrevious(); // Switch to the previous page through the controller.
})
}.margin(5)
}.width('100%')
.margin({ top: 5 })
}
}
```
![controll](figures/controll.gif)
## Playback Direction
You can set the playback direction for the \<Swiper> component through its **vertical** attribute.
When **vertical** is set to **true**, vertical swiping is used. The default value of **vertical** is **false**.
Example of using horizontal swiping:
```ts
Swiper(this.swiperController) {
...
}
.indicator(true)
.vertical(false)
```
![horizontal-swiping](figures/horizontal-swiping.PNG)
Example of using vertical swiping:
```ts
Swiper(this.swiperController) {
...
}
.indicator(true)
.vertical(true)
```
![vertical-swiping](figures/vertical-swiping.PNG)
## Child Components Per Page
You can set the number of child components per page for the \<Swiper> component through its [displayCount](../reference/arkui-ts/ts-container-swiper.md#attributes) attribute.
To display two child components per page:
```ts
Swiper(this.swiperController) {
Text("0")
.width(250)
.height(250)
.backgroundColor(Color.Gray)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("1")
.width(250)
.height(250)
.backgroundColor(Color.Green)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("2")
.width(250)
.height(250)
.backgroundColor(Color.Pink)
.textAlign(TextAlign.Center)
.fontSize(30)
Text("3")
.width(250)
.height(250)
.backgroundColor(Color.Blue)
.textAlign(TextAlign.Center)
.fontSize(30)
}
.indicator(true)
.displayCount(2)
```
![two](figures/two.PNG)
# 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)
# Responsive Grid Layout
## Overview
As an auxiliary positioning tool, the responsive grid layout is handy in UI design on mobile devices. It exhibits the following advantages:
1. Provides rules for layout design and resolves issues of dynamic layout across devices with different sizes. By dividing a page into equal-width columns and rows, you can easily locate and typeset page elements.
2. Provides a unified positioning method for the system to ensure layout consistency across layouts on different devices. This can reduce the complexity of design and development and improve work efficiency.
3. Provides a flexible spacing adjustment method for applications to accommodate special layout requirements. You can adjust the spacing between columns and between rows to control the typesetting of the entire page.
4. Completes the wrapping and adaptation automatically when overflow occurs. When the number of page elements exceeds the capacity of a row or column, they automatically wrap to a new row or column and adapt the typesetting to different devices.
The [\<GridRow>](../reference/arkui-ts/ts-container-gridrow.md) component is the responsive grid container component and must be used together with the [\<GridCol>](../reference/arkui-ts/ts-container-gridcol.md) child component.
## GridRow
### 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) | Device Description |
| ---- | --------------- | --------- |
| xs | [0, 320) | Device of the minimum size.|
| sm | [320,&nbsp;520) | Small-sized device. |
| md | [520,&nbsp;840) | Medium-sized device.|
| lg | [840,&nbsp;+∞) | Large-sized device. |
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', '520vp', '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–520 vp, the breakpoint is sm. If the value is 520–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.
```ts
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
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)
})
}
```
![en-us_image_0000001511421272](figures/en-us_image_0000001511421272.gif)
### Columns
In the **\<GridRow>**, **columns** is used to set the total number of columns in the responsive grid layout.
- The default value of **columns** is 12. If **columns** is not set, the responsive grid layout is divided into 12 columns at any breakpoint.
```ts
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow() {
ForEach(this.bgColors, (item, index) => {
GridCol() {
Row() {
Text(`${index + 1}`)
}.width('100%').height('50')
}.backgroundColor(item)
})
}
```
![en-us_image_0000001563060709](figures/en-us_image_0000001563060709.png)
- When **columns** is set to a number, the responsive 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
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
@State currentBp: string = 'unknown';
...
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%')
```
![en-us_image_0000001511421268](figures/en-us_image_0000001511421268.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
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown]
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)
})
}
```
![en-us_image_0000001563060689](figures/en-us_image_0000001563060689.gif)
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.
### Alignment
In the responsive grid layout, you can set the **direction** attribute of **\<GridRow>** to define the direction in which child components are arranged. The options are **GridRowDirection.Row** (from left to right) or **GridRowDirection.RowReverse** (from right to left). An appropriate **direction** value can make the page layout more flexible and meet the design requirements.
- When child components are arranged from left to right (default):
```ts
GridRow({ direction: GridRowDirection.Row }){}
```
![en-us_image_0000001511740488](figures/en-us_image_0000001511740488.png)
- When child components are arranged from right to left (default):
```ts
GridRow({ direction: GridRowDirection.RowReverse }){}
```
![en-us_image_0000001562940517](figures/en-us_image_0000001562940517.png)
### Gutters
In the **\<GridRow>** component, **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 is set to **10**.
```ts
GridRow({ gutter: 10 }){}
```
![en-us_image_0000001511740476](figures/en-us_image_0000001511740476.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 } }){}
```
![en-us_image_0000001511900456](figures/en-us_image_0000001511900456.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 })
```
### 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
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow({ columns: 8 }) {
ForEach(this.bgColors, (color, index) => {
GridCol({ span: 2 }) {
Row() {
Text(`${index}`)
}.width('100%').height('50vp')
}
.backgroundColor(color)
})
}
```
![en-us_image_0000001511421264](figures/en-us_image_0000001511421264.png)
- When the value type is **GridColColumnOption**, you can assign values specific to the screen size (xs, sm, md, lg, xl, xxl).
```ts
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
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)
})
}
```
![en-us_image_0000001511740492](figures/en-us_image_0000001511740492.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
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
GridRow() {
ForEach(this.bgColors, (color, index) => {
GridCol({ offset: 2 }) {
Row() {
Text('' + index)
}.width('100%').height('50vp')
}
.backgroundColor(color)
})
}
```
![en-us_image_0000001563060705](figures/en-us_image_0000001563060705.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
@State bgColors: Color[] = [Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Pink, Color.Grey, Color.Blue, Color.Brown];
...
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)
})
}
```
![en-us_image_0000001562700433](figures/en-us_image_0000001562700433.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 component 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: 4 }) {
Row() {
Text('1')
}.width('100%').height('50vp')
}.backgroundColor(Color.Red)
GridCol({ order: 3 }) {
Row() {
Text('2')
}.width('100%').height('50vp')
}.backgroundColor(Color.Orange)
GridCol({ order: 2 }) {
Row() {
Text('3')
}.width('100%').height('50vp')
}.backgroundColor(Color.Yellow)
GridCol({ order: 1 }) {
Row() {
Text('4')
}.width('100%').height('50vp')
}.backgroundColor(Color.Green)
}
```
![en-us_image_0000001511580892](figures/en-us_image_0000001511580892.png)
- When the value type is **GridColColumnOption**, you can assign values specific to the screen size (xs, sm, md, lg, xl, xxl). You can set 1234 for xs, 2341 for sm, 3412 for md, and 2431 for lg.
```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)
}
```
![en-us_image_0000001511900444](figures/en-us_image_0000001511900444.gif)
## Nesting of Responsive Grid Components
Responsive grid components can be contained in other responsive grid components.
In the following example, the responsive grid divides the entire space into 12 parts. **\<GridCol>** is nested in **\<GridRow>** at the first layer, which is divided into the large area in the center and the footer area. **\<GridCol>** is nested in **\<GridRow>** at the second layer, which is divided into the left and right areas. The child component space is divided based on the space allocation of the parent component at the upper layer. For example, the pink area is made up of 12 columns of the screen space, and the green and blue areas are made up of 12 columns of the **\<GridRow>** parent component.
```ts
@Entry
@Component
struct GridRowExample {
build() {
GridRow() {
GridCol({ span: { sm: 12 } }) {
GridRow() {
GridCol({ span: { sm: 2 } }) {
Row() {
Text('left').fontSize(24)
}
.justifyContent(FlexAlign.Center)
.height('90%')
}.backgroundColor('#ff41dbaa')
GridCol({ span: { sm: 10 } }) {
Row() {
Text('right').fontSize(24)
}
.justifyContent(FlexAlign.Center)
.height('90%')
}.backgroundColor('#ff4168db')
}
.backgroundColor('#19000000')
.height('100%')
}
GridCol({ span: { sm: 12 } }) {
Row() {
Text('footer').width('100%').textAlign(TextAlign.Center)
}.width('100%').height('10%').backgroundColor(Color.Pink)
}
}.width('100%').height(300)
}
}
```
![en-us_image_0000001563060697](figures/en-us_image_0000001563060697.png)
To sum up, the responsive grid components are powerful tools with a wide range of customization capabilities. With the required attributes set at different breakpoints, such as **Columns**, **Margin**, **Gutter**, and **span**, the layout is created automatically. You do not need to pay attention to the specific device type and device state (such as landscape and portrait).
# Linear Layout
## Overview
Linear layout is the most frequently used layout in development, built with the [\<Row>](../reference/arkui-ts/ts-container-row.md) and [\<Column>](../reference/arkui-ts/ts-container-column.md) linear containers. The linear layout is the basis of other layouts. Its child components are arranged in sequence linearly in the horizontal direction, as in a **\<Row>** container, or vertical direction, as in a **\<Column>** container.
**Figure 1** Child component arrangement in a \<Column> container
![arrangement-child-elements-column](figures/arrangement-child-elements-column.png)
**Figure 2** Child component arrangement in a \<Row> container
![arrangement-child-elements-row](figures/arrangement-child-elements-row.png)
## Basic Concepts
- Layout container: container component that is able to lay out other elements as its child elements. The layout container calculates the size of its child elements and arranges the layout.
- Layout child element: element inside the layout container.
- Main axis: axis along which child elements are laid out by default in the linear layout container. The main axis is vertical for the **\<Row>** container and horizontal for the **\<Column>** container.
- Cross axis: axis that runs perpendicular to the main axis. The cross axis is horizontal for the **\<Row>** container and vertical for the **\<Column>** container.
- Spacing: vertical distance between layout child elements.
## Spacing of Layout Child Elements in Arrangement Direction
In the layout container, use the **space** attribute to equally space child elements in the arrangement direction.
### In \<Column> Container
**Figure 3** Layout child element spacing in the arrangement direction in the \<Column> container
![arrangement-direction-column](figures/arrangement-direction-column.png)
```ts
Column({ space: 20 }) {
Text('space: 20').fontSize(15).fontColor(Color.Gray).width('90%')
Row().width('90%').height(50).backgroundColor(0xF5DEB3)
Row().width('90%').height(50).backgroundColor(0xD2B48C)
Row().width('90%').height(50).backgroundColor(0xF5DEB3)
}.width('100%')
```
![arrangement-direction-column-sample](figures/arrangement-direction-column-sample.png)
### In \<Row> Container
**Figure 4** Layout child element spacing in the arrangement direction in the \<Row> container
![arrangement-direction-row](figures/arrangement-direction-row.png)
```ts
Row({ space: 35 }) {
Text('space: 35').fontSize(15).fontColor(Color.Gray)
Row().width('10%').height(150).backgroundColor(0xF5DEB3)
Row().width('10%').height(150).backgroundColor(0xD2B48C)
Row().width('10%').height(150).backgroundColor(0xF5DEB3)
}.width('90%')
```
![en-us_image_0000001562700509](figures/en-us_image_0000001562700509.png)
## Alignment of Layout Child Elements Along Cross Axis
In the layout container, use the **alignItems** attribute to set the alignment mode of child elements along the cross axis. The alignment performance is consistent across screens of various sizes. The value is of the [VerticalAlign Type](../reference/arkui-ts/ts-appendix-enums.md#verticalalign) type when the cross axis is in the vertical direction and the [HorizontalAlign](../reference/arkui-ts/ts-appendix-enums.md#horizontalalign) type when the cross axis is in the horizontal direction.
The layout container also provides the **alignSelf** attribute to control the alignment mode of a single child element along the main axis. This attribute has a higher priority than the **alignItems** attribute. This means that, if **alignSelf** is set, it will overwrite the **alignItems** setting on the corresponding child element.
### Horizontal Alignment of Layout Child Elements in \<Column> Container
**Figure 5** Horizontal alignment of layout child elements in the \<Column> container
![horizontal-arrangement-child-column](figures/horizontal-arrangement-child-column.png)
- **HorizontalAlign.Start**: Child elements are left aligned horizontally.
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').alignItems(HorizontalAlign.Start).backgroundColor('rgb(242,242,242)')
```
![en-us_image_0000001511580964](figures/en-us_image_0000001511580964.png)
- **HorizontalAlign.Center**: Child elements are center-aligned horizontally.
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').alignItems(HorizontalAlign.Center).backgroundColor('rgb(242,242,242)')
```
![en-us_image_0000001562820897](figures/en-us_image_0000001562820897.png)
- **HorizontalAlign.End**: Child elements are right-aligned horizontally.
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').alignItems(HorizontalAlign.End).backgroundColor('rgb(242,242,242)')
```
![en-us_image_0000001511421348](figures/en-us_image_0000001511421348.png)
### Vertical Alignment of Layout Child Elements in \<Row> Container
**Figure 6** Vertical alignment of layout child elements in \<Row> container
![horizontal-arrangement-child-row](figures/horizontal-arrangement-child-row.png)
- **VerticalAlign.Top**: Child elements are top-aligned vertically.
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).alignItems(VerticalAlign.Top).backgroundColor('rgb(242,242,242)')
```
![en-us_image_0000001563060765](figures/en-us_image_0000001563060765.png)
- **VerticalAlign.Center**: Child elements are center-aligned vertically.
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).alignItems(VerticalAlign.Center).backgroundColor('rgb(242,242,242)')
```
![en-us_image_0000001562700505](figures/en-us_image_0000001562700505.png)
- **VerticalAlign.Bottom**: Child elements are bottom-aligned vertically.
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).alignItems(VerticalAlign.Bottom).backgroundColor('rgb(242,242,242)')
```
![en-us_image_0000001563060781](figures/en-us_image_0000001563060781.png)
## Arrangement of Layout Child Elements Along Main Axis
In the layout container, you can use the **justifyContent** attribute to set the arrangement mode of child elements along the main axis. The arrangement may begin from the start point or end point of the main axis, or the space of the main axis can be evenly divided.
### In \<Column> Container
**Figure 7** Arrangement of layout child elements along main axis in the \<Column> container
![vertial-arrangement-child-column](figures/vertial-arrangement-child-column.png)
- **justifyContent(FlexAlign.Start)**: The items are aligned with each other toward the start edge of the container along the main axis.
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Start)
```
![en-us_image_0000001562700501](figures/en-us_image_0000001562700501.png)
- **justifyContent(FlexAlign.Center)**: The elements are aligned with each other toward the center of the container along the main axis. The space between the first component and the main-start is the same as that between the last component and the main-end.
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Center)
```
![en-us_image_0000001562700517](figures/en-us_image_0000001562700517.png)
- **justifyContent(FlexAlign.End)**: The elements are aligned with each other toward the end edge of the container along the main axis.
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.End)
```
![en-us_image_0000001562940585](figures/en-us_image_0000001562940585.png)
- **justifyContent(FlexAlign.Spacebetween)**: The elements are evenly distributed along the main axis. The space between any two adjacent elements is the same. The first element is aligned with the main-start, the last element is aligned with the main-end, and the remaining elements are distributed so that the space between any two adjacent elements is the same.
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceBetween)
```
![en-us_image_0000001511900532](figures/en-us_image_0000001511900532.png)
- **justifyContent(FlexAlign.SpaceAround)**: The elements are evenly distributed along the main axis. The space between any two adjacent elements is the same. The space between the first element and main-start, and that between the last element and cross-main are both half the size of the space between two adjacent elements.
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceAround)
```
![en-us_image_0000001562700525](figures/en-us_image_0000001562700525.png)
- **justifyContent(FlexAlign.SpaceEvenly)**: The elements are evenly distributed along the main axis. The space between the first element and main-start, the space between the last element and main-end, and the space between any two adjacent elements are the same.
```ts
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceEvenly)
```
![en-us_image_0000001563060785](figures/en-us_image_0000001563060785.png)
### In \<Row> Container
**Figure 8** Arrangement of layout child elements along main axis in the \<Row> container
![vertial-arrangement-child-row](figures/vertial-arrangement-child-row.png)
- **justifyContent(FlexAlign.Start)**: The items are aligned with each other toward the start edge of the container along the main axis.
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Start)
```
![en-us_image_0000001511421356](figures/en-us_image_0000001511421356.png)
- **justifyContent(FlexAlign.Center)**: The elements are aligned with each other toward the center of the container along the main axis. The space between the first component and the main-start is the same as that between the last component and the main-end.
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Center)
```
![en-us_image_0000001511900516](figures/en-us_image_0000001511900516.png)
- **justifyContent(FlexAlign.End)**: The elements are aligned with each other toward the end edge of the container along the main axis.
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.End)
```
![en-us_image_0000001562940601](figures/en-us_image_0000001562940601.png)
- **justifyContent(FlexAlign.Spacebetween)**: The elements are evenly distributed along the main axis. The space between any two adjacent elements is the same. The first element is aligned with the main-start, the last element is aligned with the main-end, and the remaining elements are distributed so that the space between any two adjacent elements is the same.
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceBetween)
```
![en-us_image_0000001562700521](figures/en-us_image_0000001562700521.png)
- **justifyContent(FlexAlign.SpaceAround)**: The elements are evenly distributed along the main axis. The space between any two adjacent elements is the same. The space between the first element and main-start, and that between the last element and cross-main are both half the size of the space between two adjacent elements.
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceAround)
```
![en-us_image_0000001562820893](figures/en-us_image_0000001562820893.png)
- **justifyContent(FlexAlign.SpaceEvenly)**: The elements are evenly distributed along the main axis. The space between the first element and main-start, the space between the last element and main-end, and the space between any two adjacent elements are the same.
```ts
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceEvenly)
```
![en-us_image_0000001511421352](figures/en-us_image_0000001511421352.png)
## Adaptive Stretching
In linear layout, adaptive stretching is achieved by using the [\<Blank>](../reference/arkui-ts/ts-basic-components-blank.md) component, which automatically fills the empty spaces in the container – **\<Row>** or **\<Column>** – along the main axis. Just add the width and height as a percentage, and then when the screen width and height change, adaptive stretching takes effect.
```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%')
}
}
```
**Figure 9** Portrait mode
![en-us_image_0000001562820881](figures/en-us_image_0000001562820881.png)
**Figure 10** Landscape mode
![en-us_image_0000001511421332](figures/en-us_image_0000001511421332.png)
## Adaptive Scaling
Adaptive scaling means that the size of a child component is automatically adjusted according to a preset ratio to fit into the container across devices of various screen sizes. In linear layout, adaptive scaling can be achieved using either of the following methods:
- When the container size is determined, use **layoutWeight** to set the weight of a child component during layout. The container space is then allocated along the main axis among the component and sibling components based on the set layout weight, and the component size setting is ignored.
```ts
@Entry
@Component
struct layoutWeightExample {
build() {
Column() {
Text('1:2:3').width('100%')
Row() {
Column() {
Text('layoutWeight(1)')
.textAlign(TextAlign.Center)
}.layoutWeight(2).backgroundColor(0xF5DEB3).height('100%')
Column() {
Text('layoutWeight(2)')
.textAlign(TextAlign.Center)
}.layoutWeight(4).backgroundColor(0xD2B48C).height('100%')
Column() {
Text('layoutWeight(6)')
.textAlign(TextAlign.Center)
}.layoutWeight(6).backgroundColor(0xF5DEB3).height('100%')
}.backgroundColor(0xffd306).height('30%')
Text('2:5:3').width('100%')
Row() {
Column() {
Text('layoutWeight(2)')
.textAlign(TextAlign.Center)
}.layoutWeight(2).backgroundColor(0xF5DEB3).height('100%')
Column() {
Text('layoutWeight(5)')
.textAlign(TextAlign.Center)
}.layoutWeight(5).backgroundColor(0xD2B48C).height('100%')
Column() {
Text('layoutWeight(3)')
.textAlign(TextAlign.Center)
}.layoutWeight(3).backgroundColor(0xF5DEB3).height('100%')
}.backgroundColor(0xffd306).height('30%')
}
}
}
```
**Figure 11** Landscape mode
![en-us_image_0000001511421336](figures/en-us_image_0000001511421336.png)
**Figure 12** Portrait mode
![en-us_image_0000001511580968](figures/en-us_image_0000001511580968.png)
- When the container size is determined, set the width of a child component in percentage. The container space is then allocated among the component and sibling components based on the set percentage.
```ts
@Entry
@Component
struct WidthExample {
build() {
Column() {
Row() {
Column() {
Text('left width 20%')
.textAlign(TextAlign.Center)
}.width('20%').backgroundColor(0xF5DEB3).height('100%')
Column() {
Text('center width 50%')
.textAlign(TextAlign.Center)
}.width('50%').backgroundColor(0xD2B48C).height('100%')
Column() {
Text('right width 30%')
.textAlign(TextAlign.Center)
}.width('30%').backgroundColor(0xF5DEB3).height('100%')
}.backgroundColor(0xffd306).height('30%')
}
}
}
```
**Figure 13** Landscape mode
![en-us_image_0000001563060777](figures/en-us_image_0000001563060777.png)
**Figure 14** Portrait mode
![en-us_image_0000001511740564](figures/en-us_image_0000001511740564.png)
## Adaptive Extension
Adaptive extension allows users to drag the scrollbar to display the page content outside the screen. It is applicable to the scenario where the content extends beyond the viewport in linear layout. Below are the methods to implement adaptive extension in linear layout:
- [Add a scrollbar to a \<List> component](arkts-layout-development-create-list.md#adding-a-scrollbar): If the list items cannot be fully displayed on one screen, you can place the child elements in different components and employ a scrollbar to display them. Use the **scrollBar** attribute to set the scrollbar status and the **edgeEffect** attribute to set the rebound effect when the scrollbar has reached the edge.
- Use a **\<Scroll>** component: When one screen is not able to accommodate the full content, you can wrap a **\<Scroll>** component at the outer layer of the **\<Column>** or **\<Row>** component to implement.
Example of using a **\<Scroll>** component in the vertical layout:
```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) // The scrollbar is always displayed.
.scrollBarColor(Color.Gray) // The scrollbar color is gray.
.scrollBarWidth(10) // The scrollbar width is 10.
.edgeEffect(EdgeEffect.Spring) // The spring effect is produced when the scrollbar has reached the edge.
}
}
```
![en-us_image_0000001511900524](figures/en-us_image_0000001511900524.gif)
Example of using a **\<Scroll>** component in the horizontal layout:
```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 })
})
}.height('100%')
}
.backgroundColor(0xDCDCDC)
.scrollable(ScrollDirection.Horizontal) // Horizontal scrolling.
.scrollBar(BarState.On) // The scrollbar is always displayed.
.scrollBarColor(Color.Gray) // The scrollbar color is gray.
.scrollBarWidth(10) // The scrollbar width is 10.
.edgeEffect(EdgeEffect.Spring) // The spring effect is produced when the scrollbar has reached the edge.
}
}
```
![en-us_image_0000001562940609](figures/en-us_image_0000001562940609.gif)
# 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&nbsp;and&nbsp;(device-type:&nbsp;wearable)&nbsp;and&nbsp;(max-height:&nbsp;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&nbsp;screen&nbsp;and&nbsp;(min-height:&nbsp;50)&nbsp;and&nbsp;(max-height:&nbsp;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&nbsp;and&nbsp;(min-height:&nbsp;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 (,&nbsp;) | 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)
# Layout Update Animation
[Attribute animation](../reference/arkui-ts/ts-animatorproperty.md) (animation) and [explicit animation](../reference/arkui-ts/ts-explicit-animation.md) (animateTo) are the most basic and common animation features provided by ArkUI. When the layout attributes (such as the [size](../reference/arkui-ts/ts-universal-attributes-size.md) and [position](../reference/arkui-ts/ts-universal-attributes-location.md)) attributes change, you can use the attribute animation or explicit animation to transit to the new layout parameter status based on the animation parameters.
| Animation Type| Description | Application Scenario |
| ---- | ---------------------------------------- | -------- |
| Attribute animation| The animation setting is simple. The animation is automatically triggered when the attribute changes. | Simple animation scenario|
| Explicit animation| Changes in a closure trigger animations, including component addition and deletion caused by data changes and component attribute changes. Complex animations can be performed.| Complex animation scenarios|
## Using Explicit Animation to Generate Layout Update Animation
The API for explicit animation is as follows:
```ts
animateTo(value: AnimateParam, event: () => void): void
```
The first parameter specifies the animation parameter, and the second parameter is the closure function of the animation.
The following is an example of using explicit animation to produce a layout update animation. In the example, after the alignItems attribute of the Column component is changed, the layout of its child components changes. As long as the attribute is modified in the closure function of **animateTo**, all changes caused by the attribute are performed to the end value according to the animation parameter of animateTo.
```ts
@Entry
@Component
struct LayoutChange {
// Used to control the alignItems attribute of a column.
@State itemAlign: HorizontalAlign = HorizontalAlign.Start;
allAlign: HorizontalAlign[] = [HorizontalAlign.Start, HorizontalAlign.Center, HorizontalAlign.End];
alignIndex: number = 0;
build() {
Column() {
Column({ space: 10 }) {
Button("1").width(100).height(50)
Button("2").width(100).height(50)
Button("3").width(100).height(50)
}
.margin(20)
.alignItems(this.itemAlign)
.borderWidth(2)
.width("90%")
.height(200)
Button("click").onClick(() => {
// The animation duration is 1000 ms, and the curve is EaseInOut.
animateTo({ duration: 1000, curve: Curve.EaseInOut }, () => {
this.alignIndex = (this.alignIndex + 1) % this.allAlign.length;
// Modify the this.itemAlign parameter in the closure function to change the layout of children in the Column container. Use an animation to transition to the new position.
this.itemAlign = this.allAlign[this.alignIndex];
});
})
}
.width("100%")
.height("100%")
}
}
```
![layoutChange1](figures/layoutChange1.gif)
In addition to directly changing the layout mode, you can also directly change the width, height, and position of a component.
```ts
@Entry
@Component
struct LayoutChange2 {
@State myWidth: number = 100;
@State myHeight: number = 50;
// Flag. true and false correspond to a group of myWidth and myHeight values respectively.
@State flag: boolean = false;
build() {
Column({ space: 10 }) {
Button("text")
.type(ButtonType.Normal)
.width(this.myWidth)
.height(this.myHeight)
.margin(20)
Button("area: click me")
.fontSize(12)
.margin(20)
.onClick(() => {
animateTo({ duration: 1000, curve: Curve.Ease }, () => {
// In the animation closure, the state variable that controls the width and height of the first button is changed based on the flag bit so that the width and height of the first button are animated.
if (this.flag) {
this.myWidth = 100;
this.myHeight = 50;
} else {
this.myWidth = 200;
this.myHeight = 100;
}
this.flag = !this.flag;
});
})
}
.width("100%")
.height("100%")
}
}
```
In the click event of the second button, the **animateTo** API is used to modify the **this.myWidth** and **this.myHeight** state variables in the closure. The two state variables are the width and height attribute values of the first button. Therefore, the width and height animation is performed for the first button. The display effect is shown below.
![layoutChange2_animateTo](figures/layoutChange2_animateTo.gif)
At the same time, the second button also produces a position animation. After the width and height of the first button are changed, the layout of other components in the column is also changed. The layout of the second button is also changed because the width and height of the first button are changed in the closure.
If you do not want the second button to have an animation effect, you can use either of the following methods: 1. Add a container outside the first button so that the sizes before and after the animation are within the range of the container. In this way, the position of the second button is not affected by the position of the first button. The modified key code is as follows:
```ts
Column({ space: 10 }) {
Column() {
// The button is placed in a container that is large enough so that it does not affect the position of the outer component.
Button("text")
.type(ButtonType.Normal)
.width(this.myWidth)
.height(this.myHeight)
}
.margin(20)
.width(200)
.height(100)
Button("area: click me")
.fontSize(12)
.onClick(() => {
animateTo({ duration: 1000, curve: Curve.Ease }, () => {
// In the animation closure, the state variable that controls the width and height of the first button is changed based on the flag bit so that the width and height of the first button are animated.
if (this.flag) {
this.myWidth = 100;
this.myHeight = 50;
} else {
this.myWidth = 200;
this.myHeight = 100;
}
this.flag = !this.flag;
});
})
}
.width("100%")
.height("100%")
```
![layoutChange2_animateTo_change](figures/layoutChange2_animateTo_change.gif)
2. Add layout constraints to the second button, for example, position constraints, so that the position of the second button is not affected by the width and height of the first button. Sample code:
```ts
Column({ space: 10 }) {
Button("text")
.type(ButtonType.Normal)
.width(this.myWidth)
.height(this.myHeight)
.margin(20)
Button("area: click me")
.fontSize(12)
// Set the position attribute to a fixed value so that the layout position is not affected by the width and height of the first button.
.position({ x: "30%", y: 200 })
.onClick(() => {
animateTo({ duration: 1000, curve: Curve.Ease }, () => {
// In the animation closure, the state variable that controls the width and height of the first button is changed based on the flag bit so that the width and height of the first button are animated.
if (this.flag) {
this.myWidth = 100;
this.myHeight = 50;
} else {
this.myWidth = 200;
this.myHeight = 100;
}
this.flag = !this.flag;
});
})
}
.width("100%")
.height("100%")
```
## Using Attribute Animation to Generate Layout Update Animation
The explicit animation places the modification of the attribute of the animation to be executed in the closure function to trigger the animation. The attribute animation does not need to use the closure. You only need to add the animation attribute to the end of the attribute of the component to be executed.
The API of the attribute animation is as follows:
```ts
animation(value: AnimateParam)
```
The input parameter is an animation parameter. If you want the component to generate an animation with the change of an attribute value, you need to add this attribute before the animation attribute. Some attribute changes do not want to generate attribute animations through animations. Therefore, attribute animations can be placed after animations. The example of explicit animation above is easily changed to be implemented with property animation. Sample code:
```ts
@Entry
@Component
struct LayoutChange2 {
@State myWidth: number = 100;
@State myHeight: number = 50;
@State flag: boolean = false;
@State myColor: Color = Color.Blue;
build() {
Column({ space: 10 }) {
Button("text")
.type(ButtonType.Normal)
.width(this.myWidth)
.height(this.myHeight)
// Animation takes effect only for the type, width, and height attributes. The duration is 1000 ms, and the curve is Ease.
.animation({ duration: 1000, curve: Curve.Ease })
.backgroundColor(this.myColor)
.margin(20)
// Animation does not take effect for the backgroundColor and margin attributes.
Button("area: click me")
.fontSize(12)
.onClick(() => {
// Change the attribute value. Animation transition is performed for attributes configured with attribute animation.
if (this.flag) {
this.myWidth = 100;
this.myHeight = 50;
this.myColor = Color.Blue;
} else {
this.myWidth = 200;
this.myHeight = 100;
this.myColor = Color.Pink;
}
this.flag = !this.flag;
})
}
}
}
```
In the preceding example, the animation attribute of the first button takes effect only for the type, width, and height attributes written before the animation, but does not take effect for the backgroundColor and margin attributes written after the animation. In the running result, the width and height attributes execute the animation based on the animation parameters. However, the backgroundColor directly jumps and no animation is generated. The display effect is shown below.
![size-change-animation](figures/size-change-animation.gif)
>**NOTE**
>
> 1. When the attribute animation is used, the animation is executed according to the specified attribute animation parameters. Each component can configure attribute animations with different parameters for its own attributes.
>
> 2. Explicit animations are executed on all GUI differences caused before and after animation closures, and the same animation parameters are used. Therefore, explicit animations are applicable to scenarios where animations are executed in a unified manner. In addition, explicit animations can also be used for animations caused by non-attribute variables, such as if/else conditions and deletion of array elements used by Forach.
>
> 3. If an attribute animation is configured for an attribute and the attribute value is changed in the explicit animation closure, the attribute animation takes effect first and the animation parameters of the attribute animation are used.
# 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 properties 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. By default, NavRouter provides click response processing. Developers do not need to customize click event logic. 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 a developer 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** Layout of a single page
![en-us_image_0000001511740532](figures/en-us_image_0000001511740532.png)
Set mode to NavigationMode.Stack so that the Navigation component can be displayed on a single page.
```ts
Navigation() {
...
}
.mode(NavigationMode.Stack)
```
![Single Page 1] (figures /Single Page 1.jpg)
- Column Mode
**Figure 2** Column layout
![en-us_image_0000001562820845](figures/en-us_image_0000001562820845.png)
Set mode to NavigationMode.Split. 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 property to set the title bar mode.
- Mini mode
Common title bar, which is used 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
Emphasis title bar, which is used 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 Menu Bar
The menu bar is in the upper right corner of the Navigation component. Developers can set the menu bar through the menus property. The menus supports two parameter types: Array&lt;[NavigationMenuItem](../reference/arkui-ts/ts-basic-components-navigation.md#navigationmenuitem %E7%B1%BB %E5%9E %8B %E8%AF %B4%E6%98%8E)&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. Developers can set the toolbar by setting the toolbar properties.
**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. TabContent can be classified into bottom navigation, top navigation, and side navigation. The navigation bar is located at the bottom, top, and side, 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 TabContent, as shown in Figure 2. TabContent displays the corresponding content page.
Figure 2 Using Tabs and TabContent
![tabs-tabscontent](figures/tabs-tabscontent.png)
The content corresponding to each TabContent must have a tab page, which can be configured through the tabBar attribute of TabContent. You can set the tabBar attribute on the TabContent component to set the content on the corresponding tab page. The tabBar functions as the content tab page.
```ts
TabContent() {
Text('Homepage content').fontSize(30)
}
.tabBar ('Home')
```
When setting multiple contents, place them in sequence in Tabs.
```ts
Tabs() {
TabContent() {
Text('Homepage content').fontSize(30)
}
.tabBar ('Home')
TabContent() {
Text('Recommendation').fontSize(30)
}
.tabBar ('Recommend')
TabContent() {
Text ('Discovered content').fontSize (30)
}
.tabBar ('Discovery')
TabContent() {
Text ('My Content').fontSize (30)
}
.tabBar ("My")
}
```
## Bottom navigation
Bottom navigation is the most common navigation mode in applications. The bottom navigation is located at the bottom of the level-1 page of the application. When the user opens the application, the function classification of the entire application and the content corresponding to the tab can be distinguished. In addition, the bottom navigation is located at the bottom of the application, which facilitates one-hand operations of the user. The bottom navigation generally exists as a main navigation form of an application, and a function of the bottom navigation is to classify content that a user cares about according to a function, cater to a use habit of the user, and facilitate content switching between different modules.
Figure 3 Navigation bar at the bottom
![Bottom Navigation](figures/Bottom Navigation.gif)
The barPosition parameter of Tabs is used to set the position of the navigation bar. By default, the navigation bar is on the top, and the default value of the barPosition parameter is Start. To set the bottom navigation, you need to transfer parameters in Tabs. Set barPosition to End.
```ts
Tabs({ barPosition: BarPosition.End }) {
// TabContent: home page, discovery, recommendation, and my
...
}
```
## Top Navigation
When there are many content categories, the probability that users browse different content is similar, and users need to quickly switch between different content categories, the top navigation mode is generally used to further divide the bottom navigation content. Common information applications classify content into attention, video, and digital, alternatively, in a theme application, a theme is further divided into a picture, a video, a font, and the like.
Figure 4 Navigation bar on the top
![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: follow, video, game, digital, technology, sports, and movie
...
}
```
## Side Navigation
Side navigation is a navigation mode that is seldom used by applications. It is more applicable to landscape screens and is used to perform navigation operations on applications. Because the visual habit of the user is from left to right, the side navigation bar is the left sidebar by default.
Figure 5 Side navigation bar
![Side Navigation](figures/Side Navigation.png)
To implement the side navigation bar, you need to set the vertical attribute of Tabs to true. In the bottom navigation and top navigation implementations, the default value is false, indicating that the content page and navigation bar are arranged vertically.
```ts
Tabs({ barPosition: BarPosition.Start }) {
// TabContent: home page, discovery, recommendation, and my
...
}
.vertical(true)
.barWidth(100)
.barHeight(200)
```
>**NOTE**
>
> - When vertical is set to true, the tabbar width fully occupies the screen width by default. You need to set barWidth to a proper value.
>
> - If vertical is set to true, the tabbar height is the actual content height by default. You need to set barHeight to a proper value.
## Restricting the Slide Switch of the Navigation Bar
By default, the navigation bar supports sliding. On some pages that require multi-level classification of content information, for example, when the bottom navigation and top navigation are supported, the sliding effect of the bottom navigation bar conflicts with that of the top navigation. In this case, the sliding of the bottom navigation bar needs to be restricted, this prevents poor user experience.
Figure 6 Restricting the sliding of the navigation bar at the bottom
![Restricted Navigation](figures/Restricted Navigation.gif)
The attribute that controls the sliding switch is scrollable. The default value is true, indicating that the sliding switch is allowed. To restrict the sliding switch, set this parameter 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, Recommend, and My
...
}
.scrollable(false)
```
## Fixed navigation bar
When the content categories are relatively fixed and not scalable, for example, the bottom navigation content categories are generally fixed, and the number of categories ranges from 3 to 5, the fixed navigation bar is used. The fixed navigation bar cannot be scrolled or dragged. The content is evenly divided into the width of the tab bar.
Figure 7 Fixed navigation bar
![Fixed Navigation](figures/Fixed Navigation.gif)
The barMode attribute of Tabs specifies whether the navigation bar can be scrolled. The default value is Fixed.
```ts
Tabs({ barPosition: BarPosition.End }) {
// TabContent: home page, discovery, recommendation, and my
...
}
.barMode(BarMode.Fixed)
```
## Scrolling Navigation Bar
The scrolling navigation bar can be used to set the top navigation bar or side navigation bar. If there are many content categories and the screen width cannot accommodate all category tabs, a scrollable navigation bar is required. Users can tap and slide to load hidden tab content.
Figure 8 Scrollable navigation bar
![Scrolling Navigation](figures/Scrolling Navigation.gif)
To scroll the navigation bar, you need to set the barMode attribute of the Tabs component. The default value is Fixed, indicating that the navigation bar is fixed. To scroll the navigation bar, set the barMode attribute 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 navigation bar at the bottom is generally used as the main page of the application. For better user experience, text and corresponding semantic icons are combined to indicate the tab content. In this case, you need to customize the style of the navigation tab.
Figure 9 Customizing the navigation bar
![custom-navigation-bar](figures/custom-navigation-bar.png)
By default, the system uses underscores (_) to mark active tabs. The customized navigation bar needs to implement the corresponding style to distinguish active tabs from inactive tabs.
To customize the navigation bar, you need to use the tabBar parameters and transfer the customized function component style in CustomBuilder mode supported by the tabBar. For example, declare the customized function component of the TabBuilder. The input parameters include the tab text title, corresponding position index, and image resources in the selected and unselected states. The UI display style is determined based on whether the active currentIndex matches the targetIndex corresponding to 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)
}
```
Transfer the customized function component to the tabBar attribute corresponding to TabContent and transfer the corresponding parameters.
```ts
TabContent() {
Column(){
Text('My Content')
}
.width('100%')
.height('100%')
.backgroundColor('#007DFF')
}
.tabBar(this.TabBuilder('My', 0, $r('app.media.mine_selected'), $r('app.media.mine_normal')))
```
## Switch to a specified tab page.
If the customized navigation bar is not used, the default tabs implement the switching logic. After the customized navigation bar is used, the logic for switching tab pages needs to be manually implemented. That is, when the user taps a corresponding tab, the screen needs to display a corresponding content page.
Figure 10 Using the customized navigation bar to switch to a specified tab page
![Switching to a Specified Tab Page](figures/Switching to a Specified Tab Page.gif)
To switch a specified tab page, you need to use the TabsController. The TabsController is the controller of the Tabs component and is used to control the Tabs component to switch tab pages. The changeIndex method of TabsController is used to jump to the TabContent content corresponding to a specified index value.
```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 the customized navigation bar, transfer 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 ('Found', 1))
TabContent(){
...
}.tabBar (this.TabBuilder ('recommend', 2))
TabContent(){
...
}
.tabBar (this.TabBuilder (' My',3))
}
```
## Swipe to switch the navigation bar.
If the customized navigation bar is not used, the Tabs implements the switchover association between the TabBar and TabContent by default. However, after the customized navigation bar is used, the TabsController can be used to implement the association between the clicked tab and the page content, but cannot implement the association between the page content and the tab when the page is sliding. That is, when a subscriber swipes on the screen to switch page content, the tab bar needs to be switched to the tab corresponding to the content.
Figure 11 Tab page content is not associated when you slide to switch between tab pages
![Final effect 11] (figures / Final effect 11.gif)
In this case, you need to use the onChange event method provided by Tabs to listen to the index change and transfer the active index value to currentIndex to switch the tab content.
```ts
Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
TabContent() {
...
}.tabBar (this.TabBuilder ('Home', 0))
TabContent() {
...
}.tabBar (this.TabBuilder ('Found', 1))
TabContent() {
...
}.tabBar (this.TabBuilder ('Recommend', 2))
TabContent() {
...
}
.tabBar (this.TabBuilder ('My', 3))
}.onChange((index) => {
this.currentIndex = index
})
```
Figure 12 Linkage between content and tabs
![Final Effect] (figures / Final Effect.gif)
## Samples
For details about the implementation of tabs, see the following example:
- [Healthy Life] (https://gitee.com/openharmony/codelabs/tree/master/ETSUI/Healthy_life)
- [Common Components and Layout] (https://gitee.com/openharmony/codelabs/tree/master/ETSUI/ArkTSComponents)
# Page Transition Animation
During page redirection, one page disappears and the other page appears. In this case, you can configure the page transition parameters of each page to customize the page transition effect. [Page transition](../reference/arkui-ts/ts-page-transition-animation.md) effect is written in the **pageTransition** API. PageTransitionEnter and PageTransitionExit are used to specify the animation effect of page entry and exit.
PageTransitionEnter's interface is:
```ts
PageTransitionEnter({type?: RouteType,duration?: number,curve?: Curve | string,delay?: number})
```
The interface of PageTransitionExit is:
```ts
PageTransitionExit({type?: RouteType,duration?: number,curve?: Curve | string,delay?: number})
```
Defines the PageTransitionEnter and PageTransitionExit components. The slide, translate, scale, and opacity attributes can be used to define different page transition effects. For PageTransitionEnter, these effects indicate the start value during entry. For PageTransitionExit, these effects indicate the end value during exit. This method is similar to that for configuring component transition. In addition, PageTransitionEnter provides the onEnter interface to call back the entry animation of a customized page, and PageTransitionExit provides the onExit interface to call back the exit animation of a customized page.
The type parameter in the preceding interface indicates the type of the route that takes effect, which may be confused by developers. One of the two pages must exit and the other must enter. If you switch from page A to page B through the router.pushUrl operation, page A exits and the exit animation is displayed. Page B enters and the entry animation is displayed. If the router.back operation is performed to return from page B to page A, page B exits and the exit animation is displayed. Page A enters and the entry animation is displayed. That is, PageTransitionEnter of a page may be an entry animation of a new page caused by a new page (push, stack), or an entry animation of an old page in a page stack caused by a page return (back, pop, stack). To distinguish the two types of entry animations, the type parameter is provided so that developers can define all types of page transition effects.
## Set type to RouteType.None.
If the value of type is RouteType.None, the push and pop operations on the page stack are valid. The default value of type is RouteType.None.
```ts
// page A
pageTransition() {
// Define the effect when the page is displayed. The page slides in from the left for 1200 ms. The effect takes effect no matter whether the push or pop operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.None, duration: 1200 })
.slide(SlideEffect.Left)
// Define the effect when the page exits. The page slides out to the left for 1000 ms. The effect takes 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
// page B
pageTransition() {
// Define the effect when the page is displayed. The page slides in from the right for 1000 ms. The effect takes effect no matter whether the push or pop operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.None, duration: 1000 })
.slide(SlideEffect.Right)
// Define the effect when the page exits. The page slides out to the right for 1200 ms. The effect takes 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 standard 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 operations | Transition effect of page A | Transition effect of page B |
| ---------------------------- | ---------------------------------- | ---------------------------------- |
| router.pushUrl. The new page B is displayed from page A.| Exit the page. The PageTransitionExit takes effect and slides to the left to display the screen. | The page is displayed. PageTransitionEnter takes effect and slides from the right to the screen.|
| router.back: Return from page B to page A. | The page is displayed, PageTransitionEnter takes effect, and you can slide from the left to the screen.| Exit the page. The PageTransitionExit takes effect and slides out of the screen to the right. |
| router.pushUrl. The new page A is displayed from page B.| The page is displayed, PageTransitionEnter takes effect, and you can slide from the left to the screen.| Exit the page. The PageTransitionExit takes effect and slides out of the screen to the right. |
| router.back: Return from page A to page B. | Exit the page. The PageTransitionExit takes effect and slides to the left to display the screen. | The page is displayed. PageTransitionEnter takes effect and slides from the right to the screen.|
If you want the page accessed by pushUrl to always slide in from the right and the page exited by 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 the transition effects of the four pages.
## Set type to RouteType.Push or RouteType.Pop.
If type is set to RouteType.Push, this parameter is valid only for the push operation of the page stack. If type is set to RouteType.Pop, this parameter is valid only for the pop operation of the page stack.
```ts
// page A
pageTransition() {
// Define the effect when the page is entered. The page slides in from the right for 1200 ms. The effect takes effect only when the push operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Push, duration: 1200 })
.slide(SlideEffect.Right)
// Define the effect when the page is entered. The page slides in from the left for 1200 ms. The effect takes effect only when the pop operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Pop, duration: 1200 })
.slide(SlideEffect.Left)
// Define the effect when the page exits. The page slides out to the left for 1000 ms. The effect takes effect only when the push operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Left)
// Define the effect when the page exits. The page slides out to the right for 1000 ms. The effect takes effect only when the pop operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Right)
}
```
```ts
// page B
pageTransition() {
// Define the effect when the page is entered. The page slides in from the right for 1000 ms. The effect takes effect only when the push operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Right)
//Define the effect when the page is entered. The page slides in from the left for 1000 ms. The effect takes effect only when the pop operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Left)
// Define the effect when the page exits. The page slides out to the left for 1200 ms. The effect takes effect only when the push operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Push, duration: 1200 })
.slide(SlideEffect.Left)
// Define the effect when the page exits. The page slides out to the right for 1200 ms. The effect takes effect only when the pop operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Pop, duration: 1200 })
.slide(SlideEffect.Right)
}
```
The preceding code completely defines all possible page transition styles. Assume that the page stack is in the standard 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 operations | Transition effect of page A | Transition effect of page B |
| ---------------------------- | ---------------------------------------- | ---------------------------------------- |
| router.pushUrl. The new page B is displayed from page A.| The page exits. The transition style of PageTransitionExit whose type is RouteType.Push takes effect. The screen slides out to the left.| The page is displayed. The transition style of PageTransitionEnter whose type is RouteType.Push takes effect. The page slides from the right to the screen.|
| router.back: Return from page B to page A. | The page is displayed. The transition style of PageTransitionEnter whose type is RouteType.Pop takes effect. Slide from the left to the screen.| The page exits. The transition style of PageTransitionExit whose type is RouteType.Pop takes effect. The screen slides out to the right.|
| router.pushUrl. The new page A is displayed from page B.| The page is displayed. The transition style of PageTransitionEnter whose type is RouteType.Push takes effect. The page slides from the right to the screen.| The page exits. The transition style of PageTransitionExit whose type is RouteType.Push takes effect. The screen slides out to the left.|
| router.back: Return from page A to page B. | The page exits. The transition style of PageTransitionExit whose type is RouteType.Pop takes effect. The screen slides out to the right.| The page is displayed. The transition style of PageTransitionEnter whose type is RouteType.Pop takes effect. Slide from the left to the screen.|
>**NOTE**
>
> 1. The transition style of each page can be independently configured by developers. However, the transition involves two pages. Developers need to consider the transition effect of the two pages, for example, the transition duration.
>
> 2. If no matching page transition style is defined, the page uses the default page transition style.
## Disable the transition of a page.
```ts
pageTransition() {
PageTransitionEnter({ type: RouteType.None, duration: 0 })
PageTransitionExit({ type: RouteType.None, duration: 0 })
}
```
You can set the page transition duration to 0 so that no page transition animation is displayed on the page.
## Example Scenario
The following describes an example of a page transition animation that defines all four page transition styles.
```ts
// page A
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(() => {
//Route to the next page and perform the 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() {
//Define the effect when the page is entered. The page slides in from the right for 1000 ms. The effect takes effect only when the push operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Right)
//Define the effect when the page is entered. The page slides in from the left for 1000 ms. The effect takes effect only when the pop operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Left)
//Define the effect when the page exits. The page slides out to the left for 1000 ms. The effect takes effect only when the push operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Left)
//Define the effect when the page exits. The page slides out to the right for 1000 ms. The effect takes effect only when the pop operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Right)
}
}
```
```ts
// page B
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(() => {
//Route to the next page and perform the 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() {
//Define the effect when the page is entered. The page slides in from the right for 1000 ms. The effect takes effect only when the push operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Right)
//Define the effect when the page is entered. The page slides in from the left for 1000 ms. The effect takes effect only when the pop operation is performed on the page stack.
PageTransitionEnter({ type: RouteType.Pop, duration: 1000 })
.slide(SlideEffect.Left)
//Define the effect when the page exits. The page slides out to the left for 1000 ms. The effect takes effect only when the push operation is performed on the page stack.
PageTransitionExit({ type: RouteType.Push, duration: 1000 })
.slide(SlideEffect.Left)
//Define the effect when the page exits. The page slides out to the right for 1000 ms. The effect takes 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)
The following describes an example of the page transition animation whose type is None.
```ts
// page A
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(() => {
//Route to the next page and perform the 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() {
//Define the effect when the page is displayed. The page slides in from the left for 1000 ms. The effect takes effect no matter whether the push or pop operation is performed on the page stack.
PageTransitionEnter({ duration: 1000 })
.slide(SlideEffect.Left)
//Define the effect when the page exits. Compared with the normal page position, the page position is shifted by 100 vp in the x direction and 100 vp in the y direction. The transparency changes to 0. The duration is 1200 ms. The effect takes effect regardless of 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
// page B
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(() => {
//Route to the next page and perform the 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() {
//Define the effect when the page is displayed. The page slides in from the left for 1200 ms. The effect takes effect no matter whether the push or pop operation is performed on the page stack.
PageTransitionEnter({ duration: 1200 })
.slide(SlideEffect.Left)
//Define the effect when the page exits. Compared with the normal page position, the page position is shifted by 100 vp in the x direction and 100 vp in the y direction. The transparency changes to 0. The duration is 1000 ms. The effect takes effect regardless of 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)
# Recommendations for Improving Performance
If developers use low-performance code to implement functions, the normal running of applications may not be affected, but the performance of applications may be adversely affected. This section lists some performance improvement scenarios for developers to avoid performance deterioration caused by application implementation.
## Lazy data loading is recommended.
If developers directly use the cyclic rendering mode when using a long list, as shown in the following figure, all list elements are loaded at a time. On one hand, the page startup takes a long time, affecting user experience. On the other hand, the pressure and traffic on the server increase, increasing the system load.
```ts
@Entry
@Component
struct MyComponent {
The @State arr: number[] = Array.from(Array(100), (v,k) =>k); // constructs an array ranging from 0 to 99 and from 0 to 99.
build() {
List() {
ForEach(this.arr, (item: number) => {
ListItem() {
Text(`item value: ${item}`)
}
}, (item: number) => item.toString())
}
}
}
```
The preceding code loads all 100 list elements during page loading, which is not required. We want to iteratively load data from the data source and create corresponding components. Therefore, data lazy loading is required, as shown in the following figure.
```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 and loads only three list elements during page loading. Each time a list element is clicked, a list element is added.
## Use conditional rendering to replace display and hiding control.
As shown in the following figure, when developers use the visibility common attribute to control the display/hide status of a component, the component still needs to be re-created, causing performance loss.
```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)
## Replacing Flex with Column/Row
By default, the Flex container component has shrink, which causes secondary layout. As a result, the page rendering performance deteriorates to some extent.
```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)
}
}
}
```
In the preceding code, Flex can be replaced with Column and Row to avoid negative impacts caused by Flex secondary layout while ensuring that the page layout effect is the same.
```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)
## Sets the width and height of the List component.
When developers use the Scroll container component to embed the List child component, if the width and height of the List child component are not specified, all child components are loaded by default, as shown in the following figure.
```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 this scenario, you are advised to set the width and height of the List child 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)
## Reduce application sliding white blocks.
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 items preloaded in the off-screen List/Grid.
If you need to request a network image, you can download the content in advance before the item slides to the screen to reduce the number of white blocks.
The following is an example of using the cachedCount parameter:
```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 value. The appear log range will be enlarged.
}
}
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.
# Menu
Menu is a menu interface, which is generally used for right-click pop-up windows and click pop-up windows. For details, see [Menu Control] (../reference/arkui-ts/ts-universal-attributes-menu.md).
## Creating a Menu in the Default Style
The bindMenu interface needs to be called to implement menus. The bindMenu responds to the tap event of the bound widget. After the widget is bound, the widget is displayed after you tap the corresponding widget using gestures.
```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 with a Customized Style
If the default style does not meet development requirements, you can use \@CustomBuilder to customize menu content. Menus can be customized through the bindContextMenu interface.
### \@Builder Develop the content in the menu.
```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 menuItem. When you hover the cursor over the menu item, the submenu is displayed.
builder: this.SubMenu.bind(this),
})
MenuItemGroup ({ header: 'Subtitle' }) {
MenuItem ({ content: "MenuOptions" })
.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")
})
}
}
```
### Binding the bindMenu Attribute to a Component
```ts
Button('click for Menu')
.bindMenu(this.MyMenu)
```
![en-us_image_0000001511580924](figures/en-us_image_0000001511580924.png)
## Create a menu that supports right-click or touch and hold.
The bindContextMenu interface is used to customize menus and trigger menu pop-up by right-clicking or pressing and holding. The menu items that are displayed using bindContextMenu are in an independent child window and can be displayed outside the application window.
- [@Builder development menu content] (#builder development menu content) is the same as the preceding content.
- Confirm the menu pop-up mode and use the bindContextMenu property to bind the component. In the example, the shortcut menu is displayed.
```ts
Button('click for Menu')
.bindContextMenu(this.MyMenu, ResponseType.RightClick)
```
# Bubble
You can bind the **Popup** attribute to a component, specifying its content, interaction logic, and display status. It is mainly used for screen recording and message pop-up notification.
Bubbles are classified into two types: built-in bubbles with [PopupOptions](../reference/arkui-ts/ts-universal-attributes-popup.md#popupoptions) and custom bubbles with [CustomPopupOptions](../reference/arkui-ts/ts-universal-attributes-popup.md#custompopupoptions8). For PopupOptions, you can set primaryButton and secondaryButton to set bubbles with buttons. CustomPopupOptions sets the customized bubble by configuring the [builder](../quick-start/arkts-builder.md) parameter.
## Text prompt bubble
Text pop-up messages are usually used to display only text messages without any interaction. The Popup attribute needs to be bound to a component. When the show parameter in the bindPopup attribute is set to true, a pop-up message is displayed.
Bind the Popup attribute to the Button component. Each time the Button button is clicked, handlePopup switches the Boolean value. When handlePopup is set to true, bindPopup pops up.
```ts
@Entry
@Component
struct PopupExample {
@State handlePopup: boolean = false
build() {
Column() {
Button('PopupOptions')
.onClick(() => {
this.handlePopup = !this.handlePopup
})
.bindPopup(this.handlePopup, {
message: 'This is a popup with PopupOptions',
})
}.width('100%').padding({ top: 5 })
}
}
```
![en-us_image_0000001511740524](figures/en-us_image_0000001511740524.png)
## Bubble with a button
A maximum of two buttons can be set for a bubble through the primaryButton and secondaryButton attributes for simple interaction. Developers can set the action parameter to specify the operation to be triggered.
```ts
@Entry
@Component
struct PopupExample22 {
@State handlePopup: boolean = false
build() {
Column() {
Button('PopupOptions').margin({top:200})
.onClick(() => {
this.handlePopup = !this.handlePopup
})
.bindPopup(this.handlePopup, {
message: 'This is a popup with PopupOptions',
primaryButton:{
value:'Confirm',
action: () => {
this.handlePopup = !this.handlePopup
console.info('confirm Button click')
}
},
secondaryButton: {
value: 'Cancel',
action: () => {
this.handlePopup = !this.handlePopup
}
},
})
}.width('100%').padding({ top: 5 })
}
}
```
![en-us_other_0000001500740342](figures/en-us_other_0000001500740342.jpeg)
## Custom Bubbles
Developers can use the builder CustomPopupOptions to create customized bubbles. \@Builder can store customized content. In addition, parameters such as popupColor can be used to control the bubble style.
```ts
@Entry
@Component
struct Index {
@State customPopup: boolean = false
// The popup constructor defines the dialog box content.
@Builder popupBuilder() {
Row({ space: 2 }) {
Image($r("app.media.icon")).width(24).height(24).margin({ left: 5 })
Text('This is Custom Popup').fontSize(15)
}.width(200).height(50).padding(5)
}
build() {
Column() {
Button('CustomPopupOptions')
.position({x:100,y:200})
.onClick(() => {
this.customPopup = !this.customPopup
})
.bindPopup(this.customPopup, {
builder: this.popupBuilder, // Content of the bubble
placement:Placement.Bottom, // Pop-up position of the bubble
popupColor:Color.Pink // Background color of the bubble
})
}
.height('100%')
}
}
```
You can set the placement parameter to place the pop-up bubble in the required position. The pop-up window constructor triggers a pop-up message to guide the user to complete the operation, providing better UI experience for the user.
![en-us_other_0000001500900234](figures/en-us_other_0000001500900234.jpeg)
```ts
@Entry
@Component
struct Index {
@State customPopup: boolean = false
// The popup constructor defines the dialog box content.
@Builder popupBuilder() {
Row({ space: 2 }) {
Image('/images/shengWhite.png').width(30).objectFit(ImageFit.Contain)
Column(){
Text('Control Life') .fontSize(14).fontWeight(900).fontColor(Color.White).width('100%')
Text('When you want to sing, tens of millions of songs can be selected and the voice can be adjusted.').fontSize(12).fontColor('#ffeeeeee').width('100%')
}
}.width(230).height(80).padding(5)
}
build() {
Row() {
Text ('I would like to sing a song')
Image('/images/sheng.png').width(35).objectFit(ImageFit.Contain)
.onClick(() => {
this.customPopup = !this.customPopup
})
.bindPopup(this.customPopup, {
builder: this.popupBuilder,
})
}
.margin(20)
.height('100%')
}
}
```
# Page Routing
Page routing refers to the jump and data transfer between different pages in an application. The OpenHarmony provides the Router module. Through different URLs, you can easily route pages and access different pages. This document describes the functions provided by the Router module from the following aspects: [Page Jump] (#Page Jump), [Page Return] (#Page Return), and [Adding a Query Box Before Page Return] (#Adding a Query Box Before Page Return).
## Page redirection
Page jumping 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 jump 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 you can return to the current page by pressing the Back key or calling the [router.back()](../reference/apis/js-apis-router.md#routerback) method.
- 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 the current page cannot be returned.
>**Notes:**
>
>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) method can be called to clear the historical page stack and release the memory space.
In addition, the Router module 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 method is called, a target page is created and pushed to the top of the stack.
- Single: single-instance 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 function, you need to import the Router module 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() method and use the Standard instance mode (or omit it).
```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 personal center page is displayed. At the same time, the login page is destroyed and the application is directly exited when the page is returned. In this scenario, you can use the replaceUrl() method and use the Standard instance mode (or omit it).
```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 switch page (Theme). You want to click the theme option on the setting page to go to the theme switch page. In addition, ensure that only one theme switching page exists in the page stack at a time. When the page is returned, the setting page is displayed. In this scenario, the pushUrl() method can be used and the Single instance mode can be used.
```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 search result list page to go to the search result details page. In addition, if the result has been viewed, you do not need to create a details page. Instead, you can directly go to the existing details page. In this scenario, the replaceUrl() method can be used and the Single instance mode can be used.
```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 scenario does 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 the method 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 customized 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) method 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 function, you need to import the Router module to the code.
```ts
import router from '@ohos.router';
```
You can use any of the following methods to return to the page:
- Method 1: Return to the previous page.
```ts
router.back();
```
In this mode, the previous page is returned, that is, the position of the previous page in the page stack. However, the previous page must exist in the page stack to return. Otherwise, the method is invalid.
- Method 2: Return to the specified page.
```ts
router.back({
url: 'pages/Home'
});
```
In this mode, you can return to a specified page. You need to specify the path of the target page. The target page can be returned only when it exists 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'
}
});
```
In this mode, you can not only 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() method on the target page.
On the target page, call the router.getParams() method 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() method 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.
>
> In addition, 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 customized parameters transferred from the return page on the original page, you can parse the parameters in the required position. For example, parameter parsing is performed in the onPageShow() lifecycle callback.
## A query box is added before the page is returned.
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.
This document describes how to add a query box before returning to the page from two aspects: [System Default Query Box] (#System Default Query Box) and [Customized Query Box] (#Customized Query Box).
Figure 3 Adding a query box before returning to the page
![router-add-query-box-before-back](figures/router-add-query-box-before-back.gif)
### Default query 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) methods provided by the Router module.
Before using the Router function, you need to import the Router module to the code.
```ts
import router from '@ohos.router';
```
If you want to enable the function of returning to the query box on the target page, you need to call the [router.showAlertBeforeBackPage()](../reference/apis/js-apis-router.md#routershowalertbeforebackpage9) method to set the information about the returned query box before invoking the [router.back()](../reference/apis/js-apis-router.md#routerback) method. For example, define a click event processing function for the return button on the payment page:
```ts
//Define a click event processing function for the return button.
function onBackClick(): void {
//Invoke the router.showAlertBeforeBackPage() method to set the information about the returned query box.
try {
router.showAlertBeforeBackPage({
message: 'You have not completed the payment. Are you sure you want to return?' //Set the content of the inquiry box.
});
} catch (err) {
console.error(`Invoke showAlertBeforeBackPage failed, code is ${err.code}, message is ${err.message}`);
}
// Invoke the router.back() method to return to the previous page.
router.back();
}
```
The router.showAlertBeforeBackPage() method receives an object as a parameter. The object contains the following attributes:
- message: content of the query box. The value is of the string type.
If the interface is successfully called, the page is displayed on the target page and the query box is displayed. If the interface fails to be called, an exception is thrown and the error code and error information is obtained through err.code and err.message.
When the user clicks Back, a confirmation dialog box is displayed, asking the user whether to confirm the return. If you select Cancel, the target page of the current page is displayed. If you select OK, the router.back() method is triggered and the jump is performed based on the parameters.
### Custom Inquiry Box
You can use the pop-up window (../reference/apis/js-apis-promptAction.md#promptactionshowdialog) or customize a pop-up window. In this way, the application interface can be different from the default query box of the system, thereby improving user experience of the application. This document uses a pop-up window as an example to describe how to customize a query box.
Before using the Router function, you need to import the Router module to the code.
```ts
import router from '@ohos.router';
```
In the event callback, call the [promptAction.showDialog()](../reference/apis/js-apis-promptAction.md#promptactionshowdialog) method of the pop-up window.
```ts
function onBackClick() {
//The user-defined query dialog box is displayed.
promptAction.showDialog({
message:'You have not completed the payment. Are you sure you want to return?',
buttons: [
{
text: 'Cancel',
color: '#FF0000'
},
{
text: 'Confirm',
color: '#0099FF'
}
]
}).then((result) => {
if (result.index === 0) {
// The user clicks Cancel.
console.info('User canceled the operation.');
} else if (result.index === 1) {
//The user clicks OK.
console.info('User confirmed the operation.');
// Invoke the router.back() method 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 Back, a user-defined query box is displayed, asking the user whether to confirm the return. If you select Cancel, the target page of the current page is displayed. If you select OK, the router.back() method is triggered and the jump is performed based on the parameters.
# Spring Curve Animation
ArkUI provides [Preset Animation Curve] (../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 a 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 API 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) function is the transition effect in the component. You can define the transition effect of one or more transition styles such as translation, transparency, rotation, and scaling. This parameter must be used together with [animateTo] (arkts-layout-update-animation.md# uses explicit animation to generate layout update animation). The component transition effect can be generated only when they are used together.
## 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
Based on the ArkTS declarative development paradigm, the Ark development framework is a simplified, high-performance, and cross-device UI development framework. It provides the capabilities required for building the OpenHarmony application UI, including:
- **ArkTS**
ArkTS is a UI development language extended based on TypeScript (TS) and is a superset of TS. Extension capabilities include various decorators, custom components, and UI description mechanisms. 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. For details about the basic knowledge of ArkTS, see [Learning ArkTS] (../quick-start/arkts-get-started.md).
- **Layout**
The layout is a necessary element of the UI. It defines the position of a component on the UI. The ArkUI framework provides multiple layout modes. In addition to the basic linear layout, cascading layout, elastic layout, relative layout, and grid layout, the ArkUI framework also provides relatively complex lists, grids, and rotation.
- **Component**
Components are necessary elements of the UI and form the appearance of the UI. Components directly provided by the framework are called system components, and components defined by developers are called custom components. The built-in components of the system include buttons, option buttons, progress bars, and texts. Developers can set the rendering effect of built-in components in the system in chain invoking 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.
- Page Routing and Component Navigation
An application may contain multiple pages. Page routing can be used to switch between pages. Navigation between components may exist on a page, for example, a typical column. You can use the navigation component to implement navigation between components.
- **Graph**
The Ark development framework provides the display capability of multiple types of images and multiple customized drawing capabilities to meet developers' customized drawing requirements. It supports shape drawing, color filling, text drawing, deformation and cropping, and image embedding.
- **Animation**
Animation is one of the important elements of the UI. Excellent animation design can greatly improve user experience. The framework provides rich animation capabilities, including attribute animation, explicit animation, customized transition animation, and animation APIs in addition to built-in animation effects of components, developers can customize animation tracks by encapsulating physical models or invoking animation capability APIs.
- Interaction Event
Interaction events are necessary elements for the interaction between the UI and users. The Ark development framework provides multiple interaction events. In addition to common events such as touch events, mouse events, keyboard key events, and focus events, the Ark development framework also includes gesture events that are further recognized based on common events. The gesture event includes a single gesture such as a tap gesture, a touch and hold gesture, a drag gesture, a pinch gesture, a rotation gesture, a slide gesture, and a combined gesture event combined by using a single gesture event.
## Characteristics
- High development efficiency and good development experience
- Simple code: The UI is described in a way close to natural semantics, and you do not need to care about how the framework implements UI drawing and rendering.
- Data-driven UI change: Enables developers to focus on their own service logic processing. When the UI changes, developers do not need to compile the UI code for switching between different UIs. Instead, developers only need to compile the data that causes the UI change. The specific UI change is handed over to the framework.
- Good development experience: The GUI is also code, which improves the programming experience of developers.
- High Performance
- Declarative UI frontend and backend layering: The UI backend is constructed using the C++ language and provides basic components, layout, dynamic effects, interaction events, component status management, and rendering pipelines for the frontend.
- Language compiler and runtime optimization: unified bytecode, efficient FFI-Foreign Function Interface, AOT-Ahead Of Time, engine minimization, and type optimization.
- The ecosystem is easy to promote quickly.
Be able to leverage the mainstream language ecosystem to quickly promote the language. The language is relatively neutral and friendly. There are corresponding standard organizations that can gradually evolve.
## 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**
When the Ark language is used, the UI normal form syntax parsing capability, cross-language invoking capability, and TS high-performance running environment are provided.
- **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.
## Development process
When the UI development framework is used to develop applications, the following development process is involved: Developers can understand the UI development process of the entire application by referring to the ../quick-start/start-with-ets-stage.md example.
| Task | Introduction | Guide |
| ----------- | ----------------------------------- | ---------------------------------------- |
| Learning ArkTS | The basic syntax, state management, and rendering control scenarios of ArkTS are introduced. | - [Basic Syntax] (../quick-start/arkts-basic-syntax-overview.md)<br>- [Status Management] (../quick-start/arkts-state-management-overview.md)<br>- [Rendering Control] (../quick-start/arkts-rendering-control-overview.md)|
| Developing the Layout | This section describes several common layout modes and how to improve layout performance. | -&nbsp;[Common Layout](arkts-layout-development-overview.md)<br>-&nbsp;[Layout Performance](arkts-layout-development-performance-boost.md)|
| Add Component | This topic describes common built-in components, custom components, and GUI elements supported by APIs.| -&nbsp;[Common Components](arkts-common-components-button.md)<br>- [Customized Component] (../quick-start/arkts-create-custom-components.md)<br>-&nbsp;[Bubble and Menu](arkts-popup-and-menu-components-popup.md)|
| Setting Page Routes and Component Navigation| This topic describes how to set page routes and navigation between components. | -&nbsp;[Page Route] (arkts-routing.md)<br>-&nbsp;[Component Navigation] (arkts-navigation-navigation.md)|
| Display Chart | Describes how to display images, draw custom geometry, and draw custom graphics using the canvas. | -&nbsp; [image] (arkts-graphics-display.md)<br>-&nbsp;[geometric shape](arkts-geometric-shape-drawing.md)<br>-&nbsp;[Canvas](arkts-drawing-customization-on-canvas.md)|
| Using Animation | This topic describes the typical scenarios of using animations on components and pages. | -&nbsp;[Animation on the page](arkts-layout-update-animation.md)<br>-&nbsp; [animation between pages] (arkts-zoom-animation.md)|
| Binding Events | This topic describes the basic concepts of events and how to use common events and gesture events. | -&nbsp;[Common Event](arkts-common-events-touch-screen-event.md)<br>-&nbsp;[Gesture event](arkts-gesture-events-binding.md)|
## Samples
For details about the declarative development paradigm based on ArkTS, see the following example:
[ArkTS component set] (https://gitee.com/openharmony/applications_app_samples/tree/master/code/UI/ArkTsComponentClollection/ComponentCollection): a set of components, common methods, animations, and global methods.
# 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)
...@@ -11,7 +11,7 @@ You only need to perform operations in [Resource Files](#resource-files) and [Re ...@@ -11,7 +11,7 @@ You only need to perform operations in [Resource Files](#resource-files) and [Re
Resource files store application content in multiple languages. This framework uses JSON files to store resource definitions. Place the resource file of each locale in the i18n directory described in [File Organization](js-framework-file.md). Resource files store application content in multiple languages. This framework uses JSON files to store resource definitions. Place the resource file of each locale in the i18n directory described in [File Organization](js-framework-file.md).
Resource files should be named in _language-script-region_.json format. For example, the resource file for Hong Kong(China) in the traditional script is named zh-Hant-HK. You can omit the region, for example, zh-CN for simplified Chinese, or omit both the script and region, for example, zh for Chinese. Resource files should be named in _language-script-region_.json format. For example, the resource file for Hong Kong(China) in the traditional script is named zh-Hant-HK. You can omit the region, for example, en-US for simplified Chinese, or omit both the script and region, for example, zh for Chinese.
``` ```
......
# Overview # UI Development (JavaScript-compatible Web-like Development Paradigm) Overview
The JavaScript-compatible web-like development paradigm uses the classical three-stage programming model, in which OpenHarmony Markup Language (HML) is used for building layouts, CSS for defining styles, and JavaScript for adding processing logic. UI components are associated with data through one-way data-binding. This means that when data changes, the UI automatically updates with the new data. This development paradigm has a low learning curve for frontend web developers, allowing them to quickly transform existing web applications into ArkUI applications. It could be helpful if you are developing small- and medium-sized applications with simple UIs. The JavaScript-compatible web-like development paradigm uses the classical three-stage programming model, in which OpenHarmony Markup Language (HML) is used for building layouts, CSS for defining styles, and JavaScript for adding processing logic. UI components are associated with data through one-way data-binding. This means that when data changes, the UI automatically updates with the new data. This development paradigm has a low learning curve for frontend web developers, allowing them to quickly transform existing web applications into ArkUI applications. It could be helpful if you are developing small- and medium-sized applications with simple UIs.
......
...@@ -218,7 +218,7 @@ Use the **justifyContent** parameter to set alignment of items on the main axis. ...@@ -218,7 +218,7 @@ Use the **justifyContent** parameter to set alignment of items on the main axis.
.backgroundColor(0xAFEEEE) .backgroundColor(0xAFEEEE)
``` ```
![zh-cn_image_0000001263339461](figures/mainSpacearound.png) ![en-us_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. - **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.
...@@ -233,7 +233,7 @@ Use the **justifyContent** parameter to set alignment of items on the main axis. ...@@ -233,7 +233,7 @@ Use the **justifyContent** parameter to set alignment of items on the main axis.
.backgroundColor(0xAFEEEE) .backgroundColor(0xAFEEEE)
``` ```
![zh-cn_image_0000001263139411](figures/mainSpaceevenly.png) ![en-us_image_0000001263139411](figures/mainSpaceevenly.png)
#### Alignment on the Cross Axis #### Alignment on the Cross Axis
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册