feat(ui): TouchManager for ease in adding interactivity

This commit is contained in:
Nathan Walker
2022-01-08 11:11:10 -08:00
parent 06c00d2252
commit 14c1b05e69
8 changed files with 324 additions and 8 deletions

View File

@ -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;

View File

@ -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,
});
}

View File

@ -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.
*/

View File

@ -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);

View File

@ -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 {

View File

@ -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',
}
/**

View 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;
}
}

View File

@ -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';