#DAYU200体验官# 起司播客App 原创 精华
@toc
主题
本帖使用Dayu200为开发板,展示一个在线播音App - 起司播客。
预览效果图



Dayu200的预览配置
为了大幅提高UI的开发效率,降低Dayu200的使用门槛,在开发过程中,强烈建议使用DevEco Studio 3.0 Beta3(OpenHarmony)的MatePadPro作为预览配置,并调整到竖屏模式,最终与Dayu200上的效果近似一致。
资源导入
本案例为了简单起见,文字与颜色直接写在代码中,仅图片资源需要导入,将全部所需图片拖到pages的新建img子目录中:

首页结构
使用默认的index.ets入口页作为启动页,分析页面的结构,可以一个Column,从上至下依次是导航栏、分类标题、分类卡片列表、筛选栏、播客作品列表。
导航栏
导航栏的左侧是一个按列布局的两行文字,右侧是一个头像:
Row {
        Column {
          Text('起司播客')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
          Text('爱情,生活,舒缓')
            .fontSize(15)
            .fontColor('#A3A1AF')
        }
	.alignItems(HorizontalAlign.Start)
        Blank()
        Image($r("app.media.profile"))
          .objectFit(ImageFit.Contain)
          .width(49)
          .height(49)
      }
      .width('100%')
      .padding({left:20,top:20,bottom:10,right:20})

分类标题
标题是一行文字,左对齐:
Row {
        Text('分类')
          .fontSize(20)
          .fontColor('#1E3354')
      }
      .width('100%')
      .padding({left:20,top:10,bottom:10,right:20})
分类卡片列表
定义分类列表的数据:
var cate = [
    {
      title: '音乐 & 娱乐',
      img: '/pages/img/cate1.png',
      total: '84',
      album: ['', '', '', '', '', '', ''],
    },
    {
      title: '生活 & 舒缓',
      img: '/pages/img/cate2.png',
      total: '96',
      album: ['', '', '', '', '', '', ''],
    },
    {
      title: '教育 & 学习',
      img: '/pages/img/cate3.png',
      total: '72',
      album: ['', '', '', '', '', '', ''],
    },
  ]
专辑数据:
var albums = [
    {
      title: 'Ngobam',
      cate: '音乐 & 娱乐',
      tag: 'pop',
      img: '/pages/img/item1.png',
      eps: '84',
      artist: 'Gofar Hilman'
    },
    {
      title: 'Semprod',
      cate: '生活 & 舒缓',
      tag: 'pop',
      img: '/pages/img/item2.png',
      eps: '44',
      artist: 'Kugo娱乐'
    },
    {
      title: 'Sruput Nendang',
      cate: '教育 & 学习',
      tag: 'pop',
      img: '/pages/img/item3.png',
      eps: '46',
      artist: 'Macro & Marlo'
    },
  ]
卡片本身由背景层和卡片文字组成。
背景层:
Column {
                Image(item.img)
                  .objectFit(ImageFit.Cover)
                  .borderRadius(20)
        }
              .width('100%')
              .height('100%')
卡片文字又上下排列的Column组成:
Column {
                Blank()
                Column {
                  Text(item.title)
                    .fontSize(16)
                    .opacity(0.9)
                    .fontWeight(FontWeight.Bold)
                  Text(item.total + "个播客   ")
                    .fontSize(15)
                    .opacity(0.4)
                }
                .borderRadius(20)
                .alignItems(HorizontalAlign.Start)
                .backgroundColor(Color.White)
                .opacity(0.6)
                .backdropBlur(8)
                .padding(20)
                .width('100%')
              }
              .alignItems(HorizontalAlign.Start)
              .width('100%')
              .height('100%')

将背景层和卡片文字组合起来,加上用户点击的互动:
Stack {
              Column {
                Image(item.img)
                  .objectFit(ImageFit.Cover)
                  .borderRadius(20)
              }
              .width('100%')
              .height('100%')
              Column {
                Blank()
                Column {
                  Text(item.title)
                    .fontSize(16)
                    .opacity(0.9)
                    .fontWeight(FontWeight.Bold)
                  Text(item.total + "个播客   ")
                    .fontSize(15)
                    .opacity(0.4)
                }
                .borderRadius(20)
                .alignItems(HorizontalAlign.Start)
                .backgroundColor(Color.White)
                .opacity(0.6)
                .backdropBlur(8)
                .padding(20)
                .width('100%')
              }
              .alignItems(HorizontalAlign.Start)
              .width('100%')
              .height('100%')
            }
		.padding( left: 20 )
            .width(224)
            .height(296)
            .onClick( {() =>
              router.push(
                uri: 'pages/channel',
                params: {
                  place: item
                }
              )
            })

