12

[MAUI]模仿网易云音乐黑胶唱片的交互实现 - 林晓lx

 1 year ago
source link: https://www.cnblogs.com/jevonsflash/p/17351565.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client


用过网易云音乐App的同学应该都比较熟悉它播放界面。

这是一个良好的交互设计,留声机的界面隐喻准确地向人们传达产品概念和使用方法:当手指左右滑动时,便模拟了更换唱盘从而导向切换歌曲的交互功能。

今天在 .NET MAUI 中我们来实现这个交互效果,先来看看效果:

在这里插入图片描述

使用.NET MAU实现跨平台支持,本项目可运行于Android、iOS平台。

创建页面布局

项目模拟了网易云音乐的播放主界面,可播放本地音乐文件。使用MatoMusic.Core作为播放内核,此项目对其将不再赘述。请阅读此博文[MAUI 项目实战] 音乐播放器(二):播放内核

新建.NET MAUI项目,命名CloudMusicGroove,项目引用MatoMusic.Core。

将界面图片资源文件拷贝到项目\Resources\Images中,这些界面图片资源可通过解包官方apk的方式轻松获取。

在这里插入图片描述

将他们包含在MauiImage资源清单中。

<MauiImage Include="Resources\Images\*" />

创建页面的静态布局,布局如下图所示

在这里插入图片描述

其中唱盘元素是一个300 × 300的圆形,专辑封面为200 × 200的圆形,图片的圆形区域是通过裁剪实现的,代码如下:

<Grid 
        VerticalOptions="Start"
        HorizontalOptions="Start">
    <Image Source="ic_disc.png"
            WidthRequest="300"
            HeightRequest="300" />

    <Image HeightRequest="200"
            WidthRequest="200"
            x:Name="AlbumArtImage"
            Margin="0"
            Source="{Binding  CurrentMusic.AlbumArt}"
            VerticalOptions="CenterAndExpand"
            HorizontalOptions="CenterAndExpand"
            Aspect="AspectFill">

        <Image.Clip>
            <RoundRectangleGeometry  CornerRadius="125"
                                        Rect="0,0,200,200" />
        </Image.Clip>
    </Image>

</Grid>

设置留声机唱针元素,代码如下:

<Image WidthRequest="100"
    HeightRequest="167"
    HorizontalOptions="Center"
    VerticalOptions="Start"
    Margin="70,-50,0,0"
    Source="ic_needle.png"
    x:Name="AlbumNeedle" />

创建PitContentLayout区域,这个区域是一个3 × 2的网格布局,用来放置三个功能区域

在PitContentLayout中创建三个PitGrid控件,并对这三个功能区域的PitGrid控件命名,LeftPitMiddlePitRightPit,代码如下:

<Grid  x:Name="PitContentLayout"
        Opacity="1"
        BindingContext="{Binding CurrentMusicRelatedViewModel}">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*"></ColumnDefinition>
        <ColumnDefinition Width="2*"></ColumnDefinition>
        <ColumnDefinition Width="1*"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <controls1:PitGrid x:Name="LeftPit"
                        Background="pink"
                        PitName="LeftPit">
    </controls1:PitGrid>
    <controls1:PitGrid Grid.Column="1"
                        x:Name="MiddlePit"
                        Background="azure"
                        
                        PitName="MiddlePit">
    </controls1:PitGrid>
    <controls1:PitGrid Grid.Column="2"
                        x:Name="RightPit"
                        Background="lightyellow"
                        PitName="RightPit">

    </controls1:PitGrid>


</Grid>
在这里插入图片描述

创建手势控件

手势控件,或称为手势容器控件,它来对拖拽物进行包装,以赋予拖拽物响应平移手势的能力。

创建一个容器控件HorizontalPanContainer,控件包含的PanGestureRecognizer提供了当手指在屏幕移动这一过程的描述

<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiSample.Controls.HorizontalPanContainer">
    <ContentView.GestureRecognizers>
        <PanGestureRecognizer PanUpdated="PanGestureRecognizer_OnPanUpdated"></PanGestureRecognizer>
        <TapGestureRecognizer Tapped="TapGestureRecognizer_OnTapped"></TapGestureRecognizer>

    </ContentView.GestureRecognizers>
</ContentView>

创建一个手势控件。他将留声机唱盘区域包裹起来。这样当手指在唱盘区域滑动时,就可以触发平移手势事件。

