Browse Source

Platform: handle (multi-)touch input in AndroidApplication.

As usual, the most trash fire platform of them all. Ugh. I chose to
ignore certain aspects and suggestions and made it behave more like
Emscripten and SDL2, because that makes more sense to me.

Co-authored-by: nodoteve <nodoteve@yandex.com>
pull/651/head
Vladimír Vondruš 2 years ago
parent
commit
005d4c224f
  1. 17
      doc/changelog.dox
  2. 2
      doc/credits.dox
  3. 345
      src/Magnum/Platform/AndroidApplication.cpp
  4. 179
      src/Magnum/Platform/AndroidApplication.h
  5. 2
      src/Magnum/Platform/EmscriptenApplication.cpp
  6. 2
      src/Magnum/Platform/EmscriptenApplication.h
  7. 8
      src/Magnum/Platform/Sdl2Application.cpp
  8. 2
      src/Magnum/Platform/Sdl2Application.h
  9. 21
      src/Magnum/Platform/Test/AndroidApplicationTest.cpp

17
doc/changelog.dox

@ -339,14 +339,17 @@ See also:
@relativeref{Platform::GlfwApplication,tickEvent()} to match the interface
of @ref Platform::Sdl2Application (see [mosra/magnum#577](https://github.com/mosra/magnum/issues/577)
and [mosra/magnum#580](https://github.com/mosra/magnum/pull/580))
- Multi-touch support in @ref Platform::Sdl2Application and
@ref Platform::EmscriptenApplication through new
- Multi-touch support in @ref Platform::Sdl2Application,
@ref Platform::EmscriptenApplication and
@ref Platform::AndroidApplication through new
@relativeref{Platform::Sdl2Application,PointerEvent} and
@relativeref{Platform::Sdl2Application,PointerMoveEvent} that unify mouse
and touch input events. This also means @ref Platform::EmscriptenApplication
finally supports touch drag ([mosra/magnum#532](https://github.com/mosra/magnum/issues/532))
--- no touch-to-mouse event passthrough needs to be implemented, it works
out of the box with the new pointer events.
@relativeref{Platform::Sdl2Application,PointerMoveEvent} that unify mouse,
pen and touch input events. This also means
@ref Platform::EmscriptenApplication finally supports touch drag
([mosra/magnum#532](https://github.com/mosra/magnum/issues/532)) --- no
touch-to-mouse event passthrough needs to be implemented, it works out of
the box with the new pointer events. The Android implementation is
building upon [mosra/magnum#527](https://github.com/mosra/magnum/pull/527).
@subsubsection changelog-latest-new-scenegraph SceneGraph library

2
doc/credits.dox

@ -205,6 +205,8 @@ Are the below lists missing your name or something's wrong?
library additions
- **Nick Skelsey** ([\@NSkelsey](https://github.com/NSkelsey)) ---
documentation copy-editing
- **[\@nodoteve](https://github.com/nodoteve)** --- initial multi-touch
support in @ref Platform::AndroidApplication
- **[\@LB--](https://github.com/LB--)** --- warning fixes, Windows
buildsystem improvements
- **Olga Turanksaya** ([\@olga-python](https://github.com/olga-python)) ---

345
src/Magnum/Platform/AndroidApplication.cpp

@ -4,6 +4,7 @@
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
2020, 2021, 2022, 2023, 2024
Vladimír Vondruš <mosra@centrum.cz>
Copyright © 2021 nodoteve <nodoteve@yandex.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
@ -256,25 +257,79 @@ AndroidApplication::Pointers motionEventButtons(AInputEvent* event) {
return pointers;
}
AndroidApplication::Pointers motionEventPointers(AInputEvent* event, const AndroidApplication::Pointers pressedButtons) {
switch(AMotionEvent_getToolType(event, 0)) {
Containers::Pair<AndroidApplication::PointerEventSource, AndroidApplication::Pointers> motionEventPointers(AInputEvent* event, std::size_t i, const AndroidApplication::Pointers pressedButtons) {
switch(AMotionEvent_getToolType(event, i)) {
case AMOTION_EVENT_TOOL_TYPE_MOUSE:
/** @todo MouseButton4 / MouseButton5, once they're added &
tested */
return (AndroidApplication::Pointer::MouseLeft|
AndroidApplication::Pointer::MouseMiddle|
AndroidApplication::Pointer::MouseRight) & pressedButtons;
return {AndroidApplication::PointerEventSource::Mouse,
(AndroidApplication::Pointer::MouseLeft|
AndroidApplication::Pointer::MouseMiddle|
AndroidApplication::Pointer::MouseRight) & pressedButtons};
case AMOTION_EVENT_TOOL_TYPE_FINGER:
return AndroidApplication::Pointer::Finger;
return {AndroidApplication::PointerEventSource::Touch,
AndroidApplication::Pointer::Finger};
case AMOTION_EVENT_TOOL_TYPE_STYLUS:
/** @todo use pressedButtonsPointers once there's additional pen
button enum values */
return AndroidApplication::Pointer::Pen;
return {AndroidApplication::PointerEventSource::Pen,
AndroidApplication::Pointer::Pen};
case AMOTION_EVENT_TOOL_TYPE_ERASER:
return AndroidApplication::Pointer::Eraser;
return {AndroidApplication::PointerEventSource::Touch,
AndroidApplication::Pointer::Eraser};
case AMOTION_EVENT_TOOL_TYPE_UNKNOWN:
default:
return AndroidApplication::Pointer::Unknown;
return {AndroidApplication::PointerEventSource::Unknown,
AndroidApplication::Pointer::Unknown};
}
}
template<class T> Vector2 updatePreviousTouch(T(&previousTouches)[32], const std::int32_t id, const Containers::Optional<Vector2>& position) {
std::size_t firstFree = ~std::size_t{};
for(std::size_t i = 0; i != Containers::arraySize(previousTouches); ++i) {
/* Previous position found */
if(previousTouches[i].id == id) {
/* Update with the current position, return delta to previous */
if(position) {
const Vector2 relative = *position - previousTouches[i].position;
previousTouches[i].position = *position;
return relative;
/* Clear previous position */
} else {
previousTouches[i].id = ~Int{};
return {};
}
/* Unused slot, remember in case there won't be any previous position
found */
} else if(previousTouches[i].id == ~Int{} && firstFree == ~std::size_t{}) {
firstFree = i;
}
}
/* If we're not resetting the position and there's a place where to put the
new one, save. Otherwise don't do anything -- the touch that didn't fit
will always report as having no relative position. */
if(position && firstFree != ~std::size_t{}) {
previousTouches[firstFree].id = id;
previousTouches[firstFree].position = *position;
}
return {};
}
/* Unlike e.g. SDL, which guarantees that pointer IDs are unique among all
pointer types, here they of course don't care. So use the reported ID only
for touches and artificial constants for the rest. */
std::int32_t pointerIdForSource(AndroidApplication::PointerEventSource source, std::int32_t id) {
switch(source) {
case AndroidApplication::PointerEventSource::Touch:
return id;
case AndroidApplication::PointerEventSource::Mouse:
return -1;
case AndroidApplication::PointerEventSource::Pen:
return -2;
case AndroidApplication::PointerEventSource::Unknown:
return -3;
}
}
@ -287,9 +342,46 @@ std::int32_t AndroidApplication::inputEvent(android_app* state, AInputEvent* eve
const std::int32_t action = AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_MASK;
switch(action) {
case AMOTION_EVENT_ACTION_DOWN:
case AMOTION_EVENT_ACTION_UP: {
const Vector2 position{AMotionEvent_getX(event, 0),
AMotionEvent_getY(event, 0)};
case AMOTION_EVENT_ACTION_POINTER_DOWN:
case AMOTION_EVENT_ACTION_UP:
case AMOTION_EVENT_ACTION_POINTER_UP: {
/* Figure out which pointer actually changed in given event,
because OF COURSE the API is so horrible that this is
non-trivial. For AMOTION_EVENT_ACTION_DOWN we assume it's
the first ever pointer being pressed, and thus the count
being 1, thus the pointer that changed is the first and
only. */
std::int32_t pointerChanged;
if(action == AMOTION_EVENT_ACTION_DOWN) {
CORRADE_INTERNAL_ASSERT(AMotionEvent_getPointerCount(event) == 1);
pointerChanged = 0;
/* For AMOTION_EVENT_ACTION_UP it's ... apparently the last
remaining pointer going up. Not the primary one (see below
for the `primary` bit decision tree). The docs make it look
like the event also contains any other pointers, but it's
probably just mentioning the AMotionEvent_getHistoricalX()
etc. fields? Er? Why is it not mentioning that for for
AMOTION_EVENT_ACTION_POINTER_UP then?
https://developer.android.com/reference/android/view/MotionEvent#ACTION_UP */
} else if(action == AMOTION_EVENT_ACTION_UP) {
CORRADE_INTERNAL_ASSERT(AMotionEvent_getPointerCount(event) == 1);
pointerChanged = 0;
/* The AMOTION_EVENT_ACTION_POINTER_DOWN/_UP actually mean a
secondary pointer was pressed or released. Who would have
thought. In that case, the actual changed pointer is given
to us with this fucking atrocity of a bitmask. Well,
alright, what can I do, but why such a bitmask couldn't be
done above as well, huh??? */
} else if(action == AMOTION_EVENT_ACTION_POINTER_DOWN ||
action == AMOTION_EVENT_ACTION_POINTER_UP) {
pointerChanged = (AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
} else CORRADE_INTERNAL_ASSERT_UNREACHABLE();
const bool press =
action == AMOTION_EVENT_ACTION_DOWN ||
action == AMOTION_EVENT_ACTION_POINTER_DOWN;
const Vector2 position{AMotionEvent_getX(event, pointerChanged),
AMotionEvent_getY(event, pointerChanged)};
/* Query the currently pressed buttons. If this is not a mouse
event, it'll give back garbage, but that's fine as we won't
@ -297,10 +389,67 @@ std::int32_t AndroidApplication::inputEvent(android_app* state, AInputEvent* eve
a release, use the previously recorded pointers to figure
out what was actually pressed. */
const Pointers pressedButtons = motionEventButtons(event);
const Pointers pointers = motionEventPointers(event,
action == AMOTION_EVENT_ACTION_DOWN ?
const Containers::Pair<PointerEventSource, Pointers> sourcePointers =
motionEventPointers(event, pointerChanged, press ?
pressedButtons & ~app._previousPressedButtons :
~pressedButtons & app._previousPressedButtons);
const std::int32_t pointerId = pointerIdForSource(sourcePointers.first(), AMotionEvent_getPointerId(event, pointerChanged));
/* Decide whether this is a primary pointer. It's tempting to
use the distinction between _DOWN and _POINTER_DOWN to
distinguish a primary pointer from a secondary one, but
that'd be giving too much credit to this damn API. The
problem is that, if multiple fingers is pressed, _POINTER_UP
is fired if any of the secondary fingers are lifted, but
also if the primary finger is lifted. Which in turn means
the primary finger would be treated as secondary for the
up event, which is wrong. Second, then none of the remaining
fingers then have any reasonable way to get promoted to a
primary one, so they all stay secondary. BUT THEN, if the
last one of the secondary fingers gets lifted, _UP is fired
for it, so it suddenly becomes primary. Which a total trash
fire of a broken behavior. A mention worth a laugh is the
official Android developer blog, where in 2010 they
suggested the same thing --- in particular, when the primary
pointer is lifted, *an arbitrary one* from the rest is
chosen as primary.
https://android-developers.googleblog.com/2010/06/making-sense-of-multitouch.html
Eh. Maybe it makes sense for just two touches, but
definitely not for multiple. So let's just ignore all that
and do it by hand like in Sdl2Application and
EmscriptenApplication, to have consistent behavior across
all.
Mouse and pen is always a primary pointer. */
bool primary;
if(sourcePointers.first() == PointerEventSource::Mouse ||
sourcePointers.first() == PointerEventSource::Pen) {
primary = true;
/* For touch update primary finger info */
} else if(sourcePointers.first() == PointerEventSource::Touch) {
/* If there's no primary finger yet and this is the first
finger pressed (i.e., what AMOTION_EVENT_ACTION_DOWN
implies), it becomes the primary finger. If the primary
finger is lifted, no other finger becomes primary until
all others are lifted as well. Again, this is the same
as in Sdl2Application and EmscriptenApplication. */
if(app._primaryFingerId == ~Int{} && action == AMOTION_EVENT_ACTION_DOWN) {
CORRADE_INTERNAL_ASSERT(AMotionEvent_getPointerCount(event) == 1);
primary = true;
app._primaryFingerId = pointerId;
/* Otherwise, if this is the primary finger, mark it as
such */
} else if(app._primaryFingerId == pointerId) {
primary = true;
/* ... but if it's a release, it's no longer primary */
if(!press)
app._primaryFingerId = ~Int{};
/* Otherwise this is not the primary finger */
} else primary = false;
/* Unknown pointer is probably not a primary one */
} else primary = false;
/* The expectation is that the difference betweeen the
previously recorded set of pointers and current one will be
@ -312,8 +461,8 @@ std::int32_t AndroidApplication::inputEvent(android_app* state, AInputEvent* eve
button caused the event. */
Pointer pointer;
/* http://www.graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 */
if(pointers && !(UnsignedByte(pointers) & (UnsignedByte(pointers) - 1)))
pointer = Pointer(UnsignedByte(pointers));
if(sourcePointers.second() && !(UnsignedByte(sourcePointers.second()) & (UnsignedByte(sourcePointers.second()) - 1)))
pointer = Pointer(UnsignedByte(sourcePointers.second()));
else
pointer = Pointer::Unknown;
@ -325,41 +474,102 @@ std::int32_t AndroidApplication::inputEvent(android_app* state, AInputEvent* eve
multiple buttons being pressed didn't even trigger a press
or release event, so this scenario is seemingly impossible
to happen. */
PointerEvent e{event, pointer};
action == AMOTION_EVENT_ACTION_DOWN ?
app.pointerPressEvent(e) : app.pointerReleaseEvent(e);
/* Assuming there's never more than 256 pointers in a single
event. Even that feels like a lot. */
PointerEvent e{event, UnsignedByte(pointerChanged), sourcePointers.first(), pointer, primary, pointerId};
press ? app.pointerPressEvent(e) : app.pointerReleaseEvent(e);
/* Remember the currently pressed pointers for the next time */
app._previousPressedButtons = pressedButtons;
/* A touch screen doesn't have hover events, so remember the
position here as well. See below for why this has to be
remembered at all. */
app._previousPointerPosition = position;
/* If this is a touch press, remember its position for next
events. If this is a touch release, free the slot used by
this identifier for next events. Mouse and pen supports
hover and thus is updated only in AMOTION_EVENT_ACTION_MOVE.
See below for why this has to be remembered at all. */
if(sourcePointers.first() == PointerEventSource::Touch) {
if(press)
updatePreviousTouch(app._previousTouches, pointerId, position);
else
updatePreviousTouch(app._previousTouches, pointerId, {});
}
return e.isAccepted();
}
case AMOTION_EVENT_ACTION_MOVE: {
const Pointers pressedButtons = motionEventButtons(event);
const Pointers pointers = motionEventPointers(event, pressedButtons);
const Vector2 position{AMotionEvent_getX(event, 0),
AMotionEvent_getY(event, 0)};
const Vector2 relativePosition =
Math::isNan(app._previousPointerPosition).all() ?
Vector2{} : position - app._previousPointerPosition;
/* The thing fires move events right after press events, with
the exact same position, for (emulated?) events at least. I
suppose that's some sort of unasked-for misfeature for
"improving" UX or fixing broken apps. Not interested, filter
those out if the relative position is zero and the set of
pressed buttons is the same. Hopefully not accepting those
doesn't lead to some strange behavior. */
/* Unlike AMOTION_EVENT_ACTION_DOWN / AMOTION_EVENT_ACTION_UP,
the move event can contain multiple moving pointers so
there's no mask telling which pointer moved. Go through all
and emit a move event only for those that changed. */
bool accepted = false;
if(relativePosition != Vector2{} || pressedButtons != app._previousPressedButtons) {
PointerMoveEvent e{event, {}, pointers, relativePosition};
app.pointerMoveEvent(e);
accepted = e.isAccepted();
const std::size_t pointerCount = AMotionEvent_getPointerCount(event);
for(std::size_t i = 0; i != pointerCount; ++i) {
const Containers::Pair<PointerEventSource, Pointers> sourcePointers = motionEventPointers(event, i, pressedButtons);
const std::int32_t pointerId = pointerIdForSource(sourcePointers.first(), AMotionEvent_getPointerId(event, i));
const Vector2 position{AMotionEvent_getX(event, i),
AMotionEvent_getY(event, i)};
/* Query position relative to the previous one for the same
pointer type and identifier, update it with current.
Ideally I would get it somewhere from the platform APIs.
There's AMotionEvent_getHistoricalX()/Y(), but those are
coalesced events between the previous and currently
fired events, i.e. not the full delta. Documented here:
https://developer.android.com/reference/android/view/MotionEvent#batching
There's also AMOTION_EVENT_AXIS_RELATIVE_X/_Y, but
according to
https://developer.android.com/reference/android/view/MotionEvent#AXIS_X
the coordinate system is different for each event type,
and the last thing I want to do is adding special
handling for things the damn platform API should be
doing for me. */
Vector2 relativePosition{NoInit};
if(sourcePointers.first() == PointerEventSource::Mouse ||
sourcePointers.first() == PointerEventSource::Pen) {
relativePosition = Math::isNan(app._previousHoverPointerPosition).all() ?
Vector2{} : position - app._previousHoverPointerPosition;
app._previousHoverPointerPosition = position;
} else if(sourcePointers.first() == PointerEventSource::Touch) {
relativePosition = updatePreviousTouch(app._previousTouches, pointerId, position);
} else {
/* No relative position for Unknown */
relativePosition = {};
}
/* Decide whether this is a primary pointer. Mouse and pen
is always a primary pointer. */
bool primary;
if(sourcePointers.first() == PointerEventSource::Mouse ||
sourcePointers.first() == PointerEventSource::Pen) {
primary = true;
/* For touch, it's a primary finger only if it was
registered as such during the last press. If the primary
finger was lifted, no other finger will step into its
place until all others are lifted as well. */
} else if(sourcePointers.first() == PointerEventSource::Touch) {
primary = app._primaryFingerId == pointerId;
/* Unknown pointer is probably not a primary one */
} else primary = false;
/* The thing fires move events right after press events,
with the exact same position, for (emulated?) events at
least. I suppose that's some sort of unasked-for
misfeature for "improving" UX or fixing broken apps. Not
interested, filter those out if the relative position is
zero and the set of pressed buttons is the same.
Hopefully not accepting those doesn't lead to some
strange behavior. */
if(relativePosition != Vector2{} || pressedButtons != app._previousPressedButtons) {
/* Assuming there's never more than 256 pointers in a
single event. Even that feels like a lot. */
PointerMoveEvent e{event, UnsignedByte(i), sourcePointers.first(), {}, sourcePointers.second(), primary, pointerId, relativePosition};
app.pointerMoveEvent(e);
accepted = accepted || e.isAccepted();
}
}
/* Remember the currently pressed buttons for the next time.
@ -368,34 +578,44 @@ std::int32_t AndroidApplication::inputEvent(android_app* state, AInputEvent* eve
have a chance to resynchronize here. */
app._previousPressedButtons = pressedButtons;
/* Remember also the current position. There's
AMotionEvent_getHistoricalX()/Y(), but those are coalesced
events between the previous and currently fired move events,
i.e. not the full delta. Documented here:
https://developer.android.com/reference/android/view/MotionEvent#batching
There's also AMOTION_EVENT_AXIS_RELATIVE_X/_Y, but based on
https://developer.android.com/reference/android/view/MotionEvent#AXIS_X
the coordinate system is different for each event type, and
the last thing I want to do is adding special handling for
things the damn platform API should be doing for me. */
app._previousPointerPosition = position;
return accepted;
}
/* Like AMOTION_EVENT_ACTION_MOVE, but without anything pressed */
case AMOTION_EVENT_ACTION_HOVER_MOVE: {
const Vector2 position{AMotionEvent_getX(event, 0), AMotionEvent_getY(event, 0)};
/* Assuming there's just one pointer reported for a hover, and
it's either a mouse or a pen. Or something unknown. */
CORRADE_INTERNAL_ASSERT(AMotionEvent_getPointerCount(event) == 1);
PointerEventSource source;
switch(AMotionEvent_getToolType(event, 0)) {
case AMOTION_EVENT_TOOL_TYPE_MOUSE:
source = AndroidApplication::PointerEventSource::Mouse;
break;
case AMOTION_EVENT_TOOL_TYPE_FINGER:
CORRADE_INTERNAL_ASSERT_UNREACHABLE();
case AMOTION_EVENT_TOOL_TYPE_STYLUS:
case AMOTION_EVENT_TOOL_TYPE_ERASER:
source = AndroidApplication::PointerEventSource::Pen;
break;
case AMOTION_EVENT_TOOL_TYPE_UNKNOWN:
default:
source = AndroidApplication::PointerEventSource::Unknown;
break;
}
const std::int32_t pointerId = pointerIdForSource(source, AMotionEvent_getPointerId(event, 0));
const Vector2 position{AMotionEvent_getX(event, 0),
AMotionEvent_getY(event, 0)};
const Vector2 relativePosition =
Math::isNan(app._previousPointerPosition).all() ?
Vector2{} : position - app._previousPointerPosition;
Math::isNan(app._previousHoverPointerPosition).all() ?
Vector2{} : position - app._previousHoverPointerPosition;
/* Similarly as with AMOTION_EVENT_ACTION_MOVE, the damn thing
fires hover events with zero position delta when scrolling
the mouse wheel. Useless, filter those away. */
bool accepted = false;
if(relativePosition != Vector2{}) {
PointerMoveEvent e{event, {}, {}, relativePosition};
PointerMoveEvent e{event, 0, source, {}, {}, true, pointerId, relativePosition};
app.pointerMoveEvent(e);
accepted = e.isAccepted();
}
@ -405,7 +625,7 @@ std::int32_t AndroidApplication::inputEvent(android_app* state, AInputEvent* eve
app._previousPressedButtons = {};
/* Remember the current position. See above for why
AMotionEvent_getHistoricalX()/Y() is useless. */
app._previousPointerPosition = position;
app._previousHoverPointerPosition = position;
return accepted;
}
@ -473,6 +693,9 @@ void AndroidApplication::exec(android_app* state, Containers::Pointer<AndroidApp
void AndroidApplication::pointerPressEvent(PointerEvent& event) {
#ifdef MAGNUM_BUILD_DEPRECATED
if(!event.isPrimary())
return;
CORRADE_IGNORE_DEPRECATED_PUSH
MouseEvent mouseEvent{event._event};
mousePressEvent(mouseEvent);
@ -490,6 +713,9 @@ CORRADE_IGNORE_DEPRECATED_POP
void AndroidApplication::pointerReleaseEvent(PointerEvent& event) {
#ifdef MAGNUM_BUILD_DEPRECATED
if(!event.isPrimary())
return;
CORRADE_IGNORE_DEPRECATED_PUSH
MouseEvent mouseEvent{event._event};
mouseReleaseEvent(mouseEvent);
@ -507,6 +733,9 @@ CORRADE_IGNORE_DEPRECATED_POP
void AndroidApplication::pointerMoveEvent(PointerMoveEvent& event) {
#ifdef MAGNUM_BUILD_DEPRECATED
if(!event.isPrimary())
return;
const Vector2i roundedPosition{Math::round(event.position())};
/* If the event is due to some button being additionally pressed or one

179
src/Magnum/Platform/AndroidApplication.h

@ -6,6 +6,7 @@
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
2020, 2021, 2022, 2023, 2024
Vladimír Vondruš <mosra@centrum.cz>
Copyright © 2021 nodoteve <nodoteve@yandex.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
@ -160,6 +161,37 @@ The application by default redirects @ref Corrade::Utility::Debug "Debug",
output to Android log buffer with tag `"magnum"`, which can be then accessed
through `logcat` utility. See also @ref Corrade::Utility::AndroidLogStreamBuffer
for more information.
@section Platform-AndroidApplication-touch Touch input
The application recognizes touch and pen input and reports it as
@ref Pointer::Finger, @ref Pointer::Pen, @ref Pointer::Eraser with
@ref PointerEventSource::Touch and @ref PointerEventSource::Pen.
In case of a multi-touch scenario, @ref PointerEvent::isPrimary() /
@ref PointerMoveEvent::isPrimary() can be used to distinguish the primary touch
from secondary. For example, if an application doesn't need to recognize
gestures like pinch to zoom or rotate, it can ignore all non-primary pointer
events. @ref PointerEventSource::Mouse and @ref PointerEventSource::Pen events
are always marked as primary, for touch input the first pressed finger is
marked as primary and all following pressed fingers are non-primary. Note that
there can be up to one primary pointer for each pointer event source, e.g. a
finger, pen and a mouse press may all be marked as primary. On the other hand,
in a multi-touch scenario, if the first (and thus primary) finger is lifted, no
other finger becomes primary until all others are lifted as well. This is
consistent with the logic in @ref Sdl2Application and @ref EmscriptenApplication
but may not necessarily match what other Android applications do.
If gesture recognition is desirable, @ref PointerEvent::id() /
@ref PointerMoveEvent::id() contains a pointer ID that's unique among all
pointer event sources, which can be used to track movements of secondary,
tertiary and further touch points. The ID allocation isn't defined and you
can't rely on it to be contiguous or in any bounded range --- for example,
each new touch may generate a new ID that's only used until given finger is
lifted, and then never again, or the IDs may get heavily reused, being unique
only for the period given finger is pressed. For @ref PointerEventSource::Mouse
and @ref PointerEventSource::Pen the ID is a constant, as there's always just
a single mouse cursor or a pen stylus.
*/
class AndroidApplication {
public:
@ -179,6 +211,7 @@ class AndroidApplication {
/* The damn thing cannot handle forward enum declarations */
#ifndef DOXYGEN_GENERATING_OUTPUT
enum class PointerEventSource: UnsignedByte;
enum class Pointer: UnsignedByte;
#endif
@ -518,8 +551,22 @@ class AndroidApplication {
EGLSurface _surface;
EGLContext _glContext;
Vector2 _previousPointerPosition{Constants::nan()};
/* Contains just the Mouse* values */
/* We have no way to query previous pointer positions, so we have to
maintain them like this. For pointers capable of hover (mouse, pen)
the _previousHoverPointerPosition is used, NaN signalling that the
previous position is unknown. */
Vector2 _previousHoverPointerPosition{Constants::nan()};
/* For touches the _previousTouches array is used. The id is ~Int{} if
given slot is unused, 32 "should be enough" and is consistent with
what EmscriptenApplication does here. */
struct {
Int id = ~Int{};
Vector2 position;
} _previousTouches[32];
Int _primaryFingerId = ~Int{};
/* In order to know which mouse button was pressed / released in
current event. Contains just the Mouse* values. */
Pointers _previousPressedButtons;
/* Has to be in an Optional because it gets explicitly destroyed before
@ -530,6 +577,43 @@ class AndroidApplication {
CORRADE_ENUMSET_FRIEND_OPERATORS(Flags)
};
/**
@brief Pointer event source
@m_since_latest
@see @ref PointerEvent::source(), @ref PointerMoveEvent::source()
*/
enum class AndroidApplication::PointerEventSource: UnsignedByte {
/**
* The event source is unknown. Corresponds to
* `AMOTION_EVENT_TOOL_TYPE_UNKNOWN` and other types not listed below.
* @see @ref Pointer::Unknown
*/
Unknown,
/**
* The event is coming from a mouse. Corresponds to
* `AMOTION_EVENT_TOOL_TYPE_MOUSE`.
* @see @ref Pointer::MouseLeft, @ref Pointer::MouseMiddle,
* @ref Pointer::MouseRight
*/
Mouse,
/**
* The event is coming from a touch contact, Corresponds to
* `AMOTION_EVENT_TOOL_TYPE_FINGER`.
* @see @ref Pointer::Finger
*/
Touch,
/**
* The event is coming from a pen stylus. Corresponds to
* `AMOTION_EVENT_TOOL_TYPE_STYLUS` and `AMOTION_EVENT_TOOL_TYPE_ERASER`.
* @see @ref Pointer::Pen, @ref Pointer::Eraser
*/
Pen
};
/**
@brief Pointer type
@m_since_latest
@ -541,24 +625,28 @@ enum class AndroidApplication::Pointer: UnsignedByte {
/**
* Unknown. Corresponds to `AMOTION_EVENT_TOOL_TYPE_UNKNOWN` and other
* types not listed below.
* @see @ref PointerEventSource::Unknown
*/
Unknown = 1 << 0,
/**
* Left mouse button. Corresponds to `AMOTION_EVENT_TOOL_TYPE_MOUSE` and
* `AMOTION_EVENT_BUTTON_PRIMARY`.
* @see @ref PointerEventSource::Mouse
*/
MouseLeft = 1 << 1,
/**
* Middle mouse button. Corresponds to `AMOTION_EVENT_TOOL_TYPE_MOUSE` and
* `AMOTION_EVENT_BUTTON_SECONDARY`.
* @see @ref PointerEventSource::Mouse
*/
MouseMiddle = 1 << 2,
/**
* Right mouse button. Corresponds to `AMOTION_EVENT_TOOL_TYPE_MOUSE` and
* `AMOTION_EVENT_BUTTON_TERTIARY`.
* @see @ref PointerEventSource::Mouse
*/
MouseRight = 1 << 3,
@ -566,14 +654,20 @@ enum class AndroidApplication::Pointer: UnsignedByte {
possible to verify they match MouseButton4 / MouseButton5 in
GlfwApplication and Sdl2Application */
/** Finger. Corresponds to `AMOTION_EVENT_TOOL_TYPE_FINGER`. */
/**
* Finger. Corresponds to `AMOTION_EVENT_TOOL_TYPE_FINGER`.
* @see @ref PointerEventSource::Touch
*/
Finger = 1 << 4,
/** @todo There's AMOTION_EVENT_TOOL_TYPE_PALM, but no corresponding
constant on the Java MotionEvent class, and all links to it broken.
Accidental omission? Some scrapped feature with leftover traces? */
/** Pen. Corresponds to `AMOTION_EVENT_TOOL_TYPE_STYLUS`. */
/**
* Pen. Corresponds to `AMOTION_EVENT_TOOL_TYPE_STYLUS`.
* @see @ref PointerEventSource::Pen
*/
Pen = 1 << 5,
/** @todo There's AMOTION_EVENT_BUTTON_STYLUS_PRIMARY and
@ -581,7 +675,10 @@ enum class AndroidApplication::Pointer: UnsignedByte {
exist for EmscriptenApplication / Sdl3Application; implement chorded
behavior for those like w/ mouse buttons */
/** Eraser. Corresponds to `AMOTION_EVENT_TOOL_TYPE_ERASER`. */
/**
* Eraser. Corresponds to `AMOTION_EVENT_TOOL_TYPE_ERASER`.
* @see @ref PointerEventSource::Pen
*/
Eraser = 1 << 6
};
@ -902,9 +999,32 @@ class AndroidApplication::PointerEvent: public InputEvent {
/** @brief Moving is not allowed */
PointerEvent& operator=(PointerEvent&&) = delete;
/** @brief Pointer event source */
PointerEventSource source() const { return _source; }
/** @brief Pointer type that was pressed or released */
Pointer pointer() const { return _pointer; }
/**
* @brief Whether the pointer is primary
*
* Useful to distinguish among multiple pointers in a multi-touch
* scenario. See @ref Platform-AndroidApplication-touch for more
* information.
*/
bool isPrimary() const { return _primary; }
/**
* @brief Pointer ID
*
* Useful to distinguish among multiple pointers in a multi-touch
* scenario. See @ref Platform-AndroidApplication-touch for more
* information.
*/
/* Long is for consistency with Sdl2Application, Android uses just an
Int */
Long id() const { return _id; }
/**
* @brief Position
*
@ -913,16 +1033,20 @@ class AndroidApplication::PointerEvent: public InputEvent {
* pixel.
*/
Vector2 position() const {
return {AMotionEvent_getX(_event, 0),
AMotionEvent_getY(_event, 0)};
return {AMotionEvent_getX(_event, _i),
AMotionEvent_getY(_event, _i)};
}
private:
friend AndroidApplication;
explicit PointerEvent(AInputEvent* event, Pointer pointer): InputEvent(event), _pointer{pointer} {}
explicit PointerEvent(AInputEvent* event, UnsignedByte i, PointerEventSource source, Pointer pointer, bool primary, Int id): InputEvent(event), _source{source}, _pointer{pointer}, _primary{primary}, _i{i}, _id{id} {}
const PointerEventSource _source;
const Pointer _pointer;
const bool _primary;
const UnsignedByte _i; /* Pointer index, not ID */
const Int _id;
};
#ifdef MAGNUM_BUILD_DEPRECATED
@ -996,6 +1120,15 @@ class AndroidApplication::PointerMoveEvent: public InputEvent {
/** @brief Moving is not allowed */
PointerMoveEvent& operator=(PointerMoveEvent&&) = delete;
/**
* @brief Pointer event source
*
* Can be used to distinguish which source the event is coming from in
* case it's a movement with both @ref pointer() and @ref pointers()
* being empty.
*/
PointerEventSource source() const { return _source; }
/**
* @brief Pointer type that was added or removed from the set of pressed pointers
*
@ -1016,6 +1149,26 @@ class AndroidApplication::PointerMoveEvent: public InputEvent {
*/
Pointers pointers() const { return _pointers; }
/**
* @brief Whether the pointer is primary
*
* Useful to distinguish among multiple pointers in a multi-touch
* scenario. See @ref Platform-AndroidApplication-touch for more
* information.
*/
bool isPrimary() const { return _primary; }
/**
* @brief Pointer ID
*
* Useful to distinguish among multiple pointers in a multi-touch
* scenario. See @ref Platform-AndroidApplication-touch for more
* information.
*/
/* Long is for consistency with Sdl2Application, Android uses just an
Int */
Long id() const { return _id; }
/**
* @brief Position
*
@ -1024,8 +1177,8 @@ class AndroidApplication::PointerMoveEvent: public InputEvent {
* pixel.
*/
Vector2 position() const {
return {AMotionEvent_getX(_event, 0),
AMotionEvent_getY(_event, 0)};
return {AMotionEvent_getX(_event, _i),
AMotionEvent_getY(_event, _i)};
}
/**
@ -1042,10 +1195,14 @@ class AndroidApplication::PointerMoveEvent: public InputEvent {
private:
friend AndroidApplication;
explicit PointerMoveEvent(AInputEvent* event, Containers::Optional<Pointer> pointer, Pointers pointers, const Vector2& relativePosition): InputEvent{event}, _pointer{pointer}, _pointers{pointers}, _relativePosition{relativePosition} {}
explicit PointerMoveEvent(AInputEvent* event, UnsignedByte i, PointerEventSource source, Containers::Optional<Pointer> pointer, Pointers pointers, bool primary, Int id, const Vector2& relativePosition): InputEvent{event}, _source{source}, _pointer{pointer}, _pointers{pointers}, _primary{primary}, _i{i}, _id{id}, _relativePosition{relativePosition} {}
const PointerEventSource _source;
const Containers::Optional<Pointer> _pointer;
const Pointers _pointers;
const bool _primary;
const UnsignedByte _i; /* Pointer index, not ID */
const Int _id;
const Vector2 _relativePosition;
};

2
src/Magnum/Platform/EmscriptenApplication.cpp

@ -761,7 +761,7 @@ void EmscriptenApplication::setupCallbacks(bool resizable) {
was empirically verified by looking at behavior of a mouse
cursor on a multi-touch screen under X11, it's possible that
other systems do it differently. The same logic is used in
Sdl2Application. */
Sdl2Application and AndroidApplication. */
bool primary;
if(app._primaryFingerId == ~Int{} && event->numTouches == 1) {
primary = true;

2
src/Magnum/Platform/EmscriptenApplication.h

@ -206,7 +206,7 @@ pointer for each pointer event source. For example, a finger and a mouse press
may both be marked as primary. On the other hand, in a multi-touch scenario, if
the first (and thus primary) finger is lifted, no other finger becomes primary
until all others are lifted as well. This is consistent with the logic in
@ref Sdl2Application.
@ref Sdl2Application and @ref AndroidApplication.
If gesture recognition is desirable, @ref PointerEvent::id() /
@ref PointerMoveEvent::id() contains a pointer ID that's unique among all

8
src/Magnum/Platform/Sdl2Application.cpp

@ -1122,10 +1122,10 @@ bool Sdl2Application::mainLoopIteration() {
was empirically verified by looking at behavior of a mouse
cursor on a multi-touch screen under X11, it's possible that
other systems do it differently. The same logic is used in
EmscriptenApplication. Also, right now there's an assumption
that there is just one touch device, fingers from different
touch devices would steal the primary bit from each other on
every press. */
EmscriptenApplication and AndroidApplication. Also, right
now there's an assumption that there is just one touch
device, fingers from different touch devices would steal the
primary bit from each other on every press. */
bool primary;
if(_primaryFingerId == ~Long{} && event.type == SDL_FINGERDOWN && SDL_GetNumTouchFingers(event.tfinger.touchId) == 1) {
primary = true;

2
src/Magnum/Platform/Sdl2Application.h

@ -307,7 +307,7 @@ pointer for each pointer event source, e.g. a finger and a mouse press may both
be marked as primary. On the other hand, in a multi-touch scenario, if the
first (and thus primary) finger is lifted, no other finger becomes primary
until all others are lifted as well. The same logic is implemented in
@ref EmscriptenApplication.
@ref EmscriptenApplication and @ref AndroidApplication.
If gesture recognition is desirable, @ref PointerEvent::id() /
@ref PointerMoveEvent::id() contains a pointer ID that's unique among all

21
src/Magnum/Platform/Test/AndroidApplicationTest.cpp

@ -72,6 +72,21 @@ CORRADE_IGNORE_DEPRECATED_POP
namespace Test { namespace {
static Debug& operator<<(Debug& debug, Application::PointerEventSource value) {
debug << "PointerEventSource" << Debug::nospace;
switch(value) {
#define _c(value) case Application::PointerEventSource::value: return debug << "::" #value;
_c(Unknown)
_c(Mouse)
_c(Touch)
_c(Pen)
#undef _c
}
return debug << "(" << Debug::nospace << UnsignedInt(value) << Debug::nospace << ")";
}
Debug& operator<<(Debug& debug, Application::Pointers value) {
return Containers::enumSetDebugOutput(debug, value, "Pointers{}", {
Application::Pointer::Unknown,
@ -130,13 +145,13 @@ struct AndroidApplicationTest: Platform::Application {
/* Set to 0 to test the deprecated mouse events instead */
#if 1
void pointerPressEvent(PointerEvent& event) override {
Debug{} << "pointer press:" << event.pointer() << Debug::packed << event.position();
Debug{} << "pointer press:" << event.source() << event.pointer() << (event.isPrimary() ? "primary" : "secondary") << event.id() << Debug::packed << event.position();
}
void pointerReleaseEvent(PointerEvent& event) override {
Debug{} << "pointer release:" << event.pointer() << Debug::packed << event.position();
Debug{} << "pointer release:" << event.source() << event.pointer() << (event.isPrimary() ? "primary" : "secondary") << event.id() << Debug::packed << event.position();
}
void pointerMoveEvent(PointerMoveEvent& event) override {
Debug{} << "pointer move:" << event.pointer() << event.pointers() << Debug::packed << event.position() << Debug::packed << event.relativePosition();
Debug{} << "pointer move:" << event.source() << event.pointer() << event.pointers() << (event.isPrimary() ? "primary" : "secondary") << event.id() << Debug::packed << event.position() << Debug::packed << event.relativePosition();
}
#else
CORRADE_IGNORE_DEPRECATED_PUSH

Loading…
Cancel
Save