LVGL Animations: Cancel Competing Transitions and Keep State Consistent

Decide which state is authoritative

A panel is halfway open when the user taps Close, then Open again. Its final position looks correct, but an old completion callback hides it or changes the selected mode. The problem is not the easing curve. The application has allowed animation lifetime to become confused with logical state. This guide uses LVGL 9.2 documentation and the v9.2.2 animation implementation, with ordinary C execution callbacks as the scope.

Keep a desired state, a currently displayed value, and an animation generation. The desired state answers what the user most recently requested. The displayed value answers where the visual property is now. The generation identifies which transition may still commit a completion action. A product state should not depend on every intermediate animation reaching its endpoint; interruptions are normal input, not exceptional failures.

A new generation cancels the owned animation, starts from the current visual value, and validates the generation before committing completion.
Original conceptual engineering diagram. It expresses design dependencies, not a measured implementation.

Give each animated property one owner

LVGL identifies ordinary animations by their variable and execution-callback pair. The 9.2 animation documentation describes replacement for a matching pair. Two different wrapper callbacks that both change an object’s X coordinate are different identities, so application-level competition can still occur. A layout manager or style transition can also write a related property without being part of your animation registry.

Choose one execution callback for each independently owned property or route transitions through a component controller. Record whether an object’s position is layout-owned, directly positioned, or animated through a translation property. Avoid animating a coordinate that a layout recalculates continuously unless that interaction is intentional. Opacity, position and interaction-enabled state may require a coordinated transition even though they are different properties and can have different animation identities.

Replace from the current visual value

On a new request, first record the new desired state and retire the old generation. Cancel the old property transition explicitly when required by the component’s policy. Read the actual current property and use it as the next animation’s starting value, then choose the new endpoint from the desired state. Restarting every reversal from the original endpoint can create a visible jump.

The v9.2.2 API provides lv_anim_delete(var, exec_cb), lv_anim_set_values() and lv_anim_set_duration(). Use the exact callback identity when cancelling one property. A NULL callback is a broader selection and may stop unrelated animations on that variable. Delayed starts and early-apply settings complicate automatic replacement, so explicit component ownership is easier to review than relying on the presence of one apparently matching animation at a particular moment.

Treat completion and deletion as different events

lv_anim_set_completed_cb() reports a completed transition. lv_anim_set_deleted_cb() is a lifecycle cleanup hook, and deletion can also follow normal completion. In the pinned implementation, explicit removal invokes the deleted callback without pretending that the transition reached its target. Do not implement “panel is fully closed” by unconditionally running that action from every deletion callback.

A completion callback should check that its generation still matches the component’s current request before committing a state change. Cleanup must be idempotent and must not accidentally clear the pointer to a replacement animation. Be careful when starting or deleting animations from callbacks, because that can alter the animation list and trigger further lifecycle callbacks. The simplest component policy often keeps callback effects small and lets its controller decide the final logical state.

Preserve object and callback-data lifetime

lv_anim_start() copies the animation descriptor in this implementation, so a local descriptor used to configure a new animation is different from the target object and callback user data. Those referenced objects must remain valid while any relevant callback can run. Do not confuse a copied descriptor with a deep copy of everything it points to.

Before destroying a component, mark it as closing, cancel its owned transitions, and ensure cleanup callbacks cannot start new ones. For animation timelines, follow the documented timeline-before-object destruction order. If a callback receives a model pointer rather than an lv_obj_t pointer, object deletion elsewhere cannot automatically establish the model’s lifetime. Ownership should cover the model, widgets, timer sources and transition records together. This is a lifecycle review, not a recommendation to add unnecessary heap allocation for every animation.

Define reversals and skipped transitions deliberately

A fixed duration on every request creates different apparent speeds depending on the remaining distance. A distance-based duration can keep speed more consistent, but still needs a minimum, maximum and a policy for zero distance. Neither choice is universally correct. State whether repeated requests to the existing target are ignored, restart a transition, or update its parameters. Also decide whether reduced-motion mode jumps directly to the endpoint while preserving the same logical state changes.

Interaction policy matters during a transition. A fading button may still receive input unless the component explicitly controls that behavior. A closing panel might need to stop accepting commands immediately even though it remains visible for a short time. Keep those decisions in the state machine rather than hiding them inside the last frame of an animation. They should also hold if rendering is temporarily delayed.

Test an interrupted history, not one successful run

Use a proposed input sequence such as Open, Close before completion, Open during reversal, then destroy the component before the final callback. Repeat with zero duration, delayed start, an already-satisfied endpoint and rapid duplicate requests. Assert that the newest desired state wins, each transition record is cleaned once, no stale callback mutates the component, and the final geometry matches the surviving request.

Log request generation, property owner, start value, endpoint, cancellation and callback reason. Compare the displayed value before and after replacement to detect jumps. These tests should be run in the selected simulator and on the target when timing affects interaction; neither was executed for this article. A smooth demonstration is insufficient evidence of lifecycle correctness.

  • One owner controls each animated property and its cancellation identity.
  • Replacement starts from the current visual state under an explicit duration policy.
  • Cancellation does not masquerade as successful completion.
  • Callbacks validate generation and preserve target-data lifetime.
  • Destruction prevents new transitions before releasing the component.

Similar Posts