diff --git a/src/tappableSlider.js b/src/tappableSlider.js
new file mode 100644
index 00000000..631d876a
--- /dev/null
+++ b/src/tappableSlider.js
@@ -0,0 +1,612 @@
+"use strict";
+
+import React, { PureComponent } from "react";
+import PropTypes from "prop-types";
+
+import {
+ Animated,
+ StyleSheet,
+ PanResponder,
+ View,
+ Easing,
+ ViewPropTypes
+} from "react-native";
+
+// const shallowCompare = require("react-addons-shallow-compare"),
+// styleEqual = require("style-equal");
+
+var TRACK_SIZE = 4;
+var THUMB_SIZE = 20;
+
+function Rect(x, y, width, height) {
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+}
+
+Rect.prototype.containsPoint = function (x, y) {
+ return (
+ x >= this.x &&
+ y >= this.y &&
+ x <= this.x + this.width &&
+ y <= this.y + this.height
+ );
+};
+
+var DEFAULT_ANIMATION_CONFIGS = {
+ spring: {
+ friction: 7,
+ tension: 100
+ },
+ timing: {
+ duration: 150,
+ easing: Easing.inOut(Easing.ease),
+ delay: 0
+ }
+ // decay : { // This has a serious bug
+ // velocity : 1,
+ // deceleration : 0.997
+ // }
+};
+
+export default class Slider extends PureComponent {
+ static propTypes = {
+ /**
+ * Initial value of the slider. The value should be between minimumValue
+ * and maximumValue, which default to 0 and 1 respectively.
+ * Default value is 0.
+ *
+ * *This is not a controlled component*, e.g. if you don't update
+ * the value, the component won't be reset to its inital value.
+ */
+ value: PropTypes.number,
+
+ /**
+ * If true the user won't be able to move the slider.
+ * Default value is false.
+ */
+ disabled: PropTypes.bool,
+
+ /**
+ * Initial minimum value of the slider. Default value is 0.
+ */
+ minimumValue: PropTypes.number,
+
+ /**
+ * Initial maximum value of the slider. Default value is 1.
+ */
+ maximumValue: PropTypes.number,
+
+ /**
+ * Step value of the slider. The value should be between 0 and
+ * (maximumValue - minimumValue). Default value is 0.
+ */
+ step: PropTypes.number,
+
+ /**
+ * The color used for the track to the left of the button. Overrides the
+ * default blue gradient image.
+ */
+ minimumTrackTintColor: PropTypes.string,
+
+ /**
+ * The color used for the track to the right of the button. Overrides the
+ * default blue gradient image.
+ */
+ maximumTrackTintColor: PropTypes.string,
+
+ /**
+ * The color used for the thumb.
+ */
+ thumbTintColor: PropTypes.string,
+
+ /**
+ * The size of the touch area that allows moving the thumb.
+ * The touch area has the same center has the visible thumb.
+ * This allows to have a visually small thumb while still allowing the user
+ * to move it easily.
+ * The default is {width: 40, height: 40}.
+ */
+ thumbTouchSize: PropTypes.shape({
+ width: PropTypes.number,
+ height: PropTypes.number
+ }),
+
+ /**
+ * Callback continuously called while the user is dragging the slider.
+ */
+ onValueChange: PropTypes.func,
+
+ /**
+ * Callback called when the user starts changing the value (e.g. when
+ * the slider is pressed).
+ */
+ onSlidingStart: PropTypes.func,
+
+ /**
+ * Callback called when the user finishes changing the value (e.g. when
+ * the slider is released).
+ */
+ onSlidingComplete: PropTypes.func,
+
+ /**
+ * The style applied to the slider container.
+ */
+ style: ViewPropTypes.style,
+
+ /**
+ * The style applied to the track.
+ */
+ trackStyle: ViewPropTypes.style,
+
+ /**
+ * The style applied to the thumb.
+ */
+ thumbStyle: ViewPropTypes.style,
+
+ /**
+ * Set this to true to visually see the thumb touch rect in green.
+ */
+ debugTouchArea: PropTypes.bool,
+
+ /**
+ * Set to true to animate values with default 'timing' animation type
+ */
+ animateTransitions: PropTypes.bool,
+
+ /**
+ * Custom Animation type. 'spring' or 'timing'.
+ */
+ animationType: PropTypes.oneOf(["spring", "timing"]),
+
+ /**
+ * Used to configure the animation parameters. These are the same parameters in the Animated library.
+ */
+ animationConfig: PropTypes.object,
+
+ /**
+ * Used enable/disable ditect change of slider value to the tapped position
+ */
+ tapDirectChange: PropTypes.bool
+ };
+ state = {
+ containerSize: { width: 0, height: 0 },
+ trackSize: { width: 0, height: 0 },
+ thumbSize: { width: 0, height: 0 },
+ allMeasured: false,
+ value: new Animated.Value(this.props.value),
+ firstTouchOnThumb: undefined
+ };
+ static defaultProps = {
+ value: 0,
+ minimumValue: 0,
+ maximumValue: 1,
+ step: 0,
+ minimumTrackTintColor: "#3f3f3f",
+ maximumTrackTintColor: "#b3b3b3",
+ thumbTintColor: "#343434",
+ thumbTouchSize: { width: 40, height: 40 },
+ debugTouchArea: false,
+ animationType: "timing",
+ tapDirectChange: false
+ };
+ componentWillMount() {
+ this._panResponder = PanResponder.create({
+ onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
+ onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
+ onPanResponderGrant: this._handlePanResponderGrant,
+ onPanResponderMove: this._handlePanResponderMove,
+ onPanResponderRelease: this._handlePanResponderEnd,
+ onPanResponderTerminationRequest: this._handlePanResponderRequestEnd,
+ onPanResponderTerminate: this._handlePanResponderEnd
+ });
+ }
+ componentWillReceiveProps(nextProps) {
+ var newValue = nextProps.value;
+
+ if (this.props.value !== newValue) {
+ if (this.props.animateTransitions) {
+ this._setCurrentValueAnimated(newValue);
+ } else {
+ this._setCurrentValue(newValue);
+ }
+ }
+ }
+ // shouldComponentUpdate(nextProps, nextState) {
+ // // We don't want to re-render in the following cases:
+ // // - when only the 'value' prop changes as it's already handled with the Animated.Value
+ // // - when the event handlers change (rendering doesn't depend on them)
+ // // - when the style props haven't actually change
+
+ // return (
+ // shallowCompare(
+ // {
+ // props: this._getPropsForComponentUpdate(this.props),
+ // state: this.state
+ // },
+ // this._getPropsForComponentUpdate(nextProps),
+ // nextState
+ // ) ||
+ // !styleEqual(this.props.style, nextProps.style) ||
+ // !styleEqual(this.props.trackStyle, nextProps.trackStyle) ||
+ // !styleEqual(this.props.thumbStyle, nextProps.thumbStyle)
+ // );
+ // }
+ render() {
+ var {
+ minimumValue,
+ maximumValue,
+ minimumTrackTintColor,
+ maximumTrackTintColor,
+ thumbTintColor,
+ styles,
+ style,
+ trackStyle,
+ thumbStyle,
+ debugTouchArea,
+ ...other
+ } = this.props;
+ var {
+ value,
+ containerSize,
+ trackSize,
+ thumbSize,
+ allMeasured
+ } = this.state;
+ var mainStyles = styles || defaultStyles;
+ var thumbLeft = value.interpolate({
+ inputRange: [minimumValue, maximumValue],
+ outputRange: [0, containerSize.width - thumbSize.width]
+ //extrapolate: 'clamp',
+ });
+ var valueVisibleStyle = {};
+ if (!allMeasured) {
+ valueVisibleStyle.opacity = 0;
+ }
+
+ var minimumTrackStyle = {
+ position: "absolute",
+ width: Animated.add(thumbLeft, thumbSize.width / 2),
+ marginTop: -trackSize.height,
+ backgroundColor: minimumTrackTintColor,
+ ...valueVisibleStyle
+ };
+
+ var touchOverflowStyle = this._getTouchOverflowStyle();
+
+ return (
+
+
+
+
+
+ {debugTouchArea === true &&
+ this._renderDebugThumbTouchRect(thumbLeft)}
+
+
+ );
+ }
+
+ _getPropsForComponentUpdate(props) {
+ var {
+ value,
+ onValueChange,
+ onSlidingStart,
+ onSlidingComplete,
+ style,
+ trackStyle,
+ thumbStyle,
+ ...otherProps
+ } = props;
+
+ return otherProps;
+ }
+
+ _handleStartShouldSetPanResponder = (
+ e: Object /*gestureState: Object*/
+ ): boolean => {
+ // Should we become active when the user presses down on the thumb?
+ let isTouchOnThumb = this._thumbHitTest(e);
+ this.setState({ firstTouchOnThumb: isTouchOnThumb });
+ return this.props.tapDirectChange ? true : isTouchOnThumb;
+ };
+
+ _handleMoveShouldSetPanResponder(/*e: Object, gestureState: Object*/): boolean {
+ // Should we become active when the user moves a touch over the thumb?
+ return false;
+ }
+
+ _handlePanResponderGrant = (e: Object, gestureState: Object) => {
+ this._previousLeft = this._getThumbLeft(this._getCurrentValue());
+ this._fireChangeEvent("onSlidingStart");
+
+ if (this.props.tapDirectChange && !this.state.firstTouchOnThumb) {
+ this._setCurrentValue(
+ this._getValue(
+ gestureState,
+ e.nativeEvent.locationX - this.props.thumbTouchSize.width / 2
+ )
+ );
+ this._fireChangeEvent("onValueChange");
+ }
+ };
+ _handlePanResponderMove = (e: Object, gestureState: Object) => {
+ if (this.props.disabled) {
+ return;
+ }
+
+ if (!this.props.tapDirectChange || this.state.firstTouchOnThumb) {
+ this._setCurrentValue(this._getValue(gestureState));
+ this._fireChangeEvent("onValueChange");
+ }
+ };
+ _handlePanResponderRequestEnd(e: Object, gestureState: Object) {
+ // Should we allow another component to take over this pan?
+ return false;
+ }
+ _handlePanResponderEnd = (e: Object, gestureState: Object) => {
+ if (this.props.disabled) {
+ return;
+ }
+
+ if (this.state.firstTouchOnThumb) {
+ this._setCurrentValue(this._getValue(gestureState));
+ }
+
+ this.setState({ firstTouchOnThumb: undefined });
+
+ this._fireChangeEvent("onSlidingComplete");
+ };
+
+ _measureContainer = (x: Object) => {
+ this._handleMeasure("containerSize", x);
+ };
+ _measureTrack = (x: Object) => {
+ this._handleMeasure("trackSize", x);
+ };
+
+ _measureThumb = (x: Object) => {
+ this._handleMeasure("thumbSize", x);
+ };
+
+ _handleMeasure = (name: string, x: Object) => {
+ var { width, height } = x.nativeEvent.layout;
+ var size = { width: width, height: height };
+
+ var storeName = `_${name}`;
+ var currentSize = this[storeName];
+ if (
+ currentSize &&
+ width === currentSize.width &&
+ height === currentSize.height
+ ) {
+ return;
+ }
+ this[storeName] = size;
+
+ if (this._containerSize && this._trackSize && this._thumbSize) {
+ this.setState({
+ containerSize: this._containerSize,
+ trackSize: this._trackSize,
+ thumbSize: this._thumbSize,
+ allMeasured: true
+ });
+ }
+ };
+
+ _getRatio = (value: number) => {
+ return (
+ (value - this.props.minimumValue) /
+ (this.props.maximumValue - this.props.minimumValue)
+ );
+ };
+
+ _getThumbLeft = (value: number) => {
+ var ratio = this._getRatio(value);
+ return (
+ ratio * (this.state.containerSize.width - this.state.thumbSize.width)
+ );
+ };
+
+ _getValue = (gestureState: Object, offset: number) => {
+ var length = this.state.containerSize.width - this.state.thumbSize.width;
+ var thumbLeft = offset ? offset : this._previousLeft + gestureState.dx;
+
+ var ratio = thumbLeft / length;
+
+ if (this.props.step) {
+ return Math.max(
+ this.props.minimumValue,
+ Math.min(
+ this.props.maximumValue,
+ this.props.minimumValue +
+ Math.round(
+ (ratio * (this.props.maximumValue - this.props.minimumValue)) /
+ this.props.step
+ ) *
+ this.props.step
+ )
+ );
+ } else {
+ return Math.max(
+ this.props.minimumValue,
+ Math.min(
+ this.props.maximumValue,
+ ratio * (this.props.maximumValue - this.props.minimumValue) +
+ this.props.minimumValue
+ )
+ );
+ }
+ };
+
+ _getCurrentValue = () => {
+ return this.state.value.__getValue();
+ };
+
+ _setCurrentValue = (value: number) => {
+ this.state.value.setValue(value);
+ };
+
+ _setCurrentValueAnimated = (value: number) => {
+ var animationType = this.props.animationType;
+ var animationConfig = Object.assign(
+ {},
+ DEFAULT_ANIMATION_CONFIGS[animationType],
+ this.props.animationConfig,
+ { toValue: value }
+ );
+
+ Animated[animationType](this.state.value, animationConfig).start();
+ };
+
+ _fireChangeEvent = event => {
+ if (this.props[event]) {
+ this.props[event](this._getCurrentValue());
+ }
+ };
+
+ _getTouchOverflowSize = () => {
+ var state = this.state;
+ var props = this.props;
+
+ var size = {};
+ if (state.allMeasured === true) {
+ size.width = Math.max(
+ 0,
+ props.thumbTouchSize.width - state.thumbSize.width
+ );
+ size.height = Math.max(
+ 0,
+ props.thumbTouchSize.height - state.containerSize.height
+ );
+ }
+
+ return size;
+ };
+
+ _getTouchOverflowStyle = () => {
+ var { width, height } = this._getTouchOverflowSize();
+
+ var touchOverflowStyle = {};
+ if (width !== undefined && height !== undefined) {
+ var verticalMargin = -height / 2;
+ touchOverflowStyle.marginTop = verticalMargin;
+ touchOverflowStyle.marginBottom = verticalMargin;
+
+ var horizontalMargin = -width / 2;
+ touchOverflowStyle.marginLeft = horizontalMargin;
+ touchOverflowStyle.marginRight = horizontalMargin;
+ }
+
+ if (this.props.debugTouchArea === true) {
+ touchOverflowStyle.backgroundColor = "orange";
+ touchOverflowStyle.opacity = 0.5;
+ }
+
+ return touchOverflowStyle;
+ };
+
+ _thumbHitTest = (e: Object) => {
+ var nativeEvent = e.nativeEvent;
+ var thumbTouchRect = this._getThumbTouchRect();
+ return thumbTouchRect.containsPoint(
+ nativeEvent.locationX,
+ nativeEvent.locationY
+ );
+ };
+
+ _getThumbTouchRect = () => {
+ var state = this.state;
+ var props = this.props;
+ var touchOverflowSize = this._getTouchOverflowSize();
+
+ return new Rect(
+ touchOverflowSize.width / 2 +
+ this._getThumbLeft(this._getCurrentValue()) +
+ (state.thumbSize.width - props.thumbTouchSize.width) / 2,
+ touchOverflowSize.height / 2 +
+ (state.containerSize.height - props.thumbTouchSize.height) / 2,
+ props.thumbTouchSize.width,
+ props.thumbTouchSize.height
+ );
+ };
+
+ _renderDebugThumbTouchRect = thumbLeft => {
+ var thumbTouchRect = this._getThumbTouchRect();
+ var positionStyle = {
+ left: thumbLeft,
+ top: thumbTouchRect.y,
+ width: thumbTouchRect.width,
+ height: thumbTouchRect.height
+ };
+
+ return (
+
+ );
+ };
+}
+
+var defaultStyles = StyleSheet.create({
+ container: {
+ height: 40,
+ justifyContent: "center"
+ },
+ track: {
+ height: TRACK_SIZE,
+ borderRadius: TRACK_SIZE / 2
+ },
+ thumb: {
+ position: "absolute",
+ width: THUMB_SIZE,
+ height: THUMB_SIZE,
+ borderRadius: THUMB_SIZE / 2
+ },
+ touchArea: {
+ position: "absolute",
+ backgroundColor: "transparent",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0
+ },
+ debugThumbTouchArea: {
+ position: "absolute",
+ backgroundColor: "green",
+ opacity: 0.5
+ }
+});
+