@nativescript-community/ui-persistent-bottomsheet
Version:
NativeScript plugin that allows you to easily add a persistent bottomsheet to your projects.
480 lines • 17.2 kB
JavaScript
import { GestureHandlerStateEvent, GestureHandlerTouchEvent, GestureState, HandlerType, Manager, PanGestureHandler, install as installGestures } from '@nativescript-community/gesturehandler';
import { AbsoluteLayout, Animation, CSSType, Color, CoreTypes, GridLayout, Property, ScrollView, Utils, View, booleanConverter } from '@nativescript/core';
const OPEN_DURATION = 200;
export let PAN_GESTURE_TAG = 12400;
const SWIPE_DISTANCE_MINIMUM = 10;
function transformAnimationValues(values) {
values.translate = { x: values.translateX || 0, y: values.translateY || 0 };
values.scale = { x: values.scaleX || 1, y: values.scaleY || 1 };
delete values.translateX;
delete values.translateY;
delete values.scaleX;
delete values.scaleY;
return values;
}
export const scrollViewProperty = new Property({
name: 'scrollViewId',
defaultValue: undefined,
valueChanged: (target, oldValue, newValue) => {
target._onScrollViewIdChanged(oldValue, newValue);
}
});
export const bottomSheetProperty = new Property({
name: 'bottomSheet',
defaultValue: undefined,
valueChanged: (target, oldValue, newValue) => {
target._onBottomSheetChanged(oldValue, newValue);
}
});
export const gestureEnabledProperty = new Property({
name: 'gestureEnabled',
defaultValue: true,
valueConverter: booleanConverter
});
export const stepsProperty = new Property({
name: 'steps',
defaultValue: [70]
});
export const stepIndexProperty = new Property({
name: 'stepIndex',
defaultValue: 0
});
export const backdropColorProperty = new Property({
name: 'backdropColor',
valueConverter: (c) => (c ? new Color(c) : null)
});
export const translationFunctionProperty = new Property({
name: 'translationFunction'
});
let PersistentBottomSheet = class PersistentBottomSheet extends AbsoluteLayout {
constructor() {
super();
this.backdropColor = null;
this.stepIndex = 0;
this.panGestureOptions = null;
this._steps = [70];
this.isAnimating = false;
this.prevDeltaY = 0;
this.viewHeight = 0;
this.scrollViewTouched = false;
this._translationY = -1;
this.gestureEnabled = true;
this._isScrollEnabled = true;
this.scrollViewAtTop = true;
this.animating = false;
this.isPassThroughParentEnabled = true;
this.on('layoutChanged', this.onLayoutChange, this);
}
get steps() {
const result = this._steps || (this.bottomSheet && this.bottomSheet.steps);
return result;
}
set steps(value) {
this._steps = value;
}
initGestures() {
const manager = Manager.getInstance();
const options = { gestureId: PAN_GESTURE_TAG++, ...this.panGestureOptions };
const gestureHandler = manager.createGestureHandler(HandlerType.PAN, options.gestureId, {
shouldStartGesture: this.shouldStartGesture.bind(this),
minDist: SWIPE_DISTANCE_MINIMUM,
...options
});
gestureHandler.on(GestureHandlerTouchEvent, this.onGestureTouch, this);
gestureHandler.on(GestureHandlerStateEvent, this.onGestureState, this);
gestureHandler.attachToView(this);
this.panGestureHandler = gestureHandler;
}
shouldStartGesture(data) {
if (this.steps.length === 0 || (this.steps.length === 1 && this.steps[0] === 0)) {
return false;
}
let deltaY = 0;
if (global.isIOS && !this.iosIgnoreSafeArea) {
deltaY -= Utils.layout.toDeviceIndependentPixels(this.getSafeAreaInsets().top);
}
const y = data.y + deltaY;
if (y < this.viewHeight + this.translationY) {
return false;
}
if (this._scrollView) {
const posY = this._scrollView && this.scrollView.getLocationRelativeTo(this).y + deltaY;
if (y >= posY && y <= posY + this.scrollView.getMeasuredHeight()) {
return false;
}
}
return true;
}
get translationY() {
return this._translationY;
}
set translationY(value) {
if (this._translationY !== -1) {
this.isScrollEnabled = value === 0;
}
this._translationY = value;
}
get translationMaxOffset() {
const steps = this.steps;
return steps[steps.length - 1];
}
initNativeView() {
super.initNativeView();
if (this.scrollView) {
this.scrollView.on('scroll', this.onScroll, this);
this.scrollView.on('touch', this.onTouch, this);
}
if (this.gestureEnabled) {
this.initGestures();
}
}
disposeNativeView() {
if (this.scrollView) {
this.scrollView.off('scroll', this.onScroll, this);
this.scrollView.off('touch', this.onTouch, this);
}
super.disposeNativeView();
if (this.panGestureHandler) {
this.panGestureHandler.off(GestureHandlerTouchEvent, this.onGestureTouch, this);
this.panGestureHandler.off(GestureHandlerStateEvent, this.onGestureState, this);
this.panGestureHandler.detachFromView();
this.panGestureHandler = null;
}
}
[gestureEnabledProperty.setNative](value) {
if (this.panGestureHandler) {
this.panGestureHandler.enabled = value;
}
else if (value && !this.panGestureHandler) {
this.initGestures();
}
}
[stepIndexProperty.setNative](value) {
this.animateToPosition(this.steps[value]);
}
[backdropColorProperty.setNative](value) {
if (!this.backDrop && this.bottomSheet) {
const index = this.getChildIndex(this.bottomSheet);
this.addBackdropView(index);
}
}
addBackdropView(index) {
this.backDrop = new AbsoluteLayout();
this.backDrop.width = this.backDrop.height = {
unit: '%',
value: 100
};
this.backDrop.backgroundColor = this.backdropColor;
this.backDrop.opacity = 0;
this.backDrop.isUserInteractionEnabled = false;
this.insertChild(this.backDrop, index);
}
get scrollView() {
return this._scrollView;
}
set scrollView(value) {
if (this._scrollView === value) {
return;
}
if (this._scrollView) {
this.scrollView.off('scroll', this.onScroll, this);
this.scrollView.off('touch', this.onTouch, this);
}
this._scrollView = value;
if (value) {
value.on('scroll', this.onScroll, this);
value.on('touch', this.onTouch, this);
}
}
_onScrollViewIdChanged(oldValue, newValue) {
if (newValue && this.bottomSheet) {
if (this.bottomSheet.isLoaded) {
const view = this.bottomSheet.getViewById(newValue);
this.scrollView = view;
}
else {
this.bottomSheet.once('loaded', () => {
const view = this.bottomSheet.getViewById(newValue);
this.scrollView = view;
});
}
}
else {
this.scrollView = null;
}
}
_onBottomSheetChanged(oldValue, newValue) {
if (oldValue) {
this.removeChild(oldValue);
}
if (newValue) {
newValue.iosOverflowSafeAreaEnabled = false;
if (!newValue.width) {
newValue.width = {
unit: '%',
value: 100
};
}
let index;
if (!newValue.parent) {
index = this.getChildrenCount();
this.addChild(newValue);
}
else {
index = this.getChildIndex(newValue);
}
if (!this.backDrop && this.backdropColor) {
this.addBackdropView(index);
}
if (this.scrollViewId) {
this._onScrollViewIdChanged(null, this.scrollViewId);
}
}
}
computeTranslationData() {
const max = this.translationMaxOffset;
let value = this._translationY;
const progress = -value / max;
if (__IOS__ && progress === 0 && !this.iosIgnoreSafeArea) {
const safeArea = this.getSafeAreaInsets();
value += Utils.layout.toDeviceIndependentPixels(safeArea.bottom);
}
if (this.translationFunction) {
return this.translationFunction(value, max, progress);
}
return {
bottomSheet: {
translateY: value
},
backDrop: {
opacity: progress
}
};
}
onLayoutChange(event) {
const contentView = event.object;
const height = Math.round(Utils.layout.toDeviceIndependentPixels(contentView.getMeasuredHeight()));
this.viewHeight = height;
if (this.bottomSheet) {
this.bottomSheet.top = {
unit: 'px',
value: contentView.getMeasuredHeight()
};
}
if (this.translationY === -1 && this.bottomSheet) {
const steps = this.steps;
const step = steps[this.stepIndex];
const ty = step;
this.translationY = -ty;
const data = this.computeTranslationData();
this.applyTrData(data);
}
}
get scrollViewVerticalOffset() {
if (global.isAndroid) {
return this.scrollView.nativeViewProtected.computeVerticalScrollOffset() / Utils.layout.getDisplayDensity();
}
else {
return this.scrollView.nativeViewProtected.contentOffset.y;
}
}
set scrollViewVerticalOffset(value) {
if (global.isAndroid) {
this.scrollView.nativeViewProtected.scrollTo(0, 0);
}
else {
this.scrollView.nativeViewProtected.contentOffset = CGPointMake(this.scrollView.nativeViewProtected.contentOffset.x, 0);
}
}
get isScrollEnabled() {
return this._isScrollEnabled;
}
set isScrollEnabled(value) {
if (this._isScrollEnabled !== value) {
this._isScrollEnabled = value;
if (this.scrollView) {
this.scrollView.isScrollEnabled = value;
}
}
}
onTouch(event) {
let touchY;
if (global.isAndroid) {
touchY = Utils.layout.toDeviceIndependentPixels(event.android.getRawY());
}
else if (global.isIOS) {
touchY = event.ios.touches.anyObject().locationInView(null).y;
}
if (event.action === 'down') {
}
else if (event.action === 'up' || event.action === 'cancel') {
if (this.scrollViewTouched) {
this.scrollViewTouched = false;
if (this.scrollViewAtTop) {
this.scrollViewAtTop = this.scrollView.verticalOffset === 0;
const y = touchY - (this.lastTouchY || touchY);
const totalDelta = this.translationY + y;
this.computeAndAnimateEndGestureAnimation(-totalDelta);
}
}
this.isScrollEnabled = true;
}
else if ((!this.scrollViewTouched || this.scrollViewAtTop) && event.action === 'move') {
if (!this.scrollViewTouched) {
this.scrollViewTouched = true;
this.lastScrollY = this.scrollViewVerticalOffset;
this.scrollViewAtTop = this.lastScrollY === 0;
if (!this.scrollViewAtTop) {
return;
}
else {
this.panGestureHandler.cancel();
}
}
const y = touchY - (this.lastTouchY || touchY);
const trY = this.constrainY(this.translationY + y);
this.translationY = trY;
const trData = this.computeTranslationData();
this.applyTrData(trData);
}
this.lastTouchY = touchY;
}
onScroll(event) {
const scrollY = event.scrollOffset || event.scrollY || 0;
if (scrollY <= 0) {
this.scrollViewAtTop = true;
return;
}
else {
const height = this.viewHeight;
if (this.translationY > height - this.translationMaxOffset) {
return;
}
else {
this.scrollViewAtTop = false;
}
}
this.lastScrollY = scrollY;
}
onGestureState(args) {
const { state, prevState, extraData, view } = args.data;
if (prevState === GestureState.ACTIVE) {
const { velocityY, translationY } = extraData;
const dragToss = 0.05;
const y = translationY - this.prevDeltaY;
const totalDelta = this.translationY + (y + dragToss * velocityY);
this.computeAndAnimateEndGestureAnimation(-totalDelta);
this.prevDeltaY = 0;
}
}
computeAndAnimateEndGestureAnimation(totalDelta) {
const steps = this.steps;
let stepIndex = 0;
let destSnapPoint = steps[stepIndex];
let distance = Math.abs(destSnapPoint - totalDelta);
for (let i = 0; i < steps.length; i++) {
const snapPoint = steps[i];
const distFromSnap = Math.abs(snapPoint - totalDelta);
if (distFromSnap <= Math.abs(destSnapPoint - totalDelta)) {
destSnapPoint = snapPoint;
stepIndex = i;
distance = distFromSnap;
}
}
stepIndexProperty.nativeValueChange(this, stepIndex);
this.animateToPosition(destSnapPoint, Math.min(distance * 2, OPEN_DURATION));
}
onGestureTouch(args) {
const data = args.data;
if (data.state !== GestureState.ACTIVE) {
return;
}
const deltaY = data.extraData.translationY;
if (this.isAnimating || deltaY === 0) {
this.prevDeltaY = deltaY;
return;
}
const y = deltaY - this.prevDeltaY;
const trY = this.constrainY(this.translationY + y);
this.translationY = trY;
const trData = this.computeTranslationData();
this.applyTrData(trData);
this.prevDeltaY = deltaY;
}
applyTrData(trData) {
Object.keys(trData).forEach((k) => {
const { target, ...others } = trData[k];
if (target) {
Object.assign(target, others);
}
if (this[k]) {
Object.assign(this[k], others);
}
});
}
constrainY(y) {
return Math.max(Math.min(y, 0), -this.translationMaxOffset);
}
async animateToPosition(position, duration = OPEN_DURATION) {
if (this.animation) {
this.animation.cancel();
}
if (this.animating) {
return;
}
this.animating = true;
if (this._scrollView && global.isAndroid) {
const time = Date.now();
const event = android.view.MotionEvent.obtain(time, time, android.view.MotionEvent.ACTION_CANCEL, 0, 0, 0);
event.setAction(android.view.MotionEvent.ACTION_CANCEL);
this.scrollView.nativeViewProtected.dispatchTouchEvent(event);
}
this.translationY = -position;
const trData = this.computeTranslationData();
const params = Object.keys(trData)
.map((k) => {
const data = trData[k];
if (data.target) {
return Object.assign({
curve: CoreTypes.AnimationCurve.easeOut,
duration
}, transformAnimationValues(trData[k]));
}
else if (this[k]) {
return Object.assign({
target: this[k],
curve: CoreTypes.AnimationCurve.easeOut,
duration
}, transformAnimationValues(trData[k]));
}
})
.filter((a) => !!a);
try {
this.animation = new Animation(params);
await this.animation.play();
}
catch (err) {
this.applyTrData(trData);
console.error('BottomSheet animation cancelled', err);
}
finally {
this.isScrollEnabled = true;
this.animating = false;
this.animation = null;
}
}
};
PersistentBottomSheet = __decorate([
CSSType('PersistentBottomSheet')
], PersistentBottomSheet);
export { PersistentBottomSheet };
backdropColorProperty.register(PersistentBottomSheet);
scrollViewProperty.register(PersistentBottomSheet);
bottomSheetProperty.register(PersistentBottomSheet);
gestureEnabledProperty.register(PersistentBottomSheet);
translationFunctionProperty.register(PersistentBottomSheet);
stepIndexProperty.register(PersistentBottomSheet);
export function install() {
installGestures();
}
//# sourceMappingURL=index.js.map