[MAUI] Imitation of Netease Cloud Music's interactive realization of vinyl records


Students who have used the Netease Cloud Music App should be familiar with its playback interface.

This is a good interaction design. The interface metaphor of the gramophone accurately conveys the product concept and usage method to people: when the finger slides left and right, it simulates the interactive function of changing the turntable to switch songs.

Today we will implement this interactive effect in .NET MAUI , let’s take a look at the effect first:

insert image description here

Using .NET MAU to achieve cross-platform support, this project can run on Android and iOS platforms.

Create page layouts

The project simulates the main playback interface of NetEase Cloud Music, which can play local music files. Using MatoMusic.Core as the playback core, this project will not repeat it. Please read this blog post [MAUI Project Combat] Music Player (2): Play Kernel

Create a new .NET MAUI project, name it CloudMusicGroove, and refer to MatoMusic.Core.

Copy the interface image resource files to the project\Resources\Images, these interface image resources can be easily obtained by unpacking the official apk.

insert image description here

Include them in the MauiImage resource manifest.

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

Create a static layout of the page, as shown in the following figure

insert image description here

Among them, the turntable element is a circle of 300 300, and the album cover is a circle of 200 200. The circular area of ​​the picture is realized by cropping. The code is as follows:

<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>

Set the gramophone needle element, the code is as follows:

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

Create the PitContentLayout area, which is a 3*2 grid layout, used to place three functional areas

Create three PitGrid controls in PitContentLayout, and name the PitGrid controls in these three functional areas, LeftPit, MiddlePit, RightPit, and the code is as follows:

<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>

insert image description here

Create gesture controls

Gesture control, or gesture container control, is used to wrap the drag object to give the drag object the ability to respond to panning gestures.

Create a container control HorizontalPanContainer, which contains a PanGestureRecognizer that provides a description of the process when the finger moves on the screen

<?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>

Create a gesture control. He wraps up the gramophone turntable area. In this way, when the finger slides over the turntable area, the pan gesture event can be triggered.

<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>

insert image description here

Create shadow controls

The shadow control is used to display the previous and next album art when sliding the turntable.

During the whole process of sliding left and right, the distance between the center point of the turntable and the center point of the adjacent turntable should be the width of the screen. As shown below

insert image description here

The distance between turntable and turntable should be

Create a shadow control, which will follow the movement of the dragged object. Of course, we only need to keep moving in the X direction.

Create a shadow control in the HorizontalPanContainer adjacent container view in NowPlayingPage, the code is as follows:

<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>

We bind the TranslationX property of the shadow control to the TranslationX property of the drag object, and the initial effect is as follows

insert image description here

The dragging area needs two shadow controls to display the album art of the previous song and the next song respectively.

We need to match the offset of the shadow control to the screen width, and we use a converter to do this.

Create the CalcValueConverter.cs file with the following code:

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();
    }

}

Add CalcValueConverter to the resource dictionary,

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

Set the converter for the property binding of the shadow control, and set the converter parameters, the code is as follows:

Left Shadow Control (Last Album Turntable)

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

Right shadow control (next album turntable)

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

insert image description here

Turntable Pluck Interaction

Of course we only want the drag object to respond to gestures only in the horizontal direction

In HorizontalPanContainer, register the response event PanGestureRecognizer_OnPanUpdated of PanGestureRecognizer, and add the following code in 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;

        ...
    }
}

Combining the three PitGrids written in the previous section, drag the turntable at this time, and when the dragging starts, enters the pit, leaves the pit, and releases, four state events of Start, In, Out, and Over are triggered respectively.

The valid areas for responding to status events are as follows

insert image description here

Create a method to detect whether the center point of the turntable is in the valid area,

When the translation direction is right, the X coordinate of the turntable center point should be greater than the start X coordinate of the right pit area;
when the translation direction is left, the X coordinate of the turntable center point should be smaller than the end X coordinate of the left pit area.

Add the code in GestureStatus.Running as follows:


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;      
    }
    ...
}

In different pits, handle the corresponding state events.

If the center point of the turntable is still within the MiddlePit area when the finger leaves, move the turntable rebound to the center point of MiddlePit.

If it is in the LeftPit or RightPit area, move the turntable to the center of the LeftPit or RightPit area.

insert image description here

At this point the basic functionality of dragging the turntable has been achieved, but when the turntable is released, the shadow turntable does not move to the center point of the MiddlePit as expected.

insert image description here

When hitting the LeftPit or RightPit area, we want the shadow control to move to the MiddlePit center point. When the shadow control is moved into place, replace the current turntable and become the new drag object. In this way, the turntable can be toggled infinitely to achieve the effect of continuous song cutting.

When the finger is released and the turntable is ready to move left or right, quickly replace the position of the shadow control with the current turntable position. With the "teleport" of the current turntable, it looks like the turntable has been replaced by a shadow turntable, but the drag thing that is active in the center of the screen is always the real control.

Add the code in GestureStatus.Completed as follows:

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;

    }

It looks like you can pluck the turntable infinitely

insert image description here

Turntable and needle animation

The turntable rotates, and the music plays accordingly. The effect of the turntable rotation is achieved by rotating the album cover picture at a speed of 20 seconds per revolution.

Create an Animation object in NowPlayingPage to control the turntable rotation.

private Animation rotateAnimation;

Write the start rotation animation method StartAlbumArtRotation and stop animation method StopAlbumArtRotation, the code is as follows:

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();
    }

}

The effect is as follows:

insert image description here

Note that when the music is paused, the rotation animation stops, and when the music resumes, the turntable should start the rotation animation from the angle it stopped before.

When turning the turntable or cutting a song, the stylus will move away from the turntable, and the effect of the stylus moving away can be realized by rotating the stylus picture 30 degrees.

insert image description here

First set the anchor point, AnchorX=0.18, AnchorY=0.059, as follows:

<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" />

When the music is playing
, when the finger starts to slide, the stylus moves away from the turntable, and the turntable stops rotating;
when the finger leaves, the stylus returns to the turntable, and the turntable continues to rotate.

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;
        ...
    }
}

The effect is as follows:

insert image description here

When pausing and resuming, the position of the stylus should also change accordingly.

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();

        }

    }
}

The effect is as follows:

insert image description here

The final effect is as follows:

insert image description here

project address

Github:maui-samples

Guess you like

Origin blog.csdn.net/jevonsflash/article/details/130353772