HarmonyOS应用<节气通>开发第3篇:首页开发(下)——动态内容实现

引言

在上一篇文章中,我们搭建了首页的骨架(Tabs容器和自定义TabBar)。这篇文章将让首页"活"起来,实现四个核心功能模块:

  • 当前节气卡片:根据日期自动显示对应节气
  • 精选文章:横向滚动展示热门文章
  • 热门分类:网格布局的功能入口
  • 时间轴:全年节气的可视化展示

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

  • 如何根据日期动态加载数据
  • 如何实现流畅的横向滚动效果
  • Grid网格布局的最佳实践
  • 自动滚动的动画实现技巧

学习目标

完成本文后,你将能够:

  • ✅ 实现基于日期的动态内容展示
  • ✅ 创建横向滚动的文章列表
  • ✅ 使用Grid组件构建网格布局
  • ✅ 实现平滑的自动滚动动画
  • ✅ 处理图片路径和资源引用

需求分析

功能模块设计

模块 功能描述 技术要点
当前节气卡片 显示今日对应节气,点击跳转详情 日期计算、条件渲染、路由跳转
精选文章 横向滚动展示3-5篇热门文章 Scroll横向滚动、固定宽度、ForEach
热门分类 4列网格展示功能入口 Grid布局、columnsTemplate
时间轴 全年24节气横向展示,自动滚动 Scroller控制、animateTo动画

核心实现

步骤1: 实现当前节气卡片

功能说明

根据当前日期,自动匹配对应的节气,并展示:

  • 节气名称(如"立春")
  • 阳历日期(如"2024-02-04")
  • 简短描述
  • 精美背景图
完整代码
typescript 复制代码
// components/CurrentHolidayCard.ets

import router from '@ohos.router';
import { holidays } from '../mock/HolidayMockData';
import { getCurrentDate } from '../utils/DateUtils';
import type { Holiday } from '../models/HolidayModel';

@Component
export struct CurrentHolidayCard {
  // 当前节气数据
  @State currentHoliday: Holiday | null = null;
  
  /**
   * 组件初始化
   */
  aboutToAppear() {
    this.loadCurrentHoliday();
  }
  
  /**
   * 加载当前节气
   * 根据月份和日期查找最接近的节气
   */
  loadCurrentHoliday(): void {
    const { month, day } = getCurrentDate();
    
    console.info('[CurrentHolidayCard] 当前日期: ' + month + '月' + day + '日');
    
    // 查找最接近的节气(前后15天内)
    const holiday = holidays.find((h: Holiday) => {
      const [hMonth, hDay] = h.solarDate.split('-').slice(1, 3).map(Number);
      return hMonth === month && Math.abs(hDay - day) <= 15;
    });
    
    // 如果没找到,默认显示第一个节气
    this.currentHoliday = holiday || holidays[0];
    
    console.info('[CurrentHolidayCard] 当前节气: ' + this.currentHoliday.name);
  }
  
  /**
   * 构建UI
   */
  build() {
    if (!this.currentHoliday) {
      // 加载中状态
      Column() {
        LoadingProgress()
          .width(40)
          .height(40)
        
        Text('加载中...')
          .fontSize(12)
          .fontColor('#999999')
          .margin({ top: 8 })
      }
      .width('100%')
      .height(200)
      .justifyContent(FlexAlign.Center)
    } else {
      // 显示节气卡片
      this.buildCard()
    }
  }
  
