Thursday, April 13, 2017

Overview

I’ve been working on my app for a while now and I’ve decided to use react-native because I want to speed up the development and also learn a cool new tool for creating apps. I’ve been learning about how to animate views in react native and I want to share my findings with you.

Content

Result

We’re going to build a button that will load a progress bar if we long press.

Press and hold

Explanation

We start, as usual, by importing components from react and react-native. We also need a variable called ACTION_TIMER to determine how long we have to hold the button and another variable called COLORS which we’ll use to determine the start and end colors for the progress bar.

1
2
3
4
5
6
7
8
9
10
11
import React, { Component } from 'react';
import {
  Text,
  View,
  StyleSheet,
  Animated,
  TouchableWithoutFeedback
} from 'react-native';

var ACTION_TIMER = 200;
var COLORS = ['rgb(255,255,255)', 'rgb(111,235,62)']

Next we’ll create the skeleton of the class which will display the button. No fancy animations yet, just the button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class AnimatedButton extends Component {
    constructor() {
        super();
        this.state = {
            textComplete: '',
            buttonWidth: 0,
            buttonHeight: 0
        }
    }
  
    render(): ReactElement {
        return (
            <View>
                <TouchableWithoutFeedback>
                    <View style={styles.button} >
                        <Animated.View style={styles.animatedFill} />
                        <Text style={styles.text}>Press And Hold Me</Text>
                    </View>
                </TouchableWithoutFeedback>
        
                <View>
                    <Text>{this.state.textComplete}</Text>
                </View>
            </View>
        )
    }
}

We also need the style.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const styles = StyleSheet.create({
    container: {
        flex: 1,
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center'
    },
    button: {
        padding: 10,
        borderWidth: 3,
        borderColor: '#111'
    },
    text: {
        backgroundColor: 'transparent',
        color: '#111'
    },
    animatedFill: {
        position: 'absolute',
        top: 0,
        left: 0
    }
});

This is what we have so far.

First Step

Now the fun part. Let’s add animations. In AnimatedButton, change the constructor and add a new componentDidMount function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constructor() {
    super();
    this.state = {
        textComplete: '',
        pressAction: new Animated.Value(0),
        buttonWidth: 0,
        buttonHeight: 0
    }
}
  
componentDidMount() {
    this._value = 0;
    this.state.pressAction.addListener(
        (v) => this._value = v.value
    );
}

Here are the event handlers. handlePressIn will animate when the user holds the button and handlePressOut will handle when the user lets go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
handlePressIn() {
    Animated.timing(this.state.pressAction, {
        duration: ACTION_TIMER,
        toValue: 1
    }).start(this.animationActionComplete);
}

handlePressOut() {
    Animated.timing(this.state.pressAction, {
        duration: this._value * ACTION_TIMER,
        toValue: 0
    }).start();
}

animationActionComplete() {
    var message = '';
    if (this._value === 1) {
        message = 'You held it long enough!';
    }
    this.setState({ textComplete: message });
}

We now need to add the event handlers to our render function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
render(): ReactElement {
    return (
        <View style={styles.container}>
            <TouchableWithoutFeedback 
                onPressIn={this.handlePressIn} 
                onPressOut={this.handlePressOut}
            >
                <View style={styles.button} >
                    <Animated.View style={styles.bgFill} />
                    <Text style={styles.text}>Press And Hold Me</Text>
                </View>
            </TouchableWithoutFeedback>
            
            <View>
                <Text>{this.state.textComplete}</Text>
            </View>
        </View>
    );
}

We need to know the width and height of the button, so we’ll get them at runtime.

1
2
3
4
5
6
getButtonWidthLayout(e) {
    this.setState({
        buttonWidth: e.nativeEvent.layout.width - 6,
        buttonHeight: e.nativeEvent.layout.height - 6
    });
}

Now we need to animate when the button is clicked. We do this using styles.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
getProgressStyles() {
    var width = this.state.pressAction.interpolate({
        inputRange: [0, 1],
        outputRange: [0, this.state.buttonWidth]
    });
    var animatedColor = this.state.pressAction.interpolate({
        inputRange: [0, 1],
        outputRange: COLORS
    });

    return {
        width: width,
        height: this.state.buttonHeight,
        backgroundColor: animatedColor
    }
}

The final render function will look like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
render(): ReactElement {
    return (
      <View style={styles.container}>     
            <TouchableWithoutFeedback 
                onPressIn={this.handlePressIn.bind(this)} 
                onPressOut={this.handlePressOut.bind(this)}
            >
                <View style={styles.button} 
                onLayout={this.getButtonWidthLayout.bind(this)}>

                    <Animated.View style={[
                      styles.animatedFill,
                      this.getProgressStyles()]} />
                    
                    <Text style={styles.text}>Press And Hold Me</Text>

                </View>
            </TouchableWithoutFeedback>

            <View>
                <Text>{this.state.textComplete}</Text>
            </View>
       </View>
    );
}

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// ----------------------------------
// IMPORTS
// ----------------------------------

import React, { Component } from 'react';
import {
  Text,
  View,
  StyleSheet,
  Animated,
  TouchableWithoutFeedback
} from 'react-native';

var ACTION_TIMER = 2;
var COLORS = ['rgb(255,255,255)', 'rgb(111,235,62)']

// ----------------------------------
// CLASSES
// ----------------------------------

export default class App extends Component {
  render() {
    return (
      <View style={styles.container}>
        <AnimatedButton />
      </View>
    );
  }
}

class AnimatedButton extends Component {
  constructor() {
    super();
    this.state = {
      textComplete: '',
      pressAction: new Animated.Value(0)
    }
  }
  
  componentDidMount() {
    this._value = 0;
    this.state.pressAction.addListener(
      (v) => this._value = v.value
    );
  }
  
  handlePressIn() {
    Animated.timing(this.state.pressAction, {
      duration: ACTION_TIMER,
      toValue: 11
    }).start(this.animationActionComplete.bind(this));
  }
  
  handlePressOut() {
    Animated.timing(this.state.pressAction, {
      duration: this._value * ACTION_TIMER
    }).start();
  }
  
  animationActionComplete() {
    var message = '';
    if (this._value === 1) {
      message = 'You held it long enough!';
    }
    this.setState({ textComplete: message });
  }
  
  getButtonWidthLayout(e) {
    this.setState({
        buttonWidth: e.nativeEvent.layout.width - 6,
        buttonHeight: e.nativeEvent.layout.height - 6
    });
  }
  
  getProgressStyles() {
    var width = this.state.pressAction.interpolate({
        inputRange: [0, 1],
        outputRange: [0, this.state.buttonWidth]
    });
    var animatedColor = this.state.pressAction.interpolate({
        inputRange: [0, 1],
        outputRange: COLORS
    });

    return {
        width: width,
        height: this.state.buttonHeight,
        backgroundColor: animatedColor
    }
  }
  
  render(): ReactElement {
    return (
      <View style={styles.container}>
            <TouchableWithoutFeedback 
                onPressIn={this.handlePressIn.bind(this)} 
                onPressOut={this.handlePressOut.bind(this)}
            >
                <View style={styles.button} 
                onLayout={this.getButtonWidthLayout.bind(this)}>
                    <Animated.View style={[
                      styles.animatedFill,
                      this.getProgressStyles()]} />
                    <Text style={styles.text}>Press And Hold Me</Text>
                </View>
            </TouchableWithoutFeedback>
            <View>
                <Text>{this.state.textComplete}</Text>
            </View>
       </View>
    );
  }
}


// ----------------------------------
// STYLES
// ----------------------------------

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center'
  },
  button: {
    padding: 10,
    borderWidth: 3,
    borderColor: '#111'
  },
  text: {
    backgroundColor: 'transparent',
    color: '#111'
  },
  animatedFill: {
    position: 'absolute',
    top: 0,
    left: 0
  }
});

References





Random Posts