使用横向滑动的List组件和ForEach对卡片数据进行循环:
List {
        ForEach(this.cate, item => {
          ListItem {
            Stack {
              Column {
                Image(item.img)
                  .objectFit(ImageFit.Cover)
                  .borderRadius(20)
              }
              .width('100%')
              .height('100%')
              Column {
                Blank()
                Column {
                  Text(item.title)
                    .fontSize(16)
                    .opacity(0.9)
                    .fontWeight(FontWeight.Bold)
                  Text(item.total + "个播客   ")
                    .fontSize(15)
                    .opacity(0.4)
                }
                .borderRadius(20)
                .alignItems(HorizontalAlign.Start)
                .backgroundColor(Color.White)
                .opacity(0.6)
                .backdropBlur(8)
                .padding(20)
                .width('100%')
              }
              .alignItems(HorizontalAlign.Start)
              .width('100%')
              .height('100%')
            }.padding( left: 20 )
            .width(224)
            .height(296)
            .onClick( () =>{
              router.push(
                uri: 'pages/channel',
                params: {
                  place: item
                }
              )
            })
          }
        })
      }
      .listDirection(Axis.Horizontal)
      .width('100%')
      .height(296)

筛选栏
定义筛选栏的数据数组和对应的索引数组:
var filters: string[] = [
    "流行", "最近", "音乐", "舒缓", "R&B"
  ]
  var filterIndices: number[] = this.filters.map{(_, index) => index}
定义用户选中的筛选栏状态变量:
@State var selected: number = 0
筛选栏是一系列的自定义按钮。单个按钮由图标加文字组成,第一个流行按钮有带火的图标。筛选栏中如果任何一个按钮被用户点击,则显示按钮的背景以及文字变粗体:
Button {
              Row {
                if (index == 0) {
                  Image($r("app.media.fire"))
                    .objectFit(ImageFit.Contain)
                    .width(16).height(16)
                    .margin({ right: 10 })
                }
                Text(this.filters[index])
                  .fontSize(17)
                  .fontWeight(this.selected == index ? FontWeight.Bold : FontWeight.Lighter)
                  .fontColor(this.selected == index ? '#413E50' : '#A3A1AF')
              }.padding(15)
            }
            .type(ButtonType.Normal)
            .backgroundColor(this.selected == index ? '#EDF0FC' : Color.White)
            .borderRadius(10)
            .height(50)
            .onClick( () =>{
              this.selected = index
            })

将筛选按钮数据使用横向滑动的List和ForEach进行循环渲染,即可得到按钮组:
List( {space: 10} ) {
        ForEach(this.filterIndices,  index =>{
          ListItem {
            Button {
              Row {
                if (index == 0) {
                  Image($r("app.media.fire"))
                    .objectFit(ImageFit.Contain)
                    .width(16).height(16)
                    .margin({ right: 10 })
                }
                Text(this.filters[index])
                  .fontSize(17)
                  .fontWeight(this.selected == index ? FontWeight.Bold : FontWeight.Lighter)
                  .fontColor(this.selected == index ? '#413E50' : '#A3A1AF')
              }.padding(15)
            }
            .type(ButtonType.Normal)
            .backgroundColor(this.selected == index ? '#EDF0FC' : Color.White)
            .borderRadius(10)
            .height(50)
            .onClick( () =>{
              this.selected = index
            })
          }
        })
      }
      .listDirection(Axis.Horizontal)
      .width('100%')
      .height(90)
      .padding(20)


音乐列表
单个的音乐条目由专辑图标、音乐名和作者、所属分类和专辑数目组合在一行之内。
圆角的专辑图标:
Image(item.img)
                  .objectFit(ImageFit.Cover)
                  .width(56)
                  .height(56)
                  .borderRadius(20)

音乐名和作者:
Row {
                  Text(item.title)
                    .fontSize(16)
                    .opacity(0.9)
                  Text("|")
                    .fontSize(15)
                    .opacity(0.05)
                    .padding(left:10,right:10)
                  Text(item.artist)
                    .fontSize(15)
                    .opacity(0.4)
                }
                .width('90%')

