[Angular实战网易云]——14、底部信息与功能(二)

本文详细介绍了在Angular应用中实现网易云音乐底部信息与功能,包括播放面板的歌曲列表展示、列表滚动功能的实现。通过父子组件通信,处理歌曲选择和切歌效果。同时,利用BetterScroll插件添加滚动条并处理歌曲高亮,解决随机播放模式下的歌曲匹配问题。
摘要由CSDN通过智能技术生成

播放面板

底部除了播放歌曲时的显示信息,还要显示当前的播放列表,对于播放列表选择提取成单独的子组件,这也涉及到父子组件的通信。


歌曲列表

  • wy-player.component.html
   <!--歌曲面板图标-->
   <p class="open" (click)="toggleListPanel()">
       <span></span>
   </p>
   <!-- 播放面板 -->
   <app-wy-player-panel 
     [songList]="songList"
     [currentSong]="currentSong"
     [currentIndex]="currentIndex" 
     [show]="showPanel"
     (onClose)="showPanel = false"
     (onChangeSong)="onChangeSong($event)"
    ></app-wy-player-panel>

当打开该面板的时候,左侧需要显示当前的歌曲列表以及当前的歌曲,所以会像子组件传songList 与 currentSong,而在当前歌曲前要显示小图标,所以将当前歌曲的索引也传给子组件。

  • wy-player.component.ts
    songList: Song[]; // 歌曲列表
    currentIndex: number; // 当前歌曲下标
    currentSong: Song; // 当前歌曲
    showVolumePanel = false; // 是否显示音量面板
    showPanel = false; // 是否显示列表面板


	 // 控制音量面板
    toggleVolPanel () {
        this.togglePanel('showVolumePanel')
    }

    // 控制列表面板
    toggleListPanel () {
        this.togglePanel('showPanel')
    }

	    // 面板回调
    togglePanel (type: string) {
        this[type] = !this[type];
        if (this.showVolumePanel || this.showPanel) {
            this.bindDocumentClickListener();
        } else {
            this.unbindDocumentClickListener();
        }
    }

	
    // 绑定音量事件
    private bindDocumentClickListener () {
        console.log('win', this.winClick);
        if (!this.winClick) {
            console.log('self', this.selfClick);
            this.winClick = fromEvent(this.doc, 'click').subscribe(() => {
                if (!this.selfClick) { // 说明点击了播放器以外的地方
                    console.log('self', this.selfClick);
                    this.showVolumePanel = false;
                    this.showPanel = false;
                    this.unbindDocumentClickListener();
                }
                this.selfClick = false;
            })
        }
    }

    // 解绑音量事件
    private unbindDocumentClickListener () {
        console.log('win', this.winClick);
        if (this.winClick) {
            this.winClick.unsubscribe();
            this.winClick = null;
        }
    }

当点击图标时,触发toggleListPanel(),此时将会触发回调,将 ‘showPanel’ 传给togglePanel()回调。回调中根据type的类型来更改面板的显隐。当两个面板其中一个为true时,将会绑定下一次的点击事件,否则解绑事件。

  • wy-player-panel.component.html
<!-- 歌曲列表面板 -->
<div class="play-panel " [class.show]="show">
    <div class="hd">
        <div class="hdc">
            <h4>播放列表(<span>{{songList?.length}}</span>)</h4>
            <div class="add-all">
                <i class="icon" title="收藏全部"></i>收藏全部
            </div>
            <span class="line"></span>
            <div class="clear-all">
                <i class="icon trush" title="清除全部"></i>清除全部
            </div>
            <p class="playing-name">{{currentSong?.name}}</p>
            <i class="icon close" (click)="onClose.emit()"></i>
        </div>
    </div>
    <!-- 列表 -->
    <div class="bd">
        <div class="msk"></div>
        <div class="list-wrap">
            <ul >
                <li *ngFor="let item of songList; index as i" [class.current]="currentIndex === i" (click)="onChangeSong.emit(item)">  
                    <i class="col arrow"></i>
                    <div class="col name ellipsis">{{item.name}}</div>
                    <div class="col icons">
                        <i class="ico like" title="收藏"></i>
                        <i class="ico share" title="分享"></i>
                        <i class="ico trush" title="删除"></i>
                    </div>
                    <div class="singers clearfix ellipsis">
                        <div class="singer-item" *ngFor="let singer of item.ar last as isLast">
                            <a  class="col ellipsis">{{singer.name}}</a>
                            <span [hidden]="isLast">/</span>
                        </div>
                    </div>
                    <div class="col duration">{{item.dt / 1000 | formatTime}}</div>
                    <div class="col link"></div>
                </li>
            </ul>
        </div>
    </div>
    
