Ir al contenido
jolt.
← Recetas

TOQUE Y GESTOS / iOS / NATIVESCRIPT VUE

Mueve toda la escena.

El cockpit, el encuadre y las tarjetas siguen un mismo arrastre.

Del proyecto Jolt Drive ↗
Grabación nativa · iOSJolt Drive · simulador iPhone 17 Pro, iOS 26.2. Interacción real mediante idb en la app nativa, velocidad original. Interfaz en inglés; no es una grabación de dispositivo físico ni una medición de rendimiento.

El movimiento detrás.

Jolt 0.1.0-rc.10

El archivo completo del proyecto, fijado a la revisión verificada. Adáptalo con las dependencias descritas abajo.

Usa refs de NativeScript-Vue llamadas root (GestureSurface) y scrollContent (ScrollView), un motion value offset, posiciones height/closed medidas y estado active. Home y ControlSheet conectan ese offset con la cámara y las tarjetas; el hook no crea la escena 3D.

useCockpitGesture.ts
import { onMounted, onUnmounted, ref, useTemplateRef, watch } from 'nativescript-vue';
import type { ScrollView } from '@nativescript/core';
import { bind, set, type MotionValue } from '@rafaelaugusto/jolt';
import { GestureSurface, gestures, type Gestures } from '@rafaelaugusto/jolt/gestures';
import { useReducedMotion } from '@rafaelaugusto/jolt/vue';
import type { NativeElement } from '@/utils/native-element';

interface CockpitOptions {
	offset: MotionValue;
	height: number;
	closed: number;
	active: boolean;
}

export function useCockpitGesture(props: Readonly<CockpitOptions>) {
	const reduced = useReducedMotion();
	const root = useTemplateRef<NativeElement<GestureSurface>>('root');
	const scrollContent = useTemplateRef<NativeElement<ScrollView>>('scrollContent');
	const expanded = ref(false);
	let suppressTapUntil = 0;
	let panelGesture: Gestures | undefined;
	let releasePanelBinding: (() => void) | undefined;
	let releaseOffsetSync: (() => void) | undefined;
	let pendingMove: { target: number; resolve: (completed: boolean) => void } | undefined;
	const finishMove = (completed: boolean) => {
		const pending = pendingMove;
		pendingMove = undefined;
		pending?.resolve(completed);
	};
	const updateExpanded = () => {
		expanded.value = props.offset.get() < props.closed / 2;
	};
	const movePanel = (y: number) => {
		finishMove(false);
		if (!panelGesture) return Promise.resolve(false);
		const finished = new Promise<boolean>((resolve) => {
			pendingMove = { target: y, resolve };
		});
		if (reduced.value) panelGesture.jumpTo({ y });
		else panelGesture.settleTo({ y });
		if (Math.abs(panelGesture.position.y - y) < 0.1) finishMove(true);
		return finished;
	};
	const toggle = async () => {
		if (Date.now() < suppressTapUntil) return;
		const opening = (panelGesture?.position.y ?? props.offset.get()) > props.closed / 2;
		expanded.value = opening;
		movePanel(opening ? 0 : props.closed);
	};
	const close = async () => {
		expanded.value = false;
		await movePanel(props.closed);
	};
	const enter = () => {
		panelGesture?.jumpTo({ y: props.height });
		set(props.offset, props.height);
		void movePanel(props.closed).then((completed) => {
			if (completed) panelGesture?.updateBounds({ top: 0, bottom: props.closed });
		});
	};
	watch(
		() => props.active,
		(active) => {
			if (active) panelGesture?.play();
			else {
				panelGesture?.pause();
				finishMove(false);
			}
		},
	);
	onMounted(() => {
		const nativeRoot = root.value!.nativeView;
		const nativeScroll = scrollContent.value!.nativeView;
		panelGesture = gestures(nativeRoot, {
			initial: { y: props.offset.get() },
			pan: { axis: 'y', bounds: { top: 0, bottom: props.height }, failOffset: 18 },
			scrollView: nativeScroll,
			spring: { stiffness: 280, damping: 30 },
			onStart: () => {
				suppressTapUntil = Infinity;
				finishMove(false);
			},
			onEnd: (event) => {
				suppressTapUntil = Date.now() + 350;
				expanded.value = event.y + event.velocityY * 0.18 < props.closed / 2;
				panelGesture?.settleTo({ y: expanded.value ? 0 : props.closed });
			},
			onCancel: () => {
				suppressTapUntil = Date.now() + 350;
				updateExpanded();
			},
		});
		if (!panelGesture) return;
		releasePanelBinding = bind(root.value!.nativeView, { y: panelGesture.y });
		// SceneKit and gradient adapters still need a regular motion value.
		// Keep the gesture bridge scoped to this panel.
		releaseOffsetSync = panelGesture.y.subscribe((value) => {
			set(props.offset, value);
			expanded.value = value < props.closed / 2;
			if (pendingMove && Math.abs(value - pendingMove.target) < 0.1) finishMove(true);
		});
	});
	onUnmounted(() => {
		finishMove(false);
		releaseOffsetSync?.();
		releasePanelBinding?.();
		panelGesture?.dispose();
	});
	return { root, expanded, enter, close, toggle };
}
Ver demo completa ↗

Lee la guía completa ↗