[MAUI 專案實戰] 手勢控制音樂播放器(四):圓形進度條

2023-04-09 12:01:15

@


我們將繪製一個圓形的音樂播放控制元件,它包含一個圓形的進度條、專輯頁面和播放按鈕。

關於圖形繪製

使用MAUI的繪製功能,需要Microsoft.Maui.Graphics庫。

Microsoft.Maui.Graphics 是一個實驗性的跨平臺圖形庫,它可以在 .NET MAUI 中使用。它提供了一組基本的圖形元素,如矩形、圓形、線條、路徑、文字和影象。它還提供了一組基本的圖形操作,如填充、描邊、裁剪、變換和漸變。

Microsoft.Maui.Graphics在不同的目標平臺上使用一致的API存取本機圖形功能,而底層實現使用了不同的圖形渲染引擎。其中通用性較好的是SkiaSharp圖形庫,支援幾乎所有的作業系統,在不同平臺上的表現也近乎一致。

建立自定義控制元件

在專案中新增SkiaSharp繪製功能的參照Microsoft.Maui.Graphics.Skia以及SkiaSharp.Views.Maui.Controls

<ItemGroup>
    <PackageReference Include="Microsoft.Maui.Graphics.Skia" Version="7.0.59" />
    <PackageReference Include="SkiaSharp.Views.Maui.Controls" Version="2.88.3" />
</ItemGroup>

建立CircleSlider.xaml檔案,新增如下內容:

<?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"
             xmlns:forms="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
             
             x:Class="MatoMusic.Controls.CircleSlider">
  <ContentView.Content>

      <forms:SKCanvasView x:Name="canvasView"
                          PaintSurface="OnCanvasViewPaintSurface" />

    </ContentView.Content>
</ContentView>

SKCanvasView是SkiaSharp.Views.Maui.Controls封裝的View控制元件。

開啟CircleSlider.xaml.cs檔案

控制元件將包含以下可繫結屬性:

  • Maximum:最大值
  • Minimum:最小值
  • Value:當前值
  • TintColor:進度條顏色
  • ContainerColor:進度條背景顏色
  • BorderWidth:進度條寬度

定義兩個SKPaint畫筆屬性,OutlinePaint用於繪製進度條背景,ArcPaint用於繪製進度條本身。他們的描邊寬度StrokeWidth則是圓形進度條的寬度。
兩個畫筆的初始值樣式為SKPaintStyle.Stroke,描邊寬度為BorderWidth的值。

private SKPaint _outlinePaint;

public SKPaint OutlinePaint
{
    get
    {
        if (_outlinePaint == null)
        {
            SKPaint outlinePaint = new SKPaint
            {
                Style = SKPaintStyle.Stroke,
                StrokeWidth = BorderWidth,
            };
            _outlinePaint = outlinePaint;
        }
        return
            _outlinePaint;
    }
    set { _outlinePaint = value; }
}

private SKPaint _arcPaint;

public SKPaint ArcPaint
{
    get
    {
        if (_arcPaint == null)
        {
            SKPaint arcPaint = new SKPaint
            {
                Style = SKPaintStyle.Stroke,
                StrokeWidth = BorderWidth,
            };
            _arcPaint = arcPaint;
        }

        return _arcPaint;
    }
    set { _arcPaint = value; }
}

SetStrokeWidth用於設定描邊寬度,併產生一個動效,

在BorderWidth發生變更的時候,會出現一個動效。寬度會緩慢地變化至新的值。重新整理率為10ms一次,每次變化的值為1。


private float _borderWidth;

public float BorderWidth
{
    get { return _borderWidth; }
    set
    {
        var old_borderWidth = _borderWidth;

        var span = value - old_borderWidth;

        SetStrokeWidth(span, old_borderWidth);

        _borderWidth = value;

        this.ArcPaint.StrokeWidth = _borderWidth;
        this.OutlinePaint.StrokeWidth = _borderWidth;
    }
}

