Liquid Glass Easy

اللمس والحركة

كيف يستجيب السطح للإصبع، ونموذج الزجاج المتحرك الذي بُني منه المزلاج وحبة شريط التنقل.

صنف

LiquidGlassTouch#

الدليل: اللمس →

How a glass surface answers a finger, bundled as one descriptor.

Touch response is not one effect. Apple's own description of Liquid Glass is that a control can "lift, flex, and energize" the moment you touch it — a deformation and a light, two separate things that fire on the same gesture. This is the group that holds them, so a surface's whole feel travels as a single value the way its whole look travels as a LiquidGlassStyle.

Today it carries flex. null means the surface does not respond at all — no gesture listener, no ticker, nothing added to the tree.

Dart
LiquidGlassLens(
  touch: const LiquidGlassTouch(
    flex: LiquidGlassFlex(stretch: 22, lean: 0.5),
  ),
  child: const Center(child: Text('press me')),
)

المُنشئات

const LiquidGlassTouch({this.flex})
المعاملالنوعالافتراضيالوصف
flexLiquidGlassFlex?Soft-body deformation under the finger: press and it compresses, drag and it elongates along the pull, pinches in the cross axis and leans after the finger, then springs back with a wobble. null leaves the surface rigid.
const LiquidGlassTouch.flexing(LiquidGlassFlex this.flex)

A response that is only a deformation — the common case, without the nesting.

الخصائص

الاسمالنوعالوصف
isEmptyboolWhether anything at all happens on touch. False for a group whose every member is unset, which callers treat exactly like a null group.

الدوال

التوقيعالوصف
LiquidGlassTouch copyWith({LiquidGlassFlex? flex})

معرَّف في lib/src/widgets/utils/liquid_glass_touch.dart

صنف

LiquidGlassFlex#

الدليل: اللمس →

Touch-driven soft-body deformation for a lens that does not move.

Press a lens and it compresses under your finger; drag and it elongates along the pull, pinches in the cross axis, and leans after your finger — then springs back with a wobble when you let go.

The model: four independent edges

Unlike a scale transform — which can only grow symmetrically around one anchor — this moves the lens's left, right, top and bottom edges independently. That is what makes it read as soft: the half of the shape nearest your finger deforms more than the far half.

The grab point drives that split. Grab near the right edge and pull right: the right edge shoots out while the left barely follows. Grab dead centre and the same pull elongates both sides evenly. grip controls how localized this is (0 = fully symmetric, 1 = fully local).

Volume is preserved, not gained

Whatever the along-axis edges gain, the cross-axis edges give back: (w + g)·(h - loss) = w·h. Elongate horizontally and it genuinely gets thinner. squeeze scales that give-back (1 = full preservation, 0 = none).

Hold and tap are separate responses

holdScale resizes the lens for as long as the finger stays down — the swell of a control being held. tapScale is a one-shot pop fired the moment a tap completes, so a quick click still reads as a click even though nothing was held long enough for the hold to arrive. Both are signed: positive swells, negative yields inward.

A press also deepens the optics (LiquidGlassFlexAdvanced.refractionBoost), which is what makes the surface read as glass being compressed rather than as a rubber button popping.

Only nine knobs

Everything you would reach for while dialling in a feel is a direct parameter. The five that are set once and forgotten — content follow, optical boost, and the three spring constants — live in LiquidGlassFlexAdvanced behind advanced, so they stay out of the way without being out of reach.

Dart
LiquidGlassLens(
  touch: const LiquidGlassTouch(flex: LiquidGlassFlex()),
  style: const LiquidGlassStyle(
    shape: LiquidGlassShape.roundedRectangle(cornerRadius: 28),
  ),
  child: const Center(child: Text('press me')),
)

المُنشئات

const LiquidGlassFlex({ this.stretch = 13, this.squeeze = 0.70, this.lean = 0.50, this.grip = 0.70, this.compressInward = true, this.holdScale = 0.030, this.tapScale = 0.020, this.maxPull = 48, this.lockAxis, this.advanced = const LiquidGlassFlexAdvanced(), })
المعاملالنوعالافتراضيالوصف
stretchdouble13Peak elongation along the pull axis, in logical pixels, reached as the drag saturates at maxPull.
squeezedouble0.70How much of the along-axis gain is taken back from the cross axis. 1 preserves area exactly, 0 lets the lens simply grow.
leandouble0.50How far the whole body slides after the finger, as a fraction of stretch. This is the "it is attached to my thumb" cue; 0 keeps the lens centred and only deforms it.
gripdouble0.70How localized the deformation is to the grab point, 01. 0 — every edge shares the deformation equally, wherever you touched. 1 — the edges nearest your finger take all of it.
compressInwardbooltrueWhether driving the grabbed edge into the body compresses it. The deformation assumes the finger grips the surface and the far side lags. Pull an edge away from the middle and the body elongates. Push that same edge inward and the far side still lags, so with this on the body squashes — and squeeze then bulges the cross axis outward, since its give-back is signed both ways. false restores the original response, where the pull's magnitude drove the elongation and the shape grew whichever way it was pushed. That path is bit-identical to the pre-compression behaviour, not an approximation of it. How directional it is fades with how far off-centre the grab is and with grip: a grab on the exact middle has no near edge to push into, so it stretches symmetrically either way regardless of this flag.
holdScaledouble0.030How much the lens resizes while the finger stays down, before any drag — a signed fraction of its own size, weighted toward the grab point by grip. Positive grows it, the way a glass control swells under a press: 0.03 makes it 3% bigger on both axes. Negative compresses it, for a surface that yields inward instead. 0 leaves the size alone and lets LiquidGlassFlexAdvanced.refractionBoost carry the press on its own. This is the tap-and-hold response: it arrives on a spring and stays for as long as you hold. For the response to a quick click, see tapScale. A fraction rather than pixels so the same value reads identically on a 56pt button and a 320pt bar — a fixed pixel inset would be invisible on one and enormous on the other.
tapScaledouble0.020A one-shot size pop fired when a tap completes — the click feedback. Same signed-fraction units as holdScale; 0 disables it. A tap is a release with no meaningful drag (under kTouchSlop) inside kLongPressTimeout — Flutter's own definition, so this fires on exactly the gestures a GestureDetector.onTap would. Hold past that, or drag, and only holdScale applies. It exists because holdScale alone cannot express a click: a fast tap releases before the hold spring has arrived anywhere, so the lens barely moves. The pop is injected at release and decays over ~90 ms, and the edge springs turn that into a rebound.
maxPulldouble48Drag distance in logical pixels at which the pull saturates. The raw drag is passed through tanh(pull / maxPull), so small drags track the finger almost 1:1 while large ones stop dead at a predictable ceiling — the deformation never exceeds stretch.
lockAxisAxis?Confines the drag to one axis: Axis.horizontal pins the top and bottom edges against a pull, Axis.vertical pins the left and right. null (the default) leaves all four free. For a shape whose proportions carry meaning — a bottom nav capsule, a segmented control, a slider track — the cross axis is not a free dimension. A bar that thickens under a sideways drag, or slides up off its own bottom margin, reads as broken rather than as soft. It covers everything the pull produces on that axis: elongation, lean, and the volume give-back — so a horizontally locked lens widens without thinning. It does not touch holdScale or tapScale. Those stay uniform on both axes, because a press is a swell rather than a stretch: flattening it onto one axis reads as the surface warping instead of yielding. So a locked bar still grows in both directions under a finger; it just will not change shape when dragged. Enforced on the spring targets, so a locked edge never loads a spring from the drag at all.
advancedLiquidGlassFlexAdvancedconst LiquidGlassFlexAdvanced()The set-once knobs: content follow, optical boost, spring constants. Kept out of this constructor so the ones that matter stay visible.
const LiquidGlassFlex.subtle()

Barely-there deformation for large surfaces (cards, sheets, bars), where a big wobble would look wrong.

const LiquidGlassFlex.uniform()

Symmetric, finger-following deformation that ignores the grab point (grip 0): touch anywhere and the lens deforms the same way, leaning hard after the drag rather than yielding where you pressed it.

const LiquidGlassFlex.pronounced()

Loose, obviously-soft deformation for small controls (buttons, chips, app icons) where the jelly is the point.

الخصائص

الاسمالنوعالوصف
childFollowdoubleSee LiquidGlassFlexAdvanced.childFollow.
refractionBoostdoubleSee LiquidGlassFlexAdvanced.refractionBoost.
magnificationBoostdoubleSee LiquidGlassFlexAdvanced.magnificationBoost.
stiffnessdoubleSee LiquidGlassFlexAdvanced.stiffness.
dampingdoubleSee LiquidGlassFlexAdvanced.damping.
releaseDampingdoubleSee LiquidGlassFlexAdvanced.releaseDamping.

الدوال

التوقيعالوصف
LiquidGlassFlex copyWith({ double? stretch, double? squeeze, double? lean, …Mirrors the constructor. To change one of the set-once knobs, go through the group: copyWith(advanced: e.advanced.copyWith(damping: 20)).

معرَّف في lib/src/widgets/utils/liquid_glass_flex.dart

صنف

LiquidGlassFlexDeform#

One resolved frame of LiquidGlassFlex deformation.

The four edge values are how far each edge has moved outward from rest, in logical pixels; negative means the edge has moved inward.

المُنشئات

const LiquidGlassFlexDeform({ required this.left, required this.right, required this.top, required this.bottom, required this.childScaleX, required this.childScaleY, required this.childTranslateX, required this.childTranslateY, required this.pressAmount, })
المعاملالنوعالافتراضيالوصف
leftمطلوبdoubleOutward travel of the left edge (grows the lens leftward).
rightمطلوبdoubleOutward travel of the right edge.
topمطلوبdoubleOutward travel of the top edge.
bottomمطلوبdoubleOutward travel of the bottom edge.
childScaleXمطلوبdoubleHorizontal component of the content's transform. Together with childTranslateX this is the glass's own map, blended toward identity by LiquidGlassFlex.childFollow. See childTransform.
childScaleYمطلوبdoubleVertical component of the content's transform. See childTransform.
childTranslateXمطلوبdoubleHorizontal offset of the content's transform, in logical pixels, measured from the deformed box's top-left. See childTransform.
childTranslateYمطلوبdoubleVertical offset of the content's transform. See childTransform.
pressAmountمطلوبdoubleSmoothed "finger is down" amount, 01. Drives LiquidGlassFlex.refractionBoost.

الخصائص

الاسمالنوعالوصف
childTransformMatrix4The content's transform, anchored at the deformed box's top-left. At LiquidGlassFlex.childFollow 1 this is literally the map the glass performs on itself, so the content cannot drift away from the surface — they are the same transform, not two tuned approximations. Derivation, per axis, in rest coordinates: the glass sends the rest span 0, w to -left, w + right, which is the linear map x' = sx·x - left with sx = (w + left + right) / w. Expressed from the deformed box's own origin (which sits at rest -left) that becomes x' = sx·x, and blending toward identity by f gives scale = 1 + f·(sx - 1), translate = (1 - f)·left. Column-major, so the last column carries the translation. Built literally rather than through translate/scale so the order of the two is unambiguous: scale first, then offset.
isRestboolWhether there is nothing to draw differently — lets callers skip every wrapper widget entirely when the lens is at rest.
widthDeltadoubleTotal width gained (left + right travel).
heightDeltadoubleTotal height gained (top + bottom travel).
originShiftOffsetHow far the deformed lens's top-left sits from the rest top-left. Negative on each axis when the leading edges push outward.

الدوال

التوقيعالوصف
Size sizeFrom(Size rest)The deformed size for a lens whose rest size is rest.
Offset scaleFrom(Size rest)Deformed size ÷ rest size, as the shader's u_shapeScale. (1, 1) when undeformed. The shader evaluates the shape at REST size in a domain divided by this, so the deformation stretches the whole outline -- a circle becomes an ellipse instead of a stadium with flat runs.
Offset clipScaleFrom(Size rest)scaleFrom, but for the CLIPS rather than the shader. Identical in normal use. They part company only under LiquidGlassFlexOutline.shaderOnly, which stretches the shader's outline while pinning the clips circular -- the combination that eats the rim at a cap apex.

الأعضاء الساكنة

التوقيعالوصف
LiquidGlassFlexDeform noneThe untouched, fully settled state.

معرَّف في lib/src/widgets/utils/liquid_glass_flex.dart

صنف

LiquidGlassFlexAdvanced#

الدليل: اللمس →

The knobs of LiquidGlassFlex that are set once and then left alone — grouped so they stay out of the main constructor.

Reach for them when the feel is right but something else is off: the content is fighting the glass (childFollow), the optics are too shallow (refractionBoost), or the settle is too tight or too loose (the three spring constants).

المُنشئات

const LiquidGlassFlexAdvanced({ this.childFollow = 1, this.refractionBoost = 0.15, this.magnificationBoost = 0, this.stiffness = 320, this.damping = 24, this.releaseDamping = 17, })
المعاملالنوعالافتراضيالوصف
childFollowdouble1How strongly the lens's content follows the deformation, 01. The child is always laid out at the lens's rest size and then scaled, so it stretches as pixels instead of re-flowing — text never re-wraps and rows never re-space. 0 leaves the content undisturbed (best for dense text), 1 makes it fully rubbery.
refractionBoostdouble0.15How much harder the glass BENDS while pressed, as a fraction. 0.15 means the refraction strength rises by up to 15% at full press. Only the bending. The separate magnificationBoost carries the zoom, because they are different effects and only one of them is refraction.
magnificationBoostdouble0How much the backdrop is ZOOMED while pressed, as a fraction of LiquidGlassRefraction.magnification. 0.15 magnifies what is behind the glass by up to 15% at full press. Off by default, and deliberately separate from refractionBoost. Magnification is not a refraction depth — it scales the content behind the lens about its centre, so a press reads as the background sliding rather than as glass bending. Apple keys lensing to a surface's SIZE, not to a finger being down, and its press cue is light instead. Turn this up only if you want that zoom on purpose.
stiffnessdouble320Edge spring stiffness.
dampingdouble24Edge spring damping while the finger is down. Higher → the edges track the drag more tightly.
releaseDampingdouble17Edge spring damping after release. Lower than damping on purpose: that is what produces the recoil wobble when you let go.

الدوال

التوقيعالوصف
LiquidGlassFlexAdvanced copyWith({ double? childFollow, double? refractionB…

معرَّف في lib/src/widgets/utils/liquid_glass_flex.dart

تعداد

LiquidGlassFlexOutline#

How much of the stretched-outline behaviour is active. Debug only.

Exists so the fix can be judged against what it replaced on a running device -- a rim is one logical pixel wide, and the difference is not something a screenshot settles.

القيم

القيمةالوصف
legacyPre-fix. The shape keeps its authored pixel radius against the DEFORMED box, so a stretched circle grows flat runs and reads as a stadium.
shaderOnlyThe shader stretches the outline, but the clips stay circular. They then CROSS rather than nest: near a cap apex the clip sits inside the glass and shaves the rim off. This is the intermediate state, kept because it is the one that looks broken and is worth being able to point at.
fullShader and clips both stretch. The shipped behaviour.

معرَّف في lib/src/widgets/utils/liquid_glass_flex.dart

صنف

LiquidGlassMotionPill#

يرث من StatefulWidgetالدليل: الحركة →

The sliding thumb's whole thumb effect as one isolated, reusable component: the two-state morph (contracted ↔ expanded glass), the acceleration squash / stretch (LiquidGlassLensMotion), and the driven-lens rendering — so the same living glass pill can ride a slider track, a bottom nav bar, or anything else that moves it.

Division of labour

The HOST owns position and gesture: where the pill's centre is each frame (drags, glide springs, rubber bands — whatever its own model produces) and when the pill is grabbed. This widget owns everything the thumb itself did:

  • Morph. While active, the pill spring-grows from restSize to activeSize (0.4 s, ζ 0.6 — overshoot included); on deactivation it contracts on the softer spring (0.6 s, ζ 0.7). The optional cover (e.g. the slider's white rest pill) fades out as the glass arrives, so the two read as one crossfade.
  • Squash/stretch. While active, center is sampled every frame into the acceleration model; the resulting deviation scales the pill oppositely on the two axes. Tracking starts on activation and resets instantly when the contract-back begins.
  • Rendering. The deformation is not a rebuilt capsule: the lens renders at its REST (morph) size and the size change rides the shader's u_shapeScale + matching clip scale via LiquidGlassLens, so the end caps go elliptical instead of the shape being re-rounded at each new size — with the refraction band counter-scaled so it keeps its authored width at stretched caps.

Embedding

The widget fills whatever box the host gives it and positions the (overflowing) pill at center in that box's coordinates — place it as the child of a LiquidGlassView, or anywhere a LiquidGlassLens can render. The host just rebuilds with the new center; this widget's own ticker does the sampling.

المُنشئات

const LiquidGlassMotionPill({ super.key, required this.center, required this.active, required this.restSize, required this.activeSize, this.style, this.motion = const LiquidGlassLensMotionSpec(), this.expandStiffness = 247, this.expandDamping = 18.9, this.contractStiffness = 110, this.contractDamping = 14.7, this.cover, this.shadow, this.honorBackdropAlpha = true, this.onGlassVisibilityChanged, })
المعاملالنوعالافتراضيالوصف
keyKey?
centerمطلوبOffsetThe pill's centre in this widget's local coordinates. Update it every frame however the host moves — set directly from a drag, driven by a glide spring, anything.
activeمطلوبboolWhether the pill is "lifted": expanded to activeSize and tracking its own motion. Flip on grab, off when the pill should contract back to rest.
restSizeمطلوبSizeSize of the contracted rest pill.
activeSizeمطلوبSizeSize of the expanded (lifted) glass pill.
styleLiquidGlassStyle?Glass look; null keeps the tuned slider default. The default shape is a circular-cornered capsule tracking the morph height.
motionLiquidGlassLensMotionSpecconst LiquidGlassLensMotionSpec()Tuning of the acceleration squash/stretch.
expandStiffnessdouble247Expand spring, mapped as ω₀ = 2π / duration (0.4 s, ζ 0.6).
expandDampingdouble18.9
contractStiffnessdouble110Contract spring (0.6 s, ζ 0.7).
contractDampingdouble14.7
coverWidget?Widget drawn over the glass at rest and faded out as the morph expands — the slider passes its solid white pill here. It is its own layer above the lens, sized to the glass's VISIBLE extent (the deformed box plus the shader's edge-AA reach on the Impeller backdrop path) and clipped to the matching outline with the same stretch, so at rest it hides the glass completely and deforms as one body with it. Takes no pointers. It is clipped to the pill's own outline, so pass a plain fill: a rounded rectangle of its own would only cut back inside that outline at the caps, which is exactly the mismatch the clip removes.
shadowLiquidGlassShadow?Contact shadow drawn around the pill — the soft dark band that hugs the rim and pools underneath. Normally left null: the shadow travels with the rest of the look, in style.appearance.shadow, and this is the override for a caller that has to state it apart from the style. null on both draws none. Either way it wraps the lens rather than living inside it, so the half that falls BELOW the pill survives instead of being clipped away; the pill also hands it the current outline stretch so the ring tracks an elliptical cap while the glass is squashed. That wrap is why the style's shadow is lifted out of the appearance before the lens sees it. See LiquidGlassShadow. It paints behind the glass, so an opaque cover at rest covers the shadow along with the glass beneath it.
honorBackdropAlphabooltrueWhether the shader folds the captured backdrop's alpha into its coverage — required over an authored-transparent capture (a slider's track, a demo bar). Skia capture path only.
onGlassVisibilityChangedValueChanged<bool>?Fired when the glass leaves rest (true) and when the contraction lands back at rest and an opaque cover hides it again (false). The host owns active, but not this: the glass keeps rendering for the whole contract spring after active flips off. Lets the host drop work that only pays off while the glass shows — the slider runs its background capture exactly across this window.

معرَّف في lib/src/widgets/components/liquid_glass_motion_pill.dart

صنف

LiquidGlassLensMotion#

الدليل: الحركة →

The running simulation behind LiquidGlassLensMotionSpec. One instance per moving body; see the library doc for the calling contract.

المُنشئات

LiquidGlassLensMotion({this.spec = const LiquidGlassLensMotionSpec()})
المعاملالنوعالافتراضيالوصف
specLiquidGlassLensMotionSpecconst LiquidGlassLensMotionSpec()Live tuning; takes effect from the next track.

الخصائص

الاسمالنوعالوصف
isTrackingboolWhether the effect is live (start without a stop yet).
deviationdoubleThe current eased scale deviation, signed. 0 at rest.

الدوال

التوقيعالوصف
void start()Clears the history and samples from here on. Call when the effect goes live.
void stop()Stops sampling and drops the deformation in the same breath — an instant reset, masked by whatever animation runs over it.
double track(Offset position, {required double now, required double dt})Samples position, prunes the window, and eases the deviation toward the averaged acceleration's answer. Call once per frame while tracking, with the caller's monotonic clock now (seconds) and the dt since the previous frame. Returns the new deviation.

معرَّف في lib/src/widgets/utils/liquid_glass_lens_motion.dart

صنف

LiquidGlassLensMotionSpec#

الدليل: الحركة →

The acceleration→deformation physics of a moving glass body, extracted from the stretch slider's thumb into a component-agnostic class.

Feed it the mover's position once per frame — a slider thumb's centre, a nav-bar pill's centre, anything that travels — and read back a signed scale deviation:

scaleX = 1 + deviation scaleY = 1 − deviation

Accelerating into a drag stretches the body wide and flat; braking (or arriving from a glide) squashes it narrow and tall; constant speed leaves it undeformed — force, not speed, is what deforms it.

This class is physics only: the caller owns the Ticker, calls start when the effect goes live (the thumb lifts, the pill starts moving), track every frame with the current position, and stop the moment the effect ends (an instant reset, masked by whatever animation runs over it). How the deviation maps to pixels is each component's business.

Positions are 2-D on purpose: the two axes combine as avgX − avgY, since a vertical acceleration deforms the opposite way (Y-stretch is a negative deviation). A horizontal mover passes Offset(x, 0) and gets the collapsed one-axis behaviour.

المُنشئات

const LiquidGlassLensMotionSpec({ this.sampleWindow = 0.3, this.sensitivity = 0.00007, this.maxDeformation = 0.3, this.responseTime = 0.18, })
المعاملالنوعالافتراضيالوصف
sampleWindowdouble0.3How many seconds of recent motion are averaged to read the force — bigger = calmer, smaller = twitchier.
sensitivitydouble0.00007The gain from acceleration (px/s²) to scale deviation — how strongly a given force deforms the body.
maxDeformationdouble0.3The hard cap on |deviation| — how far the two scales may stray from 1 (clamped for visual stability).
responseTimedouble0.18Seconds for the deviation to ease toward the acceleration's answer, instead of snapping to it every frame. 0 restores the frame-locked response; higher = slower, dreamier.

الدوال

التوقيعالوصف
LiquidGlassLensMotionSpec copyWith({ double? sampleWindow, double? sensitiv…

معرَّف في lib/src/widgets/utils/liquid_glass_lens_motion.dart

صنف

LiquidGlassDraggable#

يرث من StatefulWidgetالدليل: القابل للسحب →

Wraps child so a pan gesture drags it, with the same smoothness as the built-in drag — and without baking drag into the lens itself.

The drag offset lives in an internal ValueNotifier, and only a Transform.translate rebuilds on each pan; child stays a stable subtree, so a wrapped LiquidGlassLens (or any glass) re-runs its shader/capture pipeline once, not every frame. The lens keeps sampling the backdrop correctly at its dragged position because the renderer resolves its screen rect through the full layer transform.

Dart
LiquidGlassDraggable(
  child: LiquidGlassLens(
    style: const LiquidGlassStyle(
      shape: LiquidGlassShape.squircle(cornerRadius: 44),
    ),
    child: const Center(child: Text('drag me')),
  ),
)

المُنشئات

const LiquidGlassDraggable({ super.key, required this.child, this.enabled = true, this.initialOffset = Offset.zero, this.onChanged, })
المعاملالنوعالافتراضيالوصف
keyKey?
childمطلوبWidgetThe widget to make draggable (e.g. a LiquidGlassLens).
enabledbooltrueWhether dragging is active. When false the child is returned as-is (no gesture, no transform).
initialOffsetOffsetOffset.zeroThe starting offset from the child's layout position.
onChangedValueChanged<Offset>?Called with the new offset whenever the drag moves.

معرَّف في lib/src/widgets/components/liquid_glass_draggable.dart

التعليقات

التعليقات هي مناقشات GitHub — يمكنك الرد من أيٍّ من المكانين.