@

用过网易云音乐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控件命名,LeftPitMiddlePitRightPit,代码如下:

<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. 关于css在html的三种使用方式

    关于css在html的三种使用方式 1.内联样式(直接在html里面使用style) eg:<h1 style="color:skyblue">这是一个测试标题< ...

  2. java学习笔记(五)运算符

    ++ --   自增自减 a++ 执行完这行代码后,先给b赋值,再自增 ++a 执行完这行代码前,先自增,再给b赋值 Math类

  3. ggplot axis text 拐弯

    scale_y_discrete(position = "left",labels=function(x) str_wrap(x, width=48)) +

  4. jmeter--负载测试

    负载测试 1. jmeter插件处理 2. 下载负载测试计划所需要插件 3. 负载测试计划 4. 波浪形的测试计划--测试服务器的稳定性 一般用于测试稳定的场景测试(有规律的活动/场景/接口请求等等, ...

  5. El_获取域中存储的值和El_获取域中存储的值_对象值

    2获取值 1.el表达式只能从域对象中获取值 2语法: 1.$[域名称.键}:从指定域中获取指定键的值域名称:1.pageScope2.requestScope 3.sessionScope 4.ap ...

  6. ssh原理及应用

    SSH原理与运用(一):远程登录 SSH原理与运用(一):远程登录 SSH原理与运用(二):远程操作与端口转发 SSH原理与运用(二):远程操作与端口转发 mitm应用: python开源三方库:ss ...

  7. mysql创建函数时提示1418。可选关闭二进制日志或者设置log_bin_trust_function_creators=1

    报错详情如下:1418--This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration a ...

  8. MSVC设置版本

    MSVC设置版本 在开发QT时,由于QT 5.12与MSVC 2017兼容,因此需要用MSVC 2017来编译使用QT 5.12的程序. 1 安装MSVC 2017 由于笔者电脑上安装的Visual ...

  9. 如何优化if--else

    1.现状代码 public interface IPay { void pay(); } package com.test.zyj.note.service.impl; import com.test ...

  10. P3128 [USACO15DEC]Max Flow P(树上倍增和树链剖分)

    思路1(树上倍增$ + $树上差分) 每次都修改一条从\(u\)到\(v\),不就是树上差分的专门操作吗?? 直接用倍增求\(LCA\),每次\(d[u]++,d[v]++,d[LCA(u,v)]-- ...