0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

鸿蒙OS开发实例:【瀑布流式图片浏览】

jf_46214456 来源:jf_46214456 作者:jf_46214456 2024-03-29 17:38 次阅读

介绍

瀑布流式展示图片文字,在当前产品设计中已非常常见,本篇将介绍关于WaterFlow的图片浏览场景,顺便集成Video控件,以提高实践的趣味性

准备

  1. 请参照[官方指导],创建一个Demo工程,选择Stage模型
  2. 熟读HarmonyOS 官方指导“[https://gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md”]

搜狗高速浏览器截图20240326151547.png

效果

竖屏

image.png

横屏

数据源

功能介绍

  1. 瀑布流式图片展示
  2. 横竖屏图片/视频展示

核心代码

布局

整体结构为:瀑布流 + 加载进度条

每条数据结构: 图片 + 文字 【由于没有设定图片宽高比,因此通过文字长度来自然生成瀑布流效果】

由于有点数据量,按照官方指导,采用LazyForEach懒加载方式

Stack() {
  WaterFlow({ scroller: this.scroller }) {
    LazyForEach(dataSource, item = > {
      FlowItem() {
        Column({ space: 10 }) {
          Image(item.coverUrl).objectFit(ImageFit.Cover)
            .width('100%')
            .height(this.imageHeight)
          Text(item.title)
            .fontSize(px2fp(50))
            .fontColor(Color.Black)
            .width('100%')
        }.onClick(() = > {
          router.pushUrl({ url: 'custompages/waterflow/Detail', params: item })
        })
      }
    }, item = > item)
  }
  .columnsTemplate(this.columnsTemplate)
  .columnsGap(5)
  .rowsGap(5)
  .onReachStart(() = > {
    console.info("onReachStart")
  })
  .onReachEnd(() = > {
    console.info("onReachEnd")

    if (!this.running) {
      if ((this.pageNo + 1) * 15 < this.total) {
        this.pageNo++
        this.running = true

        setTimeout(() = > {
          this.requestData()
        }, 2000)
      }
    }

  })
  .width('100%')
  .height('100%')
  .layoutDirection(FlexDirection.Column)

  if (this.running) {
     this.loadDataFooter()
  }

}
复制

横竖屏感知

横竖屏感知整体有两个场景:1. 当前页面发生变化 2.初次进入页面
这里介绍几种监听方式:

当前页面监听

import mediaquery from '@ohos.mediaquery';

//这里你也可以使用"orientation: portrait" 参数
listener = mediaquery.matchMediaSync('(orientation: landscape)');
this.listener.on('change', 回调方法)
复制

外部传参

通过UIAbility, 一直传到Page文件

事件传递

采用EeventHub机制,在UIAbility把横竖屏切换事件发出来,Page文件注册监听事件

this.context.eventHub.on('onConfigurationUpdate', (data) = > {
  console.log(JSON.stringify(data))
  let config = data as Configuration
  this.screenDirection = config.direction
  this.configureParamsByScreenDirection()
});
复制

API数据请求

这里需要设置Android 或者 iOS 特征UA

requestData() {
  let url = `https://api.apiopen.top/api/getHaoKanVideo?page=${this.pageNo}&size=15`
  let httpRequest = http.createHttp()
  httpRequest.request(
    url,
    {
      header: {
        "User-Agent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36"
      }
    }).then((value: http.HttpResponse) = > {

    if (value.responseCode == 200) {

      let searchResult: SearchResult = JSON.parse(value.result as string)

      if (searchResult) {
        this.total = searchResult.result.total

        searchResult.result.list.forEach(ItemModel = > {
          dataSource.addData(ItemModel)
        })
      }
    } else {
      console.error(JSON.stringify(value))
    }

  }).catch(e = > {

    Logger.d(JSON.stringify(e))

    promptAction.showToast({
      message: '网络异常: ' + JSON.stringify(e),
      duration: 2000
    })

  }).finally(() = > {
    this.running = false
  })

}
复制

横竖屏布局调整

因为要适应横竖屏,所以需要在原有布局的基础上做一点改造, 让瀑布流的列参数改造为@State 变量 , 让图片高度的参数改造为@State 变量

WaterFlow({ scroller: this.scroller }) {
  LazyForEach(dataSource, item = > {
    FlowItem() {
      Column({ space: 10 }) {
        Image(item.coverUrl).objectFit(ImageFit.Cover)
          .width('100%')
          .height(this.imageHeight)
        Text(item.title)
          .fontSize(px2fp(50))
          .fontColor(Color.Black)
          .width('100%')
      }.onClick(() = > {
        router.pushUrl({ url: 'custompages/waterflow/Detail', params: item })
      })
    }
  }, item = > item)
}
.columnsTemplate(this.columnsTemplate)
复制

瀑布流完整代码

API返回的数据结构

import { ItemModel } from './ItemModel'

export default class SearchResult{
  public code: number
  public message: string
  public result: childResult
}

class childResult {
  public total: number
  public list: ItemModel[]
};
复制

Item Model

export class ItemModel{
  public id: number
  public tilte: string
  public userName: string
  public userPic: string
  public coverUrl: string
  public playUrl: string
  public duration: string
}复制

WaterFlow数据源接口

import List from '@ohos.util.List';
import { ItemModel } from './ItemModel';

export class PicData implements IDataSource {

  private data: List< ItemModel > = new List< ItemModel >()

  addData(item: ItemModel){
    this.data.add(item)
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {

  }

  registerDataChangeListener(listener: DataChangeListener): void {

  }

  getData(index: number): ItemModel {
     return this.data.get(index)
  }

  totalCount(): number {
    return this.data.length
  }


}复制

布局

import http from '@ohos.net.http';
import { CommonConstants } from '../../common/CommonConstants';
import Logger from '../../common/Logger';
import { PicData } from './PicData';
import SearchResult from './Result';
import promptAction from '@ohos.promptAction'
import router from '@ohos.router';
import common from '@ohos.app.ability.common';
import { Configuration } from '@ohos.app.ability.Configuration';
import mediaquery from '@ohos.mediaquery';

let dataSource = new PicData()

/**
 * 问题: 横竖屏切换,间距会发生偶发性变化
 * 解决方案:延迟300毫秒改变参数
 *
 */
@Entry
@Component
struct GridLayoutIndex {
  private context = getContext(this) as common.UIAbilityContext;
  @State pageNo: number = 0
  total: number = 0
  @State running: boolean = true
  @State screenDirection: number = this.context.config.direction
  @State columnsTemplate: string = '1fr 1fr'
  @State imageHeight: string = '20%'
  scroller: Scroller = new Scroller()

  // 当设备横屏时条件成立
  listener = mediaquery.matchMediaSync('(orientation: landscape)');

  onPortrait(mediaQueryResult) {
    if (mediaQueryResult.matches) {
      //横屏
      this.screenDirection = 1
    } else {
      //竖屏
      this.screenDirection = 0
    }

    setTimeout(()= >{
      this.configureParamsByScreenDirection()
    }, 300)
  }

  onBackPress(){
    this.context.eventHub.off('onConfigurationUpdate')
  }

  aboutToAppear() {
    console.log('已进入瀑布流页面')

    console.log('当前屏幕方向:' + this.context.config.direction)

    if (AppStorage.Get('screenDirection') != 'undefined') {
      this.screenDirection = AppStorage.Get(CommonConstants.ScreenDirection)
    }

    this.configureParamsByScreenDirection()

    this.eventHubFunc()

    let portraitFunc = this.onPortrait.bind(this)
    this.listener.on('change', portraitFunc)

    this.requestData()
  }

  @Builder loadDataFooter() {
    LoadingProgress()
      .width(px2vp(150))
      .height(px2vp(150))
      .color(Color.Orange)
  }

  build() {
    Stack() {
      WaterFlow({ scroller: this.scroller }) {
        LazyForEach(dataSource, item = > {
          FlowItem() {
            Column({ space: 10 }) {
              Image(item.coverUrl).objectFit(ImageFit.Cover)
                .width('100%')
                .height(this.imageHeight)
              Text(item.title)
                .fontSize(px2fp(50))
                .fontColor(Color.Black)
                .width('100%')
            }.onClick(() = > {
              router.pushUrl({ url: 'custompages/waterflow/Detail', params: item })
            })
          }
        }, item = > item)
      }
      .columnsTemplate(this.columnsTemplate)
      .columnsGap(5)
      .rowsGap(5)
      .onReachStart(() = > {
        console.info("onReachStart")
      })
      .onReachEnd(() = > {
        console.info("onReachEnd")

        if (!this.running) {
          if ((this.pageNo + 1) * 15 < this.total) {
            this.pageNo++
            this.running = true

            setTimeout(() = > {
              this.requestData()
            }, 2000)
          }
        }

      })
      .width('100%')
      .height('100%')
      .layoutDirection(FlexDirection.Column)

      if (this.running) {
         this.loadDataFooter()
      }

    }

  }

  requestData() {
    let url = `https://api.apiopen.top/api/getHaoKanVideo?page=${this.pageNo}&size=15`
    let httpRequest = http.createHttp()
    httpRequest.request(
      url,
      {
        header: {
          "User-Agent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36"
        }
      }).then((value: http.HttpResponse) = > {

      if (value.responseCode == 200) {

        let searchResult: SearchResult = JSON.parse(value.result as string)

        if (searchResult) {
          this.total = searchResult.result.total

          searchResult.result.list.forEach(ItemModel = > {
            dataSource.addData(ItemModel)
          })
        }
      } else {
        console.error(JSON.stringify(value))
      }

    }).catch(e = > {

      Logger.d(JSON.stringify(e))

      promptAction.showToast({
        message: '网络异常: ' + JSON.stringify(e),
        duration: 2000
      })

    }).finally(() = > {
      this.running = false
    })

  }

  eventHubFunc() {
    this.context.eventHub.on('onConfigurationUpdate', (data) = > {
      console.log(JSON.stringify(data))
      // let config = data as Configuration
      // this.screenDirection = config.direction
      // this.configureParamsByScreenDirection()
    });
  }

  configureParamsByScreenDirection(){
    if (this.screenDirection == 0) {
      this.columnsTemplate = '1fr 1fr'
      this.imageHeight = '20%'
    } else {
      this.columnsTemplate = '1fr 1fr 1fr 1fr'
      this.imageHeight = '50%'
    }
  }

}

复制

图片详情页

import { CommonConstants } from '../../common/CommonConstants';
import router from '@ohos.router';
import { ItemModel } from './ItemModel';
import common from '@ohos.app.ability.common';
import { Configuration } from '@ohos.app.ability.Configuration';

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

  extParams: ItemModel
  @State previewUri: Resource = $r('app.media.splash')
  @State curRate: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X
  @State isAutoPlay: boolean = false
  @State showControls: boolean = true
  controller: VideoController = new VideoController()

  @State screenDirection: number = 0
  @State videoWidth: string = '100%'
  @State videoHeight: string = '70%'

  @State tipWidth: string = '100%'
  @State tipHeight: string = '30%'

  @State componentDirection: number = FlexDirection.Column
  @State tipDirection: number = FlexDirection.Column

  aboutToAppear() {
    console.log('准备加载数据')
    if(AppStorage.Get('screenDirection') != 'undefined'){
      this.screenDirection = AppStorage.Get(CommonConstants.ScreenDirection)
    }

    this.configureParamsByScreenDirection()

    this.extParams = router.getParams() as ItemModel
    this.eventHubFunc()
  }

  onBackPress(){
    this.context.eventHub.off('onConfigurationUpdate')
  }

  build() {
      Flex({direction: this.componentDirection}){
        Video({
          src: this.extParams.playUrl,
          previewUri: this.extParams.coverUrl,
          currentProgressRate: this.curRate,
          controller: this.controller,
        }).width(this.videoWidth).height(this.videoHeight)
          .autoPlay(this.isAutoPlay)
          .objectFit(ImageFit.Contain)
          .controls(this.showControls)
          .onStart(() = > {
            console.info('onStart')
          })
          .onPause(() = > {
            console.info('onPause')
          })
          .onFinish(() = > {
            console.info('onFinish')
          })
          .onError(() = > {
            console.info('onError')
          })
          .onPrepared((e) = > {
            console.info('onPrepared is ' + e.duration)
          })
          .onSeeking((e) = > {
            console.info('onSeeking is ' + e.time)
          })
          .onSeeked((e) = > {
            console.info('onSeeked is ' + e.time)
          })
          .onUpdate((e) = > {
            console.info('onUpdate is ' + e.time)
          })

        Flex({direction: this.tipDirection, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center, alignContent: FlexAlign.Center}){
          Row() {
            Button('src').onClick(() = > {
              // this.videoSrc = $rawfile('video2.mp4') // 切换视频源
            }).margin(5)
            Button('previewUri').onClick(() = > {
              // this.previewUri = $r('app.media.poster2') // 切换视频预览海报
            }).margin(5)
            Button('controls').onClick(() = > {
              this.showControls = !this.showControls // 切换是否显示视频控制栏
            }).margin(5)
          }

          Row() {
            Button('start').onClick(() = > {
              this.controller.start() // 开始播放
            }).margin(5)
            Button('pause').onClick(() = > {
              this.controller.pause() // 暂停播放
            }).margin(5)
            Button('stop').onClick(() = > {
              this.controller.stop() // 结束播放
            }).margin(5)
            Button('setTime').onClick(() = > {
              this.controller.setCurrentTime(10, SeekMode.Accurate) // 精准跳转到视频的10s位置
            }).margin(5)
          }
          Row() {
            Button('rate 0.75').onClick(() = > {
              this.curRate = PlaybackSpeed.Speed_Forward_0_75_X // 0.75倍速播放
            }).margin(5)
            Button('rate 1').onClick(() = > {
              this.curRate = PlaybackSpeed.Speed_Forward_1_00_X // 原倍速播放
            }).margin(5)
            Button('rate 2').onClick(() = > {
              this.curRate = PlaybackSpeed.Speed_Forward_2_00_X // 2倍速播放
            }).margin(5)
          }
        }
        .width(this.tipWidth).height(this.tipHeight)
      }

  }

  eventHubFunc() {
    this.context.eventHub.on('onConfigurationUpdate', (data) = > {
       console.log(JSON.stringify(data))
       let config = data as Configuration
       this.screenDirection = config.direction

       this.configureParamsByScreenDirection()

    });
  }


  configureParamsByScreenDirection(){
    if(this.screenDirection == 0){
      this.videoWidth = '100%'
      this.videoHeight = '70%'
      this.tipWidth = '100%'
      this.tipHeight = '30%'
      this.componentDirection = FlexDirection.Column

    } else {
      this.videoWidth = '60%'
      this.videoHeight = '100%'
      this.tipWidth = '40%'
      this.tipHeight = '100%'
      this.componentDirection = FlexDirection.Row

    }

  }

}

审核编辑 黄宇

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 鸿蒙
    +关注

    关注

    57

    文章

    2339

    浏览量

    42805
  • HarmonyOS
    +关注

    关注

    79

    文章

    1973

    浏览量

    30143
  • 鸿蒙OS
    +关注

    关注

    0

    文章

    188

    浏览量

    4382
收藏 人收藏

    评论

    相关推荐

    HDC2024华为发布鸿蒙原生智能:AI与OS深度融合,开启全新的AI时代

    6月21日,华为开发者大会2024(HDC.2024)召开。 HarmonyOS NEXT将AI与OS深度融合,构筑全新鸿蒙原生智能框架。大会现场,华为常务董事、终端BG董事长、智能汽车解决方案BU
    的头像 发表于 06-24 09:28 612次阅读
    HDC2024华为发布<b class='flag-5'>鸿蒙</b>原生智能:AI与<b class='flag-5'>OS</b>深度融合,开启全新的AI时代

    鸿蒙OS开发:【一次开发,多端部署】应用(资源使用)

    在页面开发过程中,经常需要用到颜色、字体、间距、图片等资源,在不同的设备或配置中,这些资源的值可能不同。
    的头像 发表于 05-21 15:43 1009次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>OS</b><b class='flag-5'>开发</b>:【一次<b class='flag-5'>开发</b>,多端部署】应用(资源使用)

    HarmonyOS实战开发-如何通过BlendMode属性来实现挂件和图片的混合

    ||---BlendModeView.ets // 视图层-应用主页面 模块依赖 本实例依赖common模块来实现日志的打印、资源 的调用、依赖动态路由模块来实现页面的动态加载。 最后 如果大家觉得这篇内容对学习鸿蒙开发有帮
    发表于 05-07 14:45

    鸿蒙OS崛起,鸿蒙应用开发工程师成市场新宠

    应用的形态也在发生着翻天覆地的变化。作为全球领先的移动操作系统和智能终端制造商,华为公司自主研发的鸿蒙OS应运而生,致力于构建一个统一的分布式操作系统,为各行各业的应用开发带来全新的可能性。 一、
    发表于 04-29 17:32

    HarmonyOS开发实例:【图片编辑应用】

    通过动态设置元素样式的方式,实现几种常见的图片操作,包括裁剪、旋转、缩放和镜像。
    的头像 发表于 04-23 09:42 435次阅读
    HarmonyOS<b class='flag-5'>开发</b><b class='flag-5'>实例</b>:【<b class='flag-5'>图片</b>编辑应用】

    OpenHarmony开发实例:【鸿蒙.bin文件烧录】

    如何使用HiBurn工具烧录鸿蒙的.bin文件到Hi3861开发板。
    的头像 发表于 04-14 09:54 427次阅读
    OpenHarmony<b class='flag-5'>开发</b><b class='flag-5'>实例</b>:【<b class='flag-5'>鸿蒙</b>.bin文件烧录】

    鸿蒙OS开发实例:【HarmonyHttpClient】网络框架

    鸿蒙上使用的Http网络框架,里面包含纯Java实现的HttpNet,类似okhttp使用,支持同步和异步两种请求方式;还有鸿蒙版retrofit,和Android版Retrofit相似的使用,解放双手般优雅使用注解、自动解析json
    的头像 发表于 04-12 16:58 827次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>OS</b><b class='flag-5'>开发</b><b class='flag-5'>实例</b>:【HarmonyHttpClient】网络框架

    鸿蒙OS开发学习:【尺寸适配实现】

    鸿蒙开发中,尺寸适配是一个重要的概念,它可以帮助我们在不同屏幕尺寸的设备上正确显示和布局我们的应用程序。本文将介绍如何在鸿蒙开发中实现尺寸适配的方法。
    的头像 发表于 04-10 16:05 1733次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>OS</b><b class='flag-5'>开发</b>学习:【尺寸适配实现】

    鸿蒙OS开发实例:【组件化模式】

    组件化一直是移动端比较流行的开发方式,有着编译运行快,业务逻辑分明,任务划分清晰等优点,针对Android端的组件化;与Android端的组件化相比,HarmonyOS的组件化可以说实现起来就颇费
    的头像 发表于 04-07 17:44 635次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>OS</b><b class='flag-5'>开发</b><b class='flag-5'>实例</b>:【组件化模式】

    鸿蒙OS开发实例:【应用事件打点】

    传统的日志系统里汇聚了整个设备上所有程序运行的过程流水日志,难以识别其中的关键信息。因此,应用开发者需要一种数据打点机制,用来评估如访问数、日活、用户操作习惯以及影响用户使用的关键因素等关键信息
    的头像 发表于 04-07 17:13 470次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>OS</b><b class='flag-5'>开发</b><b class='flag-5'>实例</b>:【应用事件打点】

    鸿蒙APP开发实战:【Api9】拍照、拍视频;选择图片、视频、文件工具类

    鸿蒙开发过程中,经常会进行系统调用,拍照、拍视频、选择图库图片、选择图库视频、选择文件。今天就给大家分享一个工具类。
    的头像 发表于 03-26 16:27 770次阅读
    <b class='flag-5'>鸿蒙</b>APP<b class='flag-5'>开发</b>实战:【Api9】拍照、拍视频;选择<b class='flag-5'>图片</b>、视频、文件工具类

    使用 Taro 开发鸿蒙原生应用 —— 快速上手,鸿蒙应用开发指南

    随着鸿蒙系统的不断完善,许多应用厂商都希望将自己的应用移植到鸿蒙平台上。最近,Taro 发布了 v4.0.0-beta.x 版本,支持使用 Taro 快速开发鸿蒙原生应用,也可将现有的
    的头像 发表于 02-02 16:09 859次阅读
    使用 Taro <b class='flag-5'>开发</b><b class='flag-5'>鸿蒙</b>原生应用 —— 快速上手,<b class='flag-5'>鸿蒙</b>应用<b class='flag-5'>开发</b>指南

    鸿蒙开发教学-图片的引用

    该接口通过图片数据源获取图片,支持本地图片和网络图片的渲染展示。其中,src是图片的数据源。
    的头像 发表于 02-01 17:36 687次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>开发</b>教学-<b class='flag-5'>图片</b>的引用

    鸿蒙OS和开源鸿蒙什么关系?

    内核,其他功能都以模块的形式存在。     华为用的是鸿蒙OS 我们都知道,华为手机的鸿蒙OS是可以运行安卓软件的,是因为系统中有安卓兼容层,所以可以简单这么理解:
    的头像 发表于 01-30 15:44 1130次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>OS</b>和开源<b class='flag-5'>鸿蒙</b>什么关系?

    免费学习鸿蒙(HarmonyOS)开发,一些地址分享

    国内一流高校。通过鸿蒙班的设立,高校可以为学生提供专业的鸿蒙OS学习环境和丰富的实践机会,培养出更多的鸿蒙开发人才,为
    发表于 01-12 20:48