// // MDL style switch component. // // // // - @see [MDL Switch](http://bit.ly/1IcHMPo) // - [Props](#props) // - [Defaults](#defaults) // // Created by ywu on 15/7/28. // import React, {Component, createRef} from 'react'; import { Animated, GestureResponderEvent, TouchableWithoutFeedback, TouchableWithoutFeedbackProps, View, } from 'react-native'; import {getTheme, Theme} from '../theme'; import {CheckedListener} from "../types"; import * as utils from '../utils'; import AnimatedThumb, {Thumb} from './SwitchThumb'; const defaultThumbRadius = 14; const defaultTrackLength = 48; const defaultTrackSize = 20; // ##
Props
export type SwitchProps = { // Toggle status of the `Switch` checked?: boolean, // Callback when the toggle status is changed. onCheckedChange?: CheckedListener, // Color of the track, when switch is checked onColor?: string, // Color of the track, when switch is off offColor?: string, // The thickness of the Switch track trackSize?: number, // The length of the Switch track trackLength?: number, // Radius of the thumb button thumbRadius?: number, // Color of the thumb, when switch is checked thumbOnColor?: string, // Color of the thumb, when switch is off thumbOffColor?: string, // Duration of the thumb sliding animation, in milliseconds thumbAniDuration?: number, // Color of the ripple layer rippleColor?: string, // Duration of the ripple effect, in milliseconds rippleAniDuration?: number, } & TouchableWithoutFeedbackProps; interface SwitchState { checked: boolean, thumbFrame: { padding: number, r: number, rippleRadii: number, x: number, }, trackLength: number, trackMargin: number, trackRadii: number, trackSize: number, } // ##
Switch
// The `Switch` component. Which is made up of a `Track` and a [`Thumb`](#thumb). export default class Switch extends Component { // ##
Defaults
static defaultProps: SwitchProps = { checked: false, thumbRadius: defaultThumbRadius, trackLength: defaultTrackLength, trackSize: defaultTrackSize, }; private theme = getTheme(); private thumbRef = createRef(); private animatedThumbLeft = new Animated.Value(0); constructor(props: SwitchProps) { super(props); this.state = { checked: false, thumbFrame: { padding: 0, r: 0, rippleRadii: 0, x: 0, }, trackLength: 0, trackMargin: 0, trackRadii: 0, trackSize: 0, }; } componentWillMount() { this.initLayout(this.props); } componentWillReceiveProps(nextProps: SwitchProps) { this.initLayout(nextProps); } // Rendering the `Switch` render() { const touchProps = { delayLongPress: this.props.delayLongPress, delayPressIn: this.props.delayPressIn, delayPressOut: this.props.delayPressOut, onLongPress: this.props.onLongPress, }; const mergedStyle = Object.assign({}, this.theme.switchStyle, utils.compact( { offColor: this.props.offColor, onColor: this.props.onColor, rippleColor: this.props.rippleColor, thumbOffColor: this.props.thumbOffColor, thumbOnColor: this.props.thumbOnColor, })); const thumbFrame = this.state.thumbFrame; const thumbProps = { checked: this.state.checked, offColor: mergedStyle.thumbOffColor, onColor: mergedStyle.thumbOnColor, radius: this.props.thumbRadius, rippleAniDuration: this.props.rippleAniDuration, rippleColor: mergedStyle.rippleColor, rippleRadius: thumbFrame.rippleRadii, style: { left: this.animatedThumbLeft, top: 0, }, thumbStyle: { borderRadius: this.props.thumbRadius, left: thumbFrame.padding, top: thumbFrame.padding, height: (this.props.thumbRadius || defaultThumbRadius) * 2, width: (this.props.thumbRadius || defaultThumbRadius) * 2, }, }; return ( ); } // Un-boxing the `Thumb` node from `AnimatedComponent`, // in order to access the component functions defined in `Thumb` private get thumb(): Thumb | null { const animatedThumb = this.thumbRef.current as any; return animatedThumb && animatedThumb.getNode() as Thumb; } private getBgColor(theme: Theme): string { const onColor = this.props.onColor || theme.onColor as string; const offColor = this.props.offColor || theme.offColor as string; return this.state.checked ? onColor : offColor; } private onPress = (event: GestureResponderEvent) => { this.confirmToggle(); this.props.onPress && this.props.onPress(event); }; private onPressIn = (event: GestureResponderEvent) => { this.startToggle(); this.props.onPressIn && this.props.onPressIn(event); }; private onPressOut = (event: GestureResponderEvent) => { this.endToggle(); this.props.onPressOut && this.props.onPressOut(event); }; // Layout the thumb according to the size of the track private layoutThumb(checked: boolean | undefined, thumbRadius: number | undefined, trackLength: number | undefined, trackSize: number | undefined ): any { trackSize = trackSize || defaultTrackSize; trackLength = trackLength || defaultTrackLength; const thumbRadii = thumbRadius || defaultThumbRadius; const rippleRadii = trackLength - trackSize; const trackRadii = trackSize / 2; const trackMargin = rippleRadii - trackRadii; // make room for ripple const thumbLeft = checked ? trackMargin + trackRadii : 0; this.animatedThumbLeft.setValue(thumbLeft); return { thumbFrame: { padding: rippleRadii - thumbRadii, r: thumbRadii, rippleRadii, x: thumbLeft, }, trackLength, trackMargin, trackRadii, trackSize, }; } // init layout according to the props private initLayout(props: SwitchProps) { const nextState = this.layoutThumb(props.checked, props.thumbRadius, props.trackLength, props.trackSize); this.setState({ ...nextState, checked: props.checked || false, }); } // Move the thumb left or right according to the current state private translateThumb() { this.animatedThumbLeft.setValue(this.state.thumbFrame.x); const newX = this.computeThumbX(this.state.checked); Animated.timing(this.animatedThumbLeft, { duration: this.props.thumbAniDuration || 300, toValue: newX, }).start(() => { this.state.thumbFrame.x = newX; }); } // Calc the next position (x-axis) of the thumb private computeThumbX(toChecked: boolean) { if (!this.state.thumbFrame.r) { return 0; } const { trackLength, trackSize } = this.state; const dx = (toChecked ? 1 : -1) * (trackLength - trackSize); return this.state.thumbFrame.x + dx; } // When a toggle action started. private startToggle() { this.thumb && this.thumb.startToggle(); } // When a toggle action is confirmed. private confirmToggle() { const prevState = this.state.checked; this.setState({checked: !prevState}, () => { this.thumb && this.thumb.confirmToggle(prevState); this.translateThumb(); if (this.props.onCheckedChange) { this.props.onCheckedChange({checked: this.state.checked}); } }); } // When a toggle action is finished (confirmed or canceled). private endToggle() { this.thumb && this.thumb.endToggle(); } }