diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..6fa8dec4 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.13.0 diff --git a/README.md b/README.md index 4d42aa5d..3b84c67a 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ To use this library you need to ensure you are using the correct version of Reac | `4.x.x` | `>=0.60`; `>=0.62` (on Windows); | | `3.1.x` | `>=0.60` | | `2.x.x` | `>= 0.60` | -| [`1.x.x`](https://github.com/react-native-community/react-native-slider/tree/937f0942f1fffc6ed88b5cf7c88d73b7878f00f0) | `<= 0.59` | +| [`1.x.x`](https://github.com/callstack/react-native-slider/tree/937f0942f1fffc6ed88b5cf7c88d73b7878f00f0) | `<= 0.59` | ## Properties @@ -93,14 +93,14 @@ To use this library you need to ensure you are using the correct version of Reac | `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 | | `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 | +| `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, iOS, Web | | `thumbSize` | Sets the size (width and height) of the thumb.
If `thumbImage` is provided, it will be scaled to this size.
Units: points on iOS, dp on Android. | number | Android, iOS, Web | -| `maximumTrackImage` | Assigns a maximum track image. Only static images are supported. The leftmost pixel of the image will be stretched to fill the track. | Image
.propTypes
.source | iOS | -| `minimumTrackImage` | Assigns a minimum track image. Only static images are supported. The rightmost pixel of the image will be stretched to fill the track. | Image
.propTypes
.source | iOS | -| `thumbImage` | Sets an image for the thumb. Only static images are supported. Needs to be a URI of a local or network image; base64-encoded SVG is not supported. | Image
.propTypes
.source | | -| `trackImage` | Assigns a single image for the track. Only static images are supported. The center pixel of the image will be stretched to fill the track. | Image
.propTypes
.source | iOS | -| [`StepMarker`](#stepmarker) | Component to be rendered for each step on the track,
with the possibility to change the styling, when thumb is at that given step | `FC` | iOS, Android, Windows | -| [`renderStepNumber`](#renderstepnumber) | Turns on the displaying of numbers of steps.
Numbers of steps are displayed under the track | bool | iOS, Android, Windows | +| `maximumTrackImage` | Assigns a maximum track image. Only static images are supported. The leftmost pixel of the image will be stretched to fill the track. | Image
.propTypes
.source | iOS, Web | +| `minimumTrackImage` | Assigns a minimum track image. Only static images are supported. The rightmost pixel of the image will be stretched to fill the track. | Image
.propTypes
.source | iOS, Web | +| `thumbImage` | Sets an image for the thumb. Only static images are supported. Needs to be a URI of a local or network image; base64-encoded SVG is not supported. | Image
.propTypes
.source | iOS, Web | +| `trackImage` | Assigns a single image for the track. Only static images are supported. The center pixel of the image will be stretched to fill the track. | Image
.propTypes
.source | iOS, Web | +| [`StepMarker`](#stepmarker) | Component to be rendered for each step on the track,
with the possibility to change the styling, when thumb is at that given step | `FC` | Android, iOS, Web | +| [`renderStepNumber`](#renderstepnumber) | Turns on the displaying of numbers of steps.
Numbers of steps are displayed under the track | bool | Android, iOS, Web | | `ref` | Reference object. | MutableRefObject | web | | `View` | [Inherited `View` props...](https://github.com/facebook/react-native-website/blob/master/docs/view.md#props) | | | @@ -236,7 +236,7 @@ You can also do this manually by: ## Contributors -This module was extracted from `react-native` core. Please, refer to [contributors graph](https://github.com/react-native-community/react-native-slider/graphs/contributors) for the complete list of contributors. +This module was extracted from `react-native` core. Please, refer to [contributors graph](https://github.com/callstack/react-native-slider/graphs/contributors) for the complete list of contributors. --- diff --git a/example-web/assets/slider-example-icon.png b/example-web/assets/slider-example-icon.png new file mode 100644 index 00000000..ddbed368 Binary files /dev/null and b/example-web/assets/slider-example-icon.png differ diff --git a/example-web/craco.config.js b/example-web/craco.config.js index 90051fbb..529b6c89 100644 --- a/example-web/craco.config.js +++ b/example-web/craco.config.js @@ -2,7 +2,7 @@ const path = require('path'); const babelInclude = require('@dealmore/craco-plugin-babel-include'); const webpack = require('webpack'); -const LIB_PATH = `../package/dist/Slider.js`; +const LIB_PATH = `../package/src`; module.exports = { webpack: { @@ -12,6 +12,19 @@ module.exports = { // make sure we don't include multiple versions of react 'react': path.resolve(__dirname, './node_modules/react'), }, + configure: webpackConfig => { + webpackConfig.resolve.extensions = [ + '.web.tsx', + '.web.ts', + '.web.js', + ...webpackConfig.resolve.extensions.filter( + extension => + !['.web.tsx', '.web.ts', '.web.js'].includes(extension), + ), + ]; + + return webpackConfig; + }, babel: { presets: [ '@babel/preset-react', diff --git a/example-web/src/App.tsx b/example-web/src/App.tsx index a096bec4..ae57897c 100644 --- a/example-web/src/App.tsx +++ b/example-web/src/App.tsx @@ -1,34 +1,40 @@ -import React from 'react'; +import React, {useState} from "react"; import { - Text, - View, - ScrollView, + Image, Platform, + Pressable, + ScrollView, StyleSheet, -// @ts-ignore -} from 'react-native'; + Text, + View, +} from "react-native"; +import Slider from "@react-native-community/slider"; -import {examples, Props as ExamplesTabProperties} from './Examples'; -import {propsExamples, Props as PropsTabProperties} from './Props'; +import {examples, type Props as ExamplesTabProperties} from "./Examples"; +import {propsExamples, type Props as PropsTabProperties} from "./Props"; + +const App = () => { + const [currentPage, setCurrentPage] = useState(0); + const titles = ["Examples", "Props"]; -function App() { const renderExampleTab = ( sliders: PropsTabProperties[] | ExamplesTabProperties[], filtered?: boolean, ) => { + const tabSliders = filtered + ? (sliders as ExamplesTabProperties[]).filter( + e => !e.platform || e.platform === Platform.OS, + ) + : sliders; + return ( - + - {(filtered - ? (sliders as ExamplesTabProperties[]).filter( - e => !e.platform || e.platform === Platform.OS, - ) - : sliders - ).map((e, i) => ( - - {e.title} + {tabSliders.map((e, i) => ( + + {e.title} {e.render()} ))} @@ -38,56 +44,182 @@ function App() { }; return ( -
-
- - {renderExampleTab(examples, true)} - {renderExampleTab(propsExamples)} + + + + + + Callstack + React Native Slider + -
-
+ + {titles.map((title, index) => { + const isActive = index === currentPage; + return ( + setCurrentPage(index)} + role="button" + style={[styles.tab, isActive && styles.activeTab]}> + + {title} + + + ); + })} + + + + {titles[currentPage]} + +
+ {currentPage === 0 + ? renderExampleTab(examples, true) + : renderExampleTab(propsExamples, true)} +
); -} +}; export default App; -const pageViewPositionSlider = { - trackColor: '#ABABAB', - thumbColor: '#1411AB', - style: { - width: '100%', - }, +const colors = { + accent: "#7C5CFF", + border: "#D9DEEA", + card: "#FFFFFF", + ink: "#201A3D", + page: "#F3F5FA", + track: "#51486F", }; const styles = StyleSheet.create({ - pagerViewContainer: { + homeScreenContainer: { flex: 1, + minHeight: "100vh" as never, + backgroundColor: colors.ink, }, - homeScreenContainer: { + header: { + backgroundColor: colors.ink, + paddingHorizontal: 20, + paddingTop: 18, + paddingBottom: 18, + }, + brandRow: { + alignItems: "center", + flexDirection: "row", + gap: 14, + marginBottom: 22, + }, + brandMark: { + width: 52, + height: 52, + borderRadius: 16, + }, + brandCopy: { + flex: 1, + }, + eyebrow: { + color: "#B9B2DF", + fontSize: 12, + fontWeight: "700", + letterSpacing: 0, + marginBottom: 2, + textTransform: "uppercase", + }, + appTitle: { + color: colors.card, + fontSize: 28, + fontWeight: "800", + letterSpacing: 0, + }, + tabBar: { + flexDirection: "row", + backgroundColor: "#332A61", + borderRadius: 8, + padding: 4, + }, + tab: { + alignItems: "center", + borderRadius: 6, flex: 1, + minHeight: 40, + justifyContent: "center", + }, + activeTab: { + backgroundColor: colors.card, + }, + tabText: { + color: "#D9D4F4", + fontSize: 14, + fontWeight: "700", + }, + activeTabText: { + color: colors.ink, + }, + pageIndicator: { + height: 32, + marginHorizontal: -4, + marginTop: 12, + }, + sectionTitle: { + color: colors.card, + fontSize: 18, + fontWeight: "700", + marginTop: 2, + }, + page: { + flex: 1, + backgroundColor: colors.page, }, scrollView: { - backgroundColor: '#F5FCFF', + backgroundColor: colors.page, }, container: { - justifyContent: 'center', - alignItems: 'center', - paddingVertical: 20, - }, - title: { - fontSize: 30, - color: pageViewPositionSlider.thumbColor, - textAlign: 'center', - width: '100%', - marginVertical: 20, - }, - instructions: { - textAlign: 'center', - color: '#333333', - marginBottom: 5, - fontSize: 20, - }, - sliderWidget: { - marginVertical: 30, + alignItems: "center", + paddingHorizontal: 20, + paddingTop: 18, + paddingBottom: 28, + }, + sliderCard: { + alignSelf: "center", + backgroundColor: colors.card, + borderColor: colors.border, + borderRadius: 8, + borderWidth: StyleSheet.hairlineWidth, + marginBottom: 14, + maxWidth: 460, + paddingHorizontal: 18, + paddingVertical: 18, + width: "100%", + boxShadow: "0 8px 18px rgba(16, 24, 40, 0.08)" as never, + }, + cardTitle: { + alignSelf: "stretch", + color: colors.ink, + flexShrink: 1, + flexWrap: "wrap", + fontSize: 16, + fontWeight: "700", + lineHeight: 22, + marginBottom: 12, + maxWidth: "100%", + textAlign: "center", + whiteSpace: "normal" as never, }, }); diff --git a/example-web/src/Examples.tsx b/example-web/src/Examples.tsx index b91f4307..3b186354 100644 --- a/example-web/src/Examples.tsx +++ b/example-web/src/Examples.tsx @@ -1,24 +1,30 @@ -import React, {useState} from 'react'; -// @ts-ignore -import {Text, View, StyleSheet, ScrollView} from 'react-native'; -// @ts-ignore -import Slider, {SliderProps} from '@react-native-community/slider'; +import React, {type FC, type JSX, useCallback, useState} from "react"; +import {Image, StyleSheet, Text, View} from "react-native"; +import Slider, {type MarkerProps, type SliderProps} from "@react-native-community/slider"; export interface Props { title: string; - render(): React.ReactElement; + render(): JSX.Element; platform?: string; } +const CONSTANTS = { + MAX_VALUE: 100, + MIN_VALUE: 10, + STEP: 10, +} as const; + const SliderExample = (props: SliderProps) => { - const [value, setValue] = useState(0); + const [value, setValue] = useState(props.value ?? 0); return ( - + {value && +value.toFixed(3)} @@ -32,7 +38,7 @@ const SlidingStartExample = (props: SliderProps) => { { + onSlidingStart={value => { setSlideStartingValue(value); setSlideStartingCount(prev => prev + 1); }} @@ -51,7 +57,7 @@ const SlidingCompleteExample = (props: SliderProps) => { { + onSlidingComplete={value => { setSlideCompletionValue(value); setSlideCompletionCount(prev => prev + 1); }} @@ -63,186 +69,682 @@ const SlidingCompleteExample = (props: SliderProps) => { ); }; +const SlidingStepsExample = (props: SliderProps) => { + const renderStepMarker = useCallback(({stepMarked}: MarkerProps) => { + return stepMarked ? ( + + + + ) : ( + + + + ); + }, []); + + return ( + + ); +}; + +const SlidingStepsNumbersExample = (props: SliderProps) => { + const renderStepMarker = useCallback(({stepMarked}: MarkerProps) => { + return stepMarked ? ( + + + + ) : ( + + + + ); + }, []); + + return ( + + ); +}; + +const SlidingStepsSmallNumbersExample = (props: SliderProps) => { + const renderStepMarker = useCallback(({stepMarked}: MarkerProps) => { + return stepMarked ? ( + + + + ) : ( + + + + ); + }, []); + + return ( + + ); +}; + +const SlidingCustomStepsThumbImageNumbersExample = (props: SliderProps) => { + const renderStepMarker = useCallback(({stepMarked}: MarkerProps) => { + return stepMarked ? ( + + + + ) : ( + + + + ); + }, []); + + return ( + + ); +}; + +const SlidingCustomStepsAnotherThumbImageNumbersExample = ( + props: SliderProps, +) => { + const renderStepMarker = useCallback(({stepMarked}: MarkerProps) => { + return stepMarked ? ( + + + + ) : ( + + + + ); + }, []); + + return ( + + ); +}; + +const InvertedSliderWithStepMarker = (props: SliderProps) => { + const renderStepMarker = useCallback(({stepMarked}: MarkerProps) => { + return stepMarked ? ( + + + + ) : ( + + + + ); + }, []); + + return ( + + ); +}; + +const SlidingCustomStepsThumbImageWithNumbersAndDifferentWidth = ( + props: SliderProps, +) => { + const renderStepMarker = useCallback(({stepMarked}: MarkerProps) => { + return stepMarked ? ( + + ) : ( + + ); + }, []); + + return ( + + ); +}; + +const MyStepMarker: FC = ({stepMarked, currentValue}) => { + return stepMarked ? ( + + + + {currentValue !== undefined ? ( + + {currentValue % 1 === 0 ? currentValue : currentValue.toFixed(2)} + + ) : ( + - + )} + + + + ) : ( + + ); +}; + +const CustomComponent: FC = ({ + stepMarked, + currentValue, + index, + max, +}) => { + if (stepMarked) { + return ( + + + {index} + + + {max} + + / + + ); + } + + return currentValue > index ? ( + + ) : ( + + ); +}; + +const SliderExampleWithCustomMarker = (props: SliderProps) => { + const [value, setValue] = useState(props.value ?? CONSTANTS.MIN_VALUE); + + return ( + + {value && +value.toFixed(3)} + + + ); +}; + +const SliderExampleWithCustomComponentAndFilledSteps = (props: SliderProps) => { + const [value, setValue] = useState(props.value || 50); + + return ( + + {value && +value.toFixed(3)} + + + ); +}; + export default SliderExample; const styles = StyleSheet.create({ + text: { + fontSize: 14, + textAlign: "center", + fontWeight: "500", + margin: 0, + }, + exampleContainer: { + alignItems: "center", + alignSelf: "stretch", + }, + trackText: { + color: "#FFFFFF", + fontSize: 10, + justifyContent: "center", + alignSelf: "center", + top: 12, + }, + trackDividerText: { + left: 18, + position: "absolute", + }, + trackDot: { + width: 10, + height: 10, + borderRadius: 10, + top: 4, + }, + empty: { + backgroundColor: "#B3BFC9", + }, + filled: { + backgroundColor: "#00629A", + }, + customComponentFrame: { + flex: 1, + flexDirection: "row", + top: -10, + opacity: 0.95, + }, + customComponentLeftFrame: { + height: 40, + width: 20, + borderTopLeftRadius: 40, + borderBottomLeftRadius: 40, + }, + customComponentRightFrame: { + height: 40, + width: 20, + borderTopRightRadius: 40, + borderBottomRightRadius: 40, + }, + divider: { + width: 2, + height: 20, + backgroundColor: "#ffffff", + justifyContent: "center", + alignItems: "center", + }, + separator: { + width: 2, + height: 20, + backgroundColor: "#00629A", + justifyContent: "center", + alignItems: "center", + }, + label: { + marginTop: 10, + width: 55, + paddingVertical: 5, + paddingHorizontal: 10, + backgroundColor: "#ffffff", + boxShadow: "0 1px 4px rgba(0, 0, 0, 0.4)" as never, + justifyContent: "center", + alignItems: "center", + }, + background: { + justifyContent: "center", + alignItems: "center", + }, + tinyLogo: { + marginVertical: 2, + aspectRatio: 1, + flex: 1, + height: "100%", + width: "100%", + }, slider: { - width: 300, + alignSelf: "stretch", opacity: 1, - height: 50, marginTop: 10, + height: 44, }, - text: { - fontSize: 14, - textAlign: 'center', - fontWeight: '500', - margin: 0, + narrowSlider: { + width: 200, + }, + outer: { + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: "#11FF11", + justifyContent: "center", + alignItems: "center", + }, + outerTrue: { + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: "#0F0FFF", + justifyContent: "center", + alignItems: "center", + }, + inner: { + width: 10, + height: 10, + borderRadius: 5, + backgroundColor: "#111111", + }, + innerTrue: { + width: 10, + height: 10, + borderRadius: 5, + backgroundColor: "#0F0FFF", + }, + offsetStepMarker: { + top: 3, + }, + container: { + alignItems: "center", + }, + customMarkerContainer: { + alignItems: "center", + paddingBottom: 80, + }, + outerSmall: { + width: 4, + height: 4, + top: 6, + borderRadius: 2, + backgroundColor: "#003366", + justifyContent: "center", + alignItems: "center", + }, + outerTrueSmall: { + width: 8, + height: 8, + borderRadius: 2, + backgroundColor: "#ABCDEF", + justifyContent: "center", + alignItems: "center", + }, + innerSmall: { + width: 7, + height: 7, + borderRadius: 1, + backgroundColor: "#223366", + }, + innerTrueSmall: { + width: 7, + height: 7, + borderRadius: 1, + backgroundColor: "#334488", }, }); export const examples: Props[] = [ { - title: 'Default settings', + title: "Default settings", render() { return ; }, }, { - title: 'Initial value: 0.5', + title: "Initial value: 0.5", render() { return ; }, }, { - title: 'minimumValue: -1, maximumValue: 2', - render(): React.ReactElement { + title: "minimumValue: -1, maximumValue: 2", + render() { return ; }, }, { - title: 'lowerLimit: 2, upperLimit: 7', - render(): React.ReactElement { - return ; + title: "step: 0.25, tap to seek on iOS", + render() { + return ; + }, + }, + { + title: "Limit on positive values [30, 80]", + render() { + return ( + + ); }, }, { - title: 'step: 0.25, tap to seek on iOS', - render(): React.ReactElement { - return ; + title: "Limit on negative values [-70, -20]", + render() { + return ( + + ); }, }, { - title: 'onSlidingStart', - render(): React.ReactElement { + title: "onSlidingStart", + render() { return ; }, }, { - title: 'onSlidingComplete', + title: "onSlidingComplete", render() { return ; }, }, { - title: 'Custom min/max track tint color', + title: "Custom min/max track tint color", render() { return ( ); }, }, { - title: 'Custom thumb tint color', + title: "Custom thumb tint color", render() { - return ; + return ; }, }, { - title: 'Custom thumb image', + title: "Custom thumb image", render() { - return ( - - ); + return ; }, }, { - title: 'Custom thumb (network image)', - platform: 'windows', + title: "Custom thumb (network image)", + platform: "windows", render() { return ( ); }, }, { - title: 'Custom track image', - platform: 'ios', + title: "Custom track image", render() { - return ; + return ; }, }, { - title: 'Custom min/max track image', - platform: 'ios', + title: "Custom min/max track image", render() { return ( ); }, }, { - title: 'Inverted slider direction', + title: "Slider with customized indicator and no numbers", + render() { + return ; + }, + }, + { + title: "Slider with customized indicator and default numbers", + render() { + return ; + }, + }, + { + title: "Slider with smaller customized indicator and default numbers", + render() { + return ; + }, + }, + { + title: "Slider with custom steps, thumbImage and steps numbers", + render() { + return ; + }, + }, + { + title: "Slider with custom steps, different thumbImage and steps numbers", + render() { + return ; + }, + }, + { + title: "Slider with custom steps, different width and thumbImage", + render() { + return ; + }, + }, + { + title: "Inverted slider direction with steps number and thumbImage", + render() { + return ; + }, + }, + { + title: "Custom step marker settings", + render() { + return ; + }, + }, + { + title: "Custom component with steps filled when passed", + render() { + return ; + }, + }, + { + title: "Inverted slider direction", render() { return ; }, }, { - title: 'Vertical slider', - platform: 'windows', + title: "Vertical slider", + platform: "windows", render() { return ; }, }, { - title: 'Disabled slider', + title: "Disabled slider", render() { return ; }, }, { - title: 'Slider with accessibilityState disabled', - platform: 'android', + title: "Slider with accessibilityState disabled", + platform: "android", render() { return ; }, }, { - title: 'Slider in horizontal scroll view', + title: "Custom thumb size (no image)", render() { - return ( - - - - - Scroll right, then slide ➔ - - - - ); + return ; }, }, - // Check the fix for the issue #743 { - title: 'With step numbers', + title: "Custom thumb size (scaled image)", render() { return ( ); }, diff --git a/example-web/src/Props.tsx b/example-web/src/Props.tsx index 715e8e40..bff0bdb5 100644 --- a/example-web/src/Props.tsx +++ b/example-web/src/Props.tsx @@ -1,20 +1,24 @@ -import React, {useState} from 'react'; -// @ts-ignore -import {Text, View, StyleSheet} from 'react-native'; -// @ts-ignore -import Slider, {SliderProps} from '@react-native-community/slider'; +import React, {type JSX, useState} from "react"; +import {StyleSheet, Text, View} from "react-native"; +import Slider, {type SliderProps} from "@react-native-community/slider"; export interface Props { title: string; - render(): React.ReactElement; + render(): JSX.Element; + platform?: string; } const SliderExample = (props: SliderProps) => { const [value, setValue] = useState(0); return ( - + {value && +value.toFixed(3)} - + ); }; @@ -26,7 +30,7 @@ const SlidingStartExample = (props: SliderProps) => { { + onSlidingStart={value => { setSlideStartingValue(value); setSlideStartingCount(prev => prev + 1); }} @@ -44,7 +48,7 @@ const SlidingCompleteExample = () => { return ( { + onSlidingComplete={value => { setSlideCompletionValue(value); setSlideCompletionCount(prev => prev + 1); }} @@ -59,143 +63,143 @@ const SlidingCompleteExample = () => { export default SliderExample; const styles = StyleSheet.create({ + exampleContainer: { + alignSelf: "stretch", + }, slider: { - width: 300, + alignSelf: "stretch", opacity: 1, height: 50, marginTop: 10, }, text: { fontSize: 14, - textAlign: 'center', - fontWeight: '500', + textAlign: "center", + fontWeight: "500", margin: 0, }, }); export const propsExamples: Props[] = [ { - title: 'Default settings', + title: "Default settings", render() { return ; }, }, { - title: 'disabled', + title: "disabled", render() { return ; }, }, { - title: 'maximumValue', + title: "maximumValue", render() { return ; }, }, { - title: 'minimumTrackTintColor', + title: "minimumTrackTintColor", render() { return ; }, }, { - title: 'minimumValue', + title: "minimumValue", render() { - return ; + return ; }, }, { - title: 'onSlidingStart', + title: "onSlidingStart", render() { return ; }, }, { - title: 'onSlidingComplete', + title: "onSlidingComplete", render() { return ; }, }, { - title: 'onValueChange', + title: "onValueChange", render() { return ; }, }, { - title: 'step', + title: "step", render() { return ; }, }, { - title: 'maximumTrackTintColor', + title: "maximumTrackTintColor", render() { return ; }, }, { - title: 'value', + title: "value", render() { return ; }, }, { - title: 'tapToSeek', - render(): React.ReactElement { - return ; + title: "tapToSeek", + render() { + return ; }, }, { - title: 'inverted', + title: "inverted", render() { return ; }, }, { - title: 'vertical', + title: "vertical", + platform: "windows", render() { return ; }, }, { - title: 'thumbTintColor', + title: "thumbTintColor", render() { - return ; + return ; }, }, { - title: 'maximumTrackImage', + title: "maximumTrackImage", render() { return ( ); }, }, { - title: 'minimumTrackImage', + title: "minimumTrackImage", render() { return ( - + ); }, }, { - title: 'thumbImage', + title: "thumbImage", render() { - return ( - - ); + return ; }, }, { - title: 'trackImage', + title: "trackImage", render() { - return ; + return ; }, }, ]; diff --git a/example-web/src/index.css b/example-web/src/index.css index ec2585e8..f32236ad 100644 --- a/example-web/src/index.css +++ b/example-web/src/index.css @@ -1,5 +1,6 @@ body { margin: 0; + min-height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; @@ -7,6 +8,15 @@ body { -moz-osx-font-smoothing: grayscale; } +html, +#root { + min-height: 100%; +} + +body { + background: #f3f5fa; +} + code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; diff --git a/example-web/src/react-native-shim.d.ts b/example-web/src/react-native-shim.d.ts new file mode 100644 index 00000000..0bd6e49a --- /dev/null +++ b/example-web/src/react-native-shim.d.ts @@ -0,0 +1,82 @@ +declare module "react-native" { + import type * as React from "react"; + + export type AccessibilityActionEvent = any; + export type ColorValue = string; + export type GestureResponderEvent = any; + export type HostComponent

= React.ComponentType

; + export type ImageSource = any; + export type ImageSourcePropType = any; + export type ImageStyle = Record; + export type LayoutChangeEvent = { + nativeEvent: { + layout: { + width: number; + height: number; + x: number; + y: number; + }; + }; + }; + export type NativeSyntheticEvent = {nativeEvent: T}; + export type StyleProp = T | T[] | null | undefined; + export type TextStyle = Record; + export type ViewProps = Record; + export type ViewStyle = Record; + + export const Image: any; + export const Platform: { + OS: string; + select(options: Record): T; + }; + export const Pressable: any; + export const ScrollView: any; + export const StyleSheet: any; + export const Text: any; + export const View: any; +} + +declare module "react-native/Libraries/Image/ImageSource" { + export type ImageSource = any; +} + +declare module "react-native/Libraries/Types/CodegenTypes" { + export type DirectEventHandler = (event: {nativeEvent: T}) => void; + export type Double = number; + export type Float = number; + export type WithDefault = T; +} + +declare module "react-native/Libraries/Utilities/codegenNativeComponent" { + import type {HostComponent} from "react-native"; + + export default function codegenNativeComponent

( + componentName: string, + ): HostComponent

; +} + +declare module "@react-native-community/slider" { + import type * as React from "react"; + import type {ViewProps} from "react-native"; + + export type MarkerProps = { + stepMarked: boolean; + currentValue: number; + index: number; + min: number; + max: number; + }; + + export type SliderProps = ViewProps & + Readonly<{ + [key: string]: any; + onSlidingComplete?: (value: number) => void; + onSlidingStart?: (value: number) => void; + onValueChange?: (value: number) => void; + StepMarker?: React.FC; + renderStepNumber?: boolean; + }>; + + const Slider: React.ForwardRefExoticComponent; + export default Slider; +} diff --git a/example-web/src/resources/ck-icon.png b/example-web/src/resources/ck-icon.png new file mode 100644 index 00000000..ee9e527f Binary files /dev/null and b/example-web/src/resources/ck-icon.png differ diff --git a/example-web/src/resources/empty.png b/example-web/src/resources/empty.png new file mode 100644 index 00000000..503d58bb Binary files /dev/null and b/example-web/src/resources/empty.png differ diff --git a/example-web/src/resources/twitter-small.png b/example-web/src/resources/twitter-small.png new file mode 100644 index 00000000..fb1f11f3 Binary files /dev/null and b/example-web/src/resources/twitter-small.png differ diff --git a/example/.bundle/config b/example/.bundle/config new file mode 100644 index 00000000..848943bb --- /dev/null +++ b/example/.bundle/config @@ -0,0 +1,2 @@ +BUNDLE_PATH: "vendor/bundle" +BUNDLE_FORCE_RUBY_PLATFORM: 1 diff --git a/example/.eslintrc.js b/example/.eslintrc.js index dcf0be08..187894b6 100644 --- a/example/.eslintrc.js +++ b/example/.eslintrc.js @@ -1,16 +1,4 @@ module.exports = { root: true, - extends: '@react-native-community', - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - overrides: [ - { - files: ['*.ts', '*.tsx'], - rules: { - '@typescript-eslint/no-shadow': ['error'], - 'no-shadow': 'off', - 'no-undef': 'off', - }, - }, - ], + extends: '@react-native', }; diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 00000000..de999559 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,75 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +**/.xcode.env.local + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ +*.keystore +!debug.keystore +.kotlin/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/screenshots +**/fastlane/test_output + +# Bundle artifact +*.jsbundle + +# Ruby / CocoaPods +**/Pods/ +/vendor/bundle/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + +# testing +/coverage + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/example/.prettierrc.js b/example/.prettierrc.js new file mode 100644 index 00000000..06860c8d --- /dev/null +++ b/example/.prettierrc.js @@ -0,0 +1,5 @@ +module.exports = { + arrowParens: 'avoid', + singleQuote: true, + trailingComma: 'all', +}; diff --git a/example/.prettierrc.json b/example/.prettierrc.json deleted file mode 100644 index fe440a6a..00000000 --- a/example/.prettierrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "arrowParens": "avoid", - "bracketSameLine": true, - "bracketSpacing": false, - "singleQuote": true, - "trailingComma": "all" -} diff --git a/example/.watchmanconfig b/example/.watchmanconfig new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/example/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/example/App.tsx b/example/App.tsx new file mode 100644 index 00000000..ece512e1 --- /dev/null +++ b/example/App.tsx @@ -0,0 +1 @@ +export {default} from "./src/App"; diff --git a/example/Gemfile b/example/Gemfile index 60770b18..51515233 100644 --- a/example/Gemfile +++ b/example/Gemfile @@ -8,9 +8,10 @@ gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' gem 'xcodeproj', '< 1.26.0' gem 'concurrent-ruby', '< 1.3.4' - + # Ruby 3.4.0 has removed some libraries from the standard library. gem 'bigdecimal' gem 'logger' gem 'benchmark' gem 'mutex_m' +gem 'nkf' diff --git a/example/Gemfile.lock b/example/Gemfile.lock index ebdbf97b..14e521c4 100644 --- a/example/Gemfile.lock +++ b/example/Gemfile.lock @@ -1,28 +1,21 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml - activesupport (7.1.3.4) - base64 - bigdecimal + CFPropertyList (3.0.9) + activesupport (6.1.7.10) concurrent-ruby (~> 1.0, >= 1.0.2) - connection_pool (>= 2.2.5) - drb i18n (>= 1.6, < 2) minitest (>= 5.1) - mutex_m tzinfo (~> 2.0) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + zeitwerk (~> 2.3) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) - base64 (0.2.0) - bigdecimal (3.1.1) + benchmark (0.5.0) + bigdecimal (4.1.2) claide (1.1.0) cocoapods (1.15.2) addressable (~> 2.8) @@ -62,56 +55,60 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.3.4) - connection_pool (2.4.1) - drb (2.1.0) - ruby2_keywords + concurrent-ruby (1.3.3) escape (0.0.4) - ethon (0.16.0) + ethon (0.18.0) ffi (>= 1.15.0) - ffi (1.17.0-arm64-darwin) - ffi (1.17.0-x86_64-linux-gnu) + logger + ffi (1.17.4) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - httpclient (2.8.3) - i18n (1.14.5) + httpclient (2.9.0) + mutex_m + i18n (1.14.8) concurrent-ruby (~> 1.0) - json (2.7.2) - minitest (5.25.1) + json (2.7.6) + logger (1.7.0) + minitest (5.25.4) molinillo (0.8.0) - mutex_m (0.1.1) + mutex_m (0.3.0) nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) - nkf (0.2.0) + nkf (0.3.0) public_suffix (4.0.7) - rexml (3.3.9) + rexml (3.4.4) ruby-macho (2.5.1) - ruby2_keywords (0.0.5) - typhoeus (1.4.1) - ethon (>= 0.9.0) + typhoeus (1.6.0) + ethon (>= 0.18.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - xcodeproj (1.25.0) + xcodeproj (1.25.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (>= 3.3.2, < 4.0) + rexml (>= 3.3.6, < 4.0) + zeitwerk (2.6.18) PLATFORMS - arm64-darwin - x86_64-linux + ruby DEPENDENCIES activesupport (>= 6.1.7.5, != 7.1.0) + benchmark + bigdecimal cocoapods (>= 1.13, != 1.15.1, != 1.15.0) + concurrent-ruby (< 1.3.4) + logger + mutex_m + nkf xcodeproj (< 1.26.0) RUBY VERSION - ruby 3.1.2p20 + ruby 2.6.10p210 BUNDLED WITH - 2.5.17 + 1.17.2 diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..3e2c3f85 --- /dev/null +++ b/example/README.md @@ -0,0 +1,97 @@ +This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). + +# Getting Started + +> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding. + +## Step 1: Start Metro + +First, you will need to run **Metro**, the JavaScript build tool for React Native. + +To start the Metro dev server, run the following command from the root of your React Native project: + +```sh +# Using npm +npm start + +# OR using Yarn +yarn start +``` + +## Step 2: Build and run your app + +With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app: + +### Android + +```sh +# Using npm +npm run android + +# OR using Yarn +yarn android +``` + +### iOS + +For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps). + +The first time you create a new project, run the Ruby bundler to install CocoaPods itself: + +```sh +bundle install +``` + +Then, and every time you update your native dependencies, run: + +```sh +bundle exec pod install +``` + +For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html). + +```sh +# Using npm +npm run ios + +# OR using Yarn +yarn ios +``` + +If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device. + +This is one way to run your app — you can also build it directly from Android Studio or Xcode. + +## Step 3: Modify your app + +Now that you have successfully run the app, let's make changes! + +Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh). + +When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload: + +- **Android**: Press the R key twice or select **"Reload"** from the **Dev Menu**, accessed via Ctrl + M (Windows/Linux) or Cmd ⌘ + M (macOS). +- **iOS**: Press R in iOS Simulator. + +## Congratulations! :tada: + +You've successfully run and modified your React Native App. :partying_face: + +### Now what? + +- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). +- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started). + +# Troubleshooting + +If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. + +# Learn More + +To learn more about React Native, take a look at the following resources: + +- [React Native Website](https://reactnative.dev) - learn more about React Native. +- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment. +- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. +- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. +- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. diff --git a/example/__tests__/App.test.tsx b/example/__tests__/App.test.tsx new file mode 100644 index 00000000..b0446b41 --- /dev/null +++ b/example/__tests__/App.test.tsx @@ -0,0 +1,18 @@ +/** + * @format + */ + +import React from 'react'; +import ReactTestRenderer from 'react-test-renderer'; +import App from '../App'; + +jest.mock('react-native-pager-view', () => { + const {View} = require('react-native'); + return ({children}: {children: React.ReactNode}) => {children}; +}); + +test('renders correctly', async () => { + await ReactTestRenderer.act(() => { + ReactTestRenderer.create(); + }); +}); diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 004ae422..b5663008 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -8,20 +8,20 @@ apply plugin: "com.facebook.react" */ react { /* Folders */ - // The root of your project, i.e. where "package.json" lives. Default is '..' - // root = file("../") - // The folder where the react-native NPM package is. Default is ../node_modules/react-native - // reactNativeDir = file("../node_modules/react-native") - // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen - // codegenDir = file("../node_modules/@react-native/codegen") - // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js - // cliFile = file("../node_modules/react-native/cli.js") + // The root of your project, i.e. where "package.json" lives. Default is '../..' + // root = file("../../") + // The folder where the react-native NPM package is. Default is ../../node_modules/react-native + // reactNativeDir = file("../../node_modules/react-native") + // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen + // codegenDir = file("../../node_modules/@react-native/codegen") + // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js + // cliFile = file("../../node_modules/react-native/cli.js") /* Variants */ // The list of variants to that are debuggable. For those we're going to - // skip the bundling of the JS bundle and the assets. By default is just 'debug'. + // skip the bundling of the JS bundle and the assets. Default is "debug", "debugOptimized". // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. - // debuggableVariants = ["liteDebug", "prodDebug"] + // debuggableVariants = ["liteDebug", "liteDebugOptimized", "prodDebug", "prodDebugOptimized"] /* Bundling */ // A list containing the node command and its flags. Default is just 'node'. @@ -49,8 +49,9 @@ react { // // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" // hermesFlags = ["-O", "-output-source-map"] + /* Autolinking */ - autolinkLibrariesWithApp() + autolinkLibrariesWithApp() } /** diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index eb98c01a..00000000 --- a/example/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 4122f36a..fb78f397 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -8,7 +8,9 @@ android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + android:usesCleartextTraffic="${usesCleartextTraffic}" + android:supportsRtl="true"> = - PackageList(this).packages.apply { - // Packages that cannot be autolinked yet can be added manually here, for example: - // add(MyReactNativePackage()) - } - - override fun getJSMainModuleName(): String = "index" - - override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG - - override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED - override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED - } - - override val reactHost: ReactHost - get() = getDefaultReactHost(this.applicationContext, reactNativeHost) + override val reactHost: ReactHost by lazy { + getDefaultReactHost( + context = applicationContext, + packageList = + PackageList(this).packages.apply { + // Packages that cannot be autolinked yet can be added manually here, for example: + // add(MyReactNativePackage()) + }, + ) + } override fun onCreate() { super.onCreate() - SoLoader.init(this, OpenSourceMergedSoMapping) - if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { - // If you opted-in for the New Architecture, we load the native entry point for this app. - load() - } + loadReactNative(this) } } diff --git a/example/android/app/src/main/res/drawable/rn_edit_text_material.xml b/example/android/app/src/main/res/drawable/rn_edit_text_material.xml index 73b37e4d..5c25e728 100644 --- a/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +++ b/example/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -17,7 +17,8 @@ android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material" android:insetRight="@dimen/abc_edit_text_inset_horizontal_material" android:insetTop="@dimen/abc_edit_text_inset_top_material" - android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"> + android:insetBottom="@dimen/abc_edit_text_inset_bottom_material" + > NSAllowsArbitraryLoads NSAllowsLocalNetworking @@ -34,17 +35,24 @@ NSLocationWhenInUseUsageDescription + RCTNewArchEnabled + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities - armv7 + arm64 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown UIViewControllerBasedStatusBarAppearance diff --git a/example/ios/example/LaunchScreen.storyboard b/example/ios/example/LaunchScreen.storyboard index a2139fff..4d6c54ad 100644 --- a/example/ios/example/LaunchScreen.storyboard +++ b/example/ios/example/LaunchScreen.storyboard @@ -16,12 +16,12 @@ -

-/// Initializes the singleton application object. This is the first line of -/// authored code executed, and as such is the logical equivalent of main() or -/// WinMain(). -/// -App::App() noexcept -{ -#if BUNDLE - JavaScriptBundleFile(L"index.windows"); - InstanceSettings().UseFastRefresh(false); -#else - JavaScriptBundleFile(L"index"); - InstanceSettings().UseFastRefresh(true); -#endif - -#if _DEBUG - InstanceSettings().UseDirectDebugger(true); - InstanceSettings().UseDeveloperSupport(true); -#else - InstanceSettings().UseDirectDebugger(false); - InstanceSettings().UseDeveloperSupport(false); -#endif - - RegisterAutolinkedNativeModulePackages(PackageProviders()); // Includes any autolinked modules - - PackageProviders().Append(make()); // Includes all modules in this project - - InitializeComponent(); -} - -/// -/// Invoked when the application is launched normally by the end user. Other entry points -/// will be used such as when the application is launched to open a specific file. -/// -/// Details about the launch request and process. -void App::OnLaunched(activation::LaunchActivatedEventArgs const& e) -{ - super::OnLaunched(e); - - Frame rootFrame = Window::Current().Content().as(); - rootFrame.Navigate(xaml_typename(), box_value(e.Arguments())); -} - -/// -/// Invoked when the application is activated by some means other than normal launching. -/// -void App::OnActivated(Activation::IActivatedEventArgs const &e) { - auto preActivationContent = Window::Current().Content(); - super::OnActivated(e); - if (!preActivationContent && Window::Current()) { - Frame rootFrame = Window::Current().Content().as(); - rootFrame.Navigate(xaml_typename(), nullptr); - } -} - -/// -/// Invoked when application execution is being suspended. Application state is saved -/// without knowing whether the application will be terminated or resumed with the contents -/// of memory still intact. -/// -/// The source of the suspend request. -/// Details about the suspend request. -void App::OnSuspending([[maybe_unused]] IInspectable const& sender, [[maybe_unused]] SuspendingEventArgs const& e) -{ - // Save application state and stop any background activity -} - -/// -/// Invoked when Navigation to a certain page fails -/// -/// The Frame which failed navigation -/// Details about the navigation failure -void App::OnNavigationFailed(IInspectable const&, NavigationFailedEventArgs const& e) -{ - throw hresult_error(E_FAIL, hstring(L"Failed to load Page ") + e.SourcePageType().Name); -} - -} // namespace winrt::example::implementation diff --git a/example/windows/example/App.h b/example/windows/example/App.h deleted file mode 100644 index eb2d5a58..00000000 --- a/example/windows/example/App.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include "App.xaml.g.h" - -#include - -namespace activation = winrt::Windows::ApplicationModel::Activation; - -namespace winrt::example::implementation -{ - struct App : AppT - { - App() noexcept; - void OnLaunched(activation::LaunchActivatedEventArgs const&); - void OnActivated(Windows::ApplicationModel::Activation::IActivatedEventArgs const &e); - void OnSuspending(IInspectable const&, Windows::ApplicationModel::SuspendingEventArgs const&); - void OnNavigationFailed(IInspectable const&, xaml::Navigation::NavigationFailedEventArgs const&); - private: - using super = AppT; - }; -} // namespace winrt::example::implementation diff --git a/example/windows/example/App.idl b/example/windows/example/App.idl deleted file mode 100644 index ad6a721d..00000000 --- a/example/windows/example/App.idl +++ /dev/null @@ -1,3 +0,0 @@ -namespace example -{ -} diff --git a/example/windows/example/App.xaml b/example/windows/example/App.xaml deleted file mode 100644 index bda9267c..00000000 --- a/example/windows/example/App.xaml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/example/windows/example/Assets/LockScreenLogo.scale-200.png b/example/windows/example/Assets/LockScreenLogo.scale-200.png deleted file mode 100644 index 735f57ad..00000000 Binary files a/example/windows/example/Assets/LockScreenLogo.scale-200.png and /dev/null differ diff --git a/example/windows/example/Assets/SplashScreen.scale-200.png b/example/windows/example/Assets/SplashScreen.scale-200.png deleted file mode 100644 index 023e7f1f..00000000 Binary files a/example/windows/example/Assets/SplashScreen.scale-200.png and /dev/null differ diff --git a/example/windows/example/Assets/Square150x150Logo.scale-200.png b/example/windows/example/Assets/Square150x150Logo.scale-200.png deleted file mode 100644 index af49fec1..00000000 Binary files a/example/windows/example/Assets/Square150x150Logo.scale-200.png and /dev/null differ diff --git a/example/windows/example/Assets/Square44x44Logo.scale-200.png b/example/windows/example/Assets/Square44x44Logo.scale-200.png deleted file mode 100644 index ce342a2e..00000000 Binary files a/example/windows/example/Assets/Square44x44Logo.scale-200.png and /dev/null differ diff --git a/example/windows/example/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/example/windows/example/Assets/Square44x44Logo.targetsize-24_altform-unplated.png deleted file mode 100644 index f6c02ce9..00000000 Binary files a/example/windows/example/Assets/Square44x44Logo.targetsize-24_altform-unplated.png and /dev/null differ diff --git a/example/windows/example/Assets/StoreLogo.png b/example/windows/example/Assets/StoreLogo.png deleted file mode 100644 index 7385b56c..00000000 Binary files a/example/windows/example/Assets/StoreLogo.png and /dev/null differ diff --git a/example/windows/example/Assets/Wide310x150Logo.scale-200.png b/example/windows/example/Assets/Wide310x150Logo.scale-200.png deleted file mode 100644 index 288995b3..00000000 Binary files a/example/windows/example/Assets/Wide310x150Logo.scale-200.png and /dev/null differ diff --git a/example/windows/example/AutolinkedNativeModules.g.cpp b/example/windows/example/AutolinkedNativeModules.g.cpp deleted file mode 100644 index cefeac35..00000000 --- a/example/windows/example/AutolinkedNativeModules.g.cpp +++ /dev/null @@ -1,18 +0,0 @@ -// AutolinkedNativeModules.g.cpp contents generated by "npx @react-native-community/cli autolink-windows" -// clang-format off -#include "pch.h" -#include "AutolinkedNativeModules.g.h" - -// Includes from @react-native-community/slider -#include - -namespace winrt::Microsoft::ReactNative -{ - -void RegisterAutolinkedNativeModulePackages(winrt::Windows::Foundation::Collections::IVector const& packageProviders) -{ - // IReactPackageProviders from @react-native-community/slider - packageProviders.Append(winrt::SliderWindows::ReactPackageProvider()); -} - -} diff --git a/example/windows/example/AutolinkedNativeModules.g.h b/example/windows/example/AutolinkedNativeModules.g.h deleted file mode 100644 index 95343bbd..00000000 --- a/example/windows/example/AutolinkedNativeModules.g.h +++ /dev/null @@ -1,10 +0,0 @@ -// AutolinkedNativeModules.g.h contents generated by "npx @react-native-community/cli autolink-windows" -// clang-format off -#pragma once - -namespace winrt::Microsoft::ReactNative -{ - -void RegisterAutolinkedNativeModulePackages(winrt::Windows::Foundation::Collections::IVector const& packageProviders); - -} diff --git a/example/windows/example/AutolinkedNativeModules.g.props b/example/windows/example/AutolinkedNativeModules.g.props deleted file mode 100644 index d12701bf..00000000 --- a/example/windows/example/AutolinkedNativeModules.g.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/example/windows/example/AutolinkedNativeModules.g.targets b/example/windows/example/AutolinkedNativeModules.g.targets deleted file mode 100644 index ef4e058e..00000000 --- a/example/windows/example/AutolinkedNativeModules.g.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - {685a83ae-36bc-4e9d-bdc6-417ebf168463} - - - diff --git a/example/windows/example/MainPage.cpp b/example/windows/example/MainPage.cpp deleted file mode 100644 index b09666f2..00000000 --- a/example/windows/example/MainPage.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "pch.h" -#include "MainPage.h" -#if __has_include("MainPage.g.cpp") -#include "MainPage.g.cpp" -#endif - -#include "App.h" - -using namespace winrt; -using namespace xaml; - -namespace winrt::example::implementation -{ - MainPage::MainPage() - { - InitializeComponent(); - auto app = Application::Current().as(); - ReactRootView().ReactNativeHost(app->Host()); - } -} diff --git a/example/windows/example/MainPage.h b/example/windows/example/MainPage.h deleted file mode 100644 index 5ed917d5..00000000 --- a/example/windows/example/MainPage.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once -#include "MainPage.g.h" -#include - -namespace winrt::example::implementation -{ - struct MainPage : MainPageT - { - MainPage(); - }; -} - -namespace winrt::example::factory_implementation -{ - struct MainPage : MainPageT - { - }; -} - diff --git a/example/windows/example/MainPage.idl b/example/windows/example/MainPage.idl deleted file mode 100644 index ae088fea..00000000 --- a/example/windows/example/MainPage.idl +++ /dev/null @@ -1,10 +0,0 @@ -#include "NamespaceRedirect.h" - -namespace example -{ - [default_interface] - runtimeclass MainPage : XAML_NAMESPACE.Controls.Page - { - MainPage(); - } -} diff --git a/example/windows/example/MainPage.xaml b/example/windows/example/MainPage.xaml deleted file mode 100644 index b9794628..00000000 --- a/example/windows/example/MainPage.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/example/windows/example/Package.appxmanifest b/example/windows/example/Package.appxmanifest deleted file mode 100644 index 9df757b2..00000000 --- a/example/windows/example/Package.appxmanifest +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - example - Bartosz Klonowski - Assets\StoreLogo.png - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/example/windows/example/PropertySheet.props b/example/windows/example/PropertySheet.props deleted file mode 100644 index 85d927cd..00000000 --- a/example/windows/example/PropertySheet.props +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/example/windows/example/ReactPackageProvider.cpp b/example/windows/example/ReactPackageProvider.cpp deleted file mode 100644 index ceb889ff..00000000 --- a/example/windows/example/ReactPackageProvider.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "pch.h" -#include "ReactPackageProvider.h" -#include "NativeModules.h" - -using namespace winrt::Microsoft::ReactNative; - -namespace winrt::example::implementation -{ - -void ReactPackageProvider::CreatePackage(IReactPackageBuilder const &packageBuilder) noexcept -{ - AddAttributedModules(packageBuilder, true); -} - -} // namespace winrt::example::implementation diff --git a/example/windows/example/ReactPackageProvider.h b/example/windows/example/ReactPackageProvider.h deleted file mode 100644 index 73379511..00000000 --- a/example/windows/example/ReactPackageProvider.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include "winrt/Microsoft.ReactNative.h" - -namespace winrt::example::implementation -{ - struct ReactPackageProvider : winrt::implements - { - public: // IReactPackageProvider - void CreatePackage(winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) noexcept; - }; -} // namespace winrt::example::implementation - diff --git a/example/windows/example/example.vcxproj b/example/windows/example/example.vcxproj deleted file mode 100644 index 41f89b63..00000000 --- a/example/windows/example/example.vcxproj +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - true - true - true - {b18dbfa9-1510-4cc9-801d-e3da5168024e} - example - example - en-US - 17.0 - true - Windows Store - 10.0 - - - $([MSBuild]::GetDirectoryNameOfFileAbove($(SolutionDir), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ - - - - - - Debug - ARM64 - - - Debug - Win32 - - - Debug - x64 - - - Release - ARM64 - - - Release - Win32 - - - Release - x64 - - - - Application - Unicode - - - true - true - - - false - true - false - - - - - - - - - - - - - - - - Use - pch.h - $(IntDir)pch.pch - Level4 - %(AdditionalOptions) /bigobj - 4453;28204 - - - - - _DEBUG;%(PreprocessorDefinitions) - - - - - NDEBUG;%(PreprocessorDefinitions) - - - - - MainPage.xaml - Code - - - - - - App.xaml - - - - - Designer - - - - - Designer - - - - - - - - - - - - - - MainPage.xaml - Code - - - - - Create - - - App.xaml - - - - - - App.xaml - - - MainPage.xaml - Code - - - - - - false - - - - - Designer - - - - - - - - - - - This project references targets in your node_modules\react-native-windows folder that are missing. The missing file is {0}. - - - - - diff --git a/example/windows/example/example.vcxproj.filters b/example/windows/example/example.vcxproj.filters deleted file mode 100644 index ec2808a9..00000000 --- a/example/windows/example/example.vcxproj.filters +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - Assets - - - Assets - - - Assets - - - Assets - - - Assets - - - Assets - - - Assets - - - - - - - - {e48dc53e-40b1-40cb-970a-f89935452892} - - - - - - - - - - - - \ No newline at end of file diff --git a/example/windows/example/packages.lock.json b/example/windows/example/packages.lock.json deleted file mode 100644 index ef739131..00000000 --- a/example/windows/example/packages.lock.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "version": 1, - "dependencies": { - "native,Version=v0.0": { - "Microsoft.JavaScript.Hermes": { - "type": "Direct", - "requested": "[0.1.23, )", - "resolved": "0.1.23", - "contentHash": "cA9t1GjY4Yo0JD1AfA//e1lOwk48hLANfuX6GXrikmEBNZVr2TIX5ONJt5tqCnpZyLz6xGiPDgTfFNKbSfb21g==" - }, - "Microsoft.UI.Xaml": { - "type": "Direct", - "requested": "[2.8.0, )", - "resolved": "2.8.0", - "contentHash": "vxdHxTr63s5KVtNddMFpgvjBjUH50z7seq/5jLWmmSuf8poxg+sXrywkofUdE8ZstbpO9y3FL/IXXUcPYbeesA==", - "dependencies": { - "Microsoft.Web.WebView2": "1.0.1264.42" - } - }, - "Microsoft.Windows.CppWinRT": { - "type": "Direct", - "requested": "[2.0.230706.1, )", - "resolved": "2.0.230706.1", - "contentHash": "l0D7oCw/5X+xIKHqZTi62TtV+1qeSz7KVluNFdrJ9hXsst4ghvqQ/Yhura7JqRdZWBXAuDS0G0KwALptdoxweQ==" - }, - "boost": { - "type": "Transitive", - "resolved": "1.83.0", - "contentHash": "cy53VNMzysEMvhBixDe8ujPk67Fcj3v6FPHQnH91NYJNLHpc6jxa2xq9ruCaaJjE4M3YrGSHDi4uUSTGBWw6EQ==" - }, - "Microsoft.Web.WebView2": { - "type": "Transitive", - "resolved": "1.0.1264.42", - "contentHash": "7OBUTkzQ5VI/3gb0ufi5U4zjuCowAJwQg2li0zXXzqkM+S1kmOlivTy1R4jAW+gY5Vyg510M+qMAESCQUjrfgA==" - }, - "common": { - "type": "Project", - "dependencies": { - "boost": "[1.83.0, )" - } - }, - "fmt": { - "type": "Project" - }, - "folly": { - "type": "Project", - "dependencies": { - "boost": "[1.83.0, )", - "fmt": "[1.0.0, )" - } - }, - "microsoft.reactnative": { - "type": "Project", - "dependencies": { - "Common": "[1.0.0, )", - "Folly": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.1.23, )", - "Microsoft.UI.Xaml": "[2.8.0, )", - "ReactCommon": "[1.0.0, )", - "boost": "[1.83.0, )" - } - }, - "reactcommon": { - "type": "Project", - "dependencies": { - "Folly": "[1.0.0, )", - "boost": "[1.83.0, )" - } - }, - "sliderwindows": { - "type": "Project", - "dependencies": { - "Microsoft.ReactNative": "[1.0.0, )", - "Microsoft.UI.Xaml": "[2.8.0, )" - } - } - }, - "native,Version=v0.0/win10-arm": { - "Microsoft.Web.WebView2": { - "type": "Transitive", - "resolved": "1.0.1264.42", - "contentHash": "7OBUTkzQ5VI/3gb0ufi5U4zjuCowAJwQg2li0zXXzqkM+S1kmOlivTy1R4jAW+gY5Vyg510M+qMAESCQUjrfgA==" - } - }, - "native,Version=v0.0/win10-arm-aot": { - "Microsoft.Web.WebView2": { - "type": "Transitive", - "resolved": "1.0.1264.42", - "contentHash": "7OBUTkzQ5VI/3gb0ufi5U4zjuCowAJwQg2li0zXXzqkM+S1kmOlivTy1R4jAW+gY5Vyg510M+qMAESCQUjrfgA==" - } - }, - "native,Version=v0.0/win10-arm64-aot": { - "Microsoft.Web.WebView2": { - "type": "Transitive", - "resolved": "1.0.1264.42", - "contentHash": "7OBUTkzQ5VI/3gb0ufi5U4zjuCowAJwQg2li0zXXzqkM+S1kmOlivTy1R4jAW+gY5Vyg510M+qMAESCQUjrfgA==" - } - }, - "native,Version=v0.0/win10-x64": { - "Microsoft.Web.WebView2": { - "type": "Transitive", - "resolved": "1.0.1264.42", - "contentHash": "7OBUTkzQ5VI/3gb0ufi5U4zjuCowAJwQg2li0zXXzqkM+S1kmOlivTy1R4jAW+gY5Vyg510M+qMAESCQUjrfgA==" - } - }, - "native,Version=v0.0/win10-x64-aot": { - "Microsoft.Web.WebView2": { - "type": "Transitive", - "resolved": "1.0.1264.42", - "contentHash": "7OBUTkzQ5VI/3gb0ufi5U4zjuCowAJwQg2li0zXXzqkM+S1kmOlivTy1R4jAW+gY5Vyg510M+qMAESCQUjrfgA==" - } - }, - "native,Version=v0.0/win10-x86": { - "Microsoft.Web.WebView2": { - "type": "Transitive", - "resolved": "1.0.1264.42", - "contentHash": "7OBUTkzQ5VI/3gb0ufi5U4zjuCowAJwQg2li0zXXzqkM+S1kmOlivTy1R4jAW+gY5Vyg510M+qMAESCQUjrfgA==" - } - }, - "native,Version=v0.0/win10-x86-aot": { - "Microsoft.Web.WebView2": { - "type": "Transitive", - "resolved": "1.0.1264.42", - "contentHash": "7OBUTkzQ5VI/3gb0ufi5U4zjuCowAJwQg2li0zXXzqkM+S1kmOlivTy1R4jAW+gY5Vyg510M+qMAESCQUjrfgA==" - } - } - } -} \ No newline at end of file diff --git a/example/windows/example/pch.cpp b/example/windows/example/pch.cpp deleted file mode 100644 index e0d2ef1a..00000000 --- a/example/windows/example/pch.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "pch.h" diff --git a/example/windows/example/pch.h b/example/windows/example/pch.h deleted file mode 100644 index 54f954f4..00000000 --- a/example/windows/example/pch.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#define NOMINMAX - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -using namespace winrt::Windows::Foundation; diff --git a/package/__test__/Slider.test.tsx b/package/__test__/Slider.test.tsx deleted file mode 100644 index f2073126..00000000 --- a/package/__test__/Slider.test.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react'; -import {fireEvent, render} from '@testing-library/react-native'; -import Slider from '../src/Slider'; - -describe('Slider', () => { - it('Calls the given onValueChange when native event is emitted', () => { - const onValueChange = jest.fn(); - const {getByTestId} = render( - , - ); - const slider = getByTestId('slider'); - fireEvent(slider, 'change', {nativeEvent: {value: 2}}); - expect(onValueChange).toHaveBeenCalledWith(2); - }); - - it('Handles provided events when sliding starts is emitted', () => { - const onSlidingStart = jest.fn(); - const {getByTestId} = render( - , - ); - const slider = getByTestId('slider'); - - fireEvent(slider, 'onRNCSliderSlidingStart', {nativeEvent: {value: 2}}); - expect(onSlidingStart).toHaveBeenCalledWith(2); - }); - - it('Handles provided events when sliding end is emitted', () => { - const onSlidingComplete = jest.fn(); - const {getByTestId} = render( - , - ); - const slider = getByTestId('slider'); - - fireEvent(slider, 'onRNCSliderSlidingComplete', {nativeEvent: {value: 2}}); - expect(onSlidingComplete).toHaveBeenCalledWith(2); - }); - - it('Calls the accessibility handler when accessibility action is triggered', () => { - const mockedAccessibilityHandler = jest.fn(); - const {getByTestId} = render( - , - ); - const slider = getByTestId('slider'); - - fireEvent(slider, 'onRNCSliderAccessibilityAction', { - actionName: 'mocked-action', - }); - expect(mockedAccessibilityHandler).toHaveBeenCalledWith({ - actionName: 'mocked-action', - }); - }); - - it('Emitts a warning in the dev console if lower and upper limits are switched', () => { - const mockedWarn = jest.fn(); - console.warn = mockedWarn; - render(); - expect(mockedWarn).toHaveBeenCalled(); - }); - - it('Provides the onLayout with the measured width', () => { - const {getByTestId} = render(); - const slider = getByTestId('slider'); - fireEvent(slider, 'onLayout', {nativeEvent: {layout: {width: 200}}}); - expect(slider).toHaveStyle({width: 200}); - }); - - it('Prevents the gesture control from being released externally', () => { - const mockedRelease = jest.fn(); - jest.mock('../src/index', () => ({ - ...jest.requireActual('../src/index'), - onResponderRelease: mockedRelease, - })); - const {getByTestId} = render(); - fireEvent(getByTestId('slider'), 'onResponderTerminationRequest'); - expect(mockedRelease).not.toHaveBeenCalled(); - }); -}); diff --git a/package/__test__/components/StepNumber.test.tsx b/package/__test__/components/StepNumber.test.tsx deleted file mode 100644 index 92828c72..00000000 --- a/package/__test__/components/StepNumber.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import {render} from '@testing-library/react-native'; -import {StepNumber} from '../../src/components/StepNumber'; - -describe('StepNumber', () => { - it('Displays number of step according to given index', () => { - const {getByText} = render( - , - ); - expect(getByText('0')).toBeDefined(); - }); -}); diff --git a/package/__test__/components/StepsIndicator.test.tsx b/package/__test__/components/StepsIndicator.test.tsx deleted file mode 100644 index 5f49a93f..00000000 --- a/package/__test__/components/StepsIndicator.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import {render} from '@testing-library/react-native'; -import {StepsIndicator} from '../../src/components/StepsIndicator'; -import {constants} from '../../src/utils/constants'; -import {Platform} from 'react-native'; -import {styles} from '../../src/utils/styles'; - -const defaultOptions = [0, 1, 2, 3, 4, 5]; -const longerOptions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - -describe('StepsIndicator', () => { - it('Renders every step provided in options', () => { - const {getByText} = render( - , - ); - for (const step of defaultOptions) { - expect(getByText(step.toString())).toBeDefined(); - } - }); - - it('Applies the big font size to the lower number of steps', () => { - const {getByTestId} = render( - , - ); - expect(getByTestId('0th-step')).toHaveStyle({ - fontSize: constants.STEP_NUMBER_TEXT_FONT_BIG, - }); - }); - - it('Applies the small font size to the number of steps higher than 9', () => { - const {getByTestId} = render( - , - ); - expect(getByTestId('0th-step')).toHaveStyle({ - fontSize: constants.STEP_NUMBER_TEXT_FONT_SMALL, - }); - }); - - it('Applies platform-dependent styles for web', () => { - Platform.OS = 'web'; - const {getByTestId} = render( - , - ); - expect(getByTestId('StepsIndicator-Container')).toHaveStyle( - styles.stepsIndicator, - ); - }); - - it('Reverts given options when isLTR is set', () => { - const {getByTestId} = render( - , - ); - expect(getByTestId('0th-step')).toHaveTextContent('5'); - expect(getByTestId('2th-step')).toHaveTextContent('3'); - }); - - it('Does not display any step numbers if prop is not set', () => { - const {queryByTestId} = render( - , - ); - expect(queryByTestId('0th-step')).toBeFalsy(); - }); -}); diff --git a/package/__test__/components/TrackMark.test.tsx b/package/__test__/components/TrackMark.test.tsx deleted file mode 100644 index b2356879..00000000 --- a/package/__test__/components/TrackMark.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import {render} from '@testing-library/react-native'; -import {MarkerProps, SliderTrackMark} from '../../src/components/TrackMark'; -import {View} from 'react-native'; - -const MockedStepMarker = ({}: MarkerProps) => ( - -); - -const MockedThumbImage = 1; - -describe('TrackMark', () => { - it('Renders the StepMarker if custom component is given', () => { - const {getByTestId} = render( - , - ); - expect(getByTestId('mockedStepMarker')).toBeDefined(); - }); - - it('Renders the StepMarker with thumbImage if provided', () => { - const {getByTestId} = render( - , - ); - expect(getByTestId('sliderTrackMark-thumbImage')).toBeDefined(); - }); -}); diff --git a/package/__tests__/Slider.test.tsx b/package/__tests__/Slider.test.tsx new file mode 100644 index 00000000..77d6a76c --- /dev/null +++ b/package/__tests__/Slider.test.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import {fireEvent, render} from '@testing-library/react-native'; +import {Text} from 'react-native'; + +import Slider, {type MarkerProps} from '../src/Slider'; + +describe('Slider', () => { + it('maps public value props to native prop names', () => { + const {getByTestId} = render( + , + ); + + expect(getByTestId('slider')).toHaveProp('minValue', 2); + expect(getByTestId('slider')).toHaveProp('maxValue', 10); + expect(getByTestId('slider')).toHaveProp('lowerLimit', 3); + expect(getByTestId('slider')).toHaveProp('upperLimit', 9); + expect(getByTestId('slider')).toHaveProp('value', 4); + expect(getByTestId('slider')).toHaveProp('step', 2); + expect(getByTestId('slider')).toHaveProp('disabled', true); + expect(getByTestId('slider')).toHaveProp('inverted', true); + expect(getByTestId('slider')).toHaveProp('tapToSeek', true); + expect(getByTestId('slider')).toHaveProp('thumbTintColor', 'red'); + }); + + it('keeps minValue and maxValue as compatibility aliases', () => { + const {getByTestId} = render( + , + ); + + expect(getByTestId('slider')).toHaveProp('minValue', 5); + expect(getByTestId('slider')).toHaveProp('maxValue', 15); + }); + + it('forwards native value events with numeric values', () => { + const onValueChange = jest.fn(); + const onSlidingStart = jest.fn(); + const onSlidingComplete = jest.fn(); + const {getByTestId} = render( + , + ); + + fireEvent(getByTestId('slider'), 'onValueChange', { + nativeEvent: {value: 0.25}, + }); + fireEvent(getByTestId('slider'), 'onSlidingStart', { + nativeEvent: {value: 0.5}, + }); + fireEvent(getByTestId('slider'), 'onSlidingComplete', { + nativeEvent: {value: 0.75}, + }); + + expect(onValueChange).toHaveBeenCalledWith(0.25); + expect(onSlidingStart).toHaveBeenCalledWith(0.5); + expect(onSlidingComplete).toHaveBeenCalledWith(0.75); + }); + + it('passes accessibility actions through unchanged', () => { + const onAccessibilityAction = jest.fn(); + const event = {nativeEvent: {actionName: 'increment'}}; + const {getByTestId} = render( + , + ); + + fireEvent(getByTestId('slider'), 'onAccessibilityAction', event); + + expect(onAccessibilityAction).toHaveBeenCalledWith(event); + }); + + it('warns when lowerLimit is greater than upperLimit', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + render(); + + expect(warn).toHaveBeenCalledWith( + 'Invalid configuration: lower limit is supposed to be smaller than upper limit', + ); + + warn.mockRestore(); + }); + + it('renders step numbers and custom markers', () => { + const StepMarker = jest.fn(({index, stepMarked}: MarkerProps) => ( + {stepMarked ? 'selected' : 'idle'} + )); + + const {getByTestId} = render( + , + ); + + expect(getByTestId('StepsIndicator-Container')).toBeTruthy(); + expect(getByTestId('0th-step')).toHaveTextContent('0'); + expect(getByTestId('1th-step')).toHaveTextContent('1'); + expect(getByTestId('2th-step')).toHaveTextContent('2'); + expect(getByTestId('marker-1')).toHaveTextContent('selected'); + expect(getByTestId('slider')).toHaveProp('thumbTintColor', 'transparent'); + expect(StepMarker).toHaveBeenCalledWith( + expect.objectContaining({ + currentValue: 1, + index: 1, + max: 2, + min: 0, + stepMarked: true, + }), + undefined, + ); + }); +}); diff --git a/package/android/build.gradle b/package/android/build.gradle index 362ad4a5..5744ce1f 100644 --- a/package/android/build.gradle +++ b/package/android/build.gradle @@ -9,18 +9,28 @@ buildscript { classpath("com.android.tools.build:gradle:7.1.1") classpath("com.facebook.react:react-native-gradle-plugin") classpath("de.undercouch:gradle-download-task:5.0.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20") } } +plugins { + id 'org.jetbrains.kotlin.plugin.compose' version '2.1.20' +} + apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'com.facebook.react' def getExtOrDefault(name) { - return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativeSlider_' + name] + return rootProject.ext.has(name) + ? rootProject.ext.get(name) + : project.properties['ReactNativeSlider_' + name] } def getExtOrIntegerDefault(name) { - return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['ReactNativeSlider_' + name]).toInteger() + return rootProject.ext.has(name) + ? rootProject.ext.get(name) + : (project.properties['ReactNativeSlider_' + name]).toInteger() } android { @@ -30,9 +40,14 @@ android { def major = agpVersion[0].toInteger() def minor = agpVersion[1].toInteger() if ((major == 7 && minor >= 3) || major >= 8) { - namespace "com.reactnativecommunity.slider" + namespace "com.callstack.slider" buildFeatures { buildConfig true + compose true + } + } else { + buildFeatures { + compose true } } @@ -59,5 +74,15 @@ repositories { dependencies { //noinspection GradleDynamicVersion api 'com.facebook.react:react-native:+' -} + implementation 'org.jetbrains.kotlin:kotlin-stdlib' + implementation platform('androidx.compose:compose-bom:2025.02.00') + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0' + + implementation 'com.facebook.react:react-android' + implementation 'androidx.activity:activity-compose:1.8.2' +} diff --git a/package/android/gradle.properties b/package/android/gradle.properties index cdf09fb3..d7ade578 100644 --- a/package/android/gradle.properties +++ b/package/android/gradle.properties @@ -1,4 +1,4 @@ -ReactNativeSlider_compileSdkVersion=30 -ReactNativeSlider_buildToolsVersion=30.0.0 -ReactNativeSlider_targetSdkVersion=30 -ReactNativeSlider_minSdkVersion=21 +ReactNativeSlider_compileSdkVersion=36 +ReactNativeSlider_buildToolsVersion=36.0.0 +ReactNativeSlider_targetSdkVersion=36 +ReactNativeSlider_minSdkVersion=24 diff --git a/package/android/gradle/wrapper/gradle-wrapper.jar b/package/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index f6b961fd..00000000 Binary files a/package/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/package/android/gradle/wrapper/gradle-wrapper.properties b/package/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 862a5356..00000000 --- a/package/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Sat Feb 09 14:36:05 CET 2019 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip diff --git a/package/android/gradlew b/package/android/gradlew deleted file mode 100644 index cccdd3d5..00000000 --- a/package/android/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/package/android/gradlew.bat b/package/android/gradlew.bat deleted file mode 100644 index e95643d6..00000000 --- a/package/android/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/package/android/src/main/AndroidManifest.xml b/package/android/src/main/AndroidManifest.xml index f8c7bb80..a2f47b60 100644 --- a/package/android/src/main/AndroidManifest.xml +++ b/package/android/src/main/AndroidManifest.xml @@ -1,3 +1,2 @@ - - \ No newline at end of file + + diff --git a/package/android/src/main/java/com/callstack/slider/InlineComposeView.kt b/package/android/src/main/java/com/callstack/slider/InlineComposeView.kt new file mode 100644 index 00000000..f52c8615 --- /dev/null +++ b/package/android/src/main/java/com/callstack/slider/InlineComposeView.kt @@ -0,0 +1,114 @@ +package com.callstack.slider + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Recomposer +import androidx.compose.ui.platform.AbstractComposeView +import androidx.compose.ui.platform.AndroidUiDispatcher +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.compositionContext +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.EventDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch + +abstract class RNCustomRenderComposeView( + protected val reactContext: ThemedReactContext, +) : AbstractComposeView(reactContext), LifecycleOwner, SavedStateRegistryOwner { + + private val lifecycleRegistry = LifecycleRegistry(this) + private val savedStateRegistryController = SavedStateRegistryController.create(this) + + override val lifecycle: Lifecycle get() = lifecycleRegistry + override val savedStateRegistry: SavedStateRegistry + get() = savedStateRegistryController.savedStateRegistry + + private var recomposerScope: CoroutineScope? = null + + var eventDispatcher: EventDispatcher? = null + + @Composable + abstract fun ComposeContent() + + @Composable + override fun Content() { + ComposeContent() + } + + init { + savedStateRegistryController.performRestore(null) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnLifecycleDestroyed(lifecycle), + ) + ensureRecomposer() + } + + override fun onAttachedToWindow() { + ensureRecomposer() + resumeLifecycle() + super.onAttachedToWindow() + } + + override fun onDetachedFromWindow() { + pauseLifecycle() + super.onDetachedFromWindow() + } + + open fun onDropInstance() { + eventDispatcher = null + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + recomposerScope?.cancel() + recomposerScope = null + } + + protected fun dispatchEvent(event: Event<*>) { + val dispatcher = eventDispatcher ?: return + post { + dispatcher.dispatchEvent(event) + } + } + + protected fun getSurfaceId(): Int = UIManagerHelper.getSurfaceId(this) + + private fun ensureRecomposer() { + if (recomposerScope != null) return + + setViewTreeLifecycleOwner(this) + setViewTreeSavedStateRegistryOwner(this) + + val scope = CoroutineScope(AndroidUiDispatcher.CurrentThread) + recomposerScope = scope + val recomposer = Recomposer(scope.coroutineContext) + scope.launch { recomposer.runRecomposeAndApplyChanges() } + compositionContext = recomposer + } + + private fun resumeLifecycle() { + when (lifecycleRegistry.currentState) { + Lifecycle.State.CREATED -> { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + } + Lifecycle.State.STARTED -> + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + else -> Unit + } + } + + private fun pauseLifecycle() { + if (lifecycleRegistry.currentState == Lifecycle.State.DESTROYED) return + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + } +} diff --git a/package/android/src/main/java/com/callstack/slider/ReactSliderPackage.kt b/package/android/src/main/java/com/callstack/slider/ReactSliderPackage.kt new file mode 100644 index 00000000..16906aaa --- /dev/null +++ b/package/android/src/main/java/com/callstack/slider/ReactSliderPackage.kt @@ -0,0 +1,15 @@ +package com.callstack.slider + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +class SliderPackage : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List = + emptyList() + + override fun createViewManagers( + reactContext: ReactApplicationContext, + ): List> = listOf(SliderViewManager()) +} diff --git a/package/android/src/main/java/com/callstack/slider/SliderEvents.kt b/package/android/src/main/java/com/callstack/slider/SliderEvents.kt new file mode 100644 index 00000000..f21edd58 --- /dev/null +++ b/package/android/src/main/java/com/callstack/slider/SliderEvents.kt @@ -0,0 +1,50 @@ +package com.callstack.slider + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event + +class ValueChangedEvent(surfaceId: Int, viewId: Int, private val value: Float) : + Event(surfaceId, viewId) { + + override fun getEventName(): String = EVENT_NAME + + override fun getEventData(): WritableMap = + Arguments.createMap().apply { + putDouble("value", value.toDouble()) + } + + companion object { + const val EVENT_NAME = "topValueChange" + } +} + +class SlidingStartEvent(surfaceId: Int, viewId: Int, private val value: Float) : + Event(surfaceId, viewId) { + + override fun getEventName(): String = EVENT_NAME + + override fun getEventData(): WritableMap = + Arguments.createMap().apply { + putDouble("value", value.toDouble()) + } + + companion object { + const val EVENT_NAME = "topSlidingStart" + } +} + +class SlidingCompleteEvent(surfaceId: Int, viewId: Int, private val value: Float) : + Event(surfaceId, viewId) { + + override fun getEventName(): String = EVENT_NAME + + override fun getEventData(): WritableMap = + Arguments.createMap().apply { + putDouble("value", value.toDouble()) + } + + companion object { + const val EVENT_NAME = "topSlidingComplete" + } +} diff --git a/package/android/src/main/java/com/callstack/slider/SliderView.kt b/package/android/src/main/java/com/callstack/slider/SliderView.kt new file mode 100644 index 00000000..8b669fb3 --- /dev/null +++ b/package/android/src/main/java/com/callstack/slider/SliderView.kt @@ -0,0 +1,166 @@ +package com.callstack.slider + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.facebook.react.uimanager.ThemedReactContext +import kotlin.math.max +import kotlin.math.min + +@OptIn(ExperimentalMaterial3Api::class) +internal class SliderView(reactContext: ThemedReactContext) : + RNCustomRenderComposeView(reactContext) { + + private val minValue = mutableStateOf(0f) + private val maxValue = mutableStateOf(1f) + private val steps = mutableStateOf(0) + private val sliderValue = mutableStateOf(0f) + private val lowerLimit = mutableStateOf(Float.NEGATIVE_INFINITY) + private val upperLimit = mutableStateOf(Float.POSITIVE_INFINITY) + private val disabled = mutableStateOf(false) + private val inverted = mutableStateOf(false) + private val minimumTrackTintColor = mutableStateOf(null) + private val maximumTrackTintColor = mutableStateOf(null) + private val thumbTintColor = mutableStateOf(null) + private val thumbSize = mutableStateOf(0f) + private var isSliding = false + + fun setMinValue(value: Float) { + minValue.value = value + normalizeRange() + clampSliderValue() + } + + fun setMaxValue(value: Float) { + maxValue.value = value + normalizeRange() + clampSliderValue() + } + + fun setStep(value: Double) { + steps.value = max(value.toInt(), 0) + } + + fun setValue(value: Float) { + sliderValue.value = clampedValue(value) + } + + fun setLowerLimit(value: Float) { + lowerLimit.value = value + clampSliderValue() + } + + fun setUpperLimit(value: Float) { + upperLimit.value = value + clampSliderValue() + } + + fun setDisabled(value: Boolean) { + disabled.value = value + } + + fun setInverted(value: Boolean) { + inverted.value = value + } + + fun setMinimumTrackTintColor(value: Int?) { + minimumTrackTintColor.value = value + } + + fun setMaximumTrackTintColor(value: Int?) { + maximumTrackTintColor.value = value + } + + fun setThumbTintColor(value: Int?) { + thumbTintColor.value = value + } + + fun setThumbSize(value: Float) { + thumbSize.value = max(value, 0f) + } + + @Composable + override fun ComposeContent() { + val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + val layoutDirection = + if (inverted.value) { + LayoutDirection.Rtl + } else { + LayoutDirection.Ltr + } + val sliderColors = SliderDefaults.colors( + thumbColor = thumbTintColor.value?.let { Color(it) } ?: Color.Unspecified, + activeTrackColor = minimumTrackTintColor.value?.let { Color(it) } ?: Color.Unspecified, + inactiveTrackColor = maximumTrackTintColor.value?.let { Color(it) } ?: Color.Unspecified, + ) + + CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) { + Slider( + value = sliderValue.value, + onValueChange = { + if (!isSliding) { + isSliding = true + dispatchEvent(SlidingStartEvent(getSurfaceId(), id, sliderValue.value)) + } + + val nextValue = clampedValue(it) + sliderValue.value = nextValue + dispatchEvent(ValueChangedEvent(getSurfaceId(), id, nextValue)) + }, + onValueChangeFinished = { + if (isSliding) { + isSliding = false + dispatchEvent(SlidingCompleteEvent(getSurfaceId(), id, sliderValue.value)) + } + }, + steps = steps.value, + valueRange = minValue.value..maxValue.value, + enabled = !disabled.value, + interactionSource = interactionSource, + colors = sliderColors, + thumb = { + if (thumbSize.value > 0f) { + SliderDefaults.Thumb( + interactionSource = interactionSource, + colors = sliderColors, + enabled = !disabled.value, + thumbSize = DpSize(thumbSize.value.dp, thumbSize.value.dp), + ) + } else { + SliderDefaults.Thumb( + interactionSource = interactionSource, + colors = sliderColors, + enabled = !disabled.value, + ) + } + }, + ) + } + } + + private fun normalizeRange() { + if (maxValue.value < minValue.value) { + maxValue.value = minValue.value + } + } + + private fun clampSliderValue() { + sliderValue.value = clampedValue(sliderValue.value) + } + + private fun clampedValue(value: Float): Float { + val lower = max(minValue.value, lowerLimit.value) + val upper = min(maxValue.value, upperLimit.value) + return min(max(value, lower), max(lower, upper)) + } +} diff --git a/package/android/src/main/java/com/callstack/slider/SliderViewManager.kt b/package/android/src/main/java/com/callstack/slider/SliderViewManager.kt new file mode 100644 index 00000000..0b23485f --- /dev/null +++ b/package/android/src/main/java/com/callstack/slider/SliderViewManager.kt @@ -0,0 +1,103 @@ +package com.callstack.slider + +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.SimpleViewManager +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.viewmanagers.SliderViewManagerDelegate +import com.facebook.react.viewmanagers.SliderViewManagerInterface + +@ReactModule(name = SliderViewManager.NAME) +internal class SliderViewManager : + SimpleViewManager(), + SliderViewManagerInterface { + + private val delegate: ViewManagerDelegate = + SliderViewManagerDelegate(this) + + override fun getName(): String = NAME + + override fun createViewInstance(context: ThemedReactContext): SliderView = SliderView(context) + + override fun getDelegate(): ViewManagerDelegate = delegate + + override fun onDropViewInstance(view: SliderView) { + super.onDropViewInstance(view) + view.onDropInstance() + } + + override fun setStep(view: SliderView, value: Double) { + view.setStep(value) + } + + override fun setMinValue(view: SliderView, value: Double) { + view.setMinValue(value.toFloat()) + } + + override fun setMaxValue(view: SliderView, value: Double) { + view.setMaxValue(value.toFloat()) + } + + override fun setValue(view: SliderView, value: Float) { + view.setValue(value) + } + + override fun setLowerLimit(view: SliderView, value: Float) { + view.setLowerLimit(value) + } + + override fun setUpperLimit(view: SliderView, value: Float) { + view.setUpperLimit(value) + } + + override fun setDisabled(view: SliderView, value: Boolean) { + view.setDisabled(value) + } + + override fun setInverted(view: SliderView, value: Boolean) { + view.setInverted(value) + } + + override fun setTapToSeek(view: SliderView, value: Boolean) = Unit + + override fun setMinimumTrackTintColor(view: SliderView, value: Int?) { + view.setMinimumTrackTintColor(value) + } + + override fun setMaximumTrackTintColor(view: SliderView, value: Int?) { + view.setMaximumTrackTintColor(value) + } + + override fun setThumbTintColor(view: SliderView, value: Int?) { + view.setThumbTintColor(value) + } + + override fun setThumbSize(view: SliderView, value: Float) { + view.setThumbSize(value) + } + + override fun setVertical(view: SliderView, value: Boolean) = Unit + + override fun setAccessibilityUnits(view: SliderView, value: String?) = Unit + + override fun setAccessibilityIncrements(view: SliderView, value: ReadableArray?) = Unit + + override fun setMaximumTrackImage(view: SliderView, value: ReadableMap?) = Unit + + override fun setMinimumTrackImage(view: SliderView, value: ReadableMap?) = Unit + + override fun setThumbImage(view: SliderView, value: ReadableMap?) = Unit + + override fun setTrackImage(view: SliderView, value: ReadableMap?) = Unit + + override fun addEventEmitters(reactContext: ThemedReactContext, view: SliderView) { + view.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id) + } + + companion object { + const val NAME = "SliderView" + } +} diff --git a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSlider.java b/package/android/src/main/java/com/reactnativecommunity/slider/ReactSlider.java deleted file mode 100644 index 19808941..00000000 --- a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSlider.java +++ /dev/null @@ -1,398 +0,0 @@ -package com.reactnativecommunity.slider; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.drawable.BitmapDrawable; -import android.os.Build; -import android.util.Log; -import android.util.AttributeSet; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import androidx.appcompat.widget.AppCompatSeekBar; - -import java.net.URL; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import javax.annotation.Nullable; -import com.facebook.react.modules.i18nmanager.I18nUtil; -/** - * Slider that behaves more like the iOS one, for consistency. - * - *

On iOS, the value is 0..1. Android SeekBar only supports integer values. For consistency, we - * pretend in JS that the value is 0..1 but set the SeekBar value to 0..100. - * - *

Note that the slider is _not_ a controlled component (setValue isn't called during dragging). - */ -public class ReactSlider extends AppCompatSeekBar { - - /** - * If step is 0 (unset) we default to this total number of steps. Don't use 100 which leads to - * rounding errors (0.200000000001). - */ - private static int DEFAULT_TOTAL_STEPS = 128; - - /** - * We want custom min..max range. Android only supports 0..max range so we implement this - * ourselves. - */ - private double mMinValue = 0; - - private double mMaxValue = 0; - - /** - * Value sent from JS (setState). Doesn't get updated during drag (slider is not a controlled - * component). - */ - private double mValue = 0; - - private boolean isSliding = false; - - /** If zero it's determined automatically. */ - private double mStep = 0; - - private double mStepCalculated = 0; - - private String mAccessibilityUnits; - - private List mAccessibilityIncrements; - - /** Real limit value based on min and max values. This comes from props */ - private double mRealLowerLimit = Long.MIN_VALUE; - - /** Lower limit based on the SeekBar progress 0..total steps */ - private int mLowerLimit; - - /** Real limit value based on min and max values. This comes from props */ - private double mRealUpperLimit = Long.MAX_VALUE; - - /** Upper limit based on the SeekBar progress 0..total steps */ - private int mUpperLimit; - - /** Thumb size in pixels (0 = default) */ - private int mThumbSizePx = 0; - - /** Original thumb drawable URI */ - @Nullable private String mThumbImageUri = null; - - /** Cached thumb tint color */ - @Nullable private Integer mThumbTintColor = null; - - public ReactSlider(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - I18nUtil sharedI18nUtilInstance = I18nUtil.getInstance(); - super.setLayoutDirection(sharedI18nUtilInstance.isRTL(context) ? LAYOUT_DIRECTION_RTL : LAYOUT_DIRECTION_LTR); - disableStateListAnimatorIfNeeded(); - } - - private void disableStateListAnimatorIfNeeded() { - // We disable the state list animator for Android 6 and 7; this is a hack to prevent T37452851 - // and https://github.com/facebook/react-native/issues/9979 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - super.setStateListAnimator(null); - } - } - - /* package */ void setMaxValue(double max) { - mMaxValue = max; - updateAll(); - } - - /* package */ void setMinValue(double min) { - mMinValue = min; - updateAll(); - } - - /*package*/ int getValidProgressValue(int progress) { - if (progress < getLowerLimit()) { - progress = getLowerLimit(); - } else if (progress > getUpperLimit()) { - progress = getUpperLimit(); - } - return progress; - } - - /* package */ void setValue(double value) { - mValue = value; - updateValue(); - } - - /* package */ void setStep(double step) { - mStep = step; - updateAll(); - } - - /* package */ void setLowerLimit(double value) { - mRealLowerLimit = value; - updateLowerLimit(); - } - - /* package */ void setUpperLimit(double value) { - mRealUpperLimit = value; - updateUpperLimit(); - } - - int getLowerLimit() { - return this.mLowerLimit; - } - - int getUpperLimit() { - return this.mUpperLimit; - } - - boolean isSliding() { - return isSliding; - } - - void isSliding(boolean isSliding) { - this.isSliding = isSliding; - } - - void setAccessibilityUnits(String accessibilityUnits) { - mAccessibilityUnits = accessibilityUnits; - } - - void setAccessibilityIncrements(List accessibilityIncrements) { - mAccessibilityIncrements = accessibilityIncrements; - } - - @Override - public void onPopulateAccessibilityEvent(AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(event); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED || - (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED && this.isAccessibilityFocused())) { - this.setupAccessibility((int)mValue); - } - } - } - - @Override - public void announceForAccessibility(CharSequence text) { - Context ctx = this.getContext(); - final AccessibilityManager manager = (AccessibilityManager) ctx.getSystemService(Context.ACCESSIBILITY_SERVICE); - - if (manager.isEnabled()) { - final AccessibilityEvent e = AccessibilityEvent.obtain(); - e.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); - e.setClassName(this.getClass().getName()); - e.setPackageName(ctx.getPackageName()); - e.getText().add(text); - - TimerTask task = new TimerTask() { - @Override - public void run() { - manager.sendAccessibilityEvent(e); - } - }; - - Timer timer = new Timer(); - timer.schedule(task, 1000); - } - } - - public void setupAccessibility(int index) { - if (mAccessibilityUnits != null && mAccessibilityIncrements != null && mAccessibilityIncrements.size() - 1 == (int)mMaxValue) { - String sliderValue = mAccessibilityIncrements.get(index); - int stringLength = mAccessibilityUnits.length(); - - String spokenUnits = mAccessibilityUnits; - if (sliderValue != null && Integer.parseInt(sliderValue) == 1) { - spokenUnits = spokenUnits.substring(0, stringLength - 1); - } - - this.announceForAccessibility(String.format("%s %s", sliderValue, spokenUnits)); - } - } - - - - /** - * Convert SeekBar's native progress value (e.g. 0..100) to a value passed to JS (e.g. -1.0..2.5). - */ - public double toRealProgress(int seekBarProgress) { - if (seekBarProgress == getMax()) { - return mMaxValue; - } - return seekBarProgress * getStepValue() + mMinValue; - } - - /** Update underlying native SeekBar's values. */ - private void updateAll() { - if (mStep == 0) { - mStepCalculated = (mMaxValue - mMinValue) / (double) DEFAULT_TOTAL_STEPS; - } - setMax(getTotalSteps()); - setKeyProgressIncrement(1); - updateLowerLimit(); - updateUpperLimit(); - updateValue(); - } - - /** Update limit based on props limit, max and min - * Fallback to upper limit if invalid configuration provided - */ - private void updateLowerLimit() { - double limit = Math.max(mRealLowerLimit, mMinValue); - int lowerLimit = (int) Math.round((limit - mMinValue) / (mMaxValue - mMinValue) * getTotalSteps()); - if(lowerLimit > mUpperLimit) { - Log.d("Invalid configuration", "upperLimit < lowerLimit; lowerLimit not set"); - }else { - mLowerLimit = Math.min(lowerLimit, mUpperLimit); - } - } - - /** Update limit based on props limit, max and min - */ - private void updateUpperLimit() { - double limit = Math.min(mRealUpperLimit, mMaxValue); - int upperLimit = (int) Math.round((limit - mMinValue) / (mMaxValue - mMinValue) * getTotalSteps()); - if (mLowerLimit > upperLimit) { - Log.d("Invalid configuration", "upperLimit < lowerLimit; upperLimit not set"); - } else { - mUpperLimit = upperLimit; - } - } - - /** Update value only (optimization in case only value is set). */ - private void updateValue() { - setProgress((int) Math.round((mValue - mMinValue) / (mMaxValue - mMinValue) * getTotalSteps())); - } - - private int getTotalSteps() { - return (int) Math.ceil((mMaxValue - mMinValue) / getStepValue()); - } - - private double getStepValue() { - return mStep > 0 ? mStep : mStepCalculated; - } - - private BitmapDrawable getBitmapDrawable(final String uri) { - BitmapDrawable bitmapDrawable = null; - ExecutorService executorService = Executors.newSingleThreadExecutor(); - Future future = executorService.submit(new Callable() { - @Override - public BitmapDrawable call() { - BitmapDrawable bitmapDrawable = null; - try { - Bitmap bitmap = null; - if (uri.startsWith("http://") || uri.startsWith("https://") || - uri.startsWith("file://") || uri.startsWith("asset://") || uri.startsWith("data:")) { - bitmap = BitmapFactory.decodeStream(new URL(uri).openStream()); - } else { - int drawableId = getResources() - .getIdentifier(uri, "drawable", getContext() - .getPackageName()); - bitmap = BitmapFactory.decodeResource(getResources(), drawableId); - } - - bitmapDrawable = new BitmapDrawable(getResources(), bitmap); - } catch (Exception e) { - e.printStackTrace(); - } - return bitmapDrawable; - } - }); - try { - bitmapDrawable = future.get(); - } catch (Exception e) { - e.printStackTrace(); - } - return bitmapDrawable; - } - - public void setThumbImage(@Nullable final String uri) { - mThumbImageUri = uri; - refreshThumb(); - } - - public void setThumbSize(final float size) { - float density = getResources().getDisplayMetrics().density; - mThumbSizePx = size > 0 ? Math.round(size * density) : 0; - refreshThumb(); - } - - public void setThumbTintColor(@Nullable final Integer color) { - mThumbTintColor = color; - if (mThumbImageUri != null || mThumbSizePx > 0) { - refreshThumb(); - } else { - applyThumbTintColorFilter(); - } - } - - private void applyThumbTintColorFilter() { - if (getThumb() == null) { - return; - } - - if (mThumbTintColor != null) { - getThumb().setColorFilter(mThumbTintColor, PorterDuff.Mode.SRC_IN); - } else { - getThumb().clearColorFilter(); - } - } - - private void refreshThumb() { - if (mThumbImageUri != null) { - BitmapDrawable drawable = getBitmapDrawable(mThumbImageUri); - if (drawable != null) { - if (mThumbSizePx > 0) { - Bitmap originalBitmap = drawable.getBitmap(); - Bitmap scaledBitmap = - Bitmap.createScaledBitmap(originalBitmap, mThumbSizePx, mThumbSizePx, true); - setThumb(new BitmapDrawable(getResources(), scaledBitmap)); - } else { - setThumb(drawable); - } - applyThumbTintColorFilter(); - // Enable alpha channel for the thumbImage - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - setSplitTrack(false); - } - return; - } - } - - if (mThumbSizePx > 0) { - Bitmap bitmap = Bitmap.createBitmap(mThumbSizePx, mThumbSizePx, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - - int fillColor = - mThumbTintColor != null - ? mThumbTintColor - : (getThumbTintList() != null ? getThumbTintList().getDefaultColor() : 0xFFFFFFFF); - - Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - fillPaint.setStyle(Paint.Style.FILL); - fillPaint.setColor(fillColor); - float radius = mThumbSizePx / 2f; - canvas.drawCircle(radius, radius, radius, fillPaint); - - Paint strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); - strokePaint.setStyle(Paint.Style.STROKE); - strokePaint.setStrokeWidth(1); - strokePaint.setColor(0x1A000000); - canvas.drawCircle(radius, radius, radius - 0.5f, strokePaint); - - setThumb(new BitmapDrawable(getResources(), bitmap)); - applyThumbTintColorFilter(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - setSplitTrack(false); - } - } else { - // No special sizing; keep existing thumb, only apply tint if needed. - applyThumbTintColorFilter(); - } - } -} diff --git a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSliderEvent.java b/package/android/src/main/java/com/reactnativecommunity/slider/ReactSliderEvent.java deleted file mode 100644 index 1fe33f6c..00000000 --- a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSliderEvent.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.reactnativecommunity.slider; - -import androidx.annotation.Nullable; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.uimanager.events.Event; - -/** - * Event emitted by a ReactSliderManager when user changes slider position. - */ -public class ReactSliderEvent extends Event { - - public static final String EVENT_NAME = "topChange"; - - private final double mValue; - private final boolean mFromUser; - - public ReactSliderEvent(int viewId, double value, boolean fromUser) { - super(viewId); - mValue = value; - mFromUser = fromUser; - } - - public double getValue() { - return mValue; - } - - public boolean isFromUser() { - return mFromUser; - } - - - @Override - public String getEventName() { - return EVENT_NAME; - } - - @Override - public short getCoalescingKey() { - return 0; - } - - @Nullable - @Override - protected WritableMap getEventData() { - return serializeEventData(); - } - - private WritableMap serializeEventData() { - WritableMap eventData = Arguments.createMap(); - eventData.putInt("target", getViewTag()); - eventData.putDouble("value", getValue()); - eventData.putBoolean("fromUser", isFromUser()); - return eventData; - } -} diff --git a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSliderManager.java b/package/android/src/main/java/com/reactnativecommunity/slider/ReactSliderManager.java deleted file mode 100644 index bdd08f18..00000000 --- a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSliderManager.java +++ /dev/null @@ -1,243 +0,0 @@ -package com.reactnativecommunity.slider; - -import android.content.Context; -import android.view.View; -import android.widget.SeekBar; -import androidx.annotation.Nullable; - -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.common.MapBuilder; -import com.facebook.react.uimanager.PixelUtil; -import com.facebook.react.uimanager.SimpleViewManager; -import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.uimanager.UIManagerHelper; -import com.facebook.react.uimanager.ViewManagerDelegate; -import com.facebook.react.uimanager.ViewProps; -import com.facebook.react.uimanager.annotations.ReactProp; -import com.facebook.react.uimanager.events.EventDispatcher; -import java.util.Map; -import com.facebook.react.viewmanagers.RNCSliderManagerInterface; -import com.facebook.react.viewmanagers.RNCSliderManagerDelegate; -import com.facebook.react.module.annotations.ReactModule; -import com.facebook.yoga.YogaMeasureMode; -import com.facebook.yoga.YogaMeasureOutput; - -/** - * Manages instances of {@code ReactSlider}. - */ -@ReactModule(name = ReactSliderManagerImpl.REACT_CLASS) -public class ReactSliderManager extends SimpleViewManager implements RNCSliderManagerInterface { - - private final ViewManagerDelegate mDelegate; - - public ReactSliderManager() { - mDelegate = new RNCSliderManagerDelegate<>(this); - } - - @Nullable - @Override - protected ViewManagerDelegate getDelegate() { - return mDelegate; - } - - private static final SeekBar.OnSeekBarChangeListener ON_CHANGE_LISTENER = - new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) { - ReactSlider slider = (ReactSlider)seekbar; - - progress = slider.getValidProgressValue(progress); - seekbar.setProgress(progress); - - ReactContext reactContext = (ReactContext) seekbar.getContext(); - if (fromUser) { - int reactTag = seekbar.getId(); - UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag) - .dispatchEvent(new ReactSliderEvent(reactTag, slider.toRealProgress(progress), true)); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekbar) { - ReactContext reactContext = (ReactContext) seekbar.getContext(); - int reactTag = seekbar.getId(); - ((ReactSlider)seekbar).isSliding(true); - UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag) - .dispatchEvent(new ReactSlidingStartEvent( - reactTag, - ((ReactSlider)seekbar).toRealProgress(seekbar.getProgress()))); - } - - @Override - public void onStopTrackingTouch(SeekBar seekbar) { - ReactContext reactContext = (ReactContext) seekbar.getContext(); - ((ReactSlider)seekbar).isSliding(false); - int reactTag = seekbar.getId(); - - EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag); - - eventDispatcher.dispatchEvent( - new ReactSlidingCompleteEvent( - reactTag, - ((ReactSlider)seekbar).toRealProgress(seekbar.getProgress())) - ); - } - }; - - @Override - public String getName() { - return ReactSliderManagerImpl.REACT_CLASS; - } - - @Override - protected ReactSlider createViewInstance(ThemedReactContext context) { - return ReactSliderManagerImpl.createViewInstance(context); - } - - @Override - @ReactProp(name = "disabled", defaultBoolean = false) - public void setDisabled(ReactSlider view, boolean disabled) { - ReactSliderManagerImpl.setDisabled(view, disabled); - } - - @Override - @ReactProp(name = "value", defaultFloat = 0f) - public void setValue(ReactSlider view, float value) { - ReactSliderManagerImpl.setValue(view, value); - } - - @Override - @ReactProp(name = "minimumValue", defaultFloat = 0f) - public void setMinimumValue(ReactSlider view, double value) { - ReactSliderManagerImpl.setMinimumValue(view, value); - } - - @Override - @ReactProp(name = "maximumValue", defaultFloat = 0f) - public void setMaximumValue(ReactSlider view, double value) { - ReactSliderManagerImpl.setMaximumValue(view, value); - } - - @Override - @ReactProp(name = "step", defaultFloat = 0f) - public void setStep(ReactSlider view, double value) { - ReactSliderManagerImpl.setStep(view, value); - } - - @Override - @ReactProp(name = "thumbTintColor", customType = "Color") - public void setThumbTintColor(ReactSlider view, Integer color) { - ReactSliderManagerImpl.setThumbTintColor(view, color); - } - - @Override - @ReactProp(name = "thumbSize", defaultFloat = 0f) - public void setThumbSize(ReactSlider view, float size) { - ReactSliderManagerImpl.setThumbSize(view, size); - } - - @Override - @ReactProp(name = "minimumTrackTintColor", customType = "Color") - public void setMinimumTrackTintColor(ReactSlider view, Integer color) { - ReactSliderManagerImpl.setMinimumTrackTintColor(view, color); - } - - @Override - @ReactProp(name = "maximumTrackTintColor", customType = "Color") - public void setMaximumTrackTintColor(ReactSlider view, Integer color) { - ReactSliderManagerImpl.setMaximumTrackTintColor(view, color); - } - - @Override - @ReactProp(name = "inverted", defaultBoolean = false) - public void setInverted(ReactSlider view, boolean inverted) { - ReactSliderManagerImpl.setInverted(view, inverted); - } - - @Override - @ReactProp(name = "accessibilityUnits") - public void setAccessibilityUnits(ReactSlider view, String accessibilityUnits) { - ReactSliderManagerImpl.setAccessibilityUnits(view, accessibilityUnits); - } - - @Override - @ReactProp(name = "accessibilityIncrements") - public void setAccessibilityIncrements(ReactSlider view, ReadableArray accessibilityIncrements) { - ReactSliderManagerImpl.setAccessibilityIncrements(view, accessibilityIncrements); - } - - @ReactProp(name = "lowerLimit") - public void setLowerLimit(ReactSlider view, float value) { - ReactSliderManagerImpl.setLowerLimit(view, value); - } - - @ReactProp(name = "upperLimit") - public void setUpperLimit(ReactSlider view, float value) { - ReactSliderManagerImpl.setUpperLimit(view, value); - } - - @Override - @ReactProp(name = "thumbImage") - public void setThumbImage(ReactSlider view, @androidx.annotation.Nullable ReadableMap source) { - ReactSliderManagerImpl.setThumbImage(view, source); - } - - @Override - public void setTestID(ReactSlider view, @Nullable String value) { - super.setTestId(view, value); - } - - @Override - protected void addEventEmitters(final ThemedReactContext reactContext, final ReactSlider view) { - view.setOnSeekBarChangeListener(ON_CHANGE_LISTENER); - } - - // these props are not available on Android, however we must override their setters - @Override - public void setMinimumTrackImage(ReactSlider view, @Nullable ReadableMap readableMap) {} - - @Override - public void setMaximumTrackImage(ReactSlider view, @Nullable ReadableMap readableMap) {} - - @Override - public void setTrackImage(ReactSlider view, @Nullable ReadableMap value) {} - - @Override - public void setTapToSeek(ReactSlider view, boolean value) {} - - @Override - public void setVertical(ReactSlider view, boolean value) {} - - @Override - public long measure( - Context context, - ReadableMap localData, - ReadableMap props, - ReadableMap state, - float width, - YogaMeasureMode widthMode, - float height, - YogaMeasureMode heightMode, - @Nullable float[] attachmentsPositions) { - ReactSlider view = new ReactSlider(context, null); - int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); - view.measure(measureSpec, measureSpec); - return YogaMeasureOutput.make( - PixelUtil.toDIPFromPixel(view.getMeasuredWidth()), - PixelUtil.toDIPFromPixel(view.getMeasuredHeight())); - } - - @Nullable - @Override - public Map getExportedCustomBubblingEventTypeConstants() { - return ReactSliderManagerImpl.getExportedCustomBubblingEventTypeConstants(); - } - - @Nullable - @Override - public Map getExportedCustomDirectEventTypeConstants() { - return ReactSliderManagerImpl.getExportedCustomDirectEventTypeConstants(); - } -} diff --git a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSliderManagerImpl.java b/package/android/src/main/java/com/reactnativecommunity/slider/ReactSliderManagerImpl.java deleted file mode 100644 index 4d76b983..00000000 --- a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSliderManagerImpl.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.reactnativecommunity.slider; - -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.os.Build; - -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.common.MapBuilder; -import com.facebook.react.uimanager.ThemedReactContext; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import javax.annotation.Nullable; - -public class ReactSliderManagerImpl { - - public static final String REACT_CLASS = "RNCSlider"; - - public static ReactSlider createViewInstance(ThemedReactContext context) { - ReactSlider slider = new ReactSlider(context, null); - - if (Build.VERSION.SDK_INT >= 21) { - /** - * The "splitTrack" parameter should have "false" value, - * otherwise the SeekBar progress line doesn't appear when it is rotated. - */ - slider.setSplitTrack(false); - } - - return slider; - } - - public static void setValue(ReactSlider view, double value) { - if (view.isSliding() == false) { - view.setValue(value); - if (view.isAccessibilityFocused() && Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { - view.setupAccessibility((int)value); - } - } - } - - public static void setMinimumValue(ReactSlider view, double value) { - view.setMinValue(value); - } - - public static void setMaximumValue(ReactSlider view, double value) { - view.setMaxValue(value); - } - - public static void setLowerLimit(ReactSlider view, double value) { - view.setLowerLimit(value); - } - - public static void setUpperLimit(ReactSlider view, double value) { - view.setUpperLimit(value); - } - - public static void setStep(ReactSlider view, double value) { - view.setStep(value); - } - - public static void setDisabled(ReactSlider view, boolean disabled) { - view.setEnabled(!disabled); - } - - public static void setThumbTintColor(ReactSlider view, Integer color) { - view.setThumbTintColor(color); - } - - public static void setMinimumTrackTintColor(ReactSlider view, Integer color) { - LayerDrawable drawable = (LayerDrawable) view.getProgressDrawable().getCurrent(); - Drawable progress = drawable.findDrawableByLayerId(android.R.id.progress); - if (color == null) { - progress.clearColorFilter(); - } else { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - progress.setColorFilter(new PorterDuffColorFilter((int)color, PorterDuff.Mode.SRC_IN)); - } - else { - progress.setColorFilter(color, PorterDuff.Mode.SRC_IN); - } - } - } - - public static void setThumbImage(ReactSlider view, @Nullable ReadableMap source) { - String uri = null; - if (source != null) { - uri = source.getString("uri"); - } - view.setThumbImage(uri); - } - - public static void setThumbSize(ReactSlider view, float size) { - view.setThumbSize(size); - } - - public static void setMaximumTrackTintColor(ReactSlider view, Integer color) { - LayerDrawable drawable = (LayerDrawable) view.getProgressDrawable().getCurrent(); - Drawable background = drawable.findDrawableByLayerId(android.R.id.background); - if (color == null) { - background.clearColorFilter(); - } else { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - background.setColorFilter(new PorterDuffColorFilter((int)color, PorterDuff.Mode.SRC_IN)); - } - else { - background.setColorFilter(color, PorterDuff.Mode.SRC_IN); - } - } - } - - public static void setInverted(ReactSlider view, boolean inverted) { - if (inverted) view.setScaleX(-1f); - else view.setScaleX(1f); - } - - public static void setAccessibilityUnits(ReactSlider view, String accessibilityUnits) { - view.setAccessibilityUnits(accessibilityUnits); - } - - public static void setAccessibilityIncrements(ReactSlider view, ReadableArray accessibilityIncrements) { - List objectList = accessibilityIncrements.toArrayList(); - List stringList = new ArrayList<>(); - for(Object item: objectList) { - stringList.add((String)item); - } - view.setAccessibilityIncrements(stringList); - } - - public static Map getExportedCustomBubblingEventTypeConstants() { - return MapBuilder.of( - ReactSliderEvent.EVENT_NAME, MapBuilder.of("registrationName", ReactSliderEvent.EVENT_NAME) - ); - } - - public static Map getExportedCustomDirectEventTypeConstants() { - return MapBuilder.of( - ReactSlidingStartEvent.EVENT_NAME, MapBuilder.of("registrationName", ReactSlidingStartEvent.EVENT_NAME), - ReactSlidingCompleteEvent.EVENT_NAME, MapBuilder.of("registrationName", ReactSlidingCompleteEvent.EVENT_NAME) - ); - } -} diff --git a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSliderPackage.java b/package/android/src/main/java/com/reactnativecommunity/slider/ReactSliderPackage.java deleted file mode 100644 index 38a3f1f7..00000000 --- a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSliderPackage.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.reactnativecommunity.slider; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.NativeModule; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.uimanager.ViewManager; -import com.facebook.react.bridge.JavaScriptModule; - -public class ReactSliderPackage implements ReactPackage { - @Override - public List createNativeModules(ReactApplicationContext reactContext) { - return Collections.emptyList(); - } - - // Deprecated from RN 0.47 - public List> createJSModules() { - return Collections.emptyList(); - } - - @Override - @SuppressWarnings("rawtypes") - public List createViewManagers(ReactApplicationContext reactContext) { - return Arrays.asList(new ReactSliderManager()); - } -} diff --git a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSlidingCompleteEvent.java b/package/android/src/main/java/com/reactnativecommunity/slider/ReactSlidingCompleteEvent.java deleted file mode 100644 index b6e2a0e3..00000000 --- a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSlidingCompleteEvent.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.reactnativecommunity.slider; - -import androidx.annotation.Nullable; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.uimanager.events.Event; - -/** - * Event emitted when the user finishes dragging the slider. - */ -public class ReactSlidingCompleteEvent extends Event { - - public static final String EVENT_NAME = "onRNCSliderSlidingComplete"; - - private final double mValue; - - public ReactSlidingCompleteEvent(int viewId, double value) { - super(viewId); - mValue = value; - } - - public double getValue() { - return mValue; - } - - @Override - public String getEventName() { - return EVENT_NAME; - } - - @Override - public short getCoalescingKey() { - return 0; - } - - @Override - public boolean canCoalesce() { - return false; - } - - @Nullable - @Override - protected WritableMap getEventData() { - return serializeEventData(); - } - - private WritableMap serializeEventData() { - WritableMap eventData = Arguments.createMap(); - eventData.putInt("target", getViewTag()); - eventData.putDouble("value", getValue()); - return eventData; - } - -} diff --git a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSlidingStartEvent.java b/package/android/src/main/java/com/reactnativecommunity/slider/ReactSlidingStartEvent.java deleted file mode 100644 index fe07d8b2..00000000 --- a/package/android/src/main/java/com/reactnativecommunity/slider/ReactSlidingStartEvent.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.reactnativecommunity.slider; - -import androidx.annotation.Nullable; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.uimanager.events.Event; - -/** - * Event emitted when the user starts dragging the slider. - */ - -public class ReactSlidingStartEvent extends Event { - public static final String EVENT_NAME = "onRNCSliderSlidingStart"; - - private final double mValue; - - public ReactSlidingStartEvent(int viewId, double value) { - super(viewId); - mValue = value; - } - - public double getValue() { - return mValue; - } - - @Override - public String getEventName() { - return EVENT_NAME; - } - - @Override - public short getCoalescingKey() { - return 0; - } - - @Override - public boolean canCoalesce() { - return false; - } - - @Nullable - @Override - protected WritableMap getEventData() { - return serializeEventData(); - } - - private WritableMap serializeEventData() { - WritableMap eventData = Arguments.createMap(); - eventData.putInt("target", getViewTag()); - eventData.putDouble("value", getValue()); - return eventData; - } - -} diff --git a/package/android/src/main/jni/CMakeLists.txt b/package/android/src/main/jni/CMakeLists.txt index 6fe63910..32493c68 100644 --- a/package/android/src/main/jni/CMakeLists.txt +++ b/package/android/src/main/jni/CMakeLists.txt @@ -1,93 +1,44 @@ cmake_minimum_required(VERSION 3.13) -set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_VERBOSE_MAKEFILE on) -set(LIB_LITERAL RNCSlider) +include(${REACT_ANDROID_DIR}/../ReactCommon/cmake-utils/react-native-flags.cmake) + +set(LIB_LITERAL Slider) set(LIB_TARGET_NAME react_codegen_${LIB_LITERAL}) set(LIB_ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..) -set(LIB_COMMON_DIR ${LIB_ANDROID_DIR}/../common/cpp) -set(LIB_ANDROID_GENERATED_JNI_DIR ${LIB_ANDROID_DIR}/build/generated/source/codegen/jni) -set(LIB_ANDROID_GENERATED_COMPONENTS_DIR ${LIB_ANDROID_GENERATED_JNI_DIR}/react/renderer/components/${LIB_LITERAL}) +set(LIB_GENERATED_DIR ${LIB_ANDROID_DIR}/build/generated/source/codegen/jni) +set(LIB_COMPONENTS_DIR ${LIB_GENERATED_DIR}/react/renderer/components/${LIB_LITERAL}) -file(GLOB LIB_CUSTOM_SRCS CONFIGURE_DEPENDS *.cpp ${LIB_COMMON_DIR}/react/renderer/components/${LIB_LITERAL}/*.cpp) -file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS ${LIB_ANDROID_GENERATED_JNI_DIR}/*.cpp ${LIB_ANDROID_GENERATED_COMPONENTS_DIR}/*.cpp) +file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS ${LIB_GENERATED_DIR}/*.cpp ${LIB_COMPONENTS_DIR}/*.cpp) add_library( ${LIB_TARGET_NAME} - SHARED - ${LIB_CUSTOM_SRCS} + OBJECT ${LIB_CODEGEN_SRCS} ) target_include_directories( ${LIB_TARGET_NAME} PUBLIC - . - ${LIB_COMMON_DIR} - ${LIB_ANDROID_GENERATED_JNI_DIR} - ${LIB_ANDROID_GENERATED_COMPONENTS_DIR} -) - -# https://github.com/react-native-community/discussions-and-proposals/discussions/816 -# This if-then-else can be removed once this library does not support version below 0.76 -if (REACTNATIVE_MERGED_SO) - target_link_libraries( - ${LIB_TARGET_NAME} - fbjni - jsi - reactnative - ) -else() - target_link_libraries( - ${LIB_TARGET_NAME} - fbjni - folly_runtime - glog - jsi - react_codegen_rncore - react_debug - react_render_componentregistry - react_render_core - react_render_debug - react_render_graphics - react_render_imagemanager - react_render_mapbuffer - react_utils - react_nativemodule_core - rrc_image - turbomodulejsijni - rrc_view - yoga - ) -endif() - -if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 81) - target_compile_reactnative_options(${LIB_TARGET_NAME} PUBLIC) -else() - target_compile_options( - ${LIB_TARGET_NAME} - PRIVATE - -fexceptions - -frtti - -std=c++20 - -Wall - -Wpedantic - -Wno-gnu-zero-variadic-macro-arguments - ) -endif() - -target_compile_options( - ${LIB_TARGET_NAME} - PRIVATE - -DLOG_TAG=\"ReactNative\" - -fexceptions - -frtti - -std=c++20 - -Wall + ${CMAKE_CURRENT_SOURCE_DIR} + ${LIB_GENERATED_DIR} + ${LIB_COMPONENTS_DIR} ) target_include_directories( ${CMAKE_PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + ${LIB_GENERATED_DIR} + ${LIB_COMPONENTS_DIR} ) + +target_link_libraries( + ${LIB_TARGET_NAME} + fbjni + jsi + reactnative +) + +target_compile_reactnative_options(${LIB_TARGET_NAME} PRIVATE) diff --git a/package/android/src/main/jni/RNCSlider.h b/package/android/src/main/jni/Slider.h similarity index 68% rename from package/android/src/main/jni/RNCSlider.h rename to package/android/src/main/jni/Slider.h index f0c0e9f5..25f52bdf 100644 --- a/package/android/src/main/jni/RNCSlider.h +++ b/package/android/src/main/jni/Slider.h @@ -3,16 +3,14 @@ #include #include #include -#include namespace facebook { namespace react { JSI_EXPORT -std::shared_ptr RNCSlider_ModuleProvider( +std::shared_ptr Slider_ModuleProvider( const std::string &moduleName, const JavaTurboModule::InitParams ¶ms); } // namespace react } // namespace facebook - diff --git a/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderComponentDescriptor.h b/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderComponentDescriptor.h deleted file mode 100644 index 8bc649ad..00000000 --- a/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderComponentDescriptor.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include -#include "RNCSliderMeasurementsManager.h" - -namespace facebook { - namespace react { - - class RNCSliderComponentDescriptor final - : public ConcreteComponentDescriptor { -#ifdef ANDROID - public: - RNCSliderComponentDescriptor(const ComponentDescriptorParameters ¶meters) - : ConcreteComponentDescriptor(parameters), measurementsManager_( - std::make_shared(contextContainer_)) {} - - void adopt(ShadowNode &shadowNode) const override { - ConcreteComponentDescriptor::adopt(shadowNode); - - - auto &rncSliderShadowNode = - static_cast(shadowNode); - - // `RNCSliderShadowNode` uses `RNCSliderMeasurementsManager` to - // provide measurements to Yoga. - rncSliderShadowNode.setSliderMeasurementsManager( - measurementsManager_); - - // All `RNCSliderShadowNode`s must have leaf Yoga nodes with properly - // setup measure function. - rncSliderShadowNode.enableMeasurement(); - } - private: - const std::shared_ptr measurementsManager_; -#else - public: - RNCSliderComponentDescriptor(const ComponentDescriptorParameters ¶meters) - : ConcreteComponentDescriptor(parameters) {} -#endif - }; - - } -} diff --git a/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderMeasurementsManager.cpp b/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderMeasurementsManager.cpp deleted file mode 100644 index fada95c9..00000000 --- a/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderMeasurementsManager.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#ifdef ANDROID -#include "RNCSliderMeasurementsManager.h" - -#include -#include -#include - -using namespace facebook::jni; - -namespace facebook::react { - -Size RNCSliderMeasurementsManager::measure( - SurfaceId surfaceId, - LayoutConstraints layoutConstraints) const { - { - std::scoped_lock lock(mutex_); - if (hasBeenMeasured_) { - return cachedMeasurement_; - } - } - - const jni::global_ref& fabricUIManager = - contextContainer_->at>("FabricUIManager"); - - static auto measure = - jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") - ->getMethod("measure"); - - auto minimumSize = layoutConstraints.minimumSize; - auto maximumSize = layoutConstraints.maximumSize; - - local_ref componentName = make_jstring("RNCSlider"); - - auto measurement = yogaMeassureToSize(measure( - fabricUIManager, - surfaceId, - componentName.get(), - nullptr, - nullptr, - nullptr, - minimumSize.width, - maximumSize.width, - minimumSize.height, - maximumSize.height)); - - std::scoped_lock lock(mutex_); - cachedMeasurement_ = measurement; - hasBeenMeasured_ = true; - return measurement; -} - -} -#endif diff --git a/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderMeasurementsManager.h b/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderMeasurementsManager.h deleted file mode 100644 index adcb27b5..00000000 --- a/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderMeasurementsManager.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifdef ANDROID -#pragma once - -#include -#include -#include - -namespace facebook::react { - - class RNCSliderMeasurementsManager { - public: - RNCSliderMeasurementsManager( - const std::shared_ptr &contextContainer) - : contextContainer_(contextContainer) {} - - Size measure(SurfaceId surfaceId, LayoutConstraints layoutConstraints) const; - - private: - const std::shared_ptr contextContainer_; - mutable std::mutex mutex_; - mutable bool hasBeenMeasured_ = false; - mutable Size cachedMeasurement_{}; - - }; -} -#endif diff --git a/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderShadowNode.cpp b/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderShadowNode.cpp deleted file mode 100644 index d10cb078..00000000 --- a/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderShadowNode.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "RNCSliderShadowNode.h" -#include "RNCSliderMeasurementsManager.h" - -namespace facebook { - namespace react { - - extern const char RNCSliderComponentName[] = "RNCSlider"; - -#ifdef ANDROID - void RNCSliderShadowNode::setSliderMeasurementsManager( - const std::shared_ptr & - measurementsManager) { - ensureUnsealed(); - measurementsManager_ = measurementsManager; - } - -#pragma mark - LayoutableShadowNode - - Size RNCSliderShadowNode::measureContent( - const LayoutContext & /*layoutContext*/, - const LayoutConstraints &layoutConstraints) const { - return measurementsManager_->measure(getSurfaceId(), layoutConstraints); - } -#endif - - } -} diff --git a/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderShadowNode.h b/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderShadowNode.h deleted file mode 100644 index b30099fe..00000000 --- a/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderShadowNode.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "RNCSliderMeasurementsManager.h" - -namespace facebook { - namespace react { - - JSI_EXPORT extern const char RNCSliderComponentName[]; - -/* - * `ShadowNode` for component. - */ - class JSI_EXPORT RNCSliderShadowNode final - : public ConcreteViewShadowNode< - RNCSliderComponentName, - RNCSliderProps, - RNCSliderEventEmitter, - RNCSliderState> { - public: - using ConcreteViewShadowNode::ConcreteViewShadowNode; - -#ifdef ANDROID - void setSliderMeasurementsManager( - const std::shared_ptr &measurementsManager); - -#pragma mark - LayoutableShadowNode - - Size measureContent( - const LayoutContext &layoutContext, - const LayoutConstraints &layoutConstraints) const override; - - private: - std::shared_ptr measurementsManager_; -#endif - - }; - - } -} diff --git a/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderState.h b/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderState.h deleted file mode 100644 index 13cdf8e5..00000000 --- a/package/common/cpp/react/renderer/components/RNCSlider/RNCSliderState.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#ifdef ANDROID -#include -#include -#include -#endif - -namespace facebook { -namespace react { - -class RNCSliderState { -public: - RNCSliderState() = default; - -#ifdef ANDROID - RNCSliderState(RNCSliderState const &previousState, folly::dynamic data){}; - folly::dynamic getDynamic() const { - return {}; - }; - MapBuffer getMapBuffer() const { - return MapBufferBuilder::EMPTY(); - }; -#endif -}; - -} -} diff --git a/package/ios/RNCSlider.h b/package/ios/RNCSlider.h deleted file mode 100644 index aa9d1855..00000000 --- a/package/ios/RNCSlider.h +++ /dev/null @@ -1,31 +0,0 @@ -#import - -#import - -@interface RNCSlider : UISlider - -@property (nonatomic, copy) RCTBubblingEventBlock onRNCSliderValueChange; -@property (nonatomic, copy) RCTBubblingEventBlock onRNCSliderSlidingStart; -@property (nonatomic, copy) RCTBubblingEventBlock onRNCSliderSlidingComplete; - -@property (nonatomic, assign) float step; -@property (nonatomic, assign) float lastValue; -@property (nonatomic, assign) bool isSliding; - -@property (nonatomic, assign) float lowerLimit; -@property (nonatomic, assign) float upperLimit; - -@property (nonatomic, strong) UIImage *trackImage; -@property (nonatomic, strong) UIImage *minimumTrackImage; -@property (nonatomic, strong) UIImage *maximumTrackImage; -@property (nonatomic, strong) UIImage *thumbImage; -@property (nonatomic, assign) CGFloat thumbSize; -@property (nonatomic, assign) bool tapToSeek; -@property (nonatomic, strong) NSString *accessibilityUnits; -@property (nonatomic, strong) NSArray *accessibilityIncrements; - -- (float) discreteValue:(float)value; -- (void) setDisabled:(bool)disabled; -- (void) refreshThumb; - -@end diff --git a/package/ios/RNCSlider.m b/package/ios/RNCSlider.m deleted file mode 100644 index 8bc1c248..00000000 --- a/package/ios/RNCSlider.m +++ /dev/null @@ -1,256 +0,0 @@ -#import "RNCSlider.h" - -@implementation RNCSlider -{ - float _unclippedValue; - bool _minimumTrackImageSet; - bool _maximumTrackImageSet; - UIImage *_thumbImage; - CGFloat _thumbSize; - UIColor *_thumbTintColor; -} - -- (instancetype)init { - if (self = [super init]) { - _upperLimit = FLT_MAX; - _lowerLimit = FLT_MIN; - } - return self; -} - -- (instancetype)initWithFrame:(CGRect)frame -{ - return [super initWithFrame:frame]; -} - -- (void)setValue:(float)value -{ - value = [self discreteValue:value]; - _unclippedValue = value; - super.value = value; - [self setupAccessibility:value]; -} - -- (void)setValue:(float)value animated:(BOOL)animated -{ - value = [self discreteValue:value]; - _unclippedValue = value; - [super setValue:value animated:animated]; - [self setupAccessibility:value]; -} - -- (void)setupAccessibility:(float)value -{ - if (self.accessibilityUnits && self.accessibilityIncrements && [self.accessibilityIncrements count] - 1 == (int)self.maximumValue) { - int index = (int)value; - NSString *sliderValue = (NSString *)[self.accessibilityIncrements objectAtIndex:index]; - NSUInteger stringLength = [self.accessibilityUnits length]; - - NSString *spokenUnits = [NSString stringWithString:self.accessibilityUnits]; - if (sliderValue && [sliderValue intValue] == 1) { - spokenUnits = [spokenUnits substringToIndex:stringLength-1]; - } - - self.accessibilityValue = [NSString stringWithFormat:@"%@ %@", sliderValue, spokenUnits]; - } -} - -- (void)setMinimumValue:(float)minimumValue -{ - super.minimumValue = minimumValue; - super.value = _unclippedValue; -} - -- (void)setMaximumValue:(float)maximumValue -{ - super.maximumValue = maximumValue; - super.value = _unclippedValue; -} - -- (void)setTrackImage:(UIImage *)trackImage -{ - if (trackImage != _trackImage) { - _trackImage = trackImage; - CGFloat width = trackImage.size.width / 2; - if (!_minimumTrackImageSet) { - UIImage *minimumTrackImage = [trackImage resizableImageWithCapInsets:(UIEdgeInsets){ - 0, width, 0, width - } resizingMode:UIImageResizingModeStretch]; - [self setMinimumTrackImage:minimumTrackImage forState:UIControlStateNormal]; - } - if (!_maximumTrackImageSet) { - UIImage *maximumTrackImage = [trackImage resizableImageWithCapInsets:(UIEdgeInsets){ - 0, width, 0, width - } resizingMode:UIImageResizingModeStretch]; - [self setMaximumTrackImage:maximumTrackImage forState:UIControlStateNormal]; - } - } -} - -- (void)setMinimumTrackImage:(UIImage *)minimumTrackImage -{ - _trackImage = nil; - _minimumTrackImageSet = true; - minimumTrackImage = [minimumTrackImage resizableImageWithCapInsets:(UIEdgeInsets){ - 0, minimumTrackImage.size.width, 0, 0 - } resizingMode:UIImageResizingModeStretch]; - [self setMinimumTrackImage:minimumTrackImage forState:UIControlStateNormal]; -} - -- (UIImage *)minimumTrackImage -{ - return [self thumbImageForState:UIControlStateNormal]; -} - -- (void)setMaximumTrackImage:(UIImage *)maximumTrackImage -{ - _trackImage = nil; - _maximumTrackImageSet = true; - maximumTrackImage = [maximumTrackImage resizableImageWithCapInsets:(UIEdgeInsets){ - 0, 0, 0, maximumTrackImage.size.width - } resizingMode:UIImageResizingModeStretch]; - [self setMaximumTrackImage:maximumTrackImage forState:UIControlStateNormal]; -} - -- (UIImage *)maximumTrackImage -{ - return [self thumbImageForState:UIControlStateNormal]; -} - -- (void)setThumbImage:(UIImage *)thumbImage -{ - _thumbImage = thumbImage; - [self refreshThumb]; -} - -- (UIImage *)thumbImage -{ - return [self thumbImageForState:UIControlStateNormal]; -} - -- (void)setThumbSize:(CGFloat)thumbSize -{ - _thumbSize = thumbSize; - [self refreshThumb]; -} - -- (void)setThumbTintColor:(UIColor *)thumbTintColor -{ - _thumbTintColor = thumbTintColor; - [super setThumbTintColor:thumbTintColor]; - - [self refreshThumb]; -} - -- (void)refreshThumb -{ - if (![NSThread isMainThread]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self refreshThumb]; - }); - return; - } - - UIImage *imageToSet = nil; - - if (_thumbSize > 0) { - CGSize newSize = CGSizeMake(_thumbSize, _thumbSize); - UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0); - CGContextRef context = UIGraphicsGetCurrentContext(); - - if (_thumbImage) { - [_thumbImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; - } else { - UIColor *fillColor = _thumbTintColor ?: self.thumbTintColor ?: [UIColor whiteColor]; - CGContextSetFillColorWithColor(context, fillColor.CGColor); - CGContextFillEllipseInRect(context, CGRectMake(0, 0, newSize.width, newSize.height)); - - CGContextSetStrokeColorWithColor(context, [[UIColor colorWithWhite:0.0 alpha:0.1] CGColor]); - CGContextSetLineWidth(context, 0.5); - CGContextStrokeEllipseInRect( - context, - CGRectMake(0.25, 0.25, newSize.width - 0.5, newSize.height - 0.5)); - } - - imageToSet = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - } else if (_thumbImage) { - imageToSet = _thumbImage; - } - - if (imageToSet) { - [self setThumbImage:imageToSet forState:UIControlStateNormal]; - [self setThumbImage:imageToSet forState:UIControlStateHighlighted]; - [self setThumbImage:imageToSet forState:UIControlStateSelected]; - } - - [UIView performWithoutAnimation:^{ - float currentValue = super.value; - float minimumValue = super.minimumValue; - float maximumValue = super.maximumValue; - - float eps = (maximumValue - minimumValue) / 1000.0f; - if (eps <= 0) { - eps = 0.0001f; - } - - float nudgedValue = currentValue + eps; - if (nudgedValue > maximumValue) { - nudgedValue = currentValue - eps; - } - if (nudgedValue < minimumValue) { - nudgedValue = minimumValue; - } - - if (nudgedValue != currentValue) { - [super setValue:nudgedValue animated:NO]; - } - [super setValue:currentValue animated:NO]; - - [self setNeedsLayout]; - [self layoutSubviews]; - [self layoutIfNeeded]; - }]; -} - -- (void)setInverted:(BOOL)inverted -{ - if (inverted) { - self.transform = CGAffineTransformMakeScale(-1, 1); - } else { - self.transform = CGAffineTransformMakeScale(1, 1); - } -} - -- (void)setDisabled:(BOOL)disabled -{ - self.enabled = !disabled; - [self layoutSubviews]; -} - -- (float)discreteValue:(float)value -{ - if (self.step > 0 && value >= self.maximumValue) { - return self.maximumValue; - } - - if (self.step > 0 && self.step <= (self.maximumValue - self.minimumValue)) { - double (^_round)(double) = ^(double x) { - if (!UIAccessibilityIsVoiceOverRunning()) { - return round(x); - } else if (self.lastValue > value) { - return floor(x); - } else { - return ceil(x); - } - }; - - return MAX(self.minimumValue, - MIN(self.maximumValue, self.minimumValue + _round((value - self.minimumValue) / self.step) * self.step) - ); - } - - return value; -} - -@end diff --git a/package/ios/RNCSliderComponentView.h b/package/ios/RNCSliderComponentView.h deleted file mode 100644 index 3bb041d7..00000000 --- a/package/ios/RNCSliderComponentView.h +++ /dev/null @@ -1,36 +0,0 @@ -#import -#import -#import "RNCSlider.h" - -NS_ASSUME_NONNULL_BEGIN - -typedef void (^RNCLoadImageCompletionBlock)(NSError * _Nullable error, UIImage * _Nullable image); -typedef void (^RNCLoadImageFailureBlock)(); - -@interface RNCSliderComponentView : RCTViewComponentView - -@property (nonatomic, copy) RCTBubblingEventBlock onRNCSliderValueChange; -@property (nonatomic, copy) RCTBubblingEventBlock onRNCSliderSlidingStart; -@property (nonatomic, copy) RCTBubblingEventBlock onRNCSliderSlidingComplete; - -@property (nonatomic, assign) float step; -@property (nonatomic, assign) float lastValue; -@property (nonatomic, assign) bool isSliding; - -@property (nonatomic, assign) float lowerLimit; -@property (nonatomic, assign) float upperLimit; - -@property (nonatomic, strong) UIImage *trackImage; -@property (nonatomic, strong) UIImage *minimumTrackImage; -@property (nonatomic, strong) UIImage *maximumTrackImage; -@property (nonatomic, strong) UIImage *thumbImage; -@property (nonatomic, assign) CGFloat thumbSize; -@property (nonatomic, assign) bool tapToSeek; -@property (nonatomic, strong) NSString *accessibilityUnits; -@property (nonatomic, strong) NSArray *accessibilityIncrements; - -- (float) discreteValue:(float)value; - -@end - -NS_ASSUME_NONNULL_END diff --git a/package/ios/RNCSliderComponentView.mm b/package/ios/RNCSliderComponentView.mm deleted file mode 100644 index 605487a8..00000000 --- a/package/ios/RNCSliderComponentView.mm +++ /dev/null @@ -1,309 +0,0 @@ -#import "RNCSliderComponentView.h" - -#import - -#import -#import -#import -#import -#import -#import "RCTImagePrimitivesConversions.h" -#import -#import "RCTFabricComponentsPlugins.h" -#import "RNCSlider.h" - -using namespace facebook::react; - -@interface RNCSliderComponentView () - -@end - - -@implementation RNCSliderComponentView -{ - RNCSlider *slider; - UIImage *_image; - BOOL _isSliding; -} - -+ (ComponentDescriptorProvider)componentDescriptorProvider -{ - return concreteComponentDescriptorProvider(); -} - -+ (BOOL)shouldBeRecycled { - return NO; -} - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - static const auto defaultProps = std::make_shared(); - _props = defaultProps; - slider = [[RNCSlider alloc] initWithFrame:self.bounds]; - [slider addTarget:self action:@selector(sliderValueChanged:) - forControlEvents:UIControlEventValueChanged]; - [slider addTarget:self action:@selector(sliderTouchStart:) - forControlEvents:UIControlEventTouchDown]; - [slider addTarget:self action:@selector(sliderTouchEnd:) - forControlEvents:(UIControlEventTouchUpInside | - UIControlEventTouchUpOutside | - UIControlEventTouchCancel)]; - - UITapGestureRecognizer *tapGesturer; - tapGesturer = [[UITapGestureRecognizer alloc] initWithTarget: self action:@selector(tapHandler:)]; - [tapGesturer setNumberOfTapsRequired: 1]; - [slider addGestureRecognizer:tapGesturer]; - - slider.value = (float)defaultProps->value; - self.contentView = slider; - } - return self; -} - -- (void)tapHandler:(UITapGestureRecognizer *)gesture { - if ([gesture.view class] != [RNCSlider class]) { - return; - } - RNCSlider *slider = (RNCSlider *)gesture.view; - slider.isSliding = _isSliding; - - // Ignore this tap if in the middle of a slide. - if (_isSliding) { - return; - } - - if (!slider.tapToSeek) { - return; - } - - CGPoint touchPoint = [gesture locationInView:slider]; - float rangeWidth = slider.maximumValue - slider.minimumValue; - - float sliderPercent; - if ([UIView userInterfaceLayoutDirectionForSemanticContentAttribute:slider.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft) { - sliderPercent = 1.0 - (touchPoint.x / slider.bounds.size.width); - } else { - sliderPercent = touchPoint.x / slider.bounds.size.width; - } - - slider.lastValue = slider.value; - float value = slider.minimumValue + (rangeWidth * sliderPercent); - - if (value < slider.lowerLimit) { - value = slider.lowerLimit; - } else if (value > slider.upperLimit) { - value = slider.upperLimit; - } - - [slider setValue:[slider discreteValue:value] animated: YES]; - - std::dynamic_pointer_cast(_eventEmitter) - ->onRNCSliderSlidingStart(RNCSliderEventEmitter::OnRNCSliderSlidingStart{.value = static_cast(slider.lastValue)}); - - // Trigger onValueChange to address https://github.com/react-native-community/react-native-slider/issues/212 - std::dynamic_pointer_cast(_eventEmitter) - ->onRNCSliderValueChange(RNCSliderEventEmitter::OnRNCSliderValueChange{.value = static_cast(slider.value)}); - - std::dynamic_pointer_cast(_eventEmitter) - ->onRNCSliderSlidingComplete(RNCSliderEventEmitter::OnRNCSliderSlidingComplete{.value = static_cast(slider.value)}); -} - -- (void)sliderValueChanged:(RNCSlider *)sender -{ - [self RNCSendSliderEvent:sender withContinuous:YES isSlidingStart:NO]; -} - -- (void)sliderTouchStart:(RNCSlider *)sender -{ - [self RNCSendSliderEvent:sender withContinuous:NO isSlidingStart:YES]; - _isSliding = YES; - sender.isSliding = YES; -} - -- (void)sliderTouchEnd:(RNCSlider *)sender -{ - [self RNCSendSliderEvent:sender withContinuous:NO isSlidingStart:NO]; - sender.isSliding = NO; - _isSliding = NO; -} - -- (void)RNCSendSliderEvent:(RNCSlider *)sender withContinuous:(BOOL)continuous isSlidingStart:(BOOL)isSlidingStart -{ - float value = [sender discreteValue:sender.value]; - - if (value < sender.lowerLimit) { - value = sender.lowerLimit; - [sender setValue:value animated:NO]; - } else if (value > sender.upperLimit) { - value = sender.upperLimit; - [sender setValue:value animated:NO]; - } - - if(!sender.isSliding) { - [sender setValue:value animated:NO]; - } - - if (continuous) { - if (sender.lastValue != value) { - std::dynamic_pointer_cast(_eventEmitter) - ->onRNCSliderValueChange(RNCSliderEventEmitter::OnRNCSliderValueChange{.value = static_cast(value)}); - } - } else { - if (!isSlidingStart) { - std::dynamic_pointer_cast(_eventEmitter) - ->onRNCSliderSlidingComplete(RNCSliderEventEmitter::OnRNCSliderSlidingComplete{.value = static_cast(value)}); - } - if (isSlidingStart) { - std::dynamic_pointer_cast(_eventEmitter) - ->onRNCSliderSlidingStart(RNCSliderEventEmitter::OnRNCSliderSlidingStart{.value = static_cast(value)}); - } - } - - sender.lastValue = value; -} - -- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps -{ - const auto &oldScreenProps = *std::static_pointer_cast(_props); - const auto &newScreenProps = *std::static_pointer_cast(props); - - if (oldScreenProps.value != newScreenProps.value) { - if (!slider.isSliding) { - slider.value = newScreenProps.value; - } - } - if (oldScreenProps.disabled != newScreenProps.disabled) { - [slider setDisabled: newScreenProps.disabled]; - } - if (oldScreenProps.step != newScreenProps.step) { - slider.step = newScreenProps.step; - } - if (oldScreenProps.inverted != newScreenProps.inverted) { - [self setInverted:newScreenProps.inverted]; - } - if (oldScreenProps.maximumValue != newScreenProps.maximumValue) { - [slider setMaximumValue:newScreenProps.maximumValue]; - } - if (slider.lowerLimit != newScreenProps.lowerLimit) { - if(newScreenProps.lowerLimit > slider.upperLimit){ - NSLog(@"Invalid configuration: upperLimit < lowerLimit; lowerLimit not set"); - } else { - slider.lowerLimit = newScreenProps.lowerLimit; - } - } - if (slider.upperLimit != newScreenProps.upperLimit) { - if(newScreenProps.upperLimit < slider.lowerLimit){ - NSLog(@"Invalid configuration: upperLimit < lowerLimit; upperLimit not set"); - } else { - slider.upperLimit = newScreenProps.upperLimit; - } - } - if (oldScreenProps.tapToSeek != newScreenProps.tapToSeek) { - slider.tapToSeek = newScreenProps.tapToSeek; - } - if (oldScreenProps.minimumValue != newScreenProps.minimumValue) { - [slider setMinimumValue:newScreenProps.minimumValue]; - } - if (oldScreenProps.thumbTintColor != newScreenProps.thumbTintColor) { - slider.thumbTintColor = RCTUIColorFromSharedColor(newScreenProps.thumbTintColor); - } - if (oldScreenProps.thumbSize != newScreenProps.thumbSize) { - slider.thumbSize = newScreenProps.thumbSize; - } - if (oldScreenProps.minimumTrackTintColor != newScreenProps.minimumTrackTintColor) { - slider.minimumTrackTintColor = RCTUIColorFromSharedColor(newScreenProps.minimumTrackTintColor); - } - if (oldScreenProps.maximumTrackTintColor != newScreenProps.maximumTrackTintColor) { - slider.maximumTrackTintColor = RCTUIColorFromSharedColor(newScreenProps.maximumTrackTintColor); - } - if (oldScreenProps.accessibilityUnits != newScreenProps.accessibilityUnits) { - NSString *convertedAccessibilityUnits = [NSString stringWithCString:newScreenProps.accessibilityUnits.c_str() - encoding:[NSString defaultCStringEncoding]]; - slider.accessibilityUnits = convertedAccessibilityUnits; - } - if (oldScreenProps.accessibilityIncrements != newScreenProps.accessibilityIncrements) { - id accessibilityIncrements = [NSMutableArray new]; - for (auto str : newScreenProps.accessibilityIncrements) { - [accessibilityIncrements addObject:[NSString stringWithUTF8String:str.c_str()]]; - } - [slider setAccessibilityIncrements:accessibilityIncrements]; - } - if (oldScreenProps.thumbImage != newScreenProps.thumbImage) { - [self loadImageFromImageSource:newScreenProps.thumbImage completionBlock:^(NSError *error, UIImage *image) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->slider setThumbImage:image]; - }); - } - failureBlock:^{ - [self->slider setThumbImage:nil]; - }]; - } - if (oldScreenProps.trackImage != newScreenProps.trackImage) { - [self loadImageFromImageSource:newScreenProps.trackImage completionBlock:^(NSError *error, UIImage *image) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->slider setTrackImage:image]; - }); - } - failureBlock:^{ - [self->slider setTrackImage:nil]; - }]; - } - if (oldScreenProps.minimumTrackImage != newScreenProps.minimumTrackImage) { - [self loadImageFromImageSource:newScreenProps.minimumTrackImage completionBlock:^(NSError *error, UIImage *image) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->slider setMinimumTrackImage:image]; - }); - } - failureBlock:^{ - [self->slider setMinimumTrackImage:nil]; - }]; - } - if (oldScreenProps.maximumTrackImage != newScreenProps.maximumTrackImage) { - [self loadImageFromImageSource:newScreenProps.maximumTrackImage completionBlock:^(NSError *error, UIImage *image) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->slider setMaximumTrackImage:image]; - }); - } - failureBlock:^{ - [self->slider setMaximumTrackImage:nil]; - }]; - } - [super updateProps:props oldProps:oldProps]; -} - - -// TODO temporarily using bridge, workaround for https://github.com/reactwg/react-native-new-architecture/discussions/31#discussioncomment-2717047, rewrite when Meta comes with a solution. -- (void)loadImageFromImageSource:(ImageSource)source completionBlock:(RNCLoadImageCompletionBlock)completionBlock failureBlock:(RNCLoadImageFailureBlock)failureBlock -{ - NSString *uri = [[NSString alloc] initWithUTF8String:source.uri.c_str()]; - if ((BOOL)uri.length) { - [[[RCTBridge currentBridge] moduleForName:@"ImageLoader"] - loadImageWithURLRequest:NSURLRequestFromImageSource(source) - size:CGSizeMake(source.size.width, source.size.height) - scale:source.scale - clipped:NO - resizeMode:RCTResizeModeCover - progressBlock:nil - partialLoadBlock:nil - completionBlock:completionBlock]; - } else { - failureBlock(); - } -} - -- (void)setInverted:(BOOL)inverted -{ - if (inverted) { - self.transform = CGAffineTransformMakeScale(-1, 1); - } else { - self.transform = CGAffineTransformMakeScale(1, 1); - } -} - -@end - -Class RNCSliderCls(void) -{ - return RNCSliderComponentView.class; -} diff --git a/package/ios/Slider-Bridging-Header.h b/package/ios/Slider-Bridging-Header.h new file mode 100644 index 00000000..b23da03e --- /dev/null +++ b/package/ios/Slider-Bridging-Header.h @@ -0,0 +1,2 @@ +#import "RCTComponent.h" +#import "RCTViewManager.h" diff --git a/package/ios/RNCSlider.xcodeproj/project.pbxproj b/package/ios/Slider.xcodeproj/project.pbxproj similarity index 50% rename from package/ios/RNCSlider.xcodeproj/project.pbxproj rename to package/ios/Slider.xcodeproj/project.pbxproj index 057244e9..95a2f18d 100644 --- a/package/ios/RNCSlider.xcodeproj/project.pbxproj +++ b/package/ios/Slider.xcodeproj/project.pbxproj @@ -3,16 +3,18 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ - 28C79A23220DC7760061DE82 /* RNCSlider.m in Sources */ = {isa = PBXBuildFile; fileRef = 28C79A21220DC7760061DE82 /* RNCSlider.m */; }; - 7682E5172887E3AB00642F2D /* RNCSliderComponentView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7682E5162887E3AB00642F2D /* RNCSliderComponentView.mm */; }; + 17E6A4A12CF7B8D100000001 /* SliderComponentView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 17E6A4A02CF7B8D100000001 /* SliderComponentView.mm */; }; + 5E555C0D2413F4C50049A1A2 /* SliderViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* SliderViewManager.m */; }; + F4FF95D7245B92E800C19C63 /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FF95D6245B92E800C19C63 /* SliderView.swift */; }; + F4FF95D9245B92E800C19C63 /* SliderViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FF95D8245B92E800C19C63 /* SliderViewManager.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ - 28C79A07220DC4CC0061DE82 /* CopyFiles */ = { + 58B511D91A9E6C8500147676 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = "include/$(PRODUCT_NAME)"; @@ -24,15 +26,17 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 28C79A09220DC4CC0061DE82 /* libRNCSlider.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNCSlider.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 28C79A1E220DC7760061DE82 /* RNCSlider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNCSlider.h; sourceTree = ""; }; - 28C79A21220DC7760061DE82 /* RNCSlider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCSlider.m; sourceTree = ""; }; - 7682E5152887E39900642F2D /* RNCSliderComponentView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNCSliderComponentView.h; sourceTree = ""; }; - 7682E5162887E3AB00642F2D /* RNCSliderComponentView.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RNCSliderComponentView.mm; sourceTree = ""; }; + 134814201AA4EA6300B7C361 /* libSlider.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSlider.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 17E6A4A02CF7B8D100000001 /* SliderComponentView.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SliderComponentView.mm; sourceTree = ""; }; + 17E6A4A22CF7B8D100000001 /* slider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = slider.h; sourceTree = ""; }; + B3E7B5891CC2AC0600A0062D /* SliderViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SliderViewManager.m; sourceTree = ""; }; + F4FF95D5245B92E700C19C63 /* Slider-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Slider-Bridging-Header.h"; sourceTree = ""; }; + F4FF95D6245B92E800C19C63 /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = ""; }; + F4FF95D8245B92E800C19C63 /* SliderViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderViewManager.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 28C79A06220DC4CC0061DE82 /* Frameworks */ = { + 58B511D81A9E6C8500147676 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -42,124 +46,123 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 28C79A00220DC4CC0061DE82 = { + 134814211AA4EA7D00B7C361 /* Products */ = { isa = PBXGroup; children = ( - 7682E5162887E3AB00642F2D /* RNCSliderComponentView.mm */, - 7682E5152887E39900642F2D /* RNCSliderComponentView.h */, - 28C79A1E220DC7760061DE82 /* RNCSlider.h */, - 28C79A21220DC7760061DE82 /* RNCSlider.m */, - 28C79A0A220DC4CC0061DE82 /* Products */, + 134814201AA4EA6300B7C361 /* libSlider.a */, ); + name = Products; sourceTree = ""; }; - 28C79A0A220DC4CC0061DE82 /* Products */ = { + 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( - 28C79A09220DC4CC0061DE82 /* libRNCSlider.a */, + 17E6A4A02CF7B8D100000001 /* SliderComponentView.mm */, + F4FF95D6245B92E800C19C63 /* SliderView.swift */, + F4FF95D8245B92E800C19C63 /* SliderViewManager.swift */, + B3E7B5891CC2AC0600A0062D /* SliderViewManager.m */, + 17E6A4A22CF7B8D100000001 /* slider.h */, + F4FF95D5245B92E700C19C63 /* Slider-Bridging-Header.h */, + 134814211AA4EA7D00B7C361 /* Products */, ); - name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 28C79A08220DC4CC0061DE82 /* RNCSlider */ = { + 58B511DA1A9E6C8500147676 /* Slider */ = { isa = PBXNativeTarget; - buildConfigurationList = 28C79A12220DC4CC0061DE82 /* Build configuration list for PBXNativeTarget "RNCSlider" */; + buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "Slider" */; buildPhases = ( - 28C79A05220DC4CC0061DE82 /* Sources */, - 28C79A06220DC4CC0061DE82 /* Frameworks */, - 28C79A07220DC4CC0061DE82 /* CopyFiles */, + 58B511D71A9E6C8500147676 /* Sources */, + 58B511D81A9E6C8500147676 /* Frameworks */, + 58B511D91A9E6C8500147676 /* CopyFiles */, ); buildRules = ( ); dependencies = ( ); - name = RNCSlider; - productName = RNCSlider; - productReference = 28C79A09220DC4CC0061DE82 /* libRNCSlider.a */; + name = Slider; + productName = RCTDataManager; + productReference = 134814201AA4EA6300B7C361 /* libSlider.a */; productType = "com.apple.product-type.library.static"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 28C79A01220DC4CC0061DE82 /* Project object */ = { + 58B511D31A9E6C8500147676 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1010; - ORGANIZATIONNAME = "React Native Community"; + LastUpgradeCheck = 0920; + ORGANIZATIONNAME = Facebook; TargetAttributes = { - 28C79A08220DC4CC0061DE82 = { - CreatedOnToolsVersion = 10.1; + 58B511DA1A9E6C8500147676 = { + CreatedOnToolsVersion = 6.1.1; }; }; }; - buildConfigurationList = 28C79A04220DC4CC0061DE82 /* Build configuration list for PBXProject "RNCSlider" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; + buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "Slider" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); - mainGroup = 28C79A00220DC4CC0061DE82; - productRefGroup = 28C79A0A220DC4CC0061DE82 /* Products */; + mainGroup = 58B511D21A9E6C8500147676; + productRefGroup = 58B511D21A9E6C8500147676; projectDirPath = ""; projectRoot = ""; targets = ( - 28C79A08220DC4CC0061DE82 /* RNCSlider */, + 58B511DA1A9E6C8500147676 /* Slider */, ); }; /* End PBXProject section */ /* Begin PBXSourcesBuildPhase section */ - 28C79A05220DC4CC0061DE82 /* Sources */ = { + 58B511D71A9E6C8500147676 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7682E5172887E3AB00642F2D /* RNCSliderComponentView.mm in Sources */, - 28C79A23220DC7760061DE82 /* RNCSlider.m in Sources */, + 17E6A4A12CF7B8D100000001 /* SliderComponentView.mm in Sources */, + F4FF95D7245B92E800C19C63 /* SliderView.swift in Sources */, + F4FF95D9245B92E800C19C63 /* SliderViewManager.swift in Sources */, + 5E555C0D2413F4C50049A1A2 /* SliderViewManager.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ - 28C79A10220DC4CC0061DE82 /* Debug */ = { + 58B511ED1A9E6C8500147676 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=*]" = arm64; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -168,55 +171,49 @@ "DEBUG=1", "$(inherited)", ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = YES; - MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; }; name = Debug; }; - 28C79A11220DC4CC0061DE82 /* Release */ = { + 58B511EE1A9E6C8500147676 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=*]" = arm64; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -225,54 +222,74 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; name = Release; }; - 28C79A13220DC4CC0061DE82 /* Debug */ = { + 58B511F01A9E6C8500147676 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + "$(SRCROOT)/../../../React/**", + "$(SRCROOT)/../../react-native/React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = Slider; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Slider-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; - 28C79A14220DC4CC0061DE82 /* Release */ = { + 58B511F11A9E6C8500147676 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + "$(SRCROOT)/../../../React/**", + "$(SRCROOT)/../../react-native/React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = Slider; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Slider-Bridging-Header.h"; + SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 28C79A04220DC4CC0061DE82 /* Build configuration list for PBXProject "RNCSlider" */ = { + 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "Slider" */ = { isa = XCConfigurationList; buildConfigurations = ( - 28C79A10220DC4CC0061DE82 /* Debug */, - 28C79A11220DC4CC0061DE82 /* Release */, + 58B511ED1A9E6C8500147676 /* Debug */, + 58B511EE1A9E6C8500147676 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 28C79A12220DC4CC0061DE82 /* Build configuration list for PBXNativeTarget "RNCSlider" */ = { + 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "Slider" */ = { isa = XCConfigurationList; buildConfigurations = ( - 28C79A13220DC4CC0061DE82 /* Debug */, - 28C79A14220DC4CC0061DE82 /* Release */, + 58B511F01A9E6C8500147676 /* Debug */, + 58B511F11A9E6C8500147676 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; - rootObject = 28C79A01220DC4CC0061DE82 /* Project object */; + rootObject = 58B511D31A9E6C8500147676 /* Project object */; } diff --git a/package/ios/SliderComponentView.mm b/package/ios/SliderComponentView.mm new file mode 100644 index 00000000..bc34c4d1 --- /dev/null +++ b/package/ios/SliderComponentView.mm @@ -0,0 +1,291 @@ +#import +#import +#import +#import +#import + +#if __has_include("slider-Swift.h") +#import "slider-Swift.h" +#elif __has_include("Slider-Swift.h") +#import "Slider-Swift.h" +#endif + +#import +#import +#import +#import + +using namespace facebook::react; + +@interface SliderComponentView : RCTViewComponentView +@end + +typedef void (^SliderLoadImageCompletionBlock)(NSError *error, UIImage *image); +typedef void (^SliderLoadImageFailureBlock)(void); + +@implementation SliderComponentView { + SliderView *_sliderView; +} + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + + _sliderView = [[SliderView alloc] initWithFrame:self.bounds]; + __weak __typeof(self) weakSelf = self; + _sliderView.onValueChange = ^(NSDictionary *event) { + __strong __typeof(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + auto eventEmitter = std::static_pointer_cast(strongSelf->_eventEmitter); + if (eventEmitter == nullptr) { + return; + } + + NSNumber *value = event[@"value"]; + eventEmitter->onValueChange(SliderViewEventEmitter::OnValueChange{ + .value = value.doubleValue + }); + }; + _sliderView.onSlidingStart = ^(NSDictionary *event) { + __strong __typeof(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + auto eventEmitter = std::static_pointer_cast(strongSelf->_eventEmitter); + if (eventEmitter == nullptr) { + return; + } + + NSNumber *value = event[@"value"]; + eventEmitter->onSlidingStart(SliderViewEventEmitter::OnSlidingStart{ + .value = value.doubleValue + }); + }; + _sliderView.onSlidingComplete = ^(NSDictionary *event) { + __strong __typeof(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + auto eventEmitter = std::static_pointer_cast(strongSelf->_eventEmitter); + if (eventEmitter == nullptr) { + return; + } + + NSNumber *value = event[@"value"]; + eventEmitter->onSlidingComplete(SliderViewEventEmitter::OnSlidingComplete{ + .value = value.doubleValue + }); + }; + + self.contentView = _sliderView; + } + + return self; +} + +- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps +{ + const auto &oldSliderProps = *std::static_pointer_cast(_props); + const auto &newSliderProps = *std::static_pointer_cast(props); + + if (oldSliderProps.minValue != newSliderProps.minValue) { + _sliderView.minValue = newSliderProps.minValue; + } + + if (oldSliderProps.maxValue != newSliderProps.maxValue) { + _sliderView.maxValue = newSliderProps.maxValue; + } + + if (oldSliderProps.step != newSliderProps.step) { + _sliderView.step = newSliderProps.step; + } + + if (oldSliderProps.value != newSliderProps.value) { + _sliderView.value = newSliderProps.value; + } + + if (oldSliderProps.lowerLimit != newSliderProps.lowerLimit) { + _sliderView.lowerLimit = newSliderProps.lowerLimit; + } + + if (oldSliderProps.upperLimit != newSliderProps.upperLimit) { + _sliderView.upperLimit = newSliderProps.upperLimit; + } + + if (oldSliderProps.disabled != newSliderProps.disabled) { + _sliderView.disabled = newSliderProps.disabled; + } + + if (oldSliderProps.inverted != newSliderProps.inverted) { + _sliderView.inverted = newSliderProps.inverted; + } + + if (oldSliderProps.tapToSeek != newSliderProps.tapToSeek) { + _sliderView.tapToSeek = newSliderProps.tapToSeek; + } + + if (oldSliderProps.minimumTrackTintColor != newSliderProps.minimumTrackTintColor) { + _sliderView.minimumTrackTintColor = RCTUIColorFromSharedColor(newSliderProps.minimumTrackTintColor); + } + + if (oldSliderProps.maximumTrackTintColor != newSliderProps.maximumTrackTintColor) { + _sliderView.maximumTrackTintColor = RCTUIColorFromSharedColor(newSliderProps.maximumTrackTintColor); + } + + if (oldSliderProps.thumbTintColor != newSliderProps.thumbTintColor) { + _sliderView.thumbTintColor = RCTUIColorFromSharedColor(newSliderProps.thumbTintColor); + } + + if (oldSliderProps.thumbSize != newSliderProps.thumbSize) { + _sliderView.thumbSize = newSliderProps.thumbSize; + } + + if (oldSliderProps.thumbImage != newSliderProps.thumbImage) { + [self loadImageFromImageSource:newSliderProps.thumbImage + completionBlock:^(__unused NSError *error, UIImage *image) { + dispatch_async(dispatch_get_main_queue(), ^{ + self->_sliderView.thumbImage = image; + }); + } + failureBlock:^{ + self->_sliderView.thumbImage = nil; + }]; + } + + if (oldSliderProps.trackImage != newSliderProps.trackImage) { + [self loadImageFromImageSource:newSliderProps.trackImage + completionBlock:^(__unused NSError *error, UIImage *image) { + dispatch_async(dispatch_get_main_queue(), ^{ + self->_sliderView.trackImage = image; + }); + } + failureBlock:^{ + self->_sliderView.trackImage = nil; + }]; + } + + if (oldSliderProps.minimumTrackImage != newSliderProps.minimumTrackImage) { + [self loadImageFromImageSource:newSliderProps.minimumTrackImage + completionBlock:^(__unused NSError *error, UIImage *image) { + dispatch_async(dispatch_get_main_queue(), ^{ + self->_sliderView.minimumTrackImage = image; + }); + } + failureBlock:^{ + self->_sliderView.minimumTrackImage = nil; + }]; + } + + if (oldSliderProps.maximumTrackImage != newSliderProps.maximumTrackImage) { + [self loadImageFromImageSource:newSliderProps.maximumTrackImage + completionBlock:^(__unused NSError *error, UIImage *image) { + dispatch_async(dispatch_get_main_queue(), ^{ + self->_sliderView.maximumTrackImage = image; + }); + } + failureBlock:^{ + self->_sliderView.maximumTrackImage = nil; + }]; + } + + [super updateProps:props oldProps:oldProps]; +} + +- (void)loadImageFromImageSource:(ImageSource)source + completionBlock:(SliderLoadImageCompletionBlock)completionBlock + failureBlock:(SliderLoadImageFailureBlock)failureBlock +{ + NSString *uri = [[NSString alloc] initWithUTF8String:source.uri.c_str()]; + if (!(BOOL)uri.length) { + failureBlock(); + return; + } + + NSURL *url = NSURLFromImageSource(source); + UIImage *localImage = [self localImageForURL:url uri:uri]; + if (localImage != nil) { + completionBlock(nil, localImage); + return; + } + + if ([url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]) { + NSURLSessionDataTask *task = [NSURLSession.sharedSession + dataTaskWithURL:url + completionHandler:^(NSData *data, __unused NSURLResponse *response, NSError *error) { + UIImage *image = data == nil ? nil : [UIImage imageWithData:data scale:source.scale]; + if (image != nil) { + completionBlock(error, image); + } else { + failureBlock(); + } + }]; + [task resume]; + return; + } + + RCTBridge *bridge = [RCTBridge currentBridge]; + id imageLoader = [bridge moduleForName:@"ImageLoader"]; + if (imageLoader == nil) { + failureBlock(); + return; + } + + [imageLoader loadImageWithURLRequest:NSURLRequestFromImageSource(source) + size:CGSizeMake(source.size.width, source.size.height) + scale:source.scale + clipped:NO + resizeMode:RCTResizeModeCover + progressBlock:nil + partialLoadBlock:nil + completionBlock:completionBlock]; +} + +- (UIImage *)localImageForURL:(NSURL *)url uri:(NSString *)uri +{ + if (url.fileURL) { + UIImage *fileImage = [UIImage imageWithContentsOfFile:url.path]; + if (fileImage != nil) { + return fileImage; + } + } + + NSArray *names = @[ + uri, + uri.lastPathComponent, + uri.lastPathComponent.stringByDeletingPathExtension, + ]; + + for (NSString *name in names) { + UIImage *namedImage = [UIImage imageNamed:name]; + if (namedImage != nil) { + return namedImage; + } + + NSString *extension = name.pathExtension; + NSString *resourceName = extension.length > 0 ? name.stringByDeletingPathExtension : name; + NSString *resourcePath = [NSBundle.mainBundle pathForResource:resourceName + ofType:extension.length > 0 ? extension : nil]; + if (resourcePath != nil) { + UIImage *resourceImage = [UIImage imageWithContentsOfFile:resourcePath]; + if (resourceImage != nil) { + return resourceImage; + } + } + } + + return nil; +} + +@end diff --git a/package/ios/SliderView.swift b/package/ios/SliderView.swift new file mode 100644 index 00000000..32795b62 --- /dev/null +++ b/package/ios/SliderView.swift @@ -0,0 +1,607 @@ +import SwiftUI +import UIKit + +struct SliderComponent: View { + @ObservedObject var state: SliderState + + private var value: Binding { + Binding( + get: { state.value }, + set: { state.setValue($0, notify: true) } + ) + } + + @ViewBuilder + private var nativeSlider: some View { + if state.step > 0 { + Slider( + value: value, + in: state.valueRange, + step: state.step, + onEditingChanged: state.setEditing(_:)) + } else { + Slider( + value: value, + in: state.valueRange, + onEditingChanged: state.setEditing(_:)) + } + } + + var body: some View { + GeometryReader { geometry in + ZStack { + nativeSlider + .opacity(state.usesCustomVisuals ? 0 : 1) + .tint(state.minimumTrackTintColor.map { Color(uiColor: $0) }) + .environment(\.layoutDirection, state.inverted ? .rightToLeft : .leftToRight) + .allowsHitTesting(!state.usesCustomVisuals) + + if state.usesCustomVisuals { + SliderVisual(state: state, width: geometry.size.width) + } + } + .disabled(state.disabled) + .contentShape(Rectangle()) + .sliderGesture( + state: state, + width: geometry.size.width, + enabled: state.tapToSeek || state.usesCustomVisuals, + minimumDistance: state.tapToSeek ? 0 : 1 + ) + } + } +} + +private struct SliderVisual: View { + @ObservedObject var state: SliderState + let width: CGFloat + + private let defaultThumbSize = CGSize(width: 28, height: 28) + + private var trackHeight: CGFloat { + let imageHeight = [ + state.trackImage?.size.height, + state.minimumTrackImage?.size.height, + state.maximumTrackImage?.size.height, + ] + .compactMap { $0 } + .max() ?? 0 + + return max(4, min(imageHeight, 18)) + } + + private var thumbSize: CGSize { + if state.thumbSize > 0 { + let size = CGFloat(state.thumbSize) + return CGSize(width: size, height: size) + } + + if let thumbImage = state.thumbImage { + let maxSide = max(thumbImage.size.width, thumbImage.size.height) + guard maxSide > 0 else { + return defaultThumbSize + } + + let scale = min(1, defaultThumbSize.width / maxSide) + return CGSize(width: thumbImage.size.width * scale, height: thumbImage.size.height * scale) + } + + return defaultThumbSize + } + + var body: some View { + let percent = state.visualPercent + let resolvedThumbSize = thumbSize + let trackInset = resolvedThumbSize.width / 2 + let trackWidth = max(width - resolvedThumbSize.width, 0) + let thumbCenterX = trackInset + trackWidth * percent + let progressWidth = state.inverted ? trackWidth * (1 - percent) : trackWidth * percent + let progressOffset = state.inverted ? trackWidth - progressWidth : 0 + + ZStack(alignment: .leading) { + ZStack(alignment: .leading) { + SliderTrackSegment( + width: trackWidth, + height: trackHeight, + color: state.maximumTrackTintColor ?? UIColor.systemGray4, + image: state.maximumTrackVisualImage + ) + + SliderTrackSegment( + width: progressWidth, + height: trackHeight, + color: state.minimumTrackTintColor ?? UIColor.systemBlue, + image: state.minimumTrackVisualImage + ) + .offset(x: progressOffset) + } + .frame(width: trackWidth, height: trackHeight, alignment: .leading) + .clipShape(Capsule()) + .offset(x: trackInset) + + SliderThumb( + color: state.thumbTintColor ?? UIColor.white, + image: state.thumbImage, + size: resolvedThumbSize + ) + .offset(x: thumbCenterX - resolvedThumbSize.width / 2) + } + .frame(width: width, height: max(resolvedThumbSize.height, trackHeight), alignment: .center) + } +} + +private struct SliderTrackImage { + let image: UIImage + let leftCap: CGFloat + let rightCap: CGFloat +} + +private struct SliderTrackSegment: View { + let width: CGFloat + let height: CGFloat + let color: UIColor + let image: SliderTrackImage? + + var body: some View { + let resolvedWidth = max(width, 0) + + Group { + if let image { + Image( + uiImage: image.image.renderedSliderTrackImage( + width: resolvedWidth, + height: height, + leftCap: image.leftCap, + rightCap: image.rightCap + ) + ) + } else { + Color(uiColor: color) + } + } + .frame(width: resolvedWidth, height: height) + } +} + +private struct SliderThumb: View { + let color: UIColor + let image: UIImage? + let size: CGSize + + private var shadowOpacity: Double { + color.cgColor.alpha > 0 ? 0.12 : 0 + } + + var body: some View { + Group { + if let image { + Image(uiImage: image) + .resizable() + } else { + Circle() + .fill(Color(uiColor: color)) + .shadow(color: Color.black.opacity(shadowOpacity), radius: 4, x: 0, y: 1) + } + } + .frame(width: size.width, height: size.height) + } +} + +private extension View { + @ViewBuilder + func sliderGesture(state: SliderState, width: CGFloat, enabled: Bool, minimumDistance: CGFloat) -> some View { + if enabled { + gesture( + DragGesture(minimumDistance: minimumDistance) + .onChanged { gesture in + guard !state.disabled else { + return + } + + state.setEditing(true) + state.setValueFromLocation( + x: gesture.location.x, + width: width, + inset: state.gestureInset(for: width) + ) + } + .onEnded { _ in + state.setEditing(false) + } + ) + } else { + self + } + } +} + +class SliderState: ObservableObject { + @Published var value: Double = 0 + @Published var minValue: Double = 0 + @Published var maxValue: Double = 1 + @Published var step: Double = 0 + @Published var lowerLimit: Double = -Double.greatestFiniteMagnitude + @Published var upperLimit: Double = Double.greatestFiniteMagnitude + @Published var disabled: Bool = false + @Published var inverted: Bool = false + @Published var tapToSeek: Bool = false + @Published var minimumTrackTintColor: UIColor? + @Published var maximumTrackTintColor: UIColor? + @Published var thumbTintColor: UIColor? + @Published var thumbImage: UIImage? + @Published var thumbSize: Double = 0 + @Published var trackImage: UIImage? + @Published var minimumTrackImage: UIImage? + @Published var maximumTrackImage: UIImage? + + var onValueChange: RCTDirectEventBlock? + var onSlidingStart: RCTDirectEventBlock? + var onSlidingComplete: RCTDirectEventBlock? + + private var isEditing = false + + var valueRange: ClosedRange { + minValue...max(minValue, maxValue) + } + + var usesCustomVisuals: Bool { + maximumTrackTintColor != nil || + thumbTintColor != nil || + thumbImage != nil || + thumbSize > 0 || + trackImage != nil || + minimumTrackImage != nil || + maximumTrackImage != nil + } + + fileprivate var minimumTrackVisualImage: SliderTrackImage? { + if let minimumTrackImage { + return SliderTrackImage(image: minimumTrackImage, leftCap: minimumTrackImage.size.width - 1, rightCap: 0) + } + + if let trackImage { + let capWidth = floor((trackImage.size.width - 1) / 2) + return SliderTrackImage(image: trackImage, leftCap: capWidth, rightCap: capWidth) + } + + return nil + } + + fileprivate var maximumTrackVisualImage: SliderTrackImage? { + if let maximumTrackImage { + return SliderTrackImage(image: maximumTrackImage, leftCap: 0, rightCap: maximumTrackImage.size.width - 1) + } + + if let trackImage { + let capWidth = floor((trackImage.size.width - 1) / 2) + return SliderTrackImage(image: trackImage, leftCap: capWidth, rightCap: capWidth) + } + + return nil + } + + var visualPercent: CGFloat { + guard maxValue > minValue else { + return inverted ? 1 : 0 + } + + let percent = min(max((value - minValue) / (maxValue - minValue), 0), 1) + return CGFloat(inverted ? 1 - percent : percent) + } + + func setMinValue(_ nextValue: Double) { + minValue = nextValue + normalizeRange() + clampValue() + } + + func setMaxValue(_ nextValue: Double) { + maxValue = nextValue + normalizeRange() + clampValue() + } + + func setStep(_ nextValue: Double) { + step = max(nextValue, 0) + clampValue() + } + + func setValue(_ nextValue: Double, notify: Bool) { + let nextValue = clampedValue(snappedValue(nextValue)) + + if value != nextValue { + value = nextValue + } + + if notify { + onValueChange?(eventPayload()) + } + } + + func setEditing(_ editing: Bool) { + guard editing != isEditing else { + return + } + + isEditing = editing + + if editing { + onSlidingStart?(eventPayload()) + } else { + onSlidingComplete?(eventPayload()) + } + } + + func gestureInset(for width: CGFloat) -> CGFloat { + if thumbSize > 0 { + return min(CGFloat(thumbSize) / 2, width / 2) + } + + if let thumbImage { + return min(thumbImage.size.width / 2, width / 2) + } + + return usesCustomVisuals ? min(14, width / 2) : 0 + } + + func setValueFromLocation(x: CGFloat, width: CGFloat, inset: CGFloat = 0) { + guard !disabled else { + return + } + + let usableWidth = max(width - inset * 2, 1) + let percent = min(max(Double((x - inset) / usableWidth), 0), 1) + let directedPercent = inverted ? 1 - percent : percent + let nextValue = minValue + directedPercent * (maxValue - minValue) + setValue(nextValue, notify: true) + } + + private func normalizeRange() { + if maxValue < minValue { + maxValue = minValue + } + } + + private func clampValue() { + setValue(value, notify: false) + } + + private func clampedValue(_ nextValue: Double) -> Double { + let lower = max(minValue, lowerLimit) + let upper = min(maxValue, upperLimit) + return min(max(nextValue, lower), max(lower, upper)) + } + + private func snappedValue(_ nextValue: Double) -> Double { + guard step > 0 else { + return nextValue + } + + return minValue + round((nextValue - minValue) / step) * step + } + + private func eventPayload() -> [String: Any] { + ["value": value] + } +} + +private extension UIImage { + func renderedSliderTrackImage(width: CGFloat, height: CGFloat, leftCap: CGFloat, rightCap: CGFloat) -> UIImage { + let targetSize = CGSize(width: max(width, 1), height: max(height, 1)) + let imageWidth = max(size.width, 1) + let sourceLeftCap = min(max(leftCap, 0), imageWidth) + let sourceRightCap = min(max(rightCap, 0), imageWidth - sourceLeftCap) + let sourceCenterWidth = max(imageWidth - sourceLeftCap - sourceRightCap, 0) + + let targetLeftCap = min(sourceLeftCap, targetSize.width) + let targetRightCap = min(sourceRightCap, max(targetSize.width - targetLeftCap, 0)) + let targetCenterWidth = max(targetSize.width - targetLeftCap - targetRightCap, 0) + + let format = UIGraphicsImageRendererFormat() + format.scale = UIScreen.main.scale + format.opaque = false + + return UIGraphicsImageRenderer(size: targetSize, format: format).image { _ in + drawSliderTrackSlice( + sourceX: 0, + sourceWidth: sourceLeftCap, + destination: CGRect(x: 0, y: 0, width: targetLeftCap, height: targetSize.height) + ) + + drawSliderTrackSlice( + sourceX: sourceLeftCap, + sourceWidth: sourceCenterWidth, + destination: CGRect(x: targetLeftCap, y: 0, width: targetCenterWidth, height: targetSize.height) + ) + + drawSliderTrackSlice( + sourceX: imageWidth - sourceRightCap, + sourceWidth: sourceRightCap, + destination: CGRect( + x: targetSize.width - targetRightCap, + y: 0, + width: targetRightCap, + height: targetSize.height + ) + ) + } + } + + private func drawSliderTrackSlice(sourceX: CGFloat, sourceWidth: CGFloat, destination: CGRect) { + guard sourceWidth > 0, destination.width > 0, destination.height > 0 else { + return + } + + guard let cgImage else { + draw(in: destination) + return + } + + let scaleX = CGFloat(cgImage.width) / max(size.width, 1) + let sourceRect = CGRect( + x: sourceX * scaleX, + y: 0, + width: sourceWidth * scaleX, + height: CGFloat(cgImage.height) + ) + .integral + + guard let slice = cgImage.cropping(to: sourceRect) else { + draw(in: destination) + return + } + + UIImage(cgImage: slice, scale: scale, orientation: imageOrientation).draw(in: destination) + } +} + +@objc(SliderView) +public class SliderView: UIView { + private let state = SliderState() + private var hostingController: UIHostingController? + + @objc public var minValue: Double = 0 { + didSet { + state.setMinValue(minValue) + } + } + + @objc public var maxValue: Double = 1 { + didSet { + state.setMaxValue(maxValue) + } + } + + @objc public var step: Double = 0 { + didSet { + state.setStep(step) + } + } + + @objc public var value: Double = 0 { + didSet { + state.setValue(value, notify: false) + } + } + + @objc public var lowerLimit: Double = -Double.greatestFiniteMagnitude { + didSet { + state.lowerLimit = lowerLimit + state.setValue(state.value, notify: false) + } + } + + @objc public var upperLimit: Double = Double.greatestFiniteMagnitude { + didSet { + state.upperLimit = upperLimit + state.setValue(state.value, notify: false) + } + } + + @objc public var disabled: Bool = false { + didSet { + state.disabled = disabled + } + } + + @objc public var inverted: Bool = false { + didSet { + state.inverted = inverted + } + } + + @objc public var tapToSeek: Bool = false { + didSet { + state.tapToSeek = tapToSeek + } + } + + @objc public var minimumTrackTintColor: UIColor? { + didSet { + state.minimumTrackTintColor = minimumTrackTintColor + } + } + + @objc public var maximumTrackTintColor: UIColor? { + didSet { + state.maximumTrackTintColor = maximumTrackTintColor + } + } + + @objc public var thumbTintColor: UIColor? { + didSet { + state.thumbTintColor = thumbTintColor + } + } + + @objc public var thumbImage: UIImage? { + didSet { + state.thumbImage = thumbImage + } + } + + @objc public var thumbSize: Double = 0 { + didSet { + state.thumbSize = thumbSize + } + } + + @objc public var trackImage: UIImage? { + didSet { + state.trackImage = trackImage + } + } + + @objc public var minimumTrackImage: UIImage? { + didSet { + state.minimumTrackImage = minimumTrackImage + } + } + + @objc public var maximumTrackImage: UIImage? { + didSet { + state.maximumTrackImage = maximumTrackImage + } + } + + @objc public var onValueChange: RCTDirectEventBlock? { + didSet { + state.onValueChange = onValueChange + } + } + + @objc public var onSlidingStart: RCTDirectEventBlock? { + didSet { + state.onSlidingStart = onSlidingStart + } + } + + @objc public var onSlidingComplete: RCTDirectEventBlock? { + didSet { + state.onSlidingComplete = onSlidingComplete + } + } + + public override init(frame: CGRect) { + super.init(frame: frame) + + clipsToBounds = false + layer.masksToBounds = false + + let hostingController = UIHostingController(rootView: SliderComponent(state: state)) + hostingController.view.backgroundColor = .clear + hostingController.view.clipsToBounds = false + hostingController.view.layer.masksToBounds = false + addSubview(hostingController.view) + self.hostingController = hostingController + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func layoutSubviews() { + super.layoutSubviews() + hostingController?.view.frame = bounds + } +} diff --git a/package/ios/SliderViewManager.m b/package/ios/SliderViewManager.m new file mode 100644 index 00000000..93cacc2f --- /dev/null +++ b/package/ios/SliderViewManager.m @@ -0,0 +1,26 @@ +#import "RCTViewManager.h" + +@interface RCT_EXTERN_MODULE(SliderViewManager, RCTViewManager) + +RCT_EXPORT_VIEW_PROPERTY(minValue, double) +RCT_EXPORT_VIEW_PROPERTY(maxValue, double) +RCT_EXPORT_VIEW_PROPERTY(step, double) +RCT_EXPORT_VIEW_PROPERTY(value, double) +RCT_EXPORT_VIEW_PROPERTY(lowerLimit, double) +RCT_EXPORT_VIEW_PROPERTY(upperLimit, double) +RCT_EXPORT_VIEW_PROPERTY(disabled, BOOL) +RCT_EXPORT_VIEW_PROPERTY(inverted, BOOL) +RCT_EXPORT_VIEW_PROPERTY(tapToSeek, BOOL) +RCT_EXPORT_VIEW_PROPERTY(minimumTrackTintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(maximumTrackTintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(thumbTintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(thumbImage, UIImage) +RCT_EXPORT_VIEW_PROPERTY(thumbSize, double) +RCT_EXPORT_VIEW_PROPERTY(trackImage, UIImage) +RCT_EXPORT_VIEW_PROPERTY(minimumTrackImage, UIImage) +RCT_EXPORT_VIEW_PROPERTY(maximumTrackImage, UIImage) +RCT_EXPORT_VIEW_PROPERTY(onValueChange, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onSlidingStart, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onSlidingComplete, RCTDirectEventBlock) + +@end diff --git a/package/ios/SliderViewManager.swift b/package/ios/SliderViewManager.swift new file mode 100644 index 00000000..06b8a01a --- /dev/null +++ b/package/ios/SliderViewManager.swift @@ -0,0 +1,13 @@ +import UIKit + +@objc(SliderViewManager) +class SliderViewManager: RCTViewManager { + + override func view() -> UIView! { + return SliderView() + } + + @objc override static func requiresMainQueueSetup() -> Bool { + return false + } +} diff --git a/package/ios/slider.h b/package/ios/slider.h new file mode 100644 index 00000000..aa06e866 --- /dev/null +++ b/package/ios/slider.h @@ -0,0 +1,2 @@ +#import +#import diff --git a/package/package.json b/package/package.json index 63179687..87985b69 100644 --- a/package/package.json +++ b/package/package.json @@ -2,7 +2,7 @@ "name": "@react-native-community/slider", "version": "5.2.0", "license": "MIT", - "author": "react-native-community", + "author": "Callstack", "homepage": "https://github.com/callstack/react-native-slider#readme", "description": "React Native component used to select a single value from a range of values.", "publishConfig": { @@ -16,8 +16,8 @@ "slider" ], "scripts": { - "prepare": "babel --extensions \".ts,.tsx\" --out-dir dist src", - "lint": "npx eslint src __test__", + "prepare": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && babel --extensions \".ts,.tsx\" --out-dir dist src", + "lint": "npx eslint src", "test": "jest", "prepack": "npx copyfiles \"./../README.md\" ./README.md" }, @@ -60,6 +60,7 @@ "jest": { "preset": "react-native", "verbose": true, + "watchman": false, "modulePathIgnorePatterns": [ "/e2e/" ] @@ -71,15 +72,15 @@ "jsxBracketSameLine": true }, "codegenConfig": { - "name": "RNCSlider", + "name": "Slider", "type": "components", "jsSrcsDir": "src", "android": { - "javaPackageName": "com.reactnativecommunity.slider" + "javaPackageName": "com.callstack.slider" }, "ios": { "componentProvider": { - "RNCSlider": "RNCSliderComponentView" + "SliderView": "SliderComponentView" } } } diff --git a/package/react-native-slider.podspec b/package/react-native-slider.podspec index b795d531..67c781fb 100644 --- a/package/react-native-slider.podspec +++ b/package/react-native-slider.podspec @@ -1,34 +1,32 @@ -require 'json' +require "json" -package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +folly_compiler_flags = "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32" Pod::Spec.new do |s| s.name = "react-native-slider" - s.version = package['version'] - s.summary = package['description'] - s.license = package['license'] + s.version = package["version"] + s.summary = package["description"] + s.license = package["license"] - s.authors = package['author'] - s.homepage = package['homepage'] - s.platforms = { :ios => "9.0", :visionos => "1.0" } + s.authors = package["author"] + s.homepage = package["homepage"] + s.platforms = { :ios => "16.0" } s.source = { :git => "https://github.com/callstack/react-native-slider.git", :tag => "v#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm}" - s.subspec "common" do |ss| - ss.source_files = "common/cpp/**/*.{cpp,h}" - ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/common/cpp\"" } - end + s.source_files = "ios/**/*.{h,m,mm,swift}" + + s.dependency "React-Core" if defined?(install_modules_dependencies) install_modules_dependencies(s) else - s.dependency 'React-Core' - s.compiler_flags = "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32 -DRCT_NEW_ARCH_ENABLED=1" - s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", - "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", - "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" + s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", + "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" } s.dependency "React-RCTFabric" s.dependency "React-Codegen" diff --git a/package/react-native.config.js b/package/react-native.config.js index ffcb1e61..97d077e1 100644 --- a/package/react-native.config.js +++ b/package/react-native.config.js @@ -2,8 +2,8 @@ module.exports = { dependency: { platforms: { android: { - libraryName: 'RNCSlider', - componentDescriptors: ['RNCSliderComponentDescriptor'], + libraryName: 'Slider', + componentDescriptors: ['SliderViewComponentDescriptor'], cmakeListsPath: 'src/main/jni/CMakeLists.txt', }, }, diff --git a/package/src/RNCSliderNativeComponent.web.tsx b/package/src/RNCSliderNativeComponent.web.tsx deleted file mode 100644 index e11e1853..00000000 --- a/package/src/RNCSliderNativeComponent.web.tsx +++ /dev/null @@ -1,398 +0,0 @@ -import React, {RefObject, useCallback} from 'react'; -import { - Animated, - View, - ColorValue, - ViewStyle, - GestureResponderEvent, - LayoutChangeEvent, - Image, - ImageSourcePropType, -} from 'react-native'; -//@ts-ignore -import type {ImageSource} from 'react-native/Libraries/Image/ImageSource'; -import {constants} from './utils/constants'; - -type Event = Readonly<{ - nativeEvent: { - value: number; - }; -}>; - -type AnimationValues = { - val: Animated.Value; - min: Animated.Value; - max: Animated.Value; - diff: Animated.Value; -}; - -export interface Props { - value: number; - minimumValue: number; - maximumValue: number; - lowerLimit?: number; - upperLimit?: number; - step: number; - minimumTrackTintColor: ColorValue; - maximumTrackTintColor: ColorValue; - thumbTintColor: ColorValue; - thumbStyle: ViewStyle; - style: ViewStyle; - inverted: boolean; - disabled: boolean; - trackHeight: number; - thumbImage?: ImageSource; - onRNCSliderSlidingStart: (event: Event) => void; - onRNCSliderSlidingComplete: (event: Event) => void; - onRNCSliderValueChange: (event: Event) => void; -} - -const valueToEvent = (value: number): Event => ({nativeEvent: {value}}); - -const RCTSliderWebComponent = React.forwardRef( - ( - { - value: initialValue = 0, - minimumValue = 0, - maximumValue = 0, - lowerLimit = 0, - upperLimit = 0, - step = 1, - minimumTrackTintColor = '#009688', - maximumTrackTintColor = '#939393', - thumbTintColor = '#009688', - thumbStyle = {}, - style = {}, - inverted = false, - disabled = false, - trackHeight = 4, - thumbImage, - onRNCSliderSlidingStart = (_: Event) => {}, - onRNCSliderSlidingComplete = (_: Event) => {}, - onRNCSliderValueChange = (_: Event) => {}, - ...others - }: Props, - forwardedRef: any, - ) => { - const containerSize = React.useRef({width: 0, height: 0}); - const containerPositionX = React.useRef(0); - const containerRef = forwardedRef || React.createRef(); - const containerPositionInvalidated = React.useRef(false); - const [value, setValue] = React.useState(initialValue || minimumValue); - const lastInitialValue = React.useRef(0); - const animationValues = React.useRef({ - val: new Animated.Value(value), - min: new Animated.Value(minimumValue), - max: new Animated.Value(maximumValue), - // make sure we never divide by 0 - diff: new Animated.Value(maximumValue - minimumValue || 1), - }).current; - - // update minimumValue & maximumValue animations - React.useEffect(() => { - animationValues.min.setValue(minimumValue); - animationValues.max.setValue(maximumValue); - // make sure we never divide by 0 - animationValues.diff.setValue(maximumValue - minimumValue || 1); - }, [animationValues, minimumValue, maximumValue]); - - // compute animated slider position based on animated value - const minPercent = React.useRef( - Animated.multiply( - new Animated.Value(100), - Animated.divide( - Animated.subtract(animationValues.val, animationValues.min), - animationValues.diff, - ), - ), - ).current; - const maxPercent = React.useRef( - Animated.subtract(new Animated.Value(100), minPercent), - ).current; - - const onValueChange = useCallback( - (value: number) => { - onRNCSliderValueChange && onRNCSliderValueChange(valueToEvent(value)); - }, - [onRNCSliderValueChange], - ); - - const onSlidingStart = useCallback( - (value: number) => { - isUserInteracting.current = true; - onRNCSliderSlidingStart && onRNCSliderSlidingStart(valueToEvent(value)); - }, - [onRNCSliderSlidingStart], - ); - - const onSlidingComplete = useCallback( - (value: number) => { - isUserInteracting.current = false; - onRNCSliderSlidingComplete && - onRNCSliderSlidingComplete(valueToEvent(value)); - }, - [onRNCSliderSlidingComplete], - ); - // Add a ref to track user interaction - const isUserInteracting = React.useRef(false); - const updateValue = useCallback( - (newValue: number) => { - // Ensure that the value is correctly rounded - const hardRounded = - decimalPrecision.current < 20 - ? Number.parseFloat(newValue.toFixed(decimalPrecision.current)) - : newValue; - - // Ensure that the new value is still between the bounds - const withinBounds = Math.max( - minimumValue, - Math.min(hardRounded, maximumValue), - ); - if (value !== withinBounds) { - setValue(withinBounds); - if (isUserInteracting.current) { - onValueChange(withinBounds); - } - return withinBounds; - } - return hardRounded; - }, - [minimumValue, maximumValue, value, onValueChange], - ); - - React.useLayoutEffect(() => { - // we have to do this check because `initialValue` gets default to `0` by - // Slider. If we don't this will get called every time `value` changes - // as `updateValue` is mutated when value changes. The result of not - // checking this is that the value constantly gets reset to `0` in - // contexts where `value` is not managed externally. - if (initialValue !== lastInitialValue.current) { - lastInitialValue.current = initialValue; - const newValue = updateValue(initialValue); - animationValues.val.setValue(newValue); - } - }, [initialValue, updateValue, animationValues]); - - React.useEffect(() => { - const invalidateContainerPosition = () => { - containerPositionInvalidated.current = true; - }; - const onDocumentScroll = (e: Event) => { - const isAlreadyInvalidated = !containerPositionInvalidated.current; - if ( - isAlreadyInvalidated && - containerRef.current && - // @ts-ignore - e.target.contains(containerRef.current) - ) { - invalidateContainerPosition(); - } - }; - //@ts-ignore - window.addEventListener('resize', invalidateContainerPosition); - //@ts-ignore - document.addEventListener('scroll', onDocumentScroll, {capture: true}); - - return () => { - //@ts-ignore - window.removeEventListener('resize', invalidateContainerPosition); - - //@ts-ignore - document.removeEventListener('scroll', onDocumentScroll, { - capture: true, - }); - }; - }, [containerRef]); - - const containerStyle = [ - { - flexGrow: 1, - flexShrink: 1, - flexBasis: 'auto', - flexDirection: 'row', - alignItems: 'center', - }, - style, - ] as ViewStyle[]; - - const trackStyle = { - height: trackHeight, - borderRadius: trackHeight / 2, - userSelect: 'none', - }; - - const minimumTrackStyle = { - ...trackStyle, - backgroundColor: minimumTrackTintColor, - flexGrow: minPercent, - }; - - const maximumTrackStyle = { - ...trackStyle, - backgroundColor: maximumTrackTintColor, - flexGrow: maxPercent, - }; - - const thumbSize = constants.THUMB_SIZE; - const thumbViewStyle = [ - { - width: thumbSize, - height: thumbSize, - backgroundColor: thumbTintColor, - zIndex: 1, - borderRadius: thumbSize / 2, - overflow: 'hidden', - }, - thumbStyle, - ] as ViewStyle[]; - - const decimalPrecision = React.useRef( - calculatePrecision(minimumValue, maximumValue, step), - ); - React.useEffect(() => { - decimalPrecision.current = calculatePrecision( - minimumValue, - maximumValue, - step, - ); - }, [maximumValue, minimumValue, step]); - - const updateContainerPositionX = () => { - const positionX = ( - containerRef as RefObject - ).current?.getBoundingClientRect().x; - containerPositionX.current = positionX ?? 0; - }; - - const getValueFromNativeEvent = (pageX: number) => { - const adjustForThumbSize = (containerSize.current.width || 1) > thumbSize; - const width = - (containerSize.current.width || 1) - - (adjustForThumbSize ? thumbSize : 0); - - if (containerPositionInvalidated.current) { - containerPositionInvalidated.current = false; - updateContainerPositionX(); - } - - const containerX = - containerPositionX.current + (adjustForThumbSize ? thumbSize / 2 : 0); - const lowerValue = minimumValue < lowerLimit ? lowerLimit : minimumValue; - const upperValue = maximumValue > upperLimit ? upperLimit : maximumValue; - - if (pageX < containerX) { - return inverted ? upperValue : lowerValue; - } else if (pageX > containerX + width) { - return inverted ? lowerValue : upperValue; - } else { - const x = pageX - containerX; - const newValue = inverted - ? maximumValue - ((maximumValue - minimumValue) * x) / width - : minimumValue + ((maximumValue - minimumValue) * x) / width; - - const valueAfterStep = step - ? Math.round(newValue / step) * step - : newValue; - const valueAfterLowerLimit = - valueAfterStep < lowerLimit ? lowerLimit : valueAfterStep; - const valueInLimitRange = - valueAfterLowerLimit > upperLimit ? upperLimit : valueAfterLowerLimit; - return valueInLimitRange; - } - }; - - const onTouchEnd = ({nativeEvent}: GestureResponderEvent) => { - const newValue = updateValue(getValueFromNativeEvent(nativeEvent.pageX)); - animationValues.val.setValue(newValue); - onSlidingComplete(newValue); - }; - - const onMove = ({nativeEvent}: GestureResponderEvent) => { - const newValue = getValueFromNativeEvent(nativeEvent.pageX); - animationValues.val.setValue(newValue); - updateValue(newValue); - }; - - const accessibilityActions = (event: any) => { - const tenth = (maximumValue - minimumValue) / 10; - switch (event.nativeEvent.actionName) { - case 'increment': - updateValue(value + (step || tenth)); - break; - case 'decrement': - updateValue(value - (step || tenth)); - break; - } - }; - - React.useImperativeHandle( - forwardedRef, - () => ({ - updateValue: (val: number) => { - updateValue(val); - }, - }), - [updateValue], - ); - - return ( - { - containerSize.current.height = layout.height; - containerSize.current.width = layout.width; - if ((containerRef as RefObject).current) { - updateContainerPositionX(); - } - }} - accessibilityActions={[ - {name: 'increment', label: 'increment'}, - {name: 'decrement', label: 'decrement'}, - ]} - onAccessibilityAction={accessibilityActions} - accessible={true} - accessibilityRole={'adjustable'} - style={containerStyle} - {...others} - // NOTE: gesture responders should all fall _after_ the {...others} - // spread operator, or they may not work appropriately. - onStartShouldSetResponder={() => !disabled} - onMoveShouldSetResponder={() => !disabled} - onResponderGrant={() => onSlidingStart(value)} - onResponderRelease={onTouchEnd} - onResponderMove={onMove}> - - - {thumbImage !== undefined ? ( - - ) : null} - - - - ); - }, -); - -// We should round number with the same precision as the min, max or step values if provided -function calculatePrecision( - minimumValue: number, - maximumValue: number, - step: number, -) { - if (!step) { - return Infinity; - } else { - // Calculate the number of decimals we can encounter in the results - const decimals = [minimumValue, maximumValue, step].map( - (value) => ((value + '').split('.').pop() || '').length, - ); - return Math.max(...decimals); - } -} - -RCTSliderWebComponent.displayName = 'RTCSliderWebComponent'; - -export default RCTSliderWebComponent; diff --git a/package/src/Slider.tsx b/package/src/Slider.tsx index c452ef9c..defb5663 100644 --- a/package/src/Slider.tsx +++ b/package/src/Slider.tsx @@ -1,356 +1,312 @@ -import React, {useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import { + AccessibilityActionEvent, Image, + type ColorValue, Platform, - AccessibilityActionEvent, - ViewProps, - ViewStyle, - ColorValue, - NativeSyntheticEvent, - StyleProp, + StyleSheet, + type NativeSyntheticEvent, + type ImageSource, + type ImageSourcePropType, + type StyleProp, View, - ImageSource, - ImageSourcePropType, + type ViewProps, + type ViewStyle, } from 'react-native'; -import RCTSliderNativeComponent from './index'; -import type {FC, Ref} from 'react'; -import {MarkerProps} from './components/TrackMark'; +import SliderNativeComponent, { + type NativeSliderProps, + type SliderChangeEvent, +} from './SliderNativeComponent'; import {StepsIndicator} from './components/StepsIndicator'; -import {styles} from './utils/styles'; +import type {MarkerProps} from './components/TrackMark'; import {constants} from './utils/constants'; - -type Event = NativeSyntheticEvent< - Readonly<{ - value: number; - /** - * Android Only. - */ - fromUser?: boolean; - }> ->; - -type WindowsProps = Readonly<{ - /** - * If true the slider will be inverted. - * Default value is false. - */ - vertical?: boolean; -}>; +import {styles as sliderStyles} from './utils/styles'; type IOSProps = Readonly<{ - /** - * Assigns a single image for the track. Only static images are supported. - * The center pixel of the image will be stretched to fill the track. - */ trackImage?: ImageSource; - - /** - * Assigns a minimum track image. Only static images are supported. The - * rightmost pixel of the image will be stretched to fill the track. - */ minimumTrackImage?: ImageSource; - - /** - * Assigns a maximum track image. Only static images are supported. The - * leftmost pixel of the image will be stretched to fill the track. - */ maximumTrackImage?: ImageSource; - - /** - * Permits tapping on the slider track to set the thumb position. - * Defaults to false on iOS. No effect on Android or Windows. - */ tapToSeek?: boolean; }>; -type Props = ViewProps & +type WindowsProps = Readonly<{ + vertical?: boolean; +}>; + +export type SliderProps = ViewProps & IOSProps & WindowsProps & Readonly<{ - /** - * Used to style and layout the `Slider`. See `StyleSheet.js` and - * `DeprecatedViewStylePropTypes.js` for more info. - */ style?: StyleProp; - - /** - * 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. - * 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. - */ value?: number; - - /** - * Step value of the slider. The value should be - * between 0 and (maximumValue - minimumValue). - * Default value is 0. - */ step?: number; - - /** - * Initial minimum value of the slider. Default value is 0. - */ minimumValue?: number; - - /** - * Initial maximum value of the slider. Default value is 1. - */ maximumValue?: number; - - /** - * The lower limit value of the slider. The user won't be able to slide below this limit. - */ + minValue?: number; + maxValue?: number; lowerLimit?: number; - - /** - * The upper limit value of the slider. The user won't be able to slide above this limit. - */ upperLimit?: number; - - /** - * The color used for the track to the left of the button. - * Overrides the default blue gradient image on iOS. - */ minimumTrackTintColor?: ColorValue; - - /** - * The color used for the track to the right of the button. - * Overrides the default blue gradient image on iOS. - */ maximumTrackTintColor?: ColorValue; - /** - * The color used to tint the default thumb images on iOS, or the - * color of the foreground switch grip on Android. - */ thumbTintColor?: ColorValue; - - /** - * If true the user won't be able to move the slider. - * Default value is false. - */ disabled?: boolean; - - /** - * Callback continuously called while the user is dragging the slider. - */ - onValueChange?: (_value: number) => void; - - /** - * Callback that is called when the user touches the slider, - * regardless if the value has changed. The current value is passed - * as an argument to the callback handler. - */ - onSlidingStart?: (_value: number) => void; - - /** - * Callback that is called when the user releases the slider, - * regardless if the value has changed. The current value is passed - * as an argument to the callback handler. - */ - onSlidingComplete?: (_value: number) => void; - - /** - * Used to locate this view in UI automation tests. - */ - testID?: string; - - /** - * Sets an image for the thumb. Only static images are supported. - */ + onValueChange?: (value: number) => void; + onSlidingStart?: (value: number) => void; + onSlidingComplete?: (value: number) => void; thumbImage?: ImageSource; - - /** - * Sets the size (width and height) of the thumb. - * If `thumbImage` is provided, it will be scaled to this size. - */ thumbSize?: number; - - /** - * If true the slider will be inverted. - * Default value is false. - */ inverted?: boolean; - - /** - * Component to be rendered for each step indicator. - */ - StepMarker?: FC; - - /** - * - */ + StepMarker?: React.FC; renderStepNumber?: boolean; - - /** - * A string of one or more words to be announced by the screen reader. - * Otherwise, it will announce the value as a percentage. - * Requires passing a value to `accessibilityIncrements` to work correctly. - * Should be a plural word, as singular units will be handled. - */ accessibilityUnits?: string; - - /** - * An array of values that represent the different increments displayed - * by the slider. All the values passed into this prop must be strings. - * Requires passing a value to `accessibilityUnits` to work correctly. - * The number of elements must be the same as `maximumValue`. - */ accessibilityIncrements?: Array; }>; -const SliderComponent = ( - { - onValueChange, - onSlidingStart, - onSlidingComplete, - onAccessibilityAction, - value = constants.SLIDER_DEFAULT_INITIAL_VALUE, - minimumValue = 0, - maximumValue = 1, - step = 0, - inverted = false, - tapToSeek = false, - lowerLimit = Platform.select({ - web: minimumValue, - default: constants.LIMIT_MIN_VALUE, - }), - upperLimit = Platform.select({ - web: maximumValue, - default: constants.LIMIT_MAX_VALUE, - }), - ...props - }: Props, - forwardedRef?: Ref, -) => { - const [currentValue, setCurrentValue] = useState( - value ?? minimumValue ?? constants.SLIDER_DEFAULT_INITIAL_VALUE, - ); - const [width, setWidth] = useState(0); - - const stepResolution = step ? step : constants.DEFAULT_STEP_RESOLUTION; +const clamp = (nextValue: number, minimumValue: number, maximumValue: number) => + Math.min(Math.max(nextValue, minimumValue), maximumValue); - const defaultStep = (maximumValue - minimumValue) / stepResolution; - const stepLength = step || defaultStep; +const resolveImageSource = (source?: ImageSource) => + source ? Image.resolveAssetSource(source as ImageSourcePropType) : undefined; - const options = Array.from( +export const Slider = React.forwardRef< + React.ElementRef, + SliderProps +>( + ( { - length: (step ? defaultStep : stepResolution) + 1, + minValue, + maxValue, + minimumValue = minValue ?? 0, + maximumValue = maxValue ?? 1, + value = constants.SLIDER_DEFAULT_INITIAL_VALUE, + lowerLimit = Platform.select({ + web: minimumValue, + default: constants.LIMIT_MIN_VALUE, + }), + upperLimit = Platform.select({ + web: maximumValue, + default: constants.LIMIT_MAX_VALUE, + }), + step = 0, + inverted = false, + tapToSeek = false, + onValueChange, + onSlidingStart, + onSlidingComplete, + onAccessibilityAction, + style, + StepMarker, + renderStepNumber, + accessibilityState, + trackImage, + minimumTrackImage, + maximumTrackImage, + thumbImage, + thumbSize, + thumbTintColor, + ...props }, - (_, index) => minimumValue + index * stepLength, - ); - - const defaultStyle = - Platform.OS === 'ios' ? styles.defaultSlideriOS : styles.defaultSlider; - const sliderStyle = {zIndex: 1, width: width}; - const style = [defaultStyle, props.style]; - - const onValueChangeEvent = (event: Event) => { - onValueChange && onValueChange(event.nativeEvent.value); - setCurrentValue(event.nativeEvent.value); - }; - - const _disabled = - typeof props.disabled === 'boolean' - ? props.disabled - : props.accessibilityState?.disabled === true; - - const _accessibilityState = - typeof props.disabled === 'boolean' - ? {...props.accessibilityState, disabled: props.disabled} - : props.accessibilityState; - - const onSlidingStartEvent = onSlidingStart - ? (event: Event) => { - onSlidingStart(event.nativeEvent.value); - } - : null; - const onSlidingCompleteEvent = onSlidingComplete - ? (event: Event) => { - onSlidingComplete(event.nativeEvent.value); + forwardedRef, + ) => { + const [currentValue, setCurrentValue] = useState( + clamp(value ?? minimumValue, minimumValue, maximumValue), + ); + const [width, setWidth] = useState(0); + + useEffect(() => { + if (lowerLimit >= upperLimit) { + console.warn( + 'Invalid configuration: lower limit is supposed to be smaller than upper limit', + ); } - : null; - const onAccessibilityActionEvent = onAccessibilityAction - ? (event: AccessibilityActionEvent) => { - onAccessibilityAction(event); + }, [lowerLimit, upperLimit]); + + useEffect(() => { + setCurrentValue(clamp(value ?? minimumValue, minimumValue, maximumValue)); + }, [maximumValue, minimumValue, value]); + + const stepResolution = step || constants.DEFAULT_STEP_RESOLUTION; + const defaultStep = (maximumValue - minimumValue) / stepResolution; + const stepLength = step || defaultStep; + const options = useMemo( + () => + Array.from( + { + length: + Math.max(Math.round(step ? defaultStep : stepResolution), 0) + 1, + }, + (_, index) => minimumValue + index * stepLength, + ), + [defaultStep, minimumValue, step, stepLength, stepResolution], + ); + + const nativeStep = useMemo(() => { + if (!step) { + return 0; } - : null; - - const passedValue = Number.isNaN(value) || !value ? undefined : value; - useEffect(() => { - if (lowerLimit >= upperLimit) { - console.warn( - 'Invalid configuration: lower limit is supposed to be smaller than upper limit', - ); - } - }, [lowerLimit, upperLimit]); + if (Platform.OS === 'android') { + return Math.max( + Math.round(Math.abs((maximumValue - minimumValue) / step)) - 1, + 0, + ); + } - return ( - { - setWidth(event.nativeEvent.layout.width); - }} - style={[style, {justifyContent: 'center'}]}> - {props.StepMarker || !!props.renderStepNumber ? ( - - ) : null} - ) => { + const nextValue = event.nativeEvent.value; + setCurrentValue(nextValue); + onValueChange?.(nextValue); + }, + [onValueChange], + ); + + const handleSlidingStart = useCallback( + (event: NativeSyntheticEvent) => { + onSlidingStart?.(event.nativeEvent.value); + }, + [onSlidingStart], + ); + + const handleSlidingComplete = useCallback( + (event: NativeSyntheticEvent) => { + onSlidingComplete?.(event.nativeEvent.value); + }, + [onSlidingComplete], + ); + + const handleAccessibilityAction = onAccessibilityAction + ? (event: AccessibilityActionEvent) => { + onAccessibilityAction(event); } - ref={forwardedRef} + : undefined; + + const disabled = + typeof props.disabled === 'boolean' + ? props.disabled + : accessibilityState?.disabled === true; + + const nextAccessibilityState = + typeof props.disabled === 'boolean' + ? {...accessibilityState, disabled: props.disabled} + : accessibilityState; + + const defaultStyle = + Platform.OS === 'ios' + ? sliderStyles.defaultSlideriOS + : sliderStyles.defaultSlider; + + const shouldRenderStepOverlay = !!StepMarker || !!renderStepNumber; + const shouldRenderCustomStepMarker = !!StepMarker; + + const resolvedTrackImage = + Platform.OS === 'web' ? trackImage : resolveImageSource(trackImage); + const resolvedMinimumTrackImage = + Platform.OS === 'web' + ? minimumTrackImage + : resolveImageSource(minimumTrackImage); + const resolvedMaximumTrackImage = + Platform.OS === 'web' + ? maximumTrackImage + : resolveImageSource(maximumTrackImage); + const resolvedThumbImage = + Platform.OS === 'web' || shouldRenderStepOverlay + ? thumbImage + : resolveImageSource(thumbImage); + + return ( + { + setWidth(event.nativeEvent.layout.width); + }} style={[ - sliderStyle, defaultStyle, - {alignContent: 'center', alignItems: 'center'}, - ]} - onChange={onValueChangeEvent} - onRNCSliderSlidingStart={onSlidingStartEvent} - onRNCSliderSlidingComplete={onSlidingCompleteEvent} - onRNCSliderValueChange={onValueChangeEvent} - disabled={_disabled} - onStartShouldSetResponder={() => true} - onResponderTerminationRequest={() => false} - onRNCSliderAccessibilityAction={onAccessibilityActionEvent} - thumbTintColor={ - props.thumbImage && !!props.StepMarker - ? 'transparent' - : props.thumbTintColor - } - /> - - ); -}; + style, + {justifyContent: 'center', overflow: 'visible'}, + ]}> + {shouldRenderStepOverlay ? ( + + ) : null} + )} + accessibilityState={nextAccessibilityState} + disabled={disabled} + inverted={inverted} + tapToSeek={tapToSeek} + minValue={minimumValue} + maxValue={maximumValue} + lowerLimit={lowerLimit} + upperLimit={upperLimit} + value={Number.isNaN(value) ? undefined : value} + step={nativeStep} + trackImage={resolvedTrackImage} + minimumTrackImage={resolvedMinimumTrackImage} + maximumTrackImage={resolvedMaximumTrackImage} + thumbImage={ + shouldRenderCustomStepMarker ? undefined : resolvedThumbImage + } + thumbSize={thumbSize} + thumbTintColor={ + shouldRenderCustomStepMarker || + (thumbImage && shouldRenderStepOverlay) + ? 'transparent' + : thumbTintColor + } + ref={forwardedRef} + onValueChange={handleValueChange} + onSlidingStart={onSlidingStart ? handleSlidingStart : undefined} + onSlidingComplete={ + onSlidingComplete ? handleSlidingComplete : undefined + } + onAccessibilityAction={handleAccessibilityAction} + onStartShouldSetResponder={() => true} + onResponderTerminationRequest={() => false} + style={[ + styles.base, + defaultStyle, + { + alignContent: 'center', + alignItems: 'center', + overflow: 'visible', + width, + }, + ]} + /> + + ); + }, +); + +export type {MarkerProps}; + +Slider.displayName = 'Slider'; -const SliderWithRef = React.forwardRef(SliderComponent); +export default Slider; -export default SliderWithRef; +const styles = StyleSheet.create({ + base: { + minHeight: 40, + }, +}); diff --git a/package/src/RNCSliderNativeComponent.ts b/package/src/SliderNativeComponent.ts similarity index 52% rename from package/src/RNCSliderNativeComponent.ts rename to package/src/SliderNativeComponent.ts index 62f97abd..dcc67a65 100644 --- a/package/src/RNCSliderNativeComponent.ts +++ b/package/src/SliderNativeComponent.ts @@ -1,19 +1,23 @@ -import type {ColorValue, HostComponent, ViewProps} from 'react-native'; -import {ImageSource, codegenNativeComponent} from 'react-native'; +import { + type ColorValue, + codegenNativeComponent, + type HostComponent, + type ImageSource, + type ViewProps, +} from 'react-native'; import type { - Float, - WithDefault, DirectEventHandler, - BubblingEventHandler, Double, + Float, + WithDefault, } from 'react-native/Libraries/Types/CodegenTypes'; -type Event = Readonly<{ - value: Float; +export type SliderChangeEvent = Readonly<{ + value: Double; fromUser?: boolean; }>; -export interface NativeProps extends ViewProps { +export interface NativeSliderProps extends ViewProps { accessibilityUnits?: string; accessibilityIncrements?: ReadonlyArray; disabled?: WithDefault; @@ -22,25 +26,23 @@ export interface NativeProps extends ViewProps { tapToSeek?: WithDefault; maximumTrackImage?: ImageSource; maximumTrackTintColor?: ColorValue; - maximumValue?: Double; minimumTrackImage?: ImageSource; minimumTrackTintColor?: ColorValue; - minimumValue?: Double; - onChange?: BubblingEventHandler; - onRNCSliderSlidingStart?: DirectEventHandler; - onRNCSliderSlidingComplete?: DirectEventHandler; - onRNCSliderValueChange?: BubblingEventHandler; - step?: Double; - testID?: string; thumbImage?: ImageSource; thumbTintColor?: ColorValue; thumbSize?: Float; trackImage?: ImageSource; + step?: Double; + minValue?: Double; + maxValue?: Double; value?: Float; lowerLimit?: Float; upperLimit?: Float; + onValueChange?: DirectEventHandler | null; + onSlidingStart?: DirectEventHandler | null; + onSlidingComplete?: DirectEventHandler | null; } -export default codegenNativeComponent('RNCSlider', { - interfaceOnly: true, -}) as HostComponent; +export default codegenNativeComponent( + 'SliderView', +) as HostComponent; diff --git a/package/src/SliderNativeComponent.web.tsx b/package/src/SliderNativeComponent.web.tsx new file mode 100644 index 00000000..ac553bc5 --- /dev/null +++ b/package/src/SliderNativeComponent.web.tsx @@ -0,0 +1,273 @@ +import React, { + useCallback, + useEffect, + useState, + type ForwardedRef, +} from 'react'; +import { + Image, + View, + type ColorValue, + type GestureResponderEvent, + type HostComponent, + type ImageSourcePropType, + type LayoutChangeEvent, +} from 'react-native'; + +import type {NativeSliderProps} from './SliderNativeComponent'; + +const TRACK_HEIGHT = 4; +const THUMB_SIZE = 20; + +const clamp = (value: number, minValue: number, maxValue: number) => + Math.min(Math.max(value, minValue), maxValue); + +const snapToStep = (value: number, minValue: number, step?: number) => { + if (!step) { + return value; + } + + return minValue + Math.round((value - minValue) / step) * step; +}; + +const getImageUri = (source: unknown): string | undefined => { + if (!source) { + return undefined; + } + + if (typeof source === 'string') { + return source; + } + + if (Array.isArray(source)) { + return getImageUri(source[0]); + } + + if (typeof source === 'object') { + const imageSource = source as {default?: unknown; uri?: unknown}; + + if (typeof imageSource.uri === 'string') { + return imageSource.uri; + } + + return getImageUri(imageSource.default); + } + + return undefined; +}; + +const getTrackImageStyle = (source: unknown, width: number) => { + const uri = getImageUri(source); + + if (!uri) { + return undefined; + } + + return { + backgroundImage: `url(${JSON.stringify(uri)})`, + backgroundPosition: 'left center', + backgroundRepeat: 'no-repeat', + backgroundSize: `${Math.max(width, 1)}px 100%`, + } as never; +}; + +const SliderNativeComponent = React.forwardRef( + ( + { + minValue = 0, + maxValue = 1, + value: valueProp, + step, + onValueChange, + onSlidingStart, + onSlidingComplete, + onLayout, + style, + disabled, + inverted, + lowerLimit = minValue, + upperLimit = maxValue, + minimumTrackTintColor = '#007aff', + maximumTrackTintColor = '#b3b3b3', + thumbTintColor = '#ffffff', + accessibilityIncrements: _accessibilityIncrements, + accessibilityUnits: _accessibilityUnits, + maximumTrackImage, + minimumTrackImage, + tapToSeek: _tapToSeek, + thumbImage, + thumbSize: thumbSizeProp, + trackImage, + vertical: _vertical, + ...props + }: NativeSliderProps, + ref: ForwardedRef, + ) => { + const [width, setWidth] = useState(0); + const [value, setValue] = useState(valueProp ?? minValue); + const thumbSize = thumbSizeProp ?? (thumbImage ? 48 : THUMB_SIZE); + + useEffect(() => { + setValue(valueProp ?? minValue); + }, [minValue, valueProp]); + + const updateValue = useCallback( + (event: GestureResponderEvent) => { + if (disabled) { + return; + } + + const nextPercent = width > 0 ? event.nativeEvent.locationX / width : 0; + const percent = inverted ? 1 - nextPercent : nextPercent; + const nextValue = clamp( + snapToStep( + minValue + percent * (maxValue - minValue), + minValue, + step, + ), + lowerLimit, + upperLimit, + ); + + setValue(nextValue); + onValueChange?.({nativeEvent: {value: nextValue}} as never); + }, + [ + disabled, + inverted, + lowerLimit, + maxValue, + minValue, + onValueChange, + step, + upperLimit, + width, + ], + ); + + const startSliding = useCallback( + (event: GestureResponderEvent) => { + if (disabled) { + return; + } + + onSlidingStart?.({nativeEvent: {value}} as never); + updateValue(event); + }, + [disabled, onSlidingStart, updateValue, value], + ); + + const completeSliding = useCallback(() => { + if (disabled) { + return; + } + + onSlidingComplete?.({nativeEvent: {value}} as never); + }, [disabled, onSlidingComplete, value]); + + const handleLayout = useCallback( + (event: LayoutChangeEvent) => { + setWidth(event.nativeEvent.layout.width); + onLayout?.(event); + }, + [onLayout], + ); + + const percent = + maxValue === minValue + ? 0 + : ((value - minValue) / (maxValue - minValue)) * 100; + const renderedPercent = inverted ? 100 - percent : percent; + const hasTrackImage = !!getImageUri(trackImage); + const maximumTrackImageStyle = getTrackImageStyle( + maximumTrackImage ?? trackImage, + width, + ); + const minimumTrackImageStyle = getTrackImageStyle(minimumTrackImage, width); + const trackHeight = + maximumTrackImageStyle || minimumTrackImageStyle || hasTrackImage + ? 20 + : TRACK_HEIGHT; + + return ( + true} + onMoveShouldSetResponder={() => true} + onResponderGrant={startSliding} + onResponderMove={updateValue} + onResponderRelease={completeSliding} + onResponderTerminate={completeSliding}> + + + + {thumbImage ? ( + + ) : ( + + )} + + ); + }, +); + +export default SliderNativeComponent as unknown as HostComponent; diff --git a/package/src/components/StepNumber.tsx b/package/src/components/StepNumber.tsx index 09f38c03..d7e8e5f0 100644 --- a/package/src/components/StepNumber.tsx +++ b/package/src/components/StepNumber.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {StyleProp, Text, TextStyle, View} from 'react-native'; +import {type StyleProp, Text, type TextStyle, View} from 'react-native'; + import {styles} from '../utils/styles'; export const StepNumber = ({ diff --git a/package/src/components/StepsIndicator.tsx b/package/src/components/StepsIndicator.tsx index 0ac98316..11a61893 100644 --- a/package/src/components/StepsIndicator.tsx +++ b/package/src/components/StepsIndicator.tsx @@ -1,11 +1,11 @@ -import React, {FC, Fragment, useCallback, useMemo} from 'react'; +import React, {type FC, Fragment, useCallback, useMemo} from 'react'; import {Platform, View} from 'react-native'; -import {StepNumber} from './StepNumber'; -import {MarkerProps, SliderTrackMark} from './TrackMark'; -//@ts-ignore import type {ImageSource} from 'react-native/Libraries/Image/ImageSource'; -import {styles} from '../utils/styles'; + +import {StepNumber} from './StepNumber'; +import {type MarkerProps, SliderTrackMark} from './TrackMark'; import {constants} from '../utils/constants'; +import {styles} from '../utils/styles'; export const StepsIndicator = ({ options, @@ -15,6 +15,7 @@ export const StepsIndicator = ({ renderStepNumber, thumbImage, isLTR, + thumbSize, }: { options: number[]; sliderWidth: number; @@ -23,6 +24,7 @@ export const StepsIndicator = ({ renderStepNumber?: boolean; thumbImage?: ImageSource; isLTR?: boolean; + thumbSize?: number; }) => { const stepNumberFontStyle = useMemo(() => { return { @@ -35,12 +37,24 @@ export const StepsIndicator = ({ const platformDependentStyles = useMemo(() => { const isWeb = Platform.OS === 'web'; + const isIOS = Platform.OS === 'ios'; + const trackInset = isIOS + ? (thumbSize || constants.IOS_DEFAULT_THUMB_SIZE) / 2 + : sliderWidth * constants.MARGIN_HORIZONTAL_PADDING; + return { stepIndicatorContainerStyle: isWeb ? styles.stepsIndicator + : isIOS + ? { + ...styles.stepsIndicator, + left: trackInset, + position: 'absolute' as const, + right: trackInset, + } : { ...styles.stepsIndicator, - marginHorizontal: sliderWidth * constants.MARGIN_HORIZONTAL_PADDING, + marginHorizontal: trackInset, }, stepIndicatorElementStyle: isWeb ? { @@ -50,9 +64,12 @@ export const StepsIndicator = ({ } : styles.stepIndicatorElement, }; - }, [sliderWidth]); + }, [sliderWidth, thumbSize]); - const values = isLTR ? options.reverse() : options; + const values = useMemo( + () => (isLTR ? [...options].reverse() : options), + [isLTR, options], + ); const renderStepIndicator = useCallback( (i: number, index: number) => { @@ -96,9 +113,11 @@ export const StepsIndicator = ({ return ( + style={[ + platformDependentStyles.stepIndicatorContainerStyle, + {pointerEvents: 'none'}, + ]}> {values.map((i, index) => renderStepIndicator(i, index))} ); diff --git a/package/src/components/TrackMark.tsx b/package/src/components/TrackMark.tsx index 21a096e2..3ee3af1c 100644 --- a/package/src/components/TrackMark.tsx +++ b/package/src/components/TrackMark.tsx @@ -1,5 +1,7 @@ -import React, {FC} from 'react'; -import {Image, ImageSource, ImageSourcePropType, View} from 'react-native'; +import React, {type FC} from 'react'; +import {Image, type ImageSourcePropType, View} from 'react-native'; +import type {ImageSource} from 'react-native/Libraries/Image/ImageSource'; + import {styles} from '../utils/styles'; export type MarkerProps = { diff --git a/package/src/index.ts b/package/src/index.ts index ebd6e915..c255d266 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -1,3 +1,4 @@ -const RNCSlider = require('./RNCSliderNativeComponent').default; +import Slider from './Slider'; -export default RNCSlider; +export type {MarkerProps, SliderProps} from './Slider'; +export default Slider; diff --git a/package/src/utils/constants.ts b/package/src/utils/constants.ts index 72b26fbf..a1b75cdb 100644 --- a/package/src/utils/constants.ts +++ b/package/src/utils/constants.ts @@ -2,8 +2,8 @@ import {Platform} from 'react-native'; export const constants = { SLIDER_DEFAULT_INITIAL_VALUE: 0, + IOS_DEFAULT_THUMB_SIZE: 28, MARGIN_HORIZONTAL_PADDING: 0.05, - // Default thumb size for web platform (used in step indicator positioning) THUMB_SIZE: 20, STEP_NUMBER_TEXT_FONT_SMALL: 8, STEP_NUMBER_TEXT_FONT_BIG: 12, diff --git a/package/typings/index.d.ts b/package/typings/index.d.ts index 6903ca7a..f6166748 100644 --- a/package/typings/index.d.ts +++ b/package/typings/index.d.ts @@ -1,7 +1,7 @@ -import * as React from 'react'; -import { FC } from 'react'; -import * as ReactNative from 'react-native'; -import { ImageURISource } from 'react-native'; +import * as React from "react"; +import {type FC} from "react"; +import * as ReactNative from "react-native"; +import {type ImageURISource} from "react-native"; type Constructor = new (...args: any[]) => T; @@ -37,40 +37,16 @@ export type MarkerProps = { }; export interface SliderPropsIOS extends ReactNative.ViewProps { - /** - * Assigns a maximum track image. Only static images are supported. - * The leftmost pixel of the image will be stretched to fill the track. - */ maximumTrackImage?: ReactNative.ImageURISource; - - /** - * Assigns a minimum track image. Only static images are supported. - * The rightmost pixel of the image will be stretched to fill the track. - */ minimumTrackImage?: ReactNative.ImageURISource; - - /** - * Permits tapping on the slider track to set the thumb position. - * Defaults to false on iOS. No effect on Android or Windows. - */ tapToSeek?: boolean; - - /** - * Sets an image for the thumb. Only static images are supported. - */ thumbImage?: ReactNative.ImageURISource; - - /** - * Assigns a single image for the track. Only static images - * are supported. The center pixel of the image will be stretched - * to fill the track. - */ trackImage?: ReactNative.ImageURISource; } export interface SliderPropsWindows extends ReactNative.ViewProps { /** - * Controls the orientation of the slider, default value is 'false' (horizontal). + * Controls the orientation of the slider, default value is false. */ vertical?: boolean; } @@ -79,128 +55,28 @@ export interface SliderProps extends SliderPropsIOS, SliderPropsAndroid, SliderPropsWindows { - /** - * If true the user won't be able to move the slider. - * Default value is false. - */ disabled?: boolean; - - /** - * The color used for the track to the right of the button. - * Overrides the default blue gradient image. - */ maximumTrackTintColor?: string; - - /** - * Initial maximum value of the slider. Default value is 1. - */ maximumValue?: number; - - /** - * The lower limit value of the slider. The user won't be able to slide below this limit. - */ + maxValue?: number; lowerLimit?: number; - - /** - * The upper limit value of the slider. The user won't be able to slide above this limit. - */ upperLimit?: number; - - /** - * The color used for the track to the left of the button. - * Overrides the default blue gradient image. - */ minimumTrackTintColor?: string; - - /** - * Initial minimum value of the slider. Default value is 0. - */ minimumValue?: number; - - /** - * Callback that is called when the user picks up the slider. - * The initial value is passed as an argument to the callback handler. - */ + minValue?: number; onSlidingStart?: (value: number) => void; - - /** - * Callback called when the user finishes changing the value (e.g. when the slider is released). - */ onSlidingComplete?: (value: number) => void; - - /** - * Callback continuously called while the user is dragging the slider. - */ onValueChange?: (value: number) => void; - - /** - * Step value of the slider. The value should be between 0 and (maximumValue - minimumValue). Default value is 0. - */ step?: number; - - /** - * Used to style and layout the Slider. See StyleSheet.js and ViewStylePropTypes.js for more info. - */ style?: ReactNative.StyleProp; - - /** - * Used to locate this view in UI automation tests. - */ testID?: string; - - /** - * 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. - * 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. - */ value?: number; - - /** - * Reverses the direction of the slider. - */ inverted?: boolean; - - /** - * Sets the size (width and height) of the thumb. - * If `thumbImage` is provided, it will be scaled to this size. - */ thumbSize?: number; - - /** - * Component to be rendered for each step indicator. - */ StepMarker?: FC; - - /** - * - */ renderStepNumber?: boolean; - - /** - * A string of one or more words to be announced by the screen reader. - * Otherwise, it will announce the value as a percentage. - * Requires passing a value to `accessibilityIncrements` to work correctly. - * Should be a plural word, as singular units will be handled. - */ accessibilityUnits?: string; - - /** - * An array of values that represent the different increments displayed - * by the slider. All the values passed into this prop must be strings. - * Requires passing a value to `accessibilityUnits` to work correctly. - * The number of elements must be the same as `maximumValue`. - */ accessibilityIncrements?: Array; - - /** - * Reference object. - */ ref?: SliderReferenceType; } diff --git a/package/windows/SliderWindows/SliderViewManager.cpp b/package/windows/SliderWindows/SliderViewManager.cpp index 16ea5218..8f248de5 100644 --- a/package/windows/SliderWindows/SliderViewManager.cpp +++ b/package/windows/SliderWindows/SliderViewManager.cpp @@ -19,7 +19,7 @@ namespace winrt::SliderWindows::implementation { // IViewManager winrt::hstring SliderViewManager::Name() noexcept { - return L"RNCSlider"; + return L"Slider"; } xaml::FrameworkElement SliderViewManager::CreateView() noexcept { @@ -73,8 +73,8 @@ namespace winrt::SliderWindows::implementation { ConstantProviderDelegate SliderViewManager::ExportedCustomDirectEventTypeConstants() noexcept { return [](winrt::IJSValueWriter const& constantWriter) { WriteCustomDirectEventTypeConstant(constantWriter, "onChange"); - WriteCustomDirectEventTypeConstant(constantWriter, L"topSlidingStart", L"onRNCSliderSlidingStart"); - WriteCustomDirectEventTypeConstant(constantWriter, L"topSlidingComplete", L"onRNCSliderSlidingComplete"); + WriteCustomDirectEventTypeConstant(constantWriter, L"topSlidingStart", L"onSliderSlidingStart"); + WriteCustomDirectEventTypeConstant(constantWriter, L"topSlidingComplete", L"onSliderSlidingComplete"); }; }