深入解读.NET MAUI音乐播放器项目(三):界面交互
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可在界面提供双向绑定支持,当其值变更时,代表切换播放歌曲。
- 调用IMmusicControlService.InitPlayer,设置播放器曲目
- 更新当前曲目的长度
- 更新上一首、下一首曲目
- 更新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>
界面效果如下
项目地址
深入解读.NET MAUI音乐播放器项目(三):界面交互的更多相关文章
- swift 音乐播放器项目-《lxy的杰伦情歌》开发实战演练
近期准备将项目转化为OC与swift混合开发.试着写一个swift音乐播放器的demo,体会到了swift相对OC的优势所在.废话不多说.先上效果图: watermark/2/text/aHR0cDo ...
- android调用音乐播放器,三种方
小弟想请问一下.怎样在自己写的程序中调用系统的音乐播放器呢. 我在google上搜索了.主要是有两种方法,可是都不是我想要的. 第一种是.使用mp3音乐文件的uri.和intent,进行调用.可是这样 ...
- 用Vue来实现音乐播放器(三十八):歌词滚动列表的问题
1.频繁切换歌曲时,歌词会跳来跳去 原因: // 歌词跳跃是因为内部有一个currentLyric对像内部有一些功能来完成歌词的跳跃 //每个currentLyric能实现歌曲的播放跳到相应的位置 是 ...
- Vue音乐播放器(三):项目目录介绍,以及图标字体、公共样式等资源准备
我们所有的开发都是基于修改src下面的目录 里面的文件去做开发即可 stylus的使用是需要下载stylus-loader的包的 渲染效果 配置修改eslintrc.js 配置了webpack.bas ...
- Android(java)学习笔记234: 服务(service)之音乐播放器
1.我们播放音乐,希望在后台长期运行,不希望因为内存不足等等原因,从而导致被gc回收,音乐播放终止,所以我们这里使用服务Service创建一个音乐播放器. 2.创建一个音乐播放器项目(使用服务) (1 ...
- Android基于发展Service音乐播放器
这是一个基于Service组件的音乐播放器,程序的音乐将会由后台的Service组件负责播放,当后台的播放状态改变时,程序将会通过发送广播通知前台Activity更新界面:当用户单击前台Activit ...
- Android(java)学习笔记177: 服务(service)之音乐播放器
1.我们播放音乐,希望在后台长期运行,不希望因为内存不足等等原因,从而导致被gc回收,音乐播放终止,所以我们这里使用服务Service创建一个音乐播放器. 2.创建一个音乐播放器项目(使用服务) (1 ...
- 躁!DJ 风格 Java 桌面音乐播放器
本文适合有 Java 基础知识的人群,跟着本文可学习和运行 Java 版桌面 DJ 音乐播放器. 本文作者:HelloGitHub-秦人 HelloGitHub 推出的<讲解开源项目>系列 ...
- Andriod小项目——在线音乐播放器
转载自: http://blog.csdn.net/sunkes/article/details/51189189 Andriod小项目——在线音乐播放器 Android在线音乐播放器 从大一开始就已 ...
- 项目源码--Android类似酷狗音乐播放器
下载源码 知识技能概要: 1.音乐文件的扫描与管理 2.音频流的解码 3. UI控件的综合使用 4.播放列表方式管理 5.随机播放方式 6.源码带详细的中文注释 ...... 详细介绍 1. 音乐文件 ...
随机推荐
- PW4052 是一颗适用于单节锂电池的、具有恒压/恒流充电模式的充电管理 IC
PW4052 是一颗适用于单节锂电池的.具有恒压/恒流充电模式的充电管理 IC.该芯片采用开关型的工作模式, 能够为单节锂电池提供快速. 高效且简单的充电管理解决方案.PW4052 采用三段式充电管理 ...
- include指令和include动作的区别
include指令和<jsp:include>动作标识的区别 1.include指令通过file属性指定被包含的文件,并且file属性不支持任何表达式: <jsp:include&g ...
- Vue 打包报错UnhandledPromiseRejectionWarning: postcss-svgo: Error in parsing SVG
解决方案 检查下自己最新写的css 或者最新引入的样式库,把里面的base64的url替换成双引号形式的 PS:我这报错是因为引入的weui.min.css里面的loading样式的`backgrou ...
- [opencv]一些重配遇到的问题(只针对我自己的电脑)
1.我的opencv版本是4.5.3 2.环境变量这样配 3.对于每个项目,项目属性这样配: 其中包含目录: C:\Users\dxd\OPCV\opencv\build\include\opencv ...
- Linux NTP工具的基本使用
NTP 时间同步 NTP(Network Time Protocol)协议,网络时间协议.利用ntp协议可以实现网络中的计算机时间同步. 实现NTP协议的工具: ntpdate:只能同步一次时间 nt ...
- f-strings: Python字符串处理的瑞士军刀
从 3.6 开始,Python 新增了一个格式化字符串的方法,称之为 f-string. 其用法就是在python原始字符串的基础上增加 f/F 前缀,以大括号 {} 标明被替换的字段. f-stri ...
- [OpenCV实战]52 在OpenCV中使用颜色直方图
颜色直方图是一种常见的图像特征,顾名思义颜色直方图就是用来反映图像颜色组成分布的直方图.颜色直方图的横轴表示像素值或像素值范围,纵轴表示该像素值范围内像素点的个数或出现频率.颜色直方图属于计算机视觉中 ...
- 聊聊web漏洞挖掘第一期
之前写2022年度总结的时候,有提到要给大家分享漏洞挖掘技巧.这里简单分享一些思路,更多的内容需要大家举一反三. 文章准备昨晚写的,昨天晚上出去唱歌,回来太晚了,耽搁了.昨天是我工作的last day ...
- 一个小而美的 C 语言项目
我最近在学习 C 语言,看的一本书叫做 <C Primer Plus>,这本书对 C 语言的描写.特性介绍.代码示例都介绍的比较详细,是小白入门 C 语言非常不错的一本书,还有一本经典书叫 ...
- Educational Codeforces Round 141 解题报告
Educational Codeforces Round 141 解题报告 \(\text{By DaiRuiChen007}\) \(\text{Contest Link}\) A. Make it ...