@

原理

定义一个拖拽物,和它拖拽的目标,拖拽物可以理解为一个平底锅(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();
}

至此我们完成了平移手势系统的搭建。

这个控件可以拓展到任何检测手指在屏幕上的移动,并可用于将移动应用于内容的用途,例如地图或者图片的平移拖拽等。

项目地址

Github:maui-samples

[MAUI 项目实战] 手势控制音乐播放器(二): 手势交互的更多相关文章

  1. 团队项目 NABCD分析java音乐播放器

    NABCD分析java音乐播放器 程设计题目:java音乐播放器 一.课程设计目的 1.编程设计音乐播放软件,使之实现音乐播放的功能. 2.培养学生用程序解决实际问题的能力和兴趣. 3.加深java中 ...

  2. Android开发实战之简单音乐播放器

    最近开始学习音频相关.所以,很想自己做一个音乐播放器,于是,花了一天学习,将播放器的基本功能实现了出来.我觉得学习知识点还是蛮多的,所以写篇博客总结一下关于一个音乐播放器实现的逻辑.希望这篇博文对你的 ...

  3. Android(java)学习笔记234: 服务(service)之音乐播放器

    1.我们播放音乐,希望在后台长期运行,不希望因为内存不足等等原因,从而导致被gc回收,音乐播放终止,所以我们这里使用服务Service创建一个音乐播放器. 2.创建一个音乐播放器项目(使用服务) (1 ...

  4. Android(java)学习笔记177: 服务(service)之音乐播放器

    1.我们播放音乐,希望在后台长期运行,不希望因为内存不足等等原因,从而导致被gc回收,音乐播放终止,所以我们这里使用服务Service创建一个音乐播放器. 2.创建一个音乐播放器项目(使用服务) (1 ...

  5. Andriod小项目——在线音乐播放器

    转载自: http://blog.csdn.net/sunkes/article/details/51189189 Andriod小项目——在线音乐播放器 Android在线音乐播放器 从大一开始就已 ...

  6. Swift实战-豆瓣电台(九)简单手势控制暂停播放(全文完)

    Swift实战-豆瓣电台(九)简单手势控制暂停播放 全屏清晰观看地址:http://www.tudou.com/programs/view/tANnovvxR8U/ 这节我们主要讲UITapGestu ...

  7. Android应用--简、美音乐播放器增加音量控制

    Android应用--简.美音乐播放器增加音量控制 2013年6月26日简.美音乐播放器继续完善中.. 题外话:上一篇博客是在6月11号发的,那篇博客似乎有点问题,可能是因为代码结构有点乱的原因,很难 ...

  8. 自定义css样式结合js控制audio做音乐播放器

    最近工作需求需要播放预览一些音乐资源,所以自己写了个控制audio的音乐播放器. 实现的原理主要是通过js调整audio的对象属性及对象方法来进行控制: 1.通过play().pause()来控制音乐 ...

  9. HTML5项目笔记4:使用Audio API设计绚丽的HTML5音乐播放器

    HTML5 有两个很炫的元素,就是Audio和 Video,可以用他们在页面上创建音频播放器和视频播放器,制作一些效果很不错的应用. 无论是视屏还是音频,都是一个容器文件,包含了一些音频轨道,视频轨道 ...

  10. swift 音乐播放器项目-《lxy的杰伦情歌》开发实战演练

    近期准备将项目转化为OC与swift混合开发.试着写一个swift音乐播放器的demo,体会到了swift相对OC的优势所在.废话不多说.先上效果图: watermark/2/text/aHR0cDo ...

随机推荐

  1. Verilog语法+:的说明

    "+:"."-:"语法看到这个语法的时候是在分析AXI lite 总线源码时碰见的,然后查阅了资料,做出如下解释. 1.用处这两个应该算是运算符,运用在多位的变 ...

  2. 结对作业——考研咨询APP

    结对作业--考研资讯系统   102陈同学105潘同学108苏同学 (排版:Markdown) 一.需求分析(NABCD模型) 1. N(Need 需求): 1)想知道每个专业考研可以考哪个专业2)想 ...

  3. SpringBoot - Lombok使用详解4(@Data、@Value、@NonNull、@Cleanup)

    六.Lombok 注解详解(4) 8,@Data (1)@Data 是一个复合注解,用在类上,使用后会生成:默认的无参构造函数.所有属性的 getter.所有非 final 属性的 setter 方法 ...

  4. 十大经典排序之堆排序(C++实现)

    堆排序 通过将无序表转化为堆,可以直接找到表中最大值或者最小值,然后将其提取出来,令剩余的记录再重建一个堆, 取出次大值或者次小值,如此反复执行就可以得到一个有序序列,此过程为堆排序. 思路: 1.创 ...

  5. Synchronized和Lock有什么区别?用Lock有什么好处?

    Synchronized 和 Lock 1.原始构成 Synchronized 是关键字属于JVM层面 (代码中以蓝色字体呈现) monitorenter .monitorexit Lock 是具体类 ...

  6. Android 自定义SeekBar (二)

    一.前言 本文在 上节 的基础上,讲解自定义拖动条的实现思路. 二.思路 先在res/values文件夹下,自定义控件属性: <?xml version="1.0" enco ...

  7. 使用git&GitHub通过两台电脑协同作业,助力办公室摸鱼

    前情提要:工作有时候负荷比较小,会接一些咸鱼上的活儿或者自己学点软件技能,这时候会出现一个情况,公司笔记本一般不带回家,家里台式机,白天在公司摸鱼编辑的文件,晚上回家想接着干怎么办呢,或是晚上在家干的 ...

  8. 1022 Digital Library (30分)

    本题题意很好读,看上去也不难写 写完运行才发现输出title只有一个单词... 后来把cin >> t换成了getline(cin, t) 还有一个坑点: Line #1: the 7-d ...

  9. 三种遍历的方法(map和forEach的区别)

    一. for循环 arr[index]可以改变原数组 二. forEach方法 forEach方法的返回值是一个undefined: 2. 在循环体内改变item的值不会影响原数组,而是只在循环体内生 ...

  10. 基于Extjs web设计器

    通过从左边的树拖入字段到右边,编辑字段属性,在界面所见即所得 进入链接 http://www.e-ipd.com:8080/crk/public/login.aspx?ReturnUrl=%2fcrk ...