Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
fb94d3fb
Unverified
Commit
fb94d3fb
authored
4 years ago
by
Tong Mu
Committed by
GitHub
4 years ago
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Animation sheet recorder (#55527)
parent
533cd7a6
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
606 additions
and
0 deletions
+606
-0
animation_sheet_test.dart
packages/flutter/test/animation/animation_sheet_test.dart
+148
-0
flutter_test.dart
packages/flutter_test/lib/flutter_test.dart
+1
-0
animation_sheet.dart
packages/flutter_test/lib/src/animation_sheet.dart
+350
-0
widget_tester.dart
packages/flutter_test/lib/src/widget_tester.dart
+23
-0
widget_tester_test.dart
packages/flutter_test/test/widget_tester_test.dart
+84
-0
No files found.
packages/flutter/test/animation/animation_sheet_test.dart
0 → 100644
View file @
fb94d3fb
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:ui'
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
/*
* Here lies tests for packages/flutter_test/lib/src/animation_sheet.dart
* because [matchesGoldenFile] does not use Skia Gold in its native package.
*/
testWidgets
(
'correctly records frames'
,
(
WidgetTester
tester
)
async
{
final
AnimationSheetBuilder
builder
=
AnimationSheetBuilder
(
frameSize:
_DecuplePixels
.
size
);
await
tester
.
pumpFrames
(
builder
.
record
(
const
_DecuplePixels
(
Duration
(
seconds:
1
)),
),
const
Duration
(
milliseconds:
200
),
const
Duration
(
milliseconds:
100
),
);
await
tester
.
pumpFrames
(
builder
.
record
(
const
_DecuplePixels
(
Duration
(
seconds:
1
)),
recording:
false
,
),
const
Duration
(
milliseconds:
200
),
const
Duration
(
milliseconds:
100
),
);
await
tester
.
pumpFrames
(
builder
.
record
(
const
_DecuplePixels
(
Duration
(
seconds:
1
)),
recording:
true
,
),
const
Duration
(
milliseconds:
400
),
const
Duration
(
milliseconds:
100
),
);
final
Widget
display
=
await
builder
.
display
();
await
tester
.
binding
.
setSurfaceSize
(
builder
.
sheetSize
());
await
tester
.
pumpWidget
(
display
);
await
expectLater
(
find
.
byWidget
(
display
),
matchesGoldenFile
(
'test.animation_sheet_builder.records.png'
));
},
skip:
isBrowser
);
testWidgets
(
'correctly wraps a row'
,
(
WidgetTester
tester
)
async
{
final
AnimationSheetBuilder
builder
=
AnimationSheetBuilder
(
frameSize:
_DecuplePixels
.
size
);
const
Duration
duration
=
Duration
(
seconds:
2
);
await
tester
.
pumpFrames
(
builder
.
record
(
const
_DecuplePixels
(
duration
)),
duration
,
const
Duration
(
milliseconds:
200
),
);
final
Widget
display
=
await
builder
.
display
();
await
tester
.
binding
.
setSurfaceSize
(
builder
.
sheetSize
(
maxWidth:
80
));
await
tester
.
pumpWidget
(
display
);
await
expectLater
(
find
.
byWidget
(
display
),
matchesGoldenFile
(
'test.animation_sheet_builder.wraps.png'
));
},
skip:
isBrowser
);
}
// An animation of a yellow pixel moving from left to right, in a container of
// (10, 1) with a 1-pixel-wide black border.
class
_DecuplePixels
extends
StatefulWidget
{
const
_DecuplePixels
(
this
.
duration
);
static
const
Size
size
=
Size
(
12
,
3
);
final
Duration
duration
;
@override
State
<
StatefulWidget
>
createState
()
=>
_DecuplePixelsState
();
}
class
_DecuplePixelsState
extends
State
<
_DecuplePixels
>
with
SingleTickerProviderStateMixin
{
AnimationController
_controller
;
@override
void
initState
()
{
super
.
initState
();
_controller
=
AnimationController
(
duration:
widget
.
duration
,
vsync:
this
,
);
_controller
.
repeat
();
}
@override
void
dispose
()
{
_controller
.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
AnimatedBuilder
(
animation:
_controller
.
view
,
builder:
(
BuildContext
context
,
Widget
child
)
{
return
CustomPaint
(
painter:
_PaintDecuplePixels
(
_controller
.
value
),
);
},
);
}
}
class
_PaintDecuplePixels
extends
CustomPainter
{
_PaintDecuplePixels
(
this
.
value
);
final
double
value
;
@override
bool
shouldRepaint
(
_PaintDecuplePixels
oldDelegate
)
{
return
oldDelegate
.
value
!=
value
;
}
@override
void
paint
(
Canvas
canvas
,
Size
size
)
{
canvas
.
save
();
final
Rect
rect
=
RectTween
(
begin:
const
Rect
.
fromLTWH
(
1
,
1
,
1
,
1
),
end:
const
Rect
.
fromLTWH
(
11
,
1
,
1
,
1
),
).
transform
(
value
);
canvas
.
drawRect
(
rect
,
Paint
()..
color
=
Colors
.
yellow
);
final
Paint
black
=
Paint
()..
color
=
Colors
.
black
;
canvas
// Top border
..
drawRect
(
const
Rect
.
fromLTRB
(
0
,
0
,
12
,
1
),
black
)
// Bottom border
..
drawRect
(
const
Rect
.
fromLTRB
(
0
,
2
,
12
,
3
),
black
)
// Left border
..
drawRect
(
const
Rect
.
fromLTRB
(
0
,
0
,
1
,
3
),
black
)
// Right border
..
drawRect
(
const
Rect
.
fromLTRB
(
11
,
0
,
12
,
3
),
black
);
canvas
.
restore
();
}
}
This diff is collapsed.
Click to expand it.
packages/flutter_test/lib/flutter_test.dart
View file @
fb94d3fb
...
...
@@ -49,6 +49,7 @@ export 'dart:async' show Future;
export
'src/_goldens_io.dart'
if
(
dart
.
library
.
html
)
'src/_goldens_web.dart'
;
export
'src/accessibility.dart'
;
export
'src/all_elements.dart'
;
export
'src/animation_sheet.dart'
;
export
'src/binding.dart'
;
export
'src/controller.dart'
;
export
'src/event_simulation.dart'
;
...
...
This diff is collapsed.
Click to expand it.
packages/flutter_test/lib/src/animation_sheet.dart
0 → 100644
View file @
fb94d3fb
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:math'
as
math
;
import
'dart:ui'
as
ui
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
/// Records the frames of an animating widget, and later displays the frames as a
/// grid in an animation sheet.
///
/// This class does not support Web, because the animation sheet utilizes taking
/// screenshots, which is unsupported on the Web. Tests that use this class must
/// be noted with `skip: isBrowser`.
/// (https://github.com/flutter/flutter/issues/56001)
///
/// Using this class includes the following steps:
///
/// * Create an instance of this class.
/// * Pump frames that render the target widget wrapped in [record]. Every frame
/// that has `recording` being true will be recorded.
/// * Adjust the size of the test viewport to the [sheetSize] (see the
/// documentation of [sheetSize] for more information).
/// * Pump a frame that renders [display], which shows all recorded frames in an
/// animation sheet, and can be matched against the golden test.
///
/// {@tool snippet}
/// The following example shows how to record an animation sheet of an [Inkwell]
/// being pressed then released.
///
/// ```dart
/// testWidgets('Inkwell animation sheet', (WidgetTester tester) async {
/// // Create instance
/// final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(48, 24));
///
/// final Widget target = Material(
/// child: Directionality(
/// textDirection: TextDirection.ltr,
/// child: InkWell(
/// splashColor: Colors.blue,
/// onTap: () {},
/// ),
/// ),
/// );
///
/// // Optional: setup before recording (`recording` is false)
/// await tester.pumpWidget(animationSheet.record(
/// target,
/// recording: false,
/// ));
///
/// final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(InkWell)));
///
/// // Start recording (`recording` is true)
/// await tester.pumpFrames(animationSheet.record(
/// target,
/// recording: true,
/// ), const Duration(seconds: 1));
///
/// await gesture.up();
///
/// await tester.pumpFrames(animationSheet.record(
/// target,
/// recording: true,
/// ), const Duration(seconds: 1));
///
/// // Adjust view port size
/// tester.binding.setSurfaceSize(animationSheet.sheetSize());
///
/// // Display
/// final Widget display = await animationSheet.display();
/// await tester.pumpWidget(display);
///
/// // Compare against golden file
/// await expectLater(
/// find.byWidget(display),
/// matchesGoldenFile('inkwell.press.animation.png'),
/// );
/// }, skip: isBrowser); // Animation sheet does not support browser https://github.com/flutter/flutter/issues/56001
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [GoldenFileComparator], which introduces Golden File Testing.
class
AnimationSheetBuilder
{
/// Starts a session of building an animation sheet.
///
/// The [frameSize] is a tight constraint for the child to be recorded, and must not
/// be null.
AnimationSheetBuilder
({
@required
this
.
frameSize
})
:
assert
(
frameSize
!=
null
);
/// The size of the child to be recorded.
///
/// This size is applied as a tight layout constraint for the child, and is
/// fixed throughout the building session.
final
Size
frameSize
;
final
List
<
Future
<
ui
.
Image
>>
_recordedFrames
=
<
Future
<
ui
.
Image
>>[];
Future
<
List
<
ui
.
Image
>>
get
_frames
async
{
final
List
<
ui
.
Image
>
frames
=
await
Future
.
wait
<
ui
.
Image
>(
_recordedFrames
,
eagerError:
true
);
assert
(()
{
for
(
final
ui
.
Image
frame
in
frames
)
{
assert
(
frame
.
width
==
frameSize
.
width
&&
frame
.
height
==
frameSize
.
height
,
'Unexpected size mismatch: frame has (
${frame.width}
,
${frame.height}
) '
'while `frameSize` is
$frameSize
.'
);
}
return
true
;
}());
return
frames
;
}
/// Returns a widget that renders a widget in a box that can be recorded.
///
/// The returned widget wraps `child` in a box with a fixed size specified by
/// [frameSize]. The `key` is also applied to the returned widget.
///
/// The `recording` defaults to true, which means the painted result of each
/// frame will be stored and later available for [display]. If `recording` is
/// false, then frames are not recorded. This is useful during the setup phase
/// that shouldn't be recorded; if the target widget isn't wrapped in [record]
/// during the setup phase, the states will be lost when it starts recording.
///
/// The `child` must not be null.
///
/// See also:
///
/// * [WidgetTester.pumpFrames], which renders a widget in a series of frames
/// with a fixed time interval.
Widget
record
(
Widget
child
,
{
Key
key
,
bool
recording
=
true
,
})
{
assert
(
child
!=
null
);
return
_AnimationSheetRecorder
(
key:
key
,
child:
child
,
size:
frameSize
,
handleRecorded:
recording
?
_recordedFrames
.
add
:
null
,
);
}
/// Constructs a widget that renders the recorded frames in an animation sheet.
///
/// The resulting widget takes as much space as its parent allows, which is
/// usually the screen size. It is then filled with all recorded frames, each
/// having a size specified by [frameSize], chronologically from top-left to
/// bottom-right in a row-major order.
///
/// This widget does not check whether its size fits all recorded frames.
/// Having too many frames can cause overflow errors, while having too few can
/// waste the size of golden files. Therefore you should usually adjust the
/// viewport size to [sheetSize] before calling this method.
///
/// The `key` is applied to the root widget.
///
/// This method can only be called if at least one frame has been recorded.
Future
<
Widget
>
display
({
Key
key
})
async
{
assert
(
_recordedFrames
.
isNotEmpty
);
final
List
<
ui
.
Image
>
frames
=
await
_frames
;
return
_CellSheet
(
key:
key
,
cellSize:
frameSize
,
children:
frames
.
map
((
ui
.
Image
image
)
=>
RawImage
(
image:
image
,
width:
frameSize
.
width
,
height:
frameSize
.
height
,
)).
toList
(),
);
}
/// Returns the smallest size that can contain all recorded frames.
///
/// This is used to adjust the viewport during unit tests, i.e. the size of
/// virtual screen. Having too many frames recorded than the default viewport
/// size can contain will lead to overflow errors, while having too few frames
/// means the golden file might be larger than necessary.
///
/// The [sheetSize] returns the smallest possible size by placing the
/// recorded frames, each of which has a size specified by [frameSize], in a
/// row-major grid with a maximum width specified by `maxWidth`, and returns
/// the size of that grid.
///
/// Setting the viewport size during a widget test usually involves
/// [TestWidgetsFlutterBinding.setSurfaceSize] and [WidgetTester.binding].
///
/// The `maxWidth` defaults to the width of the default viewport, 800.0.
///
/// This method can only be called if at least one frame has been recorded.
Size
sheetSize
({
double
maxWidth
=
_kDefaultTestViewportWidth
})
{
assert
(
_recordedFrames
.
isNotEmpty
);
final
int
cellsPerRow
=
(
maxWidth
/
frameSize
.
width
).
floor
();
final
int
rowNum
=
(
_recordedFrames
.
length
/
cellsPerRow
).
ceil
();
final
double
width
=
math
.
min
(
cellsPerRow
,
_recordedFrames
.
length
)
*
frameSize
.
width
;
return
Size
(
width
,
frameSize
.
height
*
rowNum
);
}
// The width of _kDefaultTestViewportSize in [TestViewConfiguration].
static
const
double
_kDefaultTestViewportWidth
=
800.0
;
}
typedef
_RecordedHandler
=
void
Function
(
Future
<
ui
.
Image
>
image
);
class
_AnimationSheetRecorder
extends
StatefulWidget
{
const
_AnimationSheetRecorder
({
this
.
handleRecorded
,
this
.
child
,
this
.
size
,
Key
key
,
})
:
super
(
key:
key
);
final
_RecordedHandler
handleRecorded
;
final
Widget
child
;
final
Size
size
;
@override
State
<
StatefulWidget
>
createState
()
=>
_AnimationSheetRecorderState
();
}
class
_AnimationSheetRecorderState
extends
State
<
_AnimationSheetRecorder
>
{
GlobalKey
boundaryKey
=
GlobalKey
();
void
_record
(
Duration
duration
)
{
final
RenderRepaintBoundary
boundary
=
boundaryKey
.
currentContext
.
findRenderObject
()
as
RenderRepaintBoundary
;
widget
.
handleRecorded
(
boundary
.
toImage
());
}
@override
Widget
build
(
BuildContext
context
)
{
return
Align
(
alignment:
Alignment
.
topLeft
,
child:
SizedBox
.
fromSize
(
size:
widget
.
size
,
child:
RepaintBoundary
(
key:
boundaryKey
,
child:
_PostFrameCallbacker
(
callback:
widget
.
handleRecorded
==
null
?
null
:
_record
,
child:
widget
.
child
,
),
),
),
);
}
}
// Invokes `callback` and [markNeedsPaint] during the post-frame callback phase
// of every frame.
//
// If `callback` is non-null, `_PostFrameCallbacker` adds a post-frame callback
// every time it paints, during which it calls the provided `callback` then
// invokes [markNeedsPaint].
//
// If `callback` is null, `_PostFrameCallbacker` is equivalent to a proxy box.
class
_PostFrameCallbacker
extends
SingleChildRenderObjectWidget
{
const
_PostFrameCallbacker
({
Key
key
,
Widget
child
,
this
.
callback
,
})
:
super
(
key:
key
,
child:
child
);
final
FrameCallback
callback
;
@override
_RenderPostFrameCallbacker
createRenderObject
(
BuildContext
context
)
=>
_RenderPostFrameCallbacker
(
callback:
callback
,
);
@override
void
updateRenderObject
(
BuildContext
context
,
_RenderPostFrameCallbacker
renderObject
)
{
renderObject
.
callback
=
callback
;
}
}
class
_RenderPostFrameCallbacker
extends
RenderProxyBox
{
_RenderPostFrameCallbacker
({
FrameCallback
callback
,
})
:
_callback
=
callback
;
FrameCallback
get
callback
=>
_callback
;
FrameCallback
_callback
;
set
callback
(
FrameCallback
value
)
{
_callback
=
value
;
if
(
value
!=
null
)
{
markNeedsPaint
();
}
}
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
if
(
callback
!=
null
)
{
SchedulerBinding
.
instance
.
addPostFrameCallback
(
callback
==
null
?
null
:
(
Duration
duration
)
{
callback
(
duration
);
markNeedsPaint
();
});
}
super
.
paint
(
context
,
offset
);
}
@override
void
debugFillProperties
(
DiagnosticPropertiesBuilder
properties
)
{
super
.
debugFillProperties
(
properties
);
properties
.
add
(
FlagProperty
(
'callback'
,
value:
callback
!=
null
,
ifTrue:
'has a callback'
));
}
}
// Layout children in a grid of fixed-sized cells.
//
// The sheet fills up as much space as the parent allows. The cells are
// positioned from top left to bottom right in a row-major order.
class
_CellSheet
extends
StatelessWidget
{
_CellSheet
({
Key
key
,
@required
this
.
cellSize
,
@required
this
.
children
,
})
:
assert
(
cellSize
!=
null
),
assert
(
children
!=
null
&&
children
.
isNotEmpty
),
super
(
key:
key
);
final
Size
cellSize
;
final
List
<
Widget
>
children
;
@override
Widget
build
(
BuildContext
_context
)
{
return
LayoutBuilder
(
builder:
(
BuildContext
context
,
BoxConstraints
constraints
)
{
final
double
rowWidth
=
constraints
.
biggest
.
width
;
final
int
cellsPerRow
=
(
rowWidth
/
cellSize
.
width
).
floor
();
final
List
<
Widget
>
rows
=
<
Widget
>[];
for
(
int
rowStart
=
0
;
rowStart
<
children
.
length
;
rowStart
+=
cellsPerRow
)
{
final
Iterable
<
Widget
>
rowTargets
=
children
.
sublist
(
rowStart
,
math
.
min
(
rowStart
+
cellsPerRow
,
children
.
length
));
rows
.
add
(
Row
(
textDirection:
TextDirection
.
ltr
,
children:
rowTargets
.
map
((
Widget
target
)
=>
SizedBox
.
fromSize
(
size:
cellSize
,
child:
target
,
)).
toList
(),
));
}
return
Column
(
textDirection:
TextDirection
.
ltr
,
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
rows
,
);
});
}
}
This diff is collapsed.
Click to expand it.
packages/flutter_test/lib/src/widget_tester.dart
View file @
fb94d3fb
...
...
@@ -566,6 +566,29 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
}).
then
<
int
>((
_
)
=>
count
);
}
/// Repeatedly pump frames that render the `target` widget with a fixed time
/// `interval` as many as `maxDuration` allows.
///
/// The `maxDuration` argument is required. The `interval` argument defaults to
/// 16.683 milliseconds (59.94 FPS).
Future
<
void
>
pumpFrames
(
Widget
target
,
Duration
maxDuration
,
[
Duration
interval
=
const
Duration
(
milliseconds:
16
,
microseconds:
683
),
])
{
assert
(
maxDuration
!=
null
);
// The interval following the last frame doesn't have to be within the fullDuration.
Duration
elapsed
=
Duration
.
zero
;
return
TestAsyncUtils
.
guard
<
void
>(()
async
{
binding
.
attachRootWidget
(
target
);
binding
.
scheduleFrame
();
while
(
elapsed
<
maxDuration
)
{
await
binding
.
pump
(
interval
);
elapsed
+=
interval
;
}
});
}
/// Runs a [callback] that performs real asynchronous work.
///
/// This is intended for callers who need to call asynchronous methods where
...
...
This diff is collapsed.
Click to expand it.
packages/flutter_test/test/widget_tester_test.dart
View file @
fb94d3fb
...
...
@@ -244,7 +244,9 @@ void main() {
expect
(
message
,
contains
(
'Actual: _TextFinder:<exactly one widget with text "foo" (ignoring offstage widgets): Text("foo", textDirection: ltr)>
\n
'
));
expect
(
message
,
contains
(
'Which: means one was found but none were expected
\n
'
));
});
});
group
(
'pumping'
,
()
{
testWidgets
(
'pumping'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
Text
(
'foo'
,
textDirection:
TextDirection
.
ltr
));
int
count
;
...
...
@@ -278,6 +280,28 @@ void main() {
count
=
await
tester
.
pumpAndSettle
(
const
Duration
(
seconds:
1
));
expect
(
count
,
6
);
});
testWidgets
(
'pumpFrames'
,
(
WidgetTester
tester
)
async
{
final
List
<
int
>
logPaints
=
<
int
>[];
int
initial
;
final
Widget
target
=
_AlwaysAnimating
(
onPaint:
()
{
final
int
current
=
SchedulerBinding
.
instance
.
currentFrameTimeStamp
.
inMicroseconds
;
initial
??=
current
;
logPaints
.
add
(
current
-
initial
);
},
);
await
tester
.
pumpFrames
(
target
,
const
Duration
(
milliseconds:
55
));
expect
(
logPaints
,
<
int
>[
0
,
17000
,
34000
,
50000
]);
logPaints
.
clear
();
await
tester
.
pumpFrames
(
target
,
const
Duration
(
milliseconds:
30
),
const
Duration
(
milliseconds:
10
));
expect
(
logPaints
,
<
int
>[
60000
,
70000
,
80000
]);
});
});
group
(
'find.byElementPredicate'
,
()
{
...
...
@@ -793,3 +817,63 @@ class _SingleTickerTestState extends State<_SingleTickerTest> with SingleTickerP
return
Container
();
}
}
class
_AlwaysAnimating
extends
StatefulWidget
{
const
_AlwaysAnimating
({
this
.
child
,
this
.
onPaint
,
});
final
Widget
child
;
final
VoidCallback
onPaint
;
@override
State
<
StatefulWidget
>
createState
()
=>
_AlwaysAnimatingState
();
}
class
_AlwaysAnimatingState
extends
State
<
_AlwaysAnimating
>
with
SingleTickerProviderStateMixin
{
AnimationController
_controller
;
@override
void
initState
()
{
super
.
initState
();
_controller
=
AnimationController
(
duration:
const
Duration
(
milliseconds:
100
),
vsync:
this
,
);
_controller
.
repeat
();
}
@override
void
dispose
()
{
_controller
.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
AnimatedBuilder
(
animation:
_controller
.
view
,
builder:
(
BuildContext
context
,
Widget
child
)
{
return
CustomPaint
(
painter:
_AlwaysRepaint
(
widget
.
onPaint
),
child:
widget
.
child
,
);
},
);
}
}
class
_AlwaysRepaint
extends
CustomPainter
{
_AlwaysRepaint
(
this
.
onPaint
);
final
VoidCallback
onPaint
;
@override
bool
shouldRepaint
(
CustomPainter
oldDelegate
)
=>
true
;
@override
void
paint
(
Canvas
canvas
,
Size
size
)
{
onPaint
();
}
}
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment