@@ -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