所属分类和专辑数目:
Row {
                  Text(item.cate)
                    .fontSize(16)
                    .opacity(0.4)
                  Text("·")
                    .fontSize(15)
                    .opacity(0.2)
                    .padding(left:5,right:5)
                  Text(item.eps + "个Ep")
                    .fontSize(15)
                    .opacity(0.4)
   }
                .width('90%')

将三个部分依次组合起来:
Row {
                Image(item.img)
                  .objectFit(ImageFit.Cover)
                  .width(56)
                  .height(56)
                  .borderRadius(20)
              Column {
                Blank()
                Row {
                  Text(item.title)
                    .fontSize(16)
                    .opacity(0.9)
                  Text("|")
                    .fontSize(15)
                    .opacity(0.05)
                    .padding(left:10,right:10)
                  Text(item.artist)
                    .fontSize(15)
                    .opacity(0.4)
                }
                .width('90%')
                Row {
                  Text(item.cate)
                    .fontSize(16)
                    .opacity(0.4)
                  Text("·")
                    .fontSize(15)
                    .opacity(0.2)
                    .padding(left:5,right:5)
                  Text(item.eps + "个Ep")
                    .fontSize(15)
                    .opacity(0.4)
                }
                .width('90%')
                Blank()
              }
              .width('70%')
              .height('100%')
            }
            .borderRadius(18)
            .backgroundColor('#EDF0FC')
            .margin({left:20,top:10,bottom: 10, right:20})
            .padding(10)
            .width('90%')
            .height(72)

有了单个音乐条目的实现,就可以使用一个纵向滑动的List和ForEach循环渲染,再加上用户点击跳转到音乐播放页的互动:
List {
        ForEach(this.albums, item =>{
          ListItem {
            Row {
              Image(item.img)
                  .objectFit(ImageFit.Cover)
                  .width(56)
                  .height(56)
                  .borderRadius(20)
              Column {
                Blank()
                Row {
                  Text(item.title)
                    .fontSize(16)
                    .opacity(0.9)
                  Text("|")
                    .fontSize(15)
                    .opacity(0.05)
                    .padding(left:10,right:10)
                  Text(item.artist)
                    .fontSize(15)
                    .opacity(0.4)
                }
                .width('90%')
                Row {
                  Text(item.cate)
                    .fontSize(16)
                    .opacity(0.4)
                  Text("·")
                    .fontSize(15)
                    .opacity(0.2)
                    .padding(left:5,right:5)
                  Text(item.eps + "个Ep")
                    .fontSize(15)
                    .opacity(0.4)
                }
                .width('90%')
                Blank()
              }
              .width('70%')
              .height('100%')
            }
            .borderRadius(18)
            .backgroundColor('#EDF0FC')
            .margin({left:20,top:10,bottom: 10, right:20})
            .padding(10)
            .width('90%')
            .height(72)
            .onClick( () =>{
              router.push(
                uri: 'pages/detail',
                params: {
                  place: item
                }
              )
            })
          }
        })
      }
      .width('100%')
      .height('35%')

完整代码和效果
将上述代码从上到下依次放入Column中,组成首页index.ets的完整代码:
import router from '@system.router';
@Entry
@Component
struct Index {
  @State selected: number = 0
  filters: string[] = [
    "流行", "最近", "音乐", "舒缓", "R&B"
  ]
  filterIndices: number[] = this.filters.map((_, index) => index)
  cate = [
    {
      title: '音乐 & 娱乐',
      img: '/pages/img/cate1.png',
      total: '84',
      album: ['', '', '', '', '', '', ''],
    },
    {
      title: '生活 & 舒缓',
      img: '/pages/img/cate2.png',
      total: '96',
      album: ['', '', '', '', '', '', ''],
    },
    {
      title: '教育 & 学习',
      img: '/pages/img/cate3.png',
      total: '72',
      album: ['', '', '', '', '', '', ''],
    },
  ]
  albums = [
    {
      title: 'Ngobam',
      cate: '音乐 & 娱乐',
      tag: 'pop',
      img: '/pages/img/item1.png',
      eps: '84',
      artist: 'Gofar Hilman'
    },
    {
      title: 'Semprod',
      cate: '生活 & 舒缓',
      tag: 'pop',
      img: '/pages/img/item2.png',
      eps: '44',
      artist: 'Kugo娱乐'
    },
    {
      title: 'Sruput Nendang',
      cate: '教育 & 学习',
      tag: 'pop',
      img: '/pages/img/item3.png',
      eps: '46',
      artist: 'Macro & Marlo'
    },
  ]
  build() {
    Column() {
      Row() {
        Column() {
          Text('起司播客')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
          Text('爱情,生活,舒缓')
            .fontSize(15)
            .fontColor('#A3A1AF')
        }.alignItems(HorizontalAlign.Start)
        Blank()
        Image($r("app.media.profile"))
          .objectFit(ImageFit.Contain)
          .width(49)
          .height(49)
      }
      .width('100%')
      .padding({left:20,top:20,bottom:10,right:20})
      Row() {
        Text('分类')
          .fontSize(20)
          .fontColor('#1E3354')
      }
      .width('100%')
      .padding({left:20,top:10,bottom:10,right:20})
      List() {
        ForEach(this.cate, item => {
          ListItem() {
            Stack() {
              Column() {
                Image(item.img)
                  .objectFit(ImageFit.Cover)
                  .borderRadius(20)
              }
              .width('100%')
              .height('100%')
              Column() {
                Blank()
                Column() {
                  Text(item.title)
                    .fontSize(16)
                    .opacity(0.9)
                    .fontWeight(FontWeight.Bold)
                  Text(item.total + "个播客   ").fontSize(15).opacity(0.4)
                }
                .borderRadius(20)
                .alignItems(HorizontalAlign.Start)
                .backgroundColor(Color.White)
                .opacity(0.6)
                .backdropBlur(8)
                .padding(20)
                .width('100%')
              }
              .alignItems(HorizontalAlign.Start)
              .width('100%')
              .height('100%')
            }.padding({ left: 20 })
            .width(224)
            .height(296)
            .onClick(() => {
              router.push({
                uri: 'pages/channel',
                params: {
                  place: item
                }
              })
            })
          }
        })
      }
      .listDirection(Axis.Horizontal)
      .width('100%')
      .height(296)
      List({ space: 10 }) {
        ForEach(this.filterIndices, index => {
          ListItem() {
            Button() {
              Row() {
                if (index == 0) {
                  Image($r("app.media.fire"))
                    .objectFit(ImageFit.Contain)
                    .width(16).height(16)
                    .margin({ right: 10 })
                }
                Text(this.filters[index])
                  .fontSize(17)
                  .fontWeight(this.selected == index ? FontWeight.Bold : FontWeight.Lighter)
                  .fontColor(this.selected == index ? '#413E50' : '#A3A1AF')
              }.padding(15)
            }
            .type(ButtonType.Normal)
            .backgroundColor(this.selected == index ? '#EDF0FC' : Color.White)
            .borderRadius(10)
            .height(50)
            .onClick(() => {
              this.selected = index
            })
          }
        })
      }
      .listDirection(Axis.Horizontal)
      .width('100%')
      .height(90)
      .padding(20)
      List() {
        ForEach(this.albums, item => {
          ListItem() {
            Row() {
              Image(item.img)
                .objectFit(ImageFit.Cover)
                .width(56).height(56)
                .borderRadius(20)
              Column() {
                Blank()
                Row() {
                  Text(item.title)
                    .fontSize(16)
                    .opacity(0.9)
                  Text("|")
                    .fontSize(15)
                    .opacity(0.05)
                    .padding({left:10,right:10})
                  Text(item.artist).fontSize(15).opacity(0.4)
                }
                .width('90%')
                Row() {
                  Text(item.cate)
                    .fontSize(16)
                    .opacity(0.4)
                  Text("·").fontSize(15).opacity(0.2).padding({left:5,right:5})
                  Text(item.eps + "个Ep").fontSize(15).opacity(0.4)
                }
                .width('90%')
                Blank()
              }
              .width('70%')
              .height('100%')
            }
            .borderRadius(18)
            .backgroundColor('#EDF0FC')
            .margin({left:20,top:10,bottom: 10, right:20})
            .padding(10)
            .width('90%')
            .height(72)
            .onClick(() => {
              router.push({
                uri: 'pages/detail',
                params: {
                  place: item
                }
              })
            })
          }
        })
      }
      .width('100%')
      .height('35%')
    }
    .width('100%')
  }
}

频道页
频道页是一个播客作者的详细介绍,结构的上半部分是播客信息区域(导航栏、头像、昵称、播客描述、作品数和昵称),下半部分是播客作品页。在pages下新建一个channel.ets源文件。
播客作品数据
播客个人信息:
var author = {
    desc: '听我用音乐娓娓道来',
    albums: 256,
    name: '奥珍妮博士',
    avatar: '/pages/img/uper_avatar1.png'
  }
播客作品信息:
var albums = [
    {
      title: '工作和生活之间',
      img: '/pages/img/ep1.png',
      duration: '56:38',
      eps: '56',
    },
    {
      title: '前进的力量',
      img: '/pages/img/ep2.png',
      duration: '28:01',
      eps: '35',
    }, {
      title: '让我惊喜的小猴',
      img: '/pages/img/ep3.png',
      duration: '1:40:20',
      eps: '42',
    }, {
      title: '我的爱情被疫情阻隔',
      img: '/pages/img/ep4.png',
      duration: '1:05:13',
      eps: '51',
    }, {
      title: '你为什么要振作起来?',
      img: '/pages/img/ep5.png',
      duration: '45:28',
      eps: '77',
    },
  ]
导航栏
导航栏左侧有一个返回按钮,中间是标题,右侧留空:
Row {
        Image($r("app.media.back"))
          .objectFit(ImageFit.Contain)
          .width(20)
          .height(20)
          .onClick(()=>{
            router.back()
          })
        Blank()
        Text('播客')
          .fontSize(20)
        Blank()
      }
	.width('100%')
      .padding( {left: 20, top: 20, bottom: 10, right: 20} )

播客个人信息区域
头像、昵称、播客描述、作品数和昵称都组合在一个Column中:
Column({space:15}) {
        Image(this.author.avatar)
          .objectFit(ImageFit.Contain)
          .width(84)
          .height(84)
          .borderRadius(21)
        Text(this.author.desc)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
        Row {
          Text(this.author.albums + "个Ep")
            .fontSize(15)
            .opacity(0.4)
          Text("|")
            .fontSize(15)
            .opacity(0.2)
            .padding( left: 5, right: 5 )
          Text(this.author.name)
            .fontSize(15)
            .fontColor('#A3A1AF')
        }
        Button {
          Row {
            Image($r("app.media.follow"))
              .objectFit(ImageFit.Contain)
              .width(24)
              .height(24)
              .margin({right:10})
            Text('关注')
              .fontSize(18)
              .fontColor(Color.White)
          }
        }
        .type(ButtonType.Normal)
        .backgroundColor('#2882F1')
        .width(200)
        .height(48)
        .borderRadius(8)
      }
	.height('30%')

播客作品列表
列表有一个标题区:
Row {
        Text('全部EP')
          .fontSize(20)
          .fontColor('#1E3354')
      }
      .width('100%')
      .padding({left:20})

单个播客作品项目,由作品封面、作品名、时长和作品数,组合在一个Column中:
Row {
              Image(item.img)
                .objectFit(ImageFit.Cover)
                .width(56).height(56)
                .borderRadius(20)
              Column {
                Blank()
                Row {
                  Text(item.title)
                    .fontSize(16)
                    .opacity(0.9)
                  Text("|")
                    .fontSize(15)
                    .opacity(0.05)
                    .padding( {left: 10, right: 10} )
                  Text(item.artist)
                    .fontSize(15)
                    .opacity(0.4)
                }
                .width('90%')
                Row {
                  Text(item.duration)
                    .fontSize(16)
                    .opacity(0.4)
                  Text("·")
                    .fontSize(15)
                    .opacity(0.2)
                    .padding( {left: 5, right: 5 })
                  Text(item.eps + "个Ep")
                    .fontSize(15)
                    .opacity(0.4)
                }
                .width('90%')
                Blank()
              }
              .width('70%')
              .height('100%')
            }
            .borderRadius(18)
            .backgroundColor('#EDF0FC')
            .margin( {left: 20, top: 10, bottom: 10, right: 20} )
            .padding(10)
            .width('90%')
            .height(72)
对于播客作品数据,使用List和ForEach循环渲染,加上用户互动跳转到播放页:
List {
        ForEach(this.albums, item =>{
          ListItem {
            Row {
              Image(item.img)
                .objectFit(ImageFit.Cover)
                .width(56).height(56)
                .borderRadius(20)
              Column {
                Blank()
                Row {
                  Text(item.title)
                    .fontSize(16)
                    .opacity(0.9)
                  Text("|")
                    .fontSize(15)
                    .opacity(0.05)
                    .padding( {left: 10, right: 10} )
                  Text(item.artist)
                    .fontSize(15)
                    .opacity(0.4)
                }
                .width('90%')
                Row {
                  Text(item.duration)
                    .fontSize(16)
                    .opacity(0.4)
                  Text("·")
                    .fontSize(15)
                    .opacity(0.2)
                    .padding( left: 5, right: 5 )
                  Text(item.eps + "个Ep")
                    .fontSize(15)
                    .opacity(0.4)
                }
                .width('90%')
                Blank()
              }
              .width('70%')
              .height('100%')
            }
            .borderRadius(18)
            .backgroundColor('#EDF0FC')
            .margin( {left: 20, top: 10, bottom: 10, right: 20} )
            .padding(10)
            .width('90%')
            .height(72)
            .onClick(() =>{
              router.push(
                uri: 'pages/detail',
                params: {
                  place: item
                }
              )
            })
          }
        })
      }
      .width('100%')
      .height('55%')

完整页面代码和预览效果
将以上各个子组件和数据组合起来,channel.ets源文件整个页面的代码:
import router from '@system.router';
@Entry
@Component
struct Channel {
  author = {
    desc: '听我用音乐娓娓道来',
    albums: 256,
    name: '奥珍妮博士',
    avatar: '/pages/img/uper_avatar1.png'
  }
  albums = [
    {
      title: '工作和生活之间',
      img: '/pages/img/ep1.png',
      duration: '56:38',
      eps: '56',
    },
    {
      title: '前进的力量',
      img: '/pages/img/ep2.png',
      duration: '28:01',
      eps: '35',
    }, {
      title: '让我惊喜的小猴',
      img: '/pages/img/ep3.png',
      duration: '1:40:20',
      eps: '42',
    }, {
      title: '我的爱情被疫情阻隔',
      img: '/pages/img/ep4.png',
      duration: '1:05:13',
      eps: '51',
    }, {
      title: '你为什么要振作起来?',
      img: '/pages/img/ep5.png',
      duration: '45:28',
      eps: '77',
    },
  ]
  build() {
    Column() {
      Row() {
        Image($r("app.media.back"))
          .objectFit(ImageFit.Contain)
          .width(20).height(20)
          .onClick(()=>{
            router.back()
          })
        Blank()
        Text('播客')
          .fontSize(20)
        Blank()
      }.width('100%')
      .padding({ left: 20, top: 20, bottom: 10, right: 20 })
      Column({space:15}) {
        Image(this.author.avatar)
          .objectFit(ImageFit.Contain)
          .width(84).height(84).borderRadius(21)
        Text(this.author.desc)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
        Row() {
          Text(this.author.albums + "个Ep").fontSize(15).opacity(0.4)
          Text("|").fontSize(15).opacity(0.2).padding({ left: 5, right: 5 })
          Text(this.author.name)
            .fontSize(15)
            .fontColor('#A3A1AF')
        }
        Button(){
          Row(){
            Image($r("app.media.follow"))
              .objectFit(ImageFit.Contain)
              .width(24).height(24).margin({right:10})
            Text('关注').fontSize(18).fontColor(Color.White)
          }
        }.type(ButtonType.Normal)
        .backgroundColor('#2882F1')
        .width(200).height(48).borderRadius(8)
      }.height('30%')
      Row() {
        Text('全部EP')
          .fontSize(20)
          .fontColor('#1E3354')
      }
      .width('100%')
      .padding({left:20})
      List() {
        ForEach(this.albums, item => {
          ListItem() {
            Row() {
              Image(item.img)
                .objectFit(ImageFit.Cover)
                .width(56).height(56)
                .borderRadius(20)
              Column() {
                Blank()
                Row() {
                  Text(item.title)
                    .fontSize(16)
                    .opacity(0.9)
                  Text("|")
                    .fontSize(15)
                    .opacity(0.05)
                    .padding({ left: 10, right: 10 })
                  Text(item.artist).fontSize(15).opacity(0.4)
                }
                .width('90%')
                Row() {
                  Text(item.duration)
                    .fontSize(16)
                    .opacity(0.4)
                  Text("·")
                    .fontSize(15)
                    .opacity(0.2)
                    .padding({ left: 5, right: 5 })
                  Text(item.eps + "个Ep").fontSize(15).opacity(0.4)
                }
                .width('90%')
                Blank()
              }
              .width('70%')
              .height('100%')
            }
            .borderRadius(18)
            .backgroundColor('#EDF0FC')
            .margin({ left: 20, top: 10, bottom: 10, right: 20 })
            .padding(10)
            .width('90%')
            .height(72)
            .onClick(() => {
              router.push({
                uri: 'pages/detail',
                params: {
                  place: item
                }
              })
            })
          }
        })
      }
      .width('100%')
      .height('55%')
    }
    .width('100%')
  }
}

播放页
播放页用于播放上一页的频道页的播客作品,包含导航栏、作品大图、作品名和作者、播放控制按钮。在pages目录下新建源文件detail.ets。
状态变量
播放的作品来自上一页,这里使用固定的数据。播放状态是用户可控的,使用@State变量,定义如下:
@State playing: boolean = false
  var music = {
    img: 'pages/img/ep1.png',
    author: '奥珍妮博士',
    title: '工作和生活之间',
    duration: '56:38',
  }
导航栏
导航栏左侧用于返回上一页,右侧可以将作品添加到播放列表:
Row {
        Image($r("app.media.back"))
          .objectFit(ImageFit.Contain)
          .width(20).height(20)
          .onClick({()=>
            router.back()
          })
        Blank()
        Text(' ')
          .fontSize(20)
        Blank()
        Image($r("app.media.playlist"))
          .objectFit(ImageFit.Contain)
          .width(20)
          .height(20)
      }
      .width('100%')
      .padding(30)

作品大图
作品大图带有圆角和阴影,将Image放入Column中再修饰属性即可:
Column {
        Image(this.music.img)
          .objectFit(ImageFit.Cover)
          .width(279)
          .height(326)
          .borderRadius(16)
      }
      .borderRadius(16)
      .shadow({radius: 50, color: '#cfcfcf',
        offsetX:5, offsetY: 15})
      .margin(30)

作品名和作者
作品名和作者按列布局,其中作者的文字略带不透明效果:
Column {
        Text(this.music.title)
          .fontSize(20)
        Text(this.music.author)
          .fontSize(15)
          .opacity(0.4)
      }
      .padding(30)

播放控制按钮
播放按钮可以根据作品的播放状态来切换图标,用户点击按钮可以在播放或暂停两种状态进行切换:
Row {
        Image(this.playing ? $r("app.media.pause") : $r("app.media.play"))
          .objectFit(ImageFit.Cover)
          .width(64)
          .height(64)
          .onClick(()=>{
          	this.playing = !this.playing
          })
      }
	.padding(30)


完整页面源码和预览效果
将以上子组件和数据组合起来,detail.ets源文件整个页面的代码:
import router from '@system.router';
@Entry
@Component
struct Detail {
  @State playing: boolean = false
  music = {
    img: 'pages/img/ep1.png',
    author: '奥珍妮博士',
    title: '工作和生活之间',
    duration: '56:38',
  }
  build() {
    Column() {
      Row() {
        Image($r("app.media.back"))
          .objectFit(ImageFit.Contain)
          .width(20).height(20)
          .onClick(()=>{
            router.back()
          })
        Blank()
        Text(' ')
          .fontSize(20)
        Blank()
        Image($r("app.media.playlist"))
          .objectFit(ImageFit.Contain)
          .width(20).height(20)
      }.width('100%')
      .padding(30)
      Column() {
        Image(this.music.img)
          .objectFit(ImageFit.Cover)
          .width(279).height(326)
          .borderRadius(16)
      }
      .borderRadius(16)
      .shadow({radius: 50, color: '#cfcfcf',
        offsetX:5, offsetY: 15})
      .margin(30)
      Column() {
        Text(this.music.title)
          .fontSize(20)
        Text(this.music.author)
          .fontSize(15).opacity(0.4)
      }.padding(30)
      Row() {
        Image(this.playing ? $r("app.media.pause") : $r("app.media.play"))
          .objectFit(ImageFit.Cover)
          .width(64).height(64)
        .onClick(()=>{
          this.playing = !this.playing
        })
      }.padding(30)
    }
    .width('100%')
    .height('100%')
  }
}

总结
Dayu200不仅适合设备开发,更适合App开发,配合最新的DevEco Studio 3.0,即使您手头没有设备,也可以进行相对完善的UI开发大部分工作。





















老师的创作热情最近真是高,佩服佩服
嘿嘿 必须的,赶一下
👍👍👍👍👍
得感叹一下,小波老师的审美真的很在线👍