@sector-labs/react-native-material-kit
Version:
Bringing Material Design to React Native
260 lines (224 loc) • 6.39 kB
JavaScript
//
// MDL style Spinner component.
//
// - @see [MDL Spinner](http://www.getmdl.io/components/index.html#loading-section/spinner)
// - [Props](#props)
// - [Defaults](#defaults)
// - [Built-in builders](#builders)
//
// Created by ywu on 15/8/14.
//
import React, {
Component,
} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
Platform,
View,
} from 'react-native';
import { ViewPropTypes } from '../utils';
import MKColor from '../MKColor';
import { getTheme } from '../theme';
// controlling speed of rotation: percent to degree
const SPINNER_ROTATE_INTERP = {
inputRange: [
0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1,
],
outputRange: [
'0deg', '45deg', '90deg', '135deg', '180deg', '225deg', '270deg', '315deg', '360deg',
],
};
const L_ARC_ROTATE_INTERP = Platform.OS === 'android' ? {
inputRange: [0, 0.5, 1],
outputRange: ['170deg', '0deg', '170deg'],
} : {
inputRange: [0, 0.5, 1],
outputRange: ['130deg', '-5deg', '130deg'],
};
const R_ARC_ROTATE_INTERP = Platform.OS === 'android' ? {
inputRange: [0, 0.5, 1],
outputRange: ['-170deg', '0deg', '-170deg'],
} : {
inputRange: [0, 0.5, 1],
outputRange: ['-130deg', '5deg', '-130deg'],
};
//
// ## <section id='Spinner'>Spinner</section>
// The default `Spinner` component.
//
class Spinner extends Component {
// ## <section id='props'>Props</section>
static propTypes = {
// [View Props](https://facebook.github.io/react-native/docs/view.html#props)...
...ViewPropTypes,
// Colors of the progress stroke
// - {`Array`|`String`} can be a list of colors
// or a single one
strokeColor: PropTypes.any,
// Width of the progress stroke
strokeWidth: PropTypes.number,
// Duration of the spinner animation, in milliseconds
spinnerAniDuration: PropTypes.number,
};
// ## <section id='defaults'>Defaults</section>
static defaultProps = {
strokeWidth: 3,
spinnerAniDuration: 1333,
style: {
width: 28,
height: 28,
},
};
constructor(props) {
super(props);
this._nextStrokeColorIndex = 0;
this._animatedContainerAngle = new Animated.Value(0);
this._animatedArcAngle = new Animated.Value(0);
this._aniUpdateSpinner = this._aniUpdateSpinner.bind(this);
this.theme = getTheme();
this.state = {
strokeColor: this.theme.primaryColor,
dimen: {},
};
}
// property initializers begin
_onLayout = ({ nativeEvent: { layout: { width, height } } }) => {
if (width > 0 && this.state.width !== width) {
this.setState({ dimen: { width, height } }, this._aniUpdateSpinner);
}
};
// property initializers end
// rotation & arc animation
_aniUpdateSpinner() {
const { width, height } = this.state.dimen;
if (!width || !height) {
return;
}
const duration = this.props.spinnerAniDuration || 1500;
this._animatedContainerAngle.setValue(0);
this._animatedArcAngle.setValue(0);
this._updateStrokeColor(() => {
Animated.parallel([
Animated.timing(this._animatedContainerAngle, {
duration,
toValue: 1,
useNativeDriver: true,
}),
Animated.timing(this._animatedArcAngle, {
duration,
toValue: 1,
useNativeDriver: true,
}),
]).start(({ finished }) => finished && setImmediate(this._aniUpdateSpinner));
});
}
// render the specified part (left or right) of the arc
_renderSpinnerLayer(left) {
const { width, height } = this.state.dimen;
const radii = width / 2;
const arcStyle = {
width,
height,
position: 'absolute',
borderColor: this.state.strokeColor,
borderBottomColor: MKColor.Transparent,
borderRadius: radii,
borderWidth: this.props.strokeWidth || 3,
};
if (!width || !height) {
return undefined;
}
let arcInterpolate;
if (left) {
arcInterpolate = L_ARC_ROTATE_INTERP;
arcStyle.borderRightColor = MKColor.Transparent;
} else {
arcInterpolate = R_ARC_ROTATE_INTERP;
arcStyle.right = 0;
arcStyle.borderLeftColor = MKColor.Transparent;
}
arcStyle.transform = [
{ rotate: this._animatedArcAngle.interpolate(arcInterpolate) },
];
return (
<View // the clipper layer
style={{
height,
width: radii,
overflow: 'hidden',
position: 'absolute',
left: left ? 0 : radii,
}}
>
<Animated.View // the arc
style={arcStyle}
/>
</View>
);
}
_updateStrokeColor(cb) {
const colors = this.props.strokeColor || this.theme.spinnerStyle.strokeColor;
let nextColor;
if (Array.isArray(colors)) {
const index = this._nextStrokeColorIndex % colors.length || 0;
this._nextStrokeColorIndex = index + 1;
nextColor = colors[index];
} else {
nextColor = colors;
}
this.setState({ strokeColor: nextColor || this.theme.primaryColor }, cb);
}
render() {
return (
<Animated.View // the container layer
ref="container"
style={[
{
transform: [
{ rotate: this._animatedContainerAngle.interpolate(SPINNER_ROTATE_INTERP) },
],
},
Spinner.defaultProps.style,
this.props.style,
]}
onLayout={this._onLayout}
>
{this._renderSpinnerLayer(true) /* spinner-left */}
{this._renderSpinnerLayer(false) /* spinner-right */}
</Animated.View>
);
}
}
// --------------------------
// Builder
//
const {
Builder,
} = require('../builder');
//
// ## Spinner builder
//
class SpinnerBuilder extends Builder {
build() {
const BuiltSpinner = class extends Spinner {};
BuiltSpinner.defaultProps = Object.assign({}, Spinner.defaultProps, this.toProps());
return BuiltSpinner;
}
}
// define builder method for each prop
SpinnerBuilder.defineProps(Spinner.propTypes);
// ----------
// ## <section id="builders">Built-in builders</section>
//
function spinner() {
return new SpinnerBuilder();
}
function singleColorSpinner() {
return spinner().withStrokeColor(getTheme().primaryColor);
}
// ## Public interface
module.exports = Spinner;
Spinner.Builder = SpinnerBuilder;
Spinner.spinner = spinner;
Spinner.singleColorSpinner = singleColorSpinner;