9-社区动态(Stack布局)
涉及知识点:
-
stack布局
-
video组件
3.组件状态控制: @State、@Prop以及@Link装饰器 -
组件状态控制:@Observed&@ObjectLink装饰器
课时: 2
1 任务
1.1 需求
完成社区动态功能,社区动态显示用户发布的跟游戏相关的短视频,自动循环播放发布者发布的短视频,向下滑动切换到下一条短视频,短视频播放页面显示发布者,视频标题,视频描述,发布者头像,点赞数,评论数,收藏数,转发数等
1.2 界面原型
社区动态页面,上下滑动可以播放其他短视频,点击可以暂停和继续播放,可以点赞和收藏:
2 预备知识
2.1 Stack布局(层叠布局)
层叠布局官方指南:
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-layout-development-stack-layout
- 固定定位与层叠
- 依次入栈(代码渲染顺序)
- 默认居中堆叠,可通过alignContent参数控制对齐位置
- 调整层级关系:Z序控制,zIndex
2.2 Video组件
播放视频,控制视频播放状态。
官方参考:
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-common-components-video-player
视频加载(src属性设置视频链接):
-
本地视频
-
在rawfile下,$rawfile(‘文件路径’)
-
沙箱路径,file:///data/storage/el2/base/haps/entry/files/show.mp4
-
-
网络视频
-
申请权限ohos.permission.INTERNET
-
指定视频连接:https://www.example.com/example.mp4
-
常用参数:
-
src:加载视频
-
previewUri:海报
-
controller: 播放控制器
private controller: VideoController = new VideoController()
常用属性:
-
muted:是否静音
-
controls:是否显示默认控制条
-
autoPlay:是否自动播放
-
loop:是否循环播放
-
objectFit:视频适配模式
枚举类型说明:
枚举说明-公共定义-ArkTS组件-ArkUI(方舟UI框架)-应用框架 - 华为HarmonyOS开发者
- ImageFit.Contain:保持宽高比进行缩小或者放大,使得图片完全显示在显示边界内
- ImageFit.Cover:保持宽高比进行缩小或者放大,使得图片两边都大于或等于显示边界
- ImageFit.Auto:图像会根据其自身尺寸和组件的尺寸进行适当缩放,以在保持比例的同时填充视图。
- ImageFit.Fill:不保持宽高比进行放大缩小,使得图片充满显示边界。
事件回调:
播放开始(onStart),播放暂停(onPause)、播放结束(onFinish)、播放失败(onError)、播放停止(onStop)、视频准备完成(onPrepared),进度条定位(onSeeked),播放进度变化(onUpdate)、全屏非全屏播放状态变化(onFullscreenChange)等。
视频组件控制器(VideoController):
开始播放(start)、暂停(pause)、停止(stop)、重置(reset)、指定播放位置(setCurrentTime),请求全屏播放(requestFullscreen),退出全屏播放(exitFullscreen)
2.3 组件状态维护
官方参考:
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-state-management-v1
2.3.1 @State 组件内状态
- 私有,只能从组件内访问
- 必须本地初始化
- 与子组件中的@Prop装饰变量之间建立单向数据同步
- 与@Link、@ObjectLink装饰变量之间建立双向数据同步
- 变量类型:
- Object、class、string、number、boolean、enum类型
- 数组
- Date
- Map、Set
2.3.2 @Prop 父子单向传递
- @Prop装饰的变量是可变的,但不会同步给父组件
- 父组件的修改会覆盖子组件的修改
- 可以本地初始化
- 私有,只能从组件内访问
- 变量类型:
- Object、class、string、number、boolean、enum类型
- 数组
- Date
- Map、Set
2.3.3 @Link 父子双向传递
- 双向同步
- 禁止本地初始化
- 私有,只能在所属组件内访问
- 变量类型:
- Object、class、string、number、boolean、enum类型
- 数组
- Date
- Map、Set
2.3.4 @Observed
- 加在类前面
- 观察对象类属性变化
2.3.5 @ObjectLink装饰器
- 接收@Observed装饰的类的实例
- 和父组件中对应的状态变量建立双向数据绑定
- 不支持简单类型
- 不能本地初始化,必须从父组件初始化
- 不能在@Entry中使用(因为需要从父组件实例化)
3 新建社区动态展示组件
在components文件夹下新建ArkTS文件,命名为:CommunityVideoComponent,并编写基础代码:
@Component
export default struct CommunityVideoComponent{build() {Column(){Text('社区动态').fontColor(Color.White)}.width('100%').height('100%').backgroundColor(Color.Black)}
}
Note:
需要播放视频,背景设置为黑色
4 封装视频信息
在model下新建ArkTS文件,命名为VideoBean,封装id,标题,视频连接,分享次数、点赞数、是否小红心、评论数等信息,并生成构造方法:
export default class VideoBean {id: number;title: string;//视频标题url:Resource;//视频连接shareCount:number; //分享次数likeCount:number; //点赞数isLike:boolean; //是否小红心commentCount: number;//评论数author: string; //作者videoDesc:string; //视频描述previewUrl: Resource; //海报isStar:boolean ;//是否收藏starCount: number ;//收藏数constructor(id: number, title: string, url: Resource, shareCount: number, likeCount: number, isLike: boolean,commentCount: number,author:string,videoDesc:string,previewUrl:Resource,isStar:boolean,starCount:number) {this.id = id;this.title = title;this.url = url;this.shareCount = shareCount;this.likeCount = likeCount;this.isLike = isLike;this.commentCount = commentCount;this.author = author;this.videoDesc = videoDesc;this.previewUrl = previewUrl;this.isStar = isStar;this.starCount = starCount;}}
5 后台短视频数据准备
在model中新建arkts文件,命名为:CommunityVideoModel ,并在CommunityVideoModel 中编码,添加getVideos方法,准备短视频数据:
export default class CommunityVideoModel {// 获取短视频数据getVideos():Array<VideoBean>{let videos:Array<VideoBean> = [new VideoBean(1,'一起放风筝',$rawfile('video/video1.mp4'),111,333,true,666,'jerry','大风起,云飞扬,好男儿走四方!',$rawfile('video/poster1.png'),true,123),new VideoBean(2,'一起跳泥坑',$rawfile('video/video2.mp4'),222,444,false,888,'tom','有坑你不躲,非要往里跳!',$rawfile('video/poster2.png'),false,321),new VideoBean(3,'虫儿飞',$rawfile('video/video3.mp4'),333,666,true,999,'jerry','虫儿飞,虫儿飞,搞笑的虫子不会飞!',$rawfile('video/poster3.png'),true,312),new VideoBean(4,'一起玩',$rawfile('video/video4.mp4'),444,888,false,777,'jerry','早起的虫儿,被鸟吃!',$rawfile('video/poster4.png'),false,777),new VideoBean(5,'神笔马良',$rawfile('video/video5.mp4'),444,888,false,777,'tom','这是神笔马良么?',$rawfile('video/poster5.png'),false,555),new VideoBean(6,'光头强改行',$rawfile('video/video6.mp4'),444,888,false,777,'jerry','光头强从此不砍树了!',$rawfile('video/poster6.png'),false,893),new VideoBean(7,'每次都说没问题',$rawfile('video/video7.mp4'),444,888,false,777,'tom','每次都说没问题,这次是真的么?',$rawfile('video/poster7.png'),false,532),new VideoBean(8,'侠客行',$rawfile('video/video8.mp4'),444,888,false,777,'jerry','何人倚剑白云间',$rawfile('video/poster8.png'),false,236),new VideoBean(9,'心静自然凉',$rawfile('video/video9.mp4'),444,888,false,777,'jerry','心静自然凉,鹰飞要凉凉',$rawfile('video/poster9.png'),false,956),new VideoBean(10,'这一片江上',$rawfile('video/video10.mp4'),444,888,false,777,'jerry','这一片江山都是你的,别往那边看,那边是别人的!',$rawfile('video/poster10.png'),true,8444),];return videos;}
}
6 短视频轮播
界面原型:
- 先在MainPage中调用社区动态组件:
TabContent(){//Text('动态')CommunityVideoComponent()//社区动态}//.tabBar('动态').tabBar(this.MyTabBuilder(TabID.COMMUNITY))
预览效果:
- 封装视频播放组件
在components文件下新建arkts文件,命名为:PlayVideoComponent:
@Component
export struct PlayVideoComponent{@Prop videoBean:VideoBean;private videoController: VideoController = new VideoController();@Prop isPlay:boolean ;//初始时是否自动播放视频build() {Column(){Video({src:this.videoBean.url,controller:this.videoController,previewUri:this.videoBean.previewUrl}).controls(true).loop(true).autoPlay(this.isPlay).objectFit(ImageFit.Fill).width('100%').height('40%')}.width('100%').height('100%')}}
Note:
视频信息需要父组件传入,是否自动播放需要由父组件控制,因此需暴露这两个属性,并使用单向传递,即使用@Prop
- 建立轮播组件并调用
在CommunityVideoComponent中编码,首先定义变量:
private communityVideoModel:CommunityVideoModel = new CommunityVideoModel();@State currentIdx: number = 0;
然后在Column布局中使用轮播组件:
// Text('社区动态')// .fontColor(Color.White)Swiper(){ForEach(this.communityVideoModel.getVideos(),(item:VideoBean,index:number)=>{if(index === this.currentIdx){PlayVideoComponent({videoBean:item,isPlay:true})}else{PlayVideoComponent({videoBean:item,isPlay:false})}},(item:VideoBean)=>JSON.stringify(item))}.indicator(false).loop(false).vertical(true).onChange((index)=>{console.info('idx:'+index)this.currentIdx = index;})
Note:
根据swiper组件的当前索引进行循环渲染一个默认自动播放还是不自动播放的视频播放组件,如不这样控制则所有视频会一起播放。
轮播组件设置为上下滑动,不要自动轮播,不要出现指示条
启动本地模拟器进行测试(video组件不能在预览器中预览):
上下滑动可以自由切换视频,并不会所有视频一起播放。
- 使用点击控制播放和暂停
在PlayVideoComponent中编码,首先定义枚举型变量控制播放状态(在组件外面写代码):
enum PlayState {STOP = 0,START = 1,PAUSE = 2
}
@Component
export struct PlayVideoComponent{
...
然后在组件内定义控制播放状态的变量:
export struct PlayVideoComponent{...@State playState: number = PlayState.START;build() {...
在Video组件上添加单击事件,控制播放状态,同时隐藏播放控制条:
Video({src:this.videoBean.url,controller:this.videoController,previewUri:this.videoBean.previewUrl}).controls(false)//隐藏控制条.loop(true).autoPlay(this.isPlay).objectFit(ImageFit.Fill).onClick(()=>{if(this.playState === PlayState.START){this.playState = PlayState.PAUSEthis.videoController.pause();}else if(this.playState === PlayState.PAUSE){this.playState = PlayState.START;this.videoController.start();}else {this.playState = PlayState.STARTthis.videoController.start();}})
在本地模拟器中预览效果,测试是否可以通过点击控制视频播放和继续:
- Tab切换时停止播放短视频
当Tabs组件切换到首页或者其他页签时,短视频是在后台播放的,需要tabs索引和swiper索引联合控制短视频是否播放。
首先修改CommunityVideoComponent
,添加变量,暴露给父组件(包含tabs布局的MainPage)控制是否自动播放:
@Prop canPlay:boolean;
然后,在Swiper组件中加入父组件参与控制:
Swiper(){ForEach(this.communityVideoModel.getVideos(),(item:VideoBean,index:number)=>{if(index === this.currentIdx && this.canPlay){PlayVideoComponent({videoBean:item,isPlay:true})}else{PlayVideoComponent({videoBean:item,isPlay:false})}},(item:VideoBean)=>JSON.stringify(item))}
最后在MainPage中,调用社区动态组件时,传入播放控制变量,根据当前显示的TabID是否是社区动态页签决定是否播放社区动态中的视频:
...
@State isComunityVideoShow: boolean = false;
...build(){ Tabs({barPosition:BarPosition.End}){TabContent(){//Text('动态')//CommunityVideoComponent()CommunityVideoComponent({canPlay:this.isComunityVideoShow})//社区动态}//.tabBar('动态').tabBar(this.MyTabBuilder(TabID.COMMUNITY))...}.width('100%').height('100%').onChange((index)=>{this.pageIndex = index;//控制换页if(this.pageIndex === TabID.COMMUNITY){this.isComunityVideoShow = true;}else{this.isComunityVideoShow = false;}})
在本地模拟器中测试,当切换到首页时,播放的短视频应该是没有声音的,处于停止状态,切换回社区动态时,短视频正常播放。
7 视频描述视图
界面原型:
在components目录下新建arkts文件,命名为VideoDescComponent,编写如下骨架代码,视频bean需要父组件传入:
@Component
export struct VideoDescComponent {@Prop videoBean:VideoBean;build(){}
}
在build函数中布局视频描述信息:
Column({space:5}){Text(`@${this.videoBean.author}`).fontSize(20).fontColor(Color.White)Text(this.videoBean.title).fontSize(18).fontColor(Color.White).fontWeight(FontWeight.Bold)Text(this.videoBean.videoDesc).fontSize(15).fontColor(Color.White)}.width('95%').height('30%').alignItems(HorizontalAlign.Start).offset({y:'40%'})//相对定位
Note:
offset:相对定位,相对于自己本身的渲染位置在y轴上偏移40%
在PlayVideoComponent中调用,由于视频组件和描述组件需要叠放,因此将跟容器改为Stack堆叠布局:
build() {Stack({alignContent:Alignment.End}){Video({src:this.videoBean.url,controller:this.videoController,previewUri:this.videoBean.previewUrl})...}).width('100%').height('40%')//视频描述视图VideoDescComponent({videoBean:this.videoBean})}.width('100%').height('100%')}
Note:
Stack: 堆叠布局,设置在底部对齐
在本地模拟器中运行,测试叠放效果:
8 评论视图
界面原型:
- 完成评论视图布局
在components目录下新建arkts文件,命名为VideoCommentComponent,并完成头像、小红心,评论,收藏,转发等布局:
@Component
export struct VideoCommentComponent {@Prop videoBean:VideoBean;build(){Column(){Image($r('app.media.head_logo')).width(50).height(50).border({width:3,color:Color.White,radius:25}).objectFit(ImageFit.Contain)Image(this.videoBean.isLike ?$r('app.media.like1'): $r('app.media.like3')).width(35).height(35).margin({top:30})Text(this.videoBean.likeCount === 0 ? '点赞' : this.videoBean.likeCount.toString()).fontSize(15).fontColor(Color.White)Image($r('app.media.comment3')).width(35).height(35).margin({top:20})Text(this.videoBean.commentCount === 0 ? '评论' : this.videoBean.commentCount.toString()).fontSize(15).fontColor(Color.White)Image(this.videoBean.isStar ?$r("app.media.star1"): $r("app.media.star3")).width(35).height(35).margin({top:30})Text(this.videoBean.starCount === 0 ? '收藏' : this.videoBean.starCount.toString()).fontSize(15).fontColor(Color.White)Image($r('app.media.share3')).width(35).height(35).margin({top:20})Text(this.videoBean.shareCount === 0 ? '分享' : this.videoBean.shareCount.toString()).fontSize(15).fontColor(Color.White)}.offset({ x: '-5%', y: '10%' })}
}
在本地模拟器中的测试效果:
- 小红心和收藏功能
在小红心和小星星上添加点击事件,处理状态变化:
Image(this.videoBean.isLike ?$r('app.media.like1'): $r('app.media.like3')).width(35).height(35).margin({top:30}).onClick(()=>{if(this.videoBean.isLike){this.videoBean.likeCount --}else{this.videoBean.likeCount ++}this.videoBean.isLike = !this.videoBean.isLike})...Image(this.videoBean.isStar ?$r("app.media.star1"): $r("app.media.star3")).width(35).height(35).margin({top:30}).onClick(()=>{if(this.videoBean.isStar){this.videoBean.starCount --}else{this.videoBean.starCount ++}this.videoBean.isStar = !this.videoBean.isStar})
现在你可以在本地模拟器中测试代码,小红心和小星星的状态会发生变化,但是一划走再回来,或者切换到其他页面下,又恢复了初始数据,就是组件的状态跟踪没有处理好,我们对videoBean使用的是单向的数据传递,即使用Prop,改成双向数据传递有两种策略:
方式一:
- 将PlayVideoComponent中的videoBean属性都改成@State并赋初值
@State videoBean:VideoBean = new VideoBean(1,'一起放风筝',$rawfile('video/video1.mp4'),111,333,true,666,'jerry','大风起,云飞扬,好男儿走四方!',$rawfile('video/poster1.png'),true,123);
- 将VideoCommentCompoent中的videoBean改成@Link
@Link videoBean:VideoBean;
这样可以实现双向数据更新
方式一,不可以将PlayVideoComponent中的videoBean改成@Link,否则无法从CommunityVideoComponent组件中通过循环渲染赋值。
方式二(推荐):
- 在VideoBean类前加@Observed
@Observed
export default class VideoBean {
...
}
- 将PlayVideoComponent和VideoCommentCompoent中的videoBean都改成@ObjectLink
@ObjectLink videoBean:VideoBean;
Note:
@Observed:加在类前面
@ObjectLink: 加在组件引用的对象前面,当组件改变该对象内部属性时会同步给所有引用该类的实例,实现其他组件数据的同步刷新。
在本地模拟器中测试,经过以上修改后,小红心,小星星的状态可以在上下滑动以及页面切换中正常维护同步。
参考
代码仓
https://gitee.com/snowyvalley/harmony-app-dev-basic-course.git