【擁抱鴻蒙】HarmonyOS之構建一個自定義彈框
?
?
彈窗是一種模態窗口,通常用來展示用戶當前需要的或用戶必須關注的信息或操作。在UI開發中,彈框是重要且不可忽視的組件。
HarmonyOS內置了多種系統彈框,分別有AlertDialog 、TextPickerDialog 、DatePickerDialog以及TimePickerDialog等。
?
本文將詳細介紹系統彈框的封裝和使用,并著重展現自定義彈框的實現。
?
?
系統彈框
AlertDialog
AlertDialog是警告彈窗,一般由App主動彈出,用于警告和確認用戶的操作行為,需用戶手動點擊操作按鈕來取消或進行下一步。
?
AlertDialog的實現
如下圖中的“刪除聯系人”彈框,一個AlertDialog包含標題、內容和操作區三個部分組成,操作區包含兩個按鈕,我們可以在按鈕的點擊事件里添加對應響應邏輯。
?

?
以上彈框的實現代碼如下:
?
AlertDialog.show(
{
title: '刪除聯系人', // 標題
message: '是否需要刪除所選聯系人?', // 內容
autoCancel: false, // 點擊遮障層時,是否關閉彈窗。
alignment: DialogAlignment.Bottom, // 彈窗在豎直方向的對齊方式
offset: { dx: 0, dy: -20 }, // 彈窗相對alignment位置的偏移量
primaryButton: {
value: '取消',
action: () => {
console.info('Callback when the first button is clicked');
}
},
secondaryButton: {
value: '刪除',
fontColor: '#D94838',
action: () => {
console.info('Callback when the second button is clicked');
}
},
cancel: () => { // 點擊遮障層關閉dialog時的回調
console.info('Closed callbacks');
}
}
)
})
?
AlertDialog的封裝
我們可以對AlertDialog進行封裝,作為工具類調用。
?
export class CommonUtils {
/\*\*
\* Common alert dialog
\* @param title 標題
\* @param msg 提示信息
\* @param context 需要保存狀態的UIAbility所對應的context
\* @param primaryCallback 第一個按鈕點擊事件的回調
\* @param secondCallback 第二個按鈕點擊事件的回調
\*/
commonAlertDialog(title:ResourceStr, msg: ResourceStr, context: common.UIAbilityContext, primaryCallback: Function, secondCallback: Function) {
AlertDialog.show({
title: title,
message: msg,
alignment: DialogAlignment.Bottom,
offset: {
dx: 0,
dy: CommonConstants.DY\_OFFSET
},
primaryButton: {
value: $r('app.string.cancel\_button'),
action: () => {
primaryCallback();
}
},
secondaryButton: {
value: $r('app.string.definite\_button'),
action: () => {
context.terminateSelf()
secondCallback();
}
}
});
}
}
這里創建了CommonUtils的工具類,把標題、提示信息作為創建自定義彈框的參數,按鈕的點擊事件可在回調里分別實現。
?
有了這種封裝,我們就能很容易地在App里調用一個風格統一的AlertDialog彈框了。
?
CommonUtils.commonAlertDialog("提示", "是否退出登錄", context, () => {
// 取消
}, () => {
// 確認
});
?
TextPickerDialog
這是一種文本滑動選擇彈窗,一般用于從多個選項中單選內容,再將用戶所選的內容返回給調用方。
如下圖所示,這里實現了一個選擇“足球主隊”的彈窗,用戶上下滑動滑塊再點擊“確認”就可以完成選擇。

TextPickerDialog的實現
?
@Entry
@Component
struct TextPickerDialogDemo {
@State select: number = 2;
private fruits: string[] = ['巴塞羅那', '曼城', '利物浦', '邁阿密國際', '拜仁慕尼黑', '多特蒙德', 'AC米蘭', '那不勒斯'];
?
build() {
Column() {
Button('TextPickerDialog')
.margin(20)
.onClick(() => {
TextPickerDialog.show({
range: this.fruits, // 設置文本選擇器的選擇范圍
selected: this.select, // 設置初始選中項的索引值。
onAccept: (value: TextPickerResult) => { // 點擊彈窗中的“確定”按鈕時觸發該回調。
// 設置select為按下確定按鈕時候的選中項index,這樣當彈窗再次彈出時顯示選中的是上一次確定的選項
this.select = value.index;
console.info("TextPickerDialog:onAccept()" + JSON.stringify(value));
},
onCancel: () => { // 點擊彈窗中的“取消”按鈕時觸發該回調。
console.info("TextPickerDialog:onCancel()");
},
onChange: (value: TextPickerResult) => { // 滑動彈窗中的選擇器使當前選中項改變時觸發該回調。
console.info('TextPickerDialog:onChange()' + JSON.stringify(value));
}
})
})
}
.width('100%')
}
}
?
TextPickerDialog的封裝
?
我們可以將選項作為參數進行TextPickerDialog的封裝,并提供用戶確認選項的回調。
這里的range的類型為:string[] | string[][] | Resource | TextPickerRangeContent[] | TextCascadePickerRangeContent[],提供了多種數據源類型,我們一般使用Resource方便多語言適配。
?
export class CommonUtils {
/\*\*
\* Text picker dialog
\* @param items 選項
\* @param textCallback 選中返回
\*/
textPickerDialog(items: Resource, textCallback: Function) {
if (this.isEmpty(items)) {
Logger.error(CommonConstants.TAG\_COMMON\_UTILS, 'item is null')
return;
}
?
TextPickerDialog.show({
range: items,
canLoop: false,
selected: 0,
onAccept: (result: TextPickerResult) => {
textCallback(result.value);
},
onCancel: () => {
Logger.info(CommonConstants.TAG\_COMMON\_UTILS, 'TextPickerDialog canceled')
}
});
}
}
?
對工具類中的TextPickerDialog的調用如下:
?
CommonUtils.textPickerDialog($r('app.strarray.club\_array'), (selectedValue: string) => {
this.club = selectedValue;
})
}
?
這里的app.strarray.club\_array指向resources中的配置文件stringarray.json5,其內容如下:
?
{
"strarray": [
{
"name": "club\_array",
"value": [
{
"value": "巴塞羅那"
},
{
"value": "曼城"
},
{
"value": "利物浦"
},
{
"value": "邁阿密國際"
},
{
"value": "拜仁慕尼黑"
},
{
"value": "AC米蘭"
},
{
"value": "多特蒙德"
},
{
"value": "阿賈克斯"
}
]
}
]
}
?
DatePickerDialog
DatePickerDialog是日期選擇器彈框,用于選擇特定格式的日期,并返回給調用方。
?

?
?
?
DatePickerDialog的實現
以“出生日期”選擇器彈框為例,我們通過如下代碼可以實現:
?
let selectedDate = new Date('1949-10-1');
DatePickerDialog.show({
start: new Date('1900-1-1'), // 設置選擇器的起始日期
end: new Date('2000-12-31'), // 設置選擇器的結束日期
selected: selectedDate, // 設置當前選中的日期
lunar: false,
onDateAccept: (value: Date) => { // 點擊彈窗中的“確定”按鈕時觸發該回調
// 通過Date的setFullYear方法設置按下確定按鈕時的日期,這樣當彈窗再次彈出時顯示選中的是上一次確定的日期
selectedDate.setFullYear(value.getFullYear(), value.getMonth() + 1, value.getDate())
console.info('DatePickerDialog:onDateAccept()' + JSON.stringify(value))
},
onCancel: () => { // 點擊彈窗中的“取消”按鈕時觸發該回調
console.info('DatePickerDialog:onCancel()')
},
onDateChange: (value: Date) => { // 滑動彈窗中的滑動選擇器使當前選中項改變時觸發該回調
console.info('DatePickerDialog:onDateChange()' + JSON.stringify(value))
}
})
})
?
DatePickerDialog的封裝
?
日期選擇器包含起始日期、截止日期和默認選中日期三個參數,我們只需對用戶確認選擇后的回調里響應即可。
?
export class CommonUtils {
/\*\*
\* Date picker dialog
\* @param dateCallback 確認選中日期回調
\*/
datePickerDialog(dateCallback: Function) {
DatePickerDialog.show({
start: new Date(CommonConstants.START\_TIME),
end: new Date(),
selected: new Date(CommonConstants.SELECT\_TIME),
lunar: false,
onDateAccept: (value: Date) => {
let year: number = value.getFullYear();
let month: number = value.getMonth() + 1;
let day: number = value.getDate();
let selectedDate: string = `${year}${CommonConstants.DATE\_YEAR}`+`${month}${CommonConstants.DATE\_MONTH}`+`${day}${CommonConstants.DATE\_DAY}`;
dateCallback(selectedDate);
}
});
}
}
?
基于以上封裝,datePickerDialog的調用可以簡單地實現如下:
?
CommonUtils.datePickerDialog((dateValue: string) => {
this.birthdate = dateValue;
})
?
自定義彈框
除了系統彈框,還可以對彈框進行自定義。自定義彈框更加靈活,適用于更多的業務場景。
?
這里,我們實現一個包含多選器的自定義彈框,其實現效果如下圖所示。

不難看出,這個彈框由標題、選擇列表和按鈕操作區構成。
?
自定義彈框需要使用裝飾器@CustomDialog,
我們創建一個名為CustomDialogWidget的struct,并添加三個屬性。
?
* items是數據源;
* selectedContent是選中結果拼接而成的字符串;
* controller是自定義彈框的控制器,其類型為CustomDialogController。
?
export default struct CustomDialogWidget {
@State items: Array<CustomItem> = [];
@Link selectedContent: string;
private controller?: CustomDialogController;
}
?
在組件的aboutToAppear()中實現數據源的獲取,使用到resmgr.ResourceManager的getStringArrayValue方法。
?
aboutToAppear(): void {
let context: Context = getContext(this);
if (CommonUtils.isEmpty(context) || CommonUtils.isEmpty(context.resourceManager)) {
Logger.error(CommonConstants.TAG\_CUSTOM, 'context or resourceManager is null');
return;
}
?
let manager = context.resourceManager;
manager.getStringArrayValue($r('app.strarray.hobbies\_data').id, (error, hobbyArray) => {
if (!CommonUtils.isEmpty(error)) {
Logger.error(CommonConstants.TAG\_CUSTOM, 'error = ' + JSON.stringify(error));
} else {
hobbyArray.forEach((itemTitle: string) => {
let item = new CustomItem();
item.title = itemTitle;
item.isChecked = false;
this.items.push(item);
?
Logger.info(item.title);
});
}
});
}
?
然后在Build()中實現其界面的搭建:
?
build() {
Column() {
// 標題
Text($r('app.string.title\_hobbies'))
.fontSize($r('app.float.title\_hobbies\_size'))
.fontColor($r('app.color.custom\_color'))
.lineHeight($r('app.float.title\_line\_height'))
.fontWeight(CommonConstants.BIGGER)
.alignSelf(ItemAlign.Start)
.margin({ left: $r('app.float.title\_left\_distance') })
?
// 選項列表
List() {
ForEach(this.items, (item: CustomItem) => {
ListItem() {
Row() {
Text(item.title)
.fontSize($r('app.float.label\_size'))
.fontColor($r('app.color.custom\_color'))
.layoutWeight(CommonConstants.WEIGHT\_ONE)
.textAlign(TextAlign.Start)
.fontWeight(CommonConstants.BIGGER)
.margin({ left: $r('app.float.label\_left\_distance') })
Toggle({ type: ToggleType.Checkbox, isOn: false })
.onChange((isCheck) => {
item.isChecked = isCheck;
})
.width($r('app.float.toggle\_size'))
.height($r('app.float.toggle\_size'))
.margin({ right: $r('app.float.toggle\_right\_distance') })
}
}
.height($r('app.float.options\_height'))
.margin({
top: $r('app.float.options\_top\_distance'),
bottom:$r('app.float.options\_bottom\_distance')
})
}, (item: CustomItem) => JSON.stringify(item.title))
}
.margin({
top: $r('app.float.list\_top\_distance'),
bottom: $r('app.float.list\_bottom\_distance')
})
.divider({
strokeWidth: $r('app.float.divider\_height'),
color: $r('app.color.divider\_color')
})
.listDirection(Axis.Vertical)
.edgeEffect(EdgeEffect.None)
.width(CommonConstants.FULL\_WIDTH)
.height($r('app.float.options\_list\_height'))
?
// 操作按鈕
Row() {
Button($r('app.string.cancel\_button'))
.dialogButtonStyle()
.onClick(() => {
this.controller?.close();
})
?
Blank()
.backgroundColor($r('app.color.custom\_blank\_color'))
.width($r('app.float.blank\_width'))
.opacity($r('app.float.blank\_opacity'))
.height($r('app.float.blank\_height'))
?
Button($r('app.string.definite\_button'))
.dialogButtonStyle()
.onClick(() => {
this.setSelectedItems(this.items);
this.controller?.close();
})
}
}
}
?
在確定按鈕的回調中,我們調用setSelectedItems(),其實現如下:
?
setSelectedItems(items: CustomItem[]) {
if (CommonUtils.isEmpty(items)) {
Logger.error(CommonConstants.TAG\_HOME, "Items is empty")
return;
}
?
let selectedText: string = items.filter((isCheckedItem: CustomItem) => isCheckedItem?.isChecked)
.map<string>((checkedItem: CustomItem) => {
return checkedItem.title!;
})
.join(CommonConstants.COMMA);
?
if (!CommonUtils.isEmpty(selectedText)) {
this.selectedContent = selectedText;
}
}
}
?
這里我們還用到了組件的屬性擴展方法封裝(用于提取重復的屬性代碼進行復用):
?
@Extend(Button)
function dialogButtonStyle() {
.fontSize($r('app.float.button\_text\_size'))
.fontColor(Color.Blue)
.layoutWeight(CommonConstants.WEIGHT\_ONE)
.height($r('app.float.button\_height'))
.backgroundColor(Color.White)
}
?
自定義彈框的調用
?
自定義彈框的調用基于CustomDialogController,將CustomDialogWidget作為它的參數builder即可實現控制器調出我們預期的自定義彈框。
?
@State birthdate: string = '';
@State sex: string = '';
@State hobbies: string = '';
private sexArray: Resource = $r('app.strarray.sex\_array');
?
customDialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogWidget({
selectedContent: this.hobbies
}),
alignment: DialogAlignment.Bottom,
customStyle: true,
offset: {
dx: 0,
dy: CommonConstants.DY\_OFFSET
}
});
?
以上,我們總結了HarmonyOS系統彈框和自定義彈框的實現、封裝及調用。
?
我是鄭知魚??,歡迎大家討論與指教。
如果你覺得有所收獲,也請點贊????收藏??關注??我吧~~
具體代碼見:customDialog
?

浙公網安備 33010602011771號