resizable_splitter 2.0.0-dev.1
resizable_splitter: ^2.0.0-dev.1 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.0.0-dev.1 #
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.