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.
Libraries
- liquid_reveal
- A liquid page reveal that measures its own box rather than the screen.