</div>
  • wy-player-panel.component.ts
    @Input() songList: Song[];
    @Input() currentSong: Song;
    @Input() currentIndex: number;
    @Input() show: boolean;

    @Output() onClose = new EventEmitter
    @Output() onChangeSong = new EventEmitter


    ngOnChanges (changes: SimpleChanges): void {
        if (changes['songList']) {
            console.log('songList', this.songList);
        }
        if (changes['currentSong']) {
            console.log('currentSong', this.currentSong);
        }
    }

当列表面板开启时,接受父组件传来的歌曲列表songList,当前歌曲currentSong,当前歌曲索引currentIndex。将歌曲列表渲染到DOM中,当点击列表中其他歌曲的时候,将歌曲广播给父组件,父组件接收到后将会更新当前歌曲,实现选择切歌的效果

  • wy-player.componetn.ts
   // 改变当前歌曲
    onChangeSong (song: Song) {
        this.updateCurrentIndex(this.playList, song);
    }

列表滚动

歌曲渲染之后但无法滑动,歌单内的歌曲也无法全部展示出来,因此需要添加滚动条,此时借助betterScroll插件来实现这个功能。

BetterScroll 是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件。它的核心是借鉴的 iscroll 的实现,它的 API 设计基本兼容 iscroll,在 iscroll 的基础上又扩展了一些 feature 以及做了一些性能优化。BetterScroll 是使用纯 JavaScript 实现的,这意味着它是无依赖的。

核心滚动

主要步骤是先安装插件,在需要使用的地方导入包,然后声明一下插件即可。 创建新组建wy-scroll增加插件复用性

  • wy-scroll.compoent.ts
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core';
import BScroll from '@better-scroll/core'
import ScrollBar from '@better-scroll/scroll-bar'
import MouseWheel from '@better-scroll/mouse-wheel'
BScroll.use(ScrollBar);
BScroll.use(MouseWheel);

