横扫鸿蒙弹窗乱象,SmartDialog出世

前言

但凡用过鸿蒙原生弹窗的小伙伴,就能体会到它们是有多么的难用和奇葩,什么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 模拟器目前有些问题,会导致动画闪烁,请忽略;注:真机动画丝滑流畅,无任何问题

极简用法

typescript 复制代码
// dialog
SmartDialog.show({
  builder: dialogArgs,
  builderArgs: Math.random(),
})

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

// loading
SmartDialog.showLoading()

安装

typescript 复制代码
ohpm install ohos_smart_dialog 

配置

下述的配置项,可能会有一点多,但,这也是为了极致的体验;同时也是无奈之举,相关配置难以在内部去闭环处理,只能在外部去配置

这些配置,只需要配置一次,后续无需关心

完成下述的配置后,你将可以在任何地方使用弹窗,没有任何限制

初始化

  • 因为弹窗需要处理跨页面交互,必须要监控路由
typescript 复制代码
@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%')
  }
}

返回事件监听

别问我为啥返回事件的监听,处理的这么不优雅,鸿蒙里面没找全局返回事件监听,我也没辙。。。

  • 如果你无需处理返回事件,可以使用下述写法
typescript 复制代码
// Entry页面处理
@Entry
@Component
struct Index {
  onBackPress(): boolean | void {
    return OhosSmartDialog.onBackPressed()()
  }
}

// 路由子页面
struct JumpPage {
  build() {
    NavDestination() {
      // ....
    }
    .onBackPressed(OhosSmartDialog.onBackPressed())
  }
}
  • 如果你需要处理返回事件,在OhosSmartDialog.onBackPressed()中传入你的方法即可
typescript 复制代码
// 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
typescript 复制代码
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)
    // ...
  }
}

适配暗黑模式

  • 为了极致的体验,深色模式切换时,打开态弹窗也应刷新为对应模式的样式,故需要进行下述配置
typescript 复制代码
export default class EntryAbility extends UIAbility {  
  onConfigurationUpdate(newConfig: Configuration): void {  
    OhosSmartDialog.onConfigurationUpdate(newConfig)  
  }  
}

SmartConfig

  • 支持全局配置弹窗的默认属性
typescript 复制代码
function init() {
  // show
  SmartDialog.config.custom.maskColor = "#75000000"
  SmartDialog.config.custom.alignment = Alignment.Center

  // showAttach
  SmartDialog.config.attach.attachAlignmentType = SmartAttachAlignmentType.center
}
  • 检查弹窗是否存在
typescript 复制代码
// 检查当前是否有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 自定样式十分简单
typescript 复制代码
SmartDialog.showLoading({ builder: customLoading })

但是对于大家来说,肯定是想用 SmartDialog.showLoading() 这种简单写法,所以支持自定义全局默认样式

  • 需要在 OhosSmartDialog 上配置自定义的全局默认样式
typescript 复制代码
@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 样式
typescript 复制代码
SmartDialog.showLoading()

// 支持入参,可以在特殊场景下灵活配置
SSmartDialog.showLoading({ builderArgs: 1 })

CustomDialog

  • 下方会共用的方法
typescript 复制代码
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));
}

传参弹窗

typescript 复制代码
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())
}

多位置弹窗

typescript 复制代码
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())
}

跨页面交互

  • 正常使用,无需设置什么参数
typescript 复制代码
export function customJumpPage() {
  SmartDialog.show({
    builder: dialogJumpPage,
  })
}

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

关闭指定弹窗

typescript 复制代码
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)
}

自定义遮罩

typescript 复制代码
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())
}

AttachDialog

默认定位

typescript 复制代码
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())
}

多方向定位

typescript 复制代码
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())
}

Loading

对于Loading而言,应该有几个比较明显的特性

  • loading和dialog都存在页面上,哪怕dialog打开,loading都应该显示dialog之上
  • loading应该具有单一特性,多次打开loading,页面也应该只存在一个loading
  • 刷新特性,多次打开loading,后续打开的loading样式,应该覆盖之前打开的loading样式
  • loading使用频率非常高,应该支持强大的拓展和极简的使用

从上面列举几个特性而言,loading是一个非常特殊的dialog,所以需要针对其特性,进行定制化的实现

当然了,内部已经屏蔽了细节,在使用上,和dialog的使用没什么区别

默认loading

typescript 复制代码
SmartDialog.showLoading()

自定义Loading

  • 点击loading后,会再次打开一个loading,从效果图可以看出它的单一刷新特性
typescript 复制代码
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())
}

最后

鸿蒙版的SmartDialog,相信会对开发鸿蒙的小伙伴们有一些帮助~.~

现在就业环境真是让人头皮发麻,现在的各种技术群里,看到好多人公司各种拖欠工资,各种失业半年的情况

淦,不知道还能写多长时间代码!

相关推荐
xiangpanf29 分钟前
Laravel 10.x重磅升级:五大核心特性解析
android
robotx3 小时前
安卓线程相关
android
消失的旧时光-19434 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon5 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon5 小时前
VSYNC 信号完整流程2
android
dalancon5 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
不爱吃糖的程序媛6 小时前
OpenHarmony 工程结构剖析
harmonyos
用户69371750013846 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android6 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才7 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android