ruler_scrubber 1.0.0 copy "ruler_scrubber: ^1.0.0" to clipboard
ruler_scrubber: ^1.0.0 copied to clipboard

A performant, accessible ruler-style numeric scrubber for Flutter.

ruler_scrubber #

pub package license: MIT

An accessible, performant ruler-style numeric input for Flutter. The ruler scrolls under a stationary needle, so a wide range stays scrubbable at a fine grain instead of being compressed into one screen width of slider track.

[A ruler scrubber being scrubbed: the card lights up, the price counts along with the ruler, and the flick coasts to a stop]

The card lights up under the finger, the value counts along with the ruler, and the flick carries on scrolling after the finger has gone.

[Three ruler scrubbers on a light card, the middle one lit up mid-scrub]

[The same three scrubbers on a dark card, the middle one lit up mid-scrub]

All three pictures are of the widget itself, rendered by tool/screenshot_test.dart and tool/animation_test.dart rather than captured by hand. In the stills the middle scrubber is held mid-drag, which is why its card and needle carry the accent colour.

Why a ruler #

A slider maps its whole range onto the width of its track, so on a range of 0…100 at a resolution of 0.01 a single pixel is worth several steps and the exact value can only be reached by luck. A ruler decouples the two: the value moves by tickStep per tick under the finger no matter how wide the range is, and a flick coasts, so a distant value costs a gesture rather than a pixel of travel.

Install #

flutter pub add ruler_scrubber

The package is built on material_ui, the official Material Design library that used to live inside the SDK as package:flutter/material.dart. If your app still imports the SDK copy, run Flutter's own migration once and everything lines up:

dart fix --apply --code=migrate_design_widgets

Until you run it the scrubber still builds and scrubs normally, but it looks for a material_ui theme your app does not yet provide and falls back to the baseline Material palette rather than yours. Flutter's MaterialUiCompatibilityBridge does not cover this direction — it carries a modern theme down to legacy widgets, not the other way about. Passing an explicit style skips the theme lookup altogether and is unaffected either way.

Usage #

import 'package:ruler_scrubber/ruler_scrubber.dart';

RulerScrubber(
  value: price,
  min: 0,
  max: 100,
  step: 0.01,     // the value snaps to whole pennies
  tickStep: 0.02, // one tick of ruler is worth two of them
  semanticLabel: 'Price',
  formatValue: (value) => '${value.toStringAsFixed(2)} pounds',
  onChanged: (value) => setState(() => price = value),
  onChangeEnd: (value) => context.read<Quote>().recalculate(value),
)

The widget is presentation only: it reports the value it was scrubbed to and draws the value it is given. Hold that value yourself and pass it back in — setting it from anywhere other than the ruler runs the ruler to it.

Parameters #

Parameter Type Description
value double Where the ruler sits. Clamped into [min, max].
min, max double The ends of the range.
tickStep double How much the value changes over one tick — the scale of the ruler, and so how fast it moves under the finger.
step double? Granularity of the reported value. null scrubs continuously.
onChanged ValueChanged<double> Every value the scrub passes through.
onChangeEnd ValueChanged<double>? The value the ruler came to rest on, for work too expensive to run per frame.
semanticLabel String Spoken name of the control.
formatValue String Function(double)? Spoken form of the value. Defaults to two decimal places.
style RulerScrubberStyle? Colours, card shape and shadows. Defaults to the ambient ThemeData.

tickStep and step do different jobs and are worth setting separately. tickStep is the feel of the control — how far the finger travels per unit of value. step is the contract with your model — which values are legal. A tickStep coarser than step scrubs quickly but still lands on exact values.

Styling #

Omit style and the scrubber derives accessible defaults from the surrounding Material theme. Pass one to use design-system tokens instead:

RulerScrubberStyle(
  shape: const StadiumBorder(side: BorderSide(width: 1.5)),
  backgroundColor: tokens.surface,
  borderColor: tokens.border,
  activeBorderColor: tokens.accent,
  minorTickColor: tokens.borderSubtle,
  majorTickColor: tokens.textSecondary,
  needleColor: tokens.textSecondary,
  activeShadows: const [BoxShadow(blurRadius: 12, color: Color(0x22000000))],
  activeNeedleShadows: const [BoxShadow(blurRadius: 6, color: Color(0x33000000))],
)

The card and needle take their active treatment while a scrub is in progress — including the coast after a flick — so the field being edited is obvious in a form full of them.

The border

shape takes any OutlinedBorder, so the scrubber can be given the same corner as everything else on your screen and this package needs no opinion about which corner that is:

shape: const RoundedRectangleBorder(          // the default, radius 10
  side: BorderSide(width: 1),
  borderRadius: BorderRadius.all(Radius.circular(10)),
),
shape: const StadiumBorder(side: BorderSide(width: 1.5)),   // a pill
shape: const ContinuousRectangleBorder(...),                // a superellipse
shape: SmoothRectangleBorder(...),                          // figma_squircle
shape: const RoundedRectangleBorder(side: BorderSide.none), // no border

The shape's own side is drawn as given, except for its colour: that comes from borderColor and activeBorderColor so the outline can light up while the ruler is being scrubbed. Squircles work the same as anything else — SmoothRectangleBorder is an OutlinedBorder — but they come from your pubspec.yaml rather than this package's.

Fixed geometry (tick spacing, needle size, default card radius, animation durations) lives in ruler_scrubber_metrics.dart as top-level constants, if you need to line something else up with the ruler.

Accessibility #

The scrubber presents itself as a slider to the platform's assistive technology: semanticLabel is its name, formatValue renders its value, and the increase/decrease actions nudge by step — or by a twentieth of the range when step is null. Scrubbing clicks once per tick via HapticFeedback.selectionClick, and a ruler running to a value set elsewhere stays silent.

Performance #

The ticks are painted from the scroll offset rather than laid out inside the scrollable, so a range worth thousands of them costs the same frame as one worth a dozen — only the handful under the viewport is ever drawn. The painter repaints from the scroll position alone, behind a RepaintBoundary, so nothing above the ruler rebuilds while a finger is on it. The end fade is a gradient on the tick paint rather than a mask, which keeps a scrub off the compositor.

Example #

example/ is a runnable app showing a price, a stepped percentage and a scrubber styled from explicit tokens, in both brightnesses.

cd example
flutter create .   # generate the platform folders for your machine
flutter run

Development #

flutter analyze
flutter test
flutter test tool/screenshot_test.dart   # regenerate doc/*.png
flutter test tool/animation_test.dart    # regenerate doc/scrubbing.gif

The screenshots are rendered from the widget itself, so they cannot drift from what it actually looks like. The tool needs a system font to draw real text with; it looks for the macOS ones and fails loudly if it finds none.

License #

MIT — see LICENSE.

1
likes
0
points
132
downloads

Publisher

unverified uploader

Weekly Downloads

A performant, accessible ruler-style numeric scrubber for Flutter.

Repository (GitHub)
View/report issues

Topics

#slider #input #widget #accessibility

License

unknown (license)

Dependencies

flutter, material_ui

More

Packages that depend on ruler_scrubber