@

用过网易云音乐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(); } }
}

效果如下:

最终效果如下:

项目地址

Github:maui-samples

[MAUI]模仿网易云音乐黑胶唱片的交互实现的更多相关文章

  1. UWP 动画系列之模仿网易云音乐动画

    一.前言 最近在弄毕业设计(那时坑爹选了制作个UWP商店的APP),一个人弄得烦躁,在这里记录一些在做毕业设计时的学习过程.由于我的毕业设计是做一个音乐播放器,那么Windows商店上优秀的软件当然是 ...

  2. NeteaseCloudWebApp模仿网易云音乐的vue自己从开源代码中学习到的

    github地址: https://github.com/javaSwing/NeteaseCloudWebApp 1.Vue.prototype.$http = Axios // 类似于vue-re ...

  3. UWP仿网易云音乐之1-TitleBar

    首先,创建一个UWP的项目.我使用的是Visual Studio 2017 社区版. 如图,我们将项目命名为UWP-Music. 现在我们先标题栏的配色调整与网易云音乐一致. 我们先分析一下标题栏,默 ...

  4. 用RotateDrawable实现网易云音乐唱片机效果

    imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="唱片机" title=""> ...

  5. Android版网易云音乐唱片机唱片磁盘旋转及唱片机机械臂动画关键代码实现思路

     Android版网易云音乐唱片机唱片磁盘旋转及唱片机机械臂动画关键代码实现思路 先看一看我的代码运行结果. 代码运行起来初始化状态: 点击开始按钮,唱片机的机械臂匀速接近唱片磁盘,同时唱片磁盘也 ...

  6. Flutter仿网易云音乐:播放界面

    写在前头 本来是要做一个仿网易云音乐的flutter项目,但是因为最近事情比较多,项目周期跨度会比较长,因此分几个步骤来完成.这是仿网易云音乐项目系列文章的第一篇.没有完全照搬网易云音乐的UI,借鉴了 ...

  7. 高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM

    简介 这是一个使用Java(以后还会推出Kotlin版本)语言,从0开发一个Android平台,接近企业级的项目(我的云音乐),包含了基础内容,高级内容,项目封装,项目重构等知识:主要是使用系统功能, ...

  8. Swift高仿iOS网易云音乐Moya+RxSwift+Kingfisher+MVC+MVVM

    效果 列文章目录 因为目录比较多,每次更新这里比较麻烦,所以推荐点击到主页,然后查看iOS Swift云音乐专栏. 目简介 这是一个使用Swift(还有OC版本)语言,从0开发一个iOS平台,接近企业 ...

  9. OC高仿iOS网易云音乐AFNetworking+SDWebImage+MJRefresh+MVC+MVVM

    效果 因为OC版本大部分截图和Swift版本一样,所以就不再另外截图了. 列文章目录 因为目录比较多,每次更新这里比较麻烦,所以推荐点击到主页,然后查看iOS云音乐专栏. 目简介 这是一个使用OC语言 ...

  10. 模拟制作网易云音乐(AudioContext)

    记得好早前在慕课网上看到一款可视化音乐播放器,当前是觉得很是神奇,还能这么玩.由于当时刚刚转行不久,好多东西看得稀里糊涂不明白,于是趁着现在有时间又重新梳理了一遍,然后参照官网的API模拟做了一款网易 ...

随机推荐

  1. 查看shell 用户连接数

    w | grep pts |wc -l

  2. 英码科技边缘计算智慧工地解决方案——给工地戴上AI“安全帽”

    据统计显示,2021年全国共发生房屋市政工程生产安全事故734起,死亡840人:且近3年来,工地事故数量.死亡人数连续攀升.这不仅仅是一个普通的数字,每个数字都代表一个独特.鲜活的生命.为什么每年会发 ...

  3. 网络基础-分层思想和TCP/TP协议族

    一 .分层思想 首先,什么是分层?1984年国际标准化组织(iso)颁布了开放系统互联(osi)参考模型:一个开放式体系结构,将网络分成七层. 分层 功能 应用层 网络服务与最终用户的一个接口 表示层 ...

  4. django 批量提交

    https://www.cnblogs.com/lbzbky/articles/11792545.html

  5. 这篇文章汇聚33个BUG!来挑战一下,看看你能找出来几个?

    你好呀,我是歪歪. 前几天看到"Qunar技术沙龙"公众号推送了一篇关于他们举办了一场"Code Review大赛"的文章. 看到 Code Review 我很 ...

  6. Android笔记--动态申请权限

    动态申请权限 在动态申请权限这里,一共分为两种不同的模式,分别是Lazy模式(懒汉式)和Hungry模式(饿汉式),这两种模式区分的话,可以通俗地解释一下就是,对于懒汉来说,只有在我们点击某个按钮需要 ...

  7. 纠删码在实时视频流中的应用丨Dev for Dev 专栏

    本文为「Dev for Dev 专栏」系列内容,作者为声网网络体验团队王瑞. 01 背景 在实时音视频通话中,音视频质量受网络丢包影响较大,特别是对于视频. 为什么视频对丢包更敏感呢?通常来说,音频的 ...

  8. Flex布局原理【转载】

    引言 CSS3中的 Flexible Box,或者叫flexbox,是用于排列元素的一种布局模式. 顾名思义,弹性布局中的元素是有伸展和收缩自身的能力的. 相比于原来的布局方式,如float.posi ...

  9. Django笔记六之外键ForeignKey介绍

    这一篇笔记介绍 Django 系统 model 的外键处理,ForeignKey 以及相应的处理方法. 这是一种一对多的字段类型,表示两张表之间的关联关系. 本篇笔记的目录如下: on_delete ...

  10. java多线程基础小白指南--synchronized同步块

    sychronized是java多线程非常关键的一个知识点,这篇博客将从synchronized几个用法以及代码来学习. sychronized的作用是能够保证同一时间只有一个线程来运行这块代码,达到 ...