橫掃鴻蒙彈窗亂象,SmartDialog出世

xdd666發表於2024-08-11

前言

但凡用過鴻蒙原生彈窗的小夥伴,就能體會到它們是有多麼的難用和奇葩,什麼AlertDialog,CustomDialog,SubWindow,bindXxx,只要大家用心去體驗,就能發現他們有很多離譜的設計和限制,時常就是一邊用,一邊罵罵咧咧的吐槽

實屬無奈,就把鴻蒙版的SmartDialog寫出來了

flutter的自帶的dialog是可以應對日常場景,例如:簡單的開啟一個彈窗,非UI模組使用,跨頁面互動之類;flutter_smart_dialog 是補齊了大多數的業務場景和一些強大的特殊能力,flutter_smart_dialog 對於flutter而言,日常場景是錦上添花,特殊場景是雪中送炭

但是 ohos_smart_dialog 對於鴻蒙而言,日常場景就是雪中送炭!單單一個使用方式而言,就是吊打鴻蒙的CustomDialog,CustomDialog的各種限制和使用方式,我不想再去提及和吐槽了

有時候,簡潔的使用,才是最大的魅力

鴻蒙版的SmartDialog有什麼優勢?

  • 單次初始化後即可使用,無需多處配置相關Component
  • 優雅,極簡的用法
  • 非UI區域內使用,自定義Component
  • 返回事件處理,最佳化的跨頁面互動
  • 多彈窗能力,多位置彈窗:上下左右中間
  • 定位彈窗:自動定位目標Component
  • 極簡用法的loading彈窗
  • 等等......

目前 flutter_smart_dialog 的程式碼量16w+,完整復刻其功能,工作量非常大,目前只能逐步實現一些基礎能力,由於鴻蒙api的設計和相關限制,用法和相關初始化都有一定程度的妥協

鴻蒙版本的SmartDialog,功能會逐步和 flutter_smart_dialog 對齊(長期),api會盡量保持一致

效果

  • Tablet 模擬器目前有些問題,會導致動畫閃爍,請忽略;注:真機動畫絲滑流暢,無任何問題

attachLocation

customTag

customJumpPage

極簡用法

// dialog
SmartDialog.show({
  builder: dialogArgs,
  builderArgs: Math.random(),
})

@Builder
function dialogArgs(args: number) {
  Text(args.toString()).padding(50).backgroundColor(Color.White)
}

// loading
SmartDialog.showLoading()

安裝

  • github:https://github.com/xdd666t/ohos_smart_dialog
  • ohos:https://ohpm.openharmony.cn/#/cn/detail/ohos_smart_dialog
ohpm install ohos_smart_dialog 

配置

下述的配置項,可能會有一點多,但,這也是為了極致的體驗;同時也是無奈之舉,相關配置難以在內部去閉環處理,只能在外部去配置

這些配置,只需要配置一次,後續無需關心

完成下述的配置後,你將可以在任何地方使用彈窗,沒有任何限制

初始化

  • 因為彈窗需要處理跨頁面互動,必須要監控路由
@Entry
@Component
struct Index {
  navPathStack: NavPathStack = new NavPathStack()

  build() {
    Stack() {
      // here: monitor router
      Navigation(OhosSmartDialog.registerRouter(this.navPathStack)) {
        MainPage()
      }
      .mode(NavigationMode.Stack)
      .hideTitleBar(true)
      .navDestination(pageMap)

      // here
      OhosSmartDialog()
    }.height('100%').width('100%')
  }
}

返回事件監聽

別問我為啥返回事件的監聽,處理的這麼不優雅,鴻蒙裡面沒找全域性返回事件監聽,我也沒轍。。。

  • 如果你無需處理返回事件,可以使用下述寫法
// Entry頁面處理
@Entry
@Component
struct Index {
  onBackPress(): boolean | void {
    return OhosSmartDialog.onBackPressed()()
  }
}

// 路由子頁面
struct JumpPage {
  build() {
    NavDestination() {
      // ....
    }
    .onBackPressed(OhosSmartDialog.onBackPressed())
  }
}
  • 如果你需要處理返回事件,在OhosSmartDialog.onBackPressed()中傳入你的方法即可
// Entry頁面處理
@Entry
@Component
struct Index {
  onBackPress(): boolean | void {
    return OhosSmartDialog.onBackPressed(this.onCustomBackPress)()
  }

  onCustomBackPress(): boolean {
    return false
  }
}

// 路由子頁面
@Component
struct JumpPage {
  build() {
    NavDestination() {
      // ...
    }
    .onBackPressed(OhosSmartDialog.onBackPressed(this.onCustomBackPress))
  }

  onCustomBackPress(): boolean {
    return false
  }
}

路由監聽

  • 一般來說,你無需關注SmartDialog的路由監聽,因為內部已經設定了路由監聽攔截器
  • 但是,NavPathStack僅支援單攔截器(setInterception),如果業務程式碼也使用了這個api,會導致SmartDialog的路由監聽被覆蓋,從而失效

如果出現該情況,請參照下述解決方案

  • 在你的路由監聽類中手動呼叫OhosSmartDialog.observe
export default class YourNavigatorObserver implements NavigationInterception {
  willShow?: InterceptionShowCallback = (from, to, operation, isAnimated) => {
    OhosSmartDialog.observe.willShow?.(from, to, operation, isAnimated)
    // ...
  }
  didShow?: InterceptionShowCallback = (from, to, operation, isAnimated) => {
    OhosSmartDialog.observe.didShow?.(from, to, operation, isAnimated)
    // ...
  }
}

適配暗黑模式

  • 為了極致的體驗,深色模式切換時,開啟態彈窗也應重新整理為對應模式的樣式,故需要進行下述配置
export default class EntryAbility extends UIAbility {  
  onConfigurationUpdate(newConfig: Configuration): void {  
    OhosSmartDialog.onConfigurationUpdate(newConfig)  
  }  
}

SmartConfig

  • 支援全域性配置彈窗的預設屬性
function init() {
  // show
  SmartDialog.config.custom.maskColor = "#75000000"
  SmartDialog.config.custom.alignment = Alignment.Center

  // showAttach
  SmartDialog.config.attach.attachAlignmentType = SmartAttachAlignmentType.center
}
  • 檢查彈窗是否存在
// 檢查當前是否有CustomDialog,AttachDialog或LoadingDialog處於開啟狀態
let isExist = SmartDialog.checkExist()

// 檢查當前是否有AttachDialog處於開啟狀態
let isExist = SmartDialog.checkExist({ dialogTypes: [SmartAllDialogType.attach] })

// 檢查當前是否有tag為“xxx”的dialog處於開啟狀態
let isExist = SmartDialog.checkExist({ tag: "xxx" })

配置全域性預設樣式

  • ShowLoading 自定樣式十分簡單
SmartDialog.showLoading({ builder: customLoading })

但是對於大家來說,肯定是想用 SmartDialog.showLoading() 這種簡單寫法,所以支援自定義全域性預設樣式

  • 需要在 OhosSmartDialog 上配置自定義的全域性預設樣式
@Entry
@Component
struct Index {
  build() {
    Stack() {
      OhosSmartDialog({
        // custom global loading
        loadingBuilder: customLoading,
      })
    }.height('100%').width('100%')
  }
}

@Builder
export function customLoading(args: ESObject) {
  LoadingProgress().width(80).height(80).color(Color.White)
}
  • 配置完你的自定樣式後,使用下述程式碼,就會顯示你的 loading 樣式
SmartDialog.showLoading()

// 支援入參,可以在特殊場景下靈活配置
SSmartDialog.showLoading({ builderArgs: 1 })

CustomDialog

  • 下方會共用的方法
export function randomColor(): string {
  const letters: string = '0123456789ABCDEF';
  let color = '#';
  for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)];
  }
  return color;
}