@Component({
    selector: 'app-wy-scroll',
    template: `
  <div class="wy-scroll" #wrap>
      <ng-content></ng-content>
  </div>
  `,
    styles: [`.wy-scroll{ width: 100%; height: 100%; overflow: hidden }`],
    encapsulation: ViewEncapsulation.None,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class WyScrollComponent implements AfterViewInit, OnChanges {

    @Input() refreshDelay = 50; // 刷新插件延迟时间
    @Input() data: any[];	// 父组件传递列表

    private bs: BScroll; // 声明插件
    @ViewChild('wrap', { static: true }) private wrapRef: ElementRef

	// 
    ngOnChanges (changes: SimpleChanges): void {
        if (changes['data']) {
            this.refreshScroll();
        }
    }

    ngAfterViewInit (): void {
        this.bs = new BScroll(this.wrapRef.nativeElement, {
            // 滚动条
            scrollbar: {
                interactive: true // 滚动条可以交互
            },
            // 鼠标滚轮
            mouseWheel: {
                speed: 20, // 滚轮速度
                invert: false, // 滚轮方向
                easeTime: 300 // 动画缓动时长
            }
        })
    }

	// 刷新插件回调
    private refresh () {
        this.bs.refresh();
    }
	// 刷新插件
    refreshScroll () {
    	// 设置定时器,等待DOM渲染后执行
        setTimeout(() => {
            this.refresh()
        }, this.refreshDelay);
    }

}

在陆续安装完Scroll(滚动条)和MouseWheel(鼠标滚轮)之后使用use完成引用。按照文档属性设置相应的参数。
效果图


在实现滚动条之后,要实现切歌时高亮图标能跟着切歌的当前歌曲,在点击上一曲和下一曲的时候能实时切换,并且在播放歌曲的时候,打开歌曲列表面板能够及时滚动到当前歌曲。

点击上一曲或者下一曲的时候,会改变当前的歌曲,此时在ngOnChanges钩子处监听当前歌曲的变化和歌曲列表面板的显隐变化,如果当前歌曲有变化或者面板变为显示就将滚动条滑向当前歌曲处

  • wy-player-panel.component.ts
    @Input() songList: Song[]; // 歌曲列表
    @Input() currentSong: Song; // 当前歌曲
    @Input() currentIndex: number; // 当前歌曲索引
    @Input() show: boolean; // 面板显隐

    @Output() onClose = new EventEmitter // 面板是否关闭
    @Output() onChangeSong = new EventEmitter  // 改变当前歌曲为选中的列表歌曲

    @ViewChildren(WyScrollComponent) private wyScroll: QueryList<WyScrollComponent>  // 滚动条

    scrollY = 0;  // 滚动条Y轴偏移量

	 // 数据监听
    ngOnChanges (changes: SimpleChanges): void {
    	 // 监听歌单列表
        if (changes['songList']) {
            console.log('songList', this.songList);
        }
         // 监听当前歌曲变化
        if (changes['currentSong']) {
            if (this.currentSong) {
                if (this.show) {
                    this.scrollToCurrent();
                }
            }
        }
         // 监听面板显隐
        if (changes['show']) {
            console.log('refresh');
            if (!changes['show'].firstChange && this.show) {
                this.wyScroll.first.refreshScroll()
                if (this.currentSong) {
                    setTimeout(() => {
                        if (this.currentSong) {
                            this.scrollToCurrent();
                        }
                    }, 80);
                }
            }
        }
    }
	 // 移动滚动条
    private scrollToCurrent () {
        const songListRefs = this.wyScroll.first.el.nativeElement.querySelectorAll('ul li');
        if (songListRefs.length) {
            const currentLi = songListRefs[this.currentIndex || 0] as HTMLElement;
            const offsetTop = currentLi.offsetTop;
            const offsetHeight = currentLi.offsetHeight;
            if (offsetTop - Math.abs(this.scrollY) > offsetHeight * 5 || offsetTop < Math.abs(this.scrollY)) {
                this.wyScroll.first.scrollToElement(currentLi, 300, false, false);
            }
        }
    }
  • wy-scroll.component.ts
    constructor(readonly el: ElementRef) { }
	
	   ngAfterViewInit (): void {
        this.bs = new BScroll(this.wrapRef.nativeElement, {
            scrollbar: {
                interactive: true
            },
            mouseWheel: {
                speed: 20,
                invert: false,
                easeTime: 300
            }
        });
        this.bs.on('scrollEnd', ({ y }) => this.onScrollEnd.emit(y));
    }

先获取滚动条插件节点中所有的li,如果长度不为0即表示存在当前歌曲,此时根据当前歌曲索引拿到当前歌曲的节点,并且声明当前节点距离顶部的偏移量以及每个节点的高度。并且在初始化视图之后添加对滚动条插件的滚动结束触发机制,来将当前以滚动的距离广播给父组件。


此时要判断当前歌曲是否在可视范围内,如果在就不需要调整滚动条。当歌曲节点距离顶部的偏移量大于滚动条的Y轴偏移量以及当前节点距离顶部的偏移量小于滚动条的Y轴偏移量即表示脱离可视范围,此时就要调用插件的scrollToElement方法,将会滚动到指定的目标元素处。


当点击歌单播放歌曲时,此时点击播放模式为随机后切换歌曲并打开面板将会出现当前歌曲与列表歌曲不匹配的问题。原因是播放歌曲后调整为随机模式并切换歌曲时,此时的当前歌曲索引为父组件传来的值,并没有进行更新索引。

  • wy-player-panel.component.ts
     currentIndex: number; // 取消传入,计算当前歌曲索引

	   // 数据监听
     ngOnChanges (changes: SimpleChanges): void {
         // 监听当前歌曲变化
        if (changes['currentSong']) {
            if (this.currentSong) {
                this.currentIndex =  findIndex(this.songList, this.currentSong);
                if (this.show) {
                    this.scrollToCurrent();
                }
            } 
        }
    }
  • array.ts
// 查找当前歌曲在列表中的索引
export function findIndex(list: Song[], currentSong: Song): number{
    return list.findIndex(item => item.id === currentSong.id)
}

当前歌曲发生变化后,将会重新查找当前歌曲在歌曲列表中的索引。


更改完成后,刷新页面,先将模式调整为随机,此时播放歌单并点击下一曲,将会发现依然是顺序播放。原因是在首页渲染之后,点击歌单的回调中并没有判断当前播放模式。

  • home-component.ts
	
    private playState: PlayState; // 当前播放模式

	this.store$.pipe(select(getPlayer)).subscribe(res => this.playState = res)

	   // 点击歌单,获取歌单信息
    onPlaySheet (id: number) {
        this.sheetServe.playSheet(id).subscribe((list) => {
            this.store$.dispatch(SetSongList({ songList: list }));

            let trueIndex = 0, trueList = list.slice();

            if (this.playState.playMode.type === "random") {
                trueList = shuffle(list || []);
                trueIndex = findIndex(trueList, list[trueIndex]);
            }

            this.store$.dispatch(SetPlayList({ playList: trueList }));
            this.store$.dispatch(SetCurrentIndex({ currentIndex: trueIndex }));
        })
    }

首页初始化时先订阅当前播放信息,在点击播放歌单时判断当前播放模式是否为随机,是则打乱歌单数组,重新计算当前歌曲的索引。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值