鸿蒙应用开发实战:从零起步,构建你的第一个影视APP(第2篇:声明式UI基础:构建刷剧首页布局)#跟着猫哥学鸿蒙
标签:HarmonyOS, 声明式UI, ArkTS组件
嘿,朋友们!上篇我们搭好了“影享家”APP的地基,看到那个橙色欢迎页是不是心痒难耐?今天,我们直奔主题:用鸿蒙的声明式UI搞定首页布局。为什么从这里入手?因为影视APP的灵魂就是“刷刷刷”——用户一打开,就想滑动手指,看到热门剧集的封面海报、简介和评分。声明式UI让这事变得像搭积木:描述你想要的样子,系统帮你渲染。
本篇聚焦首页的骨架:顶部搜索栏、热门推荐的Grid卡片流、底部Tab导航。零基础的你,别怕ArkTS的@Component,看完就能自己拼出个瀑布流。转行前端党?鸿蒙的UI语法像React,但更轻量、无DOM烦恼。编程爱好者?这里有热重载的快感,改改代码就见效。
我们用“影享家”的热门剧集模块举例:模拟从API拉10部剧的数据,渲染成可点击卡片。后期加网络模块后,就能真枪实弹加载豆瓣热榜了。
声明式UI的核心魅力:从“命令式”到“描述式”
先来点理论铺垫,避免你敲代码时一脸懵。传统UI开发(比如Android的XML+Java)是“命令式”:一步步告诉机器“先画个Button,再设位置”。累不说,还易出布局错位。鸿蒙的声明式UI则像写诗:用build()函数描述树状结构,系统自动diff更新。核心组件有:
- 布局容器:Column(竖排,像div flex-direction:column)、Row(横排)、Grid(网格,完美刷剧卡片)。
- 状态绑定:@State变量驱动UI变化,比如视频列表数据更新,页面自动重绘。
- 事件处理:onClick等,响应用户滑动、点击。
- 样式链式:.fontSize(16).margin(10),链式调用,代码干净如丝。
好处?响应式强,适配不同屏(手机/平板),性能高——鸿蒙的Ark引擎编译后跑得飞起。实战中,我们用Grid实现“无限滚动”预留位,后续接List组件扩展。
项目架构更新:首页模块的蓝图
上篇的目录树基础上,我们加了首页的子组件。看这Mermaid图:Index.ets下嵌VideoCard和SearchBar,数据从mock数据源流转。

