HarmonyOS应用<节气通>开发第1篇:启动页开发——留下第一印象的2秒

引言

启动页(Splash Screen)是用户打开应用时看到的第一个界面,虽然只停留短短2秒,但它决定了用户对应用的第一印象。一个好的启动页应该简洁美观,同时利用这段时间预加载数据,提升用户体验。

为什么启动页如此重要?

用户在点击应用图标后,会经历一个"冷启动"过程。在这个过程中:

  • 系统需要加载应用框架(耗时约300-500ms)
  • 应用需要初始化基础服务(耗时约200-500ms)
  • 最后才显示启动页

如果这个等待时间没有内容展示,用户会看到一片空白,产生"应用没反应"的错觉。而一个精心设计的启动页可以:

  1. 填补等待空白:让用户知道应用正在启动
  2. 传递品牌价值:通过Logo和slogan强化品牌形象
  3. 预加载数据:利用等待时间准备首屏内容

通过本文,你将掌握在鸿蒙中:

  • 如何创建启动页并实现自动跳转
  • 如何使用定时器控制页面切换
  • 如何根据季节动态设置背景图
  • 如何优化启动页的视觉体验

学习目标

完成本文后,你将能够:

  • ✅ 使用Timer实现定时跳转
  • ✅ 实现季节动态背景图
  • ✅ 设计简洁美观的启动页UI
  • ✅ 处理页面跳转的路由异常
  • ✅ 避免启动页的性能问题
  • ✅ 设计可复用的启动页组件

需求分析

功能模块设计

模块 功能描述 技术要点
UI展示 Logo、名称、背景图 Stack布局、Image组件
季节背景 根据季节动态切换背景 Date API、条件判断
定时跳转 2秒后自动跳转 setTimeout、router
异常处理 路由跳转失败处理 try-catch
数据预加载 提前加载首屏数据 async/await

架构设计

启动页在应用架构中的位置

启动页是用户旅程的起点,但它并不是孤立的。在整体架构中,启动页需要与以下模块协作:

复制代码
┌─────────────────────────────────────────────────────────────┐
│                     应用启动流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 系统启动                                                │
│     └── AbilityStage.onCreate()                            │
│          │                                                 │
│          ▼                                                 │
│  2. 应用初始化                                              │
│     └── 初始化日志服务、存储服务、网络服务等                  │
│          │                                                 │
│          ▼                                                 │
│  3. 启动页显示                                              │
│     └── Splash.ets (本文重点)                               │
│          ├── 显示Logo和品牌信息                             │
│          ├── 预加载首屏数据                                │
│          └── 定时/条件触发跳转                              │
│          │                                                 │
│          ▼                                                 │
│  4. 首页显示                                                │
│     └── Index.ets                                          │
│          └── 使用预加载数据快速渲染                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

状态管理架构

启动页涉及的状态相对简单,但仍需要合理设计:

typescript 复制代码
// 启动页状态分类
interface SplashState {
  // UI状态 - 使用@State
  opacity: number;           // 淡入动画透明度
  isReady: boolean;          // 是否可以跳转

  // 持久化状态 - 使用@StorageLink
  hasOnboarded: boolean;     // 是否已完成引导
  lastVisitTime: number;     // 上次访问时间

  // 只读配置 - 使用@StorageProp
  appName: string;           // 应用名称
  enableSeasonBg: boolean;    // 是否启用季节背景
}

状态选择原则

状态类型 装饰器 说明
动画透明度 @State 组件私有,只在启动页使用
引导状态 @StorageLink 需要持久化,跨页面共享
应用配置 @StorageProp 只读配置,不需要修改

生命周期管理

启动页的生命周期需要特别注意资源管理:

typescript 复制代码
@Entry
@Component
struct Splash {
  private timerId: number = -1;  // 定时器ID
  private preloadTask: Promise<void> | null = null;  // 预加载任务

  aboutToAppear() {
    // 1. 启动页显示时,开始预加载数据
    this.preloadTask = this.preloadData();

    // 2. 启动淡入动画
    this.playFadeInAnimation();

    // 3. 设置跳转定时器
    this.scheduleNavigation();
  }

  aboutToDisappear() {
    // 4. 清理定时器,防止内存泄漏
    this.clearTimer();

    // 5. 取消未完成的预加载任务(可选)
    // 如果预加载耗时过长,页面已经跳转,可以选择取消
  }
}

为什么生命周期顺序重要?

  • aboutToAppear:页面即将显示,可以开始异步操作
  • build:渲染UI,不应执行耗时操作
  • aboutToDisappear:页面即将销毁,必须清理资源

异常处理架构

启动页可能遇到的异常及处理策略:

异常场景 影响 处理策略
路由跳转失败 用户无法进入首页 记录日志,显示错误提示
数据预加载失败 首页可能显示空白 使用缓存数据降级
图片资源缺失 显示默认背景 提供备选资源
定时器未清理 内存泄漏 在aboutToDisappear中清理

核心实现

步骤1: 创建启动页基础结构

功能说明

创建Splash页面,包含季节动态背景图、Logo、应用名称,使用Stack布局实现层叠效果。

运行效果
完整代码
typescript 复制代码
// pages/Splash.ets
import router from '@ohos.router';

@Entry
@Component
struct Splash {
  // 根据季节获取背景图
  private getSeasonBg(): string {
    const month = new Date().getMonth() + 1; // 1-12
    if (month >= 3 && month <= 5) {
      return 'bg/seasons/chun.png'; // 春季
    } else if (month >= 6 && month <= 8) {
      return 'bg/seasons/xia.png'; // 夏季
    } else if (month >= 9 && month <= 11) {
      return 'bg/seasons/qiu.png'; // 秋季
    } else {
      return 'bg/seasons/dong.png'; // 冬季
    }
  }

  build() {
    Stack({ alignContent: Alignment.Center }) {
      // 根据季节显示背景图
      Image($rawfile(this.getSeasonBg()))
        .width('100%')
        .height('100%')
        .objectFit(ImageFit.Cover);

      Column({ space: 16 }) {
        // 标题区域 - 使用毛玻璃效果
        Column({ space: 8 }) {
          Text('节气通')
            .fontSize(36)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')

          Text('把握时令之美,传承东方智慧')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .opacity(0.9)
        }
        .padding({ top: 20, right: 32, bottom: 20, left: 32 })
        .backgroundColor('rgba(0, 0, 0, 0.1)')
        .borderRadius(20)
        .backdropBlur(10)

        // Logo
        Image($r('app.media.logo'))
          .width(120)
          .height(120)
          .borderRadius(60)
          .backgroundColor('rgba(255, 255, 255, 0.2)')
          .padding(8)
          .shadow({ radius: 12, color: 'rgba(0, 0, 0, 0.3)', offsetX: 0, offsetY: 4 })
      }
    }
    .width('100%')
    .height('100%')
    .onAppear(() => {
      setTimeout(() => {
        try {
          router.replaceUrl({
            url: 'pages/Index'
          });
        } catch (err) {
          console.error(`Router error: ${JSON.stringify(err)}`);
        }
      }, 2000);
    })
  }
}

步骤2: 实现定时跳转

typescript 复制代码
.onAppear(() => {
  setTimeout(() => {
    try {
      router.replaceUrl({
        url: 'pages/Index'
      });
    } catch (err) {
      console.error(`Router error: ${JSON.stringify(err)}`);
    }
  }, 2000);
})

步骤3: 数据预加载

typescript 复制代码
import common from '@ohos.app.ability.common';
import { StorageService } from '../services/StorageService';

@Entry
@Component
struct Splash {
  private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;

  aboutToAppear() {
    // 预加载数据(后台执行)
    this.preloadData();

    // 定时跳转
    setTimeout(() => {
      try {
        router.replaceUrl({ url: 'pages/Index' });
      } catch (err) {
        console.error(`Router error: ${JSON.stringify(err)}`);
      }
    }, 2000);
  }

  async preloadData() {
    try {
      const storageService = new StorageService(this.context);
      await storageService.init();
    } catch (error) {
      console.error('预加载数据失败:', error);
    }
  }

  build() {
    // ... UI代码
  }
}

步骤4: 淡入动画

typescript 复制代码
@Entry
@Component
struct Splash {
  @State opacity: number = 0;

  aboutToAppear() {
    animateTo({
      duration: 800,
      curve: Curve.EaseOut
    }, () => {
      this.opacity = 1;
    });

    setTimeout(() => {
      try {
        router.replaceUrl({ url: 'pages/Index' });
      } catch (err) {
        console.error(`Router error: ${JSON.stringify(err)}`);
      }
    }, 2000);
  }

  build() {
    Stack() {
      Image($rawfile(this.getSeasonBg()))
        .width('100%')
        .height('100%')
        .objectFit(ImageFit.Cover)

      Column() {
        // 内容...
      }
    }
    .width('100%')
    .height('100%')
    .opacity(this.opacity)
  }
}

设计思路

方案对比

在实现启动页时,我们面临多个设计决策。以下是方案对比分析:

决策1: 背景图方案
方案 优点 缺点 适用场景
固定背景图 实现简单,体积小 缺乏动态感 简单应用
季节背景图 动态变化,贴合主题 需准备4套图片 推荐
轮播背景图 内容丰富 开发复杂,可能喧宾夺主 内容型应用

选择理由:应用主题是二十四节气文化,季节背景能增强用户对"时令"概念的感知,让首次打开应用的用户立刻感受到应用的核心价值。


决策2: 跳转时机
方案 优点 缺点 适用场景
固定时间跳转 实现简单,用户可预期 可能浪费用户等待时间 推荐
数据加载完成后跳转 不浪费等待时间 用户可能等待过久 数据量大的应用
用户点击跳转 完全由用户控制 用户可能忘记点击 需要强制展示场景

选择理由:当前版本数据量较小,2秒足够完成数据预加载,固定时间跳转能给用户稳定的预期。


决策3: 跳转方式
方式 特点 是否保留历史
router.pushUrl 推入新页面 保留,用户可返回
router.replaceUrl 替换当前页面 不保留,无法返回
router.back 返回上一页 需要有历史记录

选择理由:启动页是应用的"入口",用户不应该能够返回到启动页。replaceUrl确保启动页不会留在导航栈中。


决策4: 动画效果
动画类型 效果 推荐度
无动画 立即显示 ❌ 突兀
淡入动画 透明度渐变 推荐
缩放动画 Logo从小放大 ⚠️ 可能分散注意力
组合动画 多个动画同时播放 ⚠️ 过于复杂

选择理由:简单的淡入动画既能平滑过渡,又不会过于花哨,适合作为品牌展示。


关键实现细节

Stack布局的层叠原理
typescript 复制代码
Stack({ alignContent: Alignment.Center }) {
  // 第一个子组件:背景图(最底层)
  Image($rawfile('bg.png'))

  // 第二个子组件:半透明遮罩(中间层)
  Column()
    .backgroundColor('rgba(0,0,0,0.3)')

  // 第三个子组件:内容(最上层)
  Column() {
    Text('节气通')
    Image($r('app.media.logo'))
  }
}

为什么使用Stack而不是Column嵌套?

如果用Column实现:

typescript 复制代码
// ❌ 不推荐:Column嵌套实现背景
Column() {
  Image($rawfile('bg.png'))  // 会被内容撑开
  Column() {  // 需要绝对定位
    Text('节气通')
  }
}

Stack的优势:

  • 自动填满父容器
  • 子组件按z-index层叠
  • 不需要手动计算位置

