mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-18 05:18:39 +08:00
feat(ui): TouchManager for ease in adding interactivity
This commit is contained in:
10
packages/core/ui/core/view-base/index.d.ts
vendored
10
packages/core/ui/core/view-base/index.d.ts
vendored
@ -229,6 +229,16 @@ export abstract class ViewBase extends Observable {
|
||||
*/
|
||||
public static unloadedEvent: string;
|
||||
|
||||
/**
|
||||
* String value used when hooking to creation event
|
||||
*/
|
||||
public static createdEvent: string;
|
||||
|
||||
/**
|
||||
* String value used when hooking to disposeNativeView event
|
||||
*/
|
||||
public static disposeNativeViewEvent: string;
|
||||
|
||||
public ios: any;
|
||||
public android: any;
|
||||
|
||||
|
@ -248,7 +248,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
public static loadedEvent = 'loaded';
|
||||
public static unloadedEvent = 'unloaded';
|
||||
public static createdEvent = 'created';
|
||||
public static disposeNativeView = 'disposeNativeView';
|
||||
public static disposeNativeViewEvent = 'disposeNativeView';
|
||||
|
||||
private _onLoadedCalled = false;
|
||||
private _onUnloadedCalled = false;
|
||||
@ -765,7 +765,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
|
||||
public disposeNativeView() {
|
||||
this.notify({
|
||||
eventName: ViewBase.disposeNativeView,
|
||||
eventName: ViewBase.disposeNativeViewEvent,
|
||||
object: this,
|
||||
});
|
||||
}
|
||||
|
4
packages/core/ui/core/view/index.d.ts
vendored
4
packages/core/ui/core/view/index.d.ts
vendored
@ -8,7 +8,9 @@ import { LinearGradient } from '../../styling/linear-gradient';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, AccessibilityEventOptions } from '../../../accessibility/accessibility-types';
|
||||
import { CoreTypes } from '../../../core-types';
|
||||
import { CSSShadow } from '../../styling/css-shadow';
|
||||
import { ViewCommon } from './view-common';
|
||||
|
||||
export * from './view-common';
|
||||
// helpers (these are okay re-exported here)
|
||||
export * from './view-helper';
|
||||
|
||||
@ -99,7 +101,7 @@ export interface ShownModallyData extends EventData {
|
||||
* This class is the base class for all UI components.
|
||||
* A View occupies a rectangular area on the screen and is responsible for drawing and layouting of all UI components within.
|
||||
*/
|
||||
export abstract class View extends ViewBase {
|
||||
export abstract class View extends ViewCommon {
|
||||
/**
|
||||
* String value used when hooking to layoutChanged event.
|
||||
*/
|
||||
|
@ -4,6 +4,7 @@ import { View as ViewDefinition, Point, Size, ShownModallyData } from '.';
|
||||
import { booleanConverter, ShowModalOptions, ViewBase } from '../view-base';
|
||||
import { getEventOrGestureName } from '../bindable';
|
||||
import { layout } from '../../../utils';
|
||||
import { isObject } from '../../../utils/types';
|
||||
import { Color } from '../../../color';
|
||||
import { Property, InheritedProperty } from '../properties';
|
||||
import { EventData } from '../../../data/observable';
|
||||
@ -13,7 +14,7 @@ import { ViewHelper } from './view-helper';
|
||||
|
||||
import { PercentLength } from '../../styling/style-properties';
|
||||
|
||||
import { observe as gestureObserve, GesturesObserver, GestureTypes, GestureEventData, fromString as gestureFromString } from '../../gestures';
|
||||
import { observe as gestureObserve, GesturesObserver, GestureTypes, GestureEventData, fromString as gestureFromString, TouchManager, TouchAnimationOptions } from '../../gestures';
|
||||
|
||||
import { CSSUtils } from '../../../css/system-classes';
|
||||
import { Builder } from '../../builder';
|
||||
@ -81,6 +82,9 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
public accessibilityValue: string;
|
||||
public accessibilityHint: string;
|
||||
|
||||
public touchAnimation: boolean | TouchAnimationOptions;
|
||||
public ignoreTouchAnimation: boolean;
|
||||
|
||||
protected _closeModalCallback: Function;
|
||||
public _manager: any;
|
||||
public _modalParent: ViewCommon;
|
||||
@ -153,6 +157,17 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded() {
|
||||
if (!this.isLoaded) {
|
||||
const enableTapAnimations = TouchManager.enableGlobalTapAnimations && (this.hasListeners('tap') || this.hasListeners('tapChange') || this.getGestureObservers(GestureTypes.tap));
|
||||
if (!this.ignoreTouchAnimation && (this.touchAnimation || enableTapAnimations)) {
|
||||
console.log('view:', Object.keys((<any>this)._observers));
|
||||
TouchManager.addAnimations(this);
|
||||
}
|
||||
}
|
||||
super.onLoaded();
|
||||
}
|
||||
|
||||
public _closeAllModalViewsInternal(): boolean {
|
||||
if (_rootModalViews && _rootModalViews.length > 0) {
|
||||
_rootModalViews.forEach((v) => {
|
||||
@ -399,7 +414,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract _hideNativeModalView(parent: ViewCommon, whenClosedCallback: () => void);
|
||||
protected _hideNativeModalView(parent: ViewCommon, whenClosedCallback: () => void) {}
|
||||
|
||||
protected _raiseLayoutChangedEvent() {
|
||||
const args: EventData = {
|
||||
@ -1151,7 +1166,31 @@ export const iosIgnoreSafeAreaProperty = new InheritedProperty({
|
||||
valueConverter: booleanConverter,
|
||||
});
|
||||
iosIgnoreSafeAreaProperty.register(ViewCommon);
|
||||
accessibilityIdentifierProperty.register(ViewCommon);
|
||||
|
||||
const touchAnimationProperty = new Property<ViewCommon, boolean | TouchAnimationOptions>({
|
||||
name: 'touchAnimation',
|
||||
valueChanged(view, oldValue, newValue) {
|
||||
view.touchAnimation = newValue;
|
||||
},
|
||||
valueConverter(value) {
|
||||
if (isObject(value)) {
|
||||
return <any>value;
|
||||
} else {
|
||||
return booleanConverter(value);
|
||||
}
|
||||
},
|
||||
});
|
||||
touchAnimationProperty.register(ViewCommon);
|
||||
|
||||
const ignoreTouchAnimationProperty = new Property<ViewCommon, boolean>({
|
||||
name: 'ignoreTouchAnimation',
|
||||
valueChanged(view, oldValue, newValue) {
|
||||
view.ignoreTouchAnimation = newValue;
|
||||
},
|
||||
valueConverter: booleanConverter,
|
||||
});
|
||||
ignoreTouchAnimationProperty.register(ViewCommon);
|
||||
|
||||
accessibilityLabelProperty.register(ViewCommon);
|
||||
accessibilityValueProperty.register(ViewCommon);
|
||||
accessibilityHintProperty.register(ViewCommon);
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { GestureEventData, GesturesObserver as GesturesObserverDefinition } from '.';
|
||||
import { View } from '../core/view';
|
||||
|
||||
export * from './touch-manager';
|
||||
|
||||
export enum GestureEvents {
|
||||
gestureAttached = 'gestureAttached',
|
||||
touchDown = 'touchDown',
|
||||
touchUp = 'touchUp',
|
||||
}
|
||||
|
||||
export enum GestureTypes {
|
||||
|
10
packages/core/ui/gestures/index.d.ts
vendored
10
packages/core/ui/gestures/index.d.ts
vendored
@ -1,6 +1,8 @@
|
||||
import { View } from '../core/view';
|
||||
import { EventData } from '../../data/observable';
|
||||
|
||||
export * from './touch-manager';
|
||||
|
||||
/**
|
||||
* Events emitted during gesture lifecycle
|
||||
*/
|
||||
@ -10,6 +12,14 @@ export enum GestureEvents {
|
||||
* Provides access to the native gesture recognizer for further customization
|
||||
*/
|
||||
gestureAttached = 'gestureAttached',
|
||||
/**
|
||||
* When a touch down was detected
|
||||
*/
|
||||
touchDown = 'touchDown',
|
||||
/**
|
||||
* When a touch up was detected
|
||||
*/
|
||||
touchUp = 'touchUp',
|
||||
}
|
||||
|
||||
/**
|
||||
|
251
packages/core/ui/gestures/touch-manager.ts
Normal file
251
packages/core/ui/gestures/touch-manager.ts
Normal file
@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Provides various helpers for adding easy touch handling animations.
|
||||
* Use when needing to implement more interactivity with your UI regarding touch down/up behavior.
|
||||
*/
|
||||
import { GestureEventData, GestureEventDataWithState, TouchGestureEventData } from '.';
|
||||
import { Animation } from '../animation';
|
||||
import { AnimationDefinition } from '../animation/animation-interfaces';
|
||||
import { View } from '../core/view';
|
||||
import { isObject, isFunction } from '../../utils/types';
|
||||
import { GestureEvents, GestureStateTypes, GestureTypes } from './gestures-common';
|
||||
|
||||
export type TouchAnimationFn = (view: View) => void;
|
||||
export type TouchAnimationOptions = {
|
||||
up?: TouchAnimationFn | AnimationDefinition;
|
||||
down?: TouchAnimationFn | AnimationDefinition;
|
||||
};
|
||||
export enum TouchAnimationTypes {
|
||||
up = 'up',
|
||||
down = 'down',
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage interactivity in your apps easily with TouchManager.
|
||||
* Store reusable down/up animation settings for touches as well as optionally enable automatic tap (down/up) animations for your app.
|
||||
*/
|
||||
export class TouchManager {
|
||||
/**
|
||||
* Enable animations for all tap bindings in the UI.
|
||||
*/
|
||||
static enableGlobalTapAnimations: boolean;
|
||||
/**
|
||||
* Define reusable touch animations to use on views with touchAnimation defined or with enableGlobalTapAnimations on.
|
||||
*/
|
||||
static animations: TouchAnimationOptions;
|
||||
/**
|
||||
* Native Touch handlers (iOS only) registered with the view through the TouchManager.
|
||||
* The TouchManager uses this internally but makes public for other versatility if needed.
|
||||
*/
|
||||
static touchHandlers: Array<{ view: View; handler: any /* UIGestureRecognizer */ }>;
|
||||
/**
|
||||
* When using NativeScript AnimationDefinition's for touch animations this will contain any instances for finer grain control of starting/stopping under various circumstances.
|
||||
* The TouchManager uses this internally but makes public for other versatility if needed.
|
||||
*/
|
||||
static touchAnimationDefinitions: Array<{ view: View; animation: Animation; type: TouchAnimationTypes }>;
|
||||
/**
|
||||
* The TouchManager uses this internally.
|
||||
* Adds touch animations to view based upon it's touchAnimation property or TouchManager.animations.
|
||||
* @param view NativeScript view instance
|
||||
*/
|
||||
static addAnimations(view: View) {
|
||||
// console.log("tapHandler:", tapHandler);
|
||||
const handleDown = (view?.touchAnimation && (<TouchAnimationOptions>view?.touchAnimation).down) || (TouchManager.animations && TouchManager.animations.down);
|
||||
const handleUp = (view?.touchAnimation && (<TouchAnimationOptions>view?.touchAnimation).up) || (TouchManager.animations && TouchManager.animations.up);
|
||||
|
||||
if (global.isIOS) {
|
||||
if (view?.ios?.addTargetActionForControlEvents) {
|
||||
// can use UIControlEvents
|
||||
console.log('added UIControlEvents!');
|
||||
if (!TouchManager.touchHandlers) {
|
||||
TouchManager.touchHandlers = [];
|
||||
}
|
||||
TouchManager.touchHandlers.push({
|
||||
view,
|
||||
handler: TouchControlHandler.initWithOwner(new WeakRef(view)),
|
||||
});
|
||||
|
||||
if (handleDown) {
|
||||
(<UIControl>view.ios).addTargetActionForControlEvents(TouchManager.touchHandlers[TouchManager.touchHandlers.length - 1].handler, GestureEvents.touchDown, UIControlEvents.TouchDown | UIControlEvents.TouchDragEnter);
|
||||
view.on(GestureEvents.touchDown, (args) => {
|
||||
console.log('touchDown {N} event');
|
||||
TouchManager.startAnimationForType(view, TouchAnimationTypes.down);
|
||||
});
|
||||
}
|
||||
if (handleUp) {
|
||||
(<UIControl>view.ios).addTargetActionForControlEvents(TouchManager.touchHandlers[TouchManager.touchHandlers.length - 1].handler, GestureEvents.touchUp, UIControlEvents.TouchDragExit | UIControlEvents.TouchCancel | UIControlEvents.TouchUpInside | UIControlEvents.TouchUpOutside);
|
||||
view.on(GestureEvents.touchUp, (args) => {
|
||||
console.log('touchUp {N} event');
|
||||
TouchManager.startAnimationForType(view, TouchAnimationTypes.up);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// console.log("use UILongPressGestureRecognizer!");
|
||||
console.log('added longPress to:', view.id);
|
||||
|
||||
if (handleDown || handleUp) {
|
||||
view.on(GestureEvents.gestureAttached, (args: GestureEventData) => {
|
||||
if (args.type === GestureTypes.longPress) {
|
||||
(<UILongPressGestureRecognizer>args.ios).minimumPressDuration = 0;
|
||||
}
|
||||
});
|
||||
view.on(GestureTypes.longPress, (args: GestureEventDataWithState) => {
|
||||
switch (args.state) {
|
||||
case GestureStateTypes.began:
|
||||
if (handleDown) {
|
||||
console.log('longPress began:', args.view.id, args.state);
|
||||
TouchManager.startAnimationForType(<View>args.view, TouchAnimationTypes.down);
|
||||
}
|
||||
break;
|
||||
case GestureStateTypes.cancelled:
|
||||
case GestureStateTypes.ended:
|
||||
if (handleUp) {
|
||||
TouchManager.startAnimationForType(<View>args.view, TouchAnimationTypes.up);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (handleDown || handleUp) {
|
||||
view.on(GestureTypes.touch, (args: TouchGestureEventData) => {
|
||||
switch (args.action) {
|
||||
case 'down':
|
||||
if (handleDown) {
|
||||
view.notify({
|
||||
eventName: GestureEvents.touchDown,
|
||||
object: view,
|
||||
data: args.android,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'up':
|
||||
case 'cancel':
|
||||
if (handleUp) {
|
||||
view.notify({
|
||||
eventName: GestureEvents.touchUp,
|
||||
object: view,
|
||||
data: args.android,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
if (handleDown) {
|
||||
view.on(GestureEvents.touchDown, (args) => {
|
||||
console.log('touchDown {N} event');
|
||||
TouchManager.startAnimationForType(view, TouchAnimationTypes.down);
|
||||
});
|
||||
}
|
||||
if (handleUp) {
|
||||
view.on(GestureEvents.touchUp, (args) => {
|
||||
console.log('touchUp {N} event');
|
||||
TouchManager.startAnimationForType(view, TouchAnimationTypes.up);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view.on(View.disposeNativeViewEvent, (args) => {
|
||||
console.log('calling disposeNativeView:', args.eventName, 'TouchManager.touchHandlers.length:', TouchManager.touchHandlers.length);
|
||||
const index = TouchManager.touchHandlers?.findIndex((handler) => handler.view === args.object);
|
||||
if (index > -1) {
|
||||
TouchManager.touchHandlers.splice(index, 1);
|
||||
}
|
||||
TouchManager.touchAnimationDefinitions = TouchManager.touchAnimationDefinitions?.filter((d) => d.view !== args.object);
|
||||
console.log('after clearing with disposeNativeView:', args.eventName, 'TouchManager.touchHandlers.length:', TouchManager.touchHandlers.length);
|
||||
console.log('TouchManager.touchAnimationDefinitions.length:', TouchManager.touchAnimationDefinitions.length);
|
||||
});
|
||||
}
|
||||
|
||||
static startAnimationForType(view: View, type: TouchAnimationTypes) {
|
||||
if (view) {
|
||||
const animate = function (definition: AnimationDefinition | TouchAnimationFn) {
|
||||
if (definition) {
|
||||
if (isFunction(definition)) {
|
||||
(<TouchAnimationFn>definition)(view);
|
||||
} else {
|
||||
if (!TouchManager.touchAnimationDefinitions) {
|
||||
TouchManager.touchAnimationDefinitions = [];
|
||||
}
|
||||
// reuse animations for each type
|
||||
let touchAnimation: Animation;
|
||||
// triggering animations should always cancel other animations which may be in progress
|
||||
for (const d of TouchManager.touchAnimationDefinitions) {
|
||||
if (d.view === view && d.animation) {
|
||||
d.animation.cancel();
|
||||
if (d.type === type) {
|
||||
touchAnimation = d.animation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!touchAnimation) {
|
||||
touchAnimation = new Animation([
|
||||
{
|
||||
target: view,
|
||||
...(<AnimationDefinition>definition),
|
||||
},
|
||||
]);
|
||||
TouchManager.touchAnimationDefinitions.push({
|
||||
view,
|
||||
type,
|
||||
animation: touchAnimation,
|
||||
});
|
||||
}
|
||||
touchAnimation.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
};
|
||||
// always use instance defined animation over global
|
||||
if (isObject(view.touchAnimation) && view.touchAnimation[type]) {
|
||||
animate((<any>view).touchAnimation[type]);
|
||||
} else if (TouchManager.animations?.[type]) {
|
||||
// fallback to globally defined
|
||||
animate(TouchManager.animations?.[type]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export let TouchControlHandler: {
|
||||
initWithOwner: (owner: WeakRef<View>) => any;
|
||||
};
|
||||
ensureTouchControlHandlers();
|
||||
|
||||
function ensureTouchControlHandlers() {
|
||||
if (global.isIOS) {
|
||||
@NativeClass
|
||||
class TouchHandlerImpl extends NSObject {
|
||||
private _owner: WeakRef<View>;
|
||||
static ObjCExposedMethods = {
|
||||
touchDown: { returns: interop.types.void, params: [interop.types.id] },
|
||||
touchUp: { returns: interop.types.void, params: [interop.types.id] },
|
||||
};
|
||||
|
||||
static initWithOwner(owner: WeakRef<View>): TouchHandlerImpl {
|
||||
const handler = <TouchHandlerImpl>TouchHandlerImpl.new();
|
||||
handler._owner = owner;
|
||||
return handler;
|
||||
}
|
||||
|
||||
touchDown(args) {
|
||||
this._owner?.get?.().notify({
|
||||
eventName: GestureEvents.touchDown,
|
||||
object: this._owner?.get?.(),
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
|
||||
touchUp(args) {
|
||||
this._owner?.get?.().notify({
|
||||
eventName: GestureEvents.touchUp,
|
||||
object: this._owner?.get?.(),
|
||||
data: args,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TouchControlHandler = TouchHandlerImpl;
|
||||
}
|
||||
}
|
@ -27,8 +27,8 @@ export * from './editable-text-base';
|
||||
export { Frame, setActivityCallbacks } from './frame';
|
||||
export type { NavigationEntry, NavigationContext, NavigationTransition, BackstackEntry, ViewEntry, AndroidActivityCallbacks } from './frame';
|
||||
|
||||
export { GesturesObserver, TouchAction, GestureTypes, GestureStateTypes, SwipeDirection, GestureEvents } from './gestures';
|
||||
export type { GestureEventData, GestureEventDataWithState, TapGestureEventData, PanGestureEventData, PinchGestureEventData, RotationGestureEventData, SwipeGestureEventData, TouchGestureEventData } from './gestures';
|
||||
export { GesturesObserver, TouchAction, GestureTypes, GestureStateTypes, SwipeDirection, GestureEvents, TouchManager } from './gestures';
|
||||
export type { GestureEventData, GestureEventDataWithState, TapGestureEventData, PanGestureEventData, PinchGestureEventData, RotationGestureEventData, SwipeGestureEventData, TouchGestureEventData, TouchAnimationOptions } from './gestures';
|
||||
|
||||
export { HtmlView } from './html-view';
|
||||
export { Image } from './image';
|
||||
|
Reference in New Issue
Block a user