Panresponder + interpolate - javascript

I have a Component Parent which has an Animated.value "this.progress" which takes values in [1, 3].
The Parent renders a Component Child and pass it the progress as a props:
// File Parent.js
import React from "react";
import { Animated } from "react-native";
import Child from "./Child.js"
class Parent extends React.Component {
constructor(props) {
super(props);
this.progress = new Animated.Value(1)
}
render() {
return (
<Child
progress = {this.progress}
/>
);
}
}
The Child position depends on the progress:
if progress = 1, the child position is [1, 3]
if progress = 2, the child position is [2, 2]
if progress = 3, the child position is [1, 1]
If an animation changes the value of the progress, the Child must move accoringly (e.g., if progress is animated from 1 to 2, the user must see the Child move from [1, 3] to [2, 2] in a smooth way).
To that end I use, two interpolations:
// In the constructor of Child.js
this.interpolateX = this.props.progress.interpolate({
inputRange: [1, 2, 3],
outputRange: [1, 2, 1],
});
this.interpolateY = this.props.progress.interpolate({
inputRange: [1, 2, 3],
outputRange: [3, 2, 1],
});
and I use them to fix the position of the Child using "transform":
// Render method of Child.js
render() {
return (
<Animated.View
style={[
{
transform: [
{
translateX: this.interpolateX,
},
{
translateY: this.interpolateY,
},
],
},
{ position: 'absolute' },
{ left: 0 },
{ top: 0 },
]}
/>
);
}
It works well, but now I also want to make the Child movable using the user finger. To that end I defined a panResponder. The problem is that the panResponder also uses a transform. I now have 2 transforms:
one used to interpolate the Child's position from this.props.progress ;
one used by the panResponder to move the Child with the user finger.
I don't know how to combine these two transform. Here is what I tried:
// File Child.js
import React from "react";
import { Animated, PanResponder } from "react-native";
class Child extends React.Component {
constructor(props) {
super(props);
this.interpolateX = this.props.progress.interpolate({
inputRange: [1, 2, 3],
outputRange: [1, 2, 1],
});
this.interpolateY = this.props.progress.interpolate({
inputRange: [1, 2, 3],
outputRange: [3, 2, 1],
});
this.offset = new Animated.ValueXY({ x: 0, y: 0 });
this._val = { x: 0, y: 0 };
this.offset.addListener((value) => (this._val = value));
// Initiate the panResponder
this.panResponder = PanResponder.create({
// Ask to be the responder
onStartShouldSetPanResponder: () => true,
// Called when the gesture starts
onPanResponderGrant: () => {
this.offset.setOffset({
x: this._val.x,
y: this._val.y,
});
this.offset.setValue({ x: 0, y: 0 });
},
// Called when a move is made
onPanResponderMove: Animated.event([
null,
{ dx: this.offset.x, dy: this.offset.y },
]),
onPanResponderRelease: (evt, gesturestate) => {
console.log(
"released with offsetX: " +
this.offset.x._value +
"/" +
this.offset.y._value
);
},
});
}
render() {
const panStyle = {
transform: this.offset.getTranslateTransform(),
};
return (
<Animated.View
{...this.panResponder.panHandlers}
style={[
panStyle,
{
transform: [
{
translateX: this.interpolateX,
},
{
translateY: this.interpolateY,
},
],
},
{ position: "absolute" },
{ left: 0 },
{ top: 0 },
]}
/>
);
}
}
export default Child;
It does not work. panStyle seems to be ignored. I guess you cannot have two transforms together.
I also tried to "add" the two expressions in the transforms without success (maybe its feasible).
My last idea was to have a boolean "isMoving" equal to true when the user moves the Child. If isMoving is false, I use the first transform, otherwise, I use the second one. It seems promising but I did not succeed in making it work.
Could you tell me how I could do that?

