Skip to content

Commit

Permalink
add click event support
Browse files Browse the repository at this point in the history
this PR adds click support to listeners.
Click is a new event type (like pointer down, pointer move).
It encompasses two stages (pointer down + pointer up) in the same object or group of objects that belong to the listener.
It processes all the phases of a click gesture
this PR also:
- guarantees that the click gesture is applied only once per frame (no double actions from overlapping shapes)
- supports starting the click gesture in one shape and ending it in another shape of the same listener (by promoting the hover state to the group and not on individual shapes)

Diffs=
405ca998b add click event support (#7668)

Co-authored-by: hernan <[email protected]>
  • Loading branch information
bodymovin and bodymovin committed Jul 29, 2024
1 parent 2e513d2 commit 5dd8cd9
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 37 deletions.
2 changes: 1 addition & 1 deletion .rive_head
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4732c37b5e8afae84ec54d662682380598292ac3
405ca998b7729cb4f84448fe681e9c4a82243099
4 changes: 4 additions & 0 deletions include/rive/animation/state_machine_instance.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Shape;
class StateMachineLayerInstance;
class HitComponent;
class HitShape;
class ListenerGroup;
class NestedArtboard;
class NestedEventListener;
class NestedEventNotifier;
Expand Down Expand Up @@ -146,6 +147,7 @@ class StateMachineInstance : public Scene, public NestedEventNotifier, public Ne
}
return nullptr;
}
const LayerState* layerState(size_t index);
#endif
void updateDataBinds();

Expand All @@ -157,6 +159,7 @@ class StateMachineInstance : public Scene, public NestedEventNotifier, public Ne
size_t m_layerCount;
StateMachineLayerInstance* m_layers;
std::vector<std::unique_ptr<HitComponent>> m_hitComponents;
std::vector<std::unique_ptr<ListenerGroup>> m_listenerGroups;
StateMachineInstance* m_parentStateMachineInstance = nullptr;
NestedArtboard* m_parentNestedArtboard = nullptr;
std::vector<DataBind*> m_dataBinds;
Expand All @@ -178,6 +181,7 @@ class HitComponent
{}
virtual ~HitComponent() {}
virtual HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) = 0;
virtual void prepareEvent(Vec2D position, ListenerType hitType) = 0;
#ifdef WITH_RIVE_TOOLS
virtual bool hitTest(Vec2D position) const = 0;
#endif
Expand Down
12 changes: 12 additions & 0 deletions include/rive/gesture_click_phase.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#ifndef _RIVE_GESTURE_CLICK_PHASE_HPP_
#define _RIVE_GESTURE_CLICK_PHASE_HPP_
namespace rive
{
enum class GestureClickPhase : int
{
out = 0,
down = 1,
clicked = 2,
};
}
#endif
1 change: 1 addition & 0 deletions include/rive/listener_type.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum class ListenerType : int
up = 3,
move = 4,
event = 5,
click = 6,
};
}
#endif
197 changes: 161 additions & 36 deletions src/animation/state_machine_instance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "rive/animation/state_machine_fire_event.hpp"
#include "rive/data_bind_flags.hpp"
#include "rive/event_report.hpp"
#include "rive/gesture_click_phase.hpp"
#include "rive/hit_result.hpp"
#include "rive/math/aabb.hpp"
#include "rive/math/hit_test.hpp"
Expand Down Expand Up @@ -190,7 +191,6 @@ class StateMachineLayerInstance
{
uint32_t totalWeight = 0;
auto stateFrom = stateFromInstance->state();
// printf("stateFrom->transitionCount(): %zu\n", stateFrom->transitionCount());
for (size_t i = 0, length = stateFrom->transitionCount(); i < length; i++)
{
auto transition = stateFrom->transition(i);
Expand Down Expand Up @@ -425,6 +425,45 @@ class StateMachineLayerInstance
float m_holdTime = 0.0f;
};

class ListenerGroup
{
public:
ListenerGroup(const StateMachineListener* listener) : m_listener(listener) {}
void consume() { m_isConsumed = true; }
//
void hover() { m_isHovered = true; }
void reset()
{
m_isConsumed = false;
m_prevIsHovered = m_isHovered;
m_isHovered = false;
if (m_clickPhase == GestureClickPhase::clicked)
{
m_clickPhase = GestureClickPhase::out;
}
}
bool isConsumed() { return m_isConsumed; }
bool isHovered() { return m_isHovered; }
bool prevHovered() { return m_prevIsHovered; }
void clickPhase(GestureClickPhase value) { m_clickPhase = value; }
GestureClickPhase clickPhase() { return m_clickPhase; }
const StateMachineListener* listener() const { return m_listener; };
// A vector storing the previous position for this specific listener gorup
Vec2D previousPosition;

private:
// Consumed listeners aren't processed again in the current frame
bool m_isConsumed = false;
// This variable holds the hover status of the the listener itself so it can
// be shared between all shapes that target it
bool m_isHovered = false;
// Variable storing the previous hovered state to check for hover changes
bool m_prevIsHovered = false;
// A click gesture is composed of three phases and is shared between all shapes
GestureClickPhase m_clickPhase = GestureClickPhase::out;
const StateMachineListener* m_listener;
};

/// Representation of a Shape from the Artboard Instance and all the listeners it
/// triggers. Allows tracking hover and performing hit detection only once on
/// shapes that trigger multiple listeners.
Expand All @@ -444,8 +483,7 @@ class HitShape : public HitComponent
bool hasDownListener = false;
bool hasUpListener = false;
float hitRadius = 2;
Vec2D previousPosition;
std::vector<const StateMachineListener*> listeners;
std::vector<ListenerGroup*> listeners;

bool hitTest(Vec2D position) const
#ifdef WITH_RIVE_TOOLS
Expand All @@ -466,6 +504,29 @@ class HitShape : public HitComponent
return shape->hitTest(hitArea);
}

