resizable_splitter 2.1.3
resizable_splitter: ^2.1.3 copied to clipboard
Accessible splitter widget for Flutter layouts that need drag-to-resize panels.
Changelog #
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
2.1.3 #
Changed #
- Requires
equatable ^2.1.0: migrated off the now-deprecatedEquatableMixintoEquatableas a mixin.
Fixed #
- Hovering (or dragging) the divider could make it less visible instead of
highlighting it, which read as a broken/flickering hover state when sweeping
across the divider. The default hovered/dragged colors replaced the opaque
resting color (
outlineVariant) with a bare low-alphaonSurfacetint; over a dark surface that composites to a color darker than the resting bar (and to a washed-out one on light surfaces). The default state colors are now the resting color with the emphasis tint composited on top, so hover and drag can only ever add emphasis. ExplicitSplitterDividerStyle.color/dividerColorvalues are unaffected. - The grab slop (
interactiveExtent > thickness) did not work where it overhangs a platform view (for example aWebViewpane) on macOS: hover still tracked, but presses in that slop were swallowed by the native view, so grabbing the divider from that side silently did nothing. The slop was transparent padding, and the macOS compositor only routes pointer input above a platform view to Flutter where a later Flutter layer actually paints. The divider's interactive area now paints an imperceptible base layer (alpha 1/255) across its full extent - the same technique the 2.1.2 drag shield uses- so presses in the slop reach Flutter and the divider can be grabbed from
either side. Opting out of
shieldPlatformViewsalso opts out of this paint. The same iOS/web limitation as 2.1.2 applies: a painted Flutter layer is not an input target on those platforms.
- so presses in the slop reach Flutter and the divider can be grabbed from
either side. Opting out of
2.1.2 #
Fixed #
- The divider could still stay stuck in the dragging state when the pointer was
released over a platform view (for example a
WebView) on macOS - the case 2.1.1 targeted but did not fully resolve. Arming the shield earlier was not enough: the shield's barrier defaulted to a fully transparent fill, which paints nothing, so no Flutter layer was composited above the platform view. With nothing painted there the native view stayed the topmost surface, and the OS delivered the pointer release to it instead of to Flutter, so the drag never ended. The shield now always paints an imperceptible layer above platform views while dragging (independent ofdragBarrierColor/dragBarrierBuilder), so the release reaches Flutter and the drag ends reliably. Known limitation: iOS and web platform views may still need an app-supplied pointer interceptor, as a painted Flutter layer is not an input target on those platforms.
2.1.1 #
Fixed #
- A divider next to a platform view (for example a
WebView) could stay stuck in the dragging state when the pointer was released over that view:controller.isDraggingstayedtrueand the divider kept tracking as if the button were still held. The platform-view shield was armed only once the drag was recognized, so a divider that also handles a tap or double-tap left a brief window - before the drag won the gesture arena - in which the neighboring platform view could capture the pointer and swallow the release. The shield is now armed on pointer-down and its lifetime is bounded by the press, so the release is delivered reliably and the drag always ends. The visible drag barrier still appears only while a drag is in progress.
2.1.0 #
Adds two magnetic-snap shaping controls and a large internal reorganization.
The public API is unchanged apart from the two additive MagneticSnap options
below (both default to the previous behavior), so existing code keeps working.
Added #
MagneticSnap.falloff(and theSplitterSnapBehavior.magnetic(falloff:)parameter): aCurvethat shapes the magnetic pull across the influence zone. The linear nearness (1at the point,0at the tolerance edge) is passed through the curve before being scaled bystrength, so an ease-in curve such asCurves.easeInCubiclets the divider track the pointer freely until it is close, then catch harder near the point for a snappier feel. Defaults toCurves.linear, which reproduces the previous behavior exactly - so this is a backward-compatible addition.MagneticSnap.settleFactor(and theSplitterSnapBehavior.magnetic(settleFactor:)parameter): a[0, 1]fraction of the tolerance defining a small core around each point where the divider settles exactly onto it, giving the pull a crisp finish (it stays pushable - moving the pointer past the core resumes the pull). Defaults to0, which keeps the never-quite-lands pull - a backward-compatible addition.
Changed #
- Adopted
equatablefor the equality of the value and configuration types (SplitterPosition,SplitterState,SplitterLayout,SplitterPaneConstraints,SplitterChangeDetails, the snap behaviors, and the theme/style types). The hand-written==/hashCodewere replaced byEquatableMixinpropsover the same fields, so equality, hash codes,toString,copyWith, andlerpare unchanged - this is purely an internal simplification, not a behavior change. - Added
equatableandmetadependencies.metabacks the@immutable/@internalannotations applied across the source. - Reorganized
lib/srcintomodel/,solver/,theme/, andwidget/folders and split the large widget files into focused parts. This is internal only: the package barrel still exports the same public types, soimport 'package:resizable_splitter/resizable_splitter.dart'is unaffected. - Rebuilt the example into an interactive showcase - snapping, constraints, collapse, pixel pinning, an IDE-style layout, and accessibility - replacing the previous minimal demo.
2.0.0 #
A ground-up rebuild around a pure constraint solver. Every interaction (drag, keyboard, snap, semantics) now operates on the effective on-screen position, so the stored value can no longer disagree with what is drawn.
Breaking changes #
SplitterController.valueis now an atomicSplitterState(the requestedSplitterPositionplus any collapsed pane); wasdoublein 1.x. Set the position withcontroller.jumpTo(SplitterPosition), read it withcontroller.position, and the on-screen ratio withcontroller.effectiveFraction.initialRatio->initialPosition(aSplitterPosition).- Divider styling grouped into
divider: SplitterDividerStyle(...), replacingdividerThickness/dividerColor/dividerHoverColor/dividerActiveColor/handleHitSlop/handleBuilder. The color is aWidgetStateProperty<Color?>resolved againsthovered/focused/dragged. - Pane limits grouped into
startConstraints/endConstraints(SplitterPaneConstraints), replacingminPanelSize/minStartPanelSize/minEndPanelSize. Adds per-panemaxExtent. A pane is collapsible when itscollapsedExtentis set (a nullabledouble?in[0, minExtent]; null = not collapsible) - this replaces the separatecollapsiblebool, so an unreachable/contradictory collapse config is now unrepresentable. minRatio/maxRatio->minStartFraction/maxStartFraction.- Snapping grouped into
snap: SplitterSnapBehavior(points, tolerance, pixelTolerance), replacingsnapPoints/snapTolerance. - Callbacks
onRatioChanged/onDragStart/onDragEnd(carrying adouble) ->onChanged/onChangeStart/onChangeEndcarryingSplitterChangeDetails(the request, the effective fraction, both pane extents, and aSplitterChangeSource). crampedBehavior(CrampedBehavior) ->constraintPolicy(SplitterConstraintPolicy.favorStart/favorEnd/proportional).ResizableSplitterThemeOverridesremoved. The single, all-nullableResizableSplitterThemeDatais now both theThemeExtensionand theResizableSplitterThemedata, which fixes a partial-override clobber bug by construction.SplitterController.animateTono longer takesframes(it is vsync-driven) and now returnsFuture<SplitterAnimationStatus>(wasFuture<void>), so a caller can tell a completed run from one a drag cancelled or a disposal ended.- The
Axisre-export was dropped; import it frompackage:flutter/material.dart. - Pixel
minExtent/maxExtentare now hard limits that always win over the fractionalminStartFraction/maxStartFractioncaps when the two disagree (previously a fractional cap could override a feasible pixel minimum). surplusPolicynow defaults toSplitterSurplusPolicy.leaveGap(wasgiveToStart), somaxExtentis a true maximum by default: leftover space becomes a gap between the panes instead of overflowing one past its max.SplitterLayout.isConstrained(bool) ->resolution(aSplitterResolution:exact/clamped/minShortage/maxSurplus/fractionConflict/collapsed/inactive). It also gainsminStartExtent/maxStartExtentand derivedcanIncrease/canDecrease.- The redundant
SplitterValuetype is removed;SplitterChangeDetailsis now a standalone value type carrying the same fields plussource. SplitterDividerStyle.hitSlop(additive padding) ->interactiveExtent(the total grab target across the bar). Defaults to 48 (the Material minimum touch target), up from an effective ~6px; migrate withinteractiveExtent = thickness + 2 * oldHitSlop.onChangeEndnow also fires for a canceled drag, so everyonChangeStartis balanced by exactly one end.SplitterChangeDetails.end(aSplitterChangeEnd) reportscommittedvscanceled.- Behavioral renames:
blockerColor->dragBarrierColor;overlayEnabled->shieldPlatformViews;antiAliasingWorkaround(and the solver'ssnapToDevicePixels) ->snapToPhysicalPixels;fallbackMainAxisExtent->fallbackExtent;UnboundedBehavior.flexExpand/.limitedBox->shrinkToChildren/useFallbackExtent;SplitterChangeSource.programmatic->doubleTapReset. SplitterController.layoutis now cleared (notifiesnull) when the controller detaches from its splitter, rather than retaining the last geometry.
Added #
SplitterController.layout/layoutListenable: the resolved on-screen geometry (SplitterLayout- effective fraction, both pane extents, available extent, the legalminStartExtent/maxStartExtentband with derivedcanIncrease/canDecrease, aresolution, andcollapsedPane) as an observable separate from the request. A pixel pin's fraction shifts when the container resizes without the request changing, so this is the signal for that class of change.nullbefore the first layout (no pretending a pixel request already has a fraction), and cleared when the controller detaches.SplitterState(the atomic controller value) andSplitterController.jumpTo/position.SplitterAnimationStatus(completed/canceled/detached), the result of ananimateTorun.SplitterSurplusPolicy(giveToStart/giveToEnd/proportional/leaveGap) + asurplusPolicyargument: the solver now defines the surplus case (both maximums too small to fill the space) explicitly, instead of silently overflowing a maximum. Defaults toleaveGap, which keeps both panes at their max and renders the leftover as a gap.- Framework-grade accessibility: a keyboard focus ring (with
WidgetState.focusedin the divider color resolver andSplitterHandleDetails.isFocusedfor custom grips), localizable semantics viaSplitterSemanticsLabels(on the widget or the theme), and assistive increase/decrease actions gated on whether the divider can actually move that way (dropped at a hard bound). interactiveExtentonSplitterDividerStyle: the grab target across the bar, default 48, decoupled from the visiblethicknessand from layout.- Pixel pinning:
SplitterPosition.startPixels/endPixelskeep a pane's pixel width as the container resizes (true fixed sidebars). - Collapse/expand:
controller.collapse(SplitterPane.start | SplitterPane.end),expand(),toggleCollapse(), withcollapsedPane/isCollapsed. Restores the prior position automatically; emitscollapse/restorechange events. - State restoration via
ResizableSplitter.restorationId. - Deferred resize (
deferredResize): a preview line tracks the drag and the panes settle once on release - for expensive pane subtrees. - Customizable drag barrier (
dragBarrierBuilder) over the platform-view shield. SplitterSnapBehavior.pixelTolerancefor a size-independent snap feel.- Live snap modes alongside the existing release snapping:
SplitterSnapBehavior.magneticpulls the divider toward a point during the drag and can be pushed through (no release-time jump), andSplitterSnapBehavior.stickycaptures onto a point and holds it until the pointer escapes past a hysteresis radius.SplitterSnapBehavioris now a sealed type (ReleaseSnap/MagneticSnap/StickySnap); the unnamedSplitterSnapBehavior(...)constructor still builds release snapping, so existing call sites are unchanged. - The bounded layout is backed by a dedicated
RenderObjectthat resolves the split inperformLayoutagainst the real constraints (noLayoutBuilder). This is behavior-preserving - the public widget, controller, handle, and solver are unchanged - but it adds intrinsic sizing and dry layout: aResizableSplittercan now sit underIntrinsicWidth/IntrinsicHeight(or any parent that queries intrinsics) and size to its panes, where the previous layout threw. Painting (each pane clipped to its box), clipping, and hit testing (the divider winning inside its interactive slop) also move into the render object. The resolved layout is published from the layout pass, solayoutListenablenotifies once per resolved change, after the frame.
Fixed #
- Collapse is now part of the atomic controller value, so collapsing and then writing an equal value can no longer silently desync the controller from the UI (it reported expanded while the pane stayed collapsed).
- Collapsibility is enforced: a pane only collapses if it has a
collapsedExtent, andcollapsedExtentis asserted<= minExtent(collapse can no longer enlarge a pane).controller.layout.collapsedPanereports the resolved collapse, so collapsing a fixed pane is a visible no-op rather than a phantom. effectiveFractionnow reports the true on-screen value and updates on container resize (vialayoutListenable); it no longer leaks the unclamped request after settling onto a constrained target.- Stored ratio now equals the visible ratio: honest drag/keyboard callbacks, and the ~200px drag dead zone and cramped-drag crash are gone by construction.
- RTL: drag and arrow keys move with the pointer; the start pane lays out on the right.
- The controller rejects
NaN/ out-of-range values at the source. interactiveExtentenlarges the grab target by overlapping the panes instead of widening the divider footprint, and collapses to the visible thickness on a non-resizable divider so a static bar cannot steal the panes' hits.- Overflow-safe under containers smaller than the divider; each pane is clipped.
- A bounded main axis with an unbounded cross axis (e.g. a horizontal splitter
in a
Column) no longer throws an infinite-size error; the layout sizes to the panes' cross extent instead. - Animation is vsync-driven, cancels on drag, and honors
MediaQuery.disableAnimations. - Animation lifecycle is now deterministic: an
animateTofuture no longer hangs when the splitter is disposed mid-run (resolvesdetached), a controller swap no longer lets the animation bleed onto the new controller, and a cancelled run is distinguishable from a completed one (no phantom programmatic change after a cancel). - Animation contract: a fresh
animateToalways supersedes a run in progress (even when the target is already current), a listener's reentrant write cancels the run, a run from a collapsed pane clears the collapse and animates out, and the target is resolved through the solver socompletedmeans the divider actually arrived (no stall against a target clamped off-screen). - Swapping the controller (or axis) during an active drag now ends the drag on the original controller instead of stranding it flagged as dragging.
- The drag shield degrades gracefully when there is no
Overlayancestor (the drag still works) instead of throwing from a reusable layout primitive. - Physical-pixel snapping (
snapToPhysicalPixels) now applies to every solve - drag, keyboard, snap matching, semantics, deferred preview, and the published layout - not just the initial layout, so callbacks can no longer report an extent the layout never drew. The snap config moved from a per-solveargument ontoSplitterSolveritself. - Drag is measured in local space (correct under
Transform); the stuck-drag router is keyed by pointer id (independent concurrent drags). - Slider semantics: role, enabled/disabled state, focus, text direction, and assistive adjustment decoupled from the keyboard flag.
- The change-callback contract is now explicit and honest:
onChanged/onChangeStart/onChangeEndfire for interactions (drag, keyboard, assistive adjust, snap, double-tap) and collapse/expand only; programmatic writes (jumpTo/updateRatio/reset/animateTo) and restoration are observed through thecontrollerandlayoutListenableinstead (documented, not a silent partial contract).
1.1.1 #
- Refined drag coalescing, semantics percentages, and anti-alias minima handling.
1.1.0 #
- Theming refresh:
ResizableSplitterThemeplus aThemeExtensiondrive divider styling, keyboard steps, overlays, and unbounded policies. - Layout policies:
UnboundedBehavior(LimitedBoxopt-in),CrampedBehavior, andantiAliasingWorkaroundfor crisp panes. - Interactions:
resizabletoggle,onHandleTap/onHandleDoubleTap, and controller multi-attach guard. - Fixed precedence so per-instance constructor arguments override themed switches.
- Tests expanded to cover new theming, policies, and interaction paths.
1.0.0 #
- Initial release of
ResizableSplitterwith drag-to-resize layouts. - Keyboard navigation, screen-reader semantics, and customizable divider styling.
SplitterControllerfor programmatic control and testing.