Pan events in ios 13 aren't behaving as expected - swipe

I'm trying to fix a bug that appears only on iOS13 with a horizontally scrolling image gallery. We're using Hammer.js, and users are able to swipe left (to get the next image) properly, but they aren't reliably able to swipe right (to get the previous image). As I said, this is iOS 13, on both Safari and Chrome.
I set up logs to check the pan events, and here's what happens.
If you drag your finger to the left, the deltaX (distance on the x-axis) behaves appropriately - the distance goes something like -11, -22, -41, -58, -68, -84, -101, -114, -124, and we hit the next slide function.
But if you drag your finger to the right, the delta-X goes something like 21, 27, 42, 67, 88, 102, 124, 138, 142, -61. That last -61 doesn't trigger the previous slide function because it's too small (we only trigger previous or next if your pan is greater than 1/4 the screen width) And it's in the wrong direction, so occasionally swiping to previous triggers the next slide!
I can't figure out where that random last number comes from. It's generally less than 100 and can be positive or negative. And it only happens when panning to the right. Occasionally (1 out of 10 times) we don't get that last wonky number and swiping right works, but it's rare and doesn't seem related to velocity or distance.
The code is super simple:
mc.on('panstart', function(e) {
//overlay stuff
});
mc.on('pan', function(e) {
//debounced overlay stuff for prefetching the next slide
});
mc.on('panend pancancel', function(e) {
if (Math.abs(e.deltaX) > gallery.width / 4) {
if (e.deltaX < 0) {
next();
} else {
prev();
}
} else {
//do nothing
}
});

Related

Why does 'offset' exist in React Native Panresponder?

TL;DR: I am using the Panresponder code from the React Native docs, and need help understanding why the 'offset' value is used, as opposed to just using the animated value.
Full Question:
The Scenario:
I am using a Panresponder in React Native to drag and drop objects around the screen. I am using standard code from the RN docs.
Basically, the draggable object has an animated position value. When you click the object, the offset on that animated value is set to the animated value, and the animated value is set to zero. As you drag, the animated value is incrementally set to the magnitude of how far it has been dragged in that gesture. When you release the object, the offset is added to the animated value, and the offset is then set to zero.
Example:
For example, if the object starts from position 0, then initially both the animated value and the offset are set to 0. If you drag the object by 100px, the animated value gradually increases from 0 to 100 as you drag. When you release, the zero offset is added to the animated value (so nothing happens). If you click the object again, the offset is set to 100, and the animated value is re-set to 0. If you drag the object another 50px, the animated value increases from 0 to 50. When you release the object, the 100 offset is added to the animated value, which becomes 150, and the offset is re-set to zero.
In this way, the animated value always holds the distance dragged in the current gesture, with the offset saving the position that the object was at before the current drag gesture started, and when you release the object, that saved offset value is tacked onto the animated value, so that when the object is at rest, the animated value contains the total distance that the object has been dragged by all gestures combined.
Code:
Here's the code I'm using to do this:
this.animatedValue.addListener((value) => this._value = value); // Make this._value hold the value of this.animatedValue (essentially extract the x and y values from the more complex animatedValue)
this.panResponder = PanResponder.create({
onPanResponderGrant: () => { // When user clicks to initiate drag
this.animatedValue.setOffset({ // Save 'distance dragged so far' in offset
x: this._value.x,
y: this._value.y,
})
this.animatedValue.setValue({ x: 0, y: 0}) // Set this.animatedValue to (0, 0) so that it will hold only 'distance so far in current gesture'
},
onPanResponderMove: Animated.event([ // As object is dragged, continually update animatedValue
null, { dx: this.animatedValue.x, dy: this.animatedValue.y}
]),
onPanResponderRelease: (e, gestureState) => { // On release, add offset to animatedValue and re-set offset to zero.
this.animatedValue.flattenOffset();
}
}
My Question:
This code works perfectly well. When I don't understand though, is why do we need the offset? Why do we need to re-set the animated value to zero on every new gesture, save its value in offset, and re-add that to the animated value after it's finished being dragged? When the object is released, it ends up just holding the total distance dragged, so why not just use the animated value and not use the offset? With my example above, why not just increment animated value to 100 when you drag it 100px, then when you click and drag it again, keep updating the animated value?
Possible Solution:
The only advantage I can think of to using the offset is that animatedValue will now allow you to keep track of the 'distance so far in current gesture', as opposed to just 'total distance so far over all gestures combined'. There might be a scenario in which you need the 'distance so far in current gesture' value, so I'm wondering if this is the only reason to ever use the offset, or is there a more fundamental reason I'm missing why we should use it all the time?
Any insight would be great.
Thanks!
Actually the logic isn't right in the example you used because it's just a partial example using flattenOffset that isn't meant to be used for standard drag/drop behaviour (see the bottom paragraph: https://animationbook.codedaily.io/flatten-offset/):
Because we reset our offset and our animated value in the onPanResponderGrant, the call to flattenOffset is unnecessary, here. However, in the case where we want to trigger an animation from the released location to another location, flattenOffset is required.
The whole point of the offset is that you don't need to keep track of the absolute position value in a separate variable. So you were right to doubt the need for the offset given that you where storing the absolute position in this._value.
At the beginning of a drag, the x/y values of the Animated.Value start from [0, 0], so the drag is relative to the starting position:
offset + [0, 0] = absolute position at the beginning of a drag
offset + [x, y] = absolute position at the end of the drag
For the next drag to start at the right position, you just need to add [x, y] to the offset, which is done by extractOffset():
this.panResponder = PanResponder.create({
// Allow dragging
onStartShouldSetPanResponder: (e, gesture) => true,
// Update position on move
onPanResponderMove: (e, gestureState)=> {
Animated.event([
null,
{dx: this.animatedValue.x, dy: this.animatedValue.y},
])(e, gestureState)
},
// Update offset once we're done moving
onPanResponderRelease: (e, gestureState)=> {
this.animatedValue.extractOffset();
}
});
Thanks to the offset, you don't need this._value anymore to get the proper drag behaviour.
Because it's better to have the entire animated value's state be self-contained, so you can pass its value to a transform. Of course maybe you don't want the "total distance travelled" in which case, well, don't use offsets, but if you do, using AnimatedValue's offset is the best solution.
Let me show you why by coding up an example of tracking the total distance travelled between touches without using the built-in offset:
this.offsetValue = {x: 0, y:0};
this.panResponder = PanResponder.create({
onPanResponderGrant: () => { // When user clicks to initiate drag
this.animatedValue.setValue({ x: 0, y: 0}) // Set this.animatedValue to (0, 0) so that it will hold only 'distance so far in current gesture'
},
onPanResponderMove: Animated.event([ // As object is dragged, continually update animatedValue
null, { dx: this.animatedValue.x, dy: this.animatedValue.y}
]),
onPanResponderRelease: (e, gestureState) => {
// Set the offset to the current position
this.offsetValue = {x: gestureState.dx, y: gestureState.dy}
// Reset our animatedvalue since the offset is now all good
this.animatedValue.setValue({ x: 0, y: 0})
}
}
This works, and it's less code you now have the raw value for the current touch in Animated.Value and if you want the total distance moved you can use this.offsetValue. Except... how do you apply it to get the total distance exactly? You might think you can do this:
<Animated.View
style={{
transform: [
{ translateX: this.offset.x + this.animatedValue.x },
{ translateY: this.offset.y + this.animatedValue.y },
],
}}
{...this.panResponder.panHandlers}
/>
But this will be an error because animatedValue.x isn't a number obviously. You could use ._value directly but then what's the point of using Animated? The entire idea is that you can pass a single Animated object to a transform property. So that's why you simply use the object's internal offset.

Highcharts conditionally disable marker on hover

New to highcharts - I've got a chart that I have markers disabled on series
plotOptions: {
series: {
marker: {
enabled: false
}
}
},
which is great for the drawing of the lines, but, when I hover over the chart the markers are there. this is good, however, I do not want the marker to be present if the y value on the xAxis is 0 (empty) but I do want them when the y value on the xAxis is greater than one.
I was able to do this before by just setting the y value to null, disabling hover for that series, but the nulls present on this series - drawn as a stacked areaspline graph causes the spline to get drawn improperly (no spline, jagged edges when using the connectNulls: true option in series.
So how do I, and/or can I, conditionally disable a marker on hover based on the y value on an xAxis?
I have been looking at wrapping highcharts prototypes, which I am already doing to control some crosshair behavior drawCrosshair(): https://www.highcharts.com/docs/extending-highcharts/extending-highcharts but I can't seem to find anything that controls the drawing of the markers at that level
A very static approach to this is simply addressing each point with Y-value of 0. Then you could disable the hover-marker for each of these. One such point would look like this (JSFiddle demo):
{
y:0,
marker:{
states:{
hover:{
enabled:false
}
}
}
}
And used in a series:
series: [{
marker: {
enabled: false
},
data: [3, {y:0, marker:{states:{hover:{enabled:false}}}}, 3, 5, 1, 2, 12]
}]
As you can see, it's not pretty, but might help those who need an ad-hoc solution.
A dynamic approach to this is intercepting the setState-function of the Point.
For example, wrapping it and preventing the handling if the y-value is 0:
(function (H) {
H.wrap(H.Point.prototype, 'setState', function (proceed, state, move) {
if(this.y === 0) return;
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
});
}(Highcharts));
See this JSFiddle demonstration of it in action.

d3 line chart data point is cropped from min and max domain values

i am working on d3 charts with react and new on both react and d3. The issue i am trying to solve is;
I have a line chart and i have useInteractiveGuideline enabled, so when i hover over the graph, i can see what my value is on that point(with a circle on the line). The problem is; for the y axis values that are close to min or max grid lines, the data point is cropped just from the edge domain values and becomes an half circle.
line chart with cropped data points
I need some sort of padding to see the whole point on edges.
Is there any way to do that other than changing the domain values? I don't wanna change the domain values since my data is dynamic and can reach to very big numbers, so it may not be stable.
nv.addGraph(() => {
let chart = nv.models.lineChart()
.padData(true)
.noData('')
.margin({ "top": 20, "right": 20, "bottom": 20, "left": 20 })
.yDomain([0, max])
.useInteractiveGuideline(true);
chart.xAxis
.showMaxMin(false)
.ticks(5)
.tickFormat((d) => {
return ticks[d]
})
chart.yAxis.tickFormat((d) => {
return d;
})
d3.select('#chart')
.datum(chartData)
.transition().duration(700)
.call(chart);
return chart;
});
I've added below line and issue is solved
chart.lines.clipEdge(false);

How to use Sprite class in Microsoft.DirectX to draw text and allow the mouse to rotate the screen?

I am using Microsoft.DirectX.Direct3D and Microsoft.DirectX to render some data in 3D.
Now, I would like to draw text to indicate the height value inside the space. As shown the picture below:
Now I am facing two main problems:
Problem 1: As you can see, the draw text is set upside down. What can I do to make it to be show correctly?
Problem 2: I am getting Direct3DXException error when I tried to rotate the screen. The error message as per below:
Below is my code for DrawText() using Sprite class
Microsoft.DirectX.Direct3D Device = new Device(0, DeviceType.Hardware, this.panel1,
CreateFlags.HardwareVertexProcessing, presentationParameters);
private void Render()
{
device.BeginScene();
if(checkBox1.checked)
{
DrawText();
}
device.EndScene();
device.SetTexture(0, null);
device.Present();
}
private void DrawText()
{
Sprite fontsprite = new Sprite(device);
fontsprite.Begin(SpriteFlags.AlphaBlend | SpriteFlags.ObjectSpace);
_font = new Microsoft.DirectX.Direct3D.Font(device, 10, 10, FontWeight.Bold, 1, false, CharacterSet.Ansi, Precision.Default, FontQuality.ClearType, PitchAndFamily.DefaultPitch, "face1");
_font.DrawText(fontsprite, "1.5 Km", 120, 15, Color.Black);
_font.DrawText(fontsprite, "2.5 Km", 120, 25, Color.Black);
_font.DrawText(fontsprite, "3.5 Km", 120, 35, Color.Black);
fontsprite.Transform.Translate(120, 100, 100);
fontsprite.End();
}
Need some help on resolving problem 1 and problem 2. Thank you.
Update: Problem 1 has been solved by myself. Now left problem 2 only.
Code to solve problem 1:
fontsprite.Transform = Matrix.RotationZ((float)Math.PI);
fontsprite.Transform *= Matrix.Translation(heightBoxSize, heightValue, heightBoxSize);
fontsprite.Transform.Translate(100, 100, 100);
I think it is because high memory consumption because you are creating new Sprite in every frame. (and 60 frames for each second?)
This problem happened where, rotating the screen using the mouse event, it will constantly update the result of device in frontsprite.
Therefore I suggest you to move part of the codes out.
Move the declaration part to global and then move initialization to Initialization() part.
Also move the fontsprite.End() to the Final() part.
I hopes that help.

KineticJS - update drawFunc after an object on another stage has been moved

With the help of a fellow stackoverflow user, I am able to change the position of a two lines and a circle on the stage using the following:
var circle2 = new Kinetic.Circle({
drawFunc: function(canvas) {
var context2 = canvas.getContext();
var centerX2 = blueLine2.getPosition().x;
centerY2 = greenLine2.getPosition().y;
context2.drawImage(gArrow2, -156, -23 + centerY2, 11, 23);
context2.drawImage(gArrow2, 156, -23 + centerY2, 11, 23);
context2.drawImage(bArrow2, centerX2, 156, 23, 11);
context2.drawImage(bArrow2, centerX2, -156, 23, 11);
context2.beginPath();
context2.arc(centerX2, centerY2, this.getRadius(), 0, 2 * Math.PI, false);
context2.lineWidth = this.getStrokeWidth();
context2.strokeStyle = this.getStroke();
context2.stroke();
},
x: cx + gx,
y: cy + gy,
radius: 70,
stroke: '#00ffff',
strokeWidth: 3,
opacity: 0.5
});
layer2.add(circle2);
It works great, now my challenge is if I move a line on a second stage for example the horizontal line, I can also move the horizonal line on the first stage using:
greenLine2.on('dragend', function (event) {
var y1 = greenLine2.getPosition().y;
greenLine3.setPoints([0, 256 + y1, 512, 256 + y1]);
centerY3 = 256 + y1;
layer3.draw();
layer2.draw();
});
However I can't update the layer to move also the vertical line and the circle. I would appreciate your suggestions, thanks in advance.
Lets say that greenLine2 is the one you're moving, and you want greenLine3 to move to the same position on the other stage. I'm going to assume the stages are the same size, but you can change up the code to account for these changes.
greenLine2.on('dragmove', function (event) {
var userPos = stage.getUserPosition(); //if this doesnt work the way you need, try a different approach, such as below:
//var userPos = greenLine.getPosition(); //other possibility
greenLine3.setPosition(userPos);
layer3.draw();
layer2.draw();
});
and if you want other things to move as well, you can do the same kind of code using .setPosition() with some offset so that the drawing is relative.
Another approach would be to create a group in each stage, and make the group draggable, that way, you can drag all the items in a group at the same time, and synchronously across stages.

Resources