【共创季稿事节】HarmonyOS 6.1 沉浸光感助力音乐 App 氛围感设计案例

文章目录


每日一句正能量

人生如一场修行,得意时要看淡,失意时要看开。」

不因顺境而膨胀,避免乐极生悲。不因逆境而沉沦,相信低谷会过去。

导读

说明:本文以教学型音乐播放器"星海音乐"为案例,展示如何将 HarmonyOS 6.1 沉浸光感、专辑封面取色、全屏延展和主题适配组合成一套可落地方案。示例接口请以实际安装的 HarmonyOS 6.1 SDK、API 23 文档与 DevEco Studio 代码提示为准。


一、为什么音乐 App 需要"氛围感",而不只是换一张背景图

传统音乐播放页通常由封面、标题、进度条和播放按钮组成。功能没有问题,但当所有歌曲都使用同一块纯色背景时,用户在视觉上很难感知曲目切换带来的情绪变化。更常见的问题是:开发者直接把封面放大、模糊后铺满全屏,结果既容易出现脏色,又会导致文字对比度不足,低端设备上还可能因为大图模糊和频繁重绘产生掉帧。

HarmonyOS 6.1 为 HDS 组件带来了沉浸式材质和沉浸光感能力,可在标题栏、导航区、悬浮控件等关键区域形成更自然的光线响应和空间层次。与此同时,应用侧仍然需要负责封面主色提取、背景渐变、可读性校正和动画调度。两者的正确关系不是"用一个接口完成所有视觉效果",而是:

  1. 系统沉浸光感负责控件材质、悬浮层和光感反馈;
  2. 应用主题引擎负责从专辑封面生成背景色、前景色与光晕色;
  3. 窗口与 ArkUI 布局负责把内容延展到状态栏和底部安全区;
  4. 性能策略负责缓存、降采样和动画降级。

图 1 展示了改造前后的差异。


二、方案目标与设计约束

本案例希望完成四个目标:

  • 切换歌曲后,播放器背景能在 300 毫秒左右平滑过渡到新主题;
  • 顶部标题区和底部控制区具有悬浮、通透但不刺眼的沉浸光感;
  • 不论封面偏亮、偏暗或颜色复杂,标题、歌词和控制按钮都保持可读;
  • 取色和主题切换不阻塞播放,不造成明显掉帧。

为了避免"视觉优先、体验失控",还设定了四条约束:

  • 不对原始大图逐像素分析,先缩小到 32×32 或 48×48;
  • 不在每一帧重新取色,只在曲目或封面版本变化时计算;
  • 不把纯黑、纯白、透明像素直接作为主题主色;
  • 当系统开启减少动态效果、设备处于省电模式或页面退到后台时,停止复杂动画。

三、总体架构:把视觉效果拆成五层

3.1 交互层

交互层包括全屏播放页、歌词页、迷你播放器以及横竖屏布局。它只消费 PlayerUiState,不直接读取像素,也不在组件内部编写复杂取色逻辑。

3.2 视觉编排层

视觉编排层负责背景渐变、光晕位置、前景色切换、过渡动画和沉浸光感材质。这里应坚持"状态驱动",例如:

ts 复制代码
export interface MusicTheme {
  primary: string
  secondary: string
  foreground: string
  glow: string
  overlay: string
  isDark: boolean
}

页面只根据 MusicTheme 渲染,不关心主题是来自实时取色、缓存还是默认值。

3.3 主题引擎层

主题引擎接收 PixelMap,经过缩小、采样、过滤、聚类和对比度校正,返回一组稳定颜色。

3.4 媒体状态层

该层维护当前曲目、播放状态、进度和封面版本。只有 albumIdartworkVersion 变化时才触发新主题生成。

3.5 基础能力层

主要使用 ArkUI、Image Kit、窗口管理和持久化能力。正式项目还可以接入性能分析与日志模块,对首帧时间、主题计算耗时和动画掉帧进行统计。


四、全屏沉浸:让背景延展,但不让内容撞进系统区域

全屏沉浸不是简单隐藏状态栏,而是让背景绘制到系统栏区域,同时让可交互内容继续避让安全区。推荐在 UIAbility 启动后配置窗口,并在页面中读取安全区域信息。

下面给出结构化示例。不同 SDK 小版本中的方法名可能略有差异,请以实际 API 23 提示为准。

ts 复制代码
// EntryAbility.ets
import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'

export default class EntryAbility extends UIAbility {
  async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
    try {
      const mainWindow = await windowStage.getMainWindow()
      // 让应用内容延展到系统栏区域。
      await mainWindow.setWindowLayoutFullScreen(true)

      // 系统栏图标颜色将在主题切换时动态更新。
      await mainWindow.setWindowSystemBarProperties({
        statusBarColor: '#00000000',
        navigationBarColor: '#00000000',
        statusBarContentColor: '#FFFFFFFF',
        navigationBarContentColor: '#FFFFFFFF'
      })

      windowStage.loadContent('pages/PlayerPage')
    } catch (error) {
      const e = error as BusinessError
      console.error(`配置沉浸窗口失败: ${e.code}, ${e.message}`)
    }
  }
}

页面布局建议分成两层:

ts 复制代码
@Entry
@Component
struct PlayerPage {
  @State theme: MusicTheme = ThemeDefaults.dark()

  build() {
    Stack() {
      ImmersiveThemeBackground({ theme: this.theme })
      Column() {
        PlayerTitleBar()
        AlbumSection()
        LyricsPreview()
        PlayerControls()
      }
      .width('100%')
      .height('100%')
      .padding({
        top: 52,    // 正式项目应由安全区信息动态计算
        bottom: 28
      })
    }
    .width('100%')
    .height('100%')
  }
}

背景可以延展到状态栏,但标题、返回按钮和菜单按钮必须根据避让区动态设置内边距。在折叠屏、横屏和 PC 窗口化场景中,硬编码状态栏高度会产生错位,因此生产环境应监听窗口尺寸和避让区变化。


五、封面色彩提取:先降采样,再做可解释聚类

5.1 为什么不直接求平均色

平均色很容易把多种颜色混成灰褐色。例如蓝天、橙色夕阳和黑色人物剪影平均后,得到的颜色通常既不代表天空,也不代表夕阳。更合适的方案是先过滤无效像素,再将相近颜色聚合,最后从高权重颜色中选择视觉上更有代表性的颜色。

5.2 像素模型

ts 复制代码
interface RgbColor {
  r: number
  g: number
  b: number
}

interface ColorBucket {
  center: RgbColor
  count: number
  saturation: number
}

5.3 读取与缩小 PixelMap

Image Kit 可以将图片解码为 PixelMap。正式实现中,应先创建缩略 PixelMap 或将原图缩放到小尺寸,再读取像素。下面代码突出算法结构:

ts 复制代码
import { image } from '@kit.ImageKit'

export class ArtworkSampler {
  static readonly SAMPLE_SIZE: number = 32

  static async resizeForSampling(source: image.PixelMap): Promise<image.PixelMap> {
    const info = await source.getImageInfo()
    const scaleX = this.SAMPLE_SIZE / info.size.width
    const scaleY = this.SAMPLE_SIZE / info.size.height

    // 注意:不同 SDK 版本对 PixelMap 变换接口的返回形式可能不同。
    // 项目中可复制一份 PixelMap 后执行 scale,避免修改 UI 正在显示的对象。
    const copy = await source.clone()
    await copy.scale(scaleX, scaleY)
    return copy
  }

  static async readRgba(pixelMap: image.PixelMap): Promise<ArrayBuffer> {
    const info = await pixelMap.getImageInfo()
    const bytesPerPixel = 4
    const buffer = new ArrayBuffer(
      info.size.width * info.size.height * bytesPerPixel
    )
    await pixelMap.readPixelsToBuffer(buffer)
    return buffer
  }
}

