NativeScript 手势处理 #

手势概述 #

NativeScript 提供了丰富的手势识别系统,支持各种触摸交互。

text
┌─────────────────────────────────────────────────────────────┐
│                    手势类型                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  基本手势                                                    │
│  ├── tap          点击                                      │
│  ├── doubleTap    双击                                      │
│  ├── longPress    长按                                      │
│  └── touch        触摸事件                                  │
│                                                             │
│  滑动手势                                                    │
│  ├── swipe        滑动                                      │
│  ├── pan          拖拽                                      │
│  └── pinch        捏合                                      │
│                                                             │
│  复合手势                                                    │
│  ├── rotation     旋转                                      │
│  └── 组合手势                                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

基本手势 #

点击手势 (tap) #

xml
<!-- XML 中绑定 -->
<Button text="Click Me" tap="onTap" />
<Image src="~/image.png" tap="onImageTap" />
typescript
// TypeScript 处理
export function onTap(args: EventData) {
    const button = args.object as Button;
    console.log('Button tapped');
}

双击手势 (doubleTap) #

xml
<Label text="Double Tap Me" doubleTap="onDoubleTap" />
typescript
export function onDoubleTap(args: EventData) {
    console.log('Double tapped');
}

长按手势 (longPress) #

xml
<Button text="Long Press Me" longPress="onLongPress" />
typescript
export function onLongPress(args: EventData) {
    console.log('Long pressed');
}

触摸事件 (touch) #

xml
<GridLayout touch="onTouch">
    <Label text="Touch Area" />
</GridLayout>
typescript
import { TouchGestureEventData } from '@nativescript/core';

export function onTouch(args: TouchGestureEventData) {
    const action = args.action;
    
    switch (action) {
        case 'down':
            console.log('Touch started');
            break;
        case 'move':
            console.log('Touch moved', args.getX(), args.getY());
            break;
        case 'up':
            console.log('Touch ended');
            break;
    }
}

滑动手势 #

滑动手势 (swipe) #

xml
<GridLayout swipe="onSwipe" class="swipe-area">
    <Label text="Swipe Me" />
</GridLayout>
typescript
import { SwipeGestureEventData } from '@nativescript/core';

export function onSwipe(args: SwipeGestureEventData) {
    const direction = args.direction;
    
    switch (direction) {
        case SwipeDirection.left:
            console.log('Swiped left');
            break;
        case SwipeDirection.right:
            console.log('Swiped right');
            break;
        case SwipeDirection.up:
            console.log('Swiped up');
            break;
        case SwipeDirection.down:
            console.log('Swiped down');
            break;
    }
}

拖拽手势 (pan) #

xml
<GridLayout pan="onPan" class="pan-area">
    <Label text="Drag Me" id="draggable" />
</GridLayout>
typescript
import { PanGestureEventData } from '@nativescript/core';

let startX = 0;
let startY = 0;

export function onPan(args: PanGestureEventData) {
    const view = args.object as View;
    
    switch (args.state) {
        case GestureStateTypes.began:
            startX = view.translateX;
            startY = view.translateY;
            break;
            
        case GestureStateTypes.changed:
            view.translateX = startX + args.deltaX;
            view.translateY = startY + args.deltaY;
            break;
            
        case GestureStateTypes.ended:
            console.log('Pan ended');
            break;
    }
}

捏合手势 (pinch) #

xml
<Image src="~/image.png" pinch="onPinch" id="pinchable" />
typescript
import { PinchGestureEventData } from '@nativescript/core';

let initialScale = 1;

export function onPinch(args: PinchGestureEventData) {
    const view = args.object as View;
    
    switch (args.state) {
        case GestureStateTypes.began:
            initialScale = view.scaleX;
            break;
            
        case GestureStateTypes.changed:
            const newScale = initialScale * args.scale;
            view.scaleX = newScale;
            view.scaleY = newScale;
            break;
    }
}

旋转手势 (rotation) #

xml
<Image src="~/image.png" rotation="onRotation" id="rotatable" />
typescript
import { RotationGestureEventData } from '@nativescript/core';

let initialRotation = 0;

export function onRotation(args: RotationGestureEventData) {
    const view = args.object as View;
    
    switch (args.state) {
        case GestureStateTypes.began:
            initialRotation = view.rotation;
            break;
            
        case GestureStateTypes.changed:
            view.rotation = initialRotation + args.rotation;
            break;
    }
}

使用 Gestures 模块 #

编程式添加手势 #

typescript
import { Gestures, GestureTypes, GestureEventData } from '@nativescript/core';

const view = page.getViewById('myView');

// 添加点击手势
view.on(GestureTypes.tap, (args: GestureEventData) => {
    console.log('Tapped');
});

