今天在 .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();}}
}

效果如下:

最终效果如下:

模仿网易云音乐黑胶唱片的交互实现相关推荐

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

    文章目录 创建页面布局 创建手势控件 创建影子控件 唱盘拨动交互 唱盘和唱针动画 项目地址 用过网易云音乐App的同学应该都比较熟悉它播放界面. 这是一个良好的交互设计,留声机的界面隐喻准确地向人们传 ...

  2. 网易云音乐黑胶唱片html,黑胶唱片感浓厚:网易云音乐MB1蓝牙音箱要“还原声音本真”...

    网易云音乐的受欢迎程度,让大家知道了音乐播放器也能用出情怀,其界面设计.歌曲推荐.人性化创意等让许多用户欲罢不能.而在各大厂商的智能音箱纷纷登场之际,拥有庞大用户量和智能算法的网易云音乐却" ...

  3. 自定义QListWidget实现item被hover时改变图标样式(模仿网易云音乐选项列表)(方法二)

    环境配置 :MinGW + QT 5.12 效果图: 这里需要说明一下:QListWidget是鼠标press时item就会被选中,自定义的TestListWidget类重写了mousePressEv ...

  4. 自定义QListWidget实现item被hover时改变图标样式(模仿网易云音乐选项列表)(方法一)

    环境配置 :MinGW + QT 5.12 效果图: 这里需要说明一下:QListWidget是鼠标press时item就会被选中,自定义的TestListWidget类重写了mousePressEv ...

  5. 基于Qt模仿网易云音乐播放器

    基于Qt模仿网易云音乐界面,目前只实现了部分界面,后续继续完善改造. 部分代码: #ifndef MYSQLDATAMGR_H #define MYSQLDATAMGR_H#include <Q ...

  6. 模仿网易云音乐鲸云特效动效

    JinyunEffect 项目地址:tyhjh/JinyunEffect  简介: 模仿网易云音乐鲸云特效动效 更多:作者   提 Bug 标签: # Android 粒子特效--网易云鲸云特效 文 ...

  7. 微信小程序实战教程:模仿—网易云音乐(二)

    接上一篇:微信小程序实战教程:模仿-网易云音乐(一) wxml进行渲染: <!--歌词--> <view class="lyric-content" hidden ...

  8. Qt之模仿网易云音乐 广告Banner

    文章目录 环境 代码 使用 效果 代码分享 环境 vs2013 + Qt5.6.2 代码 NetEasyBanner.h #ifndef NETEASYBANNER_H #define NETEASY ...

  9. duilib 模仿网易云音乐

    duilib 模仿网易云音乐 duilib+cef+html实现,目前只有前端简单框架 想要交流的,可以加qq:1021766106,或者微信:Official6-8一起交流技术栈

最新文章

  1. 【Kubernetes】两篇文章 搞懂 K8s 的 fannel 网络原理
  2. java中什么表示菜单项_下列类型中,表示菜单项的是( )。_学小易找答案
  3. MySQL查询in操作 查询结果按in集合顺序显示(转)
  4. ThoughtWorks洞见领域驱动设计思维导图笔记
  5. python init main_python 模块中的 __init__.py __main__.py
  6. Jquery实现验证码功能 完美效果 jsp php 页面均可调用
  7. mysql reset_mysql的reset命令
  8. vue-router 源码:实现一个简单的 vue-router
  9. 关于Java的权限修饰符(public,private,protected,默认friendly)
  10. win7用VMware安装CentOs7搭建Linux环境
  11. MyBatis源码阅读(一) --- 源码阅读环境搭建
  12. 【回归分析】[3]--回归方程的显著性检验
  13. 【雅思大作文考官范文】——第十五篇:'high salaries' essay
  14. 同步电机模型的MATLAB仿真模型
  15. 26367411153598389kygoq
  16. 【2021四川省赛】E.Don‘t Really Like How The Story Ends 图论
  17. Python 搭建excel数据分析环境(符Demo源码)
  18. 【FLink】Assigned key must not be null
  19. n个元素进栈,有几种出栈方式
  20. jeecgboot:主表(antd table)默认选中第一条记录

热门文章

  1. 注意力缺陷多动障碍儿童个案分析
  2. C# VLC播放视频实现布满在控件上
  3. Python 1-22 测试
  4. iOS AVplayer 基本使用方法
  5. VSCode取消references的方法记录
  6. i5功耗最低的cpu_i7一定比i5强?未必!电脑CPU套路深,你中招了吗?
  7. sql脚本自动完成数据库创建、切换、建表、数据初始化
  8. 口袋linux设备,口袋中的Linux
  9. WebView 使用系统服务下载 实现文件下载功能
  10. e2 android,魅蓝E2怎么拆机 魅蓝E2手机拆解图文教程