毛玻璃效果的实现原理
typescript 复制代码
Column() {
  // 内容...
}
.backgroundColor('rgba(0, 0, 0, 0.1)')  // 半透明黑色背景
.backdropBlur(10)                       // 模糊半径10
.borderRadius(20)                       // 圆角

参数说明

  • backgroundColor:使用rgba设置半透明背景,确保文字可读性
  • backdropBlur:模糊半径,值越大越模糊
  • borderRadius:圆角,让毛玻璃效果边缘更柔和

组件变体

变体1: 带品牌动画的启动页

如果需要更炫酷的效果,可以添加Logo入场动画:

typescript 复制代码
@Entry
@Component
struct AnimatedSplash {
  @State logoScale: number = 0.5;
  @State logoOpacity: number = 0;
  @State contentOpacity: number = 0;

  aboutToAppear() {
    // 1. Logo缩放+淡入
    animateTo({
      duration: 600,
      curve: Curve.EaseOut
    }, () => {
      this.logoScale = 1;
      this.logoOpacity = 1;
    });

    // 2. 内容淡入(延迟200ms)
    setTimeout(() => {
      animateTo({ duration: 400 }, () => {
        this.contentOpacity = 1;
      });
    }, 200);

    // 3. 定时跳转
    setTimeout(() => {
      router.replaceUrl({ url: 'pages/Index' });
    }, 2500);
  }

  build() {
    Stack() {
      Image($rawfile(this.getSeasonBg()))
        .width('100%')
        .height('100%')

      Column({ space: 24 }) {
        Image($r('app.media.logo'))
          .width(120)
          .height(120)
          .scale({ x: this.logoScale, y: this.logoScale })
          .opacity(this.logoOpacity)

        Column({ space: 8 }) {
          Text('节气通')
            .fontSize(36)
            .fontOpacity(this.contentOpacity)
        }
      }
    }
    .width('100%')
    .height('100%')
  }
}

动画时序

复制代码
0ms      ──────────────────────────────────► 2500ms
│                                                    │
├─ Logo动画 ─────────►                                  │
│  0ms → 600ms      │                                  │
│                   ├─ 内容动画 ───────►                │
│                   │  200ms → 600ms                   │
│                   │                                  │
│                   └─ 定时跳转 ────────────────────────►
│                            2500ms

变体2: 带进度指示的启动页

如果预加载数据量较大,可以显示加载进度:

typescript 复制代码
@Entry
@Component
struct ProgressSplash {
  @State progress: number = 0;
  @State statusText: string = '正在加载...';

  aboutToAppear() {
    this.preloadWithProgress();
  }

  async preloadWithProgress() {
    const steps = [
      { text: '正在加载配置...', weight: 20 },
      { text: '正在加载数据...', weight: 50 },
      { text: '正在准备界面...', weight: 80 },
      { text: '即将进入...', weight: 100 }
    ];

    for (const step of steps) {
      this.statusText = step.text;
      await this.simulateLoad(step.weight);
    }

    router.replaceUrl({ url: 'pages/Index' });
  }

  async simulateLoad(targetProgress: number) {
    while (this.progress < targetProgress) {
      this.progress += 5;
      await new Promise(resolve => setTimeout(resolve, 50));
    }
  }

  build() {
    Stack() {
      Image($rawfile(this.getSeasonBg()))
        .width('100%')
        .height('100%')

      Column({ space: 24 }) {
        Image($r('app.media.logo'))
          .width(100)
          .height(100)

        Text(this.statusText)
          .fontSize(14)
          .fontColor('#FFFFFF')

        Progress({ value: this.progress, total: 100 })
          .width(200)
          .color('#4A9B6D')
      }
    }
    .width('100%')
    .height('100%')
  }
}

变体3: 判断是否显示启动页

如果用户已经完成引导,不想每次都显示启动页:

typescript 复制代码
@Entry
@Component
struct SmartSplash {
  @StorageLink('hasOnboarded') hasOnboarded: boolean = false;

