Skip to content

Commit 9c95a78

Browse files
Scroll flutter frames chart to selected frame on first load (#4930)
1 parent aa47d0b commit 9c95a78

File tree

2 files changed

+256
-116
lines changed

2 files changed

+256
-116
lines changed

packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart

Lines changed: 186 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -73,49 +73,15 @@ class _FlutterFramesChart extends StatefulWidget {
7373

7474
final bool isVisible;
7575

76-
@override
77-
_FlutterFramesChartState createState() => _FlutterFramesChartState();
78-
}
79-
80-
class _FlutterFramesChartState extends State<_FlutterFramesChart>
81-
with AutoDisposeMixin {
82-
static const _defaultFrameWidthWithPadding =
83-
FlutterFramesChartItem.defaultFrameWidth + densePadding * 2;
84-
85-
static const _outlineBorderWidth = 1.0;
86-
87-
double get _yAxisUnitsSpace => scaleByFontFactor(48.0);
76+
static double get frameNumberSectionHeight => scaleByFontFactor(20.0);
8877

89-
static double get _frameNumberSectionHeight => scaleByFontFactor(20.0);
90-
91-
double get _frameChartScrollbarOffset => defaultScrollBarOffset;
92-
93-
late final ScrollController _framesScrollController;
94-
95-
FlutterFrame? _selectedFrame;
96-
97-
/// Milliseconds per pixel value for the y-axis.
98-
///
99-
/// This value will result in a y-axis time range spanning two times the
100-
/// target frame time for a single frame (e.g. 16.6 * 2 for a 60 FPS device).
101-
double get _msPerPx =>
102-
// Multiply by two to reach two times the target frame time.
103-
1 / widget.displayRefreshRate * 1000 * 2 / defaultChartHeight;
78+
static double get frameChartScrollbarOffset => defaultScrollBarOffset;
10479

10580
@override
106-
void initState() {
107-
super.initState();
108-
_framesScrollController = ScrollController();
109-
110-
cancelListeners();
111-
_selectedFrame = widget.framesController.selectedFrame.value;
112-
addAutoDisposeListener(widget.framesController.selectedFrame, () {
113-
setState(() {
114-
_selectedFrame = widget.framesController.selectedFrame.value;
115-
});
116-
});
117-
}
81+
_FlutterFramesChartState createState() => _FlutterFramesChartState();
82+
}
11883

84+
class _FlutterFramesChartState extends State<_FlutterFramesChart> {
11985
@override
12086
void didChangeDependencies() {
12187
super.didChangeDependencies();
@@ -125,11 +91,6 @@ class _FlutterFramesChartState extends State<_FlutterFramesChart>
12591
@override
12692
void didUpdateWidget(_FlutterFramesChart oldWidget) {
12793
super.didUpdateWidget(oldWidget);
128-
if (_framesScrollController.hasClients &&
129-
_framesScrollController.atScrollBottom) {
130-
unawaited(_framesScrollController.autoScrollToBottom());
131-
}
132-
13394
if (!collectionEquals(oldWidget.frames, widget.frames)) {
13495
_maybeShowShaderJankMessage();
13596
}
@@ -156,12 +117,6 @@ class _FlutterFramesChartState extends State<_FlutterFramesChart>
156117
}
157118
}
158119

159-
@override
160-
void dispose() {
161-
_framesScrollController.dispose();
162-
super.dispose();
163-
}
164-
165120
@override
166121
Widget build(BuildContext context) {
167122
// TODO(https://github.com/flutter/devtools/issues/4576): animate showing
@@ -175,14 +130,27 @@ class _FlutterFramesChartState extends State<_FlutterFramesChart>
175130
bottom: denseSpacing,
176131
),
177132
height: defaultChartHeight +
178-
_frameNumberSectionHeight +
179-
_frameChartScrollbarOffset,
133+
_FlutterFramesChart.frameNumberSectionHeight +
134+
_FlutterFramesChart.frameChartScrollbarOffset,
180135
child: Row(
181136
children: [
182-
Expanded(child: _buildChart()),
137+
Expanded(
138+
child: LayoutBuilder(
139+
builder: (context, constraints) {
140+
return FramesChart(
141+
framesController: widget.framesController,
142+
frames: widget.frames,
143+
displayRefreshRate: widget.displayRefreshRate,
144+
constraints: constraints,
145+
);
146+
},
147+
),
148+
),
183149
const SizedBox(width: defaultSpacing),
184150
Padding(
185-
padding: EdgeInsets.only(bottom: _frameChartScrollbarOffset),
151+
padding: EdgeInsets.only(
152+
bottom: _FlutterFramesChart.frameChartScrollbarOffset,
153+
),
186154
child: FramesChartControls(
187155
framesController: widget.framesController,
188156
frames: widget.frames,
@@ -193,69 +161,167 @@ class _FlutterFramesChartState extends State<_FlutterFramesChart>
193161
),
194162
);
195163
}
164+
}
196165

197-
Widget _buildChart() {
198-
return LayoutBuilder(
199-
builder: (context, constraints) {
200-
final themeData = Theme.of(context);
201-
final chart = Scrollbar(
202-
thumbVisibility: true,
203-
controller: _framesScrollController,
204-
child: Padding(
205-
padding: EdgeInsets.only(bottom: _frameChartScrollbarOffset),
206-
child: RoundedOutlinedBorder(
207-
child: ListView.builder(
208-
controller: _framesScrollController,
209-
scrollDirection: Axis.horizontal,
210-
itemCount: widget.frames.length,
211-
itemExtent: _defaultFrameWidthWithPadding,
212-
itemBuilder: (context, index) => FlutterFramesChartItem(
213-
framesController: widget.framesController,
214-
index: index,
215-
frame: widget.frames[index],
216-
selected: widget.frames[index] == _selectedFrame,
217-
msPerPx: _msPerPx,
218-
availableChartHeight:
219-
defaultChartHeight - 2 * _outlineBorderWidth,
220-
displayRefreshRate: widget.displayRefreshRate,
221-
),
222-
),
166+
@visibleForTesting
167+
class FramesChart extends StatefulWidget {
168+
const FramesChart({
169+
required this.framesController,
170+
required this.frames,
171+
required this.displayRefreshRate,
172+
required this.constraints,
173+
});
174+
175+
final FlutterFramesController framesController;
176+
177+
final List<FlutterFrame> frames;
178+
179+
final double displayRefreshRate;
180+
181+
final BoxConstraints constraints;
182+
183+
@override
184+
State<FramesChart> createState() => _FramesChartState();
185+
}
186+
187+
class _FramesChartState extends State<FramesChart> with AutoDisposeMixin {
188+
static const _defaultFrameWidthWithPadding =
189+
FlutterFramesChartItem.defaultFrameWidth + densePadding * 2;
190+
191+
static const _outlineBorderWidth = 1.0;
192+
193+
double get _yAxisUnitsSpace => scaleByFontFactor(48.0);
194+
195+
late final ScrollController _framesScrollController;
196+
197+
FlutterFrame? _selectedFrame;
198+
199+
int? _selectedFrameIndex;
200+
201+
/// Milliseconds per pixel value for the y-axis.
202+
///
203+
/// This value will result in a y-axis time range spanning two times the
204+
/// target frame time for a single frame (e.g. 16.6 * 2 for a 60 FPS device).
205+
double get _msPerPx =>
206+
// Multiply by two to reach two times the target frame time.
207+
1 / widget.displayRefreshRate * 1000 * 2 / defaultChartHeight;
208+
209+
@override
210+
void initState() {
211+
super.initState();
212+
213+
cancelListeners();
214+
_selectedFrame = widget.framesController.selectedFrame.value;
215+
if (_selectedFrame != null) {
216+
_selectedFrameIndex = widget.frames.indexOf(_selectedFrame!);
217+
}
218+
addAutoDisposeListener(widget.framesController.selectedFrame, () {
219+
setState(() {
220+
_selectedFrame = widget.framesController.selectedFrame.value;
221+
});
222+
});
223+
224+
final initialScrollOffset = _calculateInitialHorizontalScrollOffset();
225+
_framesScrollController = ScrollController(
226+
initialScrollOffset: initialScrollOffset,
227+
);
228+
}
229+
230+
@override
231+
void didUpdateWidget(FramesChart oldWidget) {
232+
super.didUpdateWidget(oldWidget);
233+
if (_framesScrollController.hasClients &&
234+
_framesScrollController.atScrollBottom) {
235+
unawaited(_framesScrollController.autoScrollToBottom());
236+
}
237+
}
238+
239+
double _calculateInitialHorizontalScrollOffset() {
240+
final selectedIndex = _selectedFrameIndex;
241+
if (selectedIndex == null) return 0.0;
242+
243+
final chartWidthWithoutAxisLabels =
244+
widget.constraints.maxWidth - _yAxisUnitsSpace;
245+
final totalFramesInView =
246+
chartWidthWithoutAxisLabels ~/ _defaultFrameWidthWithPadding;
247+
final fullFrameRangeInView = Range(0, totalFramesInView);
248+
249+
if (fullFrameRangeInView.contains(selectedIndex)) return 0.0;
250+
251+
return math.max(
252+
0.0,
253+
(selectedIndex - totalFramesInView / 2) * _defaultFrameWidthWithPadding,
254+
);
255+
}
256+
257+
@override
258+
void dispose() {
259+
_framesScrollController.dispose();
260+
super.dispose();
261+
}
262+
263+
@override
264+
Widget build(BuildContext context) {
265+
final themeData = Theme.of(context);
266+
final chart = Scrollbar(
267+
thumbVisibility: true,
268+
controller: _framesScrollController,
269+
child: Padding(
270+
padding: EdgeInsets.only(
271+
bottom: _FlutterFramesChart.frameChartScrollbarOffset,
272+
),
273+
child: RoundedOutlinedBorder(
274+
child: ListView.builder(
275+
controller: _framesScrollController,
276+
scrollDirection: Axis.horizontal,
277+
itemCount: widget.frames.length,
278+
itemExtent: _defaultFrameWidthWithPadding,
279+
itemBuilder: (context, index) => FlutterFramesChartItem(
280+
framesController: widget.framesController,
281+
index: index,
282+
frame: widget.frames[index],
283+
selected: widget.frames[index] == _selectedFrame,
284+
msPerPx: _msPerPx,
285+
availableChartHeight:
286+
defaultChartHeight - 2 * _outlineBorderWidth,
287+
displayRefreshRate: widget.displayRefreshRate,
288+
onSelected: (index) => _selectedFrameIndex = index,
223289
),
224290
),
225-
);
226-
final chartAxisPainter = CustomPaint(
227-
painter: ChartAxisPainter(
228-
constraints: constraints,
229-
yAxisUnitsSpace: _yAxisUnitsSpace,
230-
displayRefreshRate: widget.displayRefreshRate,
231-
msPerPx: _msPerPx,
232-
themeData: themeData,
233-
bottomMargin:
234-
_frameChartScrollbarOffset + _frameNumberSectionHeight,
235-
),
236-
);
237-
final fpsLinePainter = CustomPaint(
238-
painter: FPSLinePainter(
239-
constraints: constraints,
240-
yAxisUnitsSpace: _yAxisUnitsSpace,
241-
displayRefreshRate: widget.displayRefreshRate,
242-
msPerPx: _msPerPx,
243-
themeData: themeData,
244-
bottomMargin:
245-
_frameChartScrollbarOffset + _frameNumberSectionHeight,
246-
),
247-
);
248-
return Stack(
249-
children: [
250-
chartAxisPainter,
251-
Padding(
252-
padding: EdgeInsets.only(left: _yAxisUnitsSpace),
253-
child: chart,
254-
),
255-
fpsLinePainter,
256-
],
257-
);
258-
},
291+
),
292+
),
293+
);
294+
final chartAxisPainter = CustomPaint(
295+
painter: ChartAxisPainter(
296+
constraints: widget.constraints,
297+
yAxisUnitsSpace: _yAxisUnitsSpace,
298+
displayRefreshRate: widget.displayRefreshRate,
299+
msPerPx: _msPerPx,
300+
themeData: themeData,
301+
bottomMargin: _FlutterFramesChart.frameChartScrollbarOffset +
302+
_FlutterFramesChart.frameNumberSectionHeight,
303+
),
304+
);
305+
final fpsLinePainter = CustomPaint(
306+
painter: FPSLinePainter(
307+
constraints: widget.constraints,
308+
yAxisUnitsSpace: _yAxisUnitsSpace,
309+
displayRefreshRate: widget.displayRefreshRate,
310+
msPerPx: _msPerPx,
311+
themeData: themeData,
312+
bottomMargin: _FlutterFramesChart.frameChartScrollbarOffset +
313+
_FlutterFramesChart.frameNumberSectionHeight,
314+
),
315+
);
316+
return Stack(
317+
children: [
318+
chartAxisPainter,
319+
Padding(
320+
padding: EdgeInsets.only(left: _yAxisUnitsSpace),
321+
child: chart,
322+
),
323+
fpsLinePainter,
324+
],
259325
);
260326
}
261327
}
@@ -336,6 +402,7 @@ class FlutterFramesChartItem extends StatelessWidget {
336402
required this.msPerPx,
337403
required this.availableChartHeight,
338404
required this.displayRefreshRate,
405+
this.onSelected,
339406
});
340407

341408
static const defaultFrameWidth = 28.0;
@@ -359,6 +426,8 @@ class FlutterFramesChartItem extends StatelessWidget {
359426

360427
final double displayRefreshRate;
361428

429+
final void Function(int)? onSelected;
430+
362431
@override
363432
Widget build(BuildContext context) {
364433
final themeData = Theme.of(context);
@@ -419,7 +488,7 @@ class FlutterFramesChartItem extends StatelessWidget {
419488

420489
final content = Padding(
421490
padding: EdgeInsets.only(
422-
bottom: _FlutterFramesChartState._frameNumberSectionHeight,
491+
bottom: _FlutterFramesChart.frameNumberSectionHeight,
423492
),
424493
child: InkWell(
425494
onTap: _selectFrame,
@@ -471,7 +540,7 @@ class FlutterFramesChartItem extends StatelessWidget {
471540
content,
472541
Container(
473542
margin: EdgeInsets.only(top: defaultChartHeight),
474-
height: _FlutterFramesChartState._frameNumberSectionHeight,
543+
height: _FlutterFramesChart.frameNumberSectionHeight,
475544
alignment: AlignmentDirectional.center,
476545
child: Text(
477546
'${frame.id}',
@@ -500,6 +569,7 @@ class FlutterFramesChartItem extends StatelessWidget {
500569
);
501570
}
502571
framesController.handleSelectedFrame(frame);
572+
onSelected?.call(index);
503573
}
504574
}
505575

0 commit comments

Comments
 (0)