  /**
   * 构建节气卡片
   */
  @Builder
  buildCard(): void {
    Stack({ alignContent: Alignment.BottomStart }) {
      // ===== 背景图 =====
      Image('rawfile://bg/holidays/' + this.currentHoliday!.id + '.png')
        .width('100%')
        .height(200)
        .borderRadius(16)
        .objectFit(ImageFit.Cover)
      
      // ===== 渐变遮罩 =====
      Row()
        .width('100%')
        .height(200)
        .linearGradient({
          angle: 180,
          colors: [
            ['#00000000', 0.3],   // 顶部透明
            ['#000000BB', 1]       // 底部半透明黑
          ]
        })
        .borderRadius(16)
      
      // ===== 内容区域 =====
      Column({ space: 8 }) {
        // 节气名称
        Text(this.currentHoliday!.name)
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        
        // 阳历日期
        Text(this.currentHoliday!.solarDate)
          .fontSize(14)
          .fontColor('#FFFFFF')
          .opacity(0.9)
        
        // 描述文字
        Text(this.currentHoliday!.description)
          .fontSize(13)
          .fontColor('#FFFFFF')
          .opacity(0.8)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .padding(16)
      .width('100%')
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .height(200)
    .onClick(() => {
      // 点击跳转到详情页
      try {
        router.pushUrl({
          url: 'pages/Detail',
          params: { 
            holidayId: this.currentHoliday!.id,
            title: this.currentHoliday!.name
          }
        });
      } catch (error) {
        console.error('路由跳转失败: ' + JSON.stringify(error));
      }
    })
  }
}
代码解析

1. 日期匹配逻辑

typescript 复制代码
const holiday = holidays.find((h: Holiday) => {
  const [hMonth, hDay] = h.solarDate.split('-').slice(1, 3).map(Number);
  return hMonth === month && Math.abs(hDay - day) <= 15;
});

原理:

  • 解析节气的阳历日期(如"2024-02-04")
  • 提取月份和日期
  • 查找当前月份且日期相差不超过15天的节气

2. 渐变遮罩效果

typescript 复制代码
.linearGradient({
  angle: 180,  // 从上到下
  colors: [
    ['#00000000', 0.3],   // 30%位置: 完全透明
    ['#000000BB', 1]       // 100%位置: 半透明黑
  ]
})

效果:

  • 顶部透明,不遮挡图片
  • 底部半透明黑,增强文字可读性
  • 平滑过渡,视觉效果自然

步骤2: 实现精选文章横向滚动

文章卡片组件
typescript 复制代码
// components/ArticleCard.ets

import router from '@ohos.router';
import type { Article } from '../models/ArticleModel';

@Component
export struct ArticleCard {
  @Prop article: Article;
  
  build() {
    Column({ space: 8 }) {
      // ===== 封面图 =====
      Image(this.getImageSource())
        .width(160)
        .height(100)
        .borderRadius(12)
        .objectFit(ImageFit.Cover)
      
      // ===== 标题 =====
      Text(this.article.title)
        .fontSize(14)
        .fontColor('#333333')
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .width(160)
      
      // ===== 标签 =====
      if (this.article.tags && this.article.tags.length > 0) {
        Text(this.article.tags[0])
          .fontSize(11)
          .fontColor('#4A9B6D')
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .backgroundColor('#E8F5E9')
          .borderRadius(4)
      }
    }
    .width(160)
    .padding(8)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({ 
      radius: 4, 
      color: '#0D000000', 
      offsetX: 0, 
      offsetY: 2 
    })
    .onClick(() => {
      try {
        router.pushUrl({
          url: 'pages/ArticleDetail',
          params: { articleId: this.article.id }
        });
      } catch (error) {
        console.error('路由跳转失败: ' + JSON.stringify(error));
      }
    })
  }
  
  /**
   * 获取图片源
   * 支持rawfile和base/media两种路径
   */
  getImageSource(): string | Resource {
    if (this.article.coverImage.startsWith('rawfile://')) {
      return this.article.coverImage;  // rawfile路径
    } else {
      return $r('app.media.' + this.article.coverImage);  // base/media资源
    }
  }
}
横向滚动列表
typescript 复制代码
@Builder
buildFeaturedArticles(): void {
  Column({ space: 12 }) {
    // ===== 标题栏 =====
    Row() {
      Text('精选文章')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .fontColor('#333333')
      
      Blank()
      
      Text('查看更多')
        .fontSize(13)
        .fontColor('#4A9B6D')
        .onClick(() => {
          try {
            router.pushUrl({ url: 'pages/Encyclopedia' });
          } catch (error) {
            console.error('路由跳转失败: ' + JSON.stringify(error));
          }
        })
    }
    .width('100%')
    
    // ===== 横向滚动列表 =====
    Scroll() {
      Row({ space: 12 }) {
        ForEach(this.featuredArticles, (article: Article) => {
          ArticleCard({ article: article })
        }, (article: Article) => article.id)
      }
      .padding({ left: 16, right: 16 })
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
  }
  .padding({ left: 16, right: 16 })
}
关键技术点

1. 固定宽度

typescript 复制代码
ArticleCard({ article: article })
  .width(160)  // 必须固定宽度

原因:

  • 横向滚动需要知道每个子项的宽度
  • 不固定宽度会导致布局混乱

2. 隐藏滚动条

typescript 复制代码
.scrollBar(BarState.Off)

效果:

  • 保持滚动功能
  • 视觉上更简洁
  • 适合移动端交互

步骤3: 实现热门分类网格

分类卡片组件
typescript 复制代码
// components/CategoryCard.ets

import router from '@ohos.router';

interface CategoryItem {
  id: string;
  title: string;
  icon: Resource;
  route?: string;
}

@Component
export struct CategoryCard {
  @Prop category: CategoryItem;
  
  build() {
    Column({ space: 8 }) {
      // 图标
      Image(this.category.icon)
        .width(40)
        .height(40)
      
      // 标题
      Text(this.category.title)
        .fontSize(13)
        .fontColor('#333333')
    }
    .width('100%')
    .height(80)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({ 
      radius: 2, 
      color: '#0D000000', 
      offsetX: 0, 
      offsetY: 1 
    })
    .onClick(() => {
      if (this.category.route) {
        try {
          router.pushUrl({ url: this.category.route });
        } catch (error) {
          console.error('路由跳转失败: ' + JSON.stringify(error));
        }
      }
    })
  }
}
网格布局
typescript 复制代码
@Builder
buildCategories(): void {
  Column({ space: 12 }) {
    Text('热门分类')
      .fontSize(18)
      .fontWeight(FontWeight.Medium)
      .fontColor('#333333')
    
    Grid() {
      ForEach(this.categories, (category: CategoryItem) => {
        GridItem() {
          CategoryCard({ category: category })
        }
      }, (category: CategoryItem) => category.id)
    }
    .columnsTemplate('1fr 1fr 1fr 1fr')
    .rowsGap(12)
    .columnsGap(12)
  }
  .padding({ left: 16, right: 16 })
}
Grid布局详解

columnsTemplate语法:

typescript 复制代码
.columnsTemplate('1fr 1fr 1fr 1fr')  // 4列,每列等宽
.columnsTemplate('1fr 2fr 1fr')      // 3列,中间列是两边的2倍
.columnsTemplate('100px 1fr 100px')  // 混合单位

fr单位:

  • flexible的缩写
  • 表示弹性空间
  • 1fr 1fr = 两列等分

步骤4: 实现时间轴自动滚动

时间轴组件
typescript 复制代码
// components/Timeline.ets

import router from '@ohos.router';
import { holidays } from '../mock/HolidayMockData';
import { getCurrentDate } from '../utils/DateUtils';
import type { Holiday } from '../models/HolidayModel';

@Component
export struct Timeline {
  // 滚动控制器
  private scroller: Scroller = new Scroller();
  
  // 当前选中的节气ID
  @State selectedId: string = '';
  
  /**
   * 组件初始化
   */
  aboutToAppear() {
    // 获取当前节气ID
    const currentDate = getCurrentDate();
    const currentHoliday = holidays.find((h: Holiday) => {
      const [month, day] = h.solarDate.split('-').slice(1, 3).map(Number);
      return month === currentDate.month && Math.abs(day - currentDate.day) <= 15;
    });
    
    if (currentHoliday) {
      this.selectedId = currentHoliday.id;
    }
    
    // 启动自动滚动
    this.startAutoScroll();
  }
  
  /**
   * 自动滚动
   * 每3秒滚动一个节气位置
   */
  startAutoScroll(): void {
    setInterval(() => {
      animateTo({ 
        duration: 1000,
        curve: Curve.EaseInOut
      }, () => {
        this.scroller.scrollBy(80, 0);
      });
    }, 3000);
  }
  
  /**
   * 构建UI
   */
  build() {
    Column({ space: 12 }) {
      Text('节气时间轴')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .fontColor('#333333')
      
      // 横向滚动的时间轴
      Scroll(this.scroller) {
        Row({ space: 16 }) {
          ForEach(holidays, (holiday: Holiday) => {
            this.buildTimelineItem(holiday)
          }, (holiday: Holiday) => holiday.id)
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
    }
  }
  
  /**
   * 构建单个时间节点
   */
  @Builder
  buildTimelineItem(holiday: Holiday): void {
    const isSelected = holiday.id === this.selectedId;
    
    Column({ space: 6 }) {
      // 圆点
      Circle()
        .width(12)
        .height(12)
        .fill(isSelected ? '#4A9B6D' : '#CCCCCC')
      
      // 节气名称
      Text(holiday.name)
        .fontSize(12)
        .fontColor(isSelected ? '#4A9B6D' : '#666666')
        .fontWeight(isSelected ? FontWeight.Bold : FontWeight.Normal)
      
      // 日期
      Text(holiday.solarDate.slice(5))
        .fontSize(11)
        .fontColor('#999999')
    }
    .width(60)
    .onClick(() => {
      this.selectedId = holiday.id;
      
      // 点击跳转到详情页
      try {
        router.pushUrl({
          url: 'pages/Detail',
          params: { holidayId: holiday.id }
        });
      } catch (error) {
        console.error('路由跳转失败: ' + JSON.stringify(error));
      }
    })
  }
}
动画实现详解

1. animateTo平滑动画

typescript 复制代码
animateTo({ 
  duration: 1000,         // 动画时长
  curve: Curve.EaseInOut  // 缓动曲线
}, () => {
  this.scroller.scrollBy(80, 0);
});

优点:

  • 比setInterval更流畅
  • 支持缓动效果
  • 性能更好

2. Scroller控制器

typescript 复制代码
private scroller: Scroller = new Scroller();

Scroll(this.scroller) {
  // 内容
}

// 控制滚动
this.scroller.scrollBy(80, 0);  // 相对滚动
this.scroller.scrollTo(100, 0); // 绝对滚动

常见问题与解决方案

问题1: 图片路径判断错误

现象 :

rawfile图片显示不出来,控制台报错"Resource not found"。

解决方案:

typescript 复制代码
getImageSource(): string | Resource {
  if (this.imagePath.startsWith('rawfile://')) {
    return this.imagePath;  // rawfile用字符串
  } else {
    return $r('app.media.' + this.imagePath);  // base/media用$r()
  }
}

规则:

  • rawfile:// 开头的路径: 直接使用字符串
  • 其他路径: 使用$r('app.media.xxx')引用

问题2: ForEach缺少key导致警告

现象 :

控制台警告:"ForEach needs key generator function"。

解决方案:

typescript 复制代码
ForEach(items, (item) => {
  Text(item.name)
}, (item) => item.id)  // 第三个参数是key生成函数

为什么需要key:

  • 帮助框架识别列表项
  • 优化渲染性能
  • 避免不必要的重建

问题3: 自动滚动卡顿

现象 :

使用setInterval实现自动滚动,页面明显卡顿。

解决方案:

typescript 复制代码
setInterval(() => {
  animateTo({ 
    duration: 1000,
    curve: Curve.EaseInOut
  }, () => {
    this.scroller.scrollBy(80, 0);
  });
}, 3000);

优化建议:

  • 使用animateTo添加动画
  • 调整滚动距离和频率

本章小结

核心知识点

本文详细实现了首页的四个核心功能模块:

1. 当前节气卡片

  • 根据日期动态匹配节气
  • 渐变遮罩增强可读性
  • 点击跳转详情页

2. 精选文章横向滚动

  • Scroll组件横向滚动
  • 固定宽度保证布局
  • ForEach提供唯一key

3. 热门分类网格

  • Grid布局实现4列网格
  • columnsTemplate定义列宽
  • rowsGap/columnsGap设置间距

4. 时间轴自动滚动

  • Scroller控制滚动位置
  • animateTo实现平滑动画
  • 定时自动滚动效果

最佳实践总结

图片路径处理

typescript 复制代码
if (path.startsWith('rawfile://')) {
  return path;
} else {
  return $r('app.media.' + path);
}

ForEach使用

typescript 复制代码
ForEach(items, (item) => {
  Card({ item })
}, (item) => item.id)

横向滚动

typescript 复制代码
Scroll() {
  Row({ space: 12 }) {
    ForEach(items, (item) => {
      Card().width(160)
    })
  }
}
.scrollable(ScrollDirection.Horizontal)

下一步预告

首页的核心功能已经完成!在下一篇文章中,我们将:

  • 深入讲解自定义TabBar的实现细节
  • 实现Tab切换的状态管理
  • 应用主题色到导航栏
  • 适配不同屏幕尺寸

相关链接

相关推荐
想你依然心痛1 小时前
HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与HMAF的“芯界智脑“——PC端AI智能体沉浸式芯片设计与EDA验证工作台
人工智能·华为·ar·harmonyos·智能体
前端不太难1 小时前
鸿蒙游戏 HUD 如何设计?
游戏·状态模式·harmonyos
Swift社区1 小时前
HarmonyOS鸿蒙三方库移植:选 vcpkg 还是 lycium_plusplus?两种“框架化”方案对比
华为·harmonyos
互联网散修1 小时前
鸿蒙实战:图片编辑器——高性能纹理马赛克画笔
华为·编辑器·harmonyos·纹理马赛克
jiguang1271 小时前
Windows11安装eNSP华为网络仿真工具平台
网络·华为
特立独行的猫a2 小时前
鸿蒙 PC 平台 Rust 语言第三方库与应用移植全景指南
华为·rust·harmonyos·三方库·鸿蒙pc
yuegu7772 小时前
HarmonyOS应用<节气通>开发第5篇:节气详情页(上)——页面布局与数据展示
华为·harmonyos
花椒技术14 小时前
复杂直播业务做 RN 跨端,我们最后保留了哪些 Native 边界
react native·react.js·harmonyos
瑶总迷弟15 小时前
使用 mis-tei 在昇腾310P上部署 bge-m3模型
pytorch·python·华为·语言模型·自然语言处理·cnn·unix