snap 1.0.5

  • Readme
  • Changelog
  • Example
  • Installing
  • 78

snap #

Cosmos Software Awesome Flutter

Pub License

An extensive snap tool/widget for Flutter that allows very flexible snap management and snapping between your widgets.

Inspired by WhatsApp's in-app Youtube player.

It is highly recommended to read the documentation and run the example project on a real device to fully understand and inspect the full range of capabilities.

Media | Description | How-to-Use

Notice #

  • flick works as intended on actual devices even if it might appear to fail rarely on simulators. Don't be discouraged!

Recent #

  • flick is now added. It is amazing! See Media for examples.

Media #

Watch on Youtube:

v1.0.0 with Flick

v0.1.0



Description #

This is an extensive snap tool/widget for Flutter that allows very flexible snap management and snapping between your widgets.

Just wrap your snapper widget with the SnapController widget, fill the parameters, define your snappable widget and this package will take care of everything else.

How-to-Use #

"The view is what is being moved. It is the widget that snaps to the bound. The bound is what the view is being snapped to."

First, define two GlobalKeys- one for your view and one for your bound:

GlobalKey bound = GlobalKey();
GlobalKey view = GlobalKey();

Then, create a SnapController such as:

SnapController(
  uiChild(),                //uiChild
  false,                    //useCache
  view,                     //viewKey
  bound,                    //boundKey
  Offset.zero,              //constraintsMin
  const Offset(1.0, 1.0),   //constraintsMax
  const Offset(0.75, 0.75), //flexibilityMin
  const Offset(0.75, 0.75), //flexibilityMax
 {Key key,
  customBoundWidth  : 0,
  customBoundHeight : 0,
  snapTargets       : [
    const SnapTarget(Pivot.topLeft, Pivot.topLeft),
    const SnapTarget(Pivot.topRight, Pivot.topRight),
    const SnapTarget(Pivot.bottomLeft, Pivot.bottomLeft),
    const SnapTarget(Pivot.bottomRight, Pivot.bottomRight),
    const SnapTarget(Pivot.center, Pivot.center)
  ],
  minSnapDistance   : 100.0,
  animateSnap       : true,
  useFlick          : true,
  flickSensitivity  : 0.075,
  onMove            : _onMove,
  onDragStart       : _onDragStart,
  onDragUpdate      : _onDragUpdate,
  onDragEnd         : _onDragEnd,
  onSnap            : _onSnap})

Widget uiChild() {
  return Container(
    key: view,
    ...
  ); 
}

void _onMove(Offset offset);

void _onDragStart(dynamic dragDetails);
void _onDragUpdate(dynamic dragDetails);
void _onDragEnd(dynamic dragDetails);

void _onSnap(Offset offset);

Further Explanations:

For a complete set of descriptions for all parameters and methods, see the documentation.

  • Set [useCache] to true if your [uiChild] doesn't change at runtime.
  • Consider the following example:
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Expanded(
      child: Align(
        key: bound,
        alignment: const Alignment(-1.0, -1.0),
        child: SnapController(
          uiChild(),
          true,
          view,
          bound,
          Offset.zero,
          const Offset(1.0, 1.0),
          const Offset(0.75, 0.75),
          const Offset(0.75, 0.75),
          snapTargets: [
            const SnapTarget(Pivot.topLeft, Pivot.topLeft),
            const SnapTarget(Pivot.topRight, Pivot.topRight),
            const SnapTarget(Pivot.bottomLeft, Pivot.bottomLeft),
            const SnapTarget(Pivot.bottomRight, Pivot.bottomRight),
            const SnapTarget(Pivot.center, Pivot.center)
          ]
        )
      )
    )
  ]
)

In this excerpt, the bound is an Align widget which expands through a Column widget.

The SnapController is confined between Offset.zero and Offset(1.0, 1.0). This means the view will not exceed the limits of the bound.

The flexibility is confined between Offset(0.75, 0.75) and Offset(0.75, 0.75). This means that the view can be moved beyond the horizontal/vertical min/max constraints with a flexibility of 0.75 before it snaps.

The snapTargets determine from where and to where the view should snap once the movement is over. In this example:

  1. The top left corner of the view can snap to the top left corner of the bound.
  2. The top right corner of the view can snap to the top right corner of the bound.
  3. The bottom left corner of the view can snap to the bottom left corner of the bound.
  4. The bottom right corner of the view can snap to the bottom right corner of the bound.
  5. The center of the view can snap to the center of the bound.

Keep in mind that these constant values are provided only for the ease of use. snapTargets can consist of any values you wish.

  • Use [SnapControllerState]'s [bool isMoved(double treshold)] method to determine if the view is moved or not where [treshold] is the distance at which the view should be considered to be moved.

Notes #

