bigpotato
bigpotato

Reputation: 27507

React Native: Animate scale, but make it shrink to the corner and not the center

I have a View that I want to shrink to the bottom right with some margin / padding on the bottom and right sides.

I was able to make it shrink, but it shrinks to the center. The video element is the one shrinking:

enter image description here

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  View,
  PanResponder,
  Animated,
  Dimensions,
} from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignSelf: 'stretch',
    backgroundColor: '#000000',
  },
  overlay: {
    flex: 1,
    alignSelf: 'stretch',
    backgroundColor: '#0000ff',
    opacity: 0.5,
  },
  video: {
    position: 'absolute',
    backgroundColor: '#00ff00',
    bottom: 0,
    right: 0,
    width: Dimensions.get("window").width,
    height: Dimensions.get("window").height,
    padding: 10,
  }
});

function clamp(value, min, max) {
  return min < max
    ? (value < min ? min : value > max ? max : value)
    : (value < max ? max : value > min ? min : value)
}

export default class EdmundMobile extends Component {

  constructor(props) {
    super(props);
    this.state = {
      pan: new Animated.ValueXY(),
      scale: new Animated.Value(1),
    };
  }

  componentWillMount() {
    this._panResponder = PanResponder.create({
      onMoveShouldSetResponderCapture: () => true,
      onMoveShouldSetPanResponderCapture: () => true,

      onPanResponderGrant: (e, gestureState) => {
        this.state.pan.setOffset({x: this.state.pan.x._value, y: 0});
        this.state.pan.setValue({x: 0, y: 0});
      },

      onPanResponderMove: (e, gestureState) => {
        let width = Dimensions.get("window").width;
        let difference = Math.abs((this.state.pan.x._value + width) / width);
        if (gestureState.dx < 0) {
          this.setState({ scale: new Animated.Value(difference) });
          return Animated.event([
            null, {dx: this.state.pan.x, dy: 0}
          ])(e, gestureState);
        }
      },

      onPanResponderRelease: (e, {vx, vy}) => {
        this.state.pan.flattenOffset();
        if (vx >= 0) {
          velocity = clamp(vx, 3, 5);
        } else if (vx < 0) {
          velocity = clamp(vx * -1, 3, 5) * -1;
        }

        if (Math.abs(this.state.pan.x._value) > 200) {
          Animated.spring(this.state.pan, {
            toValue: {x: -Dimensions.get("window").width, y: 0},
            friction: 4
          }).start()
          Animated.spring(this.state.scale, {
            toValue: 0.2,
            friction: 4
          }).start()
        } else {
          Animated.timing(this.state.pan, {
            toValue: {x: 0, y: 0},
            friction: 4
          }).start()
          Animated.spring(this.state.scale, {
            toValue: 1,
            friction: 10
          }).start()
        }
      }
    });
  }

  render() {
    let { pan, scale } = this.state;
    let translateX = pan.x;
    let swipeStyles = {transform: [{translateX}]};
    let videoScale = scale
    let localVideoStyles = {transform: [{scale: videoScale}]};
    return (
      <View style={styles.container}>
        <Animated.View style={[styles.video, localVideoStyles]}></Animated.View>
        <Animated.View style={[styles.overlay, swipeStyles]} {...this._panResponder.panHandlers}>
        </Animated.View>
      </View>
    );
  }
}

AppRegistry.registerComponent('EdmundMobile', () => EdmundMobile);

Upvotes: 1

Views: 6425

Answers (1)

bigpotato
bigpotato

Reputation: 27507

Ok I figured out one solution. Kinda hands on, but I think it provides all the customizations I need.

So instead of transforming scale, I animate the width, height, bottom, top styles by attaching them to the state and changing them in response to the pan responder stuff.

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  View,
  PanResponder,
  Animated,
  Dimensions,
} from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignSelf: 'stretch',
    backgroundColor: '#000000',
  },
  overlay: {
    flex: 1,
    alignSelf: 'stretch',
    backgroundColor: '#0000ff',
    opacity: 0.5,
  },
  video: {
    position: 'absolute',
    backgroundColor: '#00ff00',
  }
});

