Related
I have a PageView, how can I disable the left or right scrolling. I know by using NeverScrollableScrollPhysics we can disable the scrolling but how to disable the scrolling in one direction.
You can create your own ScrollPhysics to allow only go to the right:
class CustomScrollPhysics extends ScrollPhysics {
CustomScrollPhysics({ScrollPhysics parent}) : super(parent: parent);
bool isGoingLeft = false;
#override
CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomScrollPhysics(parent: buildParent(ancestor));
}
#override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
isGoingLeft = offset.sign < 0;
return offset;
}
#override
double applyBoundaryConditions(ScrollMetrics position, double value) {
//print("applyBoundaryConditions");
assert(() {
if (value == position.pixels) {
throw FlutterError(
'$runtimeType.applyBoundaryConditions() was called redundantly.\n'
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.\n'
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n');
}
return true;
}());
if (value < position.pixels && position.pixels <= position.minScrollExtent)
return value - position.pixels;
if (position.maxScrollExtent <= position.pixels && position.pixels < value)
// overscroll
return value - position.pixels;
if (value < position.minScrollExtent &&
position.minScrollExtent < position.pixels) // hit top edge
return value - position.minScrollExtent;
if (position.pixels < position.maxScrollExtent &&
position.maxScrollExtent < value) // hit bottom edge
return value - position.maxScrollExtent;
if (!isGoingLeft) {
return value - position.pixels;
}
return 0.0;
}
}
Usage:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: PageView.builder(
itemCount: 4,
physics: CustomScrollPhysics(),
itemBuilder: (context, index) => Center(
child: Text("Item $index"),
),
));
}
You can also use the horizontal_blocked_scroll_physics library which I recently wrote that will allow you to block the left and right movements.
Came up with a slightly better approach for this.
This approach doesn't require instantiating new physics instances and can be updated dynamically.
Use the function parameter onAttemptDrag to allow or deny swipe requests. Your code in this function should be efficient as it will get called many times per second (when scrolling). Additionally, you may want to add a flag in this function that allows requests of programmatic origin to go through. For example, in the case of a "next button", this physics implementation would also block the functions jumpTo(..) and animateTo from working, so you'd need to have a flag that will temporarily return a default true for a specific page transition if the next button has been pressed. Let me know of any questions or ways to improve this.
class LockingPageScrollPhysics extends ScrollPhysics {
/// Requests whether a drag may occur from the page at index "from"
/// to the page at index "to". Return true to allow, false to deny.
final Function(int from, int to) onAttemptDrag;
/// Creates physics for a [PageView].
const LockingPageScrollPhysics(
{ScrollPhysics parent, #required this.onAttemptDrag})
: super(parent: parent);
#override
LockingPageScrollPhysics applyTo(ScrollPhysics ancestor) {
return LockingPageScrollPhysics(
parent: buildParent(ancestor), onAttemptDrag: onAttemptDrag);
}
double _getPage(ScrollMetrics position) {
if (position is PagePosition) return position.page;
return position.pixels / position.viewportDimension;
}
double _getPixels(ScrollMetrics position, double page) {
if (position is PagePosition) return position.getPixelsFromPage(page);
return page * position.viewportDimension;
}
double _getTargetPixels(
ScrollMetrics position, Tolerance tolerance, double velocity) {
double page = _getPage(position);
if (velocity < -tolerance.velocity)
page -= 0.5;
else if (velocity > tolerance.velocity) page += 0.5;
return _getPixels(position, page.roundToDouble());
}
#override
double applyBoundaryConditions(ScrollMetrics position, double value) {
assert(() {
if (value == position.pixels) {
throw FlutterError('$runtimeType.applyBoundaryConditions() was called redundantly.\n'
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.\n'
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n');
}
return true;
}());
/*
* Handle the hard boundaries (min and max extents)
* (identical to ClampingScrollPhysics)
*/
if (value < position.pixels && position.pixels <= position.minScrollExtent) // under-scroll
return value - position.pixels;
if (position.maxScrollExtent <= position.pixels && position.pixels < value) // over-scroll
return value - position.pixels;
if (value < position.minScrollExtent &&
position.minScrollExtent < position.pixels) // hit top edge
return value - position.minScrollExtent;
if (position.pixels < position.maxScrollExtent &&
position.maxScrollExtent < value) // hit bottom edge
return value - position.maxScrollExtent;
bool left = value < position.pixels;
int fromPage, toPage;
double overScroll = 0;
if (left) {
fromPage = position.pixels.ceil() ~/ position.viewportDimension;
toPage = value ~/ position.viewportDimension;
overScroll = value - fromPage * position.viewportDimension;
overScroll = overScroll.clamp(value - position.pixels, 0.0);
} else {
fromPage =
(position.pixels + position.viewportDimension).floor() ~/ position.viewportDimension;
toPage = (value + position.viewportDimension) ~/ position.viewportDimension;
overScroll = value - fromPage * position.viewportDimension;
overScroll = overScroll.clamp(0.0, value - position.pixels);
}
if (fromPage != toPage && !onAttemptDrag(fromPage, toPage)) {
return overScroll;
} else {
return super.applyBoundaryConditions(position, value);
}
}
Here's my implementation of PagePosition: https://gist.github.com/wdavies973/e596b8bb6b7ef773522e169464b53779
Here's a library I've been working on that uses this modified controller: https://github.com/RobluScouting/FlutterBoardView.
Here is another version of the #diegoveloper and #wdavies973 ideas. I got the necessity of being able to only scroll with some programming command, ignoring the user gesture. Instead of some complex workaround in the main Widget, I was able to lock the screen to scroll using the ScrollPhysics behavior. When is user interaction, the applyPhysicsToUserOffset is called before applyBoundaryConditions. With this in mind, here is my sample, hope it help someone.
Note: Once locked, it is possible to navigate the pages using the controller.animateTo(pageIndex)
/// Custom page scroll physics
// ignore: must_be_immutable
class CustomLockScrollPhysics extends ScrollPhysics {
/// Lock swipe on drag-drop gesture
/// If it is a user gesture, [applyPhysicsToUserOffset] is called before [applyBoundaryConditions];
/// If it is a programming gesture eg. `controller.animateTo(index)`, [applyPhysicsToUserOffset] is not called.
bool _lock = false;
/// Lock scroll to the left
final bool lockLeft;
/// Lock scroll to the right
final bool lockRight;
/// Creates physics for a [PageView].
/// [lockLeft] Lock scroll to the left
/// [lockRight] Lock scroll to the right
CustomLockScrollPhysics({ScrollPhysics parent, this.lockLeft = false, this.lockRight = false})
: super(parent: parent);
#override
CustomLockScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomLockScrollPhysics(parent: buildParent(ancestor), lockLeft: lockLeft, lockRight: lockRight);
}
#override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
if ((lockRight && offset < 0) || (lockLeft && offset > 0)) {
_lock = true;
return 0.0;
}
return offset;
}
#override
double applyBoundaryConditions(ScrollMetrics position, double value) {
assert(() {
if (value == position.pixels) {
throw FlutterError('$runtimeType.applyBoundaryConditions() was called redundantly.\n'
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.\n'
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n');
}
return true;
}());
/*
* Handle the hard boundaries (min and max extents)
* (identical to ClampingScrollPhysics)
*/
// under-scroll
if (value < position.pixels && position.pixels <= position.minScrollExtent) {
return value - position.pixels;
}
// over-scroll
else if (position.maxScrollExtent <= position.pixels && position.pixels < value) {
return value - position.pixels;
}
// hit top edge
else if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) {
return value - position.pixels;
}
// hit bottom edge
else if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) {
return value - position.pixels;
}
var isGoingLeft = value <= position.pixels;
var isGoingRight = value >= position.pixels;
if (_lock && ((lockLeft && isGoingLeft) || (lockRight && isGoingRight))) {
_lock = false;
return value - position.pixels;
}
return 0.0;
}
}
Usage example:
#override
void initState() {
super.initState();
controller.tabController = TabController(initialIndex: 0, length: 3, vsync: this);
}
void nextTab() => controller.tabController.animateTo(controller.tabController.index + 1);
#override
void build(BuildContext context)
return TabBarView(
controller: controller.tabController,
physics: CustomLockScrollPhysics(lockLeft: true),
children: [ InkWell(onTap: nextTab), InkWell(onTap: nextTab), Container() ],
),
}
Final note: I tried everything to avoid using some variable in the class since ScrollPhysics is #immutable, but in this case, it was the only way to successfully know if the input was a user gesture.
if you decide to use #Fabricio N. de Godoi's answer like me, there is a bug. For example after you blocked scroll right, scroll right will not work as expected. But just after that if you scroll the opposide direction page will scroll right.
And this is the solition;
Find this code
if (_lock && ((lockLeft && isGoingLeft) || (lockRight && isGoingRight))) {
_lock = false;
return value - position.pixels;
}
And replace with
if (_lock && ((lockLeft && isGoingLeft) || (lockRight && isGoingRight))) {
_lock = false;
return value - position.pixels;
} else {
_lock = true;
}
Very simple
Use ScrollController and NotificationListener
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(backgroundColor: Colors.black, body: MyWidget()));
}
}
class MyWidget extends StatefulWidget {
#override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
//Add this ScrollController
final ScrollController scrollController = ScrollController();
bool stop = false;
#override
Widget build(BuildContext context) {
return Stack(
children: [
//Add this NotificationListener
NotificationListener<ScrollUpdateNotification>(
onNotification: (notification) {
print('${notification.scrollDelta}');
if (notification.scrollDelta! < 0) {
setState(() {
stop = true;
});
//Add this jumpTo
scrollController.jumpTo(
notification.metrics.pixels - notification.scrollDelta!);
} else if (notification.scrollDelta! > 3) {
setState(() {
stop = false;
});
}
return true;
},
child: ListView.builder(
controller: scrollController,
scrollDirection: Axis.horizontal,
itemBuilder: (context, i) {
return Container(
width: 100,
color: Colors.primaries[i % Colors.primaries.length]);
}),
),
if (stop)
Center(
child: Text(
'STOP',
style: Theme.of(context).textTheme.headline1,
))
],
);
}
}
I tried the solution of diegoveloper and I found a more clean way to do it. This one locks the inverted scroll, it depends on the orientation, that's the reason why i havent used right or left, cause if you are using a listview up and down it will block the scroll up
class DirectionalScrollPhysics extends ScrollPhysics {
DirectionalScrollPhysics({
this.inverted = false,
ScrollPhysics? parent,
}) : super(parent: parent);
final bool inverted;
#override
DirectionalScrollPhysics applyTo(ScrollPhysics? ancestor) {
return DirectionalScrollPhysics(
inverted: inverted, parent: buildParent(ancestor));
}
#override
double applyBoundaryConditions(ScrollMetrics position, double value) {
final bool condition = this.inverted ? value < 0 : value > 0;
if (condition) return value;
return 0.0;
}
}
Then on your widget
ListView(
scrollDirection: Axis.horizontal,
physics: DirectionalScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
children: [],
),
I am building a horizontally scrollable list in flutter using PageView, however, I would like to be able to scroll multiple pages at the same time. It is currently possible if I scroll really fast, but it is far from ideal. Another option is to set pageSnapping to false, but then it does not snap at all, which is not what I want.
I am thinking that it might be possible to change pageSnapping from false to true if the scroll velocity is under a certain treshhold, but i don't know how I would get said velocity.
The app i am building looks something like this.
All help appreciated!
To anyone coming here in the future, i finally solved this using a Listener insted of a GestureDetector to calculate the code manually.
Here is the relevant code:
class HomeWidget extends StatefulWidget {
#override
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int _currentPage = 0;
PageController _pageController;
int t; //Tid
double p; //Position
#override
initState() {
super.initState();
_pageController = PageController(
viewportFraction: 0.75,
initialPage: 0,
);
}
#override
Widget build(BuildContext context) {
return Container(
child: Listener(
onPointerMove: (pos) { //Get pointer position when pointer moves
//If time since last scroll is undefined or over 100 milliseconds
if (t == null || DateTime.now().millisecondsSinceEpoch - t > 100) {
t = DateTime.now().millisecondsSinceEpoch;
p = pos.position.dx; //x position
} else {
//Calculate velocity
double v = (p - pos.position.dx) / (DateTime.now().millisecondsSinceEpoch - t);
if (v < -2 || v > 2) { //Don't run if velocity is to low
//Move to page based on velocity (increase velocity multiplier to scroll further)
_pageController.animateToPage(_currentPage + (v * 1.2).round(),
duration: Duration(milliseconds: 800), curve: Curves.easeOutCubic);
}
}
},
child: PageView(
controller: _pageController,
physics: ClampingScrollPhysics(), //Will scroll to far with BouncingScrollPhysics
scrollDirection: Axis.horizontal,
children: <Widget>[
//Pages
],
),
),
);
}
}
Interesting problem!
To gain the velocity of a swipe you can use a GestureDetector, unfortunately when trying to use both a GestureDetector and PageView then the PageView steals the focus from the GestureDetector so they can not be used in unison.
GestureDetector(
onPanEnd: (details) {
Velocity velocity = details.velocity;
print("onPanEnd - velocity: $velocity");
},
)
Another way however was to use DateTime in the PageView's onPageChanged to measure the change in time, instead of velocity. However this is not ideal, the code I wrote is like a hack. It has a bug (or feature) that the first swipe after a standstill on a page will only move one page, however consecutive swipes will be able to move multiple pages.
bool pageSnapping = true;
List<int> intervals = [330, 800, 1200, 1600]; // Could probably be optimised better
DateTime t0;
Widget timeBasedPageView(){
return PageView(
onPageChanged: (item) {
// Obtain a measure of change in time.
DateTime t1 = t0 ?? DateTime.now();
t0 = DateTime.now();
int millisSincePageChange = t0.difference(t1).inMilliseconds;
print("Millis: $millisSincePageChange");
// Loop through the intervals, they only affect how much time is
// allocated before pageSnapping is enabled again.
for (int i = 1; i < intervals.length; i++) {
bool lwrBnd = millisSincePageChange > intervals[i - 1];
bool uprBnd = millisSincePageChange < intervals[i];
bool withinBounds = lwrBnd && uprBnd;
if (withinBounds) {
print("Index triggered: $i , lwr: $lwrBnd, upr: $uprBnd");
// The two setState calls ensures that pageSnapping will
// always return to being true.
setState(() {
pageSnapping = false;
});
// Allows some time for the fast pageChanges to proceed
// without being pageSnapped.
Future.delayed(Duration(milliseconds: i * 100)).then((val){
setState(() {
pageSnapping = true;
});
});
}
}
},
pageSnapping: pageSnapping,
children: widgets,
);
}
I hope this helps in some way.
Edit: another answer based upon Hannes' answer.
class PageCarousel extends StatefulWidget {
#override
_PageCarouselState createState() => _PageCarouselState();
}
class _PageCarouselState extends State<PageCarousel> {
int _currentPage = 0;
PageController _pageController;
int timePrev; //Tid
double posPrev; //Position
List<Widget> widgets = List.generate(
10,
(item) =>
Container(
padding: EdgeInsets.all(8),
child: Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Index $item"),
],
),
),
));
#override
void initState() {
super.initState();
_pageController = PageController(
viewportFraction: 0.75,
initialPage: 0,
);
}
int boundedPage(int newPage){
if(newPage < 0){
return 0;
}
if(newPage >= widgets.length){
return widgets.length - 1;
}
return newPage;
}
#override
Widget build(BuildContext context) {
return Container(
child: Listener(
onPointerDown: (pos){
posPrev = pos.position.dx;
timePrev = DateTime.now().millisecondsSinceEpoch;
print("Down");
print("Time: $timePrev");
},
onPointerUp: (pos){
int newTime = DateTime.now().millisecondsSinceEpoch;
int timeDx = newTime - timePrev;
double v = (posPrev - pos.position.dx) / (timeDx);
int newPage = _currentPage + (v * 1.3).round();
print("Velocity: $v");
print("New Page: $newPage, Old Page: $_currentPage");
if (v < 0 && newPage < _currentPage || v >= 0 && newPage > _currentPage) {
_currentPage = boundedPage(newPage);
}
_pageController.animateToPage(_currentPage,
duration: Duration(milliseconds: 800), curve: Curves.easeOutCubic);
},
child: PageView(
controller: _pageController,
physics: ClampingScrollPhysics(), //Will scroll to far with BouncingScrollPhysics
scrollDirection: Axis.horizontal,
children: widgets,
),
),
);
}
}
This should ensure a decent multi page navigation.
The easiest and most natural way is to customize the physics of the PageView. You can set velocityPerOverscroll (logical pixels per second) according to your needs.
PageView(
pageSnapping: false,
physics: const PageOverscrollPhysics(velocityPerOverscroll: 1000),
Extend ScrollPhysics to control the snap behavior:
class PageOverscrollPhysics extends ScrollPhysics {
///The logical pixels per second until a page is overscrolled.
///A satisfying value can be determined by experimentation.
///
///Example:
///If the user scroll velocity is 3500 pixel/second and [velocityPerOverscroll]=
///1000, then 3.5 pages will be overscrolled/skipped.
final double velocityPerOverscroll;
const PageOverscrollPhysics({
ScrollPhysics? parent,
this.velocityPerOverscroll = 1000,
}) : super(parent: parent);
#override
PageOverscrollPhysics applyTo(ScrollPhysics? ancestor) {
return PageOverscrollPhysics(
parent: buildParent(ancestor)!,
);
}
double _getTargetPixels(ScrollMetrics position, double velocity) {
double page = position.pixels / position.viewportDimension;
page += velocity / velocityPerOverscroll;
double pixels = page.roundToDouble() * position.viewportDimension;
return pixels;
}
#override
Simulation? createBallisticSimulation(
ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
return super.createBallisticSimulation(position, velocity);
}
final double target = _getTargetPixels(position, velocity);
if (target != position.pixels) {
return ScrollSpringSimulation(spring, position.pixels, target, velocity,
tolerance: tolerance);
}
return null;
}
#override
bool get allowImplicitScrolling => false;
}
Note that it is also required to set pageSnapping: false, this is done intentionally to disable the internal physics of PageView, which we are overwriting with PageOverscrollPhysics. Although PageOverscrollPhysics might look crazy complicated, it's essentially just an adjustment of the PageScrollPhysics()class.
Is it possible to have this same effect using the package charts_flutter? In this case the user can rotate the pie chart.
User rotating pie chart
It's not possible with the current implementation of the charting library you're using unless you used their code and changed. You might be able to get it to work with the flutter circular chart plugin by hooking up your gesture detecting code and animating the value for startAngle, but I'm not sure it would do exactly what you want (or might try to redraw the entire thing each time which isn't overly performant).
I had some old code lying around that implemented most of what you want so I fixed it up a little - here's an example of just writing your own pie chart. You can copy/paste it into a file and run it as-is.
Your mileage may vary with this - I haven't tested it extensively or anything, but you're welcome to use it at least as a starting point - it has the code for drawing a pie chart and rotating according to gestures at least.
There's quite a lot of stuff in here so I'd encourage you to give it a deep read-through to see exactly what I'm doing. I don't have time to add documentation right now, but if you have any questions feel free to ask.
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: SafeArea(
child: Material(
child: RotatingPieChart(
items: [
PieChartItem(30, "one", Colors.red),
PieChartItem(210, "two", Colors.green),
PieChartItem(60, "three", Colors.blue),
PieChartItem(35, "four", Colors.teal),
PieChartItem(25, "five", Colors.orange)
],
toText: (item, _) => TextPainter(
textAlign: TextAlign.center,
text: TextSpan(
style: TextStyle(color: Colors.black, fontSize: 8.0),
text: "${item.name}\n${item.val}",
),
textDirection: TextDirection.ltr),
),
),
),
);
}
}
class PieChartItem {
final num val;
final String name;
final Color color;
PieChartItem(this.val, this.name, this.color) : assert(val != 0);
}
typedef TextPainter PieChartItemToText(PieChartItem item, double total);
class RotatingPieChart extends StatelessWidget {
final double accellerationFactor;
final List<PieChartItem> items;
final PieChartItemToText toText;
const RotatingPieChart({Key key, this.accellerationFactor = 1.0, #required this.items, #required this.toText})
: super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: AspectRatio(
aspectRatio: 1.0,
child: _RotatingPieChartInternal(
items: items,
toText: toText,
accellerationFactor: accellerationFactor,
),
),
);
}
}
class _RotationEndSimulation extends Simulation {
final double initialVelocity;
final double initialPosition;
final double accelleration;
_RotationEndSimulation({
#required this.initialVelocity,
#required double decelleration,
#required this.initialPosition,
}) : accelleration = decelleration * -1.0;
#override
double dx(double time) => initialVelocity + (accelleration * time);
#override
bool isDone(double time) => initialVelocity > 0 ? dx(time) < 0.001 : dx(time) > -0.001;
#override
double x(double time) => (initialPosition + (initialVelocity * time) + (accelleration * time * time / 2)) % 1.0;
}
class _RotatingPieChartInternal extends StatefulWidget {
final double accellerationFactor;
final List<PieChartItem> items;
final PieChartItemToText toText;
const _RotatingPieChartInternal(
{Key key, this.accellerationFactor = 1.0, #required this.items, #required this.toText})
: super(key: key);
#override
_RotatingPieChartInternalState createState() => _RotatingPieChartInternalState();
}
class _RotatingPieChartInternalState extends State<_RotatingPieChartInternal> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
#override
void initState() {
_controller = AnimationController(vsync: this);
_animation = new Tween(begin: 0.0, end: 2.0 * pi).animate(_controller);
_controller.animateTo(2 * pi, duration: Duration(seconds: 10));
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
Offset lastDirection;
Offset getDirection(Offset globalPosition) {
RenderBox box = context.findRenderObject();
Offset offset = box.globalToLocal(globalPosition);
Offset center = Offset(context.size.width / 2.0, context.size.height / 2.0);
return offset - center;
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: (details) {
lastDirection = getDirection(details.globalPosition);
},
onPanUpdate: (details) {
Offset newDirection = getDirection(details.globalPosition);
double diff = newDirection.direction - lastDirection.direction;
var value = _controller.value + (diff / pi / 2);
_controller.value = value % 1.0;
lastDirection = newDirection;
},
onPanEnd: (details) {
// non-angular velocity
Offset velocity = details.velocity.pixelsPerSecond;
var top = (lastDirection.dx * velocity.dy) - (lastDirection.dy * velocity.dx);
var bottom = (lastDirection.dx * lastDirection.dx) + (lastDirection.dy * lastDirection.dy);
var angularVelocity = top / bottom;
var angularRotation = angularVelocity / pi / 2;
var decelleration = angularRotation * widget.accellerationFactor;
_controller.animateWith(
_RotationEndSimulation(
decelleration: decelleration,
initialPosition: _controller.value,
initialVelocity: angularRotation,
),
);
},
child: AnimatedBuilder(
animation: _animation,
builder: (context, widget) {
return Stack(
fit: StackFit.passthrough,
children: [
Transform.rotate(
angle: _animation.value,
child: widget,
),
CustomPaint(
painter:
_PieTextPainter(items: this.widget.items, rotation: _animation.value, toText: this.widget.toText),
)
],
);
},
child: CustomPaint(
painter: _PieChartPainter(
items: widget.items,
),
),
),
);
}
}
abstract class _AlignedCustomPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
// for convenience I'm doing all the drawing in a 100x100 square then moving it rather than worrying
// about the actual size.
// Also, using a 100x100 square for convenience so we can hardcode values.
FittedSizes fittedSizes = applyBoxFit(BoxFit.contain, Size(100.0, 100.0), size);
var dest = fittedSizes.destination;
canvas.translate((size.width - dest.width) / 2 + 1, (size.height - dest.height) / 2 + 1);
canvas.scale((dest.width - 2) / 100.0);
alignedPaint(canvas, Size(100.0, 100.0));
}
void alignedPaint(Canvas canvas, Size size);
}
class _PieChartPainter extends _AlignedCustomPainter {
final List<PieChartItem> items;
final double total;
final double rotation;
_PieChartPainter({this.rotation = 0.0, #required this.items})
: total = items.fold(0.0, (total, el) => total + el.val);
#override
void alignedPaint(Canvas canvas, Size size) {
Rect rect = Offset.zero & size;
double soFar = rotation;
Paint outlinePaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke;
for (int i = 0; i < items.length; ++i) {
PieChartItem item = items[i];
double arcRad = item.val / total * 2 * pi;
canvas.drawArc(rect, soFar, arcRad, true, Paint()..color = item.color);
canvas.drawArc(rect, soFar, arcRad, true, outlinePaint);
soFar += arcRad;
}
}
#override
bool shouldRepaint(_PieChartPainter oldDelegate) {
return oldDelegate.rotation != rotation || oldDelegate.items != items;
}
}
class _PieTextPainter extends _AlignedCustomPainter {
final List<PieChartItem> items;
final double total;
final double rotation;
final List<double> middles;
final PieChartItemToText toText;
static final double textDisplayCenter = 0.7;
_PieTextPainter._(this.items, this.total, this.rotation, this.middles, this.toText);
factory _PieTextPainter(
{double rotation = 0.0, #required List<PieChartItem> items, #required PieChartItemToText toText}) {
double total = items.fold(0.0, (prev, el) => prev + el.val);
var middles = (() {
double soFar = rotation;
return items.map((item) {
double arcRad = item.val / total * 2 * pi;
double middleRad = (soFar) + (arcRad / 2);
soFar += arcRad;
return middleRad;
}).toList(growable: false);
})();
return _PieTextPainter._(items, total, rotation, middles, toText);
}
#override
void alignedPaint(Canvas canvas, Size size) {
for (int i = 0; i < items.length; ++i) {
var middleRad = middles[i];
var item = items[i];
var rad = size.width / 2;
var middleX = rad + rad * textDisplayCenter * cos(middleRad);
var middleY = rad + rad * textDisplayCenter * sin(middleRad);
TextPainter textPainter = toText(item, total)..layout();
textPainter.paint(canvas, Offset(middleX - (textPainter.width / 2), middleY - (textPainter.height / 2)));
}
}
#override
bool shouldRepaint(_PieTextPainter oldDelegate) {
// note that just checking items != items might not be enough.
return oldDelegate.rotation != rotation || oldDelegate.items != items || oldDelegate.toText != toText;
}
}
I have created a stateless widget, and I want to define a baseline for it, so that I can use it with the Baseline widget (https://docs.flutter.io/flutter/widgets/Baseline-class.html).
How can I do that?
If you want to define a baseline you can't do it directly in the stateless widget. You need to mess around its corresponding RenderBox that needs to implement the computeDistanceToActualBaseline() method.
Give a look to the ListTile implementation here. You will see that the _RenderListTile RenderBox implements the above method returning the baseline of the title widget.
#override
double computeDistanceToActualBaseline(TextBaseline baseline) {
assert(title != null);
final BoxParentData parentData = title.parentData;
return parentData.offset.dy + title.getDistanceToActualBaseline(baseline);
}
In this case, the baseline of the title is the bottom of the Text widget.
All this is needed because the Baseline widget tries to get the baseline of the child widget. If you don't provide a explicit baseline with the above method, it just uses its bottom position.
You can find below an example of a BaselineBox where you can set an arbitrary baseline from top.
import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
class BaselineBox extends SingleChildRenderObjectWidget {
const BaselineBox({Key key, #required this.baseline, Widget child})
: assert(baseline != null),
super(key: key, child: child);
final double baseline;
#override
RenderBaselineBox createRenderObject(BuildContext context) =>
new RenderBaselineBox(baseline: baseline);
#override
void updateRenderObject(
BuildContext context, RenderBaselineBox renderObject) {
renderObject.baseline = baseline;
}
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DoubleProperty('baseline', baseline));
}
}
class RenderBaselineBox extends RenderProxyBox {
RenderBaselineBox({
RenderBox child,
#required double baseline,
}) : assert(baseline != null),
assert(baseline >= 0.0),
assert(baseline.isFinite),
_baseline = baseline,
super(child);
double get baseline => _baseline;
double _baseline;
set baseline(double value) {
assert(value != null);
assert(value >= 0.0);
assert(value.isFinite);
if (_baseline == value) return;
_baseline = value;
markNeedsLayout();
}
#override
double computeDistanceToActualBaseline(TextBaseline baselineType) {
return _baseline;
}
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DoubleProperty('baseline', baseline));
}
}
Evening guys,
Im looking into building a plugin for Flutter that detects if the device is shaking. Now i've found how to technically do it in Swift (Detect shake gesture IOS Swift) but im stuck on how to hook it up as a Flutter plugin, because i don't have direct access to the view controller lifecycle events.
Need a way to hook up
viewDidLoad
canBecomeFirstResponder
motionEnded
Can anyone nudge me in the right direction?
The Flutter Team has already published a plugin called sensors, which can be used to detect motion from the accelerometer (and gyroscope).
import 'package:sensors/sensors.dart';
accelerometerEvents.listen((AccelerometerEvent event) {
// "calculate" "shakes" here
});
The event contains x, y and z values. Combining this with time will make it possible to check for shakes.
I am just pointing this out because it is way less to go than creating a full plugin from scratch.
You could try this plugin: shake_event
It's pretty simple to work with and works both for iOS and Android.
I ran into the same issue, so I figured Reactive Programming and RxDart could help.
You can create a BLoC (Business logic component) called sensor_bloc.dart :
import 'dart:async';
import 'dart:math';
import 'package:rxdart/rxdart.dart';
import 'package:sensors/sensors.dart';
class SensorBloc {
StreamSubscription<dynamic> _accelerometerStream;
//INPUT
final _thresholdController = StreamController<int>();
Sink<int> get threshold => _thresholdController.sink;
// OUTPUT
final _shakeDetector = StreamController<bool>();
Stream<bool> get shakeEvent => _shakeDetector.stream.transform(ThrottleStreamTransformer(Duration(seconds: 2)));
SensorBloc() {
const CircularBufferSize = 10;
double detectionThreshold = 70.0;
List<double> circularBuffer = List.filled(CircularBufferSize,0.0);
int index = 0;
double minX=0.0, maxX=0.0;
_thresholdController.stream.listen((value){
// safety
if (value > 30) detectionThreshold = value*1.0;
});
_accelerometerStream = accelerometerEvents.listen((AccelerometerEvent event){
index = (index == CircularBufferSize -1 ) ? 0: index+1;
var oldX = circularBuffer[index];
if (oldX == maxX) {
maxX = circularBuffer.reduce(max);
}
if (oldX == minX) {
minX = circularBuffer.reduce(min);
}
circularBuffer[index] = event.x;
if (event.x < minX ) minX=event.x;
if (event.x> maxX) maxX = event.x;
if (maxX-minX>detectionThreshold)
{
_shakeDetector.add(true);
circularBuffer.fillRange(0, CircularBufferSize, 0.0);
minX=0.0;
maxX=0.0;
}
});
}
void dispose() {
_shakeDetector.close();
_accelerometerStream.cancel();
_thresholdController.close();
}
}
Then, just subscribe to its events in your widget :
Declare StreamSubscription<bool> shakeSubscriber ; in your state, and hook to lifecycle events
(NB: I use a InheritedWidget giving me access to the umbrella BLoC via the static function MainWidget.bloc(context)):
StreamSubscription<bool> shakeSubscriber ;
#override
Widget build(BuildContext context) {
if(shakeSubscriber == null ) {
MainWidget.bloc(context).sensorBloc.shakeEvent.listen((_){
print("SHAKE ! *************************");
});
}
return _buildMainScaffold();
}
#override
void dispose() {
if(shakeSubscriber != null ) shakeSubscriber.cancel();
super.dispose();
}