<controls:HorizontalPanContainer Background="Transparent"
        x:Name="DefaultPanContainer"
        OnTapped="DefaultPanContainer_OnOnTapped"
        OnfinishedChoise="DefaultPanContainer_OnOnfinishedChoise">
    <controls:HorizontalPanContainer.Content>
        <Grid PropertyChanged="BindableObject_OnPropertyChanged"
                VerticalOptions="Start"
                HorizontalOptions="Start">
            <Image Source="ic_disc.png"
                    WidthRequest="300"
                    HeightRequest="300" />

            <Image HeightRequest="200"
                    WidthRequest="200"
                    x:Name="AlbumArtImage"
                    Margin="0"
                    Source="{Binding  CurrentMusic.AlbumArt}"
                    VerticalOptions="CenterAndExpand"
                    HorizontalOptions="CenterAndExpand"
                    Aspect="AspectFill">

                <Image.Clip>
                    <RoundRectangleGeometry  CornerRadius="125"
                                                Rect="0,0,200,200" />
                </Image.Clip>
            </Image>

        </Grid>

    </controls:HorizontalPanContainer.Content>
</controls:HorizontalPanContainer>

在这里插入图片描述

创建影子控件

影子控件用于滑动唱盘时,显示上一曲、下一曲的专辑封面。

在左右滑动的全程中,唱盘的中心点与相邻唱盘的中心点距离,应为屏幕宽度。如下图所示

在这里插入图片描述

唱盘与唱盘的距离应是

创建影子控件,这个控件将随拖拽物的移动而跟随移动,当然我们只需要保持X方向的移动即可。

在NowPlayingPage中的HorizontalPanContainer相邻容器视图中创建影子控件,代码如下:

<Grid TranslationX="{Binding Source={x:Reference  DefaultPanContainer} ,Path=Content.TranslationX}">
    <Image Source="ic_disc.png"
            WidthRequest="300"
            HeightRequest="300" />

    <Image HeightRequest="200"
            WidthRequest="200"
            Margin="0"
            Source="{Binding  PreviewMusic.AlbumArt}"
            VerticalOptions="CenterAndExpand"
            HorizontalOptions="CenterAndExpand"
            Aspect="AspectFill">

        <Image.Clip>
            <RoundRectangleGeometry  CornerRadius="125"
                                        Rect="0,0,200,200" />
        </Image.Clip>
    </Image>

</Grid>

我们将这个影子控件的TranslationX属性将绑定到拖拽物的TranslationX属性上,初步效果如下

在这里插入图片描述

拖拽区域需要两个影子控件,分别显示上一曲和下一曲的专辑封面。

我们需要将影子控件的偏移量与屏幕宽度作匹配,我们用转换器来实现这个功能。

创建CalcValueConverter.cs文件,代码如下:

public class CalcValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var d = (double)value;
        double compensation;
        if (double.Parse((string)parameter)>=0)
        {
            compensation=((App.Current as App).PanContainerWidth+300)/2;
        }
        else
        {
            compensation=-1.5*(App.Current as App).PanContainerWidth+300/2;
        }
        return d+compensation;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }

}

将CalcValueConverter添加至资源字典中,

<converter:CalcValueConverter x:Key="CalcValueConverter"></converter:CalcValueConverter>

对影子控件的属性绑定设置转换器,并设置转换器参数,代码如下:

左影子控件(上一曲专辑唱盘)

TranslationX="{Binding Source={x:Reference  DefaultPanContainer} ,Path=Content.TranslationX,Converter={StaticResource CalcValueConverter},ConverterParameter=-1}"

右影子控件(下一曲专辑唱盘)

TranslationX="{Binding Source={x:Reference  DefaultPanContainer} ,Path=Content.TranslationX,Converter={StaticResource CalcValueConverter},ConverterParameter=-1}"
在这里插入图片描述

唱盘拨动交互

当然我们仅希望拖拽物仅在水平方向上响应手势

在HorizontalPanContainer中,注册PanGestureRecognizer的响应事件PanGestureRecognizer_OnPanUpdated,在GestureStatus.Running添加代码如下:

private async void PanGestureRecognizer_OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
    var isInPit = false;
    switch (e.StatusType)
    {
        case GestureStatus.Running:
            var translationX = PositionX + e.TotalX;
            var translationY = PositionY;

        ...
    }
}

结合上一小节写的三个PitGrid,此时拖拽唱盘,并且在拖拽开始,进入pit,离开pit,释放时,分别触发Start,In,Out,Over四个状态事件。

响应状态事件的有效区域如下

在这里插入图片描述

创建检测唱盘中心点是否在有效区域的方法,

当平移方向为向右时,唱盘中心点的X坐标应大于右pit区域的起始X坐标;
当平移方向为向左时,唱盘中心点的X坐标应小于左pit区域的结束X坐标。

在GestureStatus.Running添加代码如下:


foreach (var item in PitLayout)
{
    var pitRegion = new Region(item.X, item.X + item.Width, item.Y, item.Y + item.Height, item.PitName);
    var isXin = (e.TotalX>0 && translationX >= pitRegion.StartX - Content.Width / 2 && pitRegion.StartX>this.Width/2)||
        (e.TotalX<0 && translationX <= pitRegion.EndX - Content.Width / 2&&pitRegion.EndX<this.Width/2);
    if (isXin)
    {
        isInPit = true;      
    }
    ...
}

