Skip to main content

design_widgets 0.1.0 copy "design_widgets: ^0.1.0" to clipboard
design_widgets: ^0.1.0 copied to clipboard

Flutter widgets for design layers, with stacked fills, strokes and effects.

design_widgets #

pub package CI License: Apache 2.0

Native Flutter widgets for design layers: DesignRectangle, described by its Layout, Appearance, Fill, Stroke and Effects, and DesignText, by its text, Typography, Layout and Appearance, with the same Fill, Stroke and Effects.

They draw what Flutter's own widgets don't: smoothed corners, strokes inside, centered on or outside an edge, stacked fills, inner shadows, progressive blurs, and text placed by its font file's own measures.

Their parameters use the names and units of Figma's design panel: fills, strokes, effects, corner smoothing, line height, letter spacing, vertical trim and the rest, with colors in hex and opacities in percent. So values can be copied from Figma as they are.

A button, gradients and a progressive blur drawn with design_widgets

Features #

  • Smoothed corners: a radius for each corner, and corner smoothing from 0 to 100%.
  • Stacked fills: any number of solid colors and linear and radial gradients, topmost first.
  • Strokes: inside, centered on or outside the edge, painted with fills of their own.
  • Shadows: drop and inner shadows, with spread on shapes.
  • Blurs: layer blurs, uniform or progressive from one point to another.
  • Text: fonts found by the names inside their files; line height, letter spacing, alignment, auto width, auto height or fixed size, and vertical trim; the same fills, strokes and effects as shapes.
  • Appearance: layer opacity, and blend modes on shapes.
  • Rendering: the render objects behind the widgets, for render objects of your own that must draw the same way.

Getting started #

flutter pub add design_widgets

Declare your fonts in the pubspec as usual and read them once before runApp. A layer holds no child: to put text on a shape, lay both in a Stack.

import 'package:design_widgets/design_widgets.dart';
import 'package:flutter/widgets.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await DesignFonts.load();
  runApp(const Directionality(textDirection: TextDirection.ltr, child: Center(child: GetStartedButton())));
}

class GetStartedButton extends StatelessWidget {
  const GetStartedButton({super.key});

  @override
  Widget build(BuildContext context) {
    return const SizedBox(
      width: 280,
      height: 72,
      child: Stack(
        alignment: Alignment.center,
        children: [
          DesignRectangle(
            appearance: ShapeAppearance(cornerRadius: .all(36)),
            fill: [
              .linearGradient(stops: [GradientStop(0, HexColor(0x5C7CFA)), GradientStop(100, HexColor(0x3B5BDB))]),
            ],
            stroke: Stroke(fill: [.solid(HexColor(0x1E3A8A))], weight: 3, align: .outside),
            effects: [.dropShadow(position: Position(y: 4), blur: 10, color: HexColor(0x1E3A8A, opacity: 35))],
          ),
          DesignText(
            'Get started',
            fontName: FontName('Roboto', 'Bold'),
            fontSize: 28,
            textAlignHorizontal: .center,
            verticalTrim: .capHeightToBaseline,
            fill: [.solid(HexColor(0xFFFFFF))],
          ),
        ],
      ),
    );
  }
}

The example app shows this button, gradients and blurs.

DesignRectangle #

import 'package:design_widgets/design_widgets.dart';

const DesignRectangle(
  layout: ShapeLayout(width: 320, height: 160),
  appearance: ShapeAppearance(cornerRadius: .all(24), cornerSmoothing: 60),
  fill: [.solid(HexColor(0xFFFFFF))],
  stroke: Stroke(fill: [.solid(HexColor(0x3B5BDB))], weight: 2, align: .outside),
  effects: [.dropShadow(position: Position(y: 4), blur: 12, color: HexColor(0x000000, opacity: 15))],
)
  • Colors are 6-digit hex values; opacity and corner smoothing are percent; sizes are logical pixels.
  • Fill, stroke and effect layers are listed topmost first.
  • Without a width or height, that side takes its parent's size.
  • It holds no child: to put content on it, lay both in a Stack.
  • Like Container, it receives taps only when it paints something, and only inside its rounded outline.
  • It is drawn by Flutter itself: colors, stacked fills, gradients, corner radii, inside strokes and drop shadows behind opaque fills come out pixel for pixel as Container, ColoredBox and BoxDecoration draw them.
  • A see-through fill doesn't show its own drop shadow through it.
  • A shadow's blur is the width of its Gaussian at half its peak: a sigma of blur / 2.3548.
  • ShapeAppearance(opacity: 50) fades the whole rectangle as one, its fill, stroke and shadows together: a fill doesn't show through its stroke. With one fill and nothing else it costs no more than the fill.
  • ShapeAppearance(blendMode: .multiply) mixes the whole rectangle, as one, with what is drawn below it: the colors below are multiplied by its own, so it only darkens. .passThrough, the default, and .normal draw it over them; they differ only in whether blend modes inside the layer reach what's below, which nothing inside a rectangle has yet.

DesignText #

import 'package:design_widgets/design_widgets.dart';

const DesignText(
  'Get started',
  fontName: FontName('Roboto', 'Bold'),
  fontSize: 22,
  textAlignHorizontal: .center,
  verticalTrim: .capHeightToBaseline,
  fill: [.solid(HexColor(0xFFFFFF))],
  stroke: Stroke(fill: [.solid(HexColor(0x1E3A8A))], weight: 2, align: .outside),
  effects: [.dropShadow(position: Position(y: 2), color: HexColor(0x172554))],
)

Fonts #

A font is named by its family and its style, the names inside the font file, such as Inter and Black. Declare the files in the app's pubspec as usual, then read them once before runApp:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await DesignFonts.load();
  runApp(const App());
}

FontName('Inter', 'Black') then draws with the file named so inside, whatever family and weight the pubspec declares it with. An app can list its fonts as an enum:

enum AppFont implements FontName {
  robotoBold('Roboto', 'Bold'),
  interBlack('Inter', 'Black');

  const AppFont(this.family, this.style);

  @override
  final String family;

  @override
  final String style;
}

DesignText('Get started', fontName: AppFont.robotoBold, fontSize: 22)

A font that can't be found is an error in debug builds, which names the font and draws a red box in the text's place; release builds draw it in the default font. So is a font Flutter can't pick: two files of one family declared with the same weight and style.

Typography #

Parameter Values Default
fontName, fontSize the font, and its size in logical pixels required
lineHeight .auto, .pixels(16), .percent(120) of the size .auto, the font's own
letterSpacing .pixels(2), .percent(5) of the size; below zero closes letters up .none
textAlignHorizontal .left, .center, .right .left
layout .autoWidth, .autoHeight(width: 174), .fixedSize(width: 70, height: 24, verticalAlign: .center) .autoWidth
verticalTrim .standard, .capHeightToBaseline .standard

A layout takes only the sizes it keeps fixed: auto width takes none, auto height a width, and a fixed size both, with where the lines sit between its top and bottom (.top, .center or .bottom).

  • Auto width is the widest line rounded up to whole pixels, and a height that follows the text is rounded to the nearest pixel. Lines break only at \n and U+2028 in auto width, and also wrap to the width otherwise.
  • Line spacing is split evenly above and below each line.
  • Letter spacing goes between letters, not after the last one, so a line's width and alignment are its letters'. A percent line height rounds to whole pixels, as Auto does; a line height in pixels is kept as it is.
  • The size is never scaled by the system's text size setting.
  • Text shadows have no spread.
  • appearance: TextAppearance(opacity: 50) fades the whole text as one, as ShapeAppearance does a rectangle.
  • Plain text is drawn with one paragraph, as Text draws it; a stroke and each shadow add one more draw of the same layout.

Gradients #

