UNPKG

16.6 kBJavaScriptView Raw
1var __rest = (this && this.__rest) || function (s, e) {
2 var t = {};
3 for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4 t[p] = s[p];
5 if (s != null && typeof Object.getOwnPropertySymbols === "function")
6 for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7 if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8 t[p[i]] = s[p[i]];
9 }
10 return t;
11};
12import React, { useEffect, useRef, useState } from 'react';
13import { View, StyleSheet, Animated, Easing, PanResponder, Platform, } from 'react-native';
14import { withTheme } from '../config';
15const TRACK_SIZE = 4;
16const THUMB_SIZE = 40;
17const TRACK_STYLE = Platform.select({ web: 0, default: -1 });
18const DEFAULT_ANIMATION_CONFIGS = {
19 spring: {
20 friction: 7,
21 tension: 100,
22 useNativeDriver: true,
23 },
24 timing: {
25 duration: 150,
26 easing: Easing.inOut(Easing.ease),
27 delay: 0,
28 useNativeDriver: true,
29 },
30};
31const getBoundedValue = (value, maximumValue, minimumValue) => {
32 return Math.max(Math.min(value, maximumValue), minimumValue);
33};
34class Rect {
35 constructor(x, y, width, height) {
36 this.x = x;
37 this.y = y;
38 this.width = width;
39 this.height = height;
40 }
41 containsPoint(x, y) {
42 return (x >= this.x &&
43 y >= this.y &&
44 x <= this.x + this.width &&
45 y <= this.y + this.height);
46 }
47}
48const Slider = (props) => {
49 const _previousLeft = useRef(0);
50 const [allMeasured, setAllMeasured] = useState(false);
51 const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
52 const [trackSize, setTrackSize] = useState({ width: 0, height: 0 });
53 const [thumbSize, setThumbSize] = useState({ width: 0, height: 0 });
54 const containerSizeValue = useRef(containerSize);
55 const trackSizeValue = useRef(trackSize);
56 const thumbSizeValue = useRef(thumbSize);
57 const isVertical = useRef(props.orientation === 'vertical');
58 const [value] = useState(new Animated.Value(getBoundedValue(props.value || 0, props.maximumValue || 1, props.minimumValue || 0)));
59 useEffect(() => {
60 containerSizeValue.current = containerSize;
61 }, [containerSize]);
62 useEffect(() => {
63 trackSizeValue.current = trackSize;
64 }, [trackSize]);
65 useEffect(() => {
66 thumbSizeValue.current = thumbSize;
67 }, [thumbSize]);
68 const measureContainer = (x) => {
69 handleMeasure('containerSize', x);
70 };
71 const measureTrack = (x) => {
72 handleMeasure('trackSize', x);
73 };
74 const measureThumb = (x) => {
75 handleMeasure('thumbSize', x);
76 };
77 const handleMeasure = (name, x) => {
78 const { width: layoutWidth, height: layoutHeight } = x.nativeEvent.layout;
79 const width = isVertical.current ? layoutHeight : layoutWidth;
80 const height = isVertical.current ? layoutWidth : layoutHeight;
81 const size = { width, height };
82 if (name === 'containerSize') {
83 setContainerSize(size);
84 }
85 if (name === 'thumbSize') {
86 setThumbSize(size);
87 }
88 if (name === 'trackSize') {
89 setTrackSize(size);
90 }
91 if (thumbSize && trackSize && containerSize) {
92 setAllMeasured(true);
93 }
94 };
95 const currentPropValue = useRef(props.value || 0);
96 const prevPropValue = useRef(0);
97 const didMountRef = useRef(false);
98 const setCurrentValue = React.useCallback((value1) => {
99 value.setValue(value1);
100 }, [value]);
101 React.useEffect(() => {
102 setCurrentValue(props.value || 0);
103 }, [props.value, setCurrentValue]);
104 useEffect(() => {
105 prevPropValue.current = props.value || 0;
106 if (didMountRef.current) {
107 const newValue = getBoundedValue(props.value || 0, props.maximumValue || 1, props.minimumValue || 0);
108 if (prevPropValue.current !== newValue) {
109 if (props.animateTransitions) {
110 setCurrentValueAnimated(new Animated.Value(newValue));
111 }
112 else {
113 setCurrentValue(newValue);
114 }
115 }
116 }
117 else {
118 didMountRef.current = true;
119 }
120 });
121 const setCurrentValueAnimated = (value1) => {
122 const { animationType } = props;
123 const animationConfig = Object.assign({}, DEFAULT_ANIMATION_CONFIGS[animationType || 'timing'], props.animationConfig, {
124 toValue: value1,
125 });
126 Animated[animationType || 'timing'](value, animationConfig).start();
127 };
128 const handleMoveShouldSetPanResponder = () => {
129 // Should we become active when the user moves a touch over the thumb?
130 if (!TRACK_STYLE) {
131 return true;
132 }
133 return false;
134 };
135 const handlePanResponderGrant = () => {
136 _previousLeft.current = getThumbLeft(currentPropValue.current);
137 fireChangeEvent('onSlidingStart');
138 };
139 const handlePanResponderMove = (_, gestureState) => {
140 if (props.disabled) {
141 return;
142 }
143 setCurrentValue(getValue(gestureState));
144 fireChangeEvent('onValueChange');
145 };
146 const handlePanResponderRequestEnd = () => {
147 // Should we allow another component to take over this pan?
148 return false;
149 };
150 const handlePanResponderEnd = (_, gestureState) => {
151 if (props.disabled) {
152 return;
153 }
154 setCurrentValue(getValue(gestureState));
155 fireChangeEvent('onSlidingComplete');
156 };
157 const thumbHitTest = ({ nativeEvent }) => {
158 const thumbTouchRect = getThumbTouchRect();
159 return thumbTouchRect.containsPoint(nativeEvent.locationX, nativeEvent.locationY);
160 };
161 const handleStartShouldSetPanResponder = (e /* gestureState: Object */) => {
162 // Should we become active when the user presses down on the thumb?
163 if (!props.allowTouchTrack && !TRACK_STYLE) {
164 return thumbHitTest(e);
165 }
166 if (!trackStyle) {
167 setCurrentValue(getOnTouchValue(e));
168 }
169 fireChangeEvent('onValueChange');
170 return true;
171 };
172 const fireChangeEvent = (event) => {
173 var _a;
174 if (props === null || props === void 0 ? void 0 : props[event]) {
175 (_a = props === null || props === void 0 ? void 0 : props[event]) === null || _a === void 0 ? void 0 : _a.call(props, currentPropValue.current);
176 }
177 };
178 // get value of where some touched on slider.
179 const getOnTouchValue = ({ nativeEvent }) => {
180 const location = isVertical.current
181 ? nativeEvent.locationY
182 : nativeEvent.locationX;
183 return getValueForTouch(location);
184 };
185 const getValueForTouch = (location) => {
186 const length = containerSizeValue.current.width - thumbSizeValue.current.width;
187 const ratio = location / length;
188 let newValue = ratio * ((props.maximumValue || 1) - (props.minimumValue || 0));
189 if (props.step) {
190 newValue = Math.round(newValue / props.step) * props.step;
191 }
192 return getBoundedValue(newValue + (props.minimumValue || 0), props.maximumValue || 1, props.minimumValue || 0);
193 };
194 const getTouchOverflowSize = () => {
195 const { thumbTouchSize } = props;
196 const size = {};
197 if (allMeasured === true) {
198 size.width = Math.max(0, ((thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.width) || THUMB_SIZE) - thumbSizeValue.current.width);
199 size.height = Math.max(0, ((thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.height) || THUMB_SIZE) -
200 containerSizeValue.current.height);
201 }
202 return size;
203 };
204 const getTouchOverflowStyle = () => {
205 const { width, height } = getTouchOverflowSize();
206 const touchOverflowStyle = {};
207 if (width !== undefined && height !== undefined) {
208 const verticalMargin = -height / 2;
209 touchOverflowStyle.marginTop = verticalMargin;
210 touchOverflowStyle.marginBottom = verticalMargin;
211 const horizontalMargin = -width / 2;
212 touchOverflowStyle.marginLeft = horizontalMargin;
213 touchOverflowStyle.marginRight = horizontalMargin;
214 }
215 if (props.debugTouchArea === true) {
216 touchOverflowStyle.backgroundColor = 'orange';
217 touchOverflowStyle.opacity = 0.5;
218 }
219 return touchOverflowStyle;
220 };
221 const getValue = (gestureState) => {
222 const location = _previousLeft.current +
223 (isVertical.current ? gestureState.dy : gestureState.dx);
224 return getValueForTouch(location);
225 };
226 React.useEffect(() => {
227 let listenerID = value.addListener((obj) => {
228 currentPropValue.current = obj.value;
229 });
230 return () => {
231 value.removeListener(listenerID);
232 };
233 });
234 const getRatio = (value1) => {
235 return ((value1 - (props.minimumValue || 0)) /
236 ((props.maximumValue || 1) - (props.minimumValue || 0)));
237 };
238 const getThumbLeft = (value1) => {
239 const ratio = getRatio(value1);
240 return (ratio * (containerSizeValue.current.width - thumbSizeValue.current.width));
241 };
242 const getThumbTouchRect = () => {
243 const { thumbTouchSize } = props;
244 const touchOverflowSize = getTouchOverflowSize();
245 const height = (touchOverflowSize === null || touchOverflowSize === void 0 ? void 0 : touchOverflowSize.height) / 2 +
246 (containerSizeValue.current.height -
247 ((thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.height) || THUMB_SIZE)) /
248 2;
249 const width = touchOverflowSize.width / 2 +
250 getThumbLeft(currentPropValue.current) +
251 (thumbSizeValue.current.width - ((thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.width) || THUMB_SIZE)) /
252 2;
253 if (isVertical.current) {
254 return new Rect(height, width, (thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.width) || THUMB_SIZE, (thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.height) || THUMB_SIZE);
255 }
256 return new Rect(width, height, (thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.width) || THUMB_SIZE, (thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.height) || THUMB_SIZE);
257 };
258 const renderDebugThumbTouchRect = (thumbLeft) => {
259 const thumbTouchRect = getThumbTouchRect();
260 const positionStyle = {
261 left: thumbLeft,
262 top: thumbTouchRect.y,
263 width: thumbTouchRect.width,
264 height: thumbTouchRect.height,
265 };
266 return <Animated.View style={positionStyle} pointerEvents="none"/>;
267 };
268 const getMinimumTrackStyles = (thumbStart) => {
269 const minimumTrackStyle = {
270 position: 'absolute',
271 };
272 if (isVertical.current) {
273 minimumTrackStyle.height = Animated.add(thumbStart, thumbSizeValue.current.height / 2);
274 minimumTrackStyle.marginLeft = trackSize.width * TRACK_STYLE;
275 }
276 else {
277 minimumTrackStyle.width = Animated.add(thumbStart, thumbSizeValue.current.width / 2);
278 minimumTrackStyle.marginTop = trackSize.height * TRACK_STYLE;
279 }
280 return minimumTrackStyle;
281 };
282 const panResponder = useRef(PanResponder.create({
283 onStartShouldSetPanResponder: handleStartShouldSetPanResponder,
284 onMoveShouldSetPanResponder: handleMoveShouldSetPanResponder,
285 onPanResponderGrant: handlePanResponderGrant,
286 onPanResponderMove: handlePanResponderMove,
287 onPanResponderRelease: handlePanResponderEnd,
288 onPanResponderTerminationRequest: handlePanResponderRequestEnd,
289 onPanResponderTerminate: handlePanResponderEnd,
290 })).current;
291 const { minimumValue, maximumValue, minimumTrackTintColor, maximumTrackTintColor, thumbTintColor, containerStyle, style, trackStyle, thumbStyle, thumbProps, debugTouchArea } = props, other = __rest(props, ["minimumValue", "maximumValue", "minimumTrackTintColor", "maximumTrackTintColor", "thumbTintColor", "containerStyle", "style", "trackStyle", "thumbStyle", "thumbProps", "debugTouchArea"]);
292 const mainStyles = containerStyle || styles;
293 const appliedTrackStyle = StyleSheet.flatten([styles.track, trackStyle]);
294 const thumbStart = value.interpolate({
295 inputRange: [minimumValue || 0, maximumValue || 1],
296 outputRange: [0, containerSize.width - thumbSize.width],
297 });
298 const valueVisibleStyle = {};
299 if (!allMeasured) {
300 valueVisibleStyle.height = 0;
301 valueVisibleStyle.width = 0;
302 }
303 const minimumTrackStyle = Object.assign(Object.assign(Object.assign({}, getMinimumTrackStyles(thumbStart)), { backgroundColor: minimumTrackTintColor }), valueVisibleStyle);
304 const touchOverflowStyle = getTouchOverflowStyle();
305 return (<View {...other} style={StyleSheet.flatten([
306 isVertical.current
307 ? mainStyles.containerVertical
308 : mainStyles.containerHorizontal,
309 style,
310 ])} onLayout={measureContainer} accessibilityRole="adjustable" accessibilityValue={{
311 min: minimumValue,
312 max: maximumValue,
313 now: props.value,
314 }}>
315 <View style={StyleSheet.flatten([
316 mainStyles.track,
317 isVertical.current
318 ? mainStyles.trackVertical
319 : mainStyles.trackHorizontal,
320 appliedTrackStyle,
321 { backgroundColor: maximumTrackTintColor },
322 ])} onLayout={measureTrack}/>
323
324 <Animated.View style={StyleSheet.flatten([
325 mainStyles.track,
326 isVertical.current
327 ? mainStyles.trackVertical
328 : mainStyles.trackHorizontal,
329 appliedTrackStyle,
330 minimumTrackStyle,
331 ])}/>
332 <SliderThumb isVisible={allMeasured} onLayout={measureThumb} style={thumbStyle} color={thumbTintColor} start={thumbStart} vertical={isVertical.current} {...thumbProps}/>
333 <View style={StyleSheet.flatten([styles.touchArea, touchOverflowStyle])} {...panResponder.panHandlers}>
334 {debugTouchArea === true && renderDebugThumbTouchRect(thumbStart)}
335 </View>
336 </View>);
337};
338const SliderThumb = (_a) => {
339 var { Component, isVisible, onLayout, style, start, color, vertical } = _a, props = __rest(_a, ["Component", "isVisible", "onLayout", "style", "start", "color", "vertical"]);
340 const ThumbComponent = Component || Animated.View;
341 const axis = vertical ? 'translateY' : 'translateX';
342 const thumbPosition = [{ [axis]: start }];
343 const styleTransform = (style && style.transform) || [];
344 const visibleStyle = isVisible ? {} : { height: 0, width: 0 };
345 return (<ThumbComponent testID="sliderThumb" onLayout={onLayout} style={StyleSheet.flatten([
346 Object.assign({ backgroundColor: color, transform: [...thumbPosition, ...styleTransform] }, visibleStyle),
347 styles.thumb,
348 style,
349 ])} {...props}/>);
350};
351Slider.defaultProps = {
352 value: 0,
353 minimumValue: 0,
354 maximumValue: 1,
355 step: 0,
356 minimumTrackTintColor: '#3f3f3f',
357 maximumTrackTintColor: '#b3b3b3',
358 allowTouchTrack: false,
359 thumbTintColor: 'red',
360 thumbTouchSize: { width: THUMB_SIZE, height: THUMB_SIZE },
361 debugTouchArea: false,
362 animationType: 'timing',
363 orientation: 'horizontal',
364};
365const styles = StyleSheet.create({
366 containerHorizontal: {
367 height: 40,
368 justifyContent: 'center',
369 },
370 containerVertical: {
371 width: 40,
372 flexDirection: 'column',
373 alignItems: 'center',
374 },
375 track: {
376 borderRadius: TRACK_SIZE / 2,
377 },
378 trackHorizontal: {
379 height: TRACK_SIZE,
380 },
381 trackVertical: {
382 flex: 1,
383 width: TRACK_SIZE,
384 },
385 thumb: {
386 position: 'absolute',
387 width: THUMB_SIZE,
388 height: THUMB_SIZE,
389 borderRadius: THUMB_SIZE / 2,
390 },
391 touchArea: {
392 position: 'absolute',
393 backgroundColor: 'transparent',
394 top: 0,
395 left: 0,
396 right: 0,
397 bottom: 0,
398 },
399 debugThumbTouchArea: {
400 position: 'absolute',
401 backgroundColor: 'green',
402 opacity: 0.5,
403 },
404});
405export { Slider };
406export default withTheme(Slider, 'Slider');