Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br/>The value should be between minimumValue and maximumValue, which default to 0 and 1 respectively. Default value is 0.<br/>_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.<br/>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.<br/>Defaults to false on iOS. On Android this is the default behaviour. | bool | iOS |
| `inverted` | Reverses the direction of the slider.<br/>Default value is false. | bool | |
| `vertical` | Changes the orientation of the slider to vertical, if set to `true`.<br/>Default value is false. | bool | Windows |
| `thumbTintColor` | Color of the foreground switch grip.<br/>**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 |
Expand Down
6 changes: 6 additions & 0 deletions example/src/Examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,12 @@ export const examples: Props[] = [
return <SliderExample step={0.25} tapToSeek={true} />;
},
},
{
title: 'step: 0.25, tap & swipe to seek on iOS',
render(): React.ReactElement {
return <SliderExample step={0.25} tapToSeek={true} swipeToSeek={true} />;
},
},
{
title: 'Limit on positive values [30, 80]',
render() {
Expand Down
6 changes: 6 additions & 0 deletions example/src/Props.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ export const propsExamples: Props[] = [
return <SliderExample tapToSeek={true} />;
},
},
{
title: 'swipeToSeek',
render(): React.ReactElement {
return <SliderExample swipeToSeek={true} />;
},
},
{
title: 'inverted',
render() {
Expand Down
76 changes: 76 additions & 0 deletions package/ios/RNCSliderComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ @implementation RNCSliderComponentView
RNCSlider *slider;
UIImage *_image;
BOOL _isSliding;
BOOL _swipeGestureEnabled;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sterlingwes Can you elaborate on why we need this private variable?
From what I see it's only used once in the whole runtime to set the gesture recognizer. But is this required?
If we could make the swipeToSeek being true by default, we wouldn't need this additional var check.

}

+ (ComponentDescriptorProvider)componentDescriptorProvider
Expand Down Expand Up @@ -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];
}
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

case UIGestureRecognizerStateBegan: {
[self updateSliderToLocation:location];
std::dynamic_pointer_cast<const RNCSliderEventEmitter>(_eventEmitter)
->onRNCSliderSlidingStart(RNCSliderEventEmitter::OnRNCSliderSlidingStart{.value = static_cast<Float>(slider.lastValue)});
break;
}

case UIGestureRecognizerStateChanged: {
[self updateSliderToLocation:location];
break;
}

case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateFailed: {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we want to avoid sending an event if the state is failed?
What will be the value sent with this event? Will it still be updated despite being failed, or will we send duplicated position?
Let's handle the UIGestureRecognizerStateFailed separately, I'm fine having it doing nothing, unless you have some other ideas.

std::dynamic_pointer_cast<const RNCSliderEventEmitter>(_eventEmitter)
->onRNCSliderSlidingComplete(RNCSliderEventEmitter::OnRNCSliderSlidingComplete{.value = static_cast<Float>(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<const RNCSliderEventEmitter>(_eventEmitter)
->onRNCSliderValueChange(RNCSliderEventEmitter::OnRNCSliderValueChange{.value = static_cast<Float>(slider.value)});
}

slider.lastValue = discreteValue;
}

- (float)calculateSliderValueFromLocation:(CGPoint)point {
CGFloat sliderWidth = slider.bounds.size.width;

if (sliderWidth <= 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When such case is possible?

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<RCTComponentViewProtocol> RNCSliderCls(void)
Expand Down
1 change: 1 addition & 0 deletions package/src/RNCSliderNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface NativeProps extends ViewProps {
inverted?: WithDefault<boolean, false>;
vertical?: WithDefault<boolean, false>;
tapToSeek?: WithDefault<boolean, false>;
swipeToSeek?: WithDefault<boolean, false>;
maximumTrackImage?: ImageSource;
maximumTrackTintColor?: ColorValue;
maximumValue?: Double;
Expand Down
8 changes: 8 additions & 0 deletions package/src/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 &
Expand Down Expand Up @@ -211,6 +217,7 @@ const SliderComponent = (
step = 0,
inverted = false,
tapToSeek = false,
swipeToSeek = false,
lowerLimit = Platform.select({
web: minimumValue,
default: constants.LIMIT_MIN_VALUE,
Expand Down Expand Up @@ -310,6 +317,7 @@ const SliderComponent = (
step={step}
inverted={inverted}
tapToSeek={tapToSeek}
swipeToSeek={swipeToSeek}
value={passedValue}
lowerLimit={lowerLimit}
upperLimit={upperLimit}
Expand Down
6 changes: 6 additions & 0 deletions package/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down