在不同的pit中,处理对应的状态事件。

若在手指离开时,唱盘的中心点还在MiddlePit区域范围内,则将唱盘回弹移动到MiddlePit中心点。

若在LeftPit或RightPit区域,则将唱盘移动到LeftPit或RightPit区域中心点。

在这里插入图片描述

此时已经实现了拖拽唱盘的基本功能,但是在释放唱盘时,影子唱盘并没有如预期那样移动到MiddlePit的中心点。

在这里插入图片描述

当命中LeftPit或RightPit区域时,我们希望影子控件移动到MiddlePit中心点。当影子控件移动到位时,替换掉当前的唱盘,成为新的拖拽物。由此可以无限的拨动唱盘实现连续切歌的效果。

当手指释放,唱盘准备向左或右移动时,迅速将影子控件的位置替换成当前唱盘的位置。用当前唱盘的“瞬移”,看起来像唱盘被影子唱盘替换掉了,但是在屏幕中心活动的拖拽物,一直是真正的那个控件。

在GestureStatus.Completed添加代码如下:

case GestureStatus.Completed:
    double destinationX;
    var view = this.CurrentView;

    if (isInPitPre)
    {
        var pitRegion = new Region(view.X, view.X + view.Width, view.Y, view.Y + view.Height, view.PitName);

        var prefix = pitRegion.StartX>this.Width/2 ? 1 : -1;
        destinationX=PositionX+prefix*(App.Current as App).PanContainerWidth;
    }
    else
    {
        destinationX=PositionX;

    }

这样看起来像可以无限地拨动唱盘了

在这里插入图片描述

唱盘和唱针动画

唱盘转动,音乐随之播放,通过将专辑封面图片以20秒每圈的速度旋转来实现唱盘旋转的效果。

在NowPlayingPage中创建一个Animation对象,用于控制唱盘旋转。

private Animation rotateAnimation;

编写启动旋转动画方法StartAlbumArtRotation以及停止动画方法StopAlbumArtRotation,代码如下:

private void StartAlbumArtRotation()
{
    this.AlbumArtImage.AbortAnimation("AlbumArtImageAnimation");
    rotateAnimation = new Animation(v => this.AlbumArtImage.Rotation = v, this.AlbumArtImage.Rotation, this.AlbumArtImage.Rotation+ 360);
    rotateAnimation.Commit(this, "AlbumArtImageAnimation", 16, 20*1000, repeat: () => true);
}

private void StopAlbumArtRotation()
{
    this.AlbumArtImage.CancelAnimations();
    if (this.rotateAnimation!=null)
    {
        this.rotateAnimation.Dispose();
    }

}

效果如下:

在这里插入图片描述

注意,当音乐暂停后,停止旋转动画,当音乐恢复播放时,转盘应从之前停止的角度开始启动旋转动画。

在拨动唱盘或切歌时,唱针将从唱盘上移开,通过旋转唱针图片30度来实现唱针移开的效果。

在这里插入图片描述

首先设置锚点,AnchorX=0.18,AnchorY=0.059,如下:

<Image WidthRequest="100"
    HeightRequest="167"
    HorizontalOptions="Center"
    VerticalOptions="Start"
    Margin="70,-50,0,0"
    Source="ic_needle.png"
    x:Name="AlbumNeedle"
    AnchorX="0.18"
    AnchorY="0.059" />

在音乐播放时
当手指开始滑动时,唱针从唱盘上移开,唱盘停止旋转;
当手指离开时,唱针回到唱盘上,唱盘继续旋转。

private async void PanActionHandler(object recipient, HorizontalPanActionArgs args)
{
    switch (args.PanType)
    {
        case HorizontalPanType.Over:

            if (MusicRelatedViewModel.IsPlaying)
            {
                await this.AlbumNeedle.RotateTo(0, 300);
                this.StartAlbumArtRotation();
            }


            break;
        case HorizontalPanType.Start:

            if (MusicRelatedViewModel.IsPlaying)
            {
                await this.AlbumNeedle.RotateTo(-30, 300);
                this.StopAlbumArtRotation();
            }
            break;
        ...
    }
}

效果如下:

在这里插入图片描述

当暂停、恢复时,唱针的位置也应该随之改变。

private async void MusicRelatedViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName==nameof(MusicRelatedViewModel.IsPlaying))
    {
        if (MusicRelatedViewModel.IsPlaying)
        {
            await this.AlbumNeedle.RotateTo(0, 300);
            this.StartAlbumArtRotation();
        }
        else
        {
            await this.AlbumNeedle.RotateTo(-30, 300);
            this.StopAlbumArtRotation();

        }

    }
}

效果如下:

在这里插入图片描述

最终效果如下:

在这里插入图片描述

Github:maui-samples


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK