[MAUI 项目实战] 音乐播放器(三):界面交互

MAUI 框架安卓入门开发01 界面设计 1.导航栏设计1.1 由于需要水平布局的设计方式,所以外围需要使用一个HorizontalStackLayout 进行包裹着所有的子控件,来达到水平布局的效果。页面大概是这样子,按钮全部由RadioButton 进行重绘 阅读详情


UI设计的本质是对于产品的理解在界面中多种形式的映射,当需求和定位不同时,对相同的功能表达出了不同的界面和交互方式。

作为播放器,界面可以是千差万别的。《番茄播放器》的iOS平台上我开发了传统版本,和基于手势播放的版本。

在这里插入图片描述
图片来自于App Store宣传图

它们界面不同,但用的同一个播放内核。

作为播放内核项目,在MatoMusic.Core的工作已经结束。本系列博文重点还是在播放器思路的解读,关于MAUI动画交互,我打算有时间另外写博客(这里给自己挖个坑)。 本项目中朴实无华的播放器界面部分,我想仅作为辅佐播放内核的示例,对于页面和控件的Xaml部分不会展开描述。

在解决方案管理器中,我们新建MatoMusic项目,作为UI部分。

页面

依赖包

在MatoMusic.csproj中添加对Abp,Abp.AutoMapper,Abp.Castle.Log4Net,CommunityToolkit.Maui的包依赖

<ItemGroup>
  <PackageReference Include="Abp" Version="7.4.0" />
  <PackageReference Include="Abp.AutoMapper" Version="7.4.0" />
  <PackageReference Include="Abp.Castle.Log4Net" Version="7.4.0" />
  <PackageReference Include="CommunityToolkit.Maui" Version="2.0.0" />
</ItemGroup>

CommunityToolkit.Maui.Views.Popup为系统提供弹窗页面支持

页面设计

已注册的路由页面

  • NowPlayingPage - 正在播放页面
  • QueuePage - 播放队列页面
  • MusicPage - 歌曲页面
  • ArtistPage - 艺术家页面
  • AlbumPage - 专辑页面
  • PlaylistPage - 歌单页面

以及导航或弹窗页面

  • MusicCollectionPage - 歌曲集合详情页面
  • PlaylistEntryPage - 歌单详情页面
  • PlaylistFunctionPage - 歌单功能列表页面
  • PlaylistChoosePage - 歌单选择页面

路由页面可以从侧滑菜单栏或功能列表中通过指定的Uri跳转

界面设计风格设计如下:

在这里插入图片描述

主页面

.NET MAUI Shell 通过提供大多数应用所需的基本功能来降低应用开发的复杂性,应用视觉对象层次结构导航,详情见官方文档

建立一个Shell页面MainPage.cs作为初始界面:
在页面Load完成后调用IMusicRelatedViewModel.InitAll()方法

public partial class MainPage : Shell, ITransientDependency
{
    private readonly IocManager iocManager;

    public MainPage(IocManager iocManager)
	{
		InitializeComponent();
        this.iocManager = iocManager;
        this.Init();
        Loaded += MainPage_Loaded;
    }

    private async void MainPage_Loaded(object sender, EventArgs e)
    {
        var musicRelatedViewModel = iocManager.Resolve<MusicRelatedService>();
        await musicRelatedViewModel.InitAll();
    }
}

在Xaml中定义各页面的层次结构,隐式注册的路由页面:

<FlyoutItem Route="NowPlayingPage" Title="正在播放" Icon="tab_home.png">
    <ShellContent x:Name="NowPlayingPageShellContent"/>
</FlyoutItem>
<FlyoutItem Route="QueuePage" Title="播放队列" Icon="tab_favorites.png">
    <ShellContent x:Name="QueuePageShellContent"/>
</FlyoutItem>
<FlyoutItem  Route="LibraryMainPage" Title="库" Icon="tab_map.png" >
    <Tab>
        <ShellContent  Title="歌曲" Icon="headphone.png"  x:Name="MusicPageShellContent"/>
        <ShellContent  Title="艺术家" Icon="microphone2.png"  x:Name="ArtistPageShellContent"/>
        <ShellContent  Title="专辑" Icon="cd2.png"  x:Name="AlbumPageShellContent"/>
    </Tab>
</FlyoutItem>
<FlyoutItem  Route="PlaylistPage"  Title="歌单" Icon="tab_map.png">
    <ShellContent x:Name="PlaylistPageShellContent"/>
</FlyoutItem>

后端代码中为各ShellContent指定页面对象

private void Init()
{
    var nowPlayingPage = iocManager.Resolve<NowPlayingPage>();
    var queuePage = iocManager.Resolve<QueuePage>();
    var playlistPage = iocManager.Resolve<PlaylistPage>();
    this.NowPlayingPageShellContent.Content = nowPlayingPage;
    this.QueuePageShellContent.Content = queuePage;
    this.PlaylistPageShellContent.Content = playlistPage;

    var musicPage = iocManager.Resolve<MusicPage>();
    var albumPage = iocManager.Resolve<AlbumPage>();
    var artistPage = iocManager.Resolve<ArtistPage>();

    this.MusicPageShellContent.Content = musicPage;
    this.ArtistPageShellContent.Content = artistPage;
    this.AlbumPageShellContent.Content = albumPage;
}

在App.xaml.cs中配置初始页面

public partial class App : Application
{
    private readonly AbpBootstrapper _abpBootstrapper;

    public App(AbpBootstrapper abpBootstrapper)
    {
        _abpBootstrapper = abpBootstrapper;
        InitializeComponent();
        _abpBootstrapper.Initialize();
        this.MainPage = abpBootstrapper.IocManager.Resolve(typeof(MainPage)) as MainPage;
    }
}

基础可视化元素类

其中ContentPage,ContentView,Popup分别继承于以下三个类别

ContentPageBase
ContentViewBase
PopupBase

他们包含Abp提供的本地化,对象映射,设置等服务,类图如下

在这里插入图片描述

ContentPage和ContentViewBase包含曲目管理器IMusicInfoManager和播放控制服务IMusicControlService,类图如下
在这里插入图片描述

导航

NavigationService,封装了初始页面的INavigation对象和导航方法

支持:

  • 路由方式的导航 - Shell 视觉层次结构中隐式注册的路由。
  • 页面导航 - 模式导航页面可以从应用的任何位置推送到堆叠导航。

PushAsync或PushModalAsync可以按文件名的页面导航

public async Task PushAsync(string pageName, object[] args = null)
 {
     var page = GetPageInstance(pageName, args);
     await mainPageNavigation.PushAsync(page);
 }
 
public async Task PushModalAsync(string pageName, object[] args = null)
{
    var page = GetPageInstance(pageName, args);
    await mainPageNavigation.PushModalAsync(page);
}

GetPageInstance通过反射的方式创建页面对象
传入对象名称,参数和工具栏项目对象,返回页面对象

private Page GetPageInstance(string obj, object[] args, IList<ToolbarItem> barItem = null)
{
    Page result = null;
    var namespacestr = "MatoMusic";
    Type pageType = Type.GetType(namespacestr + "." + obj, false);
    if (pageType != null)
    {
        try
        {
            var ctorInfo = pageType.GetConstructors()
                                  .Select(m => new
                                  {
                                      Method = m,
                                      Params = m.GetParameters(),
                                  }).Where(c => c.Params.Length == args.Length)
                                  .FirstOrDefault();
            if (ctorInfo==null)
            {
                throw new Exception("找不到对应的构造函数");
            }

            var argsDict = new Arguments();

            for (int i = 0; i < ctorInfo.Params.Length; i++)
            {
                var arg = ctorInfo.Params[i];
                argsDict.Add(arg.Name, args[i]);
            }

            var pageObj = iocManager.IocContainer.Resolve(pageType, argsDict) as Page;

            if (barItem != null && barItem.Count > 0)
            {
                foreach (var toolbarItem in barItem)
                {
                    pageObj.ToolbarItems.Add(toolbarItem);
                }
            }
            result = pageObj;
        }
        catch (Exception e)
        {
            Debug.WriteLine(e.Message);
        }
    }
    return result;
}

其中,弹窗的打开和关闭由扩展类CommunityToolkit.Maui.Views.PopupExtensions提供方法

页面资源

NET MAUI 单一项目使资源文件可以存储在统一位置上(一般是Resources文件夹下),为跨平台方案使用。详情见官方文档
将在Fonts添加FontAwesome字体文件,以及Images中添加图标png文件
在这里插入图片描述
MatoMusic.csproj文件中,对资源范围进行限定,此时的限定范围是Resources\Fonts\*和Resources\Images\*

<ItemGroup>
	<!-- App Icon -->
	<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />

	<!-- Splash Screen -->
	<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />

	<!-- Images -->
	<MauiImage Include="Resources\Images\*" />
	<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />

	<!-- Custom Fonts -->
	<MauiFont Include="Resources\Fonts\*" />

	<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
	<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

样式和主题

在移动端应用配色设计上,不同的应用应该有其独特的设计风格,但因遵循配色原理。

基本配色要求有:应该保持主色统一,使主题鲜明突出;前景、背景色反差强烈使内容更容易阅读;前景、背景有相应辅助色使界面灵动不古板。

因此系统样式应该包含:

  • PhoneForegroundBrush - 前景色
  • PhoneContrastForegroundBrush - 辅前景色
  • PhoneBackgroundBrush - 背景色
  • PhoneContrastBackgroundBrush - 辅背景色
  • PhoneAccentBrush - 主色(亮色)
  • PhoneChromeBrush - 暗色

DarkTheme.xaml暗色主题配置

    <Color x:Key="PhoneBackgroundBrush">#181818</Color>
    <Color x:Key="PhoneForegroundBrush">White</Color>
    <Color x:Key="PhoneContrastBackgroundBrush">#222326</Color>
    <Color x:Key="PhoneContrastForegroundBrush">#DFD8F7</Color>
    <Color x:Key="PhoneAccentBrush">Teal</Color>
    <Color x:Key="PhoneChromeBrush">#A5A5A5</Color>

LightTheme.xaml亮色主题配置

    <Color x:Key="PhoneBackgroundBrush">White</Color>
    <Color x:Key="PhoneForegroundBrush">#181818</Color>
    <Color x:Key="PhoneContrastBackgroundBrush">#DFD8F7</Color>
    <Color x:Key="PhoneContrastForegroundBrush">#828386</Color>
    <Color x:Key="PhoneAccentBrush">Teal</Color>
    <Color x:Key="PhoneChromeBrush">#A5A5A5</Color>

CommonResourceDictionary.xaml中定义通用的控件样式,如Label和Button控件,部分的定义如下

Label全局样式

<Style TargetType="Label">
    <Setter Property="TextColor" Value="{DynamicResource PhoneForegroundBrush}" />
    <Setter Property="FontFamily" Value="OpenSansRegular" />
</Style>

Button全局样式以及特定样式


<Style TargetType="Button">
    <Setter Property="CornerRadius" Value="8"/>
    <Setter Property="TextColor" Value="{DynamicResource PhoneContrastForegroundBrush}" />
    <Setter Property="FontFamily" Value="OpenSansRegular" />
    <Setter Property="BackgroundColor" Value="{DynamicResource PhoneContrastBackgroundBrush}" />
    <Setter Property="Padding" Value="14,10" />
    <Setter Property="Margin" Value="5,0" />

</Style>
<Style TargetType="Button" x:Key="PrimaryButton">
    <Setter Property="CornerRadius" Value="8"/>
    <Setter Property="BackgroundColor" Value="Transparent"/>
    <Setter Property="TextColor" Value="{DynamicResource PhoneContrastForegroundBrush}" />
    <Setter Property="FontFamily" Value="OpenSansRegular" />
    <Setter Property="BackgroundColor" Value="{DynamicResource PhoneAccentBrush}" />
    <Setter Property="Padding" Value="14,10" />
    <Setter Property="Margin" Value="5,0" />
</Style>

<Style TargetType="Button" x:Key="TextButton">
    <Setter Property="BackgroundColor" Value="Transparent"/>
    <Setter Property="TextColor" Value="{DynamicResource PhoneContrastForegroundBrush}" />
    <Setter Property="FontFamily" Value="OpenSansRegular" />
    <Setter Property="BorderWidth" Value="0"/>
    <Setter Property="Padding" Value="14,10" />
    <Setter Property="Margin" Value="5,0" />
</Style>

<Style TargetType="Button" x:Key="PrimaryButtonOutline">
    <Setter Property="CornerRadius" Value="8"/>
    <Setter Property="BackgroundColor" Value="Transparent"/>
    <Setter Property="TextColor" Value="{DynamicResource PhoneContrastForegroundBrush}" />
    <Setter Property="FontFamily" Value="OpenSansRegular" />
    <Setter Property="BorderWidth" Value="1"/>
    <Setter Property="BorderColor" Value="{DynamicResource PhoneAccentBrush}"/>
    <Setter Property="Padding" Value="14,10" />
    <Setter Property="Margin" Value="5,0" />
</Style>

App.xaml中将主题和通用样式囊括到资源字典中

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <style:DarkTheme />
            <!--<style:LightTheme />-->
            <style:CommonResourceDictionary />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

字体

配置
在MauiProgram.cs中,CreateMauiApp里将FontAwesome字体加入配置

public static MauiApp CreateMauiApp()
{
	var builder = MauiApp.CreateBuilder();
	builder
		.UseMatoMusic<MatoMusicModule>()
		.UseMauiApp<App>()
		.UseMauiCommunityToolkit()
		.ConfigureFonts(fonts =>
		{
			fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
			fonts.AddFont("FontAwesome.ttf", "FontAwesome");
		});
	return builder.Build();
}

在Xaml中使用:在带有文字属性的标签中,如<Button>或<Label>,设置FontFamily属性为FontAwesome,设置Text属性为FontAwesome字符内容,此值可使用“字符映射表”工具查找。

<Button Text="" FontFamily="FontAwesome"></Button>

在本地计算机中安装好FontAwesome字体后,打开“字符映射表”工具,选择字体FontAwesome,点选后可以从下面的输入框中复制内容
在这里插入图片描述
在这里插入图片描述

本地化

使用Abp提供的本地化方案

在MatoMusic.Core项目的MatoMusicCoreModule.cs中

public class MatoMusicCoreModule : AbpModule
{
	public override void PreInitialize()
	{
		LocalizationConfigurer.Configure(Configuration.Localization);
	}
    ...
}
      

Localization/MatoMusicLocalization.cs中,将提供基于Xml的本地化的语言字典配置,从MatoMusic.Core.Localization.SourceFiles资源中访问字典:

public static void Configure(ILocalizationConfiguration localizationConfiguration)
{
    localizationConfiguration.Sources.Add(
        new DictionaryBasedLocalizationSource(MatoMusicConsts.LocalizationSourceName,
            new XmlEmbeddedFileLocalizationDictionaryProvider(
                typeof(LocalizationConfigurer).GetAssembly(),
                "MatoMusic.Core.Localization.SourceFiles"
            )
        )
    );
}

在这些文件的编译模式应为嵌入的资源
在这里插入图片描述
基础可视化元素类中提供L方法,返回本地化字符串

protected virtual string L(string name)
{
    return LocalizationSource.GetString(name);
}

TranslateExtension实现IMarkupExtension,MarkupLanguage的本质,是实例化一个对象。Xaml编译器,会调用标记扩展对象的ProvideValue方法,并将返回值赋值给使用了标记扩展的属性,ProvideValue中调用L方法完成翻译

[ContentProperty("Text")]
public class TranslateExtension : DomainService, IMarkupExtension
{
    public TranslateExtension()
    {
        LocalizationSourceName = MatoMusicConsts.LocalizationSourceName;

    }
    public string Text { get; set; }

    public object ProvideValue(IServiceProvider serviceProvider)
    {
        Console.WriteLine(CultureInfo.CurrentUICulture);
        if (Text == null)
            return "";
        var translation = L(Text);
        return translation;
    }
}

在Xaml中使用:在带有文字属性的标签中,如<Button>或<Label>,Text属性的值将转换为本地化字符串值。

ViewModel

Model-View-ViewModel (MVVM) 设计模式是在称为视图的 Xaml 用户界面和基础数据(称为模型)之间的一个软件层,称之为视图模型,即ViewModel,

界面视图和ViewModel通过Xaml中定义的数据绑定进行连接。 在视图类的构造函数中,我们以注入的方式将ViewModel导入视图,并赋值给BindingContext属性,例如在NowPlayingPage.cs中:

public NowPlayingPage(NowPlayingPageViewModel nowPlayingPageViewModel)
{
	InitializeComponent();
	this.BindingContext = nowPlayingPageViewModel;
	...
}

音乐相关服务类

前一章介绍了播放核心的两个类曲目管理器IMusicInfoManager和播放控制服务IMusicControlService,界面交互对象中对这两个类进行了应用。

MusicRelatedService是播放控制服务的一层封装,它基于ViewModelBase。

抽象的来说,音乐相关服MusicRelatedService包含一系列可绑定的属性,自动维护属性值,并在设置属性值时调用放控制服务完成业务变更操作。数据作为界面交互的支撑。

主要属性:

  • NextMusic - 下一首曲目
  • PreviewMusic - 上一首曲目

NextMusic和PreviewMusic用于绑定首页的上一曲、下一曲专辑封面。

  • CurrentMusic - 当前曲目:正在播放的曲目,所有的音乐相关操作的对象都是当前曲目CurrentMusic。

CurrentMusic可在界面提供双向绑定支持,当其值变更时,代表切换播放歌曲。

  1. 调用IMmusicControlService.InitPlayer,设置播放器曲目
  2. 更新当前曲目的长度
  3. 更新上一首、下一首曲目
  4. 更新BreakPointMusicIndex值,并用SettingManager持久化当前曲目的角标编号

代码实现如下:

if (e.PropertyName == nameof(CurrentMusic))
{
    if (!Canplay || IsInited == false)
    {
        return;

    }
    await musicControlService.InitPlayer(CurrentMusic);
    DoUpdate();
    InitPreviewAndNextMusic();
    Duration = GetPlatformSpecificTime(musicControlService.Duration());
    SettingManager.ChangeSettingForApplication(CommonSettingNames.BreakPointMusicIndex, Musics.IndexOf(CurrentMusic).ToString());

}
  • Musics - 当前播放队列:可供播放的有序曲目集合,是自然播放、上一曲、下一曲、随机播放的范围。

  • Canplay - 表明当前曲目是否可供播放

实现如下:

public bool Canplay => this.CurrentMusic != null;
  • CanplayAll - 指示是否可以播放全部曲目,当当前播放队列为空时,界面将显示向导

实现如下:

public bool CanplayAll => Musics.Count > 0;
  • IsPlaying 表明是否正在播放

它的值变更由IMusicControlService.OnPlayStatusChanged事件触发,以实现自动维护属性值:

musicControlService.OnPlayStatusChanged+=MusicControlService_OnPlayStatusChanged;

private void MusicControlService_OnPlayStatusChanged(object sender, bool e)
{
    this.IsPlaying = e;
}
  • Duration - 指示当前曲目时长
  • CurrentTime - 指示当前曲目播放进度

这两个属性由一个自动定时器,每隔一段时间自动触发DoUpdate方法,以实现自动维护属性值:

public bool DoUpdate()
{
    this.CurrentTime = GetPlatformSpecificTime(musicControlService.CurrentTime());
    this.Duration = GetPlatformSpecificTime(musicControlService.Duration());

    return true;
}
  • IsShuffle - 指示随机播放模式,是否为随机播放
  • IsRepeatOne - 指示单曲循环模式,是否为单曲循环

这两个属性由SettingManager持久化其值。

  • IsInited - 指示是否完成初始化服务

主要方法:

  • InitCurrentMusic - 初始化当前曲目CurrentMusic

根据当前曲目的角标编号BreakPointMusicIndex值,从曲目库中获取当前曲目对象,并赋值给CurrentMusic。

  • InitAll - 初始化服务,用于系统启动后的一次性调用

调用IMusicControlService.RebuildMusicInfos从播放列队中读取音频列表,完成后触发OnBuildMusicInfosFinished事件,触发InitCurrentMusic。

音乐相关ViewModel类

MusicRelatedViewModel是一个抽象类,基于ViewModelBase,包含MusicRelatedService,以及曲目管理器IMusicInfoManager和播放控制服务IMusicControlService对象,其子类可直接用于界面UI元素的绑定。

MusicRelatedViewModel的子类中,MusicRelatedService对象不需要在构造函数中注入,它将在访问器中初始化。

public MusicRelatedService MusicRelatedService
{
    get
    {
        if (_musicRelatedService==null)
        {
            _musicRelatedService = IocManager.Instance.Resolve<MusicRelatedService>();
            _musicRelatedService.PropertyChanged += this.Delegate_PropertyChanged;
        }
        return _musicRelatedService;
    }

}

并且在MusicRelatedViewModel的子类中,实现了对MusicRelatedService对象属性变更的事件冒泡:

private void Delegate_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    this.RaisePropertyChanged(e.PropertyName);
}

主要属性:

  • NextMusic - 下一首曲目

  • PreviewMusic - 上一首曲目

  • CurrentMusic - 当前曲目:正在播放的曲目,所有的音乐相关操作的对象都是当前曲目CurrentMusic。

  • Musics - 当前播放队列:可供播放的有序曲目集合,是自然播放、上一曲、下一曲、随机播放的范围。

  • Canplay - 表明当前曲目是否可供播放

  • CanplayAll - 指示是否可以播放全部曲目,当当前播放队列为空时,界面将显示向导

  • IsPlaying 表明是否正在播放

  • Duration - 指示当前曲目时长

  • CurrentTime - 指示当前曲目播放进度

  • IsShuffle - 指示随机播放模式,是否为随机播放

  • IsRepeatOne - 指示单曲循环模式,是否为单曲循环

  • PlayCommand - 播放/暂停命令

  • PreCommand - 上一曲命令

  • NextCommand - 下一曲命令

  • ShuffleCommand - 切换随机模式命令

  • RepeatOneCommand - 切换单曲循环命令

  • FavouriteCommand - 设置/取消设置“我最喜爱”

数据绑定

在NowPlayingPage中,对当前播放的曲目,以及上一首、下一首的曲目信息进行显示。
对于当前曲目的长度和进度,进行显示和控制,对播放的曲目进行控制等内容:

<Grid Grid.Column="1">
    <Image HorizontalOptions="Fill"
           x:Name="PreAlbumArt"
           HeightRequest="{Binding Source={x:Reference Name=RefBox}, Path=Height}"
           WidthRequest="{Binding Source={x:Reference Name=RefBox}, Path=Width}"
           VerticalOptions="Fill"
           TranslationX="-320"
           Source="{Binding PreviewMusic.AlbumArt,Converter={StaticResource AlbumArtConverter}}">
    </Image>

    <Image 
        HeightRequest="{Binding Source={x:Reference Name=RefBox}, Path=Height}"
        WidthRequest="{Binding Source={x:Reference Name=RefBox}, Path=Width}"
        HorizontalOptions="Fill"
        VerticalOptions="Fill"
        Source="{Binding CurrentMusic.AlbumArt,Converter={StaticResource AlbumArtConverter}}">
        <Image.GestureRecognizers>
            <TapGestureRecognizer Command="{Binding SwitchPannelCommand}"></TapGestureRecognizer>
        </Image.GestureRecognizers>
    </Image>

    <Image 
         x:Name="NextAlbumArt"
         HeightRequest="{Binding Source={x:Reference Name=RefBox}, Path=Height}"
         WidthRequest="{Binding Source={x:Reference Name=RefBox}, Path=Width}"
         HorizontalOptions="Fill"
         VerticalOptions="Fill"
         TranslationX="320"                           
         Source="{Binding NextMusic.AlbumArt,Converter={StaticResource AlbumArtConverter}}">
    </Image>
</Grid>

对曲目名称,艺术家的绑定:

 <StackLayout Grid.Column="1" HorizontalOptions="Center">
     <Label Text="{Binding CurrentMusic.Title}" 
                    HorizontalOptions="FillAndExpand" 
                    HorizontalTextAlignment="Center" 
                    FontSize="{StaticResource BodyFontSize}" 
                    TextColor="White" />
     <Label Margin="0,-5,0,0" 
                    Text="{Binding CurrentMusic.Artist}" 
                LineBreakMode="TailTruncation"
                    HorizontalOptions="FillAndExpand" 
                    HorizontalTextAlignment="Center" 
                    FontSize="{StaticResource BodyFontSize}" 
                    TextColor="{DynamicResource PhoneChromeBrush}" />
 </StackLayout>

界面效果如下:
在这里插入图片描述
小窗播放控件MusicMiniView也对曲目信息进行了相似的绑定
在这里插入图片描述
进度控制区域代码:

<!--进度控制区域-->
<Grid Grid.Row="2"
              x:Name="ProgressControlLayout"
              BindingContext="{Binding}">

    <StackLayout Margin="0,0,0,0" Orientation="Horizontal">
        <Label Text="{Binding CurrentTime,Converter={StaticResource SecondsToTimeSpanConverter}}" 
                       TextColor="{DynamicResource PhoneChromeBrush}" 
                       FontSize="{StaticResource TinyFontSize}" 
                       HorizontalOptions="StartAndExpand" />
        <Label  Text="{Binding Duration,Converter={StaticResource SecondsToTimeSpanConverter}}" 
                        TextColor="{DynamicResource PhoneChromeBrush}" 
                        FontSize="{StaticResource TinyFontSize}" 
                        HorizontalOptions="End" />
    </StackLayout>
    <Slider                       
        Maximum="{Binding Duration,Converter={StaticResource SliderMaxValueConverter}}"
        Minimum="0.0"              
        MinimumTrackColor="{DynamicResource PhoneAccentBrush}"
        IsEnabled="{Binding Canplay}"
        ValueChanged="OnValueChanged"
        Value="{Binding CurrentTime,Mode=TwoWay} ">
    </Slider>
</Grid>

播放控制区域代码:

<!--播放控制按钮-->
<Grid
    Grid.Row="3"
    BindingContext="{Binding}"
    x:Name="PlayControlLayout">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*"></ColumnDefinition>
        <ColumnDefinition Width="Auto"></ColumnDefinition>
        <ColumnDefinition Width="1*"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Button VerticalOptions="Center"
            HorizontalOptions="StartAndExpand"
            Grid.Column="0"
            Command="{Binding ShuffleCommand}" 
            FontFamily="FontAwesome"
            FontSize="{StaticResource BodyFontSize}"
            Text="{Binding IsShuffle,Converter={StaticResource Bool2StringConverter},ConverterParameter=|}"/>
    <Grid Grid.Column="1"  HorizontalOptions="Center" WidthRequest="216">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"></ColumnDefinition>
            <ColumnDefinition Width="2*"></ColumnDefinition>
            <ColumnDefinition Width="1*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Button 
            
            VerticalOptions="Center" 
            Grid.Column="0"
            Command="{Binding PreCommand}" 
            FontFamily="FontAwesome"
            FontSize="{StaticResource BodyFontSize}"
            Text=""
            />
        <Button 
            VerticalOptions="Center"  
            Grid.Column="1"
            Command="{Binding PlayCommand}" 
            Style="{StaticResource PrimaryButton}"

            FontFamily="FontAwesome"
            FontSize="{StaticResource StandOutBodyFontSize}"
            Text="{Binding IsPlaying,Converter={StaticResource Bool2StringConverter},ConverterParameter=|} "/>

        <Button 
            VerticalOptions="Center"       
            Grid.Column="2"
            Command="{Binding NextCommand}"
            FontFamily="FontAwesome"
            FontSize="{StaticResource BodyFontSize}"
            Text=""/>
    </Grid>
    <Button VerticalOptions="Center" 
                    HorizontalOptions="EndAndExpand"
                    Grid.Column="2"
                    Command="{Binding RepeatOneCommand}" 
                    FontFamily="FontAwesome"
                    FontSize="{StaticResource BodyFontSize}"
                    Text="{Binding IsRepeatOne,Converter={StaticResource Bool2StringConverter},ConverterParameter=|}" />
</Grid>

列表分组显示

下列三个页面使用ListView控件呈现曲目,专辑,艺术家的可滚动垂直列表

  • MusicPage - 歌曲页面
  • ArtistPage - 艺术家页面
  • AlbumPage - 专辑页面

通过将 设置 ListView.GroupHeaderTemplateDataTemplate来自定义每个组标头的外观

在MusicGroupHeaderView.xaml中,定义分组标头的外观,由一个标题和亮色方形背景组成

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MatoMusic.MusicGroupHeaderView">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto">
            </ColumnDefinition>
            <ColumnDefinition>
            </ColumnDefinition>
        </Grid.ColumnDefinitions>
        <BoxView Color="{DynamicResource PhoneAccentBrush}"
                     Margin="0,5"
                     WidthRequest="54">
        </BoxView>
        <Label VerticalTextAlignment="Center"
               HorizontalTextAlignment="Center"
               Text="{Binding Title}"
               FontAttributes="Bold"
               TextColor="{DynamicResource PhoneForegroundBrush}"
               FontSize="{StaticResource BodyFontSize}">
        </Label>
    </Grid>

</ContentView>

在页面控件中,IsGroupingEnabled设置为true,并指定GroupHeaderTemplate属性为MusicGroupHeaderView

<ListView
    IsGroupingEnabled="true"
    <ListView.GroupHeaderTemplate>
        <DataTemplate>
            <ViewCell>
                <mato:MusicGroupHeaderView></mato:MusicGroupHeaderView>
            </ViewCell>
        </DataTemplate>
    </ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>

界面效果如下
在这里插入图片描述

项目地址

GitHub:MatoMusic

学习笔记(Maui 02 Sqlite) 对应原视频 P3-P6 阅读详情

相关推荐

MAUI技术调研1——界面设计

技术调研

qq_53373885的博客 1504

Net Maui安卓小程序如何修改程序图标APP ICON、名称及Splash Screen

Net Maui安卓小程序如何修改程序图标APP ICON、名称及Splash Screen

KensouBP的博客 2493

[MAUI 项目实战] 手势控制音乐播放器: 手势交互

pit很简单,是一个包含了名称属性的控件,这个名称属性是用来标识pit的。定义一个拖拽物,和它拖拽的目标,拖拽物可以理解为一个平底锅(pan),拖拽目标是一个坑(pit),当拖拽物进入坑时,拖拽物就会被吸附在坑里。这里用Grid作为pit控件基类型,因为Grid可以包含子控件,我们可以在pit控件中添加子控件,比如一个图片,一个文字,这样就可以让pit控件更加丰富。,这个AbsoluteLayout类型的容器控件,内包含一系列控件作为pit,这些pit集合将作为平移手势容器的判断依据。

supeerzdj的博客 388

maui界面布局

xaml 布局控件,Grid 网格布局

转眼之间 一路有你 3226

.NET MAUI与Jetpack Media3的音乐播放器实践

Jetpack Media3是Android平台上最新的媒体播放框架,提供了比ExoPlayer2更丰富的API和更好的系统集成。通知栏媒体控制:包括在快速设置中显示播放控制。系统事件响应:如响应系统的播放恢复事件。自定义通知:更灵活的通知样式定制。跨设备媒体信息共享:让汽车音响系统、智能手表等设备能够读取播放信息。通过.NET MAUI与Jetpack Media3的结合,我们可以创建一个功能丰富且与Android系统深度集成的音乐播放器。

m0_62153576的博客 153

描边盛开火玫瑰

python描边盛开火玫瑰 代码如下 import turtle as t t.setup(800,800) t.hideturtle() t.speed(11) t.penup() t.goto(50,-450) t.pensize(5) t.pencolor("black") t.seth(140) t.pendown() t.speed(10) t.circle(-300,60) t.fd(100) #jiaodu80 #1ye t.seth(10) t.fd(50) t.fillcolor("gr

weixin_50973371的博客 2065

[MAUI 项目实战] 音乐播放器(一):概述与架构

为什么想起来这个项目了呢? 这是一个Windows Phone 8的老项目,2014年用作为兴趣写了个叫“番茄播放器”的App,顺便提高编程技能。 这个项目的架构历经多次迁移,从WP8到UWP再到Xamarin.Forms。去年底随着MAUI的正式发布,又尝试把它迁移到MAUI上来。

jevonsflash的专栏 5828

Qwen3-VL、Qwen3-Next与Qwen3.5多模态架构深度解剖

多模态大模型的核心不在参数规模,而在于视觉与文本如何协同建模。本文从基础概念切入,解析视觉语言模型中视觉编码器、跨模态注意力、特征对齐与token压缩等关键原理;阐述ViT、CLIP-ViT及动态压缩模块(HDTC)在语义表征、空间感知与计算效率上的技术权衡;强调结构设计如何决定模型在医学影像、工业质检、遥感分析等高要求场景中的真实能力边界;最终聚焦通义千问最新三套VL架构——Qwen3-VL、Qwen3-Next与Qwen3.5——揭示其背后迥异的融合哲学与工程取舍,为选型、微调与推理优化提供底层依据。

weixin_33778544的博客 818

[MAUI 项目实战] 手势控制音乐播放器(一): 概述与架构

这是一篇系列博文。请关注我,学习更多.NET MAUI开发知识!在之前的博文中提到这个项目,它是为音乐播放器专门开发的基于手势控制的UI界面。此UI界面可以让用户在不看屏幕的情况下,通过手势来控制音乐播放器的各种操作,如播放、暂停、下一首、上一首。手势来控制的交互方式适合不方便看手机屏幕时简单的音乐播放需求,在驾车、运动等场景下有较好的用户体验。

jevonsflash的专栏 1358

3D高斯溅射融合Gabor滤波器:频率感知如何提升神经渲染效率与质量

在计算机视觉与图形学领域,3D场景重建旨在从多视角图像中恢复真实世界的三维结构与外观。其核心原理是通过优化场景表示,使渲染出的二维图像与输入照片尽可能一致。传统方法在效率与细节保真度间常面临权衡,高频细节(如精细纹理、锐利边缘)的重建尤为消耗算力。频率感知技术为此提供了突破方向,它借鉴信号处理思想,将场景分解为不同空间频率的成分进行针对性建模。Gabor滤波器作为经典的频率-方向分析工具,能有效刻画纹理的局部频域特征。将Gabor的频率选择性与3D高斯溅射(3DGS)这一高效显式表示结合,构成了神经Gabo

weixin_33087827的博客 272

Vue3项目实战:集成h265webjs打造高性能Web端H.265播放器

本文详细介绍了如何在Vue3项目中集成h265webjs库,构建高性能的Web端H.265播放器。通过封装播放器管理模块、利用Vue3组合式API优雅管理生命周期,并提供了从环境准备、核心集成到进阶优化的完整实战指南,有效解决了浏览器原生不支持H.265解码的难题,显著降低视频流带宽占用。

weixin_29016833的博客 364

[MAUI 项目实战] 笔记App(一):介绍与程序设计

有人说现在记事类app这么多,市场这么卷,为什么还想做一个笔记类App?一来,去年小孩刚出生,需要一个可以记录喂奶时间的app,发现市面上没有一款app能够在两步内简单记录一个时间,可能iOS可以通过备忘录配合捷径做到快速记录,但是安卓上就没有类似的app。二是,自去年做的音乐播放器以来,很长一段时间我在博客上的XF,MAUI都是在介绍局部的功能,[MAUI 项目实战]专题也很长没更新了,这次通过笔记类App做一次完整项目,包括如何上架MAUI应用等内容一并更新了。

jevonsflash的专栏 1786

【Docker】搭建一个媒体服务器插件后端API服务 - MetaTube

本教程基于群晖的NAS设备DS423+的docker功能进行搭建,DSM版为 7.2.1-69057 Update 5。MetaTube是一个媒体服务器插件,主要用于Emby和Jellyfin媒体服务器。它的主要功能是从互联网上抓取内容并响应 JSON 数据,然后通过预安装的 MetaTube 插件请求服务器并下载相应的元数据,以与 Jellyfin/Emby 服务器配合使用。使用 MetaTube 的步骤大致如下:部署后端:首先,需要部署一个名为​ 的后端 API 服务器来抓取内容。

小小笔记大大用处 1万+

[MAUI 项目实战] 手势控制音乐播放器(二): 手势交互

pit很简单,是一个包含了名称属性的控件,这个名称属性是用来标识pit的。定义一个拖拽物,和它拖拽的目标,拖拽物可以理解为一个平底锅(pan),拖拽目标是一个坑(pit),当拖拽物进入坑时,拖拽物就会被吸附在坑里。这里用Grid作为pit控件基类型,因为Grid可以包含子控件,我们可以在pit控件中添加子控件,比如一个图片,一个文字,这样就可以让pit控件更加丰富。,这个AbsoluteLayout类型的容器控件,内包含一系列控件作为pit,这些pit集合将作为平移手势容器的判断依据。

jevonsflash的专栏 791

微信H5网页授权通用方案:中转页破解域名限制,适配Hash/History路由

在Web开发中,OAuth 2.0授权码模式是实现第三方登录的通用标准,其核心原理是通过授权服务器颁发临时代码,再由客户端交换访问令牌。这一机制在微信公众号H5开发中尤为重要,用于安全获取用户身份。然而,微信平台对网页授权回调域名有严格的数量限制,这成为多子域名、多环境及微前端架构下业务扩展的主要瓶颈。其技术价值在于通过设计一个统一的中转授权页,将授权逻辑与业务逻辑解耦,让所有业务页面共享一个已配置的授权入口,从而突破域名配额限制。该方案的关键在于适配不同的前端路由模式,需精准处理Hash模式与Histor

weixin_42533120的博客 318

YOLOv12改进目录一览 | 涉及卷积层、轻量化、注意力、损失函数、Backbone、SPPF、Neck、检测头等全方位改进

1️⃣ 什么!不知道如何改进模型⁉️ 本专栏所使用并改进的每一个模块均包含详细的模块分析、原理讲解、个人总结、多种改进方式以及完整的修改流程,所有改进100%可直接运行,性价比极高。2️⃣ 找不到合适的模块⁉️ 所有改进点均为近三年顶会,顶刊提出的先进算法,将其融入到中,并进行二次创新,新颖度高,创新度高,能够适应不同的任务场景。3️⃣ 不确定自己改进的步骤、结果是否正确⁉️ 订阅专栏即可进群享受模型训练、模型改进、论文写作、投稿选刊,从入门到论文的各种答疑内容,非常适合新手。4️⃣ 团队内发表数篇SCI论

Limiiiing的博客 2万+

[MAUI 项目实战] 手势控制音乐播放器(三): 动画

我们需要一个拉扯回弹的效果,可以通过自定义缓动函数实现。我用python拟合了一个适合拖拽物回弹的曲线。模拟一种弹性拉扯的效果。

jevonsflash的专栏 1195

大模型技术全景与核心概念解析:从基础原理到AI智能体架构(实时更新中)

本文系统解析了大模型技术的核心概念与架构。首先介绍了LLM(大语言模型)、LLMOps(模型运维)、AIGC(生成内容)、AGI(通用AI)等基础概念,重点阐述了大模型的工作原理,包括预处理、推理和后处理三个阶段。深入讲解了Token分词、FLOPs计算、Embedding向量表示等关键技术,并分析了LoRA微调、MoE架构等前沿方法。文章还探讨了AI智能体的发展前景,为理解大模型技术提供了全景视角。

算法工程师 1240

[MAUI 项目实战] 手势控制音乐播放器(四):圆形进度条

在项目中添加SkiaSharp绘制功能的引用以及。

jevonsflash的专栏 2283

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

Maui Shell 来了,开启 Linux 桌面新时代!

公众号关注“GitHubDaily”设为 “星标”,每天带你逛 GitHub!大家好,我是小 G。开篇之前,先跟大家道声元旦快乐新年伊始,跟各位分享一款近期在 GitHub 上比较重磅的...

GitHubDaily 7084

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

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

由此可以无限的拨动唱盘实现连续切歌的效果。用当前唱盘的“瞬移”,看起来像唱盘被影子唱盘替换掉了,但是在屏幕中心活动的拖拽物,一直是真正的那个控件。结合上一小节写的三个PitGrid,此时拖拽唱盘,并且在拖拽开始,进入pit,离开pit,释放时,分别触发Start,In,Out,Over四个状态事件。此时已经实现了拖拽唱盘的基本功能,但是在释放唱盘时,影子唱盘并没有如预期那样移动到MiddlePit的中心点。注意,当音乐暂停后,停止旋转动画,当音乐恢复播放时,转盘应从之前停止的角度开始启动旋转动画。

jevonsflash的专栏 1102

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

【亲测免费】 Plugin.Maui.Audio 使用教程

Plugin.Maui.Audio 使用教程 项目介绍 Plugin.Maui.Audio 是一个用于 .NET MAUI 平台的开源音频播放插件。它允许开发者在他们的 MAUI 应用中轻松集成音频播放功能。该项目由 jfversluis 维护,旨在提供一个简单且高效的解决方案,以满足移动应用中对音频处理的需求。 项目快速启动 安装插件 首先,你需要在你的 MAUI 项目中安装 Plugin.Ma...

gitblog_00884的博客 1249

[MAUI 项目实战] 音乐播放器(二):播放内核

曲目排序,原理是通过交换位置实现的,iOS和Android平台都有自己的可排序列表控件,在对选中的条目进行排序(往往是提起条目-拖拽-释放)的过程中,触发事件往往提供当前条目。在传统播放器随机播放时,如果下一曲不是我想听的,我仍然想听上一曲,由于上一曲按钮是随机触发的时机,你可能找不到它了,不得不再音乐列表再搜索它。播放控制类,用于当前平台播放器对象的操作,对当前所播放曲目的暂停/播放,下一首/上一首,快进快退(寻迹),随机、单曲模式等功能的控制。同样,用到了排序逻辑,再将他的排序(

jevonsflash的专栏 2329

Apache Pulsar 集群管理实战:pulsar-admin、REST API 与 Java Admin API 全面指南

本文基于 Apache Pulsar 官方文档《Managing Clusters》,系统讲解如何通过三种管理接口(`pulsar-admin` 命令行工具、`/admin/v2/clusters` REST API、`PulsarAdmin` Java API)完成集群的创建(Provision)、元数据初始化、配置查看、更新、删除、列表查询与对等集群(Peer Cluster)配置。读完本文,

gitblog_00378的博客 277

MAUI跨平台托盘图标解决方案:H.NotifyIcon实现Android/iOS/Windows统一体验

H.NotifyIcon是一款专为MAUI框架设计的跨平台托盘图标组件,能够帮助开发者在Android、iOS和Windows系统上实现统一的系统托盘图标体验。通过单一API接口,开发者可以轻松管理应用在系统通知区域的图标显示、交互和功能扩展,有效解决多平台开发中的托盘图标适配难题。 ## 为什么选择H.NotifyIcon? 在跨平台应用开发中,系统托盘图标往往是最容易被忽视却又至关重要的功

gitblog_00704的博客 372
上一篇: [学习笔记]Rocket.Chat业务数据备份
下一篇: 集成RocketChat至现有的.Net项目中,为ChatGPT铺路
林晓lx
林晓lx 2017年优快云移动跨平台大赛优秀奖获得者 2017年优快云移动跨平台大赛优秀奖获得者
博客等级 码龄15年 3761粉丝 123原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林晓lx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值