评论区
效果展示
说在前面的
大家在听音乐或者晒短视频的时候,是不是经常会习惯性地点开评论区看看呢,下面是我两个常用软件中的评论区,今天我们试着来复刻一个看起来差不多一个评论区。
iconfont字体图标使用
总体分析
通过一些评论区的案例,我们发现大部分评论区都有着相似的布局和功能
-
首先是一个固定的头部组件如下图
-
接着是可以滑动的评论区正文部分,如下图
-
最后位于屏幕最下侧的一个组件,一般都具有评论功能和选择emoji的入口,如下图所示
部分分析
通过总体分析,我们知道一个评论区主要由三部分组成,分别是:头部组件、滑动组件、尾部组件,接下来我们具体组件具体分析一下。
头部组件
头部组件实现思路
一眼看过去就知道需要使用到Row容器,然后是一个text组件用来声明本页主题,接下来就是两中不同的排序方式(热度顺序和时间顺序)
头部组件实现代码
@Extend(Button)
//最新&最热两个按钮的代码
function fancyButton (isOn: boolean){
.width(46)
.height(32)
.fontSize(12)
.padding({left:5,right:5})
.backgroundColor(isOn ? '#fff' : '#F7F8FA')
.border({width:1,color:'#e4e5e6'})
.fontColor(isOn ? '#2f2e33' : '#8e9298')
}
@Component
export struct InfoTop{
@State isOn:boolean = true
onSort = (type:number)=>{}
build() {
Row(){
Text('全部评论')
.padding(10)
.fontSize(26)
.fontWeight(FontWeight.Bold)
Blank()
Row(){
Button('最新',{stateEffect:false})
.fancyButton(this.isOn)
.onClick(()=>{
this.isOn = true;
//onSort参数为0时表示按照时间顺序排列
this.onSort(0);
})
Button('最热',{stateEffect:false})
.fancyButton(!this.isOn)
.onClick(()=>{
this.isOn = false;
//onSort参数为1时表示按照热度顺序排列
this.onSort(1);
})
}
.margin(10)
.border({width:1,color:'#e4e5e6'})
.height(32)
.width(92)
.borderRadius(16)
.backgroundColor('#F7F8FA')
}.backgroundColor(Color.White)
.justifyContent(FlexAlign.Center)
.width('100%')
.height(60)
}
}
滑动组件
滑动组件实现思路
一个评论页面的主要组成部分就是这个可滑动的部分,所以我们想到这里采用List容器,并考虑到数据的量比较大,采用ForEach来进行滑动组件中不同评论的渲染。
再进一步看到评论的组成部分:用户头像、用户昵称、用户等级、评论内容、评论时间以及对该评论的点赞功能和点赞数量的显示,效果图如下:
滑动组件代码部分(包括点赞功能的实现)
- 入口组件中使用List容器和ForEach进行布局和渲染
//中部
List(){
ForEach(this.InfoItem1,(item:CommentData,index:number)=>{
ListItem(){
//列表项组件
InfoItem({itemObj:item});
}
.padding(10)
})
}
.width('100%')
.layoutWeight(1)//让容器高度自适应
.backgroundColor(Color.White)
.listDirection(Axis.Vertical)
.scrollBar(BarState.Auto)
.lanes(1,5)
.alignListItem(ListItemAlign.Center)
- 单个评论的组成部分代码
//此处为引入数据源
import {CommentData} from '../model/CommentData'
@Component
export struct InfoItem{
//这个boolean类型判断是否点过赞
@State stage_praise:boolean = false;
//实现父子双向传递
@ObjectLink itemObj :CommentData
build() {
Column(){
Row(){
//头像、昵称、等级
Image(this.itemObj.avater)
.width(30)
.aspectRatio(1)
.borderRadius(15)
Text(this.itemObj.name)
.fontSize(16)
.fontColor(Color.Gray)
.margin(10)
Image(this.itemObj.levelIcon)
.width(30)
.aspectRatio(1)
}.width('100%')
.justifyContent(FlexAlign.Start)
.margin(10)
//评论内容
Row(){
Text(this.itemObj.commentText)
.fontSize(16)
.fontColor(Color.Black)
}.width('80%')
//日期、点赞互动
Row(){
Text(this.itemObj.timeString)
.fontSize(12)
.fontColor(Color.Gray)
.margin(10)
Blank()
if(this.itemObj.isLike) {
Image($r('app.media.selected'))
.width(14)
.aspectRatio(1)
.onClick(()=>{
this.itemObj.likeNum-=1
this.itemObj.isLike = false;
})
}
else{
Image($r('app.media.unselect'))
.width(14)
.aspectRatio(1)
.onClick(()=>{
this.itemObj.likeNum+=1
this.itemObj.isLike = true;
})
}
Text(this.itemObj.likeNum.toString())
.fontColor(this.itemObj.isLike ? Color.Red : Color.Gray)
.margin(5)
.fontSize(14)
.onClick(()=>{
this.itemObj.isLike = !this.itemObj.isLike;
})
}.width('95%')
}.width('100%')
}
}
尾部组件
尾部组件实现思路
感觉和头部组件很相似,所以都是Row容器,接着需要使用TextInput组件来实现输入文字功能,最后插入一个emoji表情文字图片即可。
尾部组件代码
@Component
export struct InfoBottom{
@State txt:string = '123'
onSubmitComment = (content:string) =>{}
build() {
Row(){
Row() {
//此处将iconfont中的图标设置成了文字格式
Text('\ue607')
.fontFamily('myfont')
.fontSize(22)
.aspectRatio(1)
.fontColor(Color.Gray)
.margin({left:10})
//输入框组件
TextInput({placeholder:'写评论...', text:$$this.txt})
.layoutWeight(1)
.height(40)
.backgroundColor(Color.Transparent)
.borderRadius(20)
.onSubmit(()=>{
//这里不能直接添加,需要调用父组件传递过来的方法
this.onSubmitComment(this.txt)
})
}.backgroundColor('#f5f6f5')
.height(30)
.layoutWeight(1)
.borderRadius(15)
.margin(20)
Text('\ue616')
.fontFamily('myfont')
.fontSize(26)
.fontColor(Color.Gray)
.margin(20)
.onClick(()=>{
AlertDialog.show({message:'暂无emoji可用'})
})
/* Text('\ue666')
.fontFamily('myfont')
.fontSize(26)
.fontColor(Color.Gray)
.margin(20)*/
}
.width('100%')
.height(60)
.backgroundColor(Color.White)
}
}
评论功能的实现
//处理提交
handleSubmit(content:string){
//直接new一个CommentData类然后传参
const newItem:CommentData = new CommentData(
$r("app.media.avater1"),'我',3,0,content,false,new Date().getTime()
)
//最后将自己的评论加到数据的前面
this.InfoItem1 = [newItem,...this.InfoItem1]
}
总体代码
import {InfoTop} from '../constants/InfoTop'
import {InfoItem} from '../constants/InfoItem'
import font from '@ohos.font';
import { InfoBottom } from '../constants/InfoBottom';
import {CommentData,createListRange} from '../model/CommentData'
@Entry
@Component
struct Index {
@State InfoItem1:CommentData[] = createListRange()
//处理提交
handleSubmit(content:string){
const newItem:CommentData = new CommentData(
$r("app.media.avater1"),'我',3,0,content,false,new Date().getTime()
)
this.InfoItem1 = [newItem,...this.InfoItem1]
}
//处理排序 0 最新 time时间戳 1 最热 likenum点赞数
handleSort(type:number){
if(type===0){
this.InfoItem1.sort((a,b)=>{
return b.time - a.time
})
}
else{
this.InfoItem1.sort((a,b)=>{
return b.likeNum - a.likeNum;
})
}
}
//一加载Index入口页面,就进行注册
aboutToAppear(): void {
//注册字体
font.registerFont({
familyName:'myfont',
familySrc:'/fonts/iconfont.ttf'
})
//this.handleSort(0)
}
build() {
Column() {
//头部
InfoTop({
onSort:(type:number)=>{
this.handleSort(type)
}
})
//中部
List(){
ForEach(this.InfoItem1,(item:CommentData,index:number)=>{
ListItem(){
//列表项组件
InfoItem({itemObj:item});
}
.padding(10)
})
}
.width('100%')
.layoutWeight(1)//让容器高度自适应
.backgroundColor(Color.White)
.listDirection(Axis.Vertical)
.scrollBar(BarState.Auto)
.lanes(1,5)
.alignListItem(ListItemAlign.Center)
//底部
InfoBottom({
//监听
onSubmitComment:(content:string)=>{
this.handleSubmit(content);
}
})
}
}
}
@Extend(Button)
function fancyButton (isOn: boolean){
.width(46)
.height(32)
.fontSize(12)
.padding({left:5,right:5})
.backgroundColor(isOn ? '#fff' : '#F7F8FA')
.border({width:1,color:'#e4e5e6'})
.fontColor(isOn ? '#2f2e33' : '#8e9298')
}
@Component
export struct InfoTop{
@State isOn:boolean = true
onSort = (type:number)=>{}
build() {
Row(){
Text('全部评论')
.padding(10)
.fontSize(26)
.fontWeight(FontWeight.Bold)
Blank()
Row(){
Button('最新',{stateEffect:false})
.fancyButton(this.isOn)
.onClick(()=>{
this.isOn = true;
this.onSort(0);
})
Button('最热',{stateEffect:false})
.fancyButton(!this.isOn)
.onClick(()=>{
this.isOn = false;
this.onSort(1);
})
}
.margin(10)
.border({width:1,color:'#e4e5e6'})
.height(32)
.width(92)
.borderRadius(16)
.backgroundColor('#F7F8FA')
}.backgroundColor(Color.White)
.justifyContent(FlexAlign.Center)
.width('100%')
.height(60)
}
}
import {CommentData} from '../model/CommentData'
@Component
export struct InfoItem{
@State stage_praise:boolean = false;
@ObjectLink itemObj :CommentData
build() {
Column(){
Row(){
//头像、昵称、等级
Image(this.itemObj.avater)
.width(30)
.aspectRatio(1)
.borderRadius(15)
Text(this.itemObj.name)
.fontSize(16)
.fontColor(Color.Gray)
.margin(10)
Image(this.itemObj.levelIcon)
.width(30)
.aspectRatio(1)
}.width('100%')
.justifyContent(FlexAlign.Start)
.margin(10)
//评论内容
Row(){
Text(this.itemObj.commentText)
.fontSize(16)
.fontColor(Color.Black)
}.width('80%')
//日期、点赞互动
Row(){
Text(this.itemObj.timeString)
.fontSize(12)
.fontColor(Color.Gray)
.margin(10)
Blank()
if(this.itemObj.isLike) {
Image($r('app.media.selected'))
.width(14)
.aspectRatio(1)
.onClick(()=>{
this.itemObj.likeNum-=1
this.itemObj.isLike = false;
})
}
else{
Image($r('app.media.unselect'))
.width(14)
.aspectRatio(1)
.onClick(()=>{
this.itemObj.likeNum+=1
this.itemObj.isLike = true;
})
}
Text(this.itemObj.likeNum.toString())
.fontColor(this.itemObj.isLike ? Color.Red : Color.Gray)
.margin(5)
.fontSize(14)
.onClick(()=>{
this.itemObj.isLike = !this.itemObj.isLike;
})
}.width('95%')
}.width('100%')
}
}
@Component
export struct InfoBottom{
@State txt:string = '123'
onSubmitComment = (content:string) =>{}
build() {
Row(){
Row() {
Text('\ue607')
.fontFamily('myfont')
.fontSize(22)
.aspectRatio(1)
.fontColor(Color.Gray)
.margin({left:10})
TextInput({placeholder:'写评论...', text:$$this.txt})
.layoutWeight(1)
.height(40)
.backgroundColor(Color.Transparent)
.borderRadius(20)
.onSubmit(()=>{
//这里不能直接添加,需要调用父组件传递过来的方法
this.onSubmitComment(this.txt)
})
}.backgroundColor('#f5f6f5')
.height(30)
.layoutWeight(1)
.borderRadius(15)
.margin(20)
Text('\ue616')
.fontFamily('myfont')
.fontSize(26)
.fontColor(Color.Gray)
.margin(20)
.onClick(()=>{
AlertDialog.show({message:'暂无emoji可用'})
})
/* Text('\ue666')
.fontFamily('myfont')
.fontSize(26)
.fontColor(Color.Gray)
.margin(20)*/
}
.width('100%')
.height(60)
.backgroundColor(Color.White)
}
}
//准备评论的数据类
@Observed export class CommentData{
avater:Resource//头像
name:string //昵称
level:number //用户等级
likeNum:number //点赞数量
commentText:string //评论内容
isLike:boolean //是否喜欢
levelIcon:Resource //level等级
timeString:string //发布时间-基于时间戳处理后,展示给用户看的属性
time:number //时间戳
constructor(avater:Resource,name:string,level:number,likeNum:number,commentText:string,isLike:boolean,time:number) {
this.avater = avater
this.name = name
this.level = level
this.likeNum = likeNum
this.commentText = commentText
this.isLike = isLike
this.levelIcon = this.convertLevel(this.level)
this.timeString = this.convertTime(time)
this.time = time
}
convertTime(timestamp:number){
const currentTimestamp = new Date().getTime();
const timeDifference = (currentTimestamp - timestamp)/1000 //转换为秒
if(timeDifference<0||timeDifference==0){
return '刚刚'
}
else if(timeDifference<60){
return `${Math.floor(timeDifference)}秒前`
}
else if(timeDifference<3600){
return `${Math.floor(timeDifference)}分钟前`
}
else if(timeDifference<86400){
return `${Math.floor(timeDifference)}小时前`
}
else if(timeDifference<604800){
return `${Math.floor(timeDifference)}天前`
}
else if(timeDifference<2592000){
return `${Math.floor(timeDifference)}周前`
}
else if(timeDifference<31536000){
return `${Math.floor(timeDifference)}个月前`
}
else{
return `${Math.floor(timeDifference/31536000)}年前`
}
}
convertLevel(level:number){
const iconLevel = [
$r('app.media.LV1'),
$r('app.media.LV2'),
$r('app.media.LV3'),
$r('app.media.LV4'),
$r('app.media.LV5')
]
return iconLevel[level-1]
}
}
//封装一个方法,创建假数据
export const createListRange = ():CommentData[]=>{
let result :CommentData[] = new Array()
result = [
new CommentData($r("app.media.avater1"),"皖皖",Math.floor(Math.random()*6),Math.floor(Math.random()*111),'岳云鹏说的一句话很对:“不要故意不告诉老人回家而给他们一个惊喜,要提前告诉他们,提前一个月告诉,老人们就多提前高兴一个月',false,1645820201123),
new CommentData($r("app.media.avater2"),"miraculous",Math.floor(Math.random()*6),Math.floor(Math.random()*111),'我高考了在2023年',false,1645820201123),
new CommentData($r("app.media.avater3"),"雪山飞孤",Math.floor(Math.random()*6),Math.floor(Math.random()*111),'23年一年干完的事真的非常仓促',false,1645820201123),
new CommentData($r("app.media.avater4"),"阿婷",Math.floor(Math.random()*6),Math.floor(Math.random()*111),'如果爱忘了泪不想落下,那些幸福啊,让她替我到达,相爱过如果是爱的够久分开越疼把,想念你的脸颊你的发我不害怕',false,1745820201123),
new CommentData($r("app.media.avater5"),"小狼仔",Math.floor(Math.random()*6),Math.floor(Math.random()*111),'学习使我快乐',false,1645820201123),
new CommentData($r("app.media.avater6"),"小卢",Math.floor(Math.random()*6),Math.floor(Math.random()*111),'遗忘遗忘快遗忘~',false,1845820201123),
new CommentData($r("app.media.avater7"),"miraculous11",Math.floor(Math.random()*6),Math.floor(Math.random()*111),'我学着一个人一整天都不失落',false,1245820201123),
]
return result
}