A boolean variable isMoving which indicates which transform to use was the good idea. The trick was:
to put isMoving in the state;
to set the offset of my offset variable to the current value of the interpolate.
Here is the result:
// File Child.js
import React from "react";
import { Animated, PanResponder } from "react-native";
class Child extends React.Component {
constructor(props) {
super(props);
this.interpolateX = this.props.progress.interpolate({
inputRange: [1, 2, 3],
outputRange: [1, 2, 1],
});
this.interpolateY = this.props.progress.interpolate({
inputRange: [1, 2, 3],
outputRange: [3, 2, 1],
});
this.offset = new Animated.ValueXY({ x: 0, y: 0 });
this.state = {
isMoving: false,
};
// Initiate the panResponder
this.panResponder = PanResponder.create({
// Ask to be the responder
onStartShouldSetPanResponder: () => true,
// Called when the gesture starts
onPanResponderGrant: () => {
this.setState({
isMoving: true,
});
this.offset.setOffset({
x: this.interpolateX.__getValue(),
y: this.interpolateY.__getValue(),
});
this.offset.setValue({ x: 0, y: 0 });
},
// Called when a move is made
onPanResponderMove: Animated.event([
null,
{ dx: this.offset.x, dy: this.offset.y },
]),
onPanResponderRelease: (evt, gesturestate) => {
this.setState({
isMoving: false,
});
},
});
}
render() {
const panStyle = {
transform: this.offset.getTranslateTransform(),
};
if (this.state.isMoving)
return (
<Animated.View
{...this.panResponder.panHandlers}
style={[panStyle, { position: "absolute" }, { left: 0 }, { top: 0 }]}
/>
);
else
return (
<Animated.View
{...this.panResponder.panHandlers}
style={[
{
transform: [
{
translateX: this.interpolateX,
},
{
translateY: this.interpolateY,
},
],
},
{ position: "absolute" },
{ left: 0 },
{ top: 0 },
]}
/>
);
}
}
export default Child;

Related

React Native Flatlist Animated View Scrolling Problem

I tried to "change" the default refresh indicator of a Flatlist in React Native. I thought about to have something like Snapchat or Instagram or the default IOS refresh indicator instead of the ugly Android indicator. Now I tried following:
const screenHeight = Dimensions.get("screen").height;
const lockWidth = screenHeight;
const finalPosition = lockWidth;
const pan = useRef(new Animated.ValueXY({x: 0, y: 0})).current;
const translateBtn = pan.y.interpolate({
inputRange: [0, finalPosition / 0.75],
outputRange: [0, finalPosition / 2],
extrapolate: 'clamp',
});
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {},
onPanResponderMove: Animated.event([null, {dy: pan.y}], {
useNativeDriver: false,
}),
onPanResponderRelease: (e, g) => {
if (g.vy > 2 || g.dy > lockWidth / 2) {
unlock();
} else {
reset();
}
},
onPanResponderTerminate: () => reset(),
}),
).current;
const reset = () => {
setGeneralState(prevState => ({...prevState, scroll: true}))
Animated.spring(pan, {
toValue: {x: 0, y: 0},
useNativeDriver: true,
bounciness: 0,
}).start();
};
const unlock = () => {
setLoading(true)
Animated.spring(pan, {
toValue: {x: 0, y: 280},
useNativeDriver: true,
bounciness: 0,
}).start();
// alert("")
setTimeout(() => {
reset();
setLoading(false)
}, 1000)
};
It works just as I expect:
Now the problem:
When the Flatlist is so big that it can be scrolled (if there are many items) I cannot pull down to refresh anymore because React Native always tries to move the flatlist when I'm scrolling.
The FlatList:
<Animated.View
style={[
styles.receiverBox,
{transform: [{translateY: translateBtn}]},
]}
{...panResponder.panHandlers}>
<View>
<FlatList
data={["user1", "user2", "user3", "user4", "user5",]}
renderItem={({item}) => {
return(
<View style={{backgroundColor: colors.background, padding: 20, marginTop: -5, width: "100%",}}>
<Text style={{fontSize: Title1, color: colors.text, textAlign: "center",}}>{item}</Text>
</View>
)
}}
/>
</View>
</Animated.View>
Figured out that this may be a problem from react native or the combination of the flatlist with the pan responder.

stop dragging after a limit has reached