// 添加滑动手势
view.on(GestureTypes.swipe, (args: SwipeGestureEventData) => {
    console.log('Swiped:', args.direction);
});

// 添加多个手势
view.on(GestureTypes.tap | GestureTypes.doubleTap, (args) => {
    console.log('Gesture event');
});

// 移除手势
view.off(GestureTypes.tap);

手势观察者 #

typescript
import { GestureTypes, observe } from '@nativescript/core';

const observer = observe(view, GestureTypes.tap, (args) => {
    console.log('Tap observed');
});

// 取消观察
observer.disconnect();

手势服务 #

typescript
// services/gesture.service.ts
import { Injectable } from '@angular/core';
import { 
    View, 
    GestureTypes, 
    GestureEventData,
    SwipeGestureEventData,
    PanGestureEventData,
    PinchGestureEventData
} from '@nativescript/core';

@Injectable({
    providedIn: 'root'
})
export class GestureService {
    onTap(view: View, callback: (args: GestureEventData) => void): void {
        view.on(GestureTypes.tap, callback);
    }
    
    onDoubleTap(view: View, callback: (args: GestureEventData) => void): void {
        view.on(GestureTypes.doubleTap, callback);
    }
    
    onLongPress(view: View, callback: (args: GestureEventData) => void): void {
        view.on(GestureTypes.longPress, callback);
    }
    
    onSwipe(view: View, callback: (args: SwipeGestureEventData) => void): void {
        view.on(GestureTypes.swipe, callback);
    }
    
    onPan(view: View, callback: (args: PanGestureEventData) => void): void {
        view.on(GestureTypes.pan, callback);
    }
    
    onPinch(view: View, callback: (args: PinchGestureEventData) => void): void {
        view.on(GestureTypes.pinch, callback);
    }
    
    enableDraggable(view: View, bounds?: { minX: number, maxX: number, minY: number, maxY: number }): void {
        let startX = 0;
        let startY = 0;
        
        view.on(GestureTypes.pan, (args: PanGestureEventData) => {
            switch (args.state) {
                case GestureStateTypes.began:
                    startX = view.translateX;
                    startY = view.translateY;
                    break;
                    
                case GestureStateTypes.changed:
                    let newX = startX + args.deltaX;
                    let newY = startY + args.deltaY;
                    
                    if (bounds) {
                        newX = Math.max(bounds.minX, Math.min(bounds.maxX, newX));
                        newY = Math.max(bounds.minY, Math.min(bounds.maxY, newY));
                    }
                    
                    view.translateX = newX;
                    view.translateY = newY;
                    break;
            }
        });
    }
    
    enablePinchZoom(view: View, minScale: number = 0.5, maxScale: number = 3): void {
        let initialScale = 1;
        
        view.on(GestureTypes.pinch, (args: PinchGestureEventData) => {
            switch (args.state) {
                case GestureStateTypes.began:
                    initialScale = view.scaleX;
                    break;
                    
                case GestureStateTypes.changed:
                    const newScale = Math.max(minScale, Math.min(maxScale, initialScale * args.scale));
                    view.scaleX = newScale;
                    view.scaleY = newScale;
                    break;
            }
        });
    }
    
    enableSwipeNavigation(view: View, callbacks: {
        onLeft?: () => void,
        onRight?: () => void,
        onUp?: () => void,
        onDown?: () => void
    }): void {
        view.on(GestureTypes.swipe, (args: SwipeGestureEventData) => {
            switch (args.direction) {
                case SwipeDirection.left:
                    callbacks.onLeft?.();
                    break;
                case SwipeDirection.right:
                    callbacks.onRight?.();
                    break;
                case SwipeDirection.up:
                    callbacks.onUp?.();
                    break;
                case SwipeDirection.down:
                    callbacks.onDown?.();
                    break;
            }
        });
    }
}

实用示例 #

可拖拽视图 #

typescript
export class DraggableView {
    private startX = 0;
    private startY = 0;
    
    constructor(private view: View) {
        this.setupDrag();
    }
    
    private setupDrag(): void {
        this.view.on(GestureTypes.pan, (args: PanGestureEventData) => {
            switch (args.state) {
                case GestureStateTypes.began:
                    this.startX = this.view.translateX;
                    this.startY = this.view.translateY;
                    this.view.animate({ scale: { x: 1.1, y: 1.1 }, duration: 100 });
                    break;
                    
                case GestureStateTypes.changed:
                    this.view.translateX = this.startX + args.deltaX;
                    this.view.translateY = this.startY + args.deltaY;
                    break;
                    
                case GestureStateTypes.ended:
                    this.view.animate({ scale: { x: 1, y: 1 }, duration: 100 });
                    break;
            }
        });
    }
}