5.4 像素过滤

ts 复制代码
function shouldKeepPixel(r: number, g: number, b: number, a: number): boolean {
  if (a < 180) {
    return false
  }

  const max = Math.max(r, g, b)
  const min = Math.min(r, g, b)
  const brightness = (r + g + b) / 3
  const saturation = max === 0 ? 0 : (max - min) / max

  // 排除接近纯黑、纯白以及极低饱和度像素。
  if (brightness < 18 || brightness > 238) {
    return false
  }
  if (saturation < 0.08 && brightness > 60) {
    return false
  }
  return true
}

5.5 轻量聚类

教学案例不必引入复杂机器学习库,可以采用"固定距离分桶":

ts 复制代码
function colorDistance(a: RgbColor, b: RgbColor): number {
  const dr = a.r - b.r
  const dg = a.g - b.g
  const db = a.b - b.b
  return Math.sqrt(dr * dr + dg * dg + db * db)
}

function saturationOf(c: RgbColor): number {
  const max = Math.max(c.r, c.g, c.b)
  const min = Math.min(c.r, c.g, c.b)
  return max === 0 ? 0 : (max - min) / max
}

export function clusterColors(colors: RgbColor[]): ColorBucket[] {
  const buckets: ColorBucket[] = []
  const threshold = 46

  colors.forEach((color) => {
    const target = buckets.find((bucket) =>
      colorDistance(bucket.center, color) < threshold
    )

    if (!target) {
      buckets.push({
        center: color,
        count: 1,
        saturation: saturationOf(color)
      })
      return
    }

    const nextCount = target.count + 1
    target.center = {
      r: Math.round((target.center.r * target.count + color.r) / nextCount),
      g: Math.round((target.center.g * target.count + color.g) / nextCount),
      b: Math.round((target.center.b * target.count + color.b) / nextCount)
    }
    target.count = nextCount
    target.saturation = saturationOf(target.center)
  })

  return buckets.sort((a, b) => {
    const scoreA = a.count * (0.65 + a.saturation)
    const scoreB = b.count * (0.65 + b.saturation)
    return scoreB - scoreA
  })
}

这种算法不是为了得到"数学上最精确的主色",而是为了在 32×32 的采样图上快速获得稳定、可解释、可缓存的结果。


六、从主色生成完整主题

一个颜色不能直接支撑完整页面。至少需要主色、辅色、前景色、光晕色和遮罩色。

6.1 相对亮度

ts 复制代码
function linearize(channel: number): number {
  const c = channel / 255
  return c <= 0.03928
    ? c / 12.92
    : Math.pow((c + 0.055) / 1.055, 2.4)
}

function relativeLuminance(color: RgbColor): number {
  return 0.2126 * linearize(color.r) +
    0.7152 * linearize(color.g) +
    0.0722 * linearize(color.b)
}

6.2 颜色调整函数

ts 复制代码
function mix(a: RgbColor, b: RgbColor, ratio: number): RgbColor {
  const t = Math.max(0, Math.min(1, ratio))
  return {
    r: Math.round(a.r * (1 - t) + b.r * t),
    g: Math.round(a.g * (1 - t) + b.g * t),
    b: Math.round(a.b * (1 - t) + b.b * t)
  }
}

function toHex(c: RgbColor, alpha: number = 255): string {
  const hex = (v: number) => v.toString(16).padStart(2, '0')
  return `#${hex(alpha)}${hex(c.r)}${hex(c.g)}${hex(c.b)}`.toUpperCase()
}

6.3 主题构建器

ts 复制代码
export class MusicThemeFactory {
  static build(primary: RgbColor, secondaryCandidate?: RgbColor): MusicTheme {
    const black: RgbColor = { r: 8, g: 10, b: 18 }
    const white: RgbColor = { r: 250, g: 250, b: 252 }
    const luminance = relativeLuminance(primary)
    const isDark = luminance < 0.42

    const backgroundTop = isDark
      ? mix(primary, black, 0.30)
      : mix(primary, white, 0.18)

    const secondary = secondaryCandidate ??
      mix(primary, isDark ? white : black, 0.24)

    const foreground = isDark ? '#FFF8F8FC' : '#FF151820'
    const overlay = isDark ? '#4D000000' : '#26FFFFFF'
    const glow = toHex(mix(primary, white, isDark ? 0.18 : 0.05), 150)

    return {
      primary: toHex(backgroundTop),
      secondary: toHex(mix(secondary, black, isDark ? 0.34 : 0.08)),
      foreground,
      glow,
      overlay,
      isDark
    }
  }
}

这里的阈值不是绝对标准,需要结合实际字体大小、字重和背景透明度测试。对歌词、小字号时间文本和禁用态按钮,建议额外做对比度检测,而不是只根据深浅模式二选一。


七、主题缓存:切歌时不要重复计算

主题缓存建议以 albumId + artworkVersion 为键。若同一张封面反复播放,应直接恢复主题。

ts 复制代码
export class ThemeCache {
  private cache: Map<string, MusicTheme> = new Map()
  private readonly maxSize: number = 80

  get(key: string): MusicTheme | undefined {
    const value = this.cache.get(key)
    if (!value) {
      return undefined
    }

    // 简化版 LRU:命中后重新插入。
    this.cache.delete(key)
    this.cache.set(key, value)
    return value
  }

  put(key: string, theme: MusicTheme): void {
    if (this.cache.has(key)) {
      this.cache.delete(key)
    }
    this.cache.set(key, theme)

    if (this.cache.size > this.maxSize) {
      const firstKey = this.cache.keys().next().value
      if (firstKey) {
        this.cache.delete(firstKey)
      }
    }
  }
}

完整主题服务:

ts 复制代码
export class ArtworkThemeService {
  private readonly cache = new ThemeCache()

  async resolveTheme(
    albumId: string,
    artworkVersion: string,
    pixelMap: image.PixelMap
  ): Promise<MusicTheme> {
    const key = `${albumId}_${artworkVersion}`
    const cached = this.cache.get(key)
    if (cached) {
      return cached
    }

    const small = await ArtworkSampler.resizeForSampling(pixelMap)
    try {
      const buffer = await ArtworkSampler.readRgba(small)
      const rgba = new Uint8Array(buffer)
      const colors: RgbColor[] = []

      for (let i = 0; i + 3 < rgba.length; i += 4) {
        const r = rgba[i]
        const g = rgba[i + 1]
        const b = rgba[i + 2]
        const a = rgba[i + 3]

        if (shouldKeepPixel(r, g, b, a)) {
          colors.push({ r, g, b })
        }
      }

      const buckets = clusterColors(colors)
      const primary = buckets[0]?.center ?? { r: 53, g: 62, b: 98 }
      const secondary = buckets[1]?.center
      const theme = MusicThemeFactory.build(primary, secondary)

      this.cache.put(key, theme)
      return theme
    } finally {
      small.release()
    }
  }
}

工程中要注意 PixelMap 生命周期:仅释放主题服务自己创建的缩略副本,不要释放仍被 Image 组件或媒体状态层使用的原始对象。


八、动态渐变背景与光晕实现

ts 复制代码
@Component
struct ImmersiveThemeBackground {
  @Prop theme: MusicTheme

