liquid_reveal 0.1.1
liquid_reveal: ^0.1.1 copied to clipboard
A liquid page reveal that measures its own box instead of the screen, so it works inside a card, a list row or a full page. Render object, no rebuilds.
liquid_reveal #
A liquid page reveal that measures its own box, not the screen. So the same widget is a full-screen onboarding flow, a 220 pixel hero card or an 84 pixel list row, and feels the same in each.
LiquidReveal(
itemCount: pages.length,
itemBuilder: (BuildContext context, int index) => pages[index],
)
Drag it either way. Forwards, the next page grows from one edge; backwards, the previous page grows from the other. The wave follows your finger and lets go into place.
Why another one #
The liquid swipe effect has been around for years, and every implementation of
it divides by MediaQuery.of(context).size — the screen. Put one in a card
and the numbers stop meaning anything: a drag across a 200 pixel box is measured
against an 800 pixel screen, so the reveal creeps along at a quarter speed and
the commit threshold is never reached.
Here nothing asks how big the screen is. The render object measures itself:
- a drag of half the box is half the reveal, in a card or on a page;
- the wave's depth and width are fractions of the box, so it looks the same at any size;
- the commit threshold is a fraction of the box too.
What it costs #
One RenderBox. The drag and the settle animation only ever call
markNeedsPaint, so no widget rebuilds while a reveal runs — the builder is
called again exactly once, when the page changes. Two pages are laid out at a
time, however long the list is.
Settings #
LiquidReveal(
settings: const LiquidRevealSettings(
side: RevealSide.right, // or left, top, bottom
duration: Duration(milliseconds: 550),
curve: Curves.easeOutCubic, // while settling; a drag is never curved
waveAmplitude: 0.18, // fraction of the travel
waveSpread: 0.45, // fraction of the edge
commitAt: 0.35, // let go past this and the page turns
flingVelocity: 700, // px/s that commits whatever commitAt says
dragEdgeFraction: null, // null: drag anywhere; 0.25: only the edge
loop: true,
allowReverse: true, // a drag the other way goes back a page
ignoreDragWhileSettling: true,
),
itemCount: 3,
itemBuilder: builder,
)
LiquidRevealSettings.soft is a wide shallow wave for a hero card;
.sharp is narrow and deep, closer to the original effect.
dragEdgeFraction is the one to reach for when the pages have their own
horizontal gestures. Without it a drag anywhere in the box turns the page, which
is what a carousel wants and the last thing a page with a chip row wants.
From code #
final LiquidRevealController controller = LiquidRevealController();
LiquidReveal(controller: controller, itemCount: 3, itemBuilder: builder);
controller.next(); // reveal the next page
controller.previous(); // reveal the previous one, from the other edge
controller.page; // the page at the front
controller.progress; // 0 to 1, the reveal in flight
controller.offset; // page + progress, for a sliding indicator
The controller is a Listenable, so an indicator can watch it while the pages
stay still:
ListenableBuilder(
listenable: controller,
builder: (BuildContext context, Widget? _) => Dots(at: controller.offset),
)
The wave #
The boundary between the two pages is a pure function — no widget, no context — which is why it can be tested rather than eyeballed:
final LiquidWave wave = LiquidWave(
progress: 0.4,
size: const Size(300, 200),
side: RevealSide.right,
focus: 120, // where the finger is, along the edge
amplitude: 54,
spread: 90,
);
wave.boundaryAt(120); // how far the boundary has come, at that point
wave.toPath(); // the revealed region
A straight line sweeps across the box, pulled towards the incoming page by a
raised cosine centred on the finger. The bump's envelope is zero at both ends of
the gesture, so a finished reveal has a straight edge rather than a frozen
ripple, and it reaches exactly zero at spread — the boundary is provably flat
outside the bump.
When the finger is near a corner, half the wave falls outside the box and you see a fin rather than a blob. That is deliberate: the wave follows the finger instead of sliding away from it.
Both directions #
A drag decides its direction from the first movement, and cannot change its mind
until the reveal is back at zero — so a gesture that turns round mid-flight
undoes itself rather than leaving the wrong page underneath. Backwards is
allowReverse, on by default; turn it off for a flow that should only go one way.
Up to three pages are built: the one at the front, the next and the previous. Never more, however long the list is.
What it will not do #
- A vertical reveal inside a vertical scroll view loses the gesture. Flutter's
arena gives a vertical drag to the
ListView, and there is nothing a child can do about it. Use a horizontal side inside a scrolling page, put the vertical one somewhere that does not scroll, or drive it with the controller. - No jumping to an arbitrary page. The reveal is a transition between two
neighbours;
jumpTo(7)would be a cut, not a reveal. - It is not
liquid_swipe. No code is shared with it. If you want that package's full-screen behaviour and its slide icon, use that package.
Example #
/example shows the same widget at three sizes with one set of settings, a dot
indicator driven by the controller, and the four sides.
cd example && flutter run
License #
MIT.