123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728 |
- // @flow
-
- import React, { Component } from 'react';
- import { PanResponder, PixelRatio, View } from 'react-native';
- import { connect } from 'react-redux';
- import { type Dispatch } from 'redux';
-
- import { storeVideoTransform } from '../../actions';
- import styles from './styles';
-
- /**
- * The default/initial transform (= no transform).
- */
- const DEFAULT_TRANSFORM = {
- scale: 1,
- translateX: 0,
- translateY: 0
- };
-
- /**
- * The minimum scale (magnification) multiplier. 1 is equal to objectFit
- * = 'contain'.
- */
- const MIN_SCALE = 1;
-
- /*
- * The max distance from the edge of the screen where we let the user move the
- * view to. This is large enough now to let the user drag the view to a position
- * where no other displayed components cover it (such as filmstrip). If a
- * ViewPort (hint) support is added to the LargeVideo component then this
- * contant will not be necessary anymore.
- */
- const MAX_OFFSET = 100;
-
- /**
- * The max allowed scale (magnification) multiplier.
- */
- const MAX_SCALE = 5;
-
- /**
- * The threshold to allow the fingers move before we consider a gesture a
- * move instead of a touch.
- */
- const MOVE_THRESHOLD_DISMISSES_TOUCH = 5;
-
- /**
- * A tap timeout after which we consider a gesture a long tap and will not
- * trigger onPress (unless long tap gesture support is added in the future).
- */
- const TAP_TIMEOUT_MS = 400;
-
- /**
- * Type of a transform object this component is capable of handling.
- */
- type Transform = {
- scale: number,
- translateX: number,
- translateY: number
- };
-
- type Props = {
-
- /**
- * The children components of this view.
- */
- children: Object,
-
- /**
- * Transformation is only enabled when this flag is true.
- */
- enabled: boolean,
-
- /**
- * Function to invoke when a press event is detected.
- */
- onPress?: Function,
-
- /**
- * The id of the current stream that is displayed.
- */
- streamId: string,
-
- /**
- * Style of the top level transformable view.
- */
- style: Object,
-
- /**
- * The stored transforms retreived from Redux to be initially applied
- * to different streams.
- */
- _transforms: Object,
-
- /**
- * Action to dispatch when the component is unmounted.
- */
- _onUnmount: Function
- };
-
- type State = {
-
- /**
- * The current (non-transformed) layout of the View.
- */
- layout: ?Object,
-
- /**
- * The current transform that is applied.
- */
- transform: Transform
- };
-
- /**
- * An container that captures gestures such as pinch&zoom, touch or move.
- */
- class VideoTransform extends Component<Props, State> {
- /**
- * The gesture handler object.
- */
- gestureHandlers: PanResponder;
-
- /**
- * The initial distance of the fingers on pinch start.
- */
- initialDistance: number;
-
- /**
- * The initial position of the finger on touch start.
- */
- initialPosition: {
- x: number,
- y: number
- };
-
- /**
- * The actual move threshold that is calculated for this device/screen.
- */
- moveThreshold: number;
-
- /**
- * Time of the last tap.
- */
- lastTap: number;
-
- /**
- * Constructor of the component.
- *
- * @inheritdoc
- */
- constructor(props: Props) {
- super(props);
-
- this.state = {
- layout: null,
- transform:
- this._getSavedTransform(props.streamId) || DEFAULT_TRANSFORM
- };
-
- this._didMove = this._didMove.bind(this);
- this._getTransformStyle = this._getTransformStyle.bind(this);
- this._onGesture = this._onGesture.bind(this);
- this._onLayout = this._onLayout.bind(this);
- this._onMoveShouldSetPanResponder
- = this._onMoveShouldSetPanResponder.bind(this);
- this._onPanResponderGrant = this._onPanResponderGrant.bind(this);
- this._onPanResponderMove = this._onPanResponderMove.bind(this);
- this._onPanResponderRelease = this._onPanResponderRelease.bind(this);
- this._onStartShouldSetPanResponder
- = this._onStartShouldSetPanResponder.bind(this);
-
- // The move threshold should be adaptive to the pixel ratio of the
- // screen to avoid making it too sensitive or difficult to handle on
- // different pixel ratio screens.
- this.moveThreshold
- = PixelRatio.get() * MOVE_THRESHOLD_DISMISSES_TOUCH;
-
- this.gestureHandlers = PanResponder.create({
- onPanResponderGrant: this._onPanResponderGrant,
- onPanResponderMove: this._onPanResponderMove,
- onPanResponderRelease: this._onPanResponderRelease,
- onPanResponderTerminationRequest: () => true,
- onMoveShouldSetPanResponder: this._onMoveShouldSetPanResponder,
- onShouldBlockNativeResponder: () => false,
- onStartShouldSetPanResponder: this._onStartShouldSetPanResponder
- });
- }
-
- /**
- * Implements React Component's componentDidUpdate.
- *
- * @inheritdoc
- */
- componentDidUpdate(prevProps, prevState) {
- if (prevProps.streamId !== this.props.streamId) {
- this._storeTransform(prevProps.streamId, prevState.transform);
- this._restoreTransform(this.props.streamId);
- }
- }
-
- /**
- * Implements React Component's componentWillUnmount.
- *
- * @inheritdoc
- */
- componentWillUnmount() {
- this._storeTransform(this.props.streamId, this.state.transform);
- }
-
- /**
- * Renders the empty component that captures the gestures.
- *
- * @inheritdoc
- */
- render() {
- const { children, style } = this.props;
-
- return (
- <View
- onLayout = { this._onLayout }
- pointerEvents = 'box-only'
- style = { [
- styles.videoTransformedViewContainer,
- style
- ] }
- { ...this.gestureHandlers.panHandlers }>
- <View
- style = { [
- styles.videoTranformedView,
- this._getTransformStyle()
- ] }>
- { children }
- </View>
- </View>
- );
- }
-
- /**
- * Calculates the new transformation to be applied by merging the current
- * transform values with the newly received incremental values.
- *
- * @param {Transform} transform - The new transform object.
- * @private
- * @returns {Transform}
- */
- _calculateTransformIncrement(transform: Transform) {
- let {
- scale,
- translateX,
- translateY
- } = this.state.transform;
- const {
- scale: newScale,
- translateX: newTranslateX,
- translateY: newTranslateY
- } = transform;
-
- // Note: We don't limit MIN_SCALE here yet, as we need to detect a scale
- // down gesture even if the scale is already at MIN_SCALE to let the
- // user return the screen to center with that gesture. Scale is limited
- // to MIN_SCALE right before it gets applied.
- scale = Math.min(scale * (newScale || 1), MAX_SCALE);
-
- translateX = translateX + ((newTranslateX || 0) / scale);
- translateY = translateY + ((newTranslateY || 0) / scale);
-
- return {
- scale,
- translateX,
- translateY
- };
- }
-
- _didMove: Object => boolean
-
- /**
- * Determines if there was large enough movement to be handled.
- *
- * @param {Object} gestureState - The gesture state.
- * @returns {boolean}
- */
- _didMove({ dx, dy }) {
- return Math.abs(dx) > this.moveThreshold
- || Math.abs(dy) > this.moveThreshold;
- }
-
- /**
- * Returns the stored transform a stream should display with initially.
- *
- * @param {string} streamId - The id of the stream to match with a stored
- * transform.
- * @private
- * @returns {Object | null}
- */
- _getSavedTransform(streamId) {
- const { enabled, _transforms } = this.props;
-
- return (enabled && _transforms[streamId]) || null;
- }
-
- _getTouchDistance: Object => number;
-
- /**
- * Calculates the touch distance on a pinch event.
- *
- * @param {Object} evt - The touch event.
- * @private
- * @returns {number}
- */
- _getTouchDistance({ nativeEvent: { touches } }) {
- const dx = Math.abs(touches[0].pageX - touches[1].pageX);
- const dy = Math.abs(touches[0].pageY - touches[1].pageY);
-
- return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
- }
-
- _getTouchPosition: Object => Object
-
- /**
- * Calculates the position of the touch event.
- *
- * @param {Object} evt - The touch event.
- * @private
- * @returns {Object}
- */
- _getTouchPosition({ nativeEvent: { touches } }) {
- return {
- x: touches[0].pageX,
- y: touches[0].pageY
- };
- }
-
- _getTransformStyle: () => Object
-
- /**
- * Generates a transform style object to be used on the component.
- *
- * @returns {{string: Array<{string: number}>}}
- */
- _getTransformStyle() {
- const { enabled } = this.props;
-
- if (!enabled) {
- return null;
- }
-
- const {
- scale,
- translateX,
- translateY
- } = this.state.transform;
-
- return {
- transform: [
- { scale },
- { translateX },
- { translateY }
- ]
- };
- }
-
- /**
- * Limits the move matrix and then applies the transformation to the
- * component (updates state).
- *
- * Note: Points A (top-left) and D (bottom-right) are opposite points of
- * the View rectangle.
- *
- * @param {Transform} transform - The transformation object.
- * @private
- * @returns {void}
- */
- _limitAndApplyTransformation(transform: Transform) {
- const { layout } = this.state;
-
- if (layout) {
- const { scale } = this.state.transform;
- const { scale: newScaleUnlimited } = transform;
- let {
- translateX: newTranslateX,
- translateY: newTranslateY
- } = transform;
-
- // Scale is only limited to MIN_SCALE here to detect downscale
- // gesture later.
- const newScale = Math.max(newScaleUnlimited, MIN_SCALE);
-
- // The A and D points of the original View (before transform).
- const originalLayout = {
- a: {
- x: layout.x,
- y: layout.y
- },
- d: {
- x: layout.x + layout.width,
- y: layout.y + layout.height
- }
- };
-
- // The center point (midpoint) of the transformed View.
- const transformedCenterPoint = {
- x: ((layout.x + layout.width) / 2) + (newTranslateX * newScale),
- y: ((layout.y + layout.height) / 2) + (newTranslateY * newScale)
- };
-
- // The size of the transformed View.
- const transformedSize = {
- height: layout.height * newScale,
- width: layout.width * newScale
- };
-
- // The A and D points of the transformed View.
- const transformedLayout = {
- a: {
- x: transformedCenterPoint.x - (transformedSize.width / 2),
- y: transformedCenterPoint.y - (transformedSize.height / 2)
- },
- d: {
- x: transformedCenterPoint.x + (transformedSize.width / 2),
- y: transformedCenterPoint.y + (transformedSize.height / 2)
- }
- };
-
- let _MAX_OFFSET = MAX_OFFSET;
-
- if (newScaleUnlimited < scale) {
- // This is a negative scale event so we dynamycally reduce the
- // MAX_OFFSET to get the screen back to the center on
- // downscaling.
- _MAX_OFFSET = Math.min(MAX_OFFSET, MAX_OFFSET * (newScale - 1));
- }
-
- // Correct move matrix if it goes out of the view
- // too much (_MAX_OFFSET).
- newTranslateX
- -= Math.max(
- transformedLayout.a.x - originalLayout.a.x - _MAX_OFFSET,
- 0);
- newTranslateX
- += Math.max(
- originalLayout.d.x - transformedLayout.d.x - _MAX_OFFSET,
- 0);
- newTranslateY
- -= Math.max(
- transformedLayout.a.y - originalLayout.a.y - _MAX_OFFSET,
- 0);
- newTranslateY
- += Math.max(
- originalLayout.d.y - transformedLayout.d.y - _MAX_OFFSET,
- 0);
-
- this.setState({
- transform: {
- scale: newScale,
- translateX: Math.round(newTranslateX),
- translateY: Math.round(newTranslateY)
- }
- });
- }
- }
-
- _onGesture: (string, ?Object | number) => void
-
- /**
- * Handles gestures and converts them to transforms.
- *
- * Currently supported gestures:
- * - scale (punch&zoom-type scale).
- * - move
- * - press.
- *
- * Note: This component supports onPress solely to overcome the problem of
- * not being able to register gestures via the PanResponder due to the fact
- * that the entire Conference component was a single touch responder
- * component in the past (see base/react/.../Container with an onPress
- * event) - and stock touch responder components seem to have exclusive
- * priority in handling touches in React.
- *
- * @param {string} type - The type of the gesture.
- * @param {?Object | number} value - The value of the gesture, if any.
- * @returns {void}
- */
- _onGesture(type, value) {
- let transform;
-
- switch (type) {
- case 'move':
- transform = {
- ...DEFAULT_TRANSFORM,
- translateX: value.x,
- translateY: value.y
- };
- break;
- case 'scale':
- transform = {
- ...DEFAULT_TRANSFORM,
- scale: value
- };
- break;
-
- case 'press': {
- const { onPress } = this.props;
-
- typeof onPress === 'function' && onPress();
- break;
- }
- }
-
- if (transform) {
- this._limitAndApplyTransformation(
- this._calculateTransformIncrement(transform));
- }
-
- this.lastTap = 0;
- }
-
- _onLayout: Object => void
-
- /**
- * Callback for the onLayout of the component.
- *
- * @param {Object} event - The native props of the onLayout event.
- * @private
- * @returns {void}
- */
- _onLayout({ nativeEvent: { layout: { x, y, width, height } } }) {
- this.setState({
- layout: {
- x,
- y,
- width,
- height
- }
- });
- }
-
- _onMoveShouldSetPanResponder: (Object, Object) => boolean
-
- /**
- * Function to decide whether the responder should respond to a move event.
- *
- * @param {Object} evt - The event.
- * @param {Object} gestureState - Gesture state.
- * @private
- * @returns {boolean}
- */
- _onMoveShouldSetPanResponder(evt, gestureState) {
- return this.props.enabled
- && (this._didMove(gestureState)
- || gestureState.numberActiveTouches === 2);
- }
-
- _onPanResponderGrant: (Object, Object) => void
-
- /**
- * Calculates the initial touch distance.
- *
- * @param {Object} evt - Touch event.
- * @param {Object} gestureState - Gesture state.
- * @private
- * @returns {void}
- */
- _onPanResponderGrant(evt, { numberActiveTouches }) {
- if (numberActiveTouches === 1) {
- this.initialPosition = this._getTouchPosition(evt);
- this.lastTap = Date.now();
-
- } else if (numberActiveTouches === 2) {
- this.initialDistance = this._getTouchDistance(evt);
- }
- }
-
- _onPanResponderMove: (Object, Object) => void
-
- /**
- * Handles the PanResponder move (touch move) event.
- *
- * @param {Object} evt - Touch event.
- * @param {Object} gestureState - Gesture state.
- * @private
- * @returns {void}
- */
- _onPanResponderMove(evt, gestureState) {
- if (gestureState.numberActiveTouches === 2) {
- // this is a zoom event
- if (
- this.initialDistance === undefined
- || isNaN(this.initialDistance)
- ) {
- // there is no initial distance because the user started
- // with only one finger. We calculate it now.
- this.initialDistance = this._getTouchDistance(evt);
- } else {
- const distance = this._getTouchDistance(evt);
- const scale = distance / (this.initialDistance || 1);
-
- this.initialDistance = distance;
-
- this._onGesture('scale', scale);
- }
- } else if (gestureState.numberActiveTouches === 1
- && isNaN(this.initialDistance)
- && this._didMove(gestureState)) {
- // this is a move event
- const position = this._getTouchPosition(evt);
- const move = {
- x: position.x - this.initialPosition.x,
- y: position.y - this.initialPosition.y
- };
-
- this.initialPosition = position;
-
- this._onGesture('move', move);
- }
- }
-
- _onPanResponderRelease: () => void
-
- /**
- * Handles the PanResponder gesture end event.
- *
- * @private
- * @returns {void}
- */
- _onPanResponderRelease() {
- if (this.lastTap && Date.now() - this.lastTap < TAP_TIMEOUT_MS) {
- this._onGesture('press');
- }
- delete this.initialDistance;
- delete this.initialPosition;
- }
-
- _onStartShouldSetPanResponder: () => boolean
-
- /**
- * Function to decide whether the responder should respond to a start
- * (thouch) event.
- *
- * @private
- * @returns {boolean}
- */
- _onStartShouldSetPanResponder() {
- return typeof this.props.onPress === 'function';
- }
-
- /**
- * Restores the last applied transform when the component is mounted, or
- * a new stream is about to be rendered.
- *
- * @param {string} streamId - The stream id to restore transform for.
- * @private
- * @returns {void}
- */
- _restoreTransform(streamId) {
- const savedTransform = this._getSavedTransform(streamId);
-
- if (savedTransform) {
- this.setState({
- transform: savedTransform
- });
- }
- }
-
- /**
- * Stores/saves the a transform when the component is destroyed, or a
- * new stream is about to be rendered.
- *
- * @param {string} streamId - The stream id associated with the transform.
- * @param {Object} transform - The {@Transform} to save.
- * @private
- * @returns {void}
- */
- _storeTransform(streamId, transform) {
- const { _onUnmount, enabled } = this.props;
-
- if (enabled) {
- _onUnmount(streamId, transform);
- }
- }
- }
-
- /**
- * Maps dispatching of some action to React component props.
- *
- * @param {Function} dispatch - Redux action dispatcher.
- * @private
- * @returns {{
- * _onUnmount: Function
- * }}
- */
- function _mapDispatchToProps(dispatch: Dispatch<*>) {
- return {
- /**
- * Dispatches actions to store the last applied transform to a video.
- *
- * @param {string} streamId - The ID of the stream.
- * @param {Transform} transform - The last applied transform.
- * @private
- * @returns {void}
- */
- _onUnmount(streamId, transform) {
- dispatch(storeVideoTransform(streamId, transform));
- }
- };
- }
-
- /**
- * Maps (parts of) the redux state to the component's props.
- *
- * @param {Object} state - The redux state.
- * @private
- * @returns {{
- * _transforms: Object
- * }}
- */
- function _mapStateToProps(state) {
- return {
- /**
- * The stored transforms retrieved from Redux to be initially applied to
- * different streams.
- *
- * @private
- * @type {Object}
- */
- _transforms: state['features/base/media'].video.transforms
- };
- }
-
- export default connect(_mapStateToProps, _mapDispatchToProps)(VideoTransform);
|