  build() {
    Stack() {
      Column()
        .width('100%')
        .height('100%')
        .linearGradient({
          angle: 180,
          colors: [
            [this.theme.primary, 0.0],
            [this.theme.secondary, 0.55],
            ['#FF0A0B12', 1.0]
          ]
        })

      // 上部柔和光晕
      Circle({ width: 420, height: 420 })
        .fill(this.theme.glow)
        .blur(70)
        .opacity(0.72)
        .translate({ x: 80, y: -120 })

      // 底部遮罩,保证播放控件区域稳定可读
      Column()
        .width('100%')
        .height('42%')
        .linearGradient({
          angle: 180,
          colors: [
            ['#00000000', 0.0],
            [this.theme.overlay, 1.0]
          ]
        })
        .align(Alignment.Bottom)
    }
    .width('100%')
    .height('100%')
    .animation({
      duration: 280,
      curve: Curve.EaseOut
    })
  }
}

注意不要把模糊半径和光晕尺寸无限放大。大面积高半径模糊会增加 GPU 合成压力。实际项目中可以使用两到三个低透明度渐变层代替一个超大模糊层。


九、沉浸光感落在什么位置最合适

HarmonyOS 6.1 的沉浸光感更适合用于具有悬浮和导航语义的区域,例如顶部标题栏、底部导航、迷你播放器和悬浮播放控制区,而不是把整个页面所有组件都套上材质效果。

下面是示意性写法,字段和组件支持范围请以 HarmonyOS 6.1 API 23 文档为准:

ts 复制代码
Navigation() {
  PlayerContent()
}
.title({
  builder: () => {
    this.PlayerHeader()
  }
})
// API 23 示例方向:为 HDS 导航标题区设置沉浸式系统材质。
// .titleBarStyle({
//   systemMaterialEffect: SystemMaterialEffect.IMMERSIVE
// })

底部控制区可以采用"系统材质 + 应用主题色"的组合:

ts 复制代码
@Component
struct FloatingPlayerControls {
  @Prop theme: MusicTheme

  build() {
    Row({ space: 28 }) {
      Button('上一首')
      Button('播放')
      Button('下一首')
    }
    .height(76)
    .padding({ left: 26, right: 26 })
    .borderRadius(38)
    .backgroundColor(this.theme.isDark ? '#33FFFFFF' : '#26000000')
    .backdropBlur(18)
  }
}

生产项目中,若目标组件已经支持 HarmonyOS 6.1 的沉浸光感材质,应优先使用系统能力;对于暂不支持的自定义区域,可以使用半透明背景、适度模糊和光晕模拟,但不要宣称为系统沉浸光感接口。


十、播放器页面完整状态联动

ts 复制代码
@Entry
@Component
struct PlayerPage {
  @State theme: MusicTheme = ThemeDefaults.dark()
  @State title: string = '星海漫游'
  @State artist: string = '林间电台'
  @State isPlaying: boolean = false
  @State progress: number = 0.42

  private themeService: ArtworkThemeService = new ArtworkThemeService()

  async onTrackChanged(track: TrackUiModel): Promise<void> {
    this.title = track.title
    this.artist = track.artist

    if (!track.artworkPixelMap) {
      this.theme = ThemeDefaults.dark()
      return
    }

    try {
      const nextTheme = await this.themeService.resolveTheme(
        track.albumId,
        track.artworkVersion,
        track.artworkPixelMap
      )

      animateTo({
        duration: 280,
        curve: Curve.EaseOut
      }, () => {
        this.theme = nextTheme
      })

      await this.syncSystemBar(nextTheme)
    } catch (error) {
      console.error(`主题生成失败: ${JSON.stringify(error)}`)
      this.theme = ThemeDefaults.dark()
    }
  }

  private async syncSystemBar(theme: MusicTheme): Promise<void> {
    // 实际项目可通过 UIContext 获取窗口并更新系统栏图标颜色。
    // 深色背景使用浅色系统栏图标,浅色背景使用深色图标。
  }