private async void SetStrokeWidth(float span, float old_borderWidth)
{
    if (span > 0)
    {
        for (int i = 0; i <= span; i++)
        {
            await Task.Delay(10);
            this.ArcPaint.StrokeWidth = old_borderWidth + i;
            this.OutlinePaint.StrokeWidth = old_borderWidth + i;
            RefreshMainRectPadding();
        }
    }
    else
    {
        for (int i = 0; i >= span; i--)
        {
            await Task.Delay(10);
            this.ArcPaint.StrokeWidth = old_borderWidth + i;
            this.OutlinePaint.StrokeWidth = old_borderWidth + i;
            RefreshMainRectPadding();

        }
    }

}

於此同時,因為描邊寬度變化了,需要對Padding進行補償。呼叫RefreshMainRectPadding方法計算一個新的Padding值,BoderWidth縮小時,Padding也隨之增大。

private void RefreshMainRectPadding()
{
    this._mainRectPadding =  this.BorderWidth / 2;
}

在視覺上,進度條寬度從內向外擴張變細。

若設為原寬度減去計算值,從視覺上是從外向內收縮變細。

private void RefreshMainRectPadding()
{
    this._mainRectPadding =  15 -  this.BorderWidth / 2;
}

接下來寫訂閱了CanvaseView的PaintSurface事件的方法OnCanvasViewPaintSurface。在這個方法中,我們將編寫圓形進度條的繪製邏輯。

PaintSurface事件在繪製圖形時觸發。程式執行時會實時觸發這個方法,它的引數SKPaintSurfaceEventArgs事件附帶的物件具有兩個屬性:

  • Info型別SKImageInfo
  • Surface型別SKSurface

SKImageInfo物件包含如寬度和高度等有關繪圖區域的資訊,物件SKSurface為繪製本身,我們需要利用SKImageInfo寬度和高度等資訊,結合業務資料,在SKSurface繪製出我們想要的圖形。

清空上一次繪製的圖形,呼叫SKSurface.Canvas獲取Canvas物件,呼叫Canvas.Clear方法清空上一次繪製的圖形。

canvas.Clear();

rect是一個SKRect物件,進度條本身是圓形,我們需要一個正方形的區域來控制圓形區域。

sweepAngle是當前進度對應的角度,首先計算出總進度值,通過計算當前進度對應總進度的比值,換算成角度,將這一角度賦值給sweepAngle。

startAngle是進度條的起始角度,我們將其設定為-90度,即從正上方開始繪製。


SKRect rect = new SKRect(_mainRectPadding, _mainRectPadding, info.Width - _mainRectPadding, info.Height - _mainRectPadding);
float startAngle = -90;
float sweepAngle = (float)((Value / SumValue) * 360);

呼叫Canvas.DrawOval,使用OutlinePaint畫筆繪製進度條背景,它是一個圓形

canvas.DrawOval(rect, OutlinePaint);

建立繪製路徑path,呼叫AddArc方法,將rect物件和起始角度和終止角度傳入,即可繪製出弧形。

using (SKPath path = new SKPath())
{
    path.AddArc(rect, startAngle, sweepAngle);
    canvas.DrawPath(path, ArcPaint);
}

繪製部分的完整程式碼如下:

private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{

    var SumValue = Maximum - Minimum;


    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    SKRect rect = new SKRect(_mainRectPadding, _mainRectPadding, info.Width - _mainRectPadding, info.Height - _mainRectPadding);
    float startAngle = -90;
    float sweepAngle = (float)((Value / SumValue) * 360);

    canvas.DrawOval(rect, OutlinePaint);

    using (SKPath path = new SKPath())
    {
        path.AddArc(rect, startAngle, sweepAngle);
        canvas.DrawPath(path, ArcPaint);
    }
}

使用控制元件

在MainPage.xaml中新增一個CircleSlider控制元件,
設定的Maximum,是當前曲目的時長,Value是當前曲目的進度