A gradient is placed by handles. Each handle is a GradientHandle(x, y) given as a fraction of the box: (0, 0) is the top-left corner, (1, 1) the bottom-right. Stops are whole percent, 0–100. opacity (percent, default 100) fades the whole gradient; each stop's color keeps its own opacity too.

// Linear: two handles, begin (0%) and end (100%).
Fill.linearGradient(
  begin: GradientHandle(0, 0.5),
  end: GradientHandle(1, 0.5),
  stops: [GradientStop(0, HexColor(0xFE8469)), GradientStop(100, HexColor(0xF05C3A))],
)

// Radial: three handles. center (0%), end (100% along the first axis; it also
// sets the rotation) and width (100% along the second axis).
Fill.radialGradient(
  center: GradientHandle(0, 0),
  end: GradientHandle(1, 0),
  width: GradientHandle(0, 1),
  stops: [GradientStop(0, HexColor(0xFE8469)), GradientStop(100, HexColor(0xF05C3A))],
)

Handles from an SVG gradient #

Divide the numbers below by the shape's width (x) and height (y).

Linear: <linearGradient x1="…" y1="…" x2="…" y2="…">

  • begin = (x1, y1), end = (x2, y2)

Radial: <radialGradient … gradientTransform="translate(cx cy) rotate(a) scale(rx ry)"> (a single scale(r) means rx = ry = r)

  • center = (cx, cy)
  • end = (cx + rx·cos a, cy + rx·sin a)
  • width = (cx − ry·sin a, cy + ry·cos a)

With gradientTransform="matrix(a b c d e f)" instead:

  • center = (e, f), end = (e + a, f + b), width = (e + c, f + d)

For example, on a 300×100 shape, translate(0 0) rotate(0) scale(300 100) gives center (0, 0), end (300, 0) → (1, 0) and width (0, 100) → (0, 1).

Layer blur #

A layer blur blurs the whole layer, its fill, stroke and shadows together. It goes in effects with the shadows, on a rectangle or a text:

// The same everywhere.
effects: [.layerBlur(blur: 4)]

// Progressive: sharp at the top edge's middle, a blur of 8 at the bottom edge's.
effects: [.progressiveBlur(endBlur: 8)]

// From a blur of 2 at the left edge's middle to 12 at the right edge's.
effects: [.progressiveBlur(start: GradientHandle(0, 0.5), end: GradientHandle(1, 0.5), startBlur: 2, endBlur: 12)]
  • A blur is measured as a shadow's: the width of its Gaussian at half its peak, a sigma of blur / 2.3548. It reaches as far past the layer as the blur.
  • A progressive blur's handles are placed as a linear gradient's: fractions of the box. Between them the blur changes in proportion, and past either one it keeps that handle's blur.
  • A progressive blur is drawn as a few uniform blurs, their sigmas at most half a pixel apart and at most 16, each weighed along the line; each is one more layer drawn.

Rendering #

package:design_widgets/rendering.dart holds what the widgets draw with: RenderDesignRectangle, RenderDesignText, the text's TextPaintPlan, the corner outline, and the paints of fills and shadows. A render object of an app's own, such as one drawing letters from outlines, uses them to draw exactly as the widgets do.

Tests #

flutter test runs the widgets' tests, including pixel-for-pixel comparisons with Container, ColoredBox, BoxDecoration and RichText. The text tests use Roboto, whose licence is in test/fonts.

License #

Apache License 2.0: see LICENSE and NOTICE. As its section 6 says, it grants no right to the name design_widgets or its author's name.

Figma is a trademark of Figma, Inc. design_widgets is not affiliated with, sponsored or endorsed by Figma, Inc.

1
likes
160
points
188
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter widgets for design layers, with stacked fills, strokes and effects.

Repository (GitHub)
View/report issues

Topics

#widget #design #ui #gradient #figma

License

Apache-2.0 (license)

Dependencies

flutter

More

Packages that depend on design_widgets