react-native-material-kit-edge
Version:
Bringing Material Design to React Native. Fork by Airbitz
279 lines (234 loc) • 6.8 kB
JavaScript
//
// MDL-style Radio button component.
//
// - @see [MDL Radio Button](http://www.getmdl.io/components/index.html#toggles-section/radio)
// - [Props](#props)
// - [Defaults](#defaults)
//
// Created by ywu on 15/10/12.
//
import React, {
Component,
} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
TouchableWithoutFeedback,
View,
} from 'react-native';
import MKColor from '../MKColor';
import Ripple from './Ripple';
import * as utils from '../utils';
import { getTheme } from '../theme';
const DEFAULT_EXTRA_RIPPLE_RADII = 16;
//
// ## <section id='RadioButton'>RadioButton</section>
// The `RadioButton` component.
class RadioButton extends Component {
// ## <section id='props'>Props</section>
static propTypes = {
// [Ripple Props](Ripple.html#props)...
...Ripple.propTypes,
// Touchable...
...TouchableWithoutFeedback.propTypes,
// Color of the border (outer circle), when checked
borderOnColor: PropTypes.string,
// Color of the border (outer circle), when unchecked
borderOffColor: PropTypes.string,
// Color of the inner circle, when checked
fillColor: PropTypes.string,
// Toggle status
checked: PropTypes.bool,
// Group to which the Radio button belongs
group: PropTypes.object,
// Callback when the toggle status is changed
onCheckedChange: PropTypes.func,
// How far the ripple can extend outside the RadioButton's border,
// default is 16
extraRippleRadius: PropTypes.number,
};
// ## <section id='defaults'>Defaults</section>
static defaultProps = {
pointerEvents: 'box-only',
maskColor: MKColor.Transparent,
style: {
justifyContent: 'center',
alignItems: 'center',
width: 20,
height: 20,
borderWidth: 2,
borderRadius: 10,
},
};
constructor(props) {
super(props);
this.theme = getTheme();
this._animatedSize = new Animated.Value(0);
this._animatedRadius = new Animated.Value(0);
this._group = null;
this.state = {
checked: false,
width: 0,
height: 0,
};
}
componentWillMount() {
this.group = this.props.group;
this._initView(this.props.checked);
if (this.group && this.group.add instanceof Function)
this.group.add(this);
}
componentWillReceiveProps(nextProps) {
if (this.group !== nextProps.group) {
if (this.group && this.group.remove instanceof Function)
this.group.remove(this);
this.group = nextProps.group;
if (this.group && this.group.add instanceof Function)
this.group.add(this);
}
if (nextProps.checked !== this.props.checked) {
this._initView(nextProps.checked);
}
}
componentWillUnmount() {
if (this.group && this.group.remove instanceof Function)
this.group.remove(this);
}
_initView(checked) {
this.setState({ checked });
this._aniChecked(checked);
}
// property initializers begin
_onLayout = ({ nativeEvent: { layout: { width, height } } }) => {
if (width === this.state.width && height === this.state.height) {
return;
}
const padding = this.props.extraRippleRadius || DEFAULT_EXTRA_RIPPLE_RADII;
this.setState({
width: width + padding,
height: height + padding,
});
};
// Touch events handling
_onTouch = (evt) => {
if (evt.type === 'TOUCH_UP') {
if (!this.state.checked) {
this.confirmToggle();
}
}
};
// property initializers end
// When a toggle action (from the given state) is confirmed.
confirmToggle() {
const prevState = this.state.checked;
const newState = !prevState;
this.setState({ checked: newState }, () => {
this._emitCheckedChange(newState);
});
// update state of the other buttons in the group
if (this.group) {
this.group.onChecked(this, newState);
}
this._aniChecked(newState);
}
confirmUncheck() {
this.setState({ checked: false }, () => {
this._emitCheckedChange(false);
});
this._aniChecked(false);
}
_emitCheckedChange(checked) {
if (this.props.onCheckedChange) {
this.props.onCheckedChange({ checked });
}
}
// animate the checked state, by scaling the inner circle
_aniChecked(checked) {
Animated.parallel([
Animated.timing(this._animatedRadius, {
toValue: checked ? 5 : 0,
duration: 220,
}),
Animated.timing(this._animatedSize, {
toValue: checked ? 10 : 0,
duration: 220,
}),
]).start();
}
render() {
const defaultStyle = this.theme.radioStyle;
const mergedStyle = Object.assign({}, defaultStyle, utils.extractProps(this, [
'borderOnColor',
'borderOffColor',
'fillColor',
'rippleColor',
]));
const borderColor = this.state.checked ? mergedStyle.borderOnColor : mergedStyle.borderOffColor;
return (
<TouchableWithoutFeedback {...utils.extractTouchableProps(this)} >
<Ripple
{...this.props}
maskBorderRadiusInPercent={50}
rippleLocation="center"
rippleColor={mergedStyle.rippleColor}
onTouch={this._onTouch}
style={{
justifyContent: 'center',
alignItems: 'center',
width: this.state.width,
height: this.state.height,
}}
>
<View ref="outerCircle"
style={[
RadioButton.defaultProps.style,
{ borderColor },
this.props.style,
]}
onLayout={this._onLayout}
>
<Animated.View
ref="innerCircle"
style={{
backgroundColor: mergedStyle.fillColor,
width: this._animatedSize,
height: this._animatedSize,
borderRadius: this._animatedRadius,
}}
/>
</View>
</Ripple>
</TouchableWithoutFeedback>
);
}
}
//
// ## <section id='Group'>Group</section>
// Managing a group of radio buttons.
class Group {
constructor(onAdd, onRemove) {
this.buttons = [];
this.onAdd = onAdd;
this.onRemove = onRemove;
}
add(btn) {
if (this.onAdd instanceof Function && this.onAdd(btn) === false)
return;
if (this.buttons.indexOf(btn) < 0)
this.buttons.push(btn);
}
remove(btn) {
if (this.onRemove instanceof Function && this.onRemove(btn) === false)
return;
var index = this.buttons.indexOf(btn);
if (index >= 0)
this.buttons.splice(index, 1);
}
onChecked(btn, checked) {
if (checked)
this.buttons.forEach((it) => (it !== btn) && it.confirmUncheck());
}
}
// ## Public interface
module.exports = RadioButton;
RadioButton.Group = Group;