UNPKG

material-motion

Version:

Makes it easy to add rich, interactive motion to your application.

208 lines (174 loc) 5.56 kB
/** @license * Copyright 2016 - present The Material Motion Authors. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy * of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ import { allOf, when, } from '../aggregators'; import { combineLatest, } from '../combineLatest'; import { subscribe, } from '../subscribe'; import { Axis, Direction, State, ThresholdRegion, } from '../enums'; import { MotionProperty, createProperty, } from '../observables'; import { MaybeReactive, ObservableWithMotionOperators, Point2D, ScaleStyleStreams, TranslateStyleStreams, } from '../types'; import { NumericSpring, } from './NumericSpring'; import { Tossable, } from './Tossable'; export enum SwipeState { NONE = 'none', LEFT = 'left', RIGHT = 'right', }; export type SwipeableArgs = { tossable: Tossable, width$: ObservableWithMotionOperators<number>, }; export class Swipeable { static VISUAL_THRESHOLD = 72; readonly iconSpring: NumericSpring = new NumericSpring(); readonly backgroundSpring: NumericSpring = new NumericSpring(); // Should `State` be called `MotionState` so `state$` can be reserved for interactions? readonly swipeState$: MotionProperty<SwipeState> = createProperty({ initialValue: SwipeState.NONE }); readonly direction$: ObservableWithMotionOperators<Direction>; readonly isThresholdMet$: ObservableWithMotionOperators<boolean>; readonly whenThresholdCrossed$: ObservableWithMotionOperators<boolean>; readonly whenThresholdFirstCrossed$: ObservableWithMotionOperators<boolean>; /** * If an item is swiped past the threshold, it will animate by its own width * + destinationMargin, in the direction of the swipe. * * This ensures that decoration that might overflow an item's bounds (like a * shadow) isn't visible when it's been swiped away. */ readonly destinationMargin$: MotionProperty<number> = createProperty({ initialValue: 0, }); get destinationMargin(): number { return this.destinationMargin$.read(); } set destinationMargin(value: number) { this.destinationMargin$.write(value); } // There should probably be an Interaction interface that requires this of all // interactions readonly state$: ObservableWithMotionOperators<string>; readonly tossable: Tossable; readonly width$: ObservableWithMotionOperators<number>; readonly styleStreamsByTargetName: { item: TranslateStyleStreams, icon: ScaleStyleStreams, background: ScaleStyleStreams, }; constructor({ tossable, width$ }: SwipeableArgs) { this.tossable = tossable; this.width$ = width$; this.state$ = this.tossable.state$; tossable.draggable.axis = Axis.X; const ICON_SPRING_INITIAL_VALUE = 0.67; const draggable = tossable.draggable; const spring = tossable.spring; const draggedX$ = tossable.draggedLocation$.pluck({ path: 'x' }); this.iconSpring.initialValue = ICON_SPRING_INITIAL_VALUE; this.direction$ = draggedX$.threshold(0).isAnyOf([ ThresholdRegion.ABOVE ]).rewrite({ mapping: { true: Direction.RIGHT, false: Direction.LEFT, } }); this.isThresholdMet$ = draggedX$.distanceFrom(0).threshold(Swipeable.VISUAL_THRESHOLD).isAnyOf([ ThresholdRegion.ABOVE, ThresholdRegion.WITHIN, ]); this.whenThresholdCrossed$ = when(this.isThresholdMet$.dedupe()); subscribe({ sink: this.backgroundSpring.destination$, source: this.isThresholdMet$.rewrite({ mapping: { true: 1, false: 0, } }), }); subscribe({ sink: this.iconSpring.destination$, source: this.isThresholdMet$.rewrite({ mapping: { true: 1, false: ICON_SPRING_INITIAL_VALUE, } }), }); // This needs to also take velocity into consideration; right now, it only // cares about final position. subscribe({ sink: this.swipeState$, source: when(draggable.state$.isAnyOf([ State.AT_REST ])).rewriteTo({ value$: this.isThresholdMet$.rewrite({ mapping: { true: this.direction$, false: SwipeState.NONE, }, }), onlyEmitWithUpstream: true, }), }); const destinationDistance$ = width$.addedBy(this.destinationMargin$); subscribe({ sink: spring.destination$, source: combineLatest<Point2D, MaybeReactive<Point2D>>({ x: this.swipeState$.rewrite({ mapping: { [SwipeState.NONE]: 0, [SwipeState.LEFT]: destinationDistance$.multipliedBy(-1), [SwipeState.RIGHT]: destinationDistance$, } }), y: 0, }) }); this.styleStreamsByTargetName = { item: tossable.styleStreams, icon: { scale$: this.iconSpring.value$, willChange$: tossable.styleStreams.willChange$, }, background: { scale$: this.backgroundSpring.value$, willChange$: tossable.styleStreams.willChange$, }, }; } } export default Swipeable;