I started using and learning Flutter only some weeks ago so this package might have some parts that don't make sense, that should be completely different, that could be much better, etc. Please let me know! Nicely!

Any help, suggestion or criticism is appreciated!

Cheers.





[1.0.5] - 21.11.2019 #

  • [minSnapDistance] is added. If set, the snapping will not occur when no [SnapTarget] is found that is closer to the [uiChild] than this value.

  • Updated README.

[1.0.4] - 07.09.2019 #

  • Minor changes.

[1.0.3] - 30.08.2019 #

  • Minor changes.

  • Improved code style.

  • [1.0.3+1] Updated README.

  • [1.0.3+2] Updated README.

[1.0.2] - 23.08.2019 #

  • Minor changes.

  • Improved code style with trailing commas.

  • [1.0.2+1] Minor changes.

[1.0.1] - 20.08.2019 #

  • Improved code style.

  • Code excerpt added to the README.

  • [1.0.1+1] Updated README.

[1.0.0] - 18.08.2019 #

  • flick is now implemented directly to the package.

  • Fine tuning.

  • [1.0.0+1] Updated README.

[0.1.3] - 08.08.2019 #

  • Improved code style.

[0.1.2] - 08.08.2019 #

  • Minor changes.

[0.1.1] - 08.08.2019 #

  • Minor changes.

  • Documentation added.

[0.1.0] - 07.08.2019 #

  • Initial release. Documentation pending.

example/lib/main.dart

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// © Cosmos Software | Ali Yigit Bireroglu                                                                                                           /
// All material used in the making of this code, project, program, application, software et cetera (the "Intellectual Property")                     /
// belongs completely and solely to Ali Yigit Bireroglu. This includes but is not limited to the source code, the multimedia and                     /
// other asset files. If you were granted this Intellectual Property for personal use, you are obligated to include this copyright                   /
// text at all times.                                                                                                                                /
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//@formatter:off

import 'package:flutter/material.dart';

import 'package:snap/snap.dart';

List<GlobalKey> bounds = List<GlobalKey>();
List<GlobalKey> views = List<GlobalKey>();

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Snap Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: MyHomePage(title: 'Snap Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  double bottom = -200.0;

  @override
  void initState() {
    super.initState();

    for (int i = 0; i < 6; i++) {
      bounds.add(GlobalKey());
      views.add(GlobalKey());
    }
  }

  Widget description(String text) {
    return Container(
      constraints: const BoxConstraints.expand(height: 75),
      color: Colors.green,
      child: Center(
        child: Padding(
          padding: const EdgeInsets.all(10),
          child: Text(
            text,
            style: const TextStyle(
              color: Colors.white,
              fontWeight: FontWeight.bold,
              fontSize: 15,
            ),
            textAlign: TextAlign.center,
          ),
        ),
      ),
    );
  }

  Widget gap() {
    return Container(
      constraints: const BoxConstraints.expand(height: 25),
      color: Colors.transparent,
      child: Center(
        child: const Text(
          "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ",
          style: const TextStyle(
            color: Colors.black,
            fontWeight: FontWeight.bold,
            fontSize: 10,
          ),
          textAlign: TextAlign.center,
        ),
      ),
    );
  }

  Widget normalBox(Key key, String text, Color color) {
    return Container(
      key: key,
      width: 200,
      height: 200,
      color: Colors.transparent,
      child: Padding(
        padding: const EdgeInsets.all(5),
        child: Container(
          constraints: BoxConstraints.expand(),
          decoration: BoxDecoration(
            color: color,
            borderRadius: const BorderRadius.all(const Radius.circular(10.0)),
          ),
          child: Center(
            child: Text(
              text,
              style: const TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.bold,
                fontSize: 25,
              ),
              textAlign: TextAlign.center,
            ),
          ),
        ),
      ),
    );
  }

  Widget translatedBox(Key key, String text, Color color) {
    return Transform.translate(
      offset: const Offset(50, 50),
      child: Container(
        key: key,
        width: 200,
        height: 200,
        color: Colors.transparent,
        child: Padding(
          padding: const EdgeInsets.all(5),
          child: Container(
            constraints: BoxConstraints.expand(),
            decoration: BoxDecoration(
              color: color,
              borderRadius: const BorderRadius.all(const Radius.circular(10.0)),
            ),
            child: Center(
              child: Text(
                text,
                style: const TextStyle(
                  color: Colors.white,
                  fontWeight: FontWeight.bold,
                  fontSize: 25,
                ),
                textAlign: TextAlign.center,
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget smallBox(Key key, String text, Color color) {
    return Container(
      key: key,
      width: 50,
      height: 50,
      color: Colors.transparent,
      child: Padding(
        padding: const EdgeInsets.all(5),
        child: Container(
          constraints: BoxConstraints.expand(),
          decoration: BoxDecoration(
            color: color,
            borderRadius: const BorderRadius.all(const Radius.circular(0.0)),
          ),
          child: Center(
            child: Text(
              text,
              style: const TextStyle(
                color: Colors.white,
                fontWeight: FontWeight.bold,
                fontSize: 25,
              ),
              textAlign: TextAlign.center,
            ),
          ),
        ),
      ),
    );
  }

  Widget dottedBox(Key key, String text, Color color) {
    return Transform.translate(
      offset: const Offset(75, 100),
      child: Container(
        key: key,
        width: 200,
        height: 200,
        color: Colors.transparent,
        child: Stack(
          children: [
            Padding(
              padding: const EdgeInsets.all(5),
              child: Container(
                constraints: BoxConstraints.expand(),
                decoration: BoxDecoration(
                  color: color,
                  borderRadius: const BorderRadius.all(const Radius.circular(10.0)),
                ),
                child: Center(
                  child: Text(
                    text,
                    style: const TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold,
                      fontSize: 25,
                    ),
                    textAlign: TextAlign.center,
                  ),
                ),
              ),
            ),
            Align(
              alignment: const Alignment(-0.9, -0.9),
              child: Container(
                width: 10,
                height: 10,
                color: Colors.orangeAccent,
              ),
            ),
            Align(
              alignment: const Alignment(0.9, -0.9),
              child: Container(
                width: 10,
                height: 10,
                color: Colors.black,
              ),
            ),
            Align(
              alignment: const Alignment(-0.9, 0.9),
              child: Container(
                width: 10,
                height: 10,
                color: Colors.deepPurpleAccent,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget firstNormalBoxView() {
    return normalBox(
      views[0],
      "Move & Snap",
      Colors.redAccent,
    );
  }

  Widget animatedBoxView() {
    return normalBox(
      views[1],
      "Move & Snap",
      Colors.redAccent,
    );
  }

  Widget dottedBoxView() {
    return dottedBox(
      views[2],
      "Move & Snap",
      Colors.redAccent,
    );
  }

  Widget translatedBoxView() {
    return translatedBox(
      views[3],
      "Move & Snap",
      Colors.redAccent,
    );
  }

  Widget smallBoxView() {
    return smallBox(
      views[4],
      "*",
      Colors.redAccent,
    );
  }

  Widget secondNormalBoxView() {
    return normalBox(
      views[5],
      "Move & Snap",
      Colors.redAccent,
    );
  }

  Widget thirdNormalBoxView() {
    return normalBox(
      views[6],
      "Move & Snap",
      Colors.redAccent,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: PageView(
        onPageChanged: (int index) {
          if (index == 1)
            setState(() {
              bottom = 0.0;
            });
          else
            setState(() {
              bottom = -200.0;
            });
        },
        children: [
          Scaffold(
            body: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                description("The box will snap to the corners or the center."),
                gap(),
                Expanded(
                  child: Align(
                    key: bounds[0],
                    alignment: const Alignment(-1.0, -1.0),
                    child: SnapController(
                      firstNormalBoxView(),
                      true,
                      views[0],
                      bounds[0],
                      Offset.zero,
                      const Offset(1.0, 1.0),
                      const Offset(0.75, 0.75),
                      const Offset(0.75, 0.75),
                      snapTargets: [
                        const SnapTarget(Pivot.topLeft, Pivot.topLeft),
                        const SnapTarget(Pivot.topRight, Pivot.topRight),
                        const SnapTarget(Pivot.bottomLeft, Pivot.bottomLeft),
                        const SnapTarget(Pivot.bottomRight, Pivot.bottomRight),
                        const SnapTarget(Pivot.center, Pivot.center),
                      ],
                      minSnapDistance: 100,
                    ),
                  ),
                ),
              ],
            ),
          ),
          Scaffold(
            body: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                description("The box will snap to the closest side regardless of animation."),
                gap(),
                Expanded(
                  child: Align(
                    key: bounds[1],
                    alignment: const Alignment(-1.0, -1.0),
                    child: Stack(
                      children: [
                        AnimatedPositioned(
                          left: 0,
                          bottom: bottom,
                          duration: Duration(milliseconds: 300),
                          child: SnapController(
                            animatedBoxView(),
                            true,
                            views[1],
                            bounds[1],
                            Offset.zero,
                            const Offset(1.0, 1.0),
                            const Offset(0.75, 0.75),
                            const Offset(0.75, 0.75),
                            snapTargets: [
                              const SnapTarget(Pivot.closestAny, Pivot.closestAny),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
          Scaffold(
            body: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                description("The box will snap to the closest matching color."),
                gap(),
                Expanded(
                  child: Stack(
                    children: [
                      Align(
                        key: bounds[2],
                        alignment: const Alignment(-1.0, -1.0),
                        child: SnapController(
                          dottedBoxView(),
                          true,
                          views[2],
                          bounds[2],
                          Offset.zero,
                          const Offset(1.0, 1.0),
                          const Offset(0.75, 0.75),
                          const Offset(0.75, 0.75),
                          snapTargets: [
                            const SnapTarget(const Offset(0.1, 0.1), const Offset(0.1, 0.1)),
                            const SnapTarget(const Offset(0.9, 0.1), const Offset(0.85, 0.465)),
                            const SnapTarget(const Offset(0.1, 0.9), const Offset(0.1, 0.9)),
                          ],
                        ),
                      ),
                      Align(
                        alignment: const Alignment(-0.85, -0.85),
                        child: Container(
                          width: 10,
                          height: 10,
                          color: Colors.orangeAccent,
                        ),
                      ),
                      Align(
                        alignment: const Alignment(0.75, -0.1),
                        child: Container(
                          width: 10,
                          height: 10,
                          color: Colors.black,
                        ),
                      ),
                      Align(
                        alignment: const Alignment(-0.85, 0.85),
                        child: Container(
                          width: 10,
                          height: 10,
                          color: Colors.deepPurpleAccent,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
          Scaffold(
            body: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                description("The box will snap to the corners or the center while maintaining the initial offset"),
                gap(),
                Expanded(
                  child: Align(
                    key: bounds[3],
                    alignment: const Alignment(-1.0, -1.0),
                    child: SnapController(
                      translatedBoxView(),
                      true,
                      views[3],
                      bounds[3],
                      Offset(50.0 / MediaQuery.of(context).size.width, 50.0 / MediaQuery.of(context).size.height),
                      Offset(1.0, 1.0) - Offset(50.0 / MediaQuery.of(context).size.width, 50.0 / MediaQuery.of(context).size.height),
                      const Offset(0.75, 0.75),
                      const Offset(0.75, 0.75),
                      snapTargets: [
                        SnapTarget(Pivot.topLeft, Offset(50.0 / MediaQuery.of(context).size.width, 50.0 / MediaQuery.of(context).size.height)),
                        SnapTarget(Pivot.topRight, Offset(1.0 - 50.0 / MediaQuery.of(context).size.width, 50.0 / MediaQuery.of(context).size.height)),
                        SnapTarget(Pivot.bottomLeft, Offset(50.0 / MediaQuery.of(context).size.width, 1.0 - 50.0 / MediaQuery.of(context).size.height)),
                        SnapTarget(Pivot.bottomRight, Offset(1.0, 1.0) - Offset(50.0 / MediaQuery.of(context).size.width, 50.0 / MediaQuery.of(context).size.height)),
                        const SnapTarget(Pivot.center, Pivot.center),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
          Scaffold(
            body: Column(
              children: [
                description("The box will snap to the closest side or the center within its container."),
                gap(),
                Container(
                  width: 300,
                  height: 300,
                  color: Colors.orangeAccent,
                  child: Align(
                    key: bounds[4],
                    alignment: const Alignment(-1.0, -1.0),
                    child: SnapController(
                      smallBoxView(),
                      true,
                      views[4],
                      bounds[4],
                      Offset.zero,
                      const Offset(1.0, 1.0),
                      const Offset(0.15, 0.15),
                      const Offset(0.15, 0.15),
                      snapTargets: [
                        const SnapTarget(Pivot.closestAny, Pivot.closestAny),
                        const SnapTarget(Pivot.center, Pivot.center),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
          Scaffold(
            body: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                description("The box will snap to the corners or the center without animation."),
                gap(),
                Expanded(
                  child: Align(
                    key: bounds[5],
                    alignment: const Alignment(-1.0, -1.0),
                    child: SnapController(
                      secondNormalBoxView(),
                      false,
                      views[5],
                      bounds[5],
                      Offset.zero,
                      const Offset(1.0, 1.0),
                      const Offset(0.75, 0.75),
                      const Offset(0.75, 0.75),
                      snapTargets: [
                        const SnapTarget(Pivot.topLeft, Pivot.topLeft),
                        const SnapTarget(Pivot.topRight, Pivot.topRight),
                        const SnapTarget(Pivot.bottomLeft, Pivot.bottomLeft),
                        const SnapTarget(Pivot.bottomRight, Pivot.bottomRight),
                        const SnapTarget(Pivot.center, Pivot.center),
                      ],
                      animateSnap: false,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  snap: ^1.0.5

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:snap/snap.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
57
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
78
Learn more about scoring.

We analyzed this package on Apr 4, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.1
  • pana: 0.13.6
  • Flutter: 1.12.13+hotfix.8

Health suggestions

Format lib/snap_controller.dart.

Run flutter format to format lib/snap_controller.dart.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.1.0 <3.0.0
flick ^1.0.3 1.0.3
flutter 0.0.0
Transitive dependencies
collection 1.14.11 1.14.12
meta 1.1.8
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test