function clamp(value, min, max) {
  return min < max
    ? (value < min ? min : value > max ? max : value)
    : (value < max ? max : value > min ? min : value)
}

const MIN_VIDEO_WIDTH = 120;
const MIN_VIDEO_HEIGHT = 180;

export default class EdmundMobile extends Component {

  constructor(props) {
    super(props);
    this.state = {
      pan: new Animated.ValueXY(),
      width: new Animated.Value(Dimensions.get("window").width),
      height: new Animated.Value(Dimensions.get("window").height),
      bottom: new Animated.Value(0),
      right: new Animated.Value(0),
    };
  }

  componentWillMount() {
    this._panResponder = PanResponder.create({
      onMoveShouldSetResponderCapture: () => true,
      onMoveShouldSetPanResponderCapture: () => true,

      onPanResponderGrant: (e, gestureState) => {
        this.state.pan.setOffset({x: this.state.pan.x._value, y: 0});
        this.state.pan.setValue({x: 0, y: 0});
      },

      onPanResponderMove: (e, gestureState) => {
        let width = Dimensions.get("window").width;
        let difference = Math.abs((this.state.pan.x._value + width) / width);
        console.log(difference);
        if (gestureState.dx < 0) {
          const newVideoHeight = difference * Dimensions.get("window").height;
          const newVideoWidth = difference * Dimensions.get("window").width;
          if (newVideoWidth > MIN_VIDEO_WIDTH) {
            this.setState({
              width: new Animated.Value(newVideoWidth),
            });
          }
          if (newVideoHeight > MIN_VIDEO_HEIGHT) {
            this.setState({
              height: new Animated.Value(newVideoHeight),
            });
          }

          this.setState({
            bottom: new Animated.Value((1- difference) * 20),
            right: new Animated.Value((1 - difference) * 20),
          });

          return Animated.event([
            null, {dx: this.state.pan.x, dy: 0}
          ])(e, gestureState);
        }
      },

      onPanResponderRelease: (e, {vx, vy}) => {
        this.state.pan.flattenOffset();
        if (vx >= 0) {
          velocity = clamp(vx, 3, 5);
        } else if (vx < 0) {
          velocity = clamp(vx * -1, 3, 5) * -1;
        }

        if (Math.abs(this.state.pan.x._value) > 200) {
          Animated.spring(this.state.pan, {
            toValue: {x: -Dimensions.get("window").width, y: 0},
            friction: 4
          }).start()
          Animated.spring(this.state.width, {
            toValue: MIN_VIDEO_WIDTH,
            friction: 4
          }).start()
          Animated.spring(this.state.height, {
            toValue: MIN_VIDEO_HEIGHT,
            friction: 4
          }).start()
          Animated.timing(this.state.bottom, {
            toValue: 20,
          }).start()
          Animated.timing(this.state.right, {
            toValue: 20,
          }).start()
        } else {
          Animated.timing(this.state.pan, {
            toValue: {x: 0, y: 0},
          }).start()
          Animated.timing(this.state.width, {
            toValue: Dimensions.get("window").width,
            friction: 4
          }).start()
          Animated.timing(this.state.height, {
            toValue: Dimensions.get("window").height,
            friction: 4
          }).start()
          Animated.timing(this.state.bottom, {
            toValue: 0,
          }).start()
          Animated.timing(this.state.right, {
            toValue: 0,
          }).start()
        }
      }
    });
  }

  render() {
    let { pan, width, height, bottom, right } = this.state;
    let translateX = pan.x;
    let swipeStyles = {transform: [{translateX}]};
    let videoStyles = {
      width: width, 
      height: height, 
      bottom: bottom,
      right: right,
    };
    return (
      <View style={styles.container}>
        <Animated.View style={[styles.video, videoStyles]}></Animated.View>
        <Animated.View style={[styles.overlay, swipeStyles]} {...this._panResponder.panHandlers}>
        </Animated.View>
      </View>
    );
  }
}

AppRegistry.registerComponent('EdmundMobile', () => EdmundMobile);

enter image description here

Upvotes: 1

Related Questions