I am using react native gesture handlers to create a bar that can be scrolled up and down. Currently I can scroll it as much as I want. I want to modify it such that it should stop scrolling when I certain limit has reached.
export const SwipeablePanel: React.FunctionalComponent = () => {
//set up animation variables
const dragY = useRef(new Animated.Value(0));
const onGestureEvent = Animated.event(
[{ nativeEvent: { translationY: dragY.current } }],
{
useNativeDriver: true,
},
);
const onHandleStateChange = (event: any) => {
if (event.nativeEvent.oldState === State.ACTIVE) {
dragY.current.extractOffset();
}
console.log('EVENT', event.nativeEvent)
};
const animateInterpolation = dragY.current.interpolate({
inputRange: [-(SCREEN_HEIGHT * 2) / 3, 0],
outputRange: [moderateScale(80) - (SCREEN_HEIGHT * 2) / 3, 0],
extrapolate: 'clamp',
});
const animatedStyles = {
transform: [
{
translateY: animateInterpolation,
},
],
};
const whitelistBarMarginInterpolation = dragY.current.interpolate({
inputRange: [-SCREEN_HEIGHT + SCREEN_HEIGHT / 3, 0],
outputRange: [0, moderateScale(150)],
extrapolate: 'clamp',
});
const whitelistBarStyles = {
transform: [
{
translateY: whitelistBarMarginInterpolation,
},
],
};
return (
<PanGestureHandler
onGestureEvent={onGestureEvent}
onHandlerStateChange={onHandleStateChange}
activeOffsetX={[-1000, 1000]}
activeOffsetY={[-10, 10]}
>
<Animated.View
style={[
styles.container,
animatedStyles
]}>
<ScrollBar />
);
};
In onHandleStateChange, I can get values of event.nativeEvent such as
absoluteX: 237
absoluteY: 348.5
handlerTag: 109
numberOfPointers: 0
oldState: 4
state: 5
target: 5235
translationX: 7
translationY: 200.5
velocityX: 0
velocityY: 0
x: 237
y: 84.84616088867188
I want to use an if else condition in the code such that I can set limits after which point the scrolling stops. But I am not sure how to do that since the scrolling happens from the onGestureEvent.
I thought of adding checks in here but if I change it like this, it no longer works:
const onGestureEvent = () => {
Animated.event([{ nativeEvent: { translationY: dragY.current } }], {
useNativeDriver: true,
});
};
Snack Expo : https://snack.expo.io/#nhammad/insane-pizza
I tried to reproduce it but here I can't scroll at it. I am using the same code in my app and it scrolls over there.
Try ScrollView, It has prop called scrollEnabled. By help of this prop you can turn to false when you don't want user to be able to scroll any more. When false, the view cannot be scrolled via touch interaction.
Here is documentation: https://facebook.github.io/react-native/docs/scrollview.html#scrollenabled
ScrollView also has onScroll so it should be easy to implement the logic here.

React Spring - useTrail delay issues

