//
// 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 {
Animated,
LayoutChangeEvent,
TouchableWithoutFeedback,
TouchableWithoutFeedbackProps,
View,
} from 'react-native';
import {TouchEvent} from '../internal/MKTouchable';
import MKColor from '../MKColor';
import {getTheme} from '../theme';
import {CheckedListener} from '../types';
import * as utils from '../utils';
import Group from './RadioButtonGroup';
import Ripple, {RippleProps} from './Ripple';
const DEFAULT_EXTRA_RIPPLE_RADII = 16;
// ##
export type RadioButtonProps = {
// Color of the border (outer circle), when checked
borderOnColor?: string,
// Color of the border (outer circle), when unchecked
borderOffColor?: string,
// Color of the inner circle, when checked
fillColor?: string,
// Toggle status
checked?: boolean,
// Group to which the Radio button belongs
group?: Group,
// Callback when the toggle status is changed
onCheckedChange?: CheckedListener,
// How far the ripple can extend outside the RadioButton's border,
// default is 16
extraRippleRadius?: number,
} & RippleProps & TouchableWithoutFeedbackProps;
interface RadioButtonState {
checked: boolean
height: number
width: number
}
//
// ##
// The `RadioButton` component.
export default class RadioButton extends Component {
// ##
static defaultProps: RadioButtonProps = {
maskColor: MKColor.Transparent,
pointerEvents: 'box-only',
style: {
alignItems: 'center',
borderRadius: 10,
borderWidth: 2,
height: 20,
justifyContent: 'center',
width: 20,
},
};
private theme = getTheme();
private animatedSize = new Animated.Value(0);
private animatedRadius = new Animated.Value(0);
private group?: Group;
constructor(props: RadioButtonProps) {
super(props);
this.state = {
checked: false,
height: 0,
width: 0,
};
}
componentWillMount() {
this.group = this.props.group;
this.initView(this.props.checked);
this.group && this.group.add(this);
}
componentWillReceiveProps(nextProps: RadioButtonProps) {
if (this.group !== nextProps.group) {
this.group && this.group.remove(this);
this.group = nextProps.group;
this.group && this.group.add(this);
}
if (nextProps.checked !== this.props.checked) {
this.initView(nextProps.checked);
}
}
componentWillUnmount() {
this.group && this.group.remove(this);
}
// 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
this.group && this.group.onChecked(this, newState);
this.aniChecked(newState);
}
confirmUncheck() {
this.setState({checked: false},
() => this.emitCheckedChange(false));
this.aniChecked(false);
}
render() {
const defaultStyle = this.theme.radioStyle;
const mergedStyle = Object.assign({}, defaultStyle, utils.extractProps(this, [
'borderOnColor',
'borderOffColor',
'fillColor',
'rippleColor',
])) as RadioButtonProps;
const borderColor = this.state.checked ? mergedStyle.borderOnColor : mergedStyle.borderOffColor;
return (
);
}
private initView(checked: boolean = false) {
this.setState({checked});
this.aniChecked(checked);
}
private emitCheckedChange(checked: boolean) {
this.props.onCheckedChange && this.props.onCheckedChange({checked});
}
// animate the checked state, by scaling the inner circle
private aniChecked(checked: boolean) {
Animated.parallel([
Animated.timing(this.animatedRadius, {
duration: 220,
toValue: checked ? 5 : 0,
}),
Animated.timing(this.animatedSize, {
duration: 220,
toValue: checked ? 10 : 0,
}),
]).start();
}
private onLayout = ({nativeEvent: {layout: {width, height}}}: LayoutChangeEvent) => {
if (width === this.state.width && height === this.state.height) {
return;
}
const padding = this.props.extraRippleRadius || DEFAULT_EXTRA_RIPPLE_RADII;
this.setState({
height: height + padding,
width: width + padding,
});
};
// Touch events handling
private onTouch = ({type}: TouchEvent) => {
if (type === 'TOUCH_UP') {
if (!this.state.checked) {
this.confirmToggle();
}
}
};
}