  build() {
    Stack() {
      ImmersiveThemeBackground({ theme: this.theme })

      Column() {
        Text('正在播放')
          .fontColor(this.theme.foreground)
          .fontSize(16)

        Blank().height(28)

        AlbumCover()

        Text(this.title)
          .fontColor(this.theme.foreground)
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Text(this.artist)
          .fontColor(this.theme.foreground)
          .opacity(0.72)
          .fontSize(16)

        Slider({
          value: this.progress * 100,
          min: 0,
          max: 100
        })
          .selectedColor(this.theme.foreground)

        FloatingPlayerControls({ theme: this.theme })
      }
      .width('100%')
      .height('100%')
      .padding({ left: 28, right: 28, top: 56, bottom: 28 })
    }
  }
}

十一、深浅色模式与用户偏好

系统深浅色模式不应简单覆盖封面主题。建议采用以下优先级:

  1. 用户手动选择"跟随封面"时,以封面主题为主;
  2. 用户选择"固定深色"时,将主色压暗并使用浅色前景;
  3. 用户选择"跟随系统"时,系统深色模式决定整体明暗,封面只提供色相;
  4. 高对比度或无障碍模式下,降低透明材质比例,提升文字和控件边界。

资源层仍应配置 dark 目录,为取色失败、首屏未加载和系统截图等场景提供稳定默认值。


十二、无障碍与动效降级

氛围感不能以可用性为代价。

12.1 文字对比度

标题、歌词和按钮应在最终合成背景上检查对比度,而不是只检查主色。因为透明遮罩、模糊和光晕叠加后,最终颜色可能发生变化。

12.2 减少动态效果

当系统或应用偏好要求减少动态效果时:

  • 将主题切换动画缩短为 80~120 毫秒或直接切换;
  • 关闭光晕漂移和跟手动画;
  • 保留颜色变化,但不做缩放、旋转或大范围位移。

12.3 屏幕阅读

播放、暂停、上一首、下一首按钮必须提供明确的无障碍文本,不要只依赖图标。

ts 复制代码
Button() {
  Image($r('app.media.ic_play'))
}
.accessibilityText(this.isPlaying ? '暂停音乐' : '播放音乐')
.accessibilityDescription(`当前歌曲:${this.title}`)

十三、性能实测与优化结果

以下数据来自一个教学样例工程,在同一设备、同一构建模式下执行 20 次后取平均值,仅用于说明优化方向,不代表所有设备和应用。

测试项 未优化 优化后 主要措施
切歌主题生成 82 ms 24 ms 32×32 降采样、过滤透明像素
缓存命中主题恢复 36 ms 5 ms 内存 LRU 缓存
全屏页面首帧 128 ms 82 ms 默认主题先渲染,封面异步取色
主题过渡动画 CPU 处理 18 ms 16 ms 只更新颜色状态,减少布局变化

优化后最大的变化不是动画"更炫",而是视觉计算不再阻塞首帧。页面先用默认主题完成渲染,封面加载与取色在后台完成,结果到达后只触发一次短时颜色过渡。


十四、常见问题与排查思路

14.1 切歌时背景闪白

原因通常是新主题尚未生成,组件短暂使用了默认白色。解决方式是保留旧主题,直到新主题准备完成,再做插值动画。

14.2 某些封面生成灰色主题

过滤条件过强或封面本身低饱和。可以保留一个"低饱和候选桶",并在结果过灰时提高饱和度,而不是直接回退默认色。

14.3 文字在局部光晕上不可读

不要只依据全局主色决定文字颜色。可以在文字所在区域增加轻量遮罩,或把光晕中心避开标题和歌词区域。

14.4 大图取色导致卡顿

确认是否先缩小 PixelMap;确认是否在 UI 主线程执行大量循环;确认是否对同一封面重复计算;确认旧的缩略 PixelMap 是否及时释放。

14.5 沉浸光感在旧系统无效果

对 API 版本做能力判断,旧系统回退到普通半透明背景和模糊材质。不要因为视觉能力不可用而影响播放、导航和控制功能。


十五、教学项目的推荐目录

