-
Notifications
You must be signed in to change notification settings - Fork 326
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(touch): more optimized touch interactions #461
feat(touch): more optimized touch interactions #461
Conversation
✅ Deploy Preview for cornerstone-3d-docs ready!
To edit notification comments on pull requests, go to your Netlify site settings. |
Hi @Ouwen . Thanks for submitting this PR. Could you please add details as to how this PR can be tested? Thanks. |
70be5fa
to
7af3534
Compare
@jbocce I've updated the PR. The changes can be tested by running the example: |
state = JSON.parse(JSON.stringify(defaultState)); | ||
}, state.touchStartDelay - 10); | ||
tapState = JSON.parse(JSON.stringify(defaultTapState)); | ||
}, tapState.tapWindow); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should not the timeout time here be tapState.tapWindow - (currentTime - tapState.startTime.getTime())
? Having the timeout time as just tapState.tapWindow
would mean that the tap event would be (unnecessarily) delayed and thus the ultimate (UI/user) action would be delayed. Take this (exaggerated) example: take a single tap where the tap actually occurs at tapState.tapWindow - 1
ms. Then a timeout would be set for tapState.tapWindow
ms to fire the actual tap event. Thus the total time for the event to fire from the touch start is almost 2 * tapState.tapWindow
ms. Please verify that I have this correct. Apologies if I do not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the reason for the window is to aggregate multiple taps. If the user taps, there will always be a delay of tapWindowMs before the actual tap event fires.
In the case the user taps again within the TapWindow, the setTimeout would clear and wait for another tapWindowMs to pass before firing an event that 2 taps were heard.
The alternative approach to this is that we fire events every time a tap happens. This means a triple tap would actually be 3 events. This would prevent lag, but the tap events can get noisy. The windowing logic would then need to be pushed down for any listener of the tap event to set their own windows + timeouts.
Tap(1)
Tap(2)
Tap(3)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I understand why the aggregation and NO, we do NOT want the alternative approach you suggested.
I apologize because I think I misread the code before. However having both lines 379 and 391 are a bit confusing to me.
Line 379 together with the setTimeout
on line 417 sort of suggests to me that a single tap has to fall within 300ms (i.e. tapState.tapWindow), double tap has to fall within 600ms, etc. However line 391 to me reads, for example, if the second tap happens > 300ms after the first tap (and say 400ms) from the first tap , then do not do the double tap (i.e. return). JSYK I verified this via debugging whereby the debugger showed me that I did two taps within 351ms and I did NOT get a double tap. I honestly do not think we need both checks, but I could be wrong.
}; | ||
if (Math.abs(x) > state.swipeDistanceThreshold) { | ||
eventDetail.swipe = x > 0 ? Swipe.RIGHT : Swipe.LEFT; | ||
triggerEventCallback(eventDetail.element, TOUCH_SWIPE, eventDetail); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I read the code correct, it seems that both a TOUCH_SWIPE
and a TOUCH_DRAG
can be fired during the same interaction. Is that correct? If so, then that seems a bit surprising - I would expect them to be mutually exclusive. For example, one may want to swipe to navigate a stack - not, navigate and, say, pan. If I am mistaken, then ignore this comment. I suppose firing both events leaves it up to the embedding application whether both should be handled or just one. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's right, previously TOUCH_SWIPE and TOUCH_DRAG were mutually exclusive; however, this implementation required a setTimeout delay which makes either drag or swipe feel laggy. (Currently this is the case with Tap Event, see comment regarding whether to push down to embedding application layer)
Application layer will need to manage SWIPE/DRAG events happening together to have responsive touch.
For most use cases, drag and swipe gestures don't really conflict. And since drag event is clamped to 10 the affect is not that bad for a mis swipe or mis drag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having them mutually exclusive is fine - leave this as is.
Thanks @Ouwen for the updates - they definitely helped. Could you please add specific test cases to the description of the PR. For example... Test Swipe
I tried the example on my phone and I found the following...
Overall good work. Mouse and touch interactions are difficult things to get correct - so far so good. |
@jbocce thanks for taking the time to a through code review on this PR. Let me know what you think of my responses and I can make those changes. Main changes I'll be making:
Open discussion:
|
Yes please make the changes. I am not sure what you mean by the tap logic... you mean swipe logic right? If so then yes I agree. Also, in case you missed them please have a look at these overall comments I made #461 (comment). |
Hmmm, that's strange it might require faster swiping? Which device are you on?
We can change the tap to 400ms
Hmmm I think this makes sense. The tap logic currently waits for "tap chains" to complete. Instead, if a tap is outside the 48px it should start a new "tap chain" (as the follow up tap could make it a double tap) Regarding tap chains, would it be acceptable to include this as a TODO? |
I am using a Pixel 5 phone. I have (finally) managed to register swipes, but NOT consistently. It just does not feel quite right to me. Perhaps increase the time tolerance (slightly)? I feel like I am doing the same swipe gesture, yet it only detects an actual swipe like 1 out of 25 or so times. |
I am not entirely sure what you mean by creating a TODO for tap chains. However if it means that a new tap chain/sequence should be started once the previous distance tolerance has been exceeded then yes. I would say create an issue maybe? Here is what I would put as the issue details (feel free to edit/modify as you wish). I am providing this here just to be sure we are one the same page with "tap chains". 😊 Issue: Performing two taps within the (double) tap time tolerance but outside the tap distance tolerance should be registered as two individual taps. Detailed description: Whenever a tap falls within an existing tap sequence time tolerance BUT outside the tap distance tolerance, a new tap chain/sequence should be started. |
With regards to increasing the (double) tap tolerance to 400ms. Yes it makes sense for double tap, but a single tap should likely be much less than this. A similar thing is done for mouse events in mouseDownListener.ts. There is a clickDelay (i.e. single click time tolerance) of 200ms and the double click time tolerance is 400ms. Does this make sense? |
Ah I see, in this case it would make sense for touch to be 200ms in our use case. Since touch is also a little slower than click I think it may make sense to keep 300ms for sngle tap and thus 600ms for double tap (using the same language as the click implementation in this thread) |
I've increased the swipe tolerance from 200ms => 300ms which might help |
Issue regarding "Tap Chains" created and I've added a comment in the code! |
@jbocce ready for your review! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the changes @Ouwen. There is really only one left that concerns me slightly - but perhaps I am missing something.
@@ -104,9 +104,13 @@ const defaultState: ITouchStartListenerState = { | |||
|
|||
swipeDistanceThreshold: 48, | |||
swiped: false, | |||
swipeWindow: 200, // 200ms to swipe after touch start or no swipe trigger | |||
swipeToleranceMs: 300, // user has 300ms to swipe after touch start or no swipe will trigger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new value does not help my phone, nor another Android phone I tried and a Chromebook that I tried. So feel free to revert this change, but it is ok as it as well. Up to you. Thanks for trying it for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmmm that's really strange, if I manually open a browser window and go into mobile mode it can trigger
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well maybe the new tolerance is slightly better, but it still feels difficult to trigger. Let's just leave this as is for now.
@@ -376,7 +386,7 @@ function _onTouchEnd(evt: TouchEvent): void { | |||
function _checkTouchTap(evt: TouchEvent): void { | |||
const currentTime = new Date().getTime(); | |||
const startTime = state.startTime.getTime(); | |||
if (currentTime - startTime >= tapState.tapWindow) return; | |||
if (currentTime - startTime > tapState.tapToleranceMs) return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am still not convinced that both this line AND 401-402 are both needed. Why would they both be needed? They are practically identical considering line 435 resets the tap start time. I tried removing each and noticed no difference. Perhaps I am missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmmmm yeah, I think you are right we can just rely on the touch start start time for tolerance. We can remove new Date() tracking for the tap state
7c90c00
to
52f3968
Compare
@jbocce this is ready for your review! |
52f3968
to
cd866cc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test Swipe: