一多音乐专辑主页
介绍
本示例展示了音乐专辑主页。
- 头部返回栏: 因元素单一、位置固定在顶部,因此适合采用自适应拉伸,充分利用顶部区域。
- 专辑封面: 使用栅格组件控制占比,在小尺寸屏幕下封面图与歌单描述在同一行。
- 歌曲列表: 使用栅格组件控制宽度,在小尺寸屏幕下宽度为屏幕的100%,中尺寸屏幕下宽度为屏幕的50%,大尺寸屏幕下宽度为屏幕的75%。
- 播放器: 采用自适应拉伸,充分使用底部区域。
本示例使用一次开发多端部署中介绍的自适应布局能力和响应式布局能力进行多设备(或多窗口尺寸)适配,保证应用在不同设备或不同窗口尺寸下可以正常显示。
用到了媒体查询接口[@ohos.mediaquery]。
效果预览
本示例在预览器中的效果:
本示例在开发板上运行的效果:
使用说明:
1.启动应用,查看本应用在全屏状态下的效果。
2.在应用顶部,下滑出现窗口操作按钮。(建议通过外接鼠标操作,接入鼠标只需要将鼠标移动至顶部即可出现窗口)
3.点击悬浮图标,将应用悬浮在其他界面上显示。
4.拖动应用悬浮窗口的四个顶角,改变窗口尺寸,触发应用显示刷新。改变窗口尺寸的过程中,窗口尺寸可能超出屏幕尺寸,此时在屏幕中只能看到应用部分区域的显示。可以通过移动窗口位置,查看应用其它区域的显示。
开发前请熟悉鸿蒙开发指导文档 :gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md
点击或者复制转到。
工程目录
AppMarket/entry/src/main/ets/
|---model
| |---MediaData.ets // 主页用到的图片资源
| |---SongList.ets // 歌曲数据
| |---SongModule.ets // 事件监听函数模块
|---pages
| |---index.ets // 首页
|---common
| |---Content.ets // 内容组件
| |---Header.ets // 标题栏
| |---Player.ets // app模块(包含安装,展示图片,更多功能)
| |---PlayList.ets // 歌单列表
| |---PlayListCover.ets // 歌单封面
具体实现
本示例介绍如何使用自适应布局能力和响应式布局能力适配不同尺寸窗口,将页面分拆为4个部分。
标题栏
由于在不同断点下,标题栏始终只显示"返回按钮"、"歌单"以及"更多按钮",但"歌单"与"更多按钮"之间的间距不同。
通过栅格实现:将标题栏划分为"返回按钮及歌单"和"更多按钮"两部分,这两部分在不同断点下占据的列数不同。
歌单封面
通过栅格实现歌单封面,它由封面图片、歌单介绍及常用操作三部分组成这三部分的布局在md和lg断点下完全相同,但在sm断点下有较大差异,[源码参考]。
/*
* Copyright (c) 2022-2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { optionList } from '../model/SongList'
@Component
export default struct PlayListCover {
@State imgHeight: number = 0
@StorageProp('coverMargin') coverMargin: number = 0
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm'
@StorageProp('fontSize') fontSize: number = 0
@Builder
CoverImage() {
Stack({ alignContent: Alignment.BottomStart }) {
Image($r('app.media.pic_album'))
.width('100%')
.aspectRatio(1)
.borderRadius(8)
.onAreaChange((oldArea: Area, newArea: Area) => {
this.imgHeight = newArea.height as number
})
Text($r('app.string.collection_num'))
.letterSpacing(1)
.fontColor('#fff')
.fontSize(this.fontSize - 4)
.translate({ x: 10, y: '-100%' })
}
.width('100%')
.height('100%')
.aspectRatio(1)
}
@Builder
CoverIntroduction() {
Column() {
Text($r('app.string.list_name'))
.opacity(0.9)
.fontWeight(500)
.fontColor('#556B89')
.fontSize(this.fontSize + 2)
.margin({ bottom: 10 })
Text($r('app.string.playlist_Introduction'))
.opacity(0.6)
.width('100%')
.fontWeight(400)
.fontColor('#556B89')
.fontSize(this.fontSize - 2)
}
.width('100%')
.height(this.currentBreakpoint === 'sm' ? this.imgHeight : 70)
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.Center)
.padding({ left: this.currentBreakpoint === 'sm' ? 20 : 0 })
.margin({
top: this.currentBreakpoint === 'sm' ? 0 : 30,
bottom: this.currentBreakpoint === 'sm' ? 0 : 20
})
}
@Builder
CoverOptions() {
Row() {
ForEach(optionList, item => {
Column({ space: 4 }) {
Image(item.image).height(30).width(30)
Text(item.text)
.fontColor('#556B89')
.fontSize(this.fontSize - 1)
}
})
}
.width('100%')
.height(70)
.padding({
left: this.currentBreakpoint === 'sm' ? 20 : 0,
right: this.currentBreakpoint === 'sm' ? 20 : 0
})
.margin({
top: this.currentBreakpoint === 'sm' ? 15 : 0,
bottom: this.currentBreakpoint === 'sm' ? 15 : 0
})
.justifyContent(FlexAlign.SpaceBetween)
}
build() {
if (this.currentBreakpoint === 'sm') {
Column() {
GridRow() {
GridCol({ span: { sm: 4, md: 10 }, offset: { sm: 0, md: 1, lg: 1 } }) {
this.CoverImage()
}
GridCol({ span: { sm: 8, md: 10 }, offset: { sm: 0, md: 2, lg: 2 } }) {
this.CoverIntroduction()
}
GridCol({ span: { sm: 12, md: 10 }, offset: { sm: 0, md: 2, lg: 2 } }) {
this.CoverOptions()
}
}
.margin({ left: this.coverMargin, right: this.coverMargin })
.padding({ top: this.currentBreakpoint === 'sm' ? 50 : 70 })
}
} else {
Column() {
GridRow() {
GridCol({ span: { sm: 4, md: 10 }, offset: { sm: 0, md: 1, lg: 1 } }) {
this.CoverImage()
}
GridCol({ span: { sm: 8, md: 10 }, offset: { sm: 0, md: 2, lg: 2 } }) {
this.CoverIntroduction()
}
GridCol({ span: { sm: 12, md: 10 }, offset: { sm: 0, md: 2, lg: 2 } }) {
this.CoverOptions()
}.margin({
top: this.currentBreakpoint === 'sm' ? 15 : 0,
bottom: this.currentBreakpoint === 'sm' ? 15 : 0
})
}
.margin({ left: this.coverMargin, right: this.coverMargin })
.padding({ top: this.currentBreakpoint === 'sm' ? 50 : 70 })
}
.height('100%')
}
}
}
1、在sm断点下,封面图片和歌单介绍占满12列,常用操作此时会自动换行显示。
2、在lg和md断点下,封面图片,歌单和常用操作各占一行中显示。
歌单列表
通过List组件的lanes属性实现:在不同断点下,歌单列表的样式一致,但sm和md断点下是歌单列表是单列显示,lg断点下是双列显示,[源码参考]。
/*`HarmonyOS与OpenHarmony鸿蒙文档籽料:mau123789是v直接拿`
* Copyright (c) 2022-2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { songList } from '../model/SongList'
import MyDataSource from '../model/SongModule'
@Component
export default struct PlayList {
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm'
@StorageProp('fontSize') fontSize: number = 0
@Consume coverHeight: number
@Builder
PlayAll() {
Row() {
Image($r("app.media.ic_play_all"))
.height(23)
.width(23)
Text($r('app.string.play_all'))
.maxLines(1)
.padding({ left: 10 })
.fontColor('#000000')
.fontSize(this.fontSize)
Blank()
Image($r('app.media.ic_order_play'))
.width(24)
.height(24)
.margin({ right: 16 })
Image($r('app.media.ic_sort_list'))
.height(24)
.width(24)
}
.height(60)
.width('100%')
.padding({ left: 12, right: 12 })
}
@Builder
SongItem(title: string, label: Resource, singer: string) {
Row() {
Column() {
Text(title)
.fontColor('#000000')
.fontSize(this.fontSize)
.margin({ bottom: 4 })
Row() {
Image(label)
.width(16)
.height(16)
.margin({ right: 4 })
Text(singer)
.opacity(0.38)
.fontColor('#000000')
.fontSize(this.fontSize - 4)
}
}
.alignItems(HorizontalAlign.Start)
Blank()
Image($r('app.media.ic_list_more'))
.height(24)
.width(24)
}
.height(60)
.width('100%')
}
build() {
Column() {
this.PlayAll()
Scroll() {
List() {
LazyForEach(new MyDataSource(songList), item => {
ListItem() {
Column() {
this.SongItem(item.title, item.label, item.singer)
Divider()
.strokeWidth(0.5)
.color('#000')
.opacity(0.1)
}
.width('100%')
.height(50)
.padding({ left: 14, right: 14 })
}
}, item => item.id.toString())
}
.width('100%')
.lanes(this.currentBreakpoint === 'lg' ? 2 : 1)
}
.height('100%')
.flexGrow(1)
.flexShrink(1)
}
.width('100%')
.height('100%')
.borderRadius({ topLeft: 20, topRight: 20 })
.backgroundColor(Color.White)
.padding({ bottom: this.currentBreakpoint === 'sm' ? this.coverHeight : 0 })
}
}
播放控制栏
通过Blank组件实现拉伸能力:在不同断点下,播放控制栏显示的内容完全一致,唯一的区别是歌曲信息与播放控制按钮之间的间距有差异。
总体运行效果
通过在首页Column()中引用上述各组件后,可实现首页的组件整合渲染,即可完成整体页面开发,[源码参考]。
/*
* Copyright (c) 2022-2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { songList } from '../model/SongList'
import MyDataSource from '../model/SongModule'
@Component
export default struct PlayList {
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm'
@StorageProp('fontSize') fontSize: number = 0
@Consume coverHeight: number
@Builder
PlayAll() {
Row() {
Image($r("app.media.ic_play_all"))
.height(23)
.width(23)
Text($r('app.string.play_all'))
.maxLines(1)
.padding({ left: 10 })
.fontColor('#000000')
.fontSize(this.fontSize)
Blank()
Image($r('app.media.ic_order_play'))
.width(24)
.height(24)
.margin({ right: 16 })
Image($r('app.media.ic_sort_list'))
.height(24)
.width(24)
}
.height(60)
.width('100%')
.padding({ left: 12, right: 12 })
}
@Builder
SongItem(title: string, label: Resource, singer: string) {
Row() {
Column() {
Text(title)
.fontColor('#000000')
.fontSize(this.fontSize)
.margin({ bottom: 4 })
Row() {
Image(label)
.width(16)
.height(16)
.margin({ right: 4 })
Text(singer)
.opacity(0.38)
.fontColor('#000000')
.fontSize(this.fontSize - 4)
}
}
.alignItems(HorizontalAlign.Start)
Blank()
Image($r('app.media.ic_list_more'))
.height(24)
.width(24)
}
.height(60)
.width('100%')
}
build() {
Column() {
this.PlayAll()
Scroll() {
List() {
LazyForEach(new MyDataSource(songList), item => {
ListItem() {
Column() {
this.SongItem(item.title, item.label, item.singer)
Divider()
.strokeWidth(0.5)
.color('#000')
.opacity(0.1)
}
.width('100%')
.height(50)
.padding({ left: 14, right: 14 })
}
}, item => item.id.toString())
}
.width('100%')
.lanes(this.currentBreakpoint === 'lg' ? 2 : 1)
}
.height('100%')
.flexGrow(1)
.flexShrink(1)
}
.width('100%')
.height('100%')
.borderRadius({ topLeft: 20, topRight: 20 })
.backgroundColor(Color.White)
.padding({ bottom: this.currentBreakpoint === 'sm' ? this.coverHeight : 0 })
}
}