The gesture tracker follows every finger of a touch. When two or more fingers touch and all lift within 300 ms without moving more than 20 px, the ending gesture reports how many fingers tapped. Android passes the motion event time to the tracker.
171 lines
5.4 KiB
C++
171 lines
5.4 KiB
C++
#include "UnitTest++/UnitTest++.h"
|
|
|
|
#include "InputCoalescer.hpp"
|
|
#include "TouchGesture.hpp"
|
|
#include "WindowInput.hpp"
|
|
|
|
#include <cmath>
|
|
#include <numbers>
|
|
|
|
using namespace tp;
|
|
|
|
namespace {
|
|
std::vector<TouchGesture> feed(TouchGestureTracker& tracker, std::vector<TouchPoint> touches, uint32_t timeMs = 0) {
|
|
std::vector<TouchGesture> gestures;
|
|
tracker.update(touches, gestures, timeMs);
|
|
return gestures;
|
|
}
|
|
|
|
int32_t tapFingersOf(const std::vector<TouchGesture>& gestures) {
|
|
return gestures.empty() || gestures.back().phase != GesturePhase::End ? -1 : gestures.back().tapFingers;
|
|
}
|
|
|
|
class GestureLog : public WindowInputObserver {
|
|
public:
|
|
void onGesture(const TouchGesture& gesture) override { gestures.push_back(gesture); }
|
|
std::vector<TouchGesture> gestures;
|
|
};
|
|
}
|
|
|
|
SUITE(TouchGestureTracker) {
|
|
TEST(OneFingerProducesNoGesture) {
|
|
TouchGestureTracker tracker;
|
|
|
|
CHECK(feed(tracker, { { 1, { 10, 10 } } }).empty());
|
|
CHECK(!tracker.isActive());
|
|
}
|
|
|
|
TEST(SecondFingerBeginsAtTheMidpoint) {
|
|
TouchGestureTracker tracker;
|
|
|
|
const auto gestures = feed(tracker, { { 1, { 0, 0 } }, { 2, { 100, 0 } } });
|
|
|
|
REQUIRE CHECK_EQUAL(1u, gestures.size());
|
|
CHECK(gestures[0].phase == GesturePhase::Begin);
|
|
CHECK_CLOSE(50.0f, gestures[0].center.x, 0.001f);
|
|
CHECK(tracker.isActive());
|
|
}
|
|
|
|
TEST(MovingBothFingersPans) {
|
|
TouchGestureTracker tracker;
|
|
feed(tracker, { { 1, { 0, 0 } }, { 2, { 100, 0 } } });
|
|
|
|
const auto gestures = feed(tracker, { { 1, { 10, 20 } }, { 2, { 110, 20 } } });
|
|
|
|
REQUIRE CHECK_EQUAL(1u, gestures.size());
|
|
CHECK(gestures[0].phase == GesturePhase::Update);
|
|
CHECK_CLOSE(10.0f, gestures[0].pan.x, 0.001f);
|
|
CHECK_CLOSE(20.0f, gestures[0].pan.y, 0.001f);
|
|
CHECK_CLOSE(1.0f, gestures[0].scale, 0.001f);
|
|
CHECK_CLOSE(0.0f, gestures[0].rotation, 0.001f);
|
|
}
|
|
|
|
TEST(SpreadingFingersScalesAroundTheMidpoint) {
|
|
TouchGestureTracker tracker;
|
|
feed(tracker, { { 1, { 50, 0 } }, { 2, { 150, 0 } } });
|
|
|
|
const auto gestures = feed(tracker, { { 1, { 0, 0 } }, { 2, { 200, 0 } } });
|
|
|
|
REQUIRE CHECK_EQUAL(1u, gestures.size());
|
|
CHECK_CLOSE(2.0f, gestures[0].scale, 0.001f);
|
|
CHECK_CLOSE(0.0f, gestures[0].pan.x, 0.001f);
|
|
CHECK_CLOSE(100.0f, gestures[0].center.x, 0.001f);
|
|
}
|
|
|
|
TEST(TurningFingersRotates) {
|
|
TouchGestureTracker tracker;
|
|
feed(tracker, { { 1, { -50, 0 } }, { 2, { 50, 0 } } });
|
|
|
|
const auto gestures = feed(tracker, { { 1, { 0, -50 } }, { 2, { 0, 50 } } });
|
|
|
|
REQUIRE CHECK_EQUAL(1u, gestures.size());
|
|
CHECK_CLOSE(std::numbers::pi_v<float> / 2, gestures[0].rotation, 0.001f);
|
|
CHECK_CLOSE(1.0f, gestures[0].scale, 0.001f);
|
|
}
|
|
|
|
TEST(LiftingAFingerEnds) {
|
|
TouchGestureTracker tracker;
|
|
feed(tracker, { { 1, { 0, 0 } }, { 2, { 100, 0 } } });
|
|
|
|
const auto gestures = feed(tracker, { { 1, { 0, 0 } } }, 1000);
|
|
|
|
REQUIRE CHECK_EQUAL(1u, gestures.size());
|
|
CHECK(gestures[0].phase == GesturePhase::End);
|
|
CHECK_EQUAL(0, gestures[0].tapFingers);
|
|
CHECK(!tracker.isActive());
|
|
}
|
|
|
|
TEST(QuickTwoFingerTouchIsATwoFingerTap) {
|
|
TouchGestureTracker tracker;
|
|
feed(tracker, { { 1, { 0, 0 } } }, 1000);
|
|
feed(tracker, { { 1, { 0, 0 } }, { 2, { 100, 0 } } }, 1020);
|
|
CHECK(feed(tracker, { { 2, { 102, 1 } } }, 1120).empty());
|
|
|
|
CHECK_EQUAL(2, tapFingersOf(feed(tracker, {}, 1150)));
|
|
}
|
|
|
|
TEST(QuickThreeFingerTouchIsAThreeFingerTap) {
|
|
TouchGestureTracker tracker;
|
|
feed(tracker, { { 1, { 0, 0 } }, { 2, { 100, 0 } } }, 1000);
|
|
feed(tracker, { { 1, { 0, 0 } }, { 2, { 100, 0 } }, { 3, { 50, 80 } } }, 1030);
|
|
feed(tracker, { { 3, { 50, 80 } } }, 1150);
|
|
|
|
CHECK_EQUAL(3, tapFingersOf(feed(tracker, {}, 1200)));
|
|
}
|
|
|
|
TEST(PinchingIsNotATap) {
|
|
TouchGestureTracker tracker;
|
|
feed(tracker, { { 1, { 0, 0 } }, { 2, { 100, 0 } } }, 1000);
|
|
feed(tracker, { { 1, { 0, 0 } }, { 2, { 160, 0 } } }, 1100);
|
|
|
|
CHECK_EQUAL(0, tapFingersOf(feed(tracker, {}, 1200)));
|
|
}
|
|
|
|
TEST(HoldingTwoFingersIsNotATap) {
|
|
TouchGestureTracker tracker;
|
|
feed(tracker, { { 1, { 0, 0 } }, { 2, { 100, 0 } } }, 1000);
|
|
|
|
CHECK_EQUAL(0, tapFingersOf(feed(tracker, {}, 1000 + TouchGestureTracker::kTapMaximumMs + 50)));
|
|
}
|
|
|
|
TEST(OneFingerTapIsNotReported) {
|
|
TouchGestureTracker tracker;
|
|
feed(tracker, { { 1, { 0, 0 } } }, 1000);
|
|
|
|
CHECK(feed(tracker, {}, 1050).empty());
|
|
}
|
|
|
|
TEST(SwappedFingerRebasesWithoutJumping) {
|
|
TouchGestureTracker tracker;
|
|
feed(tracker, { { 1, { 0, 0 } }, { 2, { 100, 0 } } });
|
|
|
|
CHECK(feed(tracker, { { 1, { 0, 0 } }, { 3, { 400, 300 } } }).empty());
|
|
const auto gestures = feed(tracker, { { 1, { 0, 0 } }, { 3, { 410, 300 } } });
|
|
|
|
REQUIRE CHECK_EQUAL(1u, gestures.size());
|
|
CHECK_CLOSE(5.0f, gestures[0].pan.x, 0.001f);
|
|
}
|
|
}
|
|
|
|
SUITE(TouchGestureCoalescing) {
|
|
TEST(ConsecutiveUpdatesMergeIntoOne) {
|
|
InputCoalescer coalescer;
|
|
coalescer.gesture({ GesturePhase::Begin, { 50, 0 } });
|
|
coalescer.gesture({ GesturePhase::Update, { 60, 0 }, { 10, 0 }, 2.0f, 0.1f });
|
|
coalescer.gesture({ GesturePhase::Update, { 65, 5 }, { 5, 5 }, 1.5f, 0.2f });
|
|
coalescer.gesture({ GesturePhase::End, { 65, 5 } });
|
|
|
|
GestureLog log;
|
|
coalescer.deliver(log);
|
|
|
|
REQUIRE CHECK_EQUAL(3u, log.gestures.size());
|
|
CHECK(log.gestures[0].phase == GesturePhase::Begin);
|
|
CHECK(log.gestures[1].phase == GesturePhase::Update);
|
|
CHECK_CLOSE(15.0f, log.gestures[1].pan.x, 0.001f);
|
|
CHECK_CLOSE(5.0f, log.gestures[1].pan.y, 0.001f);
|
|
CHECK_CLOSE(3.0f, log.gestures[1].scale, 0.001f);
|
|
CHECK_CLOSE(0.3f, log.gestures[1].rotation, 0.001f);
|
|
CHECK_CLOSE(65.0f, log.gestures[1].center.x, 0.001f);
|
|
CHECK(log.gestures[2].phase == GesturePhase::End);
|
|
}
|
|
}
|