diff --git a/README.md b/README.md index 56902cb8..c46b18df 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ To use this library you need to ensure you are using the correct version of Reac | `testID` | Used to locate this view in UI automation tests. | string | | | `value` | Write-only property representing the value of the slider. Can be used to programmatically control the position of the thumb. Entered once at the beginning still acts as an initial value. Changing the value programmatically does not trigger any event.
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_, you don't need to update the value during dragging. | number | | | `tapToSeek` | Permits tapping on the slider track to set the thumb position.
Defaults to false on iOS. No effect on Android or Windows. | bool | iOS | +| `swipeToSeek` | Permits swiping on the slider track to set the thumb position.
Defaults to false on iOS. On Android this is the default behaviour. | bool | iOS | | `inverted` | Reverses the direction of the slider.
Default value is false. | bool | | | `vertical` | Changes the orientation of the slider to vertical, if set to `true`.
Default value is false. | bool | Windows | | `thumbTintColor` | Color of the foreground switch grip.
**NOTE:** This prop will override the `thumbImage` prop set, meaning that if both `thumbImage` and `thumbTintColor` will be set, image used for the thumb may not be displayed correctly! | [color](https://reactnative.dev/docs/colors) | Android | diff --git a/example/src/Examples.tsx b/example/src/Examples.tsx index 4724d4ee..8313edf2 100644 --- a/example/src/Examples.tsx +++ b/example/src/Examples.tsx @@ -564,6 +564,12 @@ export const examples: Props[] = [ return ; }, }, + { + title: 'step: 0.25, tap & swipe to seek on iOS', + render(): React.ReactElement { + return ; + }, + }, { title: 'Limit on positive values [30, 80]', render() { diff --git a/example/src/Props.tsx b/example/src/Props.tsx index 22498bd0..ff47d454 100644 --- a/example/src/Props.tsx +++ b/example/src/Props.tsx @@ -144,6 +144,12 @@ export const propsExamples: Props[] = [ return ; }, }, + { + title: 'swipeToSeek', + render(): React.ReactElement { + return ; + }, + }, { title: 'inverted', render() { diff --git a/package/ios/RNCSliderComponentView.mm b/package/ios/RNCSliderComponentView.mm index 81487b10..bcfbbd1d 100644 --- a/package/ios/RNCSliderComponentView.mm +++ b/package/ios/RNCSliderComponentView.mm @@ -24,6 +24,7 @@ @implementation RNCSliderComponentView RNCSlider *slider; UIImage *_image; BOOL _isSliding; + BOOL _swipeGestureEnabled; } + (ComponentDescriptorProvider)componentDescriptorProvider @@ -202,6 +203,9 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & if (oldScreenProps.tapToSeek != newScreenProps.tapToSeek) { slider.tapToSeek = newScreenProps.tapToSeek; } + if (oldScreenProps.swipeToSeek != newScreenProps.swipeToSeek) { + [self setSwipeToSeek:newScreenProps.swipeToSeek]; + } if (oldScreenProps.minimumValue != newScreenProps.minimumValue) { [slider setMinimumValue:newScreenProps.minimumValue]; } @@ -298,6 +302,78 @@ - (void)setInverted:(BOOL)inverted } } +#pragma mark - Swipe to seek + +- (void)setSwipeToSeek:(BOOL)swipeToSeek +{ + if (swipeToSeek && !_swipeGestureEnabled) { + UIPanGestureRecognizer *panGesturer; + panGesturer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panHandler:)]; + [slider addGestureRecognizer:panGesturer]; + _swipeGestureEnabled = YES; + } +} + +- (void)panHandler:(UIPanGestureRecognizer *)gesture { + CGPoint location = [gesture locationInView:slider]; + + switch (gesture.state) { + + case UIGestureRecognizerStateBegan: { + [self updateSliderToLocation:location]; + std::dynamic_pointer_cast(_eventEmitter) + ->onRNCSliderSlidingStart(RNCSliderEventEmitter::OnRNCSliderSlidingStart{.value = static_cast(slider.lastValue)}); + break; + } + + case UIGestureRecognizerStateChanged: { + [self updateSliderToLocation:location]; + break; + } + + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: + case UIGestureRecognizerStateFailed: { + std::dynamic_pointer_cast(_eventEmitter) + ->onRNCSliderSlidingComplete(RNCSliderEventEmitter::OnRNCSliderSlidingComplete{.value = static_cast(slider.value)}); + break; + } + + default: + break; + } +} + +- (void)updateSliderToLocation:(CGPoint)location { + float newValue = [self calculateSliderValueFromLocation:location]; + float discreteValue = [slider discreteValue:newValue]; + + [slider setValue:newValue animated:NO]; + + if (discreteValue != slider.lastValue) { + std::dynamic_pointer_cast(_eventEmitter) + ->onRNCSliderValueChange(RNCSliderEventEmitter::OnRNCSliderValueChange{.value = static_cast(slider.value)}); + } + + slider.lastValue = discreteValue; +} + +- (float)calculateSliderValueFromLocation:(CGPoint)point { + CGFloat sliderWidth = slider.bounds.size.width; + + if (sliderWidth <= 0) { + return slider.value; + } + + CGFloat percentage = point.x / sliderWidth; + percentage = MIN(1.0, MAX(0.0, percentage)); + + CGFloat range = slider.maximumValue - slider.minimumValue; + float newValue = slider.minimumValue + (percentage * range); + + return newValue; +} + @end Class RNCSliderCls(void) diff --git a/package/src/RNCSliderNativeComponent.ts b/package/src/RNCSliderNativeComponent.ts index 392dddae..a263ec3c 100644 --- a/package/src/RNCSliderNativeComponent.ts +++ b/package/src/RNCSliderNativeComponent.ts @@ -20,6 +20,7 @@ export interface NativeProps extends ViewProps { inverted?: WithDefault; vertical?: WithDefault; tapToSeek?: WithDefault; + swipeToSeek?: WithDefault; maximumTrackImage?: ImageSource; maximumTrackTintColor?: ColorValue; maximumValue?: Double; diff --git a/package/src/Slider.tsx b/package/src/Slider.tsx index 5ac1977d..ad344e83 100644 --- a/package/src/Slider.tsx +++ b/package/src/Slider.tsx @@ -62,6 +62,12 @@ type IOSProps = Readonly<{ * Defaults to false on iOS. No effect on Android or Windows. */ tapToSeek?: boolean; + + /** + * Permits swiping on the slider track to set the thumb position. + * Defaults to false on iOS. This is the default behaviour on Android. + */ + swipeToSeek?: boolean; }>; type Props = ViewProps & @@ -211,6 +217,7 @@ const SliderComponent = ( step = 0, inverted = false, tapToSeek = false, + swipeToSeek = false, lowerLimit = Platform.select({ web: minimumValue, default: constants.LIMIT_MIN_VALUE, @@ -310,6 +317,7 @@ const SliderComponent = ( step={step} inverted={inverted} tapToSeek={tapToSeek} + swipeToSeek={swipeToSeek} value={passedValue} lowerLimit={lowerLimit} upperLimit={upperLimit} diff --git a/package/typings/index.d.ts b/package/typings/index.d.ts index 0b548440..58147543 100644 --- a/package/typings/index.d.ts +++ b/package/typings/index.d.ts @@ -55,6 +55,12 @@ export interface SliderPropsIOS extends ReactNative.ViewProps { */ tapToSeek?: boolean; + /** + * Permits swiping on the slider track to set the thumb position. + * Defaults to false on iOS. This is the default behaviour on Android. + */ + swipeToSeek?: boolean; + /** * Sets an image for the thumb. Only static images are supported. */