[MAUI 專案實戰] 手勢控制音樂播放器(一): 概述與架構

林曉lx發表於2023-04-09

這是一篇系列博文。請關注我,學習更多.NET MAUI開發知識!

在之前的博文中提到這個專案,它是為音樂播放器專門開發的基於手勢控制的UI介面。

此UI介面可以讓使用者在不看螢幕的情況下,透過手勢來控制音樂播放器的各種操作,如播放、暫停、下一首、上一首。

在這裡插入圖片描述
手勢來控制的互動方式適合不方便看手機螢幕時簡單的音樂播放需求,在駕車、運動等場景下有較好的使用者體驗。

在這裡插入圖片描述

架構

跨平臺

使用.NET MAU實現跨平臺支援,本專案可執行於Android、iOS平臺。

在這裡插入圖片描述在這裡插入圖片描述

播放核心

播放核心使用MatoMusic.Core,檢視此博文[MAUI 專案實戰] 音樂播放器(二):播放核心

此專案重點關注的是手勢互動UI,播放核心的實現將不再贅述。

手勢原理

在播放介面的8個方向,分別放置控制區域,透過拖拽圓形專輯(pan)到控制區域(pit),實現對應的控制操作。此示例實現了快進,快退,下一曲,上一曲,播放/暫停操作,pan和pit將在下一章節介紹。

拖拽平移和控制區域的關係,可以抽象成四個狀態,分別是Out,In,Over,Start。

手勢狀態型別PanType定義如下:

  • In:pan進入pit時觸發,
  • Out:pan離開pit時觸發,
  • Over:釋放pan時觸發,
  • Start:pan開始拖拽時觸發
public enum PanType
{
    Out, In, Over, Start
}

對拖拽手勢的處理,由手勢容器控制元件PanContainer封裝,實現方式將在下一章節介紹。

一次有效的控制,經過Start -> In -> Out -> Over的狀態變化,並且手指釋放位置是在pit的範圍內。

當整個控制觸發完成後,控制元件將觸發OnfinishedChoise事件。

基本控制

在頁面中訂閱這個事件,在事件方法中實現控制邏輯。

如上一曲操作,訂閱事件後,實現如下邏輯:

private void DefaultPanContainer_OnOnfinishedChoise(object sender, PitGrid e)
{
    CurrentPitView = e;
    switch (CurrentPitView.PitName)
    {
        case "LeftPit":
        MusicRelatedViewModel.PreAction(null);
        break;

        ...
    }
}

控制元件會將當前觸發的pit傳遞給事件方法,透過pit的名稱,可以判斷當前觸發的是哪個控制區域。

控制元件在經過pit時會啟用廣播事件,使用CommunityToolkit庫的 WeakReferenceMessenger實現了訊息機制,訂閱此事件訊息可以接收到控制元件運動的細節,在事件方法中實現自己的邏輯,如介面元素樣式的改變。

public NowPlayingPage()
{
    InitializeComponent();
    WeakReferenceMessenger.Default.Register<PanActionArgs, string>(this, TokenHelper.PanAction, PanActionHandler);
    ...
}

如在拖拽開始時,顯示控制區域的提示資訊,拖拽結束時,隱藏提示資訊。

private async void PanActionHandler(object recipient, PanActionArgs args)
{
    var parentAnimation = new Animation();

    Animation scaleUpAnimation1;
    Animation scaleUpAnimation2;
    switch (args.PanType)
    {
        case PanType.Over:

            scaleUpAnimation1 = new Animation(v => this.PitContentLayout.Opacity = v, PitContentLayout.Opacity, 0, Easing.CubicOut);
            scaleUpAnimation2 = new Animation(v => this.TitleLayout.Opacity = v, TitleLayout.Opacity, 1, Easing.CubicOut);

            parentAnimation.Add(0, 1, scaleUpAnimation1);
            parentAnimation.Add(0, 1, scaleUpAnimation2);

            parentAnimation.Commit(this, "RestoreAnimation", 16, 250);
         
...

快進/快退

拖拽停留在左右控制區域超過一定時間,將觸發“快進”或“快退”

播放介面擁有一個定時器,用於拖拽快進、快退功能

private IDispatcherTimer _dispatcherTimer;

拖拽進入控制區域時,啟動定時器,停留在左右控制區域大於2s時將觸發定時器Tick事件,執行快進或快退操作。

private async void PanActionHandler(object recipient, PanActionArgs args)
{

    switch (args.PanType)
    {
        ...
        case PanType.In:

            switch (args.CurrentPit?.PitName)
            {
                case "LeftPit":

                    _dispatcherTimer =Dispatcher.CreateTimer();
                    _dispatcherTimer.Interval=new TimeSpan(0, 0, 2);

                    _dispatcherTimer.Tick+=   async (o, e) =>
                    {
                        this.TipLabel.Text = FaIcons.IconFastBackward;
                        this.TipTextLabel.Text = "快退";
                        _runCount++;
                        await MusicRelatedViewModel.StartFastSeeking(-2);


                    };
                    _dispatcherTimer.Start();
                    this.TipTextLabel.Text = "上一曲";

                    break;
    ...

_runCount是個全域性變數,記錄是否已經執行過快進或快退操作,當退出控制區域時,如果已經執行過快進或快退操作,將停止快進或快退操作,並將計時器停止。

 case PanType.Out:
    this.PitTipLayout.Children.Clear();
    if (this._runCount > 0)
    {
        MusicRelatedViewModel.EndFastSeeking();
    }
    if (_dispatcherTimer!=null)
    {
        _dispatcherTimer.Stop();

    }
    _runCount = 0;
    this.TipTextLabel.Text = string.Empty;


    break;

同理,在鬆手時,應該停止快進或快退操作,並將計時器停止。

case PanType.Over:

    ...
    MusicRelatedViewModel.EndFastSeeking();
    if (_dispatcherTimer!=null)
    {
        _dispatcherTimer.Stop();

    }
    _runCount = 0;

沉浸模式

_dispatcherTimer2是控制介面進入“沉浸模式”的定時器,當介面無操作5s之後介面將進入沉浸模式,隱藏標題欄。

private void SetupFullScreenMode(int delay = 5)
{
    _dispatcherTimer2 =Dispatcher.CreateTimer();
    _dispatcherTimer2.Interval=new TimeSpan(0, 0, delay);

    _dispatcherTimer2.Tick+=   (o, e) =>
    {

        this.MainCircleSlider.BorderWidth = 3;
        this.TitleLayout.FadeTo(0);

    };

    _dispatcherTimer2.Start();
}

有操作進行時,恢復到正常模式。

case PanType.Start:

    ...

    if (_dispatcherTimer2.IsRunning)
    {
        _dispatcherTimer2.Stop();
    }
    SetupNormalMode();

    break;

下一章將逐步展開手勢控制的實現細節。

專案地址

Github:maui-samples

相關文章