ReactNative PanResponder limit X position - android

I'm building a Music Player and I'm focusing on the progress bar.
I was able to react to swipe gestures, but I cant limit how far that gesture goes.
This is what I've done so far. I've reduced everything to the minumal:
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY()
};
}
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: (e, gestureState) => {
// Set the initial value to the current state
let x = (this.state.pan.x._value < 0) ? 0 : this.state.pan.x._value;
this.state.pan.setOffset({ x, y: 0 });
this.state.pan.setValue({ x: 0, y: 0 });
},
onPanResponderMove: Animated.event([
null, { dx: this.state.pan.x, dy: 0 },
]),
onPanResponderRelease: (e, { vx, vy }) => {
this.state.pan.flattenOffset();
}
});
}
render() {
let { pan } = this.state;
// Calculate the x and y transform from the pan value
let [translateX, translateY] = [pan.x, pan.y];
// Calculate the transform property and set it as a value for our style which we add below to the Animated.View component
let imageStyle = { transform: [{ translateX }, { translateY }] };
return (
<View style={styles.container}>
<Animated.View style={{imageStyle}} {...this._panResponder.panHandlers} />
</View>
);
}
Here there is an image showing what the problem is.
Initial position:
Wrong Position, limit exceeded:
So the idea is to stop keeping moving once the limit (left as well as right) is reached. I tried checking if _value < 0, but it didn't work since It seems to be an offset, not a position.
Well any help will be appreciated.

Instead of letting your animation die at your borders, you could interpolate your
Animated.Value with y=x, but with clamping it to your width.
return (
<View style={styles.container}>
<Animated.View
style={{
transform: [{
translateX: this.state.pan.x.interpolate({
inputRange: [0, trackWidth ],
outputRange: [0, trackWidth ],
extrapolate: 'clamp'
})
}],
}}
{...this._panResponder.panHandlers}
/>
</View>
);
Here's a more in-depth example: https://github.com/olapiv/expo-audio-player/blob/master/src/AudioSlider.js

onPanResponderMove: (e, gestureState)=> {
this.state.pan.x._value > 0 ? null : Animated.event([
null,
{dx: this.state.pan.x, dy: this.state.pan.y},
])(e, gestureState)
},

I was trying to do something similar; I wanted to have it so that you can pull the page part way and then release and it goes back to where it was.
My solution was this:
panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: (e, { dx }) => {
// This will make it so the gesture is ignored if it's only short (like a tap).
// You could also use moveX to restrict the gesture to the sides of the screen.
// Something like: moveX <= 50 || moveX >= screenWidth - 50
// (See https://facebook.github.io/react-native/docs/panresponder)
return Math.abs(dx) > 20;
},
onPanResponderMove: (e, gestureState) => (
// Here, 30 is the limit it stops at. This works in both directions
Math.abs(gestureState.dx) > 30
? null
: Animated.event([null, { dx: this.animatedVal }])(e, gestureState)
),
onPanResponderRelease: (e, { vx, dx }) => {
// Here, abs(vx) is the current speed (not velocity) of the gesture,
// and abs(dx) is the distance traveled (not displacement)
if (Math.abs(vx) >= 0.5 || Math.abs(dx) >= 30) {
doSomeAction();
}
Animated.spring(this.animatedVal, {
toValue: 0,
bounciness: 10,
}).start();
},
});

🍰
This method doesn't cancel the gesture handler if the user is still holding it down after exceeding the X limit.
Change MaxDistance & MinDistance to whatever values you like 😃
onPanResponderMove: (e, gestureState) => {
// Configure Min and Max Values
const MaxDistance = maxDistance;
const MinDistance = 0;
const dxCapped = Math.min(Math.max(parseInt(gestureState.dx), MinDistance), MaxDistance);
// If within our bounds, use our gesture.dx....else use dxCapped
const values = {}
if(gestureState.dx < MaxDistance && gestureState.dx > MinDistance){
values.dx = gestureState.dx
values.dy = gestureState.dy
}else{
values.dx = dxCapped
values.dy = gestureState.dy
}
//Animate Event
Animated.event([null, {
dx: pan.x,
dy: pan.y,
}])(e, values);
},
Hope this helps some folks. 🐱

A tricky point is that while I can not move the icon beyond that clamp but the pan.x value is indeed beyond the clamp limit although you don't see it. Then, when you want to move it back, you don't know how much swipe you need to move it back. This could be a nuance.
My solution is:
onPanResponderGrant: () => {
console.log("pan responder was granted access!")
pan.setOffset({
x: (pan.x._value>xMax)? xMax : (pan.x._value<xMin)? xMin: pan.x._value,
y: (pan.y._value>yMax)? yMax : (pan.y._value<yMin)? yMin: pan.y._value,
});
},
Then, can also console.log pan.x._value in following to double check.
onPanResponderRelease: () => {
pan.flattenOffset();}
I found this helpful for my own project. Note can only use pan.x_value not pan.x.
In my case I also used useMemo instead of useRef so the limits can be reset, which I learned from React Native's panResponder has stale value from useState?

There's a solution to a similar problem on this other question: https://stackoverflow.com/a/58886455/9021210
that can be repurposed for what you're looking for
const DRAG_THRESHOLD = /*configure min value*/
const DRAG_LIMIT = /*configure max value*/
onPanResponderMove: (e, gesture) => {
if ( (Math.abs( gesture.dy ) > DRAG_THRESHOLD) &&
(Math.abs( gesture.dy ) < DRAG_LIMIT ) )
{
return Animated.event([
null, {dx: 0, dy: pan.y}
]) (e, gesture)
}
},
This is not my answer, so I recommend you follow the link to see further explanation and if you like it upvote the original poster! :) I was trying to do the same thing and it worked for me! hope it helps.
P.S. I found that other solutions relying on checking the animation value rather than the gesture value would sometimes get stuck.

Related

Re-creating touch-based gestures with Playwright to test mobile viewports

Can someone help me re-create touch-based gestures(swipe, zoom, pinch, etc.) in Playwright to test emulated mobile devices? It has only a tap method out of the box, which works perfectly, but I need a whole set of functionality to test the mobile viewports of our web application.
I have tried so far a couple of scripts that I found online, which are not failing my tests but rather don't work for me for some reason:
await page.evaluate(() => {
const target = document.querySelector("#abc");
target.addEventListener('touchstart', (e) => {
console.log('touch start');
});
function simulateTouchEvent(element, type, touches) {
const touchEvents = [];
touches.forEach((touch) => {
touchEvents.push(new Touch({
clientX: touch.x,
clientY: touch.y,
identifier: touch.id,
target: element,
}));
});
element.dispatchEvent(new TouchEvent(type, {
touches: touchEvents,
view: window,
cancelable: true,
bubbles: true,
}));
}
simulateTouchEvent(target, 'touchstart', [{
id: "123",
x: 10,
y: 10,
}]);
simulateTouchEvent(target, 'touchend', [{
id: "123",
x: 10,
y: 10,
}]);
})
also this
const el = await page.locator(
".selector"
);
const dataTransfer = await page.evaluateHandle(
() => new DataTransfer()
);
await el.dispatchEvent("touchstart", { dataTransfer, steps: 5 });
await el.dispatchEvent("touchend", { dataTransfer, steps: 5 });
and
async function dispatchTouchEvent(
playWright: Page,
type: 'touchstart' | 'touchend' | 'touchcancel' | 'touchmove',
selector: string,
page?: Position,
screen?: Position,
client?: Position,
options?: PageExtraTouchOptions,
) {
await playWright.$eval(
selector,
(el, options) => {
const rect = el.getBoundingClientRect();
const {
client = {},
page = {},
screen = {},
type,
options: touchOpt,
} = options;
const touchObj = new Touch({
clientX: client.x,
clientY: client.y,
identifier: Date.now(),
pageX:
page.x || (client.x !== undefined ? rect.left + client.x : undefined),
pageY:
page.y || (client.y !== undefined ? rect.top + client.y : undefined),
screenX: screen.x,
screenY: screen.y,
target: el,
});
const touchEvent = new TouchEvent(type, {
bubbles: true,
cancelable: true,
...touchOpt,
changedTouches: [touchObj],
targetTouches: [touchObj],
touches: [touchObj],
});
return el.dispatchEvent(touchEvent);
},
{ client, options, page, screen, type },
);
}
await dispatchTouchEvent(
page,
"touchstart",
".selector"
);
await dispatchTouchEvent(
page,
"touchend",
".selector"
);
I recently have been also looking into Playwrights experimental android emulation feature, in the hopes that it might help me to emulate at least an android device, but haven't even been able to even run it because of ECONNREFUSED error.
Would appreciate any help, because I`m completely stuck in here.
Playwright version: 1.23;
OS: Ubuntu Neon KDE 20.04;

How to send mouse clicked coordinates from Flickable to ListView?

I'm developing an application for Android.
I have some ChartViews placed in the ListView.
Above ListView I have got a Flickable to scroll ChartViews synchronously in the x axis.
I need to show the coordinates of the point on the chart to which the user tap.
But I can't because flickable catches mouse click events and does not propagate it to other objects below.
I need the mouse event to come to a specific delegate that the user tapped on.
Is it possible to solve this problem?
Is there a way to get mouse coordinates during flick?
Is there a way to propagate mouse events to the ListView as if they came from the user?
Rectangle {
color: "#ffffff"
Component {
id: chartDelegate
Rectangle {
id: rootDelegRect
anchors {left: parent.left; right: parent.right }
height: 350
ChartView {
anchors { fill: parent;}
x: -10
y: -10
width: parent.width + 20
height: parent.height + 20
legend.visible: false
LineSeries {
name: "LineSeries"
axisX: ValueAxis {
min: 0
max: 4
labelFormat: "%.0f"
}
axisY: ValueAxis {
min: 0
max: 6
labelFormat: "%.0f"
}
XYPoint { x: 0; y: 0.0 }
XYPoint { x: 1; y: 5.2 }
XYPoint { x: 2; y: 2.4 }
XYPoint { x: 3; y: 0.1 }
XYPoint { x: 4; y: 5.1 }
}
}
MouseArea {
anchors.fill: parent
hoverEnabled :true
onMouseXChanged: console.log(mouseX,mouseY);//does not work when clicked
}
}
}
ListView { id: listViewCharts; objectName: "chart";
clip: true
anchors.top: parent.top; anchors.bottom: parent.bottom;
width: 1500
contentX:baseFlick.contentX; contentY:baseFlick.contentY;
model: listViewIdsModel
cacheBuffer: 1500
delegate: chartDelegate
}
Flickable {
id: baseFlick
anchors.fill: parent
contentHeight: listViewCharts.contentHeight
contentWidth: listViewCharts.width
}
}
Apparently this problem can only be solved using c++
To capture Flickable mouse events, do the following:
1) add the objectName of the object is Flickable
objectName: "baseFlick"
2) in c++ after creating a qml scene. Find the baseFlick object and Installs an event filter on this object
auto rootObj = engine.rootObjects().at(0);
auto item = rootObj->findChild<QQuickItem *>("baseFlick");
if(item!=nullptr){
item->installEventFilter(this);
} else qDebug()<<"ERR baseFlick item=nullptr";
3) implement the eventFilter virtual function
bool IDS2forUser::eventFilter(QObject *watched, QEvent *event)
{
...
return false;
}
In order to propagate mouse events to the ListView as if they came from the user.
In the eventFilter function:
1) Find the ListView (chart) object
auto rootObj = engine.rootObjects().at(0);
auto item = rootObj->findChild<QQuickItem *>("chart");
2) Find all of his children.
You need to look for children every time as their number may change.
Note, you should use the QQuickItem::childItems () function instead of QObject::children()
void IDS2forUser::getAllObjects(QQuickItem *parent, QList<QQuickItem *> &list) {
QList<QQuickItem *> children = parent->childItems();
foreach (QQuickItem *item, children) {
list.append(item);
getAllObjects(item,list);
}
}
3) Convert all coordinates to local coordinates of the object. And send all children events.
The whole function is presented below.
bool IDS2forUser::eventFilter(QObject *watched, QEvent *event)
{
auto rootObj = engine.rootObjects().at(0);
auto item = rootObj->findChild<QQuickItem *>("chart");
QList<QQuickItem *> list;
getAllObjects(item, list);
QQuickItem *watchedIt = dynamic_cast<QQuickItem *>(watched);
if(!watchedIt)return false;
QMouseEvent *mouseEvent = dynamic_cast<QMouseEvent *>(event);
if(!mouseEvent)return false;
foreach (QQuickItem *item, list) {
QPointF point=item->mapFromItem(watchedIt,mouseEvent->localPos());
if(point.x()<0 || point.y()<0 || point.x()>=item->width() || point.y()>=item->height())continue;
QMouseEvent mouseEvent2(mouseEvent->type(),
point,
mouseEvent->button(),mouseEvent->buttons(),Qt::NoModifier);
QCoreApplication::sendEvent(item, &mouseEvent2);
}
return false;
}

Animation transition with React Native doesn't work properly

I'm playing with react Native and I have a problem with the animation of transition.
Windows 10 - Hyper V, Visual Code Emulator Android - Ignite Boilerplate.
What I'm trying to do:
When I click, I want to show a circle with a scale animation from 0 to 2 on the click position.
What I have:
See the picture below (I have put a setTimout to see the first frame). On the first click, The component is displayed a first time very quickly with its natural width and height but the scale on 0,001 has no effect. After that, the animation begins from the scale 0,001 to 2.
With the other clicks, The circle start the first frames with the dimension of the previous circle. and then, the animation begins. But one more time, the scale : 0 has no effect at the first frame.
Here is the code of the Lunch screen:
export default class LaunchScreen extends Component {
state = {
clicks: []
};
handlePress(evt) {
console.log(evt.nativeEvent.locationX, evt.nativeEvent.locationY)
let xCoord = evt.nativeEvent.locationX;
let yCoord = evt.nativeEvent.locationY;
this
.state
.clicks
.push({x: xCoord, y: yCoord});
this.setState({clicks: this.state.clicks});
}
renderClick() {
if (this.state.clicks.length > 0) {
return this
.state
.clicks
.map((item, i) =>< ClickAnimation key = {
item.x
}
x = {
item.x
}
y = {
item.y
} />)
} else {
return <View/>
}
}
render() {
return (
<View style={styles.container}>
<ScrollView
style={styles.scrollView}
horizontal={true}
showsHorizontalScrollIndicator={true}
contentContainerStyle={styles.scrollView}>
<TouchableWithoutFeedback
style={styles.touchableWithoutFeedback}
onPress=
{evt => this.handlePress(evt)}>
<View style={styles.scrollView}>
{this.renderClick()}
</View>
</TouchableWithoutFeedback>
</ScrollView>
</View>
)
}
}
And here the component of the circle:
import React from 'react';
import PropTypes from 'prop-types';
import {Animated, View, Easing} from 'react-native';
export default class ClickAnimation extends React.Component {
constructor() {
super();
console.log(this.state)
this.state = {
scaleAnim: new Animated.Value(0.001);
};
}
componentDidMount() {
Animated
.timing(this.state.scaleAnim, {
toValue: 2,
duration: 2000
})
.start();
.scaleAnim
}
render() {
return (<Animated.View
style={{
zIndex: 10,
borderColor: "blue",
borderRadius: 400,
borderWidth: 1,
position: "absolute",
top: this.props.y,
left: this.props.x,
width: 60,
height: 60,
backgroundColor: "red",
transform: [
{
scaleY: this.state.scaleAnim
? this.state.scaleAnim
: 0
}, {
scaleX: this.state.scaleAnim
? this.state.scaleAnim
: 0
}
]
}}/>);
}
}
Do you have an Idea why this is so?
I have found the solution.
This come with this issue:
https://github.com/facebook/react-native/issues/6278
I had seen it and this is why I wrote 0,001. But 0,001 is still to little. With 0,01 it works great.
So the answer is:
Just replace 0.001 by 0.01 because it was too little.

React native android crashes on device during animations

Running RN v0.40.0 on a Physical device on Android 5.1. I'm trying to animate a text to appear with fade-in and slide up in the following way:
export default class Example extends PureComponent {
constructor(props) {
super(props);
this.translate = new Animated.Value(-15);
this.fade = new Animated.Value(0);
}
componentWillReceiveProps() {
setTimeout(() => {
Animated.timing(this.translate, {
toValue: 0,
duration: 800,
easing: Easing.inOut(Easing.ease),
}).start();
Animated.timing(this.fade, {
toValue: 1,
duration: 800,
easing: Easing.inOut(Easing.ease),
}
).start();
}, 150);
}
render() {
return (
<View>
<Animated.View
style={{
transform: [
{translateY: this.translate},
],
opacity: this.fade
}}
>
<Text>
{this.props.text}
</Text>
</Animated.View>
</View>
);
}
And after I reload JS bundle from the dev menu and go to that view app crashes with no error log, sometimes showing Application ... stopped working, sometimes not. If I start the app from the android menu again it loads ok, crashes only for the first time. It definitely has something to do with animations since before I introduced animations I had no crashes. There are no logs, no clues, please, give me some advice what could that be and what should I try and what should I check. Thanks.
Btw, on that view with animations I have a pretty heavy background image (~400k) could that be a problem?
UPD: I have narrowed it down to that it crashes when I'm trying to run animations in parallel, either with setTimeout or with Animation.parallel. What could be the problem?
Not sure what's causing the crash, but using Animated.parallel has worked for me:
Animated.parallel([
Animated.spring(
this.state.pan, {
...SPRING_CONFIG,
overshootClamping: true,
easing: Easing.linear,
toValue: {x: direction, y: -200}
}),
Animated.timing(
this.state.fadeAnim, {
toValue: 1,
easing: Easing.linear,
duration: 750,
}),
]).start();
where SPRING_CONFIG is something like
var SPRING_CONFIG = {bounciness: 0, speed: .5};//{tension: 2, friction: 3, velocity: 3};
and pan and fadeAnim values are set in the constructor this.state values as:
pan: new Animated.ValueXY(),
fadeAnim: new Animated.Value(0),
with the animated View as
<Animated.View style={this.getStyle()}>
<Text style={[styles.text, {color: this.state.textColor}]}>{this.state.theText}</Text>
</Animated.View>
and the getStyle function is
getStyle() {
return [
game_styles.word_container,
{opacity: this.state.fadeAnim},
{transform: this.state.pan.getTranslateTransform()}
];
}
This tutorial helped me set this up...good luck!

React Navigation: StackNavigator transition for Android

I am using this library https://reactnavigation.org/docs/intro/ to build android by react-native. I can make the navigation happens on android device but how I can make the screen slide in from the right and fade in from the left. It seems that this behaviour happens on iOS device but not in Android. Is there any animation configuration for android app?
Please see below animation. This is recorded in iOS.
Starting from : "#react-navigation/native": "^5.5.1",
import {createStackNavigator, TransitionPresets} from '#react-navigation/stack';
const TransitionScreenOptions = {
...TransitionPresets.SlideFromRightIOS, // This is where the transition happens
};
const CreditStack = createStackNavigator();
function CreditStackScreen() {
return (
<CreditStack.Navigator screenOptions={TransitionScreenOptions}> // Don't forget the screen options
<CreditStack.Screen
name="Credit"
component={HomeScreen}
options={headerWithLogo}
/>
<HomeStack.Screen
name="WorkerDetails"
component={WorkerDetails}
options={headerWithLogoAndBackBtn}
/>
</CreditStack.Navigator>
);
}
You can watch this video to understand more:
https://www.youtube.com/watch?v=PvjV96CNPqM&ab_channel=UnsureProgrammer
You should use transitionConfig to override default screen transitions as written on this page.
Unfortunately there is no example provided how that function works but you can find some examples in this file: \react-navigation\lib\views\CardStackStyleInterpolator.js
So your code should look like this:
const navigator = StackNavigator(scenes, {
transitionConfig: () => ({
screenInterpolator: sceneProps => {
const { layout, position, scene } = sceneProps;
const { index } = scene;
const translateX = position.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [layout.initWidth, 0, 0]
});
const opacity = position.interpolate({
inputRange: [
index - 1,
index - 0.99,
index,
index + 0.99,
index + 1
],
outputRange: [0, 1, 1, 0.3, 0]
});
return { opacity, transform: [{ translateX }] };
}
})
});
For StackNavigatoin 6.x.x
Just import
import { TransitionPresets } from '#react-navigation/stack';
Then create a config:
const screenOptionStyle = {
// headerShown: false,
...TransitionPresets.SlideFromRightIOS,
};
And finally just assign them to the Stack Navigator Screen Options:
<Stack.Navigator
screenOptions={screenOptionStyle}
>
<Stack.Screen
...
...
All the above answers are correct, but the solutions work ONLY if you are using createStackNavigator, and not if you are using createNativeStackNavigator; unfortunatelly, if you are following the get started section from react-navigation's docs, you will end up using the latter.
Here you can find a SO question speaking about the differences between the two, but the most relevant one for this questions is that many of the options that your can pass to the former (such as transitionConfig), cannot be passed to the latter.
If you are using createNativeStackNavigator this is how you can do it:
import { createNativeStackNavigator } from '#react-navigation/native-stack'
const StackNavigator = createNativeStackNavigator()
const MyNativeStackNavigator = () =>{
return <StackNavigator.Navigation
screenOptions={{
animation: 'slide_from_right', //<-- this is what will do the trick
presentation: 'card',
}}
>
{routes}
</StackNavigator.Navigator>
}
you need to import StackViewTransitionConfigs from 'react-navigation-stack'
then, override the transitionConfing function.
const myStack = createStackNavigator({
Screen1,
Screen2,
Screen3
},{
transitionConfig: () => StackViewTransitionConfigs.SlideFromRightIOS
}
On #react-navigation/stack component version, the way to do a slide from the right animation is:
<Stack.Navigator
screenOptions={{
cardStyleInterpolator: ({index, current, next, layouts: {screen}}) => {
const translateX = current.progress.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [screen.width, 0, 0],
});
const opacity = next?.progress.interpolate({
inputRange: [0, 1, 2],
outputRange: [1, 0, 0],
});
return {cardStyle: {opacity, transform: [{translateX}]}};
},
}}>
<Stack.Screen name="MainScreen" component={MainScreen} />
...
</Stack.Navigator>
Better you can use the react native navigation for this. You can configure your screen using configureScene method. Inside that method use Navigator.SceneConfigs for animating screen. It's work for both android and iOS.
You can get useful information from index.d.ts file, find the export interface TransitionConfig , then press 'Ctrl' & left_click on NavigationTransitionSpec and NavigationSceneRendererProps, then you can get everything you want.

Categories

Resources