Currently been using react-spring to do our animations in our app; unfortunately, animation is not something I excel in and the design our designer gave us for our new logo is leaving me stumped on implementation. It is pretty easy with plain old JS but implementing it in react-spring has proved a challenge that I can not get past.
The end goal for the animation is to look like this:
https://codepen.io/darylginn/pen/GRqZxBZ
Currently, I am up to this stage:
import React from "react";
import { useTrail, animated } from "react-spring";
const Loader: React.FC = () => {
// Animations
const paths = [
{
id: 1,
color: "#466FB5",
d: "M90.6672 33H16L53.3336 96.4409L90.6672 33Z",
},
{
id: 2,
color: "#0093D3",
d: "M53.3347 96.4443H128.002L90.6683 33.0034L53.3347 96.4443Z",
},
{
id: 3,
color: "#53BFA2",
d: "M128.001 96.3701H53.3336L90.6672 159.811L128.001 96.3701Z",
},
{
id: 4,
color: "#93C83F",
d: "M90.6675 159.892H165.335L128.001 96.4417L90.6675 159.892Z",
},
{
id: 5,
color: "#58B647",
d: "M165.334 159.892H90.6664L128 223.333L165.334 159.892Z",
},
{
id: 6,
color: "#F2E90B",
d: "M202.667 96.4436H128L165.334 159.894L202.667 96.4436Z",
},
{
id: 7,
color: "#FBB12C",
d: "M128.001 96.4443H202.668L165.335 33.0034L128.001 96.4443Z",
},
{
id: 8,
color: "#FF5E8D",
d: "M240 33H165.333L202.666 96.4409L240 33Z",
},
];
const trail = useTrail(paths.length, {
from: {
scale: 1,
},
to: async (next: any) => {
while (1) {
await next({ scale: 1.5 });
await next({ scale: 1 });
}
},
});
return (
<div style={{ width: "200px", height: "200px" }}>
<svg
width="72"
height="72"
viewBox="0 0 256 256"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ overflow: "visible" }}>
{trail.map(({ scale }, index) => {
const path = paths[index];
return (
<animated.path
key={path.id}
fill={path.color}
d={path.d}
style={{
transformOrigin: "center",
transform: scale.interpolate((s: any) => `scale(${s})`),
}}
/>
);
})}
</svg>
</div>
);
};
The main issue I am at now is the scale of each triangle in the SVG needs to happen one after another, but nothing I do with the useTrail make this happen.
I did try adding a delay like this to the useTrail
delay: (key: any) => key * 200
But the delay doesn't even seem to make a difference. If someone could help make sure each triangle finish it sequences before the next one starts that would be great.
Bonus if you can also help me add the colour change as seen in the original design (see link).
I have tried posting in the react-spring community but got no replies
I would change the useTrail to one useSpring for all the triangles. If you change the x value from 0 to 8, then you can use interpolate and a range for each triangle to change. For example for the second triangle you can use range:[0,1,1.5,2,8],output:[1,1,1.5,1,1]. It means that when x is between 1 and 2 it will change the scale from 1 to 1.5 to 1 and all other places it will remain 1.
const props = useSpring({
from: {
x: 0
},
config: {
duration: 4000
},
to: async (next: any) => {
while (1) {
await next({ x: 8 });
await next({ reset: true });
}
}
});
I also added the color interpolation.
{paths.map((path, index) => {
const colors = [];
for (let i = 0; i < 8; i++) {
colors.push(paths[(i + index) % 8].color);
}
return (
<animated.path
key={path.id}
fill={path.color}
d={path.d}
style={{
transformOrigin: "center",
transform: props.x
.interpolate({
range: [0, index, index + 0.5, index + 1, 8],
output: [1, 1, 1.5, 1, 1]
})
.interpolate((x) => `scale(${x})`),
fill: props.x.interpolate({
range: [0, 1, 2, 3, 4, 5, 6, 7, 8],
output: colors
})
}}
/>
);
})}
I have an example. There is some glitch with the center triangle. But I think you get the idea.
Example: https://codesandbox.io/s/animate-triangles-sequentially-with-interpolating-and-range-oer84

How to make element fixed on scroll in React Native?

This is a video of what I have so far https://drive.google.com/file/d/1tJMZfpe8hcqLcNMYiE-6pzwHX0eLofv1/view?usp=sharing
I'm trying to make the "Available for 3 months" fixed once the contentOffset.y has reached the header. Similar to what a position: sticky in css would do.
So far, I thought of doing this via onScroll prop in the Scroll View Component but the issue is, I already have animations (Animated.Event) from the parent and child component in place, meaning the only way for me to do this would be via the listener function in the Animated.Event but doing that, leads to a super choppy animation if the useNativeDriver option is set to false.
If I set that to true, the whole thing won't work (it crashes). The error is along the lines "onScroll is not a function, it's an instance of Animated.event"
So, Say we have two components, the parent is Parent.js and the child (a scroll view) is ChildScrollView.js
The ChildScrollView.js already has animations on the scroll view, but we need to add some more in the Parent.js component, to do with Navigation which the ChildScrollView.js can't handle
So it's coded like this:
Parent.js
componentWillMount() {
const { navigation } = this.props
const { scrollY } = this.state
const bgColor = scrollY.interpolate({
inputRange: [HEADER_HEIGHT / 4, HEADER_HEIGHT / 2],
outputRange: ['transparent', 'rgb(255,255,255)'],
extrapolate: 'clamp',
})
const titleColor = scrollY.interpolate({
inputRange: [0, HEADER_HEIGHT / 2],
outputRange: ['transparent', colors.paragraphs],
extrapolate: 'clamp',
})
const titleMarginTop = scrollY.interpolate({
inputRange: [0, HEADER_HEIGHT / 2],
outputRange: [HEADER_HEIGHT, 0],
extrapolate: 'clamp',
})
navigation.setParams({
bgColor,
titleColor,
titleMarginTop,
})
}
onScroll() {
}
render() {
return (
<ChildScrollView
{...childProps}
onScroll={Animated.event([
{ nativeEvent: { contentOffset: { y: scrollY } } },
], {
useNativeDriver: false, // when I do this, it become super laggy, but setting it to true makes the app crash
listener: this.onScroll,
})}
>
<MakeMeFixedOnScroll>I could do this in css with "position: sticky"</MakeMeFixedOnScroll>
</ChildScrollView>
)
}
And the child is similar,
<Animated.ScrollView
{...props}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{
useNativeDriver: false,
listener: event => {
if (onScroll) onScroll(event)
},
}
)}
scrollEventThrottle={16}
>
I would use SectionList
<SectionList
renderItem={({item, index, section}) => (
<Text style={styles[item.type]}>{item.text}</Text>
)}
renderSectionHeader={({ section: { title } }) => (
title && <Text style={styles.sticky}>{title}</Text>
)}
sections={[
{ title: null, data: [{
type: 'heading', text: '133 Random Road'
}, {
type: 'heading', text: 'Donnybrook'
}, {
type: 'subtitle', text: 'Dublin 4'
}, {
type: 'title', text: 'From E1000/month'
}]
},
{ title: 'Available For 3 Month', data: [{
type: 'text', text: 'Beautiful for bedroom...'
}]
}
]}
stickySectionHeadersEnabled
/>