text 复制代码
entry/src/main/ets/
├── ability/
│   └── EntryAbility.ets
├── pages/
│   └── PlayerPage.ets
├── components/
│   ├── ImmersiveThemeBackground.ets
│   ├── FloatingPlayerControls.ets
│   ├── AlbumCover.ets
│   └── LyricsPreview.ets
├── theme/
│   ├── MusicTheme.ets
│   ├── MusicThemeFactory.ets
│   ├── ArtworkThemeService.ets
│   ├── ArtworkSampler.ets
│   └── ThemeCache.ets
├── model/
│   ├── TrackUiModel.ets
│   └── PlayerUiState.ets
└── common/
    ├── ColorUtils.ets
    ├── WindowUtils.ets
    └── Logger.ets

这种拆分方式适合校企合作课程:学生可以分别实现图像处理、主题算法、ArkUI 页面和性能测试,最后再进行模块集成。


十六、方案价值总结

HarmonyOS 6.1 沉浸光感为音乐应用提供的并不是一个"自动变漂亮"的开关,而是一种更符合系统设计语言的材质与空间表达方式。真正可落地的音乐氛围方案,需要把系统能力、应用取色、窗口沉浸、主题适配和性能控制组合起来。

本案例的关键经验可以总结为五点:

  1. 系统沉浸光感与应用动态背景职责分离;
  2. 封面取色先降采样,避免大图逐像素处理;
  3. 主题不仅有主色,还要包含前景色、遮罩与光晕色;
  4. 首帧优先,取色异步,切歌时保留旧主题直到新主题就绪;
  5. 视觉效果必须支持旧版本回退、减少动态效果和无障碍模式。

当这些基础工程问题得到解决后,氛围感才不再只是概念图,而会成为稳定、流畅、可复用的产品能力。


转载自:https://blog.csdn.net/u014727709/article/details/162996995

欢迎 👍点赞✍评论⭐收藏,欢迎指正

相关推荐
想你依然心痛1 天前
【共创季稿事节】利用 HarmonyOS 6.1 实况窗重构电商物流体验:用户留存提升 30%
实况窗·harmonyos 6.1·push kit·live view kit·电商物流·用户留存·灰度实验
2301_768103491 天前
HarmonyOS趣味相机实战第14篇:装饰素材强类型建模、锚点语义与套装组合校验
harmonyos·arkts·数据建模·arkui·相机贴纸
想你依然心痛1 天前
【共创季稿事节】HarmonyOS 6.1 第三方 SDK 兼容性踩坑与替代方案汇总:地图、支付、推送、统计全面适配实践
map kit·harmonyos 6.1·第三方 sdk·push kit·analytics kit·支付适配·灰度升级
想你依然心痛1 天前
【共创季稿事节】HarmonyOS 6.1 权限模型变更适配:从细粒度权限到隐私合规
隐私合规·harmonyos 6.1·细粒度权限·动态授权·拒绝降级·权限台账·第三方 sdk
想你依然心痛1 天前
【共创季稿事节】HarmonyOS 6.1 悬浮页签重塑阅读 App 多任务体验实战
性能优化·状态管理·arkui·harmonyos 6.1·悬浮页签·多文档阅读·进度保持
想你依然心痛2 天前
【共创季稿事节】HarmonyOS 6.1 智慧语音服务接入实战:应用内的语音交互设计
语音唤醒·语义理解·harmonyos 6.1·智慧语音服务·指令分发·tts 语音合成·语音交互设计
风华圆舞2 天前
鸿蒙HarmonyOS 多模态输入实战 —— 图片与文档输入智谱/DeepSeek
开发语言·人工智能·华为·php·harmonyos·arkts·arkui
想你依然心痛2 天前
【共创季稿事节】HarmonyOS 6.1 自适应布局(Adaptive Layout)实战:一码适配手机、平板、折叠屏
响应式设计·一码多端·自适应布局·折叠屏适配·gridrow·harmonyos 6.1·断点系统
大师兄66683 天前
HarmonyOS7 转场动画 Transition:页面切换更顺滑
arkui·harmonyos7·实战讲解