双指缩放图片 #

typescript
export class ZoomableImageView {
    private initialScale = 1;
    private minScale = 0.5;
    private maxScale = 3;
    
    constructor(private view: View) {
        this.setupZoom();
    }
    
    private setupZoom(): void {
        this.view.on(GestureTypes.pinch, (args: PinchGestureEventData) => {
            switch (args.state) {
                case GestureStateTypes.began:
                    this.initialScale = this.view.scaleX;
                    break;
                    
                case GestureStateTypes.changed:
                    const newScale = Math.max(
                        this.minScale,
                        Math.min(this.maxScale, this.initialScale * args.scale)
                    );
                    this.view.scaleX = newScale;
                    this.view.scaleY = newScale;
                    break;
            }
        });
        
        // 双击重置
        this.view.on(GestureTypes.doubleTap, () => {
            this.view.animate({
                scale: { x: 1, y: 1 },
                duration: 200
            });
        });
    }
}

滑动切换页面 #

typescript
export class SwipeNavigator {
    constructor(
        private view: View,
        private pages: string[],
        private currentIndex: number = 0
    ) {
        this.setupSwipe();
    }
    
    private setupSwipe(): void {
        this.view.on(GestureTypes.swipe, (args: SwipeGestureEventData) => {
            if (args.direction === SwipeDirection.left) {
                this.nextPage();
            } else if (args.direction === SwipeDirection.right) {
                this.prevPage();
            }
        });
    }
    
    private nextPage(): void {
        if (this.currentIndex < this.pages.length - 1) {
            this.currentIndex++;
            this.navigate();
        }
    }
    
    private prevPage(): void {
        if (this.currentIndex > 0) {
            this.currentIndex--;
            this.navigate();
        }
    }
    
    private navigate(): void {
        Frame.topmost().navigate({
            moduleName: this.pages[this.currentIndex],
            transition: { name: 'slide' }
        });
    }
}

下拉刷新 #

typescript
export class PullToRefresh {
    private startY = 0;
    private threshold = 100;
    private isRefreshing = false;
    
    constructor(
        private view: View,
        private onRefresh: () => Promise<void>
    ) {
        this.setupPullToRefresh();
    }
    
    private setupPullToRefresh(): void {
        this.view.on(GestureTypes.pan, (args: PanGestureEventData) => {
            if (this.isRefreshing) return;
            
            switch (args.state) {
                case GestureStateTypes.began:
                    this.startY = this.view.translateY;
                    break;
                    
                case GestureStateTypes.changed:
                    if (args.deltaY > 0) {
                        this.view.translateY = Math.min(args.deltaY * 0.5, this.threshold);
                    }
                    break;
                    
                case GestureStateTypes.ended:
                    if (this.view.translateY >= this.threshold) {
                        this.triggerRefresh();
                    } else {
                        this.resetPosition();
                    }
                    break;
            }
        });
    }
    
    private async triggerRefresh(): Promise<void> {
        this.isRefreshing = true;
        
        try {
            await this.onRefresh();
        } finally {
            this.isRefreshing = false;
            this.resetPosition();
        }
    }
    
    private resetPosition(): void {
        this.view.animate({
            translate: { x: 0, y: 0 },
            duration: 200
        });
    }
}

手势冲突处理 #

同时识别多个手势 #

typescript
view.on(GestureTypes.tap | GestureTypes.doubleTap, (args) => {
    if (args.type === GestureTypes.tap) {
        // 处理单击
    } else if (args.type === GestureTypes.doubleTap) {
        // 处理双击
    }
});

手势优先级 #

typescript
// 使用延迟处理单击,避免与双击冲突
let tapTimeout: number;

view.on(GestureTypes.tap, () => {
    tapTimeout = setTimeout(() => {
        console.log('Single tap');
    }, 300);
});

view.on(GestureTypes.doubleTap, () => {
    clearTimeout(tapTimeout);
    console.log('Double tap');
});

最佳实践 #

手势设计原则 #

text
┌─────────────────────────────────────────────────────────────┐
│                    手势设计原则                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 直观性                                                  │
│     手势应该直观易懂                                        │
│                                                             │
│  2. 一致性                                                  │
│     与系统手势保持一致                                      │
│                                                             │
│  3. 反馈                                                    │
│     提供视觉或触觉反馈                                      │
│                                                             │
│  4. 容错性                                                  │
│     允许用户取消手势操作                                    │
│                                                             │
│  5. 可发现性                                                │
│     提供手势提示或教程                                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

下一步 #

现在你已经掌握了手势处理,接下来学习 性能优化,了解如何优化应用性能!

最后更新:2026-03-29