控制頁面刷新范圍(OpenHarmony)
場景說明
在實現頁面UI時,業務方需要根據業務邏輯動態更新組件的狀態,常見的如在手機桌面長按某個App的圖標時,圖標背景色、大小等會發生變化。根據業務需要,有時我們需要觸發單個組件的狀態更新,有時需要觸發部分或全部組件的狀態更新。那么如何控制組件狀態刷新的范圍呢?本例將為大家提供一種參考方案。
效果呈現
本例最終效果如下:
運行環境
本例基于以下環境開發,開發者也可以基于其他適配的版本進行開發:
- IDE: DevEco Studio 3.1 Release
- SDK: Ohos_sdk_public 3.2.12.5(API Version 9 Release)
實現思路
ArkUI可以通過頁面的狀態數據驅動UI的更新,其UI更新機制可以通過如下表達式來體現:
UI=f(state)
用戶構建了UI模型,其中參數state代表頁面組件的運行時狀態。當state改變時,UI作為返回結果,也將進行對應的改變刷新。f作為狀態管理機制,維護組件運行時的狀態變化所帶來的UI重新渲染。組件的狀態改變可通過狀態變量進行控制。
基于上述理論,如果要控制頁面的更新范圍,我們必須要:定義準確狀態變量,并控制狀態變量影響的組件范圍。
本例中包含8個APP圖標,其中涉及5種狀態變化,按照局部刷新和全局刷新可分為:
- 局部刷新(單個卡片變化)
- 點擊卡片,卡片背景色變為紅色。
- 點擊卡片,卡片進行縮放。
- 拖拽卡片,卡片位置變化。
- 全局刷新(全部卡片變化)
- 長按某個卡片,為所有卡片添加刪除圖標。
- 點擊刪除圖標外的任意地方,刪除圖標消失。
所以處理思路為,控制局部刷新的狀態變量在子組件中定義,綁定子組件,控制全局刷新的狀態變量在父組件中進行定義,并由父組件傳遞給所有子組件。如下圖:
開發步驟
由于本例重點講解刷新區域的控制,所以開發步驟會著重講解相關實現,不相關的內容不做介紹,全量代碼可參考完整代碼章節。
創建APP卡片組件作為子組件,每個卡片包含文本和刪除圖標。
具體代碼如下:
@Component
export struct AppItem {
...
build() {
Stack({ alignContent: Alignment.TopEnd }) {
Image($r('app.media.ic_public_close'))
.height(30)
.width(30)
.zIndex(2)
.offset({
x: -12,
y: 12
})
Text(this.data.title)
.width(100)
.height(100)
.fontSize(16)
.margin(10)
.textAlign(TextAlign.Center)
.borderRadius(10)
}
}
}
創建父組件,并在父組件中引用子組件。
具體代碼如下:
@Entry
@Component
struct Sample {
...
build() {
Stack({ alignContent: Alignment.Bottom }) {
Flex({ wrap: FlexWrap.Wrap }) {
// 通過循環渲染加載所有子組件
ForEach(this.items, (item: ItemProps, index: number) => {
// 引用App卡片子組件
AppItem({data: this.items[index]})
}, (item: ItemProps) => item.id.toString())
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#ffffff')
.margin({ top:50 })
}
}
由于卡片背景色變化、卡片縮放、卡片拖拽在觸發時都是針對單個卡片的狀態變化,所以在卡片子組件中定義相應的狀態變量,用來控制單個卡片的狀態變化。
本例中定義狀態變量“data”用來控制卡片拖拽時位置的刷新;定義狀態變量”downFlag“用來監聽卡片是否被按下,從而控制卡片背景色及縮放狀態的更新。
具體代碼如下:
@Component
export struct AppItem {
// 定義狀態變量data,用來控制卡片被拖拽時位置的刷新
@State data: ItemProps = {};
// 定義狀態變量downFlag用來監聽卡片是否被按下,從而控制卡片背景色及縮放狀態的更新
@State downFlag: boolean = false;
...
build() {
Stack({ alignContent: Alignment.TopEnd }) {
Image($r('app.media.ic_public_close'))
.height(30)
.width(30)
.zIndex(2)
.offset({
x: -12,
y: 12
})
Text(this.data.title)
.width(100)
.height(100)
.fontSize(16)
.margin(10)
.textAlign(TextAlign.Center)
.borderRadius(10)
// 根據狀態變量downFlag的變化,更新背景色
.backgroundColor(this.downFlag ? '#EEA8AB' : '#86C7CC')
// 背景色更新時添加屬性動畫
.animation({
duration: 500,
curve: Curve.Friction
})
// 綁定onTouch事件,監聽卡片是否被按下,根據不同狀態改變downFlag的值
.onTouch((event: TouchEvent) => {
if (event.type == TouchType.Down) {
this.downFlag = true
} else if (event.type == TouchType.Up) {
this.downFlag = false
}
})
}
// 根據狀態變量downFlag的變化,控制卡片的縮放
.scale(this.downFlag ? { x: 0.8, y: 0.8 } : { x: 1, y: 1 })
// 通過狀態變量data的變化,控制卡片位置的更新
.offset({
x: this.data.offsetX,
y: this.data.offsetY
})
// 拖動觸發該手勢事件
.gesture(
GestureGroup(GestureMode.Parallel,
...
PanGesture(this.panOption)
.onActionStart((event: GestureEvent) => {
console.info('Pan start')
})
// 拖動卡片時,改變狀態變量data的值
.onActionUpdate((event: GestureEvent) => {
this.data.offsetX = this.data.positionX + event.offsetX
this.data.offsetY = this.data.positionY + event.offsetY
})
.onActionEnd(() => {
this.data.positionX = this.data.offsetX
this.data.positionY = this.data.offsetY
console.info('Pan end')
})
)
)
}
}
長按卡片,卡片右上角會出現刪除圖標。
由于所有卡片右上角都會出現刪除圖標,所以這里需要做全局的刷新。本例在父組件中定義狀態變量“deleteVisibility”,在調用子組件時,將其作為參數傳遞給所有卡片子組件,并且通過@Link裝飾器與子組件進行雙向綁定,從而可以控制所有卡片子組件中刪除圖標的更新。
父組件代碼:
@Entry
@Component
struct Sample {
...
// 定義狀態變量deleteVisibility,控制App卡片上刪除圖標的更新
@State deleteVisibility: boolean = false
...
build() {
Stack({ alignContent: Alignment.Bottom }) {
Flex({ wrap: FlexWrap.Wrap }) {
// 通過循環渲染加載所有子組件
ForEach(this.items, (item: ItemProps, index: number) => {
// 將狀態變量deleteVisibility傳遞給每一個子組件,從而deleteVisibility變化時可以觸發所有子組件的更新
AppItem({ deleteVisibility: $deleteVisibility, data: this.items[index], onDeleteClick: this.delete })
}, (item: ItemProps) => item.id.toString())
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#ffffff')
.margin({ top:50 })
.onClick(() => {
this.deleteVisibility = false
})
}
子組件代碼:
@Component
export struct AppItem {
...
// 定義deleteVisibility狀態變量,并通過@Link裝飾器與父組件中的同名變量雙向綁定,該變量值發生變化時父子組件可雙向同步
@Link deleteVisibility: boolean;
...
build() {
Stack({ alignContent: Alignment.TopEnd }) {
// 通過deleteVisibility控制刪除圖標的隱藏和顯示,當deleteVisibility值為true時顯示,為false時隱藏
if(this.deleteVisibility){
Image($r('app.media.ic_public_close'))
.height(30)
.width(30)
.zIndex(2)
// 控制刪除圖標的顯隱
.visibility(Visibility.Visible)
.offset({
x: -12,
y: 12
})
.onClick(() => this.onDeleteClick(this.data.id))
}else{
Image($r('app.media.ic_public_close'))
.height(30)
.width(30)
.zIndex(2)
.visibility(Visibility.Hidden)
.offset({
x: -12,
y: 12
})
.onClick(() => this.onDeleteClick(this.data.id))
}
...
.gesture(
GestureGroup(GestureMode.Parallel,
// 識別長按手勢
LongPressGesture({ repeat: true })
.onAction((event: GestureEvent) => {
if (event.repeat) {
// 長按時改變deleteVisibility的值為true,從而更新刪除圖標為顯示狀態
this.deleteVisibility = true
}
console.info('LongPress onAction')
}),
...
)
)
}
}
完整代碼
本例完整代碼如下:
data.ets文件(數據模型文件)
// data.ets
// AppItem組件接口信息
export interface ItemProps {
id?: number,
title?: string,
offsetX?: number, // X偏移量
offsetY?: number, // Y偏移量
positionX?: number, // 在X的位置
positionY?: number, // 在Y的位置
}
// AppItem初始數據
export const initItemsData: ItemProps[] = [
{
id: 1,
title: 'APP1',
offsetX: 0,
offsetY: 0,
positionX: 0,
positionY: 0
},
{
id: 2,
title: 'APP2',
offsetX: 0,
offsetY: 0,
positionX: 0,
positionY: 0
},
{
id: 3,
title: 'APP3',
offsetX: 0,
offsetY: 0,
positionX: 0,
positionY: 0
},
{
id: 4,
title: 'APP4',
offsetX: 0,
offsetY: 0,
positionX: 0,
positionY: 0
},
{
id: 5,
title: 'APP5',
offsetX: 0,
offsetY: 0,
positionX: 0,
positionY: 0
},
{
id: 6,
title: 'APP6',
offsetX: 0,
offsetY: 0,
positionX: 0,
positionY: 0
},
{
id: 7,
title: 'APP7',
offsetX: 0,
offsetY: 0,
positionX: 0,
positionY: 0
},
{
id: 8,
title: 'APP8',
offsetX: 0,
offsetY: 0,
positionX: 0,
positionY: 0
},
]
AppItem.ets文件(卡片子組件)
// AppItem.ets
import { ItemProps } from '../model/data';
@Component
export struct AppItem {
// 定義狀態變量data,用來控制卡片被拖拽時位置的刷新
@State data: ItemProps = {};
// 定義狀態變量downFlag用來監聽卡片是否被按下,從而控制卡片背景色及縮放狀態的更新
@State downFlag: boolean = false;
// 定義deleteVisibility狀態變量,并通過@Link裝飾器與父組件中的同名變量雙向綁定,該變量值發生變化時父子組件可雙向同步
@Link deleteVisibility: boolean;
private onDeleteClick: (id: number) => void;
private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All });
build() {
Stack({ alignContent: Alignment.TopEnd }) {
// 通過deleteVisibility控制刪除圖標的隱藏和顯示,當deleteVisibility值為true時顯示,為false時隱藏
if(this.deleteVisibility){
Image($r('app.media.ic_public_close'))
.height(30)
.width(30)
.zIndex(2)
// 控制刪除圖標的顯隱
.visibility(Visibility.Visible)
.offset({
x: -12,
y: 12
})
.onClick(() => this.onDeleteClick(this.data.id))
}else{
Image($r('app.media.ic_public_close'))
.height(30)
.width(30)
.zIndex(2)
.visibility(Visibility.Hidden)
.offset({
x: -12,
y: 12
})
.onClick(() => this.onDeleteClick(this.data.id))
}
Text(this.data.title)
.width(100)
.height(100)
.fontSize(16)
.margin(10)
.textAlign(TextAlign.Center)
.borderRadius(10)
// 根據狀態變量downFlag的變化,更新背景色
.backgroundColor(this.downFlag ? '#EEA8AB' : '#86C7CC')
// 背景色更新時添加屬性動畫
.animation({
duration: 500,
curve: Curve.Friction
})
// 綁定onTouch事件,監聽卡片是否被按下,根據不同狀態改變downFlag的值
.onTouch((event: TouchEvent) => {
if (event.type == TouchType.Down) {
this.downFlag = true
} else if (event.type == TouchType.Up) { // 手指抬起
this.downFlag = false
}
})
}
// 根據狀態變量downFlag的變化,控制卡片的縮放
.scale(this.downFlag ? { x: 0.8, y: 0.8 } : { x: 1, y: 1 })
// 通過狀態變量data的變化,控制卡片位置的更新
.offset({
x: this.data.offsetX,
y: this.data.offsetY
})
// 拖動觸發該手勢事件
.gesture(
GestureGroup(GestureMode.Parallel,
// 識別長按手勢
LongPressGesture({ repeat: true })
.onAction((event: GestureEvent) => {
if (event.repeat) {
// 長按時改變deleteVisibility的值為true,從而更新刪除圖標為顯示狀態
this.deleteVisibility = true
}
console.info('LongPress onAction')
}),
PanGesture(this.panOption)
.onActionStart((event: GestureEvent) => {
console.info('Pan start')
})
// 拖動卡片時,改變狀態變量data的值
.onActionUpdate((event: GestureEvent) => {
this.data.offsetX = this.data.positionX + event.offsetX
this.data.offsetY = this.data.positionY + event.offsetY
})
.onActionEnd(() => {
this.data.positionX = this.data.offsetX
this.data.positionY = this.data.offsetY
console.info('Pan end')
})
)
)
}
}
Index.ets文件(父組件)
// Index.ets
import { AppItem } from '../components/MyItem';
import { initItemsData } from '../model/data';
import { ItemProps } from '../model/data';
@Entry
@Component
struct Sample {
@State items: ItemProps[] = [];
// 定義狀態變量deleteVisibility,控制App卡片上刪除圖標的更新
@State deleteVisibility: boolean = false
// 刪除指定id組件
private delete = (id: number) => {
const index = this.items.findIndex(item => item.id === id);
this.items.splice(index, 1);
}
// 生命周期函數:組件即將出現時調用
aboutToAppear() {
this.items = [...initItemsData];
}
build() {
Stack({ alignContent: Alignment.Bottom }) {
Flex({ wrap: FlexWrap.Wrap }) {
// 通過循環渲染加載所有子組件
ForEach(this.items, (item: ItemProps, index: number) => {
// 將狀態變量deleteVisibility傳遞給每一個子組件,從而deleteVisibility變化時可以觸發所有子組件的更新
AppItem({ deleteVisibility: $deleteVisibility, data: this.items[index], onDeleteClick: this.delete })
}, (item: ItemProps) => item.id.toString())
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#ffffff')
.margin({ top:50 })
.onClick(() => {
// 點擊組件,deleteVisibility值變為false,從而隱藏所有卡片的刪除圖標
this.deleteVisibility = false
})
}
}
總結
刷新范圍一般通過狀態變量進行控制,需要厘清狀態變量影響的范圍,從而當狀態變量發生改變時可同步刷新相關的UI區域。