HarmonyOS新闻卡片组件开发实战:自定义组件与List渲染深度解析


摘要: 本文详细讲解HarmonyOS新闻卡片组件的完整开发流程,涵盖自定义组件设计、List列表渲染、分类筛选功能实现。通过具体代码示例,分享新闻数据模型构建、复杂布局技巧、父子组件通信机制以及交互体验优化。适合HarmonyOS初学者学习组件化开发和列表展示功能,帮助开发者快速掌握构建现代资讯类应用的核心技术。

标签: HarmonyOS 新闻卡片 自定义组件 List渲染 分类筛选 组件通信 移动开发

大家好!鸿蒙学习“肝帝”又上线了!今天继续我的 HarmonyOS 学习之旅,这次的挑战是——搞一个功能“全家桶”的新闻资讯 App。

这个项目包含了自定义组件List 渲染分类筛选等超多实用技巧,绝对是“进阶”必备!特别适合想从“游击队”变成“正规军”,深入学习组件化开发的小伙伴!

为什么“死磕”这个新闻项目?

上次的“国风” Demo 只是开胃菜,这次我们要“来真的”了!

为啥选这个?因为“麻雀虽小,五脏俱全”啊!做一个资讯 App,我(和你们)可以一举拿下:

  • 怎么“造零件”:掌握 @Component 装饰器,造一个“高复用”卡片!
  • 怎么“刷列表”:精通 List + ForEach,还要学会用 filter 玩筛选!
  • 怎么“叠罗汉”StackColumnRow 嵌套使用,搞定复杂布局!
  • 怎么“点个赞”:实现点赞、分类这种真实的用户交互!

🚀 项目效果“卖家秀”

先上个“卖家秀”!想象一下:打开 App,一个专业的“今日头条”风界面映入眼帘。顶部是标题栏和(能横向滚动的)分类筛选条,下面是颜值超高的瀑布流新闻卡片。

image.png

每张卡片都有图有真相,标题、摘要、发布时间、阅读量、点赞功能一应俱全。点一下“科技”,列表“唰”一下就只剩科技新闻,丝滑!

跟我一步步“肝”代码

第一步:设计新闻“身份证”(数据模型)

image.png

老规矩,“兵马未动,粮草先行”。写 App 之前,先得搞清楚咱们的数据长啥样。

class NewsItem {
  id: number = 0;
  title: string = '';
  summary: string = '';
  imageUrl: string = '';
  category: string = '';
  publishTime: string = '';
  readCount: number = 0;
  isLiked: boolean = false;

  constructor(
    id: number,
    title: string,
    summary: string,
    imageUrl: string,
    category: string,
    publishTime: string,
    readCount: number,
    isLiked: boolean
  ) {
    this.id = id;
    this.title = title;
    this.summary = summary;
    this.imageUrl = imageUrl;
    this.category = category;
    this.publishTime = publishTime;
    this.readCount = readCount;
    this.isLiked = isLiked;
  }
}

笔记:
这个 NewsItem 类就是咱们的新闻“身份证”模板。比上次的“国风”商品复杂了点,加了图片 URL、阅读量、是不是点过赞… 毕竟是新闻嘛,要素得齐全!


第二步:造“轮子”!@Component 自定义卡片

OK,核心中的核心来了!我们要“造轮子”——一个可复用的“新闻卡片”组件 buildNewsCard

image.png

这次我们不用上次的 @Builder 了,而是用更“重量级”的 @Component

为啥?因为 @Builder 只是个“UI 蓝图”(函数),而 @Component 是一个拥有自己生命周期和状态的“独立公民”(结构体)!这对于需要内部交互(比如点赞)的组件来说,至关重要。

@Component
struct buildNewsCard {
  @Prop news: NewsItem // @Prop: “爹给的”数据(父组件传来)
  @State localNews: NewsItem = new NewsItem(0, '', '', '', '', '', 0, false) // @State: “自己的”数据(内部状态)

  aboutToAppear() {
    // 这是组件“出生”时会调用的方法
    if (this.news) {
      // 把“爹给的”数据,复制一份到“自己的”地盘上
      this.localNews = new NewsItem(
        this.news.id,
        this.news.title,
        this.news.summary,
        this.news.imageUrl,
        this.news.category,
        this.news.publishTime,
        this.news.readCount,
        this.news.isLiked
      );
    }
  }
  
  // ... build() 方法马上就来 ...
}

笔记:
注意看 @Prop@State 的用法!@Prop 是“爹给的”(父组件传来的),@State 是“自己私有的”(内部状态)。

我们在 aboutToAppear 这个“出生”生命周期方法里,把“爹给的”news 数据“复制”一份到“自己的”localNews 里。这样,我们就可以在组件内部随便“折腾”(比如点赞)这个 localNews,还不会“坑爹”(污染父组件的数据)!


第三步:卡片“颜值担当”——图片区(Stack 布局)

image.png

“轮子”有了,开始画它的 build() 方法。一个卡片得有“颜值”担当,我们先用 Stack(堆叠)布局,把图片作为“背景板”。

// 这是 buildNewsCard 组件里的 build() 方法
build() {
  Column({ space: 12 }) { // 用一个 Column 把图片和文字包起来
    // 图片区域
    Stack() {
      // 网络图片
      Image(this.localNews.imageUrl)
        .width('100%')
        .height(200)
        .objectFit(ImageFit.Cover) // 让图片不变形地填满,裁掉多余的
        .borderRadius(12)
        .alt($r('app.media.avatar')) // 翻车(加载失败)时的占位图

      // 分类标签
      Text(this.localNews.category)
        .fontSize(12)
        .fontColor('#FFFFFF')
        .backgroundColor('#007DFF')
        .borderRadius(4)
        .padding({ left: 8, right: 8, top: 4, bottom: 4 })
        .position({ x: 12, y: 12 }) // 精准“贴”在左上角
    }
    
    // ... 内容区(下一步) ...

  }.backgroundColor('#FFFFFF').borderRadius(12) // 整个卡片的背景和圆角
}

笔记:
Stack 布局就是“叠叠乐”,后写的会盖在先写的上面。我们用 position 把“分类标签”精准地“贴”在左上角。objectFit(ImageFit.Cover) 是个神技,能让各种比例的图片都好看!


第四步:卡片“灵魂”——内容区(Column + Row)

image.png

光有图不行,得有“灵魂”——标题和摘要。这部分紧跟在 Stack 后面,放在外层的 Column 里。

    // ... Stack 布局结束 ...
    
    // 内容区域
    Column({ space: 12 }) {
      // 标题区域
      Text(this.localNews.title)
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .fontColor('#1A1A1A')
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis }) // 超出2行变“...”

      // 摘要区域
      Text(this.localNews.summary)
        .fontSize(14)
        .fontColor('#666666')
        .maxLines(3)
        .textOverflow({ overflow: TextOverflow.Ellipsis }) // 超出3行变“...”
        .lineHeight(20)
    
      // ... 底部信息栏(下一步) ...

    }
    .padding({ left: 16, right: 16, bottom: 16 }) // 给内容区加点内边距

笔记:
这部分全是老朋友:fontSizefontWeightmaxLines(最多两行)、textOverflow(超出部分变“…”)。这套“组合拳”打出去,UI 一下子就清爽了。


第四步(续):卡片的“脚”——底部信息栏(交互核心)

image.png

卡片快完工了,还差个“脚”——底部信息栏。一个 Row(水平布局)搞定,左边放时间和阅读量,右边放点赞。

    // ... 摘要区域结束 ...
    
      // 底部信息栏
      Row() {
        // 左侧信息(发布时间、阅读量)
        Column({ space: 2 }) {
          Row({ space: 8 }) {
            Image($r('app.media.shijian')).width(18).height(18)
            Text(this.localNews.publishTime).fontSize(12).fontColor('#999999')
          }
          Row({ space: 8 }) {
            Image($r('app.media.yuedu')).width(18).height(18)
            Text(`${this.localNews.readCount}`).fontSize(12).fontColor('#999999')
          }
        }
        .layoutWeight(1) // 自动“抢”走所有剩余空间,把点赞按钮挤到最右边

        // 点赞按钮
        Row({ space: 6 }) {
          Image(this.localNews.isLiked ? $r('app.media.dianzan2') : $r('app.media.dianzan'))
            .width(30).height(30)
          Text(this.localNews.isLiked ? '已赞' : '点赞')
            .fontSize(12)
            .fontColor(this.localNews.isLiked ? '#007DFF' : '#999999')
        }
        .onClick(() => { 
          // 点击时,“反转”自己的内部状态
          this.localNews.isLiked = !this.localNews.isLiked;
        })
        .backgroundColor(this.localNews.isLiked ? '#E6F2FF' : '#F5F5F5')
        .borderRadius(6)
        .padding({left: 8, right: 8, top: 4, bottom: 4}) // 给点赞按钮加点内边距
      }
    } // 内容区的 Column 结束
  } // 整个卡片的 Column 结束
} // build() 方法结束

笔记:
layoutWeight(1) 又是神技!它让左边的信息栏“霸道”地占据所有剩余空间,从而把点赞按钮“挤”到最右边,完美实现两端对齐!

注意看“点赞”按钮!我们用了一个“三元运算符” (this.localNews.isLiked ? ... : ...)。如果 isLikedtrue,就显示“已赞”图标和蓝色;如果是 false,就显示“点赞”图标和灰色。连背景色都换了!

onClick 时,我们只修改了 this.localNews 这个“内部状态”,这就是 @Component + @State 的威力!


第五步:“组装” App!——主页面架构

image.png

“零件”造好了,现在开始“组装”我们的 App 主页面!@Entry 登场!

@Entry
@Component
export struct NewsCardDemo {
  // 伪造一堆新闻数据,记得用你刚才定义的 NewsItem 类
  @State newsList: NewsItem[] = [
    new NewsItem(1, '鸿蒙新版发布!', '性能提升30%...', 'app.media.hm', '科技', '1小时前', 1024, false),
    new NewsItem(2, 'xx明星演唱会', '现场燃爆...', 'app.media.star', '娱乐', '2小时前', 5890, true),
    new NewsItem(3, '国足又...进球了!', '球员xx梅开二度...', 'app.media.gz', '体育', '3小时前', 888, false)
    // ... 伪造更多数据 ...
  ];
  @State selectedCategory: string = '全部'; // 用@State管理当前选中的分类
  categories: string[] = ['全部', '科技', '娱乐', '体育', '财经'];

  build() {
    Column({ space: 0 }) {
      // 顶部标题栏(这里我偷懒,你也可以用@Builder写一个)
      Text('新闻资讯').fontSize(20).fontWeight(FontWeight.Bold).padding(16).width('100%')

      // 分类筛选
      this.buildCategoryFilter()

      // 新闻列表
      this.buildNewsList()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
  
  // ... buildCategoryFilter 和 buildNewsList 马上就来 ...
}

笔记:
这个主页面也用 @State 来管理“全局”的新闻列表 newsList 和当前选中的分类 selectedCategorybuild 方法里,我们用 Column 把页面分成了“上(标题)”、“中(筛选)”、“下(列表)”三个部分。


第六步:实现“横向滚动”的分类筛选条

image.png

分类筛选条是个高频需求。用 Scroll 包一个 Row,就能让它“横向滚动”。

// 在 NewsCardDemo struct 内部
@Builder
buildCategoryFilter() {
  Scroll() { // 横向滚动容器
    Row({ space: 12 }) {
      ForEach(this.categories, (category: string) => {
        Text(category)
          .fontSize(16)
          .fontColor(this.selectedCategory === category ? '#FFFFFF' : '#666666') // 选中高亮
          .backgroundColor(this.selectedCategory === category ? '#007DFF' : '#F0F0F0') // 选中高亮
          .borderRadius(20)
          .padding({left: 16, right: 16, top: 8, bottom: 8})
          .onClick(() => { 
            // 关键:点击时,更新@State变量
            this.selectedCategory = category; 
          })
      })
    }
    .padding({ left: 16, right: 16, top: 12, bottom: 12 })
  }
  .scrollBar(BarState.Off) // 关掉丑丑的滚动条
}

笔记:
又是“三元运算符”的胜利!this.selectedCategory === category ? ... : ...

最最关键的是 onClick:点击谁,谁就高亮,同时把 @State 变量 this.selectedCategory 的值改成被点击的 category

这个 @State 一变,奇迹发生了…(请看下一步)


第七步:“魔法”发生地!—— 响应式新闻列表

image.png

奇迹来了!还记得吗?ArkTS 是“响应式”的!当你点击分类条,@StateselectedCategory 一变,所有“依赖”了它的 UI 都会“自动”重新渲染!

buildNewsList 就“依赖”了它!

// 在 NewsCardDemo struct 内部
@Builder
buildNewsList() {
  List({ space: 12 }) {
    // 魔法!在这里用 filter 过滤数据!
    ForEach(this.newsList.filter(item =>
      // 如果选的是“全部”,或者 item 的分类 匹配 选中的分类
      this.selectedCategory === '全部' || item.category === this.selectedCategory
    ), (news: NewsItem) => {
      ListItem() {
        // 调用我们刚才辛辛苦苦“造的轮子”
        buildNewsCard({ news: news })
      }
    }, (news: NewsItem) => news.id.toString()) // 用 id 作为唯一标识,提升性能
  }
  .width('100%')
  .layoutWeight(1) // 占满剩余所有空间
  .padding({ left: 16, right: 16, top: 12, bottom: 12 })
}

笔记:
ForEach 里的 this.newsList.filter(...)!这就是“魔法”核心!

filter 方法会根据 this.selectedCategory 筛选出“应该”显示的新闻。

整个流程是:

  1. 你点击“科技”按钮。
  2. onClick@State 变量 selectedCategory 改成了 “科技”。
  3. ArkTS 框架发现 @State 变了,马上“通知”所有用到它的地方。
  4. buildNewsList 重新执行 build()
  5. ForEach 里的 filter 重新计算,这次只返回了 category === '科技' 的新闻。
  6. List 自动更新!

丝滑!这就是“数据驱动 UI”的魅力!


😱 “萌新”踩坑(含泪)实录

“常在河边走,哪能不湿鞋”。下面是我(含泪)总结的“踩坑”经验,各位“萌新”请拿好,可以少走几公里弯路!

巨坑 1:我改了“爹”的数据!(@Prop vs @State)

  • 现象: 我一开始在子组件 buildNewsCard 里,想点赞时直接改 this.news.isLiked… 结果,卒!要么不生效,要么“污染”了父组件。
  • 血泪教训: @Prop 是“爹”给的,是只读的! 你不能在“儿子”组件里直接改“爹”的数据!
  • 正解: 就像我们前面做的,aboutToAppear 里把 @Propnews 复制@StatelocalNews。组件内部只修改 localNews,实现“自给自足”。
// 正确做法:使用内部状态管理
@State localNews: NewsItem = new NewsItem(0, '', '', '', '', '', 0, false)

aboutToAppear() {
  if (this.news) {
    this.localNews = new NewsItem(/* 复制数据 */);
  }
}

小坑 2:图片加载“翻车”

  • 现象: 网速不好时,图片加载失败,卡片上出现一个大“窟窿”,丑爆了。
  • 正解: Image 组件有两个好基友 .alt().onError()
Image(this.localNews.imageUrl)
  .alt($r('app.media.avatar')) // 加载失败/加载中 显示的占位图
  .onError(() => {
    console.log(`图片加载失败: ${this.localNews.title}`);
  })

小坑 3:布局“偏心眼”

  • 现象: 底部信息栏,左边的日期和右边的点赞,挤在一起了,或者对不齐。
  • 正解: 善用 layoutWeight(1)!给那个你希望它“尽可能伸展”的组件(比如我们左边的信息栏 Column)加上它,它就会自动“抢”走所有剩余空间。
Column({ space: 2 }) {
  // 左侧信息内容
}
.layoutWeight(1) // 占据剩余空间

“精装修”建议(下一步玩啥)

这个 Demo 只是个“毛坯房”,想“精装修”?你可以试试:

  1. 搜索功能:在标题栏下面加个搜索框,用 filter 按“标题” (title.includes(searchText)) 搜索。
  2. 详情页面:点击卡片,跳转到完整的新闻详情页(Navigation 组件该出场了)。
  3. 下拉刷新:给 List 加上下拉刷新功能,更新 newsList 里的数据。
  4. 上拉加载:滚动到底部时,自动加载“下一页”数据(onReachEnd 事件)。

总结:你又“行”了!

呼——(擦汗)。这个项目“肝”下来,是不是感觉自己又“行”了?

image.png

我们从一个“空架子”开始,亲手“锻造”了数据模型,“封装”了高复用性的 @Component 卡片(还搞懂了 @Prop@State 的“父子关系”),用 Stack 玩转了“叠罗汉”布局,最后用 @Statefilter 联手实现了“响应式”筛选列表。

这已经是一个准专业 App 的雏形了!

从“国风” Demo 到这个新闻 App,你已经从“新手村”毕业,拿到了“高级组件”的徽章。继续保持这份热情,下一个项目,我们去挑战更酷的!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大师兄6668

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值