void prepareEvent(Vec2D position, ListenerType hitType) override
{
if (canEarlyOut && (hitType != ListenerType::down || !hasDownListener) &&
(hitType != ListenerType::up || !hasUpListener))
{
#ifdef TESTING
earlyOutCount++;
#endif
return;
}
isHovered = hitTest(position);

// // iterate all listeners associated with this hit shape
if (isHovered)
{
for (auto listenerGroup : listeners)
{

listenerGroup->hover();
}
}
}

HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) override
{
// If the shape doesn't have any ListenerType::move / enter / exit and the event
Expand All @@ -475,53 +536,90 @@ class HitShape : public HitComponent
if (canEarlyOut && (hitType != ListenerType::down || !hasDownListener) &&
(hitType != ListenerType::up || !hasUpListener))
{
#ifdef TESTING
earlyOutCount++;
#endif
return HitResult::none;
}
auto shape = m_component->as<Shape>();
bool isOver = canHit ? hitTest(position) : false;
bool hoverChange = isHovered != isOver;
isHovered = isOver;
if (hoverChange && isHovered)
{
previousPosition.x = position.x;
previousPosition.y = position.y;
}

// // iterate all listeners associated with this hit shape
for (auto listener : listeners)
for (auto listenerGroup : listeners)
{
// Always update hover states regardless of which specific listener type
// we're trying to trigger.
if (hoverChange)
if (listenerGroup->isConsumed())
{
continue;
}

bool isGroupHovered = canHit ? listenerGroup->isHovered() : false;
bool hoverChange = listenerGroup->prevHovered() != isGroupHovered;
// If hover has changes, it means that the element is hovered for the
// first time. Previous positions need to be reset to avoid jumps.
if (hoverChange && isGroupHovered)
{
if (isOver && listener->listenerType() == ListenerType::enter)
listenerGroup->previousPosition.x = position.x;
listenerGroup->previousPosition.y = position.y;
}

// Handle click gesture phases. A click gesture has two phases.
// First one attached to a pointer down actions, second one attached to a
// pointer up action. Both need to act on a shape of the listener group.
if (isGroupHovered)
{
if (hitType == ListenerType::down)
{
listener->performChanges(m_stateMachineInstance, position, previousPosition);
m_stateMachineInstance->markNeedsAdvance();
listenerGroup->clickPhase(GestureClickPhase::down);
}
else if (!isOver && listener->listenerType() == ListenerType::exit)
else if (hitType == ListenerType::up &&
listenerGroup->clickPhase() == GestureClickPhase::down)
{
listener->performChanges(m_stateMachineInstance, position, previousPosition);
m_stateMachineInstance->markNeedsAdvance();
listenerGroup->clickPhase(GestureClickPhase::clicked);
}
}
if (isOver && hitType == listener->listenerType())
else
{
listener->performChanges(m_stateMachineInstance, position, previousPosition);
if (hitType == ListenerType::down || hitType == ListenerType::up)
{
listenerGroup->clickPhase(GestureClickPhase::out);
}
}
auto listener = listenerGroup->listener();
// Always update hover states regardless of which specific listener type
// we're trying to trigger.
// If hover has changed and:
// - it's hovering and the listener is of type enter
// - it's not hovering and the listener is of type exit
if (hoverChange &&
((isGroupHovered && listener->listenerType() == ListenerType::enter) ||
(!isGroupHovered && listener->listenerType() == ListenerType::exit)))
{
listener->performChanges(m_stateMachineInstance,
position,
listenerGroup->previousPosition);
m_stateMachineInstance->markNeedsAdvance();
listenerGroup->consume();
}
// Perform changes if:
// - the click gesture is complete and the listener is of type click
// - the event type matches the listener type and it is hovering the group
if ((listenerGroup->clickPhase() == GestureClickPhase::clicked &&
listener->listenerType() == ListenerType::click) ||
(isGroupHovered && hitType == listener->listenerType()))
{
listener->performChanges(m_stateMachineInstance,
position,
listenerGroup->previousPosition);
m_stateMachineInstance->markNeedsAdvance();
listenerGroup->consume();
}
listenerGroup->previousPosition.x = position.x;
listenerGroup->previousPosition.y = position.y;
}
previousPosition.x = position.x;
previousPosition.y = position.y;
return isOver ? shape->isTargetOpaque() ? HitResult::hitOpaque : HitResult::hit
: HitResult::none;
return (isHovered && canHit)
? shape->isTargetOpaque() ? HitResult::hitOpaque : HitResult::hit
: HitResult::none;
}

