HarmonyOS NEXT 应用开发实战(七、知乎日报轮播图的完整实现)

在今天的博文中,我们将深入探讨如何在 HarmonyOS NEXT 中使用 ArkUI 实现一个轮播图组件。我们将通过一个示例代码来演示这个完整的过程,其中包含获取数据、管理数据源以及渲染组件等多个部分。

先来看下最终实现效果:

项目准备

首先,我们需要导入一些必要的模块和API,如下所示:

TypeScript 复制代码
import { getSwiperList } from "../../common/api/home";
import { getZhiHuNews } from '../../common/api/zhihu';
import { BaseResponse, ErrorResp, ZhiNewsItem } from '../../common/bean/ApiTypes';
import { Log } from '../../utils/logutil';
import { formatDate } from '../../utils/time';

在这些模块中,getSwiperListgetZhiHuNews 是用于获取轮播图和知乎新闻数据的API。

api接口的定义十分简洁,实现如下:

TypeScript 复制代码
import { setRequestConfig } from '../../utils/http';
import { BaseResponse,ZhiNewsRespData,ZhiDetailRespData } from '../bean/ApiTypes';

// 调用setRequestConfig函数进行请求配置
setRequestConfig();

const http = globalThis.$http;

// 获取知乎列表页api接口
export const getZhiHuNews = (date:string): Promise<BaseResponse<ZhiNewsRespData>> => http.get('/zhihunews/'+date);

// 获取知乎详情页api接口
export const getZhiHuDetail = (id:string): Promise<BaseResponse<ZhiDetailRespData>> => http.get('/zhihudetail/'+id);

api接口如何做到如此简洁的?这里推荐博主开源的官方http网络库封装:

HarmonyOS NEXT应用开发实战(二、封装比UniApp和小程序更简单好用的网络库)_鸿蒙网络库-CSDN博客

数据源管理

这部分才是重点!在轮播图组件中,我们需要管理数据源。这部分使用了懒加载LazyForEach。该方法需要我们先定义加载数据源,我们首先编写一个泛型的公共数据源类,该类需要实现IDataSource接口,并重写totalCount、getData等方法。在这里,我们定义了两个数据源类:BasicDataSourceSwiperDataSource。这两个类可以帮助我们管理数据的监听和更新。

代码如下所示:

TypeScript 复制代码
class BasicDataSource<T> implements IDataSource {
  // 监听器和数据数组
  private listeners: DataChangeListener[] = [];
  private originDataArray: T[] = [];

  totalCount(): number {
    return this.originDataArray.length;
  }

  getData(index: number): T {
    return this.originDataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    // 注册数据变化监听器
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    // 反注册数据变化监听器
  }

  // 通知重新加载数据
  notifyDataReload() { /* ... */ }
  
  // 通知添加数据
  notifyDataAdd(index: number) { /* ... */ }
}

class SwiperDataSource<T> extends BasicDataSource<T> {
  private dataArray: T[] = [];

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): T {
    return this.dataArray[index];
  }

  pushData(data: T): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  reloadData(): void {
    this.dataArray = [];
    this.notifyDataReload();
  }
}

组件实现

接下来是主组件的实现,在 ZhiHu 类中,我们使用 Swiper 组件来创建轮播图。

TypeScript 复制代码
@Component
export default struct ZhiHu {
  // 状态管理
  @State message: string = 'Hello World';
  private swiperController: SwiperController = new SwiperController();
  private swiperData: SwiperDataSource<ZhiNewsItem> = new SwiperDataSource();
  @State zhiNews: ZhiNewsItem[] = [];
  private currentDate = '';

  // 组件生命周期
  aboutToAppear() {
    getSwiperList().then((res) => {
      // 处理响应数据
    }).catch((err: BaseResponse<ErrorResp>) => {
      // 错误处理
    });

    getZhiHuNews(this.currentDate).then((res) => {
      this.zhiNews = res.data.stories;
      for (const itm of res.data.top_stories) {
        this.swiperData.pushData(itm);
      }
    }).catch((err: BaseResponse<ErrorResp>) => {
      // 错误处理
    });
  }