  aboutToAppear() {
    if (this.hasOnboarded) {
      // 已完成引导,直接跳转
      router.replaceUrl({ url: 'pages/Index' });
    } else {
      // 显示启动页
      this.startSplashFlow();
    }
  }

  startSplashFlow() {
    // 正常启动页流程
    setTimeout(() => {
      router.replaceUrl({ url: 'pages/Onboarding' });
    }, 2000);
  }

  build() {
    // 启动页UI
  }
}

常见问题与解决方案

问题1: 启动页白屏闪烁

现象描述

应用启动时先显示白屏,然后才显示启动页。

原因分析

  • 系统默认使用白色窗口背景
  • 应用初始化需要时间,期间没有内容可显示
  • 启动页加载完成前,用户看到空白

解决方案

json 复制代码
// 在 entry/src/main/resources/base/element/color.json 中配置窗口背景色
{
  "color": [
    {
      "name": "window_background",
      "value": "#F8F7F2"
    }
  ]
}

同时在启动页加载完成前,可以在 AbilityStage 中设置背景:

typescript 复制代码
// AbilityStage.ets
import AbilityStage from '@ohos.app.ability.AbilityStage';

export default class MyAbilityStage extends AbilityStage {
  onCreate() {
    // 设置启动背景色,避免白屏
    const window = this.context.getWindow();
    window.setWindowBackgroundColor('#F8F7F2');
  }
}

问题2: 定时器内存泄漏

现象描述

页面跳转后,定时器仍在后台运行,可能导致内存泄漏。

原因分析

  • setTimeout 是异步操作
  • 即使页面已经销毁,回调函数可能仍被执行
  • 如果回调中引用了已销毁的组件,会导致问题

解决方案

typescript 复制代码
@Entry
@Component
struct Splash {
  private timerId: number = -1;

  aboutToAppear() {
    // 保存定时器ID
    this.timerId = setTimeout(() => {
      router.replaceUrl({ url: 'pages/Index' });
    }, 2000);
  }

  aboutToDisappear() {
    // 清理定时器
    if (this.timerId !== -1) {
      clearTimeout(this.timerId);
      this.timerId = -1;
    }
  }

  build() {
    // UI代码
  }
}

最佳实践

  • 始终保存定时器ID
  • 在aboutToDisappear中清理
  • 使用-1作为"未启动"的标识

问题3: 预加载失败导致卡住

现象描述

预加载数据失败后,启动页一直停留在2秒后的状态,不跳转也不报错。

原因分析

  • 如果在aboutToAppear中await预加载
  • 预加载失败会导致异常中断
  • 后续的setTimeout永远不会执行

错误示例

typescript 复制代码
// ❌ 错误:等待数据加载完成才跳转
async aboutToAppear() {
  await this.preloadData();  // 如果失败,永远不跳转
  setTimeout(() => {
    router.replaceUrl({ url: 'pages/Index' });
  }, 2000);
}

正确做法

typescript 复制代码
// ✅ 正确:并行执行,不互相阻塞
aboutToAppear() {
  this.preloadData();  // 后台执行,不await
  setTimeout(() => {   // 定时跳转正常执行
    router.replaceUrl({ url: 'pages/Index' });
  }, 2000);
}

// ✅ 更好的做法:添加错误处理
async preloadData() {
  try {
    const data = await DataService.load();
    AppStorage.set('cachedData', data);
  } catch (error) {
    // 即使失败,也记录日志,不影响跳转
    console.error('预加载失败:', error);
  }
}

问题4: 路由跳转失败

现象描述

router.replaceUrl调用后,页面没有跳转,控制台报错。

常见原因及解决

原因 解决方案
页面路径错误 检查main_pages.json中的路径
页面未注册 确保目标页在src数组中
在build中调用路由 只在生命周期中调用
json 复制代码
// entry/src/main/resources/base/profile/main_pages.json
{
  "src": [
    "pages/Splash",
    "pages/Index",
    "pages/Detail"
  ]
}

正确调用时机

