Search code examples
javascriptcssreact-nativeanimationreact-animated

React Native Animated - Expand circle centered on press


I'm trying to create a component using Animated on React Native where a circle expands from the point of the press to encompass the entire screen.

The idea is that there's essentially a carousel of colors, say:

const colors = ['white', 'black', 'green', 'blue', 'red', 'pink'];

You start with color 0 as the background, and then, when you press the screen, a circle of color 1 expands (from nothing) from the point of press, and takes up the whole screen. At that point, with the animation complete, the colorIndex is incremented, and the animated circle simultaneously erased, so that, seamlessly, we've moved onto the next color, which is now the background. (EG, we're on 'black' as the background color now).

Then, when you press again, the process repeats, expanding a circle of "green" to fill the screen, then updating green to be the BG color, etc.

(To clarify what I'm looking for in the animation, check this GIF out (minus the icons, etc)).

I'm trying this with Animated, and code like the following, called on press of the main background component (a TouchableHighlight):

triggerSwipe(event) {
  this.setState({ location: { x: event.nativeEvent.locationX, y: event.nativeEvent.locationY } });

  Animated.timing(
    this.state.diameter,
    { toValue: Dimensions.get('window').height * 2, duration: 500 },
  ).start(() => {
    this.state.diameter.setValue(0);
    this.setState({ colorIndex: this.state.colorIndex + 1 });
  });
}

Then the idea is, in the render function, I set left and top of my absolutely positioned circle component to equal the location, and I set width and height to equal diameter, as well as borderRadius (diameter / 2.0 would not work, because diameter is an Animated object, not a number).

There are two issues going on here that I can't seem to figure out (both visible in the GIF included at the bottom of this question):

  1. There is a black flash before every animation. It covers the whole TouchableHighlight component. And it's always black. Whatever color I'm on. There is no hardcoded black in the component (!).
  2. The location determines the edge of the circle (or, more accurately, the square it's transcribed within), rather than the center. I want it to determine the center of the circle, so that the circle expands outwards from my press.

I'm assuming #1 is really hard to reason about without all the code. So I'll paste the full component code below. The 2nd one I'm hoping is a little more conceptual/easy to figure out without playing with it.

Can anyone think of a good way to assert the location of the center of the circle? I'm worried it requires constantly updating the left/top location of the circle as a function of its diameter (left: location.x - diameter._value / 2), which will get rid of all the native performance benefits of Animated, as I understand it. Any better ideas?

Here's the full component code:

import React from 'react';
import { TouchableHighlight, Animated } from 'react-native';
import { Dimensions } from 'react-native';

const colors = ['white', 'black', 'green', 'blue', 'red', 'pink'];

const color = index => colors[index % colors.length];

class ColorScape extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      colorIndex: 0,
      location: { x: 0, y: 0 },
      diameter: new Animated.Value(0)
    };
  }

  triggerSwipe(event) {
    this.setState({ location: { x: event.nativeEvent.locationX, y: event.nativeEvent.locationY } });

    Animated.timing(
      this.state.diameter,
      { toValue: Dimensions.get('window').height * 2, duration: 500 },
    ).start(() => {
      this.state.diameter.setValue(0);
      this.setState({ colorIndex: this.state.colorIndex + 1 });
    });
  }

  render() {
    const { colorIndex, diameter, location } = this.state;

    const circleStyles = {
      backgroundColor: color(colorIndex + 1),
      width: diameter,
      height: diameter,
      borderRadius: diameter,
      position: 'absolute',
      zIndex: 2,
      left: location.x,
      top: location.y
    };

    return (
      <TouchableHighlight
        style={ {
          flex: 1,
          width: '100%',
          height: '100%',
          zIndex: 1,
          backgroundColor: color(colorIndex)
        } }
        onPress={ (event) => this.triggerSwipe(event) }
      >
        <Animated.View
          style={ circleStyles }
        />
      </TouchableHighlight>
    );
  }
}

export default ColorScape;

Here's the current functionality (in the iPhone X simulator):

enter image description here


Solution

  • If you want your circle to expand from the point of click, I would recommend using transform: scale() instead of animating the width/height properties. This way you only have to animate one property.

    You will still need to center your circle. Let's say you set the width/height to 100px. Now you set transform: scale(0) as your initial size and you can scale your circle up past 1 if you want, so it will take the full screen. You can play with the math to get the timing/feel right.

    With this approach you will STILL have the same issue of the circle enlarging from the top left. That is because you have positioned it there. top/left are relative to the parent container. You'll now need to center your circle relative to itself. This means you will need to add another transform property. transform: translateX(-50%)translateY(-50%). This is going to always center your circle relative to it's center position.

    Starting transform: transform: translateX(-50%)translateY(-50%)scale(0) Enlarged transform: transform: translateX(-50%)translateY(-50%)scale(x) where x is however large you want the circle to get.