export function delay(ms?: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

傳參彈窗

export function customUseArgs() {
  SmartDialog.show({
    builder: dialogArgs,
    // 支援任何型別
    builderArgs: Math.random(),
  })
}

@Builder
function dialogArgs(args: number) {
  Text(`${args}`).fontColor(Color.White).padding(50)
    .borderRadius(12).backgroundColor(randomColor())
}

customUseArgs

多位置彈窗

export async function customLocation() {
  const animationTime = 1000
  SmartDialog.show({
    builder: dialogLocationHorizontal,
    alignment: Alignment.Start,
  })
  await delay(animationTime)
  SmartDialog.show({
    builder: dialogLocationVertical,
    alignment: Alignment.Top,
  })
}


@Builder
function dialogLocationVertical() {
  Text("location")
    .width("100%")
    .height("20%")
    .fontSize(20)
    .fontColor(Color.White)
    .textAlign(TextAlign.Center)
    .padding(50)
    .backgroundColor(randomColor())
}

@Builder
function dialogLocationHorizontal() {
  Text("location")
    .width("30%")
    .height("100%")
    .fontSize(20)
    .fontColor(Color.White)
    .textAlign(TextAlign.Center)
    .padding(50)
    .backgroundColor(randomColor())
}

customLocation

跨頁面互動

  • 正常使用,無需設定什麼引數
export function customJumpPage() {
  SmartDialog.show({
    builder: dialogJumpPage,
  })
}

@Builder
function dialogJumpPage() {
  Text("JumPage")
    .fontSize(30)
    .padding(50)
    .borderRadius(12)
    .fontColor(Color.White)
    .backgroundColor(randomColor())
    .onClick(() => {
      // 跳轉頁面
    })
}

customJumpPage

關閉指定彈窗

export async function customTag() {
  const animationTime = 1000
  SmartDialog.show({
    builder: dialogTagA,
    alignment: Alignment.Start,
    tag: "A",
  })
  await delay(animationTime)
  SmartDialog.show({
    builder: dialogTagB,
    alignment: Alignment.Top,
    tag: "B",
  })
}

@Builder
function dialogTagA() {
  Text("A")
    .width("20%")
    .height("100%")
    .fontSize(20)
    .fontColor(Color.White)
    .textAlign(TextAlign.Center)
    .padding(50)
    .backgroundColor(randomColor())
}

@Builder
function dialogTagB() {
  Flex({ wrap: FlexWrap.Wrap }) {
    ForEach(["closA", "closeSelf"], (item: string, index: number) => {
      Button(item)
        .backgroundColor("#4169E1")
        .margin(10)
        .onClick(() => {
          if (index === 0) {
            SmartDialog.dismiss({ tag: "A" })
          } else if (index === 1) {
            SmartDialog.dismiss({ tag: "B" })
          }
        })
    })
  }.backgroundColor(Color.White).width(350).margin({ left: 30, right: 30 }).padding(10).borderRadius(10)
}

customTag

自定義遮罩

export function customMask() {
  SmartDialog.show({
    builder: dialogShowDialog,
    maskBuilder: dialogCustomMask,
  })
}

@Builder
function dialogCustomMask() {
  Stack().width("100%").height("100%").backgroundColor(randomColor()).opacity(0.6)
}

@Builder
function dialogShowDialog() {
  Text("showDialog")
    .fontSize(30)
    .padding(50)
    .fontColor(Color.White)
    .borderRadius(12)
    .backgroundColor(randomColor())
    .onClick(() => customMask())
}

customMask

AttachDialog

預設定位

export function attachEasy() {
  SmartDialog.show({
    builder: dialog
  })
}

@Builder
function dialog() {
  Stack() {
    Text("Attach")
      .backgroundColor(randomColor())
      .padding(20)
      .fontColor(Color.White)
      .borderRadius(5)
      .onClick(() => {
        SmartDialog.showAttach({
          targetId: "Attach",
          builder: targetLocationDialog,
        })
      })
      .id("Attach")
  }
  .borderRadius(12)
  .padding(50)
  .backgroundColor(Color.White)
}

@Builder
function targetLocationDialog() {
  Text("targetIdDialog")
    .fontSize(20)
    .fontColor(Color.White)
    .textAlign(TextAlign.Center)
    .padding(50)
    .borderRadius(12)
    .backgroundColor(randomColor())
}

attachEasy

多方向定位

export function attachLocation() {
  SmartDialog.show({
    builder: dialog
  })
}

class AttachLocation {
  title: string = ""
  alignment?: Alignment
}

const locationList: Array<AttachLocation> = [
  { title: "TopStart", alignment: Alignment.TopStart },
  { title: "Top", alignment: Alignment.Top },
  { title: "TopEnd", alignment: Alignment.TopEnd },
  { title: "Start", alignment: Alignment.Start },
  { title: "Center", alignment: Alignment.Center },
  { title: "End", alignment: Alignment.End },
  { title: "BottomStart", alignment: Alignment.BottomStart },
  { title: "Bottom", alignment: Alignment.Bottom },
  { title: "BottomEnd", alignment: Alignment.BottomEnd },
]

@Builder
function dialog() {
  Column() {
    Grid() {
      ForEach(locationList, (item: AttachLocation) => {
        GridItem() {
          buildButton(item.title, () => {
            SmartDialog.showAttach({
              targetId: item.title,
              alignment: item.alignment,
              maskColor: Color.Transparent,
              builder: targetLocationDialog
            })
          })
        }
      })
    }.columnsTemplate('1fr 1fr 1fr').height(220)

    buildButton("allOpen", async () => {
      for (let index = 0; index < locationList.length; index++) {
        let item = locationList[index]
        SmartDialog.showAttach({
          targetId: item.title,
          alignment: item.alignment,
          maskColor: Color.Transparent,
          builder: targetLocationDialog,
        })
        await delay(300)
      }
    }, randomColor())
  }
  .borderRadius(12)
    .width(700)
    .padding(30)
    .backgroundColor(Color.White)
}

@Builder
function buildButton(title: string, onClick?: VoidCallback, bgColor?: ResourceColor) {
  Text(title)
    .backgroundColor(bgColor ?? "#4169E1")
    .constraintSize({ minWidth: 120, minHeight: 46 })
    .margin(10)
    .textAlign(TextAlign.Center)
    .fontColor(Color.White)
    .borderRadius(5)
    .onClick(onClick)
    .id(title)
}

@Builder
function targetLocationDialog() {
  Text("targetIdDialog")
    .fontSize(20)
    .fontColor(Color.White)
    .textAlign(TextAlign.Center)
    .padding(50)
    .borderRadius(12)
    .backgroundColor(randomColor())
}

attachLocation

Loading

對於Loading而言,應該有幾個比較明顯的特性

  • loading和dialog都存在頁面上,哪怕dialog開啟,loading都應該顯示dialog之上
  • loading應該具有單一特性,多次開啟loading,頁面也應該只存在一個loading
  • 重新整理特性,多次開啟loading,後續開啟的loading樣式,應該覆蓋之前開啟的loading樣式
  • loading使用頻率非常高,應該支援強大的擴充和極簡的使用

從上面列舉幾個特性而言,loading是一個非常特殊的dialog,所以需要針對其特性,進行定製化的實現

當然了,內部已經遮蔽了細節,在使用上,和dialog的使用沒什麼區別

預設loading

SmartDialog.showLoading()

loadingDefault

自定義Loading

  • 點選loading後,會再次開啟一個loading,從效果圖可以看出它的單一重新整理特性
export function loadingCustom() {
  SmartDialog.showLoading({
    builder: customLoading,
  })
}

@Builder
export function customLoading() {
  Column({ space: 5 }) {
    Text("again open loading").fontSize(16).fontColor(Color.White)
    LoadingProgress().width(80).height(80).color(Color.White)
  }
  .padding(20)
  .borderRadius(12)
  .onClick(() => loadingCustom())
  .backgroundColor(randomColor())
}

loadingCustom

最後

鴻蒙版的SmartDialog,相信會對開發鴻蒙的小夥伴們有一些幫助.

現在就業環境真是讓人頭皮發麻,現在的各種技術群裡,看到好多人公司各種拖欠工資,各種失業半年的情況

淦,不知道還能寫多長時間程式碼!

004B5DB3

相關文章