void addListener(const StateMachineListener* stateMachineListener)
void addListener(ListenerGroup* listenerGroup)
{
auto stateMachineListener = listenerGroup->listener();
auto listenerType = stateMachineListener->listenerType();
if (listenerType == ListenerType::enter || listenerType == ListenerType::exit ||
listenerType == ListenerType::move)
Expand All @@ -530,16 +628,16 @@ class HitShape : public HitComponent
}
else
{
if (listenerType == ListenerType::down)
if (listenerType == ListenerType::down || listenerType == ListenerType::click)
{
hasDownListener = true;
}
else if (listenerType == ListenerType::up)
if (listenerType == ListenerType::up || listenerType == ListenerType::click)
{
hasUpListener = true;
}
}
listeners.push_back(stateMachineListener);
listeners.push_back(listenerGroup);
}
};
class HitNestedArtboard : public HitComponent
Expand Down Expand Up @@ -615,6 +713,7 @@ class HitNestedArtboard : public HitComponent
case ListenerType::enter:
case ListenerType::exit:
case ListenerType::event:
case ListenerType::click:
break;
}
}
Expand All @@ -630,14 +729,17 @@ class HitNestedArtboard : public HitComponent
case ListenerType::enter:
case ListenerType::exit:
case ListenerType::event:
case ListenerType::click:
break;
}
}
}
}
return hitResult;
}
void prepareEvent(Vec2D position, ListenerType hitType) override {}
};

} // namespace rive

HitResult StateMachineInstance::updateListeners(Vec2D position, ListenerType hitType)
Expand All @@ -647,9 +749,19 @@ HitResult StateMachineInstance::updateListeners(Vec2D position, ListenerType hit
position -= Vec2D(m_artboardInstance->originX() * m_artboardInstance->width(),
m_artboardInstance->originY() * m_artboardInstance->height());
}

// First reset all listener groups before processing the events
for (const auto& listenerGroup : m_listenerGroups)
{
listenerGroup.get()->reset();
}
// Next prepare the event to set the common hover status for each group
for (const auto& hitShape : m_hitComponents)
{
hitShape->prepareEvent(position, hitType);
}
bool hitSomething = false;
bool hitOpaque = false;
// Finally process the events
for (const auto& hitShape : m_hitComponents)
{
// TODO: quick reject.
Expand Down Expand Up @@ -706,6 +818,17 @@ HitResult StateMachineInstance::pointerExit(Vec2D position)
return updateListeners(position, ListenerType::exit);
}

#ifdef TESTING
const LayerState* StateMachineInstance::layerState(size_t index)
{
if (index < m_machine->layerCount())
{
return m_layers[index].currentState();
}
return nullptr;
}
#endif

StateMachineInstance::StateMachineInstance(const StateMachine* machine,
ArtboardInstance* instance) :
Scene(instance), m_machine(machine)
Expand Down Expand Up @@ -791,6 +914,7 @@ StateMachineInstance::StateMachineInstance(const StateMachine* machine,
{
continue;
}
auto listenerGroup = rivestd::make_unique<ListenerGroup>(listener);
auto target = m_artboardInstance->resolve(listener->targetId());
if (target != nullptr && target->is<ContainerComponent>())
{
Expand All @@ -811,11 +935,12 @@ StateMachineInstance::StateMachineInstance(const StateMachine* machine,
{
hitShape = itr->second;
}
hitShape->addListener(listener);
hitShape->addListener(listenerGroup.get());
}
return true;
});
}
m_listenerGroups.push_back(std::move(listenerGroup));
}

for (auto nestedArtboard : instance->nestedArtboards())
Expand Down
Binary file added test/assets/click_event.riv
Binary file not shown.
Loading

0 comments on commit 5dd8cd9

Please sign in to comment.