Tried to upgrade to version 8 today and suddenly my interactions stopped working.
Went down to 7 and still, nothing worked.
6.5.2 worked again.
What did change in respect to the PanResponder/panHandlers in version 7?
@kay-is Can you try v8.0.3? A regression was introduced in 8.0.0 and fixed in 8.0.2 at least, not sure if there are others as well. Please post a reproduction otherwise.
Is this on Android, iOS or both?
Tried it in the iPhone simulator.
Was having the same issue for iOS on 8.0.2. Upgrading to 8.0.5 fixed it for me.
8.0.5 does interactions, but they are wrong.
This is my component:
import React from "react";
import { PanResponder } from "react-native";
import Svg, {
Circle,
G,
Path
} from "react-native-svg";
const placeholderImage = require("../assets/logo-pink.png");
class CircularSlider extends React.Component {
width = 275;
height = 275;
constructor(props) {
super(props);
const { width, height } = this;
const smallestSide = Math.min(width, height);
const value = this.props.value * 3.6;
this.state = {
active: false,
value: value || 0,
cx: width / 2,
cy: height / 2,
r: smallestSide / 2 * 0.85
};
}
componentWillMount = () => {
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: this.handlePanResponderMove,
onPanResponderRelease: this.handlePanResponderRelease,
onPanResponderGrant: () => this.setState({ active: true })
});
};
componentWillReceiveProps = ({ value }) => {
if (value >= 100) value = 99;
this.setState({ value: Math.round(value * 3.6) });
};
polarToCartesian = angle => {
const { cx, cy, r } = this.state,
a = (angle - 270) * Math.PI / 180.0,
x = cx + r * Math.cos(a),
y = cy + r * Math.sin(a);
return { x, y };
};
cartesianToPolar = (x, y) => {
const { cx, cy } = this.state;
return Math.round(
Math.atan((y - cy) / (x - cx)) / (Math.PI / 180) +
(x > cx ? 270 : 90)
);
};
handlePanResponderMove = ({
nativeEvent: { locationX, locationY }
}) => {
this.setState({
value: this.cartesianToPolar(locationX, locationY)
});
};
handlePanResponderRelease = () => {
this.setState({ active: false });
const value = Math.floor(this.state.value / 3.6);
if (this.props.onRelease) this.props.onRelease(value);
};
render() {
const { width, height } = this;
const { cx, cy, r, value } = this.state;
const startCoord = this.polarToCartesian(0);
const endCoord = this.polarToCartesian(value);
const path = `
M ${startCoord.x} ${startCoord.y}
A ${r} ${r} 0 ${value > 180
? 1
: 0} 1 ${endCoord.x} ${endCoord.y}`;
return (
<Svg width={width} height={height} style={this.props.style}>
<Circle
cx={cx}
cy={cy}
r={r}
stroke="#aaa"
strokeDasharray={[1, 6]}
strokeWidth={7}
fill="none"
/>
<Path stroke="#eee" strokeWidth={7} fill="none" d={path} />
<G x={endCoord.x - 7.5} y={endCoord.y - 7.5}>
<Circle
cx={7.5}
cy={7.5}
r={this.state.active ? 20 : 16}
fill="#fff"
{...this.panResponder.panHandlers}
/>
</G>
</Svg>
);
}
}
Exactly same issue here.
iOS worked by upgrading to latest but because of Android I had to downgrade to 6.5.2
@jans-y Could you provide a replication? Haven't had any time to look at this yet. Any help in solving it much appreciated!
This is causing me problems too. I'm on the latest version. Here is an example - apologies if it seems complex but it's a fairly slippery issue to repro and I've actually simplified my code a lot in order to show this. I have a fully working project available on request. My code may not be perfect, but what works in iOS 11.4 does not in 12.1. First a video showing the problem as I experience it:

The simulator on the left shows how touches stop working on 12.1 whereas they work fine on 11.4. And now the code. (This is typescript btw). The four rows are defined in my home screen as follows:
import React, { Component } from 'react';
import {
Dimensions,
StatusBar,
TextStyle,
View
} from 'react-native';
import {
DialGrid
} from '../components';
const DIAL_GRID_PARAM_1_1 = 'dialGridParam1_1';
const DIAL_GRID_PARAM_2_1 = 'dialGridParam2_1';
const DIAL_GRID_PARAM_3_1 = 'dialGridParam3_1';
const DIAL_GRID_PARAM_4_1 = 'dialGridParam4_1';
const DIAL_GRID_PARAM_1_2 = 'dialGridParam1_2';
const DIAL_GRID_PARAM_2_2 = 'dialGridParam2_2';
const DIAL_GRID_PARAM_3_2 = 'dialGridParam3_2';
const DIAL_GRID_PARAM_4_2 = 'dialGridParam4_2';
const DIAL_GRID_PARAM_1_3 = 'dialGridParam1_3';
const DIAL_GRID_PARAM_2_3 = 'dialGridParam2_3';
const DIAL_GRID_PARAM_3_3 = 'dialGridParam3_3';
const DIAL_GRID_PARAM_4_3 = 'dialGridParam4_3';
const DIAL_GRID_PARAM_1_4 = 'dialGridParam1_4';
const DIAL_GRID_PARAM_2_4 = 'dialGridParam2_4';
const DIAL_GRID_PARAM_3_4 = 'dialGridParam3_4';
const DIAL_GRID_PARAM_4_4 = 'dialGridParam4_4';
export interface HomeProps {
navigation: any;
}
export interface HomeState {
statusDisplay: string;
dialGridParam1_1: string;
dialGridParam2_1: string;
dialGridParam3_1: string;
dialGridParam4_1: string;
dialGridParam1_2: string;
dialGridParam2_2: string;
dialGridParam3_2: string;
dialGridParam4_2: string;
dialGridParam1_3: string;
dialGridParam2_3: string;
dialGridParam3_3: string;
dialGridParam4_3: string;
dialGridParam1_4: string;
dialGridParam2_4: string;
dialGridParam3_4: string;
dialGridParam4_4: string;
}
export default class Home extends Component<HomeProps, HomeState> {
width: number = Dimensions.get('window').width;
constructor (props: HomeProps) {
super(props);
const initToTen = this.scaleDisplayToTen(0);
this.state = {
statusDisplay: '_ _',
dialGridParam1_1: initToTen,
dialGridParam2_1: initToTen,
dialGridParam3_1: initToTen,
dialGridParam4_1: initToTen,
dialGridParam1_2: initToTen,
dialGridParam2_2: initToTen,
dialGridParam3_2: initToTen,
dialGridParam4_2: initToTen,
dialGridParam1_3: initToTen,
dialGridParam2_3: initToTen,
dialGridParam3_3: initToTen,
dialGridParam4_3: initToTen,
dialGridParam1_4: initToTen,
dialGridParam2_4: initToTen,
dialGridParam3_4: initToTen,
dialGridParam4_4: initToTen
};
StatusBar.setHidden(true);
}
private scaleDisplayToTen (angle: number, maxAngle: number = 360) {
const scaleToTen = Array.from(Array(11), (_, x) => `${x}`);
const multiplier = angle / maxAngle;
const length = multiplier < 1 ? scaleToTen.length : scaleToTen.length - 1;
return scaleToTen[Math.floor(multiplier * length)];
}
private onDialRelease () {
console.log('onDialRelease');
}
private statusDisplayUpdater (func: any, angle: number, maxAngle: number, stateField: string, statusDisplayStyle?: TextStyle | TextStyle[]) {
const value = func(angle, maxAngle);
this.setState({
statusDisplay: value,
...{ [`${stateField}`]: value }
});
}
public render () {
return (
<View style={{ flex: 1, justifyContent: 'center', backgroundColor: 'black' }}>
<DialGrid
numRows={4}
dialPropsList={[
[
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_1_1),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_1_1], onRelease: () => this.onDialRelease() }
},
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_2_1),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_2_1], onRelease: () => this.onDialRelease() }
},
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_3_1),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_3_1], onRelease: () => this.onDialRelease() }
},
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_4_1),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_4_1], onRelease: () => this.onDialRelease() }
}
],
[
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_1_2),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_1_2], onRelease: () => this.onDialRelease() }
},
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_2_2),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_2_2], onRelease: () => this.onDialRelease() }
},
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_3_2),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_3_2], onRelease: () => this.onDialRelease() }
},
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_4_2),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_4_2], onRelease: () => this.onDialRelease() }
}
],
[
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_1_3),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_1_3], onRelease: () => this.onDialRelease() }
},
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_2_3),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_2_3], onRelease: () => this.onDialRelease() }
},
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_3_3),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_3_3], onRelease: () => this.onDialRelease() }
},
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_4_3),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_4_3], onRelease: () => this.onDialRelease() }
}
],
[
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_1_4),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_1_4], onRelease: () => this.onDialRelease() }
},
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_2_4),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_2_4], onRelease: () => this.onDialRelease() }
},
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_3_4),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_3_4], onRelease: () => this.onDialRelease() }
},
{
onValueChange: (angle, maxAngle) => this.statusDisplayUpdater(this.scaleDisplayToTen, angle, maxAngle, DIAL_GRID_PARAM_4_4),
circularSliderProps: { labelString: this.state[DIAL_GRID_PARAM_4_4], onRelease: () => this.onDialRelease() }
}
]
]}
/>
</View>
);
}
}
So we're basically passing props to the dial grid which looks like this:
import React, { Component } from 'react';
import { StyleSheet, View } from 'react-native';
import {
DialGridRow
} from './';
import Dial,
{
DialProps
} from './Dial';
interface Props {
numRows: number;
dialPropsList: Array<DialProps>[];
}
interface State {
}
export default class DialGrid extends Component<Props, State> {
constructor (props: Props) {
super(props);
}
render () {
const dialGridRows = Array.from(Array(this.props.numRows)).map((_: any, index: number) => <DialGridRow key={index} dialPropsList={this.props.dialPropsList[index]} />);
return <View style={styles.container}>
<View style={styles.borderContainer}>
{ dialGridRows }
</View>
</View>;
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 10,
maxHeight: '58%',
alignItems: 'center'
},
borderContainer: {
flex: 1,
width: '96%',
backgroundColor: 'black',
borderWidth: 1,
borderRadius: 7,
borderColor: 'black'
},
labelContainer: {
flexDirection: 'row',
justifyContent: 'space-around'
}
});
As you see, this component is composed of a number of dial grid rows - they look like this:
import React from 'react';
import { Dimensions, View } from 'react-native';
import CircularSlider from './CircularSlider';
import Dial, { DialProps } from './Dial';
interface Props {
dialPropsList: DialProps[];
}
const DialGridRow: React.SFC<Props> = (props) => {
const listItems = props.dialPropsList.map(
(listProps: DialProps, index: number) =>
<CircularSlider
key={index}
rotationOffset={-135}
maxAngle={270}
onValueChange={ listProps.onValueChange }
btnFill={'transparent'}
textColor={'white'}
backgroundColor={'gray'}
cap={'butt'}
dialRadius={Dimensions.get('window').width / 12}
dialWidth={Dimensions.get('window').width / 24}
dialBgWidth={Dimensions.get('window').width / 24}
startGradient={'cyan'}
endGradient={'cyan'}
textSize={20}
{ ...listProps.circularSliderProps }
/>
);
return <View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-evenly' }}>
{ listItems }
</View>;
};
export default DialGridRow;
And the actual circular slider where the problem is looks like this:
import React, { Component } from 'react';
import { PanResponder, StyleSheet, View } from 'react-native';
import Svg, {
Circle,
Defs,
G,
LinearGradient,
Linecap,
Path,
Stop,
Text
} from 'react-native-svg';
export interface Props {
value?: number;
dialRadius?: number;
btnRadius?: number;
startCoord?: number;
startGradient?: string;
endGradient?: string;
dialWidth?: number;
cap?: Linecap;
dialBgWidth?: number;
backgroundColor?: string;
textSize?: number;
textFont?: string;
textColor?: string;
showValue?: boolean;
btnFill?: string;
maxAngle?: number;
rotationOffset?: number;
labelString?: string;
disabled?: boolean;
onValueChange? (angle: number, maxAngle: number): void;
onRelease? (): void;
}
interface State {
angle: number;
xCenter: number;
yCenter: number;
}
export default class CircularSlider extends Component<Props, State> {
static defaultProps = {
btnRadius: 10,
dialRadius: 80,
dialWidth: 20,
textColor: 'white',
textSize: 30,
value: 0,
showValue: true,
startGradient: '#12D8FA',
endGradient: '#12D8FA',
backgroundColor: 'white',
startCoord: 0,
cap: 'butt',
btnFill: 'transparent',
dialBgWidth: 20,
maxAngle: 360,
rotationOffset: 0,
disabled: false,
onValueChange: () => { /* */ },
onRelease: () => { /* */ }
};
_panResponder: any;
circleSlider: any;
container: any;
constructor (props: any) {
super(props);
const {
value,
dialRadius,
btnRadius,
rotationOffset,
maxAngle
} = props;
this.state = {
angle: value,
xCenter: 0,
yCenter: 0
};
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (e, gs) => true,
onStartShouldSetPanResponderCapture: (e, gs) => true,
onMoveShouldSetPanResponder: (e, gs) => true,
onMoveShouldSetPanResponderCapture: (e, gs) => true,
onPanResponderMove: (e, gs) => {
if (!this.props.disabled) {
let xOrigin =
this.state.xCenter - (dialRadius + btnRadius);
let yOrigin =
this.state.yCenter - (dialRadius + btnRadius);
let a = this.cartesianToPolar(gs.moveX - xOrigin, gs.moveY - yOrigin);
let a2 = 0;
if (a + rotationOffset > 0) {
a2 = a - (360 + rotationOffset);
} else {
a2 = a - rotationOffset;
}
const angle = a2 > 0 ? Math.min(a2, maxAngle) : maxAngle;
this.setState({ angle });
this.props.onValueChange!(angle, this.props.maxAngle!);
}
},
onPanResponderTerminationRequest: (e, gs) => true,
onPanResponderRelease: (e, gs) => this.props.onRelease && this.props.onRelease(),
onPanResponderTerminate: (e, gs) => this.props.onRelease && this.props.onRelease()
});
}
polarToCartesian (angle: number) {
let r = this.props.dialRadius!;
let hC = this.props.dialRadius! + this.props.btnRadius!;
let a = ((angle - 90) * Math.PI) / 180.0;
let x = hC + r * Math.cos(a);
let y = hC + r * Math.sin(a);
return { x, y };
}
cartesianToPolar (x: number, y: number) {
let hC = this.props.dialRadius! + this.props.btnRadius!;
if (x === 0) {
return y > hC ? 0 : 180;
} else if (y === 0) {
return x > hC ? 90 : 270;
} else {
return (
Math.round((Math.atan((y - hC) / (x - hC)) * 180) / Math.PI) +
(x >= hC ? 90 : 270)
);
}
}
handleMeasure = (ox: number, oy: number, width: number, height: number, px: number, py: number) => {
this.setState({
xCenter: px + (this.props.dialRadius! + this.props.btnRadius!),
yCenter: py + (this.props.dialRadius! + this.props.btnRadius!)
});
}
handleOnLayout = () => {
this.circleSlider.measure(this.handleMeasure);
}
render () {
let {
btnRadius,
dialRadius,
dialWidth,
rotationOffset,
textFont,
textSize,
startGradient,
endGradient,
textColor,
backgroundColor,
cap,
btnFill
} = this.props;
let width = (dialRadius! + btnRadius!) * 2;
let startCoord = this.polarToCartesian(this.props.startCoord ? this.props.startCoord : 0);
let endCoord = this.polarToCartesian(this.state.angle);
let maxAngle = this.polarToCartesian(this.props.maxAngle!);
const maxAngleY = maxAngle.y.toPrecision(4);
return (
<View
ref={r => this.container = r }
style={{ alignItems: 'center' }}
>
<Svg
onLayout={this.handleOnLayout}
ref={r => this.circleSlider = r}
width={width}
height={width}
>
<Defs>
<LinearGradient id='gradient1' x1='0%' y1='0%' x2='100%' y2='0%'>
<Stop offset='0%' stopColor={startGradient} />
<Stop offset='100%' stopColor={endGradient} />
</LinearGradient>
</Defs>
<Text
x={width / 2}
y={width / 2 + 6}
fontSize={textSize}
fontFamily={textFont ? textFont : ''}
fill={textColor}
textAnchor='middle'
>
{this.props.showValue &&
this.props.labelString ? this.props.labelString : ''}
</Text>
<G transform={`rotate(${rotationOffset} ${width / 2} ${width / 2})`}>
<Path
stroke={backgroundColor}
strokeWidth={dialWidth}
fill='none'
strokeLinecap={cap}
strokeLinejoin='round'
d={`M ${startCoord.x} ${startCoord.y} A ${dialRadius} ${dialRadius} 0 ${
(this.props.startCoord! + 180) % 360 > this.props.maxAngle! ? 0 : 1
} 1 ${maxAngle.x} ${maxAngleY}`}
/>
<Path
stroke={'url(#gradient1)'}
strokeWidth={dialWidth}
fill='none'
strokeLinecap={cap}
strokeLinejoin='round'
d={`M ${startCoord.x} ${startCoord.y} A ${dialRadius} ${dialRadius} 0 ${
(this.props.startCoord! + 180) % 360 > this.state.angle ? 0 : 1
} 1 ${endCoord.x} ${endCoord.y}`}
/>
<G x={endCoord.x - btnRadius!} y={endCoord.y - btnRadius!}>
<Circle
r={btnRadius}
cx={btnRadius}
cy={btnRadius}
fill={btnFill}
{...this._panResponder.panHandlers}
/>
</G>
</G>
</Svg>
</View>
);
}
}
Some other notes. To create the dial effect I wanted, I rotated the whole group. A quick test removing that rotation seems to remove the touch-handling issue. Similarly, if I have only one row (the code is written to work fine with anywhere between 1 and 4 rows as there are 4 lots of row props - defined as you might expect using the dial grid's numRows prop), touches work fine, but with 2 or more I see the problem as shown in the .gif.
Hope all this helps. It's a blocker for me at the moment :(
UPDATE: I tried all 8.x.x releases, my particular problem turned up in 8.0.6. And in fact, I downgraded to 6.5.2 and the problem went away. Unfortunately another problem that originally forced me to upgrade means I can't use that either....so...@msand Any chance of taking a look at this?
Can you try? https://github.com/react-native-community/react-native-svg/commit/2f513504d21a8138ac851784b3b110ccb8cceb3a
Or, if that doesn't solve it, could you please make a git repo with the needed files to run your test case?
Well..yes, it certainly solves the issue of not being able to drag the bar, so thank you. However, touches are still behaving weirdly on account of the rotation I applied to the group above. In the full project that this example comes from, I have a nav button top right that has an icon in the middle. When rotations are applied to my group, that button stops receiving touches on the icon part, but otherwise behaves correctly. If I remove rotations the whole button can be pressed. Similarly, if I add panResponder handling to the whole view of my circular slider and keep the rotation on the group, moving the dial on the left will cause the dial furthest right to move! So adding rotations seems to cause problems with the whole gesture responder system... Will try and put together a full project for you. An alternative for me is to build the graphics without applying rotations. That would probably solve all problems, with your latest commit.
I wonder if this might be related to the center point, I think that might not be set correctly yet: https://developer.apple.com/documentation/uikit/uiview/1622580-bounds?language=objc
Can you try using the inspector, and click the touchables mode, so you can see all the touchable areas, before and after rotation?
@sinewave440hz I have another attempt here: https://github.com/react-native-community/react-native-svg/commit/37f07050e4867da33b58ece871881efdb4cd1123
It seems to handle transforms correctly, and draws reasonable touchable areas in the tests I've done so far.
Here's a test case:
import React, { Component } from 'react';
import {
Text,
View,
TouchableWithoutFeedback,
StyleSheet,
Dimensions,
Animated,
Platform,
} from 'react-native';
import Svg, {
Defs,
LinearGradient,
Stop,
RadialGradient,
G,
Path,
Circle,
Rect,
ClipPath,
Symbol,
Use,
Image,
Text as SvgText,
} from 'react-native-svg';
const AnimatedG = Animated.createAnimatedComponent(G);
const AnimatedPath = Animated.createAnimatedComponent(Path);
const AnimatedRect = Animated.createAnimatedComponent(Rect);
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'white',
},
demo: {
width: 200,
height: 200,
backgroundColor: 'orange',
},
text: { color: 'white' },
});
class TouchesMove extends Component {
handlers = {
onPress: () => {
this.setEvent('onPress');
},
onPressIn: () => {
this.setEvent('onPressIn');
},
onPressOut: () => {
this.setEvent('onPressOut');
},
};
state = {
rotation: new Animated.Value(0),
event: 'Last press event will be shown here.',
};
stopAnimation() {
this.state.rotation.stopAnimation();
this.setState({ rotation: new Animated.Value(0) });
}
startAnimation() {
Animated.loop(
Animated.timing(this.state.rotation, {
useNativeDriver: true,
duration: 3000,
toValue: 1,
}),
).start();
}
componentDidMount() {
this.startAnimation();
}
setEvent(event) {
this.setState({ event });
}
render() {
return (
<View style={styles.container}>
<Svg width={200} height={200} viewBox="-100 -100 200 200">
{null || (
<Rect
fill="green"
x={-100}
y={-100}
width={200}
height={200}
hitSlop={0}
{...this.handlers}
/>
)}
<AnimatedG
transform="rotate(45)"
style={{
transform: [
{
rotate: '90deg'
},
{
rotate: this.state.rotation.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}),
},
],
}}
>
<AnimatedRect
fill="navy"
x={-50}
y={-50}
width={100}
height={100}
hitSlop={0}
{...this.handlers}
transforms="rotate(45)"
style={{
transform: [
{
rotate: '90deg'
},
{
rotate: this.state.rotation.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}),
},
],
}}
/>
{null || (
<SvgText x={-50} y={-36} fill="#fff">
{'react-native-svg <Rect>'}
</SvgText>
)}
</AnimatedG>
</Svg>
<TouchableWithoutFeedback {...this.handlers}>
<View style={styles.demo}>
<Text style={styles.text}>{'react-native <View>'}</Text>
</View>
</TouchableWithoutFeedback>
<Text>{this.state.event}</Text>
</View>
);
}
}
export default class App extends Component {
render() {
return <TouchesMove />;
}
}
I think I have interactions between native animated transforms and normal svg transforms working correctly now: https://github.com/react-native-community/react-native-svg/commit/8c05da043dea3f8d04420c2d7c9c11733e22258b
Seeing changes...but there are still issues. I changed the gesture handling to just read up/down motion on each circular slider. Again, it works fine in iOS 11.4 but is pretty messed up on 12.1 still. At the moment, the motion is read correctly, but affects another circular slider than the one being dragged, creating a bit of a whack-a-mole effect overall :) Here is the project that I've culled from my real project, it shows the problem clearly. I really it hope it helps, let me know what else you need.
https://github.com/sinewave440hz/DialTest
I did as you suggested and tried to debug the Touchables, but to be honest I can't get anything sensible from that at all, it seems to hang on the first selected touchable, which isn't even the one I first selected...
Looking at your example, I see that there are a few things I'm doing differently. I will try and apply those to my case where appropriate and see if things improve.
@sinewave440hz seems the workaround found by @folofse works: https://github.com/react-native-community/react-native-svg/issues/794#issuecomment-436575946
Also reported on the react-native repo: https://github.com/facebook/react-native/issues/21996
And https://github.com/react-navigation/react-navigation/issues/5007
It boils down to adding a overflow: hidden to the wrapping view, made a pr here: https://github.com/sinewave440hz/DialTest/pull/1
I wonder what causes this, what has changed between iOS 11 and 12 with regards to touch/gesture handling? Not sure if the underlying cause is in react-native, or this library yet. I would love any help in investigating this!
@kay-is I tried moving the panResponder in your example to a wrapping view, seems like a functional workaround for now:
import React, { Component } from 'react';
import {
View,
StyleSheet,
StyleProp,
ViewStyle,
PanResponder,
PanResponderInstance,
} from 'react-native';
import Svg, { Circle, G, Path } from 'react-native-svg';
class CircularSlider extends React.Component<
{ value: number; onRelease?: Function; style?: StyleProp<ViewStyle> },
{ active: boolean; value: number; cx: number; cy: number; r: number }
> {
width = 275;
height = 275;
private panResponder: PanResponderInstance;
constructor(
props: Readonly<{
value: number;
onRelease?: Function;
style?: StyleProp<ViewStyle>;
}>,
) {
super(props);
const { width, height } = this;
const smallestSide = Math.min(width, height);
const value = this.props.value * 3.6;
this.state = {
active: false,
value: value || 0,
cx: width / 2,
cy: height / 2,
r: smallestSide / 2 * 0.85,
};
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: this.handlePanResponderMove,
onPanResponderRelease: this.handlePanResponderRelease,
onPanResponderGrant: () => this.setState({ active: true }),
});
}
componentWillReceiveProps = ({ value }: { value: number }) => {
if (value >= 100) value = 99;
this.setState({ value: Math.round(value * 3.6) });
};
polarToCartesian = (angle: number) => {
const { cx, cy, r } = this.state,
a = (angle - 270) * Math.PI / 180.0,
x = cx + r * Math.cos(a),
y = cy + r * Math.sin(a);
return { x, y };
};
cartesianToPolar = (x: number, y: number) => {
const { cx, cy } = this.state;
return Math.round(
Math.atan((y - cy) / (x - cx)) / (Math.PI / 180) + (x > cx ? 270 : 90),
);
};
handlePanResponderMove = ({
nativeEvent: { locationX, locationY },
}: {
nativeEvent: { locationX: number; locationY: number };
}) => {
console.log({ locationX, locationY });
this.setState({
value: this.cartesianToPolar(locationX, locationY),
});
};
handlePanResponderRelease = () => {
this.setState({ active: false });
const value = Math.floor(this.state.value / 3.6);
if (this.props.onRelease) this.props.onRelease(value);
};
render() {
const { width, height } = this;
const { cx, cy, r, value } = this.state;
const startCoord = this.polarToCartesian(0);
const endCoord = this.polarToCartesian(value);
const path = `
M ${startCoord.x} ${startCoord.y}
A ${r} ${r} 0 ${value > 180 ? 1 : 0} 1 ${endCoord.x} ${endCoord.y}`;
return (
<View
style={[{ overflow: 'hidden' }, this.props.style]}
{...this.panResponder.panHandlers}
>
<Svg width={width} height={height}>
<Circle
cx={cx}
cy={cy}
r={r}
stroke="#aaa"
strokeDasharray={[1, 6]}
strokeWidth={7}
fill="none"
/>
<Path stroke="#eee" strokeWidth={7} fill="none" d={path} />
<G x={endCoord.x - 7.5} y={endCoord.y - 7.5}>
<Circle
cx={7.5}
cy={7.5}
r={this.state.active ? 20 : 16}
fill="#fff"
/>
</G>
</Svg>
</View>
);
}
}
interface Props {}
interface State {}
export default class DialTest extends Component<Props, State> {
state = {
value: 1,
};
onRelease = (value: number) => {
this.setState({ value });
};
render() {
return (
<CircularSlider
value={this.state.value}
onRelease={this.onRelease}
style={styles.container}
/>
);
}
}
const styles = StyleSheet.create({
container: {
paddingTop: 30,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0A0A0A',
},
});
@kay-is If you only want it to respond if you click the actual circle, you can check that the distance is within the radius like this:
const shouldRespond = (
e: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
const { value } = this.state;
const { x, y } = this.polarToCartesian(value);
const { locationX, locationY } = e.nativeEvent;
const dx = x - locationX;
const dy = y - locationY;
const l2norm = Math.sqrt(dx * dx + dy * dy);
return l2norm < 16;
};
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: shouldRespond,
onMoveShouldSetPanResponder: shouldRespond,
onPanResponderMove: this.handlePanResponderMove,
onPanResponderRelease: this.handlePanResponderRelease,
onPanResponderGrant: () => this.setState({ active: true }),
});
I know it's a hack, would be great to find the actual cause for these issues. Do you have any known versions of react-native, react-native-svg and ios simulator where it works correctly?
@kay-is seems like it's enough to move the panHandlers to the svg root element. I wonder if it's just because of the changing position / matrix of the G element which contains the circle, perhaps when the properties are invalidated, but gesture events keep coming, it fails to do the hit testing / inverse transformations and thus gives nonsensical locations in the gesture events.
Just to be clear, the 'overflow:hidden' workaround solved all my issues. Thank you!
@shergin Do you know about any changes related to hit testing requiring overflow: hidden for iOS 12 and react-native? Or someone on the react-native team who has seen something related to this?
https://github.com/react-native-community/react-native-svg/issues/821#issuecomment-445587423
@kay-is I think I've found the underlying reason for incorrect locations when placing the panResponder on elements inside the svg root rather than on the root itself. I have a fix available here: https://github.com/react-native-community/react-native-svg/commit/6df918356b5df6cde98b78560b3e889199bd67a4
Can you please test it?
Thank you for your effort :)
I'll test it this week.
@kay-is Android also had a bug with handling transforms: https://github.com/react-native-community/react-native-svg/commit/4b65c00912567afb050bf0d094ec6cadcbcc74ef
published v8.0.10 with the fixes
I think this can probably be closed now. Please comment/reopen if you can observe unexpected behavior with the latest version. Thanks for the issue reports!
I upgraded to version 8.0.10, but now I get errors about my props.
JSON value '40' of type NSNumber cannot be converted to NSString
Tried to convert them manually with toString but then the graphic wouldn't show anything anymore.
@kay-is Can you try with the latest version and verify that you rebuild the native code and clear the javascript cache? Seems to work fine with the latest release:
import React from 'react';
import { View, StyleSheet, PanResponder } from 'react-native';
import Svg, { Circle, G, Path } from 'react-native-svg';
export default class CircularSlider extends React.Component {
width = 275;
height = 275;
constructor(props) {
super(props);
const { width, height } = this;
const smallestSide = Math.min(width, height);
const value = this.props.value * 3.6;
this.state = {
active: false,
value: value || 0,
cx: width / 2,
cy: height / 2,
r: smallestSide / 2 * 0.85,
};
}
componentWillMount = () => {
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: this.handlePanResponderMove,
onPanResponderRelease: this.handlePanResponderRelease,
onPanResponderGrant: () => this.setState({ active: true }),
});
};
componentWillReceiveProps = ({ value }) => {
if (value >= 100) value = 99;
this.setState({ value: Math.round(value * 3.6) });
};
polarToCartesian = angle => {
const { cx, cy, r } = this.state,
a = (angle - 270) * Math.PI / 180.0,
x = cx + r * Math.cos(a),
y = cy + r * Math.sin(a);
return { x, y };
};
cartesianToPolar = (x, y) => {
const { cx, cy } = this.state;
return Math.round(
Math.atan((y - cy) / (x - cx)) / (Math.PI / 180) + (x > cx ? 270 : 90),
);
};
handlePanResponderMove = ({ nativeEvent: { locationX, locationY } }) => {
this.setState({
value: this.cartesianToPolar(locationX, locationY),
});
};
handlePanResponderRelease = () => {
this.setState({ active: false });
const value = Math.floor(this.state.value / 3.6);
if (this.props.onRelease) this.props.onRelease(value);
};
render() {
const { width, height } = this;
const { cx, cy, r, value } = this.state;
const startCoord = this.polarToCartesian(0);
const endCoord = this.polarToCartesian(value);
const path = `
M ${startCoord.x} ${startCoord.y}
A ${r} ${r} 0 ${value > 180 ? 1 : 0} 1 ${endCoord.x} ${endCoord.y}`;
return (
<View style={styles.container}>
<Svg width={width} height={height} style={this.props.style}>
<Circle
cx={cx}
cy={cy}
r={r}
stroke="#aaa"
strokeDasharray={[1, 6]}
strokeWidth={7}
fill="none"
/>
<Path stroke="#eee" strokeWidth={7} fill="none" d={path} />
<G x={endCoord.x - 7.5} y={endCoord.y - 7.5}>
<Circle
cx={7.5}
cy={7.5}
r={this.state.active ? 20 : 16}
fill="#fff"
{...this.panResponder.panHandlers}
/>
</G>
</Svg>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'black',
},
button: {
margin: 40,
padding: 20,
borderWidth: 1,
borderColor: 'black',
},
});
Could try it today with 9.2.4
Worked like a charm, thanks.
Excellent, happy to hear!
@techtic-saajan I have no idea what you're asking for.
Pan responder certainly works, and allows you to respond to pan gestures. Almost certainly, whatever you actually wanted to ask for is possible.
@techtic-saajan Read the documentation, I won't do your job for you.
https://facebook.github.io/react-native/docs/panresponder