Skip to content

Commit b717ea9

Browse files
Changed PageListViewportController to let it be driven by a simulation, so that the controller has direct access to the same clock as the simulation, which seems necessary for us to get smooth velocity measurements (Resolves #34) (#42)
1 parent 851182f commit b717ea9

File tree

5 files changed

+388
-75
lines changed

5 files changed

+388
-75
lines changed

example/lib/main_list.dart

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:example/momentum_verification/velocity_plotter.dart';
12
import 'package:flutter/material.dart';
23
import 'package:logging/logging.dart';
34
import 'package:page_list_viewport/page_list_viewport.dart';
@@ -32,7 +33,7 @@ class MyHomePage extends StatefulWidget {
3233
State<MyHomePage> createState() => _MyHomePageState();
3334
}
3435

35-
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
36+
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
3637
static const _pageCount = 20;
3738
static const _naturalPageSizeInInches = Size(8.5, 11);
3839

@@ -69,32 +70,49 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
6970
}
7071

7172
Widget _buildViewport() {
72-
return PageListViewportGestures(
73-
controller: _controller,
74-
lockPanAxis: true,
75-
child: PageListViewport(
76-
controller: _controller,
77-
pageCount: _pageCount,
78-
naturalPageSize: _naturalPageSizeInInches * 72 * MediaQuery.of(context).devicePixelRatio,
79-
pageLayoutCacheCount: 3,
80-
pagePaintCacheCount: 3,
81-
builder: (BuildContext context, int pageIndex) {
82-
return Stack(
83-
children: [
84-
Positioned.fill(
85-
child: _buildPage(pageIndex),
86-
),
87-
Center(
88-
child: Container(
89-
padding: const EdgeInsets.all(32),
90-
color: Colors.white,
91-
child: Text("Page: $pageIndex"),
92-
),
93-
)
94-
],
95-
);
96-
},
97-
),
73+
return Stack(
74+
children: [
75+
PageListViewportGestures(
76+
controller: _controller,
77+
lockPanAxis: true,
78+
child: PageListViewport(
79+
controller: _controller,
80+
pageCount: _pageCount,
81+
naturalPageSize: _naturalPageSizeInInches * 72 * MediaQuery.of(context).devicePixelRatio,
82+
pageLayoutCacheCount: 3,
83+
pagePaintCacheCount: 3,
84+
builder: (BuildContext context, int pageIndex) {
85+
return Stack(
86+
children: [
87+
Positioned.fill(
88+
child: _buildPage(pageIndex),
89+
),
90+
Center(
91+
child: Container(
92+
padding: const EdgeInsets.all(32),
93+
color: Colors.white,
94+
child: Text("Page: $pageIndex"),
95+
),
96+
)
97+
],
98+
);
99+
},
100+
),
101+
),
102+
Positioned(
103+
left: 0,
104+
right: 0,
105+
bottom: 0,
106+
height: 300,
107+
child: ColoredBox(
108+
color: Colors.black.withOpacity(0.5),
109+
child: VelocityPlotter(
110+
controller: _controller,
111+
max: const Offset(6000, 6000),
112+
),
113+
),
114+
)
115+
],
98116
);
99117
}
100118

example/lib/main_single_page_orientation.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class MyHomePage extends StatefulWidget {
3131
State<MyHomePage> createState() => _MyHomePageState();
3232
}
3333

34-
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
34+
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
3535
late final PageListViewportController _controller;
3636
final _layerLink = LayerLink();
3737

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:page_list_viewport/page_list_viewport.dart';
3+
4+
class VelocityPlotter extends StatefulWidget {
5+
const VelocityPlotter({
6+
super.key,
7+
required this.controller,
8+
required this.max,
9+
});
10+
11+
final PageListViewportController controller;
12+
final Offset max;
13+
14+
@override
15+
State<VelocityPlotter> createState() => _VelocityPlotterState();
16+
}
17+
18+
class _VelocityPlotterState extends State<VelocityPlotter> {
19+
final _velocityLogicalPoint = <Offset>[];
20+
final _velocityVisiblePoints = <Offset>[];
21+
final _accelerationLogicalPoints = <Offset>[];
22+
final _accelerationVisiblePoints = <Offset>[];
23+
final _sampleCount = ValueNotifier(0);
24+
25+
@override
26+
void initState() {
27+
super.initState();
28+
widget.controller.addListener(_onControllerChanged);
29+
}
30+
31+
@override
32+
void didUpdateWidget(covariant VelocityPlotter oldWidget) {
33+
super.didUpdateWidget(oldWidget);
34+
if (oldWidget.controller != widget.controller) {
35+
oldWidget.controller.removeListener(_onControllerChanged);
36+
widget.controller.addListener(_onControllerChanged);
37+
}
38+
}
39+
40+
@override
41+
void dispose() {
42+
super.dispose();
43+
widget.controller.removeListener(_onControllerChanged);
44+
}
45+
46+
Offset? _lastVelocity;
47+
final _stopwatch = Stopwatch();
48+
49+
void _onControllerChanged() {
50+
if (_stopwatch.isRunning == false) {
51+
_stopwatch.start();
52+
}
53+
if (_stopwatch.elapsedMilliseconds == 0) {
54+
// No time has passed. We don't want to divide things by zero.
55+
return;
56+
}
57+
final velocity = widget.controller.velocity;
58+
_velocityLogicalPoint.add(velocity);
59+
if (_lastVelocity != null) {
60+
// final acceleration = (velocity - _lastVelocity!) / (_stopwatch.elapsedMilliseconds / 1000);
61+
final acceleration = widget.controller.acceleration * 100;
62+
_accelerationLogicalPoints.add(acceleration);
63+
}
64+
65+
// Increment sample count so that we cause a repaint in the CustomPainter
66+
_sampleCount.value = _velocityLogicalPoint.length;
67+
68+
_lastVelocity = velocity;
69+
_stopwatch.reset();
70+
}
71+
72+
void _clearPlot() {
73+
_velocityLogicalPoint.clear();
74+
_velocityVisiblePoints.clear();
75+
_accelerationLogicalPoints.clear();
76+
_accelerationVisiblePoints.clear();
77+
_sampleCount.value = 0;
78+
}
79+
80+
@override
81+
Widget build(BuildContext context) {
82+
return RepaintBoundary(
83+
child: GestureDetector(
84+
onDoubleTap: _clearPlot,
85+
child: Stack(
86+
children: [
87+
Positioned.fill(
88+
child: CustomPaint(
89+
painter: _PlotterPainter(
90+
logicalPoints: _accelerationLogicalPoints,
91+
visiblePoints: _accelerationVisiblePoints,
92+
max: widget.max,
93+
color: Colors.red.withOpacity(0.5),
94+
repaint: _sampleCount,
95+
),
96+
),
97+
),
98+
Positioned.fill(
99+
child: CustomPaint(
100+
painter: _PlotterPainter(
101+
logicalPoints: _velocityLogicalPoint,
102+
visiblePoints: _velocityVisiblePoints,
103+
max: widget.max,
104+
color: Colors.greenAccent,
105+
repaint: _sampleCount,
106+
),
107+
),
108+
),
109+
],
110+
),
111+
),
112+
);
113+
}
114+
}
115+
116+
class _PlotterPainter extends CustomPainter {
117+
static const _maxSampleDisplayCount = 300;
118+
119+
_PlotterPainter({
120+
required this.max,
121+
required this.logicalPoints,
122+
required this.visiblePoints,
123+
required this.color,
124+
super.repaint,
125+
}) {
126+
pointPainter.color = color;
127+
linePaint
128+
..color = color
129+
..strokeWidth = 1
130+
..style = PaintingStyle.stroke;
131+
}
132+
133+
final Offset max;
134+
final List<Offset> logicalPoints;
135+
final List<Offset> visiblePoints;
136+
final Color color;
137+
138+
final pointPainter = Paint();
139+
final linePaint = Paint();
140+
141+
@override
142+
void paint(Canvas canvas, Size size) {
143+
_convertLogicalPointsToPlotPoints(size);
144+
145+
canvas.clipRect(Offset.zero & size);
146+
147+
final horizontalStepSize = size.width / _maxSampleDisplayCount;
148+
for (var i = 0; i < visiblePoints.length; i += 1) {
149+
final plotPoint = visiblePoints[i].translate(i * horizontalStepSize, 0);
150+
final previousPlotPoint = i > 0 ? visiblePoints[i - 1].translate((i - 1) * horizontalStepSize, 0) : null;
151+
152+
// Draw a line connecting previous and current plot point.
153+
if (previousPlotPoint != null) {
154+
canvas.drawLine(previousPlotPoint, plotPoint, linePaint);
155+
}
156+
157+
// Draw the current plot point.
158+
canvas.drawCircle(plotPoint, 2, pointPainter);
159+
}
160+
}
161+
162+
void _convertLogicalPointsToPlotPoints(Size size) {
163+
final scaleY = size.height / (max.dy * 2);
164+
165+
for (var i = 0; i < logicalPoints.length; i += 1) {
166+
final logicalPoint = logicalPoints[i];
167+
168+
final plotPoint = Offset(
169+
0,
170+
size.height - ((logicalPoint.dy + max.dy) * scaleY),
171+
);
172+
173+
visiblePoints.add(plotPoint);
174+
}
175+
176+
if (visiblePoints.length > _maxSampleDisplayCount) {
177+
visiblePoints.removeRange(0, visiblePoints.length - _maxSampleDisplayCount);
178+
}
179+
180+
logicalPoints.clear();
181+
}
182+
183+
@override
184+
bool shouldRepaint(covariant CustomPainter oldDelegate) {
185+
return false;
186+
}
187+
}

0 commit comments

Comments
 (0)