  build() {
    Navigation(this.pageStack) {
      Row() {
        Column({ space: 0 }) {
          // 标题栏
          Text("日报")
            .size({ width: '100%', height: 50 })
            .backgroundColor("#28bff1")
            .fontColor("#ffffff")
            .textAlign(TextAlign.Center)
            .fontSize("18fp");

          // 轮播图组件
          Swiper(this.swiperController) {
            LazyForEach(this.swiperData, (item: ZhiNewsItem) => {
              Stack({ alignContent: Alignment.Center }) {
                Image(item.image)
                  .width('100%')
                  .height(180)
                  .backgroundColor(0xAFEEEE)
                  .onClick(() => {
                    this.pageStack.pushDestinationByName("PageOne", { id: item.id });
                  });

                Text(item.title)
                  .padding(5)
                  .margin({ top: 60 })
                  .width('100%').height(50)
                  .textAlign(TextAlign.Center)
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Clip });
              }
            }, (item: ZhiNewsItem) => item.id)
            .autoPlay(true)
            .interval(4000)
            .loop(true);
          }
        }
      }
    }
    .mode(NavigationMode.Stack)
    .width('100%').height('100%');
  }
}

完整代码

TypeScript 复制代码
import {getSwiperList,getHotMovie} from "../../common/api/home"
import { getZhiHuNews } from '../../common/api/zhihu';
import { BaseResponse,ErrorResp,ZhiNewsItem } from '../../common/bean/ApiTypes';
import { Log } from '../../utils/logutil'
import { formatDate } from '../../utils/time';

class BasicDataSource<T> implements IDataSource {

  private listeners: DataChangeListener[] = [];
  private originDataArray: T[] = [];

  totalCount(): number {
    return this.originDataArray.length;
  }

  getData(index: number): T {
    return this.originDataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.slice(pos, 1);
    }
  }

  // 通知LazyForEach组件需要重新重载所有子组件
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    })
  }

  // 通知LazyForEach组件需要在index对应索引处添加子组件
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    })
  }
}

class SwiperDataSource<T> extends BasicDataSource<T> {

  private dataArray: T[] = [];

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): T {
    return this.dataArray[index];
  }

  // 在列表末尾添加数据并通知监听器
  pushData(data: T): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  // 重载数据
  reloadData(): void {
    // 不会引起状态变化
    this.dataArray = [];
    // 必须通过DataChangeListener来更新
    this.notifyDataReload();
  }
}

@Component
export default struct ZhiHu{
  @State message: string = 'Hello World';
  private swiperController: SwiperController = new SwiperController()
  private swiperData: SwiperDataSource<ZhiNewsItem> = new SwiperDataSource()
  @State zhiNews:ZhiNewsItem[] = []
  private currentDate= '' // 初始化为今天的日期
  private previousDate= '' // 上一天的日期
  pageStack: NavPathStack = new NavPathStack()

  // 组件生命周期
  aboutToAppear() {
    Log.info('ZhiHu aboutToAppear');
    this.currentDate = formatDate(new Date())
    getSwiperList().then((res) => {
      Log.debug(res.data.message)
      Log.debug("request","res.data.code:%{public}d",res.data.code)
      Log.debug("request","res.data.data[0]:%{public}s",res.data.data[0].id)
      Log.debug("request","res.data.data[0]:%{public}s",res.data.data[0].imageUrl)
      Log.debug("request","res.data.data[0]:%{public}s",res.data.data[0].title)
    }).catch((err:BaseResponse<ErrorResp>) => {
      Log.debug("request","err.data.code:%d",err.data.code)
      Log.debug("request",err.data.message)
    });

    //获取知乎新闻列表
    getZhiHuNews(this.currentDate).then((res) => {
      Log.debug(res.data.message)
      Log.debug("request","res.data.code:%{public}d",res.data.code)
      this.zhiNews = res.data.stories
      for (const itm of res.data.top_stories) {
        this.swiperData.pushData(itm)
      }

    }).catch((err:BaseResponse<ErrorResp>) => {
      Log.debug("request","err.data.code:%d",err.data.code)
      Log.debug("request",err.data.message)
    });
  }

  // 组件生命周期
  aboutToDisappear() {
    Log.info('ZhiHu aboutToDisappear');
  }

  build() {
    Navigation(this.pageStack){
      Row() {
        Column({ space: 0 }) {
          // 标题栏
          Text("日报")
            .size({ width: '100%', height: 50 })
            .backgroundColor("#28bff1")
            .fontColor("#ffffff")
            .textAlign(TextAlign.Center)
            .fontSize("18fp")

          // 内容项
            Swiper(this.swiperController) {
              LazyForEach(this.swiperData, (item: ZhiNewsItem) => {
                Stack({ alignContent: Alignment.Center }) {
                  Image(item.image)
                    .width('100%')
                    .height(180)
                    .backgroundColor(0xAFEEEE)
                    .zIndex(1)
                    .onClick(() => {
                      //this.pageStack.pushPathByName("PageOne", item)
                      this.pageStack.pushDestinationByName("PageOne", { id:"9773231" }).catch((e:Error)=>{
                        // 跳转失败,会返回错误码及错误信息
                        console.log(`catch exception: ${JSON.stringify(e)}`)
                      }).then(()=>{
                        // 跳转成功
                      });
                    })

                  // 显示轮播图标题
                  Text(item.title)
                    .padding(5)
                    .margin({ top:60 })
                    .width('100%').height(50)
                    .textAlign(TextAlign.Center)
                    .maxLines(2)
                    .textOverflow({overflow:TextOverflow.Clip})
                    .fontSize(20)
                    .fontColor(Color.White)
                    .opacity(100) // 设置标题的透明度 不透明度设为100%,表示完全不透明
                    .backgroundColor('#808080AA') // 背景颜色设为透明
                    .zIndex(2)
                }
              }, (item: ZhiNewsItem) => item.id)
            }
            .cachedCount(2)
            .index(1)
            .autoPlay(true)
            .interval(4000)
            .loop(true)
            .indicatorInteractive(true)
            .duration(1000)
            .itemSpace(0)
            .curve(Curve.Linear)
            .onChange((index: number) => {
              console.info(index.toString())
            })
            .onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {
              console.info("index: " + index)
              console.info("current offset: " + extraInfo.currentOffset)
            })
            .height(180) // 设置高度

          List({ space: 12 }) {
            ForEach(this.zhiNews, (item:ZhiNewsItem) => {
              ListItem() {
                Column({ space: 0 }) {
                  Row() {
                    Column({ space: 15 }) {
                      Text(item.title).fontSize(16).fontWeight(FontWeight.Bold).align(Alignment.Start).width('100%')
                      Text(item.hint).align(Alignment.Start).width('100%')
                    }.justifyContent(FlexAlign.Start).width('70%').padding(5)

                    Image(item.image).objectFit(ImageFit.Cover).borderRadius(5).height(100).padding(2)

                  }.size({ width: '100%', height: 100 })

                  Divider().strokeWidth(2).color('#F1F3F5')
                }.size({ width: '100%', height: 100 })
              }
            }, (itm:ZhiNewsItem) => itm.id)
          }.size({ width: '100%', height: '100%' })

        }.size({ width: '100%', height: '100%' })
      }
    }
    .mode(NavigationMode.Stack)
    .width('100%').height('100%')

  }
}

项目开源地址

zhihudaily: HarmonyOS NEXT 项目开发实战,仿知乎日报的实现

写在最后

最后,推荐下笔者的业余开源app影视项目"爱影家",推荐分享给与我一样喜欢免费观影的朋友。【注:该项目仅限于学习研究使用!请勿用于其他用途!】

开源地址: 爱影家app开源项目介绍及源码

https://gitee.com/yyz116/imovie

其他资源

滑块视图容器

文档中心

相关推荐
SoraLuna42 分钟前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
四口鲸鱼爱吃盐3 小时前
Pytorch | 从零构建GoogleNet对CIFAR10进行分类
人工智能·pytorch·分类
leaf_leaves_leaf4 小时前
win11用一条命令给anaconda环境安装GPU版本pytorch,并检查是否为GPU版本
人工智能·pytorch·python
夜雨飘零14 小时前
基于Pytorch实现的说话人日志(说话人分离)
人工智能·pytorch·python·声纹识别·说话人分离·说话人日志
四口鲸鱼爱吃盐4 小时前
Pytorch | 从零构建MobileNet对CIFAR10进行分类
人工智能·pytorch·分类
苏言の狗4 小时前
Pytorch中关于Tensor的操作
人工智能·pytorch·python·深度学习·机器学习
AORO_BEIDOU4 小时前
单北斗+鸿蒙系统+国产芯片,遨游防爆手机自主可控“三保险”
华为·智能手机·harmonyos
博览鸿蒙6 小时前
鸿蒙操作系统(HarmonyOS)的应用开发入门
华为·harmonyos
四口鲸鱼爱吃盐10 小时前
Pytorch | 利用VMI-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击
人工智能·pytorch·python
四口鲸鱼爱吃盐10 小时前
Pytorch | 利用PI-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击
人工智能·pytorch·python