图中,MockData是临时数据文件,存JSON剧集数组。企业级习惯:数据层分离,便于后期换成网络API。
实战动手:从布局到互动的首页构建
DevEco Studio打开上篇项目,热重载已开,我们一步步填充Index.ets。目标:一个能滑动的Grid,点卡片跳提示(模拟播放)。
步骤1:准备模拟数据
新建entry/src/main/ets/common/MockData.ts,存点影视数据。实际业务用:像爱奇艺的热播榜。
// MockData.ts - 模拟热门剧集数据(规范:接口定义+默认导出,便于类型推断)
export interface VideoItem {
  id: number;
  title: string;
  poster: string;  // 海报URL,实际用资源路径
  rating: number;  // 评分,0-10
  intro: string;   // 简介
}
export default [
  {
    id: 1,
    title: '盗墓笔记',
    poster: '$r{"pages/index/poster1"}',  // 占位资源,后续替换
    rating: 8.5,
    intro: '探险古墓的惊险之旅,吴邪一行人揭开千年谜团。'
  },
  {
    id: 2,
    title: '庆余年',
    poster: '$r{"pages/index/poster2"}',
    rating: 9.2,
    intro: '现代青年范闲穿越古代,卷入权谋漩涡。'
  },
  // ... 再加8条,凑10部
] as VideoItem[];
小贴士:接口VideoItem让ArkTS类型检查生效,避免运行时crash。数据脱敏:无真实URL,用占位符。
步骤2:自定义卡片组件
在entry/src/main/ets/components/VideoCard.ets新建卡片。组件化是鸿蒙精髓:小块复用,大页组装。
// VideoCard.ets - 视频卡片组件(实战规范:Props传入数据,事件回调上抛)
import { Component, Prop } from '@ohos/base';
import type { VideoItem } from '../common/MockData';  // 类型导入
@Component
struct VideoCard {
  @Prop video: VideoItem;  // Props:接收父组件数据
  @Prop onClick?: () => void;  // 可选回调:点击事件
  build() {
    Row() {  // 横向布局:海报+详情
      Image(this.video.poster)
        .width(120)
        .height(160)
        .cornerRadius(8)
        .onClick(() => {  // 事件绑定:模拟播放
          if (this.onClick) this.onClick();
          console.log(`播放: ${this.video.title}`);  // 日志调试
        })
      Column() {  // 竖排详情
        Text(this.video.title)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .maxLines(1)  // 标题单行省略
        Text(`评分: ${this.video.rating}`)
          .fontSize(14)
          .fontColor('#FFD700')  // 金色评分
        Text(this.video.intro)
          .fontSize(12)
          .maxLines(2)
          .lineHeight(1.2)
      }
      .width('60%')
      .padding(10)
    }
    .width('100%')
    .height(180)
    .borderRadius(12)
    .backgroundColor('#2D2D2D')  // 暗卡片,刷剧不伤眼
    .margin(5)
  }
}
代码解读:@Prop像Vue的props,安全传数据。onClick用箭头函数捕获this,避免绑定坑。样式用链式,读起来顺眼。
步骤3:组装首页布局
回pages/Index.ets,导入组件,渲染Grid。加点状态管理。
// Index.ets 更新 - 首页布局(规范:@Observed导入数据,Grid动态绑定)
import { Component, State } from '@ohos/base';
import VideoCard from '../components/VideoCard';
import videos from '../common/MockData';
import router from '@ohos.router';  // 路由导入,后续跳转用
@Component
struct Index {
  @State videoList: typeof videos = videos;  // 状态:绑定数据源
  build() {
    Column() {
      // 顶部搜索栏
      Search({ placeholder: '搜索剧集...' })
        .width('100%')
        .height(50)
        .margin(10)
        .backgroundColor('#3A3A3A')
        .borderRadius(25)
      // 热门Grid:2列瀑布流
      Grid() {
        ForEach(this.videoList, (item: any, index?: number) => {
          VideoCard({ video: item, onClick: () => this.playVideo(item.id) })
            .width('50%')  // 每列占半宽
        }, item => item.id)
      }
      .columnsTemplate('1fr 1fr')  // 两列等宽
      .width('100%')
      .height('70%')
      .scrollable(true)  // 支持滑动
      // 底部Tab(简化版,后续扩展)
      Row() {
        Text('首页').fontSize(14)
        Text('我的').fontSize(14)
      }
      .width('100%')
      .height('10%')
      .justifyContent(FlexAlign.SpaceEven)
      .backgroundColor('#1E1E1E')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#121212')  // 整体暗黑主题
  }
  // 私有方法:播放回调
  playVideo(id: number) {
    console.log(`跳转播放页,ID: ${id}`);
    // router.pushUrl({ url: 'pages/Player' });  // 预留路由
    promptAction('准备播放中...');  // 临时提示
  }
}
// 简单Search组件内联定义(生产中独立文件)
@Component
struct Search {
  @Prop placeholder: string = '';
  build() {
    TextInput({ placeholder: this.placeholder })
      .width('100%')
      .height('100%')
      .fontSize(16)
      .padding(10)
  }
}
调试提示:跑起来,滑动Grid看卡片响应。权限已开,无报错?完美。如果Grid不滑,检查scrollable(true)。
步骤4:优化与测试
- 加资源:resources/base/media下放poster1.jpg等,更新路径。
- 热测:改rating颜色,Ctrl+S见效。
- 坑避:ForEach需key(item.id),防渲染抖动。
总结与预告
首页布局搞定,“影享家”已现刷剧雏形:搜索一键、卡片点播、滑动丝滑。声明式UI的魔力,就在这种“写少跑多”——代码简洁,维护省心。企业级Tip:组件库化VideoCard,复用到搜索页。
下篇,网络接口封装:拉取真实影视数据。用HttpClient封装API,替换Mock,热榜实时更新。代码已贴,动手fork试跑,有Bug@我。码代码,刷剧两不误!
更多推荐
 
 

所有评论(0)