[MAUI 项目实战] 手势控制音乐播放器: 手势交互

news2024/11/16 15:58:48

原理

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/814800.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

腾讯地图点标记加调用

先看效果 PHP代码 <?phpnamespace kds_addons\edata\controller;use think\addons\Controller; use think\Db;class Maps extends Controller {// 经纬度计算面积function calculate_area($points){$totalArea 0;$numPoints count($points);if ($numPoints > 2) {…

Qt、Qt Creator下载、安装

一、Qt、Qtcreator简介 Qt是一个跨平台应用开发框架。 Qt Creator是一个跨平台的集成开发环境&#xff08;IDE&#xff09;&#xff0c;集成了Qt所提供的功能&#xff0c;可以单独下载使用&#xff0c;也可以结合Qt组合使用。 二、下载 下载地址&#xff1a;https://downloa…

2023 ChinaJoy | 移远通信携手高通,共创数字娱乐新体验

当前&#xff0c; 5G、AI、大数据等智能创新技术正以惊人的速度蔓延至越来越多的领域&#xff0c;从智能家居、智能交通、智能医疗到智能制造&#xff0c;改变了我们的工作和生活方式。 而在数字娱乐领域&#xff0c;智能创新技术也展现出了巨大的潜力。作为全球领先的物联网整…

13.5.5 【Linux】其他相关文件

除了前一小节谈到的 /etc/securetty 会影响到 root 可登陆的安全终端机&#xff0c; /etc/nologin 会影响到一般使用者是否能够登陆的功能之外&#xff0c;我们也知道 PAM 相关的配置文件在 /etc/pam.d &#xff0c;说明文档在 /usr/share/doc/pam-&#xff08;版本&#xff09…

牛客网Verilog刷题——VL47

牛客网Verilog刷题——VL47 题目答案 题目 实现4bit位宽的格雷码计数器。 电路的接口如下图所示&#xff1a; 输入输出描述&#xff1a; 信号类型输入/输出位宽描述clkwireIntput1时钟信号rst_nwireIntput1异步复位信号&#xff0c;低电平有效gray_outregOutput4输出格雷码计数…

管理ceph集群

文章目录 ceph的常用命令查看集群状态查看pg的状态查看mon节点状态查看osd的通用命令查看osd的容量查看osd池写入文件测试查看池的属性查看文件映射过程 添加磁盘删除磁盘 ceph的常用命令 查看集群状态 ceph osd pool application enable pool-name rbd #将池启用rbd功能 ceph…

Java集合框架-List、Set、Map

一、Java集合框架概述&#xff1a; 1.1 Collection接口继承树 JDK提供的集合API位于java.util包内。 Map接口继承树 1.2 Collection接口方法 Collection 接口 Collection 接口是 List、Set 和 Queue 接口的父接口&#xff0c;该接口里定义的方法既可用于操作 Set 集合&#…

Matlab进阶绘图第24期—悬浮柱状图

悬浮柱状图是一种特殊的柱状图。 与常规柱状图相比&#xff0c;悬浮柱状图可以通过悬浮的矩形展示最小值到最大值的范围&#xff08;或其他范围表达&#xff09;&#xff0c;因此在多个领域得到应用。 本文使用自己制作的Floatingbar小工具进行悬浮柱状图的绘制&#xff0c;先…

【Linux】Linux项目自动化构建工具 make/Makefile

目录 1. Makefile 是如何工作的 2. 依赖关系与依赖方法 3. .PHONY 4. Makefile 的特殊符号 写在最后&#xff1a; 1. Makefile 是如何工作的 make 是一个命令。 Makefile 是一个文件&#xff0c;当前目录下的一个文件。 我们先来上手写一个简单的 Makefile 文件&#…

火山引擎AB测试:广告实验深度打通巨量引擎,高效测试广告素材

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 近期&#xff0c;火山引擎AB测试DataTester上线了新版的广告AB实验&#xff0c;还推出了与巨量引擎深度打通的能力。用户可以在DataTester中直接进行广告落地页的创…

机器学习:异常检测

问题定义 anomaly&#xff0c;outlier&#xff0c; novelty&#xff0c; exceptions 不同的方法使用不同的名词定义这类问题。 应用 二分类 假如只有正常的数据&#xff0c;而异常的数据的范围非常广的话&#xff08;无法穷举&#xff09;&#xff0c;二分类这些不好做。另外就…

没钱,没人,没IT,如何成功进行数字化转型?

在数字化时代的浪潮下&#xff0c;技术的迅猛发展为企业和个人带来了前所未有的机遇和挑战。然而&#xff0c;传统的软件开发过程通常需要大量的编程工作&#xff0c;对于非技术背景的人士而言&#xff0c;这是一座高不可攀的技术门槛。如何能够快速地实现创新&#xff0c;推动…

YAML+PyYAML笔记 9 | PyYAML源码之dump(),dump_all(),safe_dump(),yaml.YAMLObject

9 | PyYAML源码之emit&#xff0c;dump&#xff0c;safe_dump&#xff0c;yaml.YAMLObject 1 yaml.dump()2 yaml.dump()3 yaml.safe_dump4 yaml.YAMLObject 1 yaml.dump() 源码&#xff1a; 作用&#xff1a; 将Python对象序列化为YAML流。如果stream为None&#xff0c;则返…

无涯教程-jQuery - Tooltip组件函数

小部件工具提示功能可与JqueryUI中的小部件一起使用。Tooltip用于显示提示。 Tooltip - 语法 $( document ).tooltip(); Tooltip - 示例 以下是显示工具提示用法的简单示例- <!doctype html> <html lang"en"><head><meta charset"utf…

【腾讯云 Cloud Studio 实战训练营】在Cloud Studio上使用React实现学生管理系统

一、前言 为了提升办公效率&#xff0c;越来越多的人开始使用云工作站的方法进行“云办公”了&#xff0c;所谓云办公即把传统意义办公场所转移到网络浏览器中&#xff0c;这样做的好处有&#xff1a; 实现真正的移动化办公&#xff0c;不再受设备、时间、地点等的束缚&#…

最近写了10篇Java技术博客【SQL和画图组件】

&#xff08;1&#xff09;Java获取SQL语句中的表名 &#xff08;2&#xff09;Java SQL 解析器实践 &#xff08;3&#xff09;Java SQL 格式化实践 &#xff08;4&#xff09;Java 画图 画图组件jgraphx项目整体介绍&#xff08;一&#xff09; 画图组件jgraphx项目导出…

Java版知识付费 Spring Cloud+Spring Boot+Mybatis+uniapp+前后端分离实现知识付费平台免费搭建

提供职业教育、企业培训、知识付费系统搭建服务。系统功能包含&#xff1a;录播课、直播课、题库、营销、公司组织架构、员工入职培训等。 提供私有化部署&#xff0c;免费售后&#xff0c;专业技术指导&#xff0c;支持PC、APP、H5、小程序多终端同步&#xff0c;支持二次开发…

VsCode与Idea编辑器更换背景图

目录 VsCode Idea VsCode 需要安装background插件 安装完成后&#xff0c;打开设置&#xff0c;搜索background 然后就可以在json文件进行图片设置&#xff0c;透明度等等 Idea 打开File -> Settings 然后找到Appearance &#xff0c; 往下滑&#xff0c;找到BackGround …

Android studio实现Button界面跳转

本教程以界面MainActivity跳转到TwoActivity为例 MainActivity对应layout&#xff1a;R.layout.activity_main TwoActivity*对应layout&#xff1a;R.layout.twolayout 1. 建立MainActivity的java文件 package com.example.myapplication;//界面跳转 所需头文件 import andro…

在人工智能的浪潮下,汽车自动驾驶的新发展新可能

原创 | 文 BFT机器人 相信生活在21世纪的人们&#xff0c;没有人会不知道什么是人工智能。如果你不知道&#xff0c;那么你就是被现社会甩在了身后。人工智能从出现到现在&#xff0c;经历了60年的发展&#xff0c;而如今人工智能更如一股大浪潮&#xff0c;席卷了全国各地&am…