手把手教你實現(xiàn)省市區(qū)鎮(zhèn)—四級地址選擇彈窗組件
前言
hello 大家好,我是無言,因為地址級聯(lián)選擇功能其實還是非常常見的,而且官方有TextPicker文本選擇組件也可以實現(xiàn)地址級聯(lián)選擇,但是我發(fā)現(xiàn)超過3級之后,文字就太多了,會很難看,不好操作等相關(guān)問題。所以有必要自己來實現(xiàn)一個好看的省市區(qū)鎮(zhèn)-四級地址級聯(lián)選擇組件。
目的
通過本篇文章小伙伴們能學(xué)到什么?我簡單的總結(jié)了一下大概有以下幾點。
- 了解到鴻蒙Next 自定義彈窗 的核心用法。
- 了解到 實現(xiàn)級聯(lián)選擇的實現(xiàn)思路和過程,不僅限于鴻蒙,也適用于其他框架和場景。
- 了解到鴻蒙Next中如何封裝自己的自定義組件。
- 了解到鴻蒙Next中組件之間是如何通信的,以及如何實現(xiàn)自己想要的功能。
效果提前看一看:
實現(xiàn)過程
一、準備工作
- 安裝好最新DevEco Studio 開發(fā)工具,創(chuàng)建一個新的空項目。
- 新增目錄結(jié)構(gòu) ets/components/cascade/ ,在下面添加文件 addressObj.ts 用于存放地址Obj對象,index.ets 用來初始化彈窗容器,CustomAddress.ets用來存放具體的級聯(lián)選擇業(yè)務(wù)代碼,Cascade.d.ts TS 聲明文件。
二、實現(xiàn)自定義彈窗
- 將官網(wǎng)自定義彈窗的示例3復(fù)制過來,放入到ets/components/cascade/index.ets中后稍微修改一下,修改的地方我都添加注釋了。 主要是 去掉 @Entry 頁面的入口組件裝飾,修改組件命名并用 export暴露組件供外部使用。
// xxx.ets
@CustomDialog
struct CustomDialogExample {
controller?: CustomDialogController
cancel: () => void = () => {
}
confirm: () => void = () => {
}
build() {
Column() {
Text('這是自定義彈窗')
.fontSize(30)
.height(100)
Button('點我關(guān)閉彈窗')
.onClick(() => {
if (this.controller != undefined) {
this.controller.close()
}
})
.margin(20)
}
}
}
// @Entry 去掉入口頁面標志
@Component
export struct CustomDialogCascade { //修改命名 注意前面加了 export 需要暴露組件
dialogController: CustomDialogController | null = new CustomDialogController({
builder: CustomDialogExample({
cancel: ()=> { this.onCancel() },
confirm: ()=> { this.onAccept() }
}),
cancel: this.existApp,
autoCancel: true,
onWillDismiss:(dismissDialogAction: DismissDialogAction)=> {
console.info("reason=" + JSON.stringify(dismissDialogAction.reason))
console.log("dialog onWillDismiss")
if (dismissDialogAction.reason == DismissReason.PRESS_BACK) {
dismissDialogAction.dismiss()
}
if (dismissDialogAction.reason == DismissReason.TOUCH_OUTSIDE) {
dismissDialogAction.dismiss()
}
},
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
customStyle: false,
cornerRadius: 20,
width: 300,
height: 200,
borderWidth: 1,
borderStyle: BorderStyle.Dashed,//使用borderStyle屬性,需要和borderWidth屬性一起使用
borderColor: Color.Blue,//使用borderColor屬性,需要和borderWidth屬性一起使用
backgroundColor: Color.White,
shadow: ({ radius: 20, color: Color.Grey, offsetX: 50, offsetY: 0}),
})
// 在自定義組件即將析構(gòu)銷毀時將dialogController置空
aboutToDisappear() {
this.dialogController = null // 將dialogController置空
}
onCancel() {
console.info('Callback when the first button is clicked')
}
onAccept() {
console.info('Callback when the second button is clicked')
}
existApp() {
console.info('Click the callback in the blank area')
}
build() {
Column() {
Button('click me')
.onClick(() => {
if (this.dialogController != null) {
this.dialogController.open()
}
}).backgroundColor(0x317aff)
}.width('100%').margin({ top: 5 })
}
}
- 修改ets/pages/index.ets 去掉無關(guān)的代碼,在頁面中引入我們的組件。
import {CustomDialogCascade} from "../components/cascade/index"
@Entry
@Component
struct Index {
build() {
RelativeContainer() {
CustomDialogCascade()
}
.height('100%')
.width('100%')
}
}
預(yù)覽一下看看效果。
三、實現(xiàn)父子組件的通信
在講后續(xù)功能前,這里有必要講一下鴻蒙開發(fā)組件狀態(tài)。
- @State用于裝飾當(dāng)前組件的狀態(tài)變量而且必須初始化,@State裝飾的變量在發(fā)生變化時,會驅(qū)動當(dāng)前組件的視圖刷新。
- @Prop用于裝飾子組件的狀態(tài)變量而且不允許本地初始化,@Prop裝飾的變量會同步父組件的狀態(tài),但只能單向同步。
- @Link用于裝飾子組件的狀態(tài)變量而且不允許本地初始化,@Prop變量同樣會同步父組件狀態(tài),但是能夠雙向同步。
- @Provide和@Consume用于跨層級傳遞狀態(tài)信息,其中@Provide用于裝飾祖先組件的狀態(tài)變量,@Consume用于裝飾后代組件的狀態(tài)變量。@Provide裝飾變量必須本地初始化,而@Consume裝飾的變量不允許本地初始化,
而且他們能夠雙向同步。 - @Props與@Link聲明接收的屬性,必須是@State的屬性,而不能是@State屬性對象中嵌套的屬性解決辦法將嵌套對象的類型用class定義, 并使用@Observed來裝飾,子組件中定義的嵌套對象的屬性, 使用@ObjectLink來裝飾。
- @Watch用來監(jiān)視狀態(tài)數(shù)據(jù)的變化,包括:@State、@Prop、@Link、@Provide、@Consume
一旦狀態(tài)數(shù)據(jù)變化,監(jiān)視的回調(diào)就會調(diào)用,這里我有必要加一個示例。
@State @Watch('onCountChange') count: number = 0
/**
* 一旦count變化,此回調(diào)函數(shù)就會自動調(diào)用
* @param count 被監(jiān)視的狀態(tài)屬性名
*/
onCountChange (count) {
// 可以在此做特定處理
}
四、完善邏輯
好了回到我們的主題,前面我們的示例中,只是子組件自己調(diào)用彈窗了,我們要實現(xiàn)以下幾個功能。
- 父組件調(diào)用子組件方法喚醒子組件彈窗。
- 父組件傳參控制選擇地址的層級數(shù)量。
- 子組件選好數(shù)據(jù)之后回調(diào)方法傳給父組件。
修改 /ets/components/cascade/index.ets,實現(xiàn)父組件傳參給子組件,子組件回調(diào)方法傳值給父組件。然后還修改了彈窗的樣式以及位置,詳細請看下面代碼。
// xxx.ets
@CustomDialog
struct CustomDialogExample {
controller?: CustomDialogController
@Prop level: number;
cancel: () => void = () => {
}
confirm: (data:string) => void = () => {
}
build() {
Column() {
Text('這是自定義彈窗')
.fontSize(30)
.height(100)
Text('層級'+this.level)
Button('點我關(guān)閉彈窗')
.onClick(() => {
if (this.controller != undefined) {
this.controller.close()
}
this.confirm('aaa') //回傳信息
})
.margin(20)
}
}
}
// @Entry 去掉入口頁面標志
@Component
export struct CustomDialogCascade { //修改命名 注意前面加了 export 需要暴露組件
@Link CustomDialogController: CustomDialogController | null ;
@Prop level: number;
cancel?: () => void
confirm?: (data:string) => void = () => {
}
aboutToAppear(): void {
this.CustomDialogController= new CustomDialogController({
builder: CustomDialogExample({
cancel: this.cancel,
confirm: this.confirm,
level:this.level,
}),
autoCancel: true,
onWillDismiss:(dismissDialogAction: DismissDialogAction)=> {
if (dismissDialogAction.reason == DismissReason.PRESS_BACK) {
dismissDialogAction.dismiss()
}
if (dismissDialogAction.reason == DismissReason.TOUCH_OUTSIDE) {
dismissDialogAction.dismiss()
}
},
alignment: DialogAlignment.Bottom,
offset: { dx: 0, dy: 0},
customStyle: false,
cornerRadius: 0,
width: '100%',
backgroundColor: Color.White,
})
}
aboutToDisappear() {
this.CustomDialogController = null // 將dialogController置空
}
build() { //因為用的 自定義彈窗功能,所以這下面可以為空
}
}
修改/ets/pages/index.ets 文件實現(xiàn)功能主要是父元素調(diào)用子組件方法,以及子組件的回調(diào)方法調(diào)用。
import {CustomDialogCascade} from "../components/cascade/index"
@Entry
@Component
struct Index {
@State CustomDialogController :CustomDialogController|null = null;
build() {
Column() {
Button('打開彈窗')
.onClick(()=>{
this.CustomDialogController?.open()
})
CustomDialogCascade(
{
level:3,//層級 最大4 最小1
CustomDialogController:this.CustomDialogController, //彈窗實體類 主要控制彈窗顯示隱藏等
confirm:(data)=>{
console.log('data',(data))
}
},
)
}
.height('100%')
.width('100%')
}
}
運行效果如下,點擊 點我關(guān)閉彈窗 按鈕可以發(fā)現(xiàn) 子組件的回調(diào)信息 aaa 在父組件中成功打印。
五、完善地址級聯(lián)邏輯
- 因為對象取值性能最好,速度也快,所以我這里采用的是對象信息結(jié)構(gòu),在 addressObj.ts 文件中存放我們的所有城市信息,因為內(nèi)容過多,我就只舉例展示部分,我將完整的城市信息,放在了本文末尾,有需要的小伙伴可以自己下載下來嘗試一下。
export const regionDict = {
"86": {
"110000": "北京市",
"120000": "天津市",
"130000": "河北省",
...
},
"110000": {
"110100": "北京市"
},
"110100": {
"110101": "東城區(qū)",
"110102": "西城區(qū)",
"110105": "朝陽區(qū)",
"110106": "豐臺區(qū)",
"110107": "石景山區(qū)",
"110108": "海淀區(qū)",
"110109": "門頭溝區(qū)",
"110111": "房山區(qū)",
"110112": "通州區(qū)",
"110113": "順義區(qū)",
"110114": "昌平區(qū)",
"110115": "大興區(qū)",
"110116": "懷柔區(qū)",
"110117": "平谷區(qū)",
"110118": "密云區(qū)",
"110119": "延慶區(qū)"
},
"110101": {
"110101001": "東華門街道",
"110101002": "景山街道"
},
...
};
- 聲明文件 Cascade.d.ts 添加類型。
export interface RegionType{
code?:string;
pcode?:string;
name?:string
}
export type levelNumber = 1 | 2 | 3 | 4;
- 修改 CustomAddress.ets 完成我們主要的核心業(yè)務(wù)代碼,內(nèi)容比較多,該添加注釋的地方我都添加了,具體功能代碼請看下面內(nèi)容。
import { regionDict } from "./addressObj"
import {RegionType,levelNumber} from './Cascade'
@CustomDialog
export struct CustomAddress {
controller?: CustomDialogController
@State region:RegionType[]=[]; //存放選中的結(jié)果
@State data: RegionType[] = [];// 提供選中的列表
@State step:number = 0;//存放步數(shù)
@State active:number = 0;//當(dāng)前高亮步驟
level:levelNumber=4; //層級 默認 為 4級 可以選鎮(zhèn)街道一級
cancel: () => void = () => {
console.log('關(guān)閉')
}
confirm: (region:RegionType[]) => void = () => {
}
// 頁面加載執(zhí)行
aboutToAppear(): void {
this.loadRegionData('86')
}
/**
* 根據(jù)父元素code生成列表數(shù)據(jù)
* @param pcode
*/
loadRegionData(pcode = '86') {
this.data.length=0
Object.keys(regionDict).forEach((code)=>{
if(code==pcode){
Object.keys(regionDict[code]).forEach((key)=>{
this.data.push({
name:regionDict[code][key],
code:key,
pcode:pcode
})
})
}
})
if(this.data.length == 0) {
this.controller?.close() //關(guān)閉彈窗
}
}
// 上面步驟選中
onStepClickSelect(index:number,item:RegionType){
this.active=index;
this.loadRegionData(item.pcode)
}
// 數(shù)據(jù)選中
onRowClickSelect(item:RegionType){
if(this.active==3 || this.active>=(this.level-1)){ //如果是到了最后一步選擇街道 則直接結(jié)束
this.region.push(item)
this.confirm(this.region)
this.controller?.close()//關(guān)閉彈窗
return
}
if(this.active==this.step){
this.step++;
this.active++;
}else{
this.region= this.region.slice(0,this.active) //數(shù)組截取
this.active++; //從選中的地方重新開始
this.step=this.active //步驟也是一樣重新開始
}
this.region.push(item)
this.loadRegionData(item.code)
}
// 獲取名稱
getLableName(){
let name =`請選擇`
switch (this.step){
case 0:
name=`請選擇省份/地區(qū)`
break;
case 1:
name=`請選擇城市`
break;
case 2:
name=`請選擇區(qū)縣`
break;
case 3:
name=`請選擇街道`
break;
}
return name
}
build() {
Column() {
// 存儲已選擇信息
Column(){
ForEach(this.region, (item: RegionType,index:number) => {
Flex({alignItems:ItemAlign.Center}){
Row(){
Text()
.backgroundColor(this.active>=index?'#396ec1':'#ff737070')
.width(6)
.height(6)
.borderRadius(10)
// 頂部線條
if (index>0){
Text()
.width(1)
.height(13)
.position({left:2,top:0})
.backgroundColor(this.active>index?'#396ec1':'#ff737070')
}
// 底部線條
if(this.step>=index){
Text()
.width(1)
.height(13)
.position({left:2,top:'50%'})
.backgroundColor(this.active>index?'#396ec1':'#ff737070')
}
}.height(25)
.width(20)
.align(Alignment.Center)
Row(){
Text(item.name).fontSize(14).fontColor('#333333')
}
.flexGrow(1)
.height(25)
.border({
width: { bottom:1 },
color: 0xf5f5f5,
style: {
left:null,
right: null,
top: null,
bottom: BorderStyle.Solid
}
})
.onClick(()=>{
this.onStepClickSelect(index,item)
})
}.width('100%')
.padding({left:10})
})
// 提示信息
Flex({alignItems:ItemAlign.Center}){
Row(){
Text()
.backgroundColor(this.active==this.step?'#396ec1':'#ff737070')
.width(6)
.height(6)
.borderRadius(10)
// 頂部線條
if(this.step){
Text()
.width(1)
.height(13)
.position({left:2,top:0})
.backgroundColor(this.active==this.step?'#396ec1':'#ff737070')
}
}.height(25)
.width(20)
.align(Alignment.Center)
Row(){
Text(this.getLableName()).fontSize(14).fontColor(this.active==this.step?'#396ec1':'#333')
}
.flexGrow(1)
.height(25)
}.width('100%')
.padding({left:10})
}.padding({top:10,bottom:10})
// 分割線
Column(){
}.height(10)
.backgroundColor(0xf5f5f5)
.width('100%')
// 下方列表
Column(){
List({ space: 5, initialIndex: 0 }) {
ForEach(this.data, (item: RegionType) => {
ListItem() {
Text('' + item.name)
.width('100%')
.fontSize(14)
.fontColor('#333333')
.textAlign(TextAlign.Start)
}
.padding({left:10})
.height(25)
.onClick(()=>{
this.onRowClickSelect(item)
})
})
}
.listDirection(Axis.Vertical) // 排列方向
.scrollBar(BarState.Off)
.friction(0.6)
.contentStartOffset(10) //列表滾動到起始位置時,列表內(nèi)容與列表顯示區(qū)域邊界保留指定距離
.contentEndOffset(10) //列表內(nèi)容與列表顯示區(qū)域邊界保留指定距離
.divider({ strokeWidth:1, color: 0xf5f5f5, startMargin: 5, endMargin: 5 }) // 每行之間的分界線
.edgeEffect(EdgeEffect.Spring) // 邊緣效果設(shè)置為Spring
.width('100%')
.height('100%')
}.height(200)
}
}
}
- 修改/ets/components/cascade/index.ets 文件。
import {CustomAddress }from "./CustomAddress"
import {RegionType,levelNumber} from './Cascade'
export {RegionType }from './Cascade' //重新暴露聲明文件類型
// @Entry 去掉入口頁面標志
@Component
export struct CustomDialogCascade { //修改命名 注意前面加了 export 需要暴露組件
@Link CustomDialogController: CustomDialogController | null ;
@Prop level: levelNumber;
cancel?: () => void
confirm?: (data:RegionType[]) => void = () => {
}
aboutToAppear(): void {
this.CustomDialogController= new CustomDialogController({
builder: CustomAddress({
cancel: this.cancel,
confirm: this.confirm,
level:this.level,
}),
autoCancel: true,
onWillDismiss:(dismissDialogAction: DismissDialogAction)=> {
if (dismissDialogAction.reason == DismissReason.PRESS_BACK) {
dismissDialogAction.dismiss()
}
if (dismissDialogAction.reason == DismissReason.TOUCH_OUTSIDE) {
dismissDialogAction.dismiss()
}
},
alignment: DialogAlignment.Bottom,
offset: { dx: 0, dy: 0},
customStyle: false,
cornerRadius: 0,
width: '100%',
backgroundColor: Color.White,
})
}
aboutToDisappear() {
this.CustomDialogController = null // 將dialogController置空
}
build() {
}
}
重新運行一下,當(dāng)街道選好之后,即可發(fā)現(xiàn)彈窗自動關(guān)閉,而且選好地址信息成功拿到。
總結(jié)
本文詳細介紹了關(guān)于在華為鴻蒙系統(tǒng)去實現(xiàn)一個自定義彈窗的詳細教程,主要是提供一些思路,了解組件之間通信的技巧,以及如何實現(xiàn)一個地址級聯(lián)選中的詳細過程。