目录
前言
功能展示
整体页面布局
最新和最热
写评论
点赞功能
界面构建
初始数据的准备
列表项部分的渲染
底部区域
index部分
知识点概述
List组件
List组件简介
ListItem组件详解
ListItemGroup组件介绍
ForEach循环渲染
列表分割线设置
列表排列方向设置
索引值计算规则
@Prop装饰器
@Prop注解的基本作用
@Prop注解的限制条件
@Prop注解的使用规则
@Prop注解的具体使用场景
全套代码
BottomCom部分
InfoCom部分
InfoItem部分
CommentDate部分
Index部分
个人主页→VON
收录专栏→鸿蒙开发小型案例总结
基础语法部分会发布于github 和 gitee上面(暂未发布)
由于一些个人的特殊原因,基础部分的代码迟迟未进行上传,我会尽快进行整理发布后会第一时间进行通知,希望大家多多谅解
前言
鸿蒙基础部分到这里也要和大家说再见了,此案例所用到的知识点众多构建过程较为复杂。希望读者们能够认真观看。我也会尽力帮助大家梳理代码以及各各部分的逻辑及其思路。
此案例可以应用于多个地方,比如博客,短视频app等都可以进行应用
功能展示
整体页面布局
整体分为三大部分,分别为头部、中间、底部。认真观察不难发现头部区域采用了Row,中间区域采用了List,底部区域采用了两个Row。整体布局为层叠布局。
最新和最热
最新和最热点击时显示的效果不同,并且列表所展示的也不同。点击最新时评论的内容会根据时间进行排序,最热时会根据点赞数进行排序。
写评论
自己可以在写评论处发布评论,列表会根据评论进行渲染。
点赞功能
界面构建
由于代码的长度限制,过于基础的部分不在进行逐一讲解,希望大家谅解
最新和最热是两个按钮组件,Extend装饰器进行修改,当未被选中时是一种状态,被选中时是另外一种状态。这两种状态的样式我是根据最左侧的颜色来进行适当调整的,大家也可以效仿。
初始数据的准备
初始数据是自己进行构建的一些数据,这些数据被打包在CommentDate中,其中点赞数和等级是随机的,其他部分都是自定义的。因为这是一个离线的静态页面,并没有实现交互功能所以目前只能这样来代替。
列表项部分的渲染
渲染部分用到了CommentDate中的数据来进行逐一渲染的,因为实现交互的是子组件,但是要修改的部分是父组件的一些内容,所以对于可变的数据用到了Prop装饰器进行的装饰。
底部区域
底部区域实现了双向绑定,当输入评论并且单击回车键后数据会进行增加。并且会排在首位,也就是下标为0的位置增添了一个全新的数据。
index部分
一些函数的定义及其功能的实现全都在index页面来进行实现的,所以这一界面要尽量保持简介,每一部分都可以进行抽取单独进行ui界面的构建,通过import来进行导入即可。
知识点概述
List组件
鸿蒙开发中的List组件是一个功能强大且常用的UI组件,用于呈现连续的数据列表。
List组件简介
- 基础定义:List组件是一种容器组件,用于展示一系列相同宽度的列表项,适合连续、多行呈现同类数据,例如图片和文本。
- 参数配置:List组件接受三个主要参数:space(子组件主轴方向的间隔)、initialIndex(初次加载时视口起始位置显示的item索引)、scroller(可滚动组件的控制器)。
ListItem组件详解
- 基本概念:ListItem是具体的列表项组件,必须配合List使用,用于展示每个具体的数据项。
- 参数设置:主要参数包括selectable(是否可被鼠标框选)和swipeAction(设置划出组件的属性)。
ListItemGroup组件介绍
- 功能描述:用于展示列表项分组,宽度默认充满List组件,也必须配合List使用。
- 主要参数:header(头部组件)、footer(尾部组件)和space(列表项间距)。
ForEach循环渲染
- 应用场景:当列表由多个相似或重复的列表项组成时,为减少代码冗余,可以使用ForEach进行循环渲染。
- 工作原理:ForEach接口基于数组类型数据进行循环渲染,需与容器组件配合使用,如List。
列表分割线设置
- 功能解释:通过divider属性设置列表项之间的分割线样式,提升视觉上的区分度和美观性。
- 参数说明:主要参数包括strokeWidth(分割线的线宽)、color(分割线的颜色)、startMargin和endMargin(分割线距离列表侧边起始端和结束端的距离)。
列表排列方向设置
- 垂直排列:List默认采用垂直排列方式,即列表项按垂直方向线性排列。
- 水平排列:通过设置listDirection属性为Axis.Horizontal,可以实现列表项的水平排列。
索引值计算规则
- 规则概述:索引值用来确定列表项在列表中的具体位置,初次加载时默认从0开始。
- 细节掌握:初始索引可以通过initialIndex参数手动设置,但需确保索引值不超过列表项总数。
@Prop装饰器
@Prop是一个方便的注解,用于在鸿蒙应用开发中实现组件之间的数据传递。
在现代软件开发中,组件化和数据传递是提高开发效率和代码可维护性的关键环节。特别是在鸿蒙这类分布式操作系统中,对组件间数据传递机制的优化尤为重要。下面将详细探讨@Prop注解的作用、限制条件、使用规则以及具体的使用场景。
@Prop注解的基本作用
- 单向数据同步:@Prop注解主要用于实现组件间的单向数据同步。这意味着,当父组件的状态发生改变时,这些改变会通过@Prop注解传递给子组件,但子组件对这些属性的修改不会影响到父组件。
- 支持的数据类型:@Prop能够处理各种基本数据类型,包括字符串、数字、布尔值和枚举类型。这保证了其在不同场景下的灵活性和适用性。
@Prop注解的限制条件
- 复杂类型的深拷贝:当@Prop涉及到复杂数据类型时(如对象或数组),会进行深拷贝操作。这一过程中,除了基本类型(如字符串、数字)、Map、Set、Date和Array外,其他类型可能会丢失。
- 使用场景限制:@Prop不能在@Entry装饰的自定义组件中使用。这限定了@Prop的使用范围,通常仅适用于页面级组件。
@Prop注解的使用规则
- 参数配置:@Prop不需要特定参数,其同步类型为单向同步。
- 类型要求严格:使用@Prop时必须明确指定被装饰变量的具体类型,不允许使用any类型,也不允许使用undefined和null作为默认值。
@Prop注解的具体使用场景
- 简单数据类型同步:例如,父组件中的@State状态可以通过@Prop注解传递给子组件。如果父组件的状态发生更新,子组件的@Prop也会相应更新。但如果子组件尝试修改这些属性,更改不会反映到父组件中。
- 数组项同步:当父组件中的@State数组项更新时,子组件中对应的@Prop也会同步更新。例如,父组件可以包含一个数字数组,每个元素用来初始化子组件中的一个@Prop。同样,子组件对@Prop的修改不会反映到父组件中。
- 类对象属性同步:父组件中的@State类对象可以用来初始化子组件的@Prop。任何父组件中对象属性的更新都会同步到子组件,但子组件对@Prop的修改依旧不会反向同步到父组件。
全套代码
BottomCom部分
@Component
struct BottomCom {
@State txt:string=''
onSubmitComment=(content:string)=>{}
build() {
Row(){
Row(){
Image($r('app.media.edit'))
.width(20)
.margin({left:10})
TextInput({
placeholder:'写评论...',
// 双向绑定
text:$$this.txt
})
.backgroundColor(Color.Transparent)
.fontSize(18)
// 回车
.onSubmit(()=>{
this.onSubmitComment(this.txt)
})
}
.height(40)
.backgroundColor('#f5f6f5')
.borderRadius(20)
.margin({left:15,right:20,top:10,bottom:10})
.layoutWeight(1)
Image($r('app.media.love_stare'))
.width(25)
Image($r("app.media.like_stare"))
.width(25)
.margin({left:15,right:10})
}
.width('100%')
.height(60)
}
}
export default BottomCom
InfoCom部分
import BottomCom from './BottomCom'
@Extend(Button)
function ButStyle(click:boolean){
.fontSize(12)
.border({width:1,color:click?'#fff':'#ffbeb7b7'})
.width(46)
.height(32)
.padding({left:5,right:5})
.fontColor(click ? '#80555858' :'#ff1f1e1e')
.backgroundColor(click ? '#fff' : '#1ae0e0e0')
}
@Component
struct InfoCom {
@State click:boolean=true
onSort=(type:number)=>{
}
build() {
Row(){
Text('全部评论')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Row(){
Button('最新')
.ButStyle(!this.click)
.onClick(()=>{
this.click=true
this.onSort(0)
})
Button('最热')
.ButStyle(this.click)
.onClick(()=>{
this.click=false
this.onSort(1)
})
}
}
.justifyContent(FlexAlign.SpaceBetween)
.padding({left:20,right:20})
.width('100%')
.height(60)
}
}
export default InfoCom
InfoItem部分
import { CommentDate } from '../model/CommentDate'
@Component
struct InfoItem {
@Prop itemObj:CommentDate
@Prop index:number
onLikeClick=(index:number)=>{
}
build() {
// 列表项组件
Column(){
Row(){
// 头像
Image(this.itemObj.avatar)
.width(30)
.borderRadius(15)
.margin({top:10,right:5})
// 昵称
Text(this.itemObj.name)
.fontColor('#808d8585')
.fontSize(14)
.margin({top:10,left:5})
// 等级
Image(this.itemObj.levelIcon)
.width(20)
.margin({left:10,top:10})
}
// 评论内容
Text(this.itemObj.commentext)
.fontSize(13)
.fontWeight(700)
.margin({top:10,left:40})
Row(){
// 时间
Text(this.itemObj.timeString)
.fontSize(10)
.fontColor(Color.Gray)
// 点赞
Row(){
Image(this.itemObj.islike ? $r('app.media.like_end') : $r('app.media.like_stare'))
.width(12)
Text(this.itemObj.likenum.toString())
.fontSize(10)
.fontColor(this.itemObj.islike ? Color.Red : Color.Gray)
}
.onClick(()=>{
this.onLikeClick(this.index)
})
}
.width('100%')
.padding({left:40,right:20,top:15})
.justifyContent(FlexAlign.SpaceBetween)
}
.alignItems(HorizontalAlign.Start)
.padding({left:15,top:10})
}
}
export default InfoItem
CommentDate部分
// 准备评论数据类
export class CommentDate{
avatar:ResourceStr;// 头像
name:string;// 昵称
level:number;// 用户等级
likenum:number;// 点赞数量
commentext:string;// 评论内容
islike:boolean;// 是否喜欢
levelIcon:Resource;// level等级
timeString:string;// 发布时间
time:number // 时间戳
constructor(avatar: ResourceStr, name: string, level: number, likenum: number, commentext: string,
islike: boolean,time:number) {
this.avatar=avatar
this.name=name
this.level=level
this.likenum=likenum
this.commentext=commentext
this.islike=islike
this.time=time
this.levelIcon=this.convertLevel(this.level)
this.timeString=this.convertTime(time)
}
//时间转换函数
convertTime(time:number){
const currentTimestamp = new Date().getTime();
// 转换为秒
const timeDifference = (currentTimestamp-time)/1000;
console.log(timeDifference.toString())
if(timeDifference<0 || timeDifference==0){
return '刚刚';
}else if(timeDifference<60){
return `${Math.floor(timeDifference)}秒前`;
}else if(timeDifference<3600){
return `${Math.floor(timeDifference/60)}分钟前`;
}else if(timeDifference<86400){
return `${Math.floor(timeDifference/3600)}小时前`;
}else if(timeDifference<604800){
return `${Math.floor(timeDifference/86400)}天前`;
}else if(timeDifference<2592000){
return `${Math.floor(timeDifference/604800)}周前`;
}else if(timeDifference<31536000){
return `${Math.floor(timeDifference/2592000)}个月前`;
}else{
return `${Math.floor(timeDifference/31536000)}年前`;
}
}
// 等级图片获取
convertLevel(Level:number){
const iconList=[
$r('app.media.lv1'),
$r('app.media.lv2'),
$r('app.media.lv3'),
$r('app.media.lv4'),
$r('app.media.lv5'),
$r('app.media.lv6')
]
return iconList[Level]
}
}
// 封装一个方法,创建假数据
export const createListRange=():CommentDate[]=>{
let result:CommentDate[]=new Array()
result=[
new CommentDate($r('app.media.tx_01'),'JohnYan',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'要是那天,我抓住你就好了',false,1705850201128),
new CommentDate($r('app.media.tx_03'),'cv工程师',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'故事不长,也不难讲,相识一场,爱而不得',false,1643800201128),
new CommentDate($r('app.media.tx_04'),'风行水上',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'后来啊,书没有读好,喜欢的人也没有在一起',false,1715850201128),
new CommentDate($r('app.media.tx_05'),'枫以',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'你根本忘不了一个认认真真爱过的人,你以为错过的是一个人,其实你错过的是一整个人生',false,1680850201128),
new CommentDate($r('app.media.tx_06'),'幼稚园里的幼稚鬼',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'有些伤痛即使已经痊愈,也会留下难以愈合的伤疤',false,1705850201128),
new CommentDate($r('app.media.tx_07'),'浮临子',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'不是所有的梦想都会成真,不是所有的伤痛都能愈合,但我们要有勇气继续前行',false,1625050201128),
new CommentDate($r('app.media.tx_08'),'╭⌒浅浅笑',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'孤独并不可怕,可怕的是渴望有人陪伴而得不到',false,1720850201128),
new CommentDate($r('app.media.tx_09'),'枕头说它不想醒',Math.floor(Math.random()*6),Math.floor(Math.random()*100),'可惜爱不是写诗 我只能欲言又止',false,1745050201128)
]
return result
}
Index部分
import InfoCom from '../components/InfoCom'
import BottomCom from '../components/BottomCom'
import InfoItem from '../components/InfoItem'
import {CommentDate,createListRange} from '../model/CommentDate'
@Entry
@Component
struct Index {
// 处理点赞时的方法
handlelike(index:number){
// 要有唯一标识
// AlertDialog.show({
// message:index.toString()
// })
// 父组件的方法,如果抽取出来,如果直接传递给子组件会有this指向问题,this通常直接指向调用者
// 需要用箭头函数包一层,保证this还是指向父组件
// 根据index进行判断
let itemData=this.commentList[index]
if(itemData.islike){
itemData.likenum-=1
}else{
itemData.likenum+=1
}
itemData.islike= !itemData.islike
// 对于复杂类型:状态对象,状态数组,只会对第一层数据进行监视变化
this.commentList.splice(index,1,itemData)
}
// 处理提交
handleSubmit(content:string){
// 将数据添加到数组最前面
const newItem:CommentDate=new CommentDate(
$r('app.media.tx_01'),'我',2,0,content,false,new Date().getTime()
)
this.commentList=[newItem,...this.commentList]
}
// 处理排序
handleSort(type:number){
if(type==0){
this.commentList.sort((a,b)=>{
return b.time-a.time
})
}else if(type==1){
this.commentList.sort((a,b)=>{
return b.likenum-a.likenum
})
}
}
// 初始化数据
@State commentList:CommentDate[]=createListRange()
// 生命周期函数,会自动执行
aboutToAppear(): void {
this.handleSort(0)
}
build() {
Column(){
//头部
InfoCom({
onSort:(type:number)=>{
this.handleSort(type)
}
})
//中间
List(){
ForEach(this.commentList,(item:CommentDate,index:number)=>{
ListItem(){
// 列表项组件
InfoItem({
index:index,
itemObj:item,
onLikeClick:(index:number)=>{
// 此处的this就是父组件
this.handlelike(index)
}
})
}
})
}
.width('100%')
.layoutWeight(1)
//底部
BottomCom({
onSubmitComment:(content:string)=>{
this.handleSubmit(content)
}
})
}
.width('100%')
.height('100%')
}
}