<controls:CircleSlider 
    HeightRequest="250"
    WidthRequest="250"
    x:Name="MainCircleSlider"
    Maximum="{Binding Duration}"
    Minimum="0.0"
    TintColor="#FFFFFF"
    ContainerColor="#4CFFFFFF"
    IsEnabled="{Binding Canplay}"
    ValueChanged="OnValueChanged"
    Value="{Binding CurrentTime,Mode=TwoWay} ">
</controls:CircleSlider>

建立專輯封面

使用MAUI的VisualElement中的Clip屬性,建立Clip裁剪,可以傳入一個Geometry物件,這裡我們使用RoundRectangleGeometry,將它的CornerRadius屬性設定為圖片寬度的一半,即可實現圓形圖片。

<Image HeightRequest="250"
        WidthRequest="250"
        Margin="7.5"
        Source="{Binding  CurrentMusic.AlbumArt}"
        VerticalOptions="CenterAndExpand"
        HorizontalOptions="CenterAndExpand"
        Aspect="AspectFill">
    <Image.Clip>
        <RoundRectangleGeometry  CornerRadius="125" Rect="0,0,250,250" />
    </Image.Clip>
</Image>

設定一個半透明背景的播放狀態指示器,當IsPlaying為False時將顯示一個播放按鈕

<Grid IsVisible="{Binding IsPlaying, Converter={StaticResource True2FalseConverter}}">
    <BoxView HeightRequest="250"
            WidthRequest="250"
            Margin="7.5"
            Color="#60000000"
            VerticalOptions="CenterAndExpand"
            HorizontalOptions="CenterAndExpand"
            CornerRadius="250" ></BoxView>
    <Label  x:Name="PauseLabel"                               
            HorizontalOptions="CenterAndExpand"
            FontSize="58"  
            TextColor="{Binding Canplay,Converter={StaticResource Bool2StringConverter},ConverterParameter=White|#434343}"
            FontFamily="FontAwesome"
            Margin="0"></Label>
</Grid>

建立PanContainer物件,用於實現拖動效果,設定AutoAdsorption屬性為True,即可實現拖動後自動吸附效果。

關於PanContainer請檢視上期的文章:平移手勢互動

用一個Grid將專輯封面,CircleSlider,以及播放狀態指示器包裹起來。完整程式碼如下

 <controls1:PanContainer BackgroundColor="Transparent"
                        x:Name="DefaultPanContainer"
                        OnTapped="DefaultPanContainer_OnOnTapped"
                        AutoAdsorption="True"
                        OnfinishedChoise="DefaultPanContainer_OnOnfinishedChoise">
    <Grid PropertyChanged="BindableObject_OnPropertyChanged"
        VerticalOptions="Start"
        HorizontalOptions="Start">
        <Image HeightRequest="250"
                WidthRequest="250"
                Margin="7.5"
                Source="{Binding  CurrentMusic.AlbumArt}"
                VerticalOptions="CenterAndExpand"
                HorizontalOptions="CenterAndExpand"
                Aspect="AspectFill">
            <Image.Clip>
                <RoundRectangleGeometry  CornerRadius="125" Rect="0,0,250,250" />
            </Image.Clip>
        </Image>
        <controls:CircleSlider>...</controls:CircleSlider>
        <Grid IsVisible="{Binding IsPlaying, Converter={StaticResource True2FalseConverter}}">
            <BoxView HeightRequest="250"
                    WidthRequest="250"
                    Margin="7.5"
                    Color="#60000000"
                    VerticalOptions="CenterAndExpand"
                    HorizontalOptions="CenterAndExpand"
                    CornerRadius="250" ></BoxView>
            <Label  x:Name="PauseLabel"                               
                    HorizontalOptions="CenterAndExpand"
                    FontSize="58"  
                    TextColor="{Binding Canplay,Converter={StaticResource Bool2StringConverter},ConverterParameter=White|#434343}"
                    FontFamily="FontAwesome"
                    Margin="0"></Label>
        </Grid>
    </Grid>
</controls1:PanContainer>


以上就是這個專案的全部內容,感謝閱讀

專案地址

Github:maui-samples