本文之前发表在 Rensu Theart 的博客 上。
最近我开始学习 NativeScript 为了给朋友开发一个应用程序。之前我使用过 Android SDK,也使用 Unity 进行过一些开发。但是这个新的应用程序需要跨平台(即 iOS 和 Android),并且需要原生感觉,这使得 Unity 不适合这种情况。所以,在尝试了 Ionic 和 NativeScript 后,我非常喜欢 NativeScript 的原生性能和低级能力。
NativeScript 的另一个优点是它允许 100% 的代码复用,这与其他任何原生 API 都不一样。此外,我感觉 Angular 2 和 TypeScript 是 Web 和应用程序开发的未来,所以我决定学习 NativeScript 的 Angular 版本。但这已经足够了,现在回到这篇文章标题的主题:如何在 NativeScript 中实现拖放 UI 元素的能力 - 以及如何在操作中限制边界框内的移动。
检测 UI 元素上触摸屏输入的整个过程方便地封装在 NativeScript 的手势 API 中。在 NativeScript 中,所有 UI 元素都继承自 View
类,该类具有 on
和 off
方法,允许你订阅或取消订阅 UI 元素检测到的所有事件和手势。然后,你可以将要检测的手势类型以及该手势的回调函数传递给这些方法。
或者,你可以使用 Angular 2 的魔力,只需将你的 TypeScript 函数连接到你在 XML 中定义 UI 元素时描述的事件即可。我觉得在 Angular 实现的代码中,这更容易理解,所以我们将在下面这样做。
你可以在 这里 阅读更多详细信息,包括可以检测哪些手势(API 中包含了你可以在应用程序中想象到的几乎所有内容)。
我们需要检测拖放事件的手势名称称为“平移”。在 NativeScript 文档中,它被描述为“操作:按下你的手指并立即开始移动它。平移执行得更慢,允许更高的精度,并且屏幕在手指离开它时立即停止响应。”
从 NativeScript 文档中,用法如下
对于 TypeScript 侧
onPan(args: PanGestureEventData) {
console.log("Pan delta: [" + args.deltaX + ", " + args.deltaY + "] state: " + args.state);
}
对于 XML 侧
<Label text="Pan here" (pan)="onPan($event)"></Label>
所以它实际上并没有什么特别的。在 XML 中,在你定义元素的地方,你只需添加 (pan)
事件并在你的 TypeScript 组件中提供回调函数。你将 PanGestureEventData
数据通过 $event
参数传递给函数。在你的函数中,你可以访问几个事件属性。我们关心的三个主要属性是 deltaX
、deltaY
(它指的是上次更新中平移的设备无关像素数量)和 state
(即手指按下、抬起或按下等)属性。
好了,现在让我们创建一个使用它来移动图片的简单应用程序。
我从 这个 Stack Overflow 页面 中获得灵感,它使用的是原生 JavaScript 而不是 Angular。
为了演示这个,让我们使用 NativeScript Angular 教程模板作为基础项目模板。在命令提示符中输入以下内容(显然你需要先设置 NativeScript。说明可以在 这里 找到)
tns create DragDrop --template nativescript-template-ng-tutorial
在模板 XML 代码下的 app.component.ts 文件中添加以下代码
<ActionBar title="Drag and Drop">
<GridLayout width="100%" height="400" backgroundColor="gray">
<Image #dragImage width="100" height="100" (pan)="onPan($event)" src="~/images/apple.jpg"></Image>
</GridLayout>
注意,我们为 Image 提供了 本地模板变量 #dragImage
,我们将在后面引用它。另外请注意,我们将回调函数 onPan
(我们将在后面创建)附加到 NativeScript 事件 (pan)
上。因此,当图像被平移(用户用手指拖动图像)时,onPan
函数将被调用。
另外,在文件顶部添加以下导入,我们将在后面需要它
import { Component, ElementRef, OnInit, ViewChild } from "@angular/core";
import { PanGestureEventData } from "ui/gestures";
import { Image } from "ui/image";
在类顶部定义以下变量
@ViewChild("dragImage") dragImage: ElementRef;
dragImageItem: Image;
prevDeltaX: number;
prevDeltaY: number;
上面唯一复杂的代码是我们在使用 Angular 的 @ViewChild 装饰器 和我们的模板变量 dragImage
(我们在 XML 代码中定义的)来检索 UI Image 元素。但是,在我们使用这个图像之前,我们需要将它存储在一个 Image
变量中,我们接下来定义它。然后有两个 delta 移动变量,我们需要使用它们来执行平移。
最佳实践是在 ngOnInit
函数中初始化变量,而不是在构造函数中,这是我们从 OnInit
类继承的。因此,让 AppComponent 实现 OnInit,如下所示
export class AppComponent implements OnInit
然后将以下函数添加到类中(在变量声明下)
public ngOnInit() {
this.dragImageItem = <Image>this.dragImage.nativeElement;
this.dragImageItem.translateX = 0;
this.dragImageItem.translateY = 0;
this.dragImageItem.scaleX = 1;
this.dragImageItem.scaleY = 1
}
接下来我们实现 onPan
函数,它将所有内容整合在一起。
onPan(args: PanGestureEventData) {
//console.log("Pana: [" + args.deltaX + ", " + args.deltaY + "] state: " + args.state);
if (args.state === 1) // down
{
this.prevDeltaX = 0;
this.prevDeltaY = 0;
}
else if (args.state === 2) // panning
{
this.dragImageItem.translateX += args.deltaX - this.prevDeltaX;
this.dragImageItem.translateY += args.deltaY - this.prevDeltaY;
this.prevDeltaX = args.deltaX;
this.prevDeltaY = args.deltaY;
}
else if (args.state === 3) // up
{
}
}
在这个函数中,我们首先检查平移手势处于哪个状态。当用户最初触摸 UI 元素时,我们重置平移变量。当用户进行平移时,Image 的 translateX 和 translateY 属性仅使用最后一帧的平移变化进行更新。这些平移属性与 CSS 动画属性的工作原理相同,因此它指的是从原点的平移。这就是我们累积改变这些属性的原因。图像的位置现在将根据平移手势进行更新。
如果我们运行代码,就会得到这里所示的效果
现在让我们更进一步,添加一些碰撞检测。这对于许多应用程序来说非常有用,例如在游戏中收集物品或不允许用户将图像移动到定义区域之外,这就是我们将要做的。为了演示如何将碰撞检测与 UI 元素的平移结合起来,我们将扩展到目前为止的内容。
首先,将本地模板变量 #container
添加到 GridLayout 中。在这个例子中,我们还将确保图像从左上角开始,通过添加 horizontalAlignment
和 verticalAlignment
属性。
<GridLayout #container width="100%" height="400" backgroundColor="gray">
<Image #dragImage width="100" height="100" horizontalAlignment="left" verticalAlignment="top" (pan)="onPan($event)" src="~/images/apple.jpg"></Image>
</GridLayout>
我们还需要在 app.component.ts 文件的顶部添加以下导入。
import { GridLayout } from "ui/layouts/grid-layout";
import { AnimationCurve } from "ui/enums";
与之前类似,由于我们现在想知道边界框(GridLayout
)的边缘在哪里,我们还需要从我们的 XML 中检索 GridLayout
。
@ViewChild("container") container: ElementRef;
itemContainer: GridLayout;
然后,我们再次将以下行添加到我们的 ngOnInit
函数中。
this.itemContainer = <GridLayout>this.container.nativeElement;
现在回到我们的 onPan
函数。我们将使用之前使用的代码,但现在将其扩展为限制图像只能在边界容器内移动。
onPan(args: PanGestureEventData) {
//console.log("Pana: [" + args.deltaX + ", " + args.deltaY + "] state: " + args.state);
if (args.state === 1) // down
{
this.prevDeltaX = 0;
this.prevDeltaY = 0;
}
else if (args.state === 2) // panning
{
this.dragImageItem.translateX += args.deltaX - this.prevDeltaX;
this.dragImageItem.translateY += args.deltaY - this.prevDeltaY;
this.prevDeltaX = args.deltaX;
this.prevDeltaY = args.deltaY;
// calculate the conversion from DP to pixels
let convFactor = this.dragImageItem.width / this.dragImageItem.getMeasuredWidth();
let edgeX = (this.itemContainer.getMeasuredWidth() - this.dragImageItem.getMeasuredWidth()) * convFactor;
let edgeY = (this.itemContainer.getMeasuredHeight() - this.dragImageItem.getMeasuredHeight()) * convFactor;
// X border
if (this.dragImageItem.translateX < 0) {
this.dragImageItem.translateX = 0;
}
else if (this.dragImageItem.translateX > edgeX) {
this.dragImageItem.translateX = edgeX;
}
// Y border
if (this.dragImageItem.translateY < 0) {
this.dragImageItem.translateY = 0;
}
else if (this.dragImageItem.translateY > edgeY) {
this.dragImageItem.translateY = edgeY;
}
}
else if (args.state === 3) // up
{
this.dragImageItem.animate({
translate: { x: 0, y: 0 },
duration: 1000,
curve: AnimationCurve.cubicBezier(0.1, 0.1, 0.1, 1)
});
}
}
解释几点。我们遇到了两个不同的坐标系被使用的问题,而且没有直接的转换方法。PanGestureEventData 的 deltaX
和 deltaY
变量使用的是设备无关像素(dp),它与 UI 元素的 View.width 变量返回的值相同。但是,对于 GridLayout,由于没有定义固定宽度,所以这个变量返回“100%”,在 dp 平移变量比较方面不太有用。使用星号 (*) 布局属性定义的元素也会出现同样的问题。所以,由于容器在调用 View.width 时没有返回所需的宽度,所以我们需要使用一个技巧。现在显然有很多方法可以做到这一点,例如检索页面的宽度,但更简单的方法是使用已知的 dp 宽度(在本例中为 100)定义图像,然后使用 getMeasuredWidth()
函数,该函数返回屏幕上的像素宽度,然后通过简单的除法运算就可以计算出转换系数。然后,我们可以将像素的宽度和高度乘以这个转换系数,就能得到容器的 dp 宽度。
现在我们知道了容器的边缘在哪里,我们只需使用四个 if 语句来确保图像不会移动到这个边框之外。然后,作为一种很好的效果,每当用户抬起手指时,图像都会自动使用动画滑回原点位置。
以下是在实践中所有这些内容的示例
在这篇文章中,我们学习了如何使用 NativeScript 拖放图像。这个相同的原则可以应用于 NativeScript 的任何 UI 元素,只需对代码进行一些简单的调整。我们还学习了如何将 UI 元素的宽度从屏幕像素转换为设备无关像素。这使我们能够实现一个基本的碰撞检测系统。最后,我们实现了一些基本的动画,使 UI 元素滑回原点位置。
感谢阅读,请留下一些反馈!