{"id":32406645,"url":"https://github.com/roipeker/just_motion","last_synced_at":"2025-10-25T12:55:59.177Z","repository":{"id":49019403,"uuid":"376918710","full_name":"roipeker/just_motion","owner":"roipeker","description":"Simple reactive animations in your Flutter apps. ","archived":false,"fork":false,"pushed_at":"2021-07-01T00:07:48.000Z","size":234,"stargazers_count":50,"open_issues_count":0,"forks_count":4,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-04-22T20:49:48.427Z","etag":null,"topics":["animation","flutter","motion"],"latest_commit_sha":null,"homepage":"https://github.com/roipeker/just_motion","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/roipeker.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-06-14T18:22:59.000Z","updated_at":"2024-04-22T20:49:48.428Z","dependencies_parsed_at":"2022-08-27T22:53:55.949Z","dependency_job_id":null,"html_url":"https://github.com/roipeker/just_motion","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/roipeker/just_motion","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roipeker%2Fjust_motion","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roipeker%2Fjust_motion/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roipeker%2Fjust_motion/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roipeker%2Fjust_motion/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/roipeker","download_url":"https://codeload.github.com/roipeker/just_motion/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roipeker%2Fjust_motion/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280958215,"owners_count":26420235,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-25T02:00:06.499Z","response_time":81,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["animation","flutter","motion"],"created_at":"2025-10-25T12:55:51.253Z","updated_at":"2025-10-25T12:55:59.164Z","avatar_url":"https://github.com/roipeker.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# just.motion\n\nFlutter package to create organic motion transitions.\n\n- [Why?](#why?)\n- [The Motion Value](#the-motion-value)\n  - [stateless hot reload](#stateless-hot-reload)\n  - [status notifier](#status-notifier)\n- [Ease Motion](#ease-motion)\n- [Spring Motion](#spring-motion)\n- [The Widgets](#the-widgets)\n- [Performance Considerations](#performance-considerations)\n- [Installation](#installation)\n- [How to contribute](#how-to-contribute)\n\n## Why?\n\nWhy not? the basic difference with _Tweens_ is that the change in value looks much cooler, it feels more \"organic\", while Tweens are more... \"mechanical\". \n\n`just.motion` is not based on `Duration` and interpolated percentage values from 0-1; but rather on distance between the current value and the target value. \n\nOn the other hand, it doesn't need a Ticker provider, nor Implicit widgets to compensate the code boilerplate. Also, no need for `AnimationController`.\n\nThere's a single internal Ticker, that manages it's own state based on the auto-subscribed `MotionValues`. \nWhen there're no active motions, it just stops. \n\nAlso, it has a very simple reactive support when `MotionValue`s (base class for `EaseValue` and `SpringValue`) are consumed inside a `Motion()` or `MotionBuilder()` widgets.\n\nIs a great option for your app's animations! as subscriptions and memory management are also automatic when using `MotionValue` with the provided `Motion`(s) widgets.\n\n## The Motion Value\n\nHere's how it works:\n\nBasic _easing_ is a well known technique in games for light computation of movements, is based on proportional velocity, and his cousin, _spring_, is based on proportional acceleration.\n\nYou set a `target` value, `MotionValue` calculates the distance, and the movement it applies is proportional to the distance: bigger distance, faster the motion.\n\nSo, as _acceleration_ is proportional to the distance, the further the target, the faster the value moves, as it gets closer and closer to the target, it hardly changes the value... that's why you can configure `minDistance` to tell the motion when is time to stop.\n\nWhile on `springs`, the _acceleration_ is proportional to the distance, if `target` is far away from `value`, a lot of acceleration is provided, increasing the _velocity_ very quickly. Unlike _easing_, as the `value` approaches the `target`, less and less acceleration is applied, but still has an ongoing acceleration, as it flies pass the `target`, the acceleration pulls it back, while `friction` helps the `value` to settle down.\n\n#### Status notifier\n\nIn `MotionValues` you can listen to status changes:\n\n```dart\nheight.addStatusListener((){\n  print(height.status);\n});\n```\n\nCheck `MotionStatus.values` to see the current available status. (Might slightly change in the near future).\n\nCurrently available status:\n```dart\nenum MotionStatus {\n  idle, /// initial status, without target.\n  target, /// when value reaches target.\n  activate, /// when target != value, and object is added in the ticker.\n  deactivate, /// when a `delay()` is called while `target!=value` (moving status)\n  delayComplete,  /// when a `delay()` ends, and moving starts.\n  moving, /// while `value` is moving towards `target`.\n  disposed  /// when the `MotionValue` is disposed from memory (can't be used again).  \n}\n```\n\n#### Stateless hot reload\nWhen you create a `MotionValue` in a StatelessWidget, or inside a `build(BuildContext)` scope. You should notify **just.motion** to auto dispose the variable for hot-reload.\n\nUse `MotionValue.stateless` for that. It will assure the disposal of the instance from the running Ticker:\n\n```dart\nfinal height = 10.ease( target: 20, stateless: true);\n```\n\u003e  `stateless` only works when using `Motion` or `MotionBuilder` widgets, not with `AnimatedBuilder`. \n\n\n\n### Ease Motion\n\n\nDeclare a var that you will use to animate some widget property:\n\n\u003e NOTE: When using the `ease()` extension, (like `10.ease()`), `int` and `double` nums will use `EaseValue` which is based on `double`.\nIs up to you to cast the `value` to `int`: (example:  `height().round()`, or `height().toInt()`).\n\n\n\n```dart\nfinal height = EaseValue( 30 );\n///or the extension approach.\nfinal height = 30.ease();\n\nfinal bgColor = EaseColor( Colors.red );\nfinal bgColor = Colors.red.ease();\n```\n_You can configure the `target` value, and other motion properties right away in both declarations._\n\nTo change the `target` value after object initialization:\n\n```dart\n/// `MotionValue` is a callable instance. So can change target as if it was a method.\nheight(100);\n\n/// If you need to change a motion property, you can use:\nheight.to( 100, ease: 30, minDistance: .1 );\n\n/// or just modify the target property.\nheight.target = 100;\n```\n\nThis is what makes **just.motion** shine. You can change the `target` anytime, and it will smoothly transition `value` towards it, without mechanic or abrupt visual cuts, like time based Tweens.\n\nTo read the current value:\n```dart\nprint( height());\n// or\nprint( height.value );\n```\n\nThe motion objects detects when `target` is modified, and runs the simulation accordingly.\nA motion object is idle, when `value` reaches `target`, and will be unsubscribed from the ticker provider.\n\nTo stop the animation, set the **`motion.value = motion.target`**, otherwise the ticker will keep running until the values are closed enough to hit the `minDistance` threshold, and deactivate themselves.\n\nYou can set an absolute value to rebuild the widget, preventing the animation, with:\n`height.value = height.target = 10;`\n\nor better yet:\n`height.set( 10 );`\n\n## Spring Motion\n\nMuch like `EaseValue`, `SpringValue` is another type of motion, just play with the parameters.\nWatch out the `minDistance`, probably for drastic bouncing, you will need to provide a very small \nnumber (like .0001)... use at discretion, and experiment with the values.\n\n```dart\nfinal height = SpringValue(10);\n\nfinal height = 10.spring()\n```\n\nHere's an example of a __bouncing button__.\n\n```dart\nclass SpringyButton extends StatelessWidget {\n  final Widget child;\n  final double pressScale;\n  SpringyButton({\n    Key? key,\n    required this.child,\n    this.pressScale = 0.75,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    final scaleValue = 1.spring(minDistance: .00025, spring: .1, stateless:true);\n    return GestureDetector(\n      onTapDown: (e) =\u003e scaleValue.to(pressScale, friction: .85),\n      onTapUp: (e) =\u003e scaleValue.to(1, friction: .92),\n      onTapCancel: () =\u003e scaleValue.to(1, friction: .92),\n      child: MotionBuilder(\n        builder: (BuildContext context, Widget? child) =\u003e Transform.scale(\n          transformHitTests: false,\n          scale: scaleValue(),\n          child: child,\n        ),\n        child: child,\n      ),\n    );\n  }\n}\n```\n\n## The Widgets\n\n`MotionValue` is a ChangeNotifier, so you can use `AnimationBuilder`:\n\n\n```dart\n@override\n  Widget build(BuildContext context) {\n    /// Warning: you will not be able to dispose these variables on hot-reload. \n    final height = 24.0.ease(target: 120, ease: 23);\n    final bgColor = Colors.black12.ease(ease: 45);\n    bgColor.to(Colors.red);\n\n    /// delay() is defined in seconds, will deactivate the ticker call until it hits the timeout. \n    height.delay(1);\n\n    return Center(\n      child: Material(\n        child: AnimatedBuilder(\n          animation: Listenable.merge([height, bgColor]),\n          builder: (context, child) {\n            return Container(\n              height: height(),\n              color: bgColor(),\n              child: Center(\n                child: Text('height: ${height().toStringAsFixed(2)}'),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n```\n\nBut motion provides a simpler Widget to repaint your animation.\n\nIf you just need to paint a \"leaf\" widget, so have no need to use the `child` optimization of `AnimatedBuilder`, nor `context`, you can't got simpler than `Motion`:\n\n```dart\nreturn Motion(\n  () =\u003e Container(\n    height: height(),\n    color: bgColor(),\n    child: Center(\n      child: Text('height: ${height().ringAsFixed(2)}'),\n    ),\n  ),\n);\n```\n\nWhen you need to cache the `child` rebuild, like `AnimatedBuilder`, you can use `MotionBuilder`:\n\n```dart\nreturn MotionBuilder(\n  builder: (context, child) =\u003e Container(\n    height: height(),\n    color: bgColor(),\n    child: Center(\n      child: child,\n    ),\n  ),\n  child: Text('animating height'),\n);\n```\n\nBoth widgets will dispose the motion values when they are removed from the widget, if the object isn't consumed by another Listener.\n\n## Performance Considerations\n\n- As there's a single ticker provider running for all `MotionValue` instances, the lifecycle of this Ticker is persistent through the lifetime of your app. Is not tied up to a Widget's `State`, meaning that is totally possible to have lots of concurrent and actives `MotionValue`, even when nothing is consuming those values (you can inspect that in the [_Flutter Performance_ tab in your IDE](https://flutter.dev/docs/perf/rendering/ui-performance)). \nWhen you initialize a motion object, **and set a target** different than the `value`... the `Ticker` will start processing the object, no matter if you are consuming the value to repaint a Widget or not. This is not a big penalty on performance by any means, as Flutter does it all the time, but be sure to orchestrate properly the `target` assignment, when you actually will consume the value.\n\n- If you are using a `StatefulWidget`, or some other state management solution that provides you with Widget lifecycles, you can manually call `motion.dispose()`. Although memory is managed internally, didn't find any leaks so far.\n\n- If you are composing nested Animations, or reusable Widgets based on **just.motion**, is better to avoid child rebuilds in your tree. Prefer the usage of `MotionBuilder()` for those scenarios. And yes, you can deeply nest motion objects into `Motion()` and `MotionBuilder()` and they will take the appropiate `values` in their builder function scope.\n\n- Remember, you can help the Flutter Engine to decide where to cache some part of the Widget tree, that will rebuild independently from the rest of the screen with [`RepaintBoundry()`](https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html), as [setState() can propagate repainting up and down the tree](https://blog.gskinner.com/archives/2020/09/flokk-how-we-built-a-desktop-app-using-flutter.html#performance). Which can lead to a percieved lost frames. Apparently this is more notorious on desktop targets, but is always cool to pay attention to those details... if you have a \"big\" area of your app that's animated somehow, and you see the performance isn't so great, try to enclose the widget with RepaintBoundry().\n\n## Installation\n\n**just_motion** is in active developing and testing stages. In a couple of days it will be available in pub.dev\n\nIn the meantime, if you wanna use it and help me improve it, you should be using dart \u003e= 2.12\n\n- Just use this repo url in your _pubspec.yaml_\n\n```yaml\ndependencies:\n  just_motion:\n    git: https://github.com/roipeker/just_motion.git\n```\n\n- To use a specific version, check the _commits_ [at the top of the page](https://github.com/roipeker/just_motion), and use the specific **commit** hash.\nFor example, for [v0.0.6+23](https://github.com/roipeker/just_motion/commit/c6ee99cbffce216e0c4587c1005f4104057d44a3)\n```yaml\ndependencies:\n  just_motion:\n    git:\n      url: https://github.com/roipeker/just_motion.git\n      ref: c6ee99cbffce216e0c4587c1005f4104057d44a3\n```\n\n- Run `flutter pub get`\n\n- Then import **just_motion** in your code:\n\n```dart\nimport 'package:just_motion/just_motion.dart';\n```\n\nNow go, and make your apps comes to life.\n\nHappy coding!\n\n## How to contribute\n\n**just.motion** is open for contributions:\n\n- Helping to fix typos, adjust, translate the readme into other languages.\n- Offering PRs for code/tests.\n- Making examples.\n- Making articles/videos.\n- Including new features or discussing the current API.\n\nAny contribution is welcome!\n\n## Getting Started\n\nThis project is a starting point for a Dart\n[package](https://flutter.dev/developing-packages/),\na library module containing code that can be shared easily across\nmultiple Flutter or Dart projects.\n\nFor help getting started with Flutter, view our \n[online documentation](https://flutter.dev/docs), which offers tutorials, \nsamples, guidance on mobile development, and a full API reference.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froipeker%2Fjust_motion","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froipeker%2Fjust_motion","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froipeker%2Fjust_motion/lists"}