typescript 复制代码
// ✅ 正确:在aboutToAppear中调用
aboutToAppear() {
  setTimeout(() => {
    router.replaceUrl({ url: 'pages/Index' });
  }, 2000);
}

// ❌ 错误:在build中调用
build() {
  Column() {
    // ...
  }
  .onClick(() => {
    router.replaceUrl({ url: 'pages/Index' });  // 不要这样做!
  })
}

问题5: 图片资源路径错误

现象描述

报错 "No such resource",图片无法显示。

原因分析

  • 资源文件不存在
  • 路径格式错误
  • 扩展名不匹配

排查步骤

  1. 确认文件位置:

    entry/src/main/resources/rawfile/
    └── bg/
    └── seasons/
    ├── chun.png
    ├── xia.png
    ├── qiu.png
    └── dong.png

  2. 确认路径格式:

typescript 复制代码
// ✅ rawfile使用 $rawfile()
Image($rawfile('bg/seasons/chun.png'))

// ✅ media资源使用 $r()
Image($r('app.media.logo'))

// ❌ 不要混用
Image($r('app.media.bg/seasons/chun'))  // 错误
  1. 确认扩展名:
typescript 复制代码
// 如果文件是 chun.jpg,就不能用 .png 结尾
return 'bg/seasons/chun.jpg';  // 不是 .png

问题6: 季节判断不准确

现象描述

用户反馈当前季节和背景图不匹配。

原因分析

  • 节气中的"季节"和天文季节有差异
  • 简单按月份判断可能不准确
  • 没有考虑闰年等因素

改进方案

typescript 复制代码
// 使用节气判断季节,而非月份
private getSolarTermSeason(): string {
  const month = new Date().getMonth() + 1;
  const day = new Date().getDate();

  // 节气分界点(近似)
  // 立春:约2月4日
  // 立夏:约5月5日
  // 立秋:约8月7日
  // 立冬:约11月7日

  if (month < 2 || (month === 2 && day < 4)) {
    return 'dong';  // 冬季
  } else if (month < 5 || (month === 5 && day < 5)) {
    return 'chun';  // 春季
  } else if (month < 8 || (month === 8 && day < 7)) {
    return 'xia';   // 夏季
  } else if (month < 11 || (month === 11 && day < 7)) {
    return 'qiu';   // 秋季
  } else {
    return 'dong';  // 冬季
  }
}

性能优化

1. 图片资源优化

启动页图片是首先加载的资源,需要特别注意:

typescript 复制代码
// ✅ 使用适当尺寸的图片
// 不要使用4000x3000的原图
// 推荐使用 1920x1080 或更小的图片

// ✅ 考虑图片格式
// 照片类:使用 JPG 或 WebP
// 图标类:使用 PNG
// WebP 格式体积更小,但需要确认设备支持

推荐尺寸

设备类型 推荐尺寸 说明
手机 1080x1920 主流分辨率
平板 1920x1200 考虑横屏

2. 减少启动时间

typescript 复制代码
// ✅ 延迟初始化非必要操作
aboutToAppear() {
  // 立即显示UI
  this.startAnimation();

  // 延迟执行重量级操作
  setTimeout(() => {
    this.preloadHeavyData();
  }, 500);
}

// ✅ 使用骨架屏提升感知速度
// 如果数据加载需要等待,可以先显示骨架UI

3. 避免阻塞主线程

typescript 复制代码
// ❌ 不好:在启动页执行耗时同步操作
aboutToAppear() {
  this.processLargeData();  // 可能阻塞UI
}

// ✅ 好:将耗时操作设为异步
aboutToAppear() {
  this.startAnimation();
  setTimeout(() => {
    this.loadDataInBackground();
  }, 100);
}

本章小结

核心知识点

本文详细讲解了启动页的实现,主要涵盖以下核心知识:

1. UI布局架构

  • Stack层叠布局实现背景+内容叠加
  • 毛玻璃效果增强文字可读性
  • 季节动态背景图提升品牌一致性

