[MAUI 项目实战] 手势控制音乐播放器(二): 手势交互
@
原理
定义一个拖拽物,和它拖拽的目标,拖拽物可以理解为一个平底锅(pan),拖拽目标是一个坑(pit),当拖拽物进入坑时,拖拽物就会被吸附在坑里。可以脑补一下下图:
你问我为什么是平底锅和坑,当然了在微软官方的写法里pan是平移的意思,而不是指代平底锅。只是通过同义词来方便理解
坑就是正好是平底锅大小的炉灶。正好可以放入平底锅。
pan和pit组成平移手势的系统,在具体代码中包含了边缘检测判定和状态机维护。我们将一步步实现平移手势功能
pit很简单,是一个包含了名称属性的控件,这个名称属性是用来标识pit的。以便当pan入坑时我们知道入了哪个坑,IsEnable是一个绑定属性,它用来控制pit是否可用的。
在这个程序中,拖拽物是一个抽象的唱盘。它的拖拽目标是周围8个图标。
交互实现
这里用Grid作为pit控件基类型,因为Grid可以包含子控件,我们可以在pit控件中添加子控件,比如一个图片,一个文字,这样就可以让pit控件更加丰富。
public class PitGrid : Grid
{
public PitGrid()
{
IsEnable = true;
}
public static readonly BindableProperty IsEnableProperty =
BindableProperty.Create("IsEnable", typeof(bool), typeof(CircleSlider), true, propertyChanged: (bindable, oldValue, newValue) =>
{
var obj = (PitGrid)bindable;
obj.Opacity = obj.IsEnable ? 1 : 0.8;
});
public bool IsEnable
{
get { return (bool)GetValue(IsEnableProperty); }
set { SetValue(IsEnableProperty, value); }
}
public string PitName { get; set; }
}
使用WeakReferenceMessenger作为消息中心,用来传递pan和pit的交互信息。
定义一个平移事件PanAction,在pan和pit产生交汇时触发。其参数PanActionArgs描述了pan和pit的交互的关系和状态。
public class PanActionArgs
{
public PanActionArgs(PanType type, PitGrid pit = null)
{
PanType = type;
CurrentPit = pit;
}
public PanType PanType { get; set; }
public PitGrid CurrentPit { get; set; }
}
手势状态类型PanType定义如下:
- In:pan进入pit时触发,
- Out:pan离开pit时触发,
- Over:释放pan时触发,
- ·Start:pan开始拖拽时触发
public enum PanType
{
Out, In, Over, Start
}
MAUI为我们开发者包装好了PanGestureRecognizer
即平移手势识别器。
平移手势更改时引发事件PanUpdated
事件,此事件附带的 PanUpdatedEventArgs对象中包含以下属性:
- StatusType,类型 GestureStatus为 ,指示是否为新启动的手势、正在运行的手势、已完成的手势或取消的手势引发了事件。
- TotalX,类型 double为 ,指示自手势开始以来 X 方向的总变化。
- TotalY,类型 double为 ,指示自手势开始以来 Y 方向的总变化。
容器控件
PanGestureRecognizer
提供了当手指在屏幕移动这一过程的描述我们需要一个容器控件来对拖拽物进行包装,以赋予拖拽物响应平移手势的能力。
创建平移手势容器控件:在Controls目录中新建PanContainer.xaml,代码如下:
<?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="MatoMusic.Controls.PanContainer">
<ContentView.GestureRecognizers>
<PanGestureRecognizer PanUpdated="PanGestureRecognizer_OnPanUpdated"></PanGestureRecognizer>
<TapGestureRecognizer Tapped="TapGestureRecognizer_OnTapped"></TapGestureRecognizer>
</ContentView.GestureRecognizers>
</ContentView>
为PanContainer添加PitLayout属性,用来存放pit的集合。
打开PanContainer.xaml.cs,添加如下代码:
private IList<PitGrid> _pitLayout;
public IList<PitGrid> PitLayout
{
get { return _pitLayout; }
set { _pitLayout = value; }
}
CurrentView属性为当前拖拽物所在的pit控件。
private PitGrid _currentView;
public PitGrid CurrentView
{
get { return _currentView; }
set { _currentView = value; }
}
添加PositionX和PositionY两个可绑定属性,用来设置拖拽物的初始位置。当值改变时,将拖拽物的位置设置为新的值。
public static readonly BindableProperty PositionXProperty =
BindableProperty.Create("PositionX", typeof(double), typeof(PanContainer), default(double), propertyChanged: (bindable, oldValue, newValue) =>
{
var obj = (PanContainer)bindable;
//obj.Content.TranslationX = obj.PositionX;
obj.Content.TranslateTo(obj.PositionX, obj.PositionY, 0);
});
public static readonly BindableProperty PositionYProperty =
BindableProperty.Create("PositionY", typeof(double), typeof(PanContainer), default(double), propertyChanged: (bindable, oldValue, newValue) =>
{
var obj = (PanContainer)bindable;
obj.Content.TranslateTo(obj.PositionX, obj.PositionY, 0);
//obj.Content.TranslationY = obj.PositionY;
});
订阅PanGestureRecognizer的PanUpdated事件:
private async void PanGestureRecognizer_OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
var isInPit = false;
var isAdsorbInPit = false;
switch (e.StatusType)
{
case GestureStatus.Started: // 手势启动
break;
case GestureStatus.Running: // 手势正在运行
break;
case GestureStatus.Completed: // 手势完成
break;
}
}
接下来我们将对手势的各状态:启动、正在运行、已完成的状态做处理
手势开始
- GestureStatus.Started:手势开始时触发, 触发动画效果,将拖拽物缩小,同时向消息订阅者发送PanType.Start消息。
case GestureStatus.Started:
Content.Scale=0.5;
WeakReferenceMessenger.Default.Send<PanActionArgs, string>(new PanActionArgs(PanType.Start, this.CurrentView), TokenHelper.PanAction);
break;
手势运行
GestureStatus.Running:手势正在运行时触发,这个状态下,
根据手指在屏幕上的移动距离来计算translationX和translationY,他们是拖拽物在X和Y方向上的移动距离。
在X轴方向不超过屏幕的左右边界,即x不得大于this.Width - Content.Width / 2,不得小于 0 - Content.Width / 2
同理
在Y轴方向不超过屏幕的上下边界,即y不得大于this.Height - Content.Height / 2,不得小于 0 - Content.Height / 2
代码如下:
case GestureStatus.Running:
var translationX =
Math.Max(0 - Content.Width / 2, Math.Min(PositionX + e.TotalX, this.Width - Content.Width / 2));
var translationY =
Math.Max(0 - Content.Height / 2, Math.Min(PositionY + e.TotalY, this.Height - Content.Height / 2));
接下来判定拖拽物边界
pit的边界是通过Region类来描述的,Region类有四个属性:StartX、EndX、StartY、EndY,分别表示pit的左右边界和上下边界。
public class Region
{
public string Name { get; set; }
public double StartX { get; set; }
public double EndX { get; set; }
public double StartY { get; set; }
public double EndY { get; set; }
}
对PitLayout中的pit进行遍历,判断拖拽物是否在pit内,如果在,则将isInPit设置为true。
判定条件是如果拖拽物的中心位置在pit的边缘内,则认为拖拽物在pit内。
```csharp
if (PitLayout != null)
{
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 = translationX >= pitRegion.StartX - Content.Width / 2 && translationX <= pitRegion.EndX - Content.Width / 2;
var isYin = translationY >= pitRegion.StartY - Content.Height / 2 && translationY <= pitRegion.EndY - Content.Height / 2;
if (isYin && isXin)
{
isInPit = true;
if (this.CurrentView == item)
{
isSwitch = false;
}
else
{
if (this.CurrentView != null)
{
isSwitch = true;
}
this.CurrentView = item;
}
}
}
}
isSwitch是用于检测是否跨过pit,当CurrentView非Null改变时,说明拖拽物跨过了紧挨着的两个pit,需要手动触发PanType.Out和PanType.In消息。
IsInPitPre用于记录在上一次遍历中是否已经发送了PanType.In消息,如果已经发送,则不再重复发送。
if (isInPit)
{
if (isSwitch)
{
WeakReferenceMessenger.Default.Send<PanActionArgs, string>(new PanActionArgs(PanType.Out, this.CurrentView), TokenHelper.PanAction);
WeakReferenceMessenger.Default.Send<PanActionArgs, string>(new PanActionArgs(PanType.In, this.CurrentView), TokenHelper.PanAction);
isSwitch = false;
}
if (!isInPitPre)
{
WeakReferenceMessenger.Default.Send<PanActionArgs, string>(new PanActionArgs(PanType.In, this.CurrentView), TokenHelper.PanAction);
isInPitPre = true;
}
}
else
{
if (isInPitPre)
{
WeakReferenceMessenger.Default.Send<PanActionArgs, string>(new PanActionArgs(PanType.Out, this.CurrentView), TokenHelper.PanAction);
isInPitPre = false;
}
this.CurrentView = null;
}
最后,将拖拽物控件移动到当前指尖的位置上:
Content.TranslationX = translationX;
Content.TranslationY = translationY;
break;
手势结束
- GustureStatus.Completed:手势结束时触发,触发动画效果,将拖拽物放大,同时回弹至原来的位置,最后向消息订阅者发送PanType.Over消息。
case GestureStatus.Completed:
Content.TranslationX= PositionX;
Content.TranslationY= PositionY;
Content.Scale= 1;
WeakReferenceMessenger.Default.Send<PanActionArgs, string>(new PanActionArgs(PanType.Over, this.CurrentView), TokenHelper.PanAction);
break;
使用控件
拖拽物
拖拽物可以是任意控件。它将响应手势。在这里定义一个圆形的250*250的半通明黑色BoxView,这个抽象的唱盘就是拖拽物。将响应“平移手势”和“点击手势”
<BoxView HeightRequest="250"
WidthRequest="250"
Margin="7.5"
Color="#60000000"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
CornerRadius="250" ></BoxView>
创建pit集合
MainPage.xaml中定义一个PitContentLayout
,这个AbsoluteLayout类型的容器控件,内包含一系列控件作为pit,这些pit集合将作为平移手势容器的判断依据。
<AbsoluteLayout x:Name="PitContentLayout">
<--pit控件-->
...
</AbsoluteLayout>
在页面加载完成后,将PitContentLayout中的pit集合赋值给平移手势容器的PitLayout属性。
private async void MainPage_Appearing(object sender, EventArgs e)
{
this.DefaultPanContainer.PitLayout=this.PitContentLayout.Children.Select(c => c as PitGrid).ToList();
}
至此我们完成了平移手势系统的搭建。
这个控件可以拓展到任何检测手指在屏幕上的移动,并可用于将移动应用于内容的用途,例如地图或者图片的平移拖拽等。
项目地址
[MAUI 项目实战] 手势控制音乐播放器(二): 手势交互的更多相关文章
- 团队项目 NABCD分析java音乐播放器
NABCD分析java音乐播放器 程设计题目:java音乐播放器 一.课程设计目的 1.编程设计音乐播放软件,使之实现音乐播放的功能. 2.培养学生用程序解决实际问题的能力和兴趣. 3.加深java中 ...
- Android开发实战之简单音乐播放器
最近开始学习音频相关.所以,很想自己做一个音乐播放器,于是,花了一天学习,将播放器的基本功能实现了出来.我觉得学习知识点还是蛮多的,所以写篇博客总结一下关于一个音乐播放器实现的逻辑.希望这篇博文对你的 ...
- Android(java)学习笔记234: 服务(service)之音乐播放器
1.我们播放音乐,希望在后台长期运行,不希望因为内存不足等等原因,从而导致被gc回收,音乐播放终止,所以我们这里使用服务Service创建一个音乐播放器. 2.创建一个音乐播放器项目(使用服务) (1 ...
- Android(java)学习笔记177: 服务(service)之音乐播放器
1.我们播放音乐,希望在后台长期运行,不希望因为内存不足等等原因,从而导致被gc回收,音乐播放终止,所以我们这里使用服务Service创建一个音乐播放器. 2.创建一个音乐播放器项目(使用服务) (1 ...
- Andriod小项目——在线音乐播放器
转载自: http://blog.csdn.net/sunkes/article/details/51189189 Andriod小项目——在线音乐播放器 Android在线音乐播放器 从大一开始就已 ...
- Swift实战-豆瓣电台(九)简单手势控制暂停播放(全文完)
Swift实战-豆瓣电台(九)简单手势控制暂停播放 全屏清晰观看地址:http://www.tudou.com/programs/view/tANnovvxR8U/ 这节我们主要讲UITapGestu ...
- Android应用--简、美音乐播放器增加音量控制
Android应用--简.美音乐播放器增加音量控制 2013年6月26日简.美音乐播放器继续完善中.. 题外话:上一篇博客是在6月11号发的,那篇博客似乎有点问题,可能是因为代码结构有点乱的原因,很难 ...
- 自定义css样式结合js控制audio做音乐播放器
最近工作需求需要播放预览一些音乐资源,所以自己写了个控制audio的音乐播放器. 实现的原理主要是通过js调整audio的对象属性及对象方法来进行控制: 1.通过play().pause()来控制音乐 ...
- HTML5项目笔记4:使用Audio API设计绚丽的HTML5音乐播放器
HTML5 有两个很炫的元素,就是Audio和 Video,可以用他们在页面上创建音频播放器和视频播放器,制作一些效果很不错的应用. 无论是视屏还是音频,都是一个容器文件,包含了一些音频轨道,视频轨道 ...
- swift 音乐播放器项目-《lxy的杰伦情歌》开发实战演练
近期准备将项目转化为OC与swift混合开发.试着写一个swift音乐播放器的demo,体会到了swift相对OC的优势所在.废话不多说.先上效果图: watermark/2/text/aHR0cDo ...
随机推荐
- 【python】第一模块 步骤四 第二课、实现飞机大战(未完待续)
第二课.实现飞机大战 一.项目介绍 项目实战:飞机大战 课程目标 掌握面向对象分析和开发的思想 能对项目进行拆分,进行模块化开发 了解项目开发的基本流程 理解并运用python的包.模块相关知识 理解 ...
- 面向对象2(Java)
封装 基本介绍 该露的露,该藏的藏,我们的程序设计要追求"高内聚,低耦合": 高内聚:类的内部数据操作细节自己完成,不允许外部干涉 低耦合:仅暴露少量的方法给外部使用 封装(数据的 ...
- PLC入门笔记7
梯形图与指令表的转换 后缀表达式 开头是MPS 结尾是MPP 中间就是MRD啦!!!! MPS 存入堆栈(将目前累加器的内容存入堆栈.(堆栈指针加一))将当前数据栈顶数据复制一份到辅助栈 栈深度+1 ...
- 06 HBase安装与伪分布式配置
1.下载压缩文件 2.解压 3.修改文件夹名 4.修改文件夹权限 5.配置环境变量 6.伪分布式配置文件 7.启动HDFS,启动Hbase 8.进入shell界面 9.停止Hbase,停止HDFS运行
- CountDownLatch/CyclicBarrierDemo/Samaphore
CountDownLatch CountDownLatch:让一些线程阻塞直到另外一些完成后才被唤醒 CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞 ...
- activiti引擎的表结构(仅记录用)
act_hi_*:'hi'表示 history,此前缀的表包含历史数据,如历史(结束)流程实例,变量,任务等等.act_ge_*:'ge'表示 general,此前缀的表为全局通用数据,用于不同场景中 ...
- CBV源码分析及模板语法之传值 过滤器 标签 继承 导入
CBV的源码分析 # CBV的源码入口从哪里看呢? CBV的核心源码: return self.dispatch(request, *args, **kwargs) def dispatch(self ...
- JavaScript之jQuery要点记录
一 属性和属性节点 1.什么是属性? 对象身上保存的变量就是属性 2.如何操作属性? 对象.属性名称 = 值; 对象.属性名称; 对象["属性名称"] = 值; 对象[" ...
- centos安装k8s
1.确保每台机器上有docker http://get.daocloud.io/#install-docker 2.关闭 每台机器上的swap,selinux swapoff -a setenforc ...
- Codeforces Round #809 (Div. 2) A-E
Codeforces Round #809 (Div. 2) 2022/7/19 下午VP 传送门:https://codeforces.com/contest/1706 A. Another Str ...