How do I update only one trace in react plotly?

I am using react-plotly to generate a large timeline of data (10,000-100,000 points) and I animate across the data in another window. I need to get a scrubber (vertical line) that moves with a react-property representing time, but I need to update the scrubber without updating the rest of the timeline, since it would take so long to do so. How can I get just the vertical line to update?
Edit: Was asked for code
In the following code, the backtracks and thresholds objects are Uint32Arrays and represent the y-axis of traces, where the x-axes are the Uint32Arrays backtracksTime and thresholdsTime. What I am trying to get is a vertical line at the x-coordinate currentTime.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Plotly from 'plotly.js';
import Plot from 'react-plotly.js';
import styles from './style.scss';
export default class ThresholdWindow extends Component {
static propTypes = {
name: PropTypes.string,
backtracks: PropTypes.object,
backtracksTime: PropTypes.object,
thresholds: PropTypes.object,
thresholdsTime: PropTypes.object,
currentTime: PropTypes.number,
}
constructor(props) {
super(props);
this.state = {
plotRevision: 0,
width: 0,
height: 0,
};
}
componentDidMount() {
const resizeObserver = new ResizeObserver(entries => {
const oldPlotRevision = this.state.plotRevision;
const rect = entries[0].contentRect;
this.setState({
plotRevision: oldPlotRevision + 1,
height: rect.height,
width: rect.width,
});
});
resizeObserver.observe(this.container);
}
shouldComponentUpdate(nextProps, nextState) {
if (this.state.plotRevision !== nextState.plotRevision) {
return true;
} else if (this.props.currentTime !== nextProps.currentTime) {
return true;
}
return false;
}
render() {
const data = [
{
name: 'Threshold',
type: 'scattergl',
mode: 'lines',
x: this.props.thresholdsTime,
y: this.props.thresholds,
side: 'above',
},
{
name: 'Backtracks',
type: 'scattergl',
mode: 'lines',
x: this.props.backtracksTime,
y: this.props.backtracks,
},
{
name: 'Current Time',
type: 'scattergl',
mode: 'lines',
x: [this.props.currentTime, this.props.currentTime],
y: [0, 1],
yaxis: 'y2',
},
];
return (
<div className={styles['threshold-window']} ref={(el) => { this.container = el; }}>
<Plot
divId={`backtracks-${this.props.name}`}
className={styles['threshold-graph']}
ref={(el) => { this.plot = el; }}
layout={{
width: this.state.width,
height: this.state.height,
yaxis: {
fixedrange: true,
},
yaxis2: {
side: 'right',
range: [0, 1],
},
margin: {
l: 35,
r: 15,
b: 20,
t: 15,
},
legend: {
orientation: 'h',
y: 1,
},
}}
revision={this.state.plotRevision}
data={data}
/>
</div>
);
}
}
Edit2: I don't actually see the currentTime line anywhere, so I'm pretty sure there's a bug somewhere.
With react-plotly.js the performance should be decent, as it will only redraw what it needs to.

Categories

Resources