2. 生命周期管理

  • aboutToAppear:初始化、启动动画、设置定时器
  • aboutToDisappear:清理定时器、取消预加载
  • 避免在build中执行异步操作

3. 状态管理策略

  • @State:组件私有状态(动画透明度)
  • @StorageLink:持久化状态(引导状态)
  • @StorageProp:只读配置

4. 异常处理机制

  • try-catch捕获路由异常
  • 预加载失败不影响跳转
  • 资源路径错误降级处理

最佳实践清单

检查项 说明 状态
设置窗口背景色 避免白屏闪烁
清理定时器 防止内存泄漏
预加载并行执行 不阻塞跳转
路由异常处理 防止崩溃
图片资源验证 确保路径正确
动画性能优化 使用硬件加速

代码模板

typescript 复制代码
// 标准启动页模板
@Entry
@Component
struct Splash {
  @State opacity: number = 0;
  private timerId: number = -1;

  aboutToAppear() {
    // 1. 淡入动画
    animateTo({ duration: 800, curve: Curve.EaseOut }, () => {
      this.opacity = 1;
    });

    // 2. 预加载数据(可选)
    this.preloadData();

    // 3. 定时跳转
    this.timerId = setTimeout(() => {
      router.replaceUrl({ url: 'pages/Index' });
    }, 2000);
  }

  aboutToDisappear() {
    if (this.timerId !== -1) {
      clearTimeout(this.timerId);
    }
  }

  async preloadData() {
    // 预加载逻辑
  }

  private getSeasonBg(): string {
    const month = new Date().getMonth() + 1;
    if (month >= 3 && month <= 5) return 'bg/seasons/chun.png';
    if (month >= 6 && month <= 8) return 'bg/seasons/xia.png';
    if (month >= 9 && month <= 11) return 'bg/seasons/qiu.png';
    return 'bg/seasons/dong.png';
  }

  build() {
    Stack() {
      Image($rawfile(this.getSeasonBg()))
        .width('100%')
        .height('100%')
        .objectFit(ImageFit.Cover)

      Column({ space: 16 }) {
        Text('节气通').fontSize(36)
        Text('把握时令之美').fontSize(16)
      }
    }
    .width('100%')
    .height('100%')
    .opacity(this.opacity)
  }
}

下一步预告

下一篇文章将讲解首页开发(上),实现应用的核心导航架构:

  • Tabs底部导航容器
  • 自定义TabBar组件
  • 页面切换状态管理
  • 主题色动态应用

相关链接

相关推荐
川石课堂软件测试1 小时前
零基础小白如何学习自动化测试
python·功能测试·学习·测试工具·jmeter·压力测试·harmonyos
Swift社区2 小时前
OpenHarmony鸿蒙PC平台移植 gifsicle:CC++ 三方库适配实践(Lycium tpc_c_cplusplus)
c语言·c++·harmonyos
川石课堂软件测试2 小时前
作为一名测试工程师如何学习Kubernetes(k8s)技能
学习·测试工具·容器·职场和发展·kubernetes·测试用例·harmonyos
yuegu7772 小时前
HarmonyOS应用<节气通>开发第4篇:TabBar导航实现
华为·harmonyos
阿钱真强道3 小时前
25 鸿蒙LiteOS GPIO轮询模式实战教程:电平读取与上升沿检测
嵌入式·harmonyos·liteos·开源鸿蒙·瑞芯微·rk2206
G_dou_3 小时前
Flutter+OpenHarmony实战:flashlight】手电筒项目
flutter·harmonyos
爱吃大芒果3 小时前
鸿蒙 ArkUI 架构蓝图:MoodLite 的 UI 渲染与数据逻辑解耦实践
ui·架构·harmonyos
nashane3 小时前
HarmonyOS 6学习:深入解析CustomDialog嵌套弹窗中的this指向陷阱与解决方案
学习·华为·harmonyos
痕忆丶4 小时前
openharmony北向开发基础之应用访问公共目录
harmonyos