@
用過網易雲音樂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控制元件命名,LeftPit
、MiddlePit
,RightPit
,程式碼如下:
<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();
}
}
}
效果如下:
最終效果如下: