1# 音乐专辑页
2
3
4本小节将以音乐专辑页为例,介绍如何使用自适应布局能力和响应式布局能力适配不同尺寸窗口。
5
6
7## 页面设计
8
9音乐专辑页的页面设计如下。
10
11  | sm | md | lg |
12| -------- | -------- | -------- |
13| ![zh-cn_image_0000001381013985](figures/zh-cn_image_0000001381013985.png) | ![zh-cn_image_0000001381133197](figures/zh-cn_image_0000001381133197.png) | ![zh-cn_image_0000001329813432](figures/zh-cn_image_0000001329813432.png) |
14
15同样观察音乐专辑的页面设计,不同断点下的页面设计有较多相似的地方。
16
17据此,我们可以将页面分拆为多个组成部分。
18
191. 标题栏
20
212. 歌单封面
22
233. 歌单列表
24
254. 播放控制栏
26
27  | sm | md | lg |
28| -------- | -------- | -------- |
29| ![zh-cn_image_0000001380933349](figures/zh-cn_image_0000001380933349.jpg) | ![zh-cn_image_0000001330133330](figures/zh-cn_image_0000001330133330.jpg) | ![zh-cn_image_0000001381013989](figures/zh-cn_image_0000001381013989.jpg) |
30
31
32## 标题栏
33
34不同断点下,标题栏始终只显示“返回按钮”、“歌单”以及“更多按钮”,但“歌单”与“更多按钮”之间的间距不同。由于不同断点下标题栏的背景色也有较大差异,因此无法使用拉伸能力实现,此场景更适合使用栅格实现。我们可以将标题栏划分为“返回按钮及歌单”和“更多按钮”两部分,这两部分在不同断点下占据的列数如下图所示。另外,还可以借助OnBreakpointChange事件,调整不同断点下这两部分的背景色。
35
36  |  | sm | md | lg |
37| -------- | -------- | -------- | -------- |
38| 效果图 | ![zh-cn_image_0000001329817776](figures/zh-cn_image_0000001329817776.png) | ![zh-cn_image_0000001381018337](figures/zh-cn_image_0000001381018337.png) | ![zh-cn_image_0000001381137517](figures/zh-cn_image_0000001381137517.jpg) |
39| 栅格布局图 | ![zh-cn_image_0000001330137692](figures/zh-cn_image_0000001330137692.png) | ![zh-cn_image_0000001329977740](figures/zh-cn_image_0000001329977740.png) | ![zh-cn_image_0000001329658136](figures/zh-cn_image_0000001329658136.png) |
40
41
42```ts
43@Component
44export struct Header {
45  @State moreBackgroundColor: Resource = $r('app.color.play_list_cover_background_color');
46  build() {
47    GridRow() {
48      GridCol({span: {sm:6, md: 6, lg:4}}) {
49        Row() {
50          Image($r('app.media.ic_back')).height('24vp').width('24vp')
51        }
52        .width('100%')
53        .height('50vp')
54        .justifyContent(FlexAlign.Start)
55        .alignItems(VerticalAlign.Center)
56        .padding({left:$r('app.float.default_margin')})
57        .backgroundColor($r('app.color.play_list_cover_background_color'))
58      }
59      GridCol({span: {sm:6, md: 6, lg:8}}) {
60        Row() {
61          Image($r('app.media.ic_add')).height('24vp').width('24vp')
62        }
63        .width('100%')
64        .height('50vp')
65        .justifyContent(FlexAlign.End)
66        .alignItems(VerticalAlign.Center)
67        .padding({right:$r('app.float.default_margin')})
68        .backgroundColor(this.moreBackgroundColor)
69      }
70    }.onBreakpointChange((currentBreakpoint) => {
71      // 调整不同断点下返回按钮及歌单的背景色
72      if (currentBreakpoint === 'sm') {
73        this.moreBackgroundColor = $r('app.color.play_list_cover_background_color');
74      } else {
75        this.moreBackgroundColor = $r('app.color.play_list_songs_background_color');
76      }
77    }).height('100%').width('100%')
78  }
79}
80```
81
82
83## 歌单封面
84
85歌单封面由封面图片、歌单介绍及常用操作三部分组成,这三部分的布局在md和lg断点下完全相同,但在sm断点下有较大差异。此场景同样可以用栅格实现。
86
87  |  | sm | md/lg |
88| -------- | -------- | -------- |
89| 效果图 | ![zh-cn_image_0000001329660244](figures/zh-cn_image_0000001329660244.jpg) | ![zh-cn_image_0000001381379829](figures/zh-cn_image_0000001381379829.png) |
90| 栅格布局图 | ![zh-cn_image_0000001381220165](figures/zh-cn_image_0000001381220165.png) | ![zh-cn_image_0000001381220169](figures/zh-cn_image_0000001381220169.png) |
91
92
93```ts
94import { optionList } from '../model/SongList'
95
96@Component
97export default struct PlayListCover {
98    @State imgHeight: number = 0;
99    @StorageProp('coverMargin') coverMargin: number = 0;
100    @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm';
101    @StorageProp('fontSize') fontSize: number = 0;
102
103    @Builder
104    CoverImage() {
105      Stack({ alignContent: Alignment.BottomStart }) {
106        Image($r('app.media.pic_album'))
107          .width('100%')
108          .aspectRatio(1)
109          .borderRadius(8)
110          .onAreaChange((oldArea: Area, newArea: Area) => {
111            this.imgHeight = newArea.height as number
112          })
113        Text($r('app.string.collection_num'))
114          .letterSpacing(1)
115          .fontColor('#fff')
116          .fontSize(this.fontSize - 4)
117          .translate({ x: 10, y: '-100%' })
118      }
119      .width('100%')
120      .height('100%')
121      .aspectRatio(1)
122    }
123
124    @Builder
125    CoverIntroduction() {
126      Column() {
127        Text($r('app.string.list_name'))
128          .opacity(0.9)
129          .fontWeight(500)
130          .fontColor('#556B89')
131          .fontSize(this.fontSize + 2)
132          .margin({ bottom: 10 })
133
134        Text($r('app.string.playlist_Introduction'))
135          .opacity(0.6)
136          .width('100%')
137          .fontWeight(400)
138          .fontColor('#556B89')
139          .fontSize(this.fontSize - 2)
140      }
141      .width('100%')
142      .height(this.currentBreakpoint === 'sm' ? this.imgHeight : 70)
143      .alignItems(HorizontalAlign.Start)
144      .justifyContent(FlexAlign.Center)
145      .padding({ left: this.currentBreakpoint === 'sm' ? 20 : 0 })
146      .margin({
147        top: this.currentBreakpoint === 'sm' ? 0 : 30,
148        bottom: this.currentBreakpoint === 'sm' ? 0 : 20
149      })
150    }
151
152    @Builder
153    CoverOptions() {
154      Row() {
155        ForEach(optionList, item => {
156          Column({ space: 4 }) {
157            Image(item.image).height(30).width(30)
158            Text(item.text)
159              .fontColor('#556B89')
160              .fontSize(this.fontSize - 1)
161          }
162        })
163      }
164      .width('100%')
165      .height(70)
166      .padding({
167        left: this.currentBreakpoint === 'sm' ? 20 : 0,
168        right: this.currentBreakpoint === 'sm' ? 20 : 0
169      })
170      .margin({
171        top: this.currentBreakpoint === 'sm' ? 15 : 0,
172        bottom: this.currentBreakpoint === 'sm' ? 15 : 0
173      })
174      .justifyContent(FlexAlign.SpaceBetween)
175    }
176  build() {
177    Column() {
178      // 借助栅格组件实现总体布局
179      GridRow() {
180        // 歌单图片
181        GridCol({ span: { sm: 4, md: 10 }, offset: { sm: 0, md: 1, lg: 1 } }) {
182          this.CoverImage()
183        }
184        // 歌单介绍
185        GridCol({ span: { sm: 8, md: 10 }, offset: { sm: 0, md: 2, lg: 2 } }) {
186          this.CoverIntroduction()
187        }
188        // 歌单操作
189        GridCol({ span: { sm: 12, md: 10 }, offset: { sm: 0, md: 2, lg: 2 } }) {
190          this.CoverOptions()
191        }.margin({
192          top: this.currentBreakpoint === 'sm' ? 15 : 0,
193          bottom: this.currentBreakpoint === 'sm' ? 15 : 0
194        })
195      }
196      .margin({ left: this.coverMargin, right: this.coverMargin })
197    }
198    .height('100%')
199    .padding({ top: this.currentBreakpoint === 'sm' ? 50 : 70 })
200  }
201}
202```
203
204
205## 歌单列表
206
207不同断点下,歌单列表的样式基本一致,但sm和md断点下是歌单列表是单列显示,lg断点下是双列显示。可以通过[List组件](../../reference/apis-arkui/arkui-ts/ts-container-list.md)的lanes属性实现这一效果。
208
209
210```ts
211import { songList } from '../model/SongList';
212import MyDataSource from '../model/SongModule'
213
214@Component
215export default struct PlayList {
216  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm';
217  @StorageProp('fontSize') fontSize: number = 0;
218  @Consume coverHeight: number;
219   @Builder
220  PlayAll() {
221    Row() {
222      Image($r("app.media.ic_play_all"))
223        .height(23)
224        .width(23)
225      Text($r('app.string.play_all'))
226        .maxLines(1)
227        .padding({ left: 10 })
228        .fontColor('#000000')
229        .fontSize(this.fontSize)
230      Blank()
231      Image($r('app.media.ic_order_play'))
232        .width(24)
233        .height(24)
234        .margin({ right: 16 })
235      Image($r('app.media.ic_sort_list'))
236        .height(24)
237        .width(24)
238    }
239    .height(60)
240    .width('100%')
241    .padding({ left: 12, right: 12 })
242  }
243
244  @Builder
245  SongItem(title: string, label: Resource, singer: string) {
246    Row() {
247      Column() {
248        Text(title)
249          .fontColor('#000000')
250          .fontSize(this.fontSize)
251          .margin({ bottom: 4 })
252        Row() {
253          Image(label)
254            .width(16)
255            .height(16)
256            .margin({ right: 4 })
257          Text(singer)
258            .opacity(0.38)
259            .fontColor('#000000')
260            .fontSize(this.fontSize - 4)
261        }
262      }
263      .alignItems(HorizontalAlign.Start)
264
265      Blank()
266      Image($r('app.media.ic_list_more'))
267        .height(24)
268        .width(24)
269    }
270    .height(60)
271    .width('100%')
272  }
273  build() {
274    Column() {
275      this.PlayAll()
276      Scroll() {
277        List() {
278          LazyForEach(new MyDataSource(songList), item => {
279            ListItem() {
280              this.SongItem(item.title, item.label, item.singer)
281            }
282          })
283        }
284        .width('100%')
285        .height('100%')
286        // 配置不同断点下歌单列表的列数
287        .lanes(this.currentBreakpoint === 'lg' ? 2 : 1)
288      }
289      .backgroundColor('#fff')
290      .margin({ top: 50, bottom: this.currentBreakpoint === 'sm' ? this.coverHeight : 0 })
291    }
292    .padding({top: 50,bottom: 48})
293  }
294}
295```
296
297
298## 播放控制栏
299
300在不同断点下,播放控制栏显示的内容完全一致,唯一的区别是歌曲信息与播放控制按钮之间的间距有差异,这是典型的拉伸能力的使用场景。
301
302
303```ts
304@Component
305export default struct Player {
306  @StorageProp('fontSize') fontSize: number = 0;
307  build() {
308    Row() {
309      Image($r('app.media.pic_album')).height(32).width(32).margin({right: 12})
310      Column() {
311        Text($r('app.string.song_name'))
312          .fontColor('#000000')
313          .fontSize(this.fontSize - 1)
314        Row() {
315          Image($r('app.media.ic_vip'))
316            .height(16)
317            .width(16)
318            .margin({ right: 4 })
319          Text($r('app.string.singer'))
320            .fontColor('#000000')
321            .fontSize(this.fontSize - 4)
322            .opacity(0.38)
323        }
324      }
325      .alignItems(HorizontalAlign.Start)
326      // 通过Blank组件实现拉伸能力
327      Blank()
328      Image($r('app.media.icon_play')).height(26).width(26).margin({right: 16})
329      Image($r('app.media.ic_next')).height(24).width(24).margin({right: 16})
330      Image($r('app.media.ic_Music_list')).height(24).width(24)
331    }
332    .width('100%')
333    .height(48)
334    .backgroundColor('#D8D8D8')
335    .alignItems(VerticalAlign.Center)
336    .padding({left: 16, right: 16})
337  }
338}
339```
340
341
342## 运行效果
343
344将页面中的四部分组合在一起,即可显示完整的页面。
345
346其中歌单封面和歌单列表这两部分的相对位置,在sm断点下是上下排布,在md和lg断点下是左右排布,也可以用栅格来实现目标效果。
347
348  |  | sm | md | lg |
349| -------- | -------- | -------- | -------- |
350| 效果图 | ![zh-cn_image_0000001381026609](figures/zh-cn_image_0000001381026609.jpg) | ![zh-cn_image_0000001381145789](figures/zh-cn_image_0000001381145789.jpg) | ![zh-cn_image_0000001329666380](figures/zh-cn_image_0000001329666380.jpg) |
351| 栅格布局图 | ![zh-cn_image_0000001330145976](figures/zh-cn_image_0000001330145976.png) | ![zh-cn_image_0000001381385985](figures/zh-cn_image_0000001381385985.png) | ![zh-cn_image_0000001381226321](figures/zh-cn_image_0000001381226321.png) |
352
353
354```ts
355import PlayListCover from '../common/PlayListCover';
356import PlayList from '../common/PlayList';
357
358@Component
359export default struct Content {
360  // ...
361  build() {
362    GridRow() {
363      // 歌单封面
364      GridCol({ span: { xs: 12, sm: 12, md: 6, lg: 4 } }) {
365        PlayListCover()
366      }
367      // 歌单列表
368      GridCol({ span: { xs: 12, sm: 12, md: 6, lg: 8 } }) {
369        PlayList()
370      }
371    }
372    .height('100%')
373  }
374}
375```
376
377最后将页面各部分组合在一起即可。
378
379
380```ts
381
382import Header from '../common/Header';
383import Player from '../common/Player';
384import Content from '../common/Content';
385
386@Entry
387@Component
388struct Index {
389  build() {
390    Column() {
391      // 标题栏
392      Header()
393      // 歌单
394      Content()
395      // 播放控制栏
396      Player()
397    }.width('100%').height('100%')
398  }
399}
400```
401
402音乐专辑页面的运行效果如下所示。
403
404  | sm | md | lg |
405| -------- | -------- | -------- |
406| ![MusicAlbum_sm_running](figures/MusicAlbum_sm_running.png) | ![MusicAlbum_md_running](figures/MusicAlbum_md_running.png) | ![MusicAlbum_lg_running](figures/MusicAlbum_lg_running.png) |
407
408## 相关实例
409
410针对音乐专辑应用,有以下相关实例可供参考:
411
412- [典型页面场景:音乐专辑页(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/SuperFeature/MultiDeviceAppDev/MusicAlbum)
413
414