Somebody knows how to change hitTest area in Flutter?
I have RenderObject and I want to paint child in other place inner my widget.
I changed paint offset and add new hitTest area with help BoxHitTestResult.addWithPaintOffset
But I don't want hitTest to trigger on a child widget with old coordinates 0,0;50,50
Below is an example of my code
class BottomPanel extends SingleChildRenderObjectWidget {
final Widget? child;
BottomPanel([this.child]) : super(child: child);
#override
BottomPanelRenderObject createRenderObject(BuildContext context) =>
BottomPanelRenderObject();
}
class BottomPanelRenderObject extends RenderProxyBoxWithHitTestBehavior {
BottomPanelRenderObject() : super(behavior: HitTestBehavior.translucent);
final newChildOffset = Offset(0, 100);
#override
Rect get paintBounds => Rect.fromLTWH(0, 0, 200, 400);
#override
void layout(Constraints constraints, {bool parentUsesSize = false}) {
super.layout(constraints, parentUsesSize: parentUsesSize);
}
#override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
final hitTestResult = super.hitTest(result, position: position);
print('hitTest: hitTestResult=$hitTestResult BoxHitTestResult=${result}');
if (child != null) {
final childHitTest = result.addWithPaintOffset(
offset: newChildOffset,
position: position,
hitTest: (result, offset) {
return child!.hitTest(result, position: offset);
},
);
print('childHitTest: BoxHitTestResult=${childHitTest}');
}
return hitTestResult;
}
#override
void setupParentData(covariant RenderObject child) {
super.setupParentData(child);
}
#override
void performLayout() {
print('performLayout');
size = paintBounds.size;
child?.layout(constraints, parentUsesSize: true);
}
#override
void performResize() {
print('performResize');
size = paintBounds.size;
}
void paintBox(PaintingContext context, Offset offset) {
context.canvas
..drawRect(
Rect.fromLTWH(0, 150, 100, 100),
Paint()..color = Colors.blueGrey,
);
}
void paintChild(PaintingContext context, Offset offset) {
if (child != null) {
context.paintChild(child!, offset);
}
}
#override
void paint(PaintingContext context, Offset offset) {
context.pushLayer(
OffsetLayer(
offset: newChildOffset,
),
paintChild,
offset,
);
layer = context.pushClipRect(
needsCompositing,
offset,
paintBounds,
paintBox,
clipBehavior: Clip.none,
);
}
}
Screenshot example
Read the flutter docs
That's resolved my problem
#override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
final parentData = child?.parentData as BottomPanelParentData?;
if (parentData != null) {
final childHitTest = result.addWithPaintOffset(
offset: parentData.offset,
position: position,
hitTest: (result, offset) {
return child!.hitTest(result, position: offset);
},
);
return childHitTest;
}
return false;
}
Related
I'm making a e-book application and user can draw lines on the WorkbookDrawingPage Widget by Stack layer.
I want to make the drawn lines saved to the device and books by using shared preference. I tried to use a Json encoding, but couldn't apply to my code.
How to save List<Object> to SharedPreferences in Flutter?
Here's my code
WorkbookViewPage ( PdfViewer and DrawingPage widget)
class WorkbookViewPage extends StatefulWidget {
String downloadedURL;
String workbookName;
WorkbookViewPage(this.downloadedURL, this.workbookName, {super.key});
#override
State<WorkbookViewPage> createState() => _WorkbookViewPageState();
}
class _WorkbookViewPageState extends State<WorkbookViewPage> {
/// Widget Key
GlobalKey _viewKey = GlobalKey();
final GlobalKey<SfPdfViewerState> _pdfViewerKey = GlobalKey();
/// Controller
PdfViewerController _pdfViewerController = PdfViewerController();
ScrollController _scrollController = ScrollController();
/// Variables
int _lastClosedPage = 1;
int _currentPage = 1;
bool _memoMode = false;
int drawingPage = 1;
bool isPenTouched = false;
/// pen size control widget Entry
bool isOverlayVisible = false;
OverlayEntry? entry;
/// Hide Overlay Widget
void hideOverlay() {
entry?.remove();
entry = null;
isOverlayVisible = false;
}
/// Count the Total Pages of the Workbook
int _countTotalPages(){
int _totalPages = _pdfViewerController.pageCount;
return _totalPages;
}
/// get Last closed page of workbook
void _loadLastClosedPage() async {
final pagePrefs = await SharedPreferences.getInstance();
_lastClosedPage = pagePrefs.getInt('${widget.workbookName}') ?? 1;
}
#override
void initState() {
super.initState();
// Load Pdf with landScape Mode
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft]);
_pdfViewerController = PdfViewerController();
String workbookName = '${widget.workbookName}';
_loadLastClosedPage();
_countTotalPages();
print("======== Loaded workbook name = ${workbookName} ==========");
}
#override
Widget build(BuildContext context) {
/// Drawing Provider
var p = context.read<DrawingProvider>();
/// set _lastClosedPage
void countLastClosedPage() async {
final pagePrefs = await SharedPreferences.getInstance();
setState(() {
pagePrefs.setInt('${widget.workbookName}', _lastClosedPage);
_lastClosedPage = (pagePrefs.getInt('${widget.workbookName}') ?? 1);
});
}
return Scaffold(
appBar: AppBar(
centerTitle: true,
backgroundColor: Colors.white,
elevation: 1,
),
body: Stack(
children: [
InteractiveViewer(
panEnabled: _memoMode ? false : true,
scaleEnabled: _memoMode ? false : true,
maxScale: 3,
child: Stack(
children: [
IgnorePointer(
ignoring: true,
child: SfPdfViewer.network(
widget.downloadedURL,
controller: _pdfViewerController,
key: _pdfViewerKey,
pageLayoutMode: PdfPageLayoutMode.single,
enableDoubleTapZooming: false,
// Save the last closed page number
onPageChanged: (details) {
_lastClosedPage = details.newPageNumber;
_currentPage = details.newPageNumber;
countLastClosedPage();
},
onDocumentLoaded: (details) {
_pdfViewerController.jumpToPage(_lastClosedPage);
_pdfViewerController.zoomLevel = 0;
print("totalpages = ${_countTotalPages().toInt()}");
},
canShowScrollHead: false,
),
),
SingleChildScrollView(
physics: NeverScrollableScrollPhysics(),
controller: _scrollController,
scrollDirection: Axis.horizontal,
child: WorkbookDrawingPage(widget.workbookName, _countTotalPages().toInt(),),
),
],
),
),
WorkbookDawingPage.dart
class WorkbookDrawingPage extends StatefulWidget {
String workbookName;
int countTotalPages;
WorkbookDrawingPage(this.workbookName, this.countTotalPages,{super.key});
#override
State<WorkbookDrawingPage> createState() => _WorkbookDrawingPageState();
}
class _WorkbookDrawingPageState extends State<WorkbookDrawingPage> {
// OverlayEntry widget key
GlobalKey _overlayViewKey = GlobalKey();
Key _viewKey = UniqueKey();
// pen size control widget Entry
bool isOverlayVisible = false;
OverlayEntry? entry;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
var p = context.read<DrawingProvider>();
void hideOverlay() {
entry?.remove();
entry = null;
isOverlayVisible = false;
}
return Container(
width: MediaQuery.of(context).size.width * (widget.countTotalPages),
height: MediaQuery.of(context).size.height * 1,
child: CustomPaint(
painter: DrawingPainter(p.lines),
child: Listener(
behavior: HitTestBehavior.translucent,
// Draw lines when stylus hit the screen
onPointerDown: (s) async {
if (s.kind == PointerDeviceKind.stylus) {
setState(() {
p.penMode ? p.penDrawStart(s.localPosition) : null;
p.highlighterMode
? p.highlighterDrawStart(s.localPosition)
: null;
p.eraseMode ? p.erase(s.localPosition) : null;
});
}
/// Stylus with button pressed touched the Screen
else if (s.kind == PointerDeviceKind.stylus ||
s.buttons == kPrimaryStylusButton) {
setState(() {
p.changeEraseModeButtonClicked = true;
});
}
},
onPointerMove: (s) {
if (s.kind == PointerDeviceKind.stylus) {
setState(() {
p.penMode ? p.penDrawing(s.localPosition) : null;
p.highlighterMode
? p.highlighterDrawing(s.localPosition)
: null;
p.eraseMode ? p.erase(s.localPosition) : null;
});
} else if (s.kind == PointerDeviceKind.stylus ||
s.buttons == kPrimaryStylusButton) {
setState(() {
p.changeEraseModeButtonClicked = true;
});
}
},
),
),
);
}
}
The application uses provider for drawing lines and here's provider class
Drawing Provider
Class DrawingProvider extends ChangeNotifier {
/// line List **/
final lines = <List<DotInfo>>[];
Color _selectedColor = Colors.black;
Color get selectedColor => _selectedColor;
/** function method to change Color **/
set changeColor(Color color) {
_selectedColor = color;
notifyListeners();
}
/** Mode Selection **/
bool _penMode = false;
bool get penMode => _penMode;
bool _highlighterMode = false;
bool get highlighterMode => _highlighterMode;
bool _eraseMode = false;
bool get eraseMode => _eraseMode;
bool _memoMode = false;
bool get memoMode => _memoMode;
set changeEraseModeButtonClicked(bool eraseMode) {
eraseMode = true;
_eraseMode = eraseMode;
print("eraseMode가 On ");
notifyListeners();
}
/** 지우개 선택 모드 **/
void changeEraseMode() {
_eraseMode = !_eraseMode;
print("eraseMode is : ${eraseMode}");
_penMode = false;
_highlighterMode = false;
notifyListeners();
}
/** 일반 펜 선택 모드 **/
void changePenMode() {
_penMode = !_penMode;
// _selectedColor = _selectedColor.withOpacity(1);
print("penMode is called : ${penMode} ");
_eraseMode = false;
_highlighterMode = false;
notifyListeners();
}
/** 형광펜 선택 모드 **/
void changeHighlighterMode() {
_highlighterMode = !_highlighterMode;
// _selectedColor = _selectedColor.withOpacity(0.3);
print("Highlighter Mode : ${highlighterMode}");
_eraseMode = false;
_penMode = false;
_opacity = 0.3;
notifyListeners();
}
/** Pen Draw Start **/
void penDrawStart(Offset offset) async {
var oneLine = <DotInfo>[];
oneLine.add(DotInfo(offset, penSize, _selectedColor,));
lines.add(oneLine);
notifyListeners();
}
/** Pen Drawing **/
void penDrawing(Offset offset) {
lines.last.add(DotInfo(offset, penSize, _selectedColor));
notifyListeners();
}
/** highlighter Start **/
void highlighterDrawStart(Offset offset) {
var oneLine = <DotInfo>[];
oneLine
.add(DotInfo(offset, highlighterSize, _selectedColor.withOpacity(0.3),));
lines.add(oneLine);
notifyListeners();
}
/** 터치 후 형광 펜 그리기 **/
void highlighterDrawing(Offset offset) {
lines.last
.add(DotInfo(offset, highlighterSize, _selectedColor.withOpacity(0.3)));
notifyListeners();
}
/** 선 지우기 메서드 **/
void erase(Offset offset) {
final eraseRange = 15;
for (var oneLine in List<List<DotInfo>>.from(lines)) {
for (var oneDot in oneLine) {
if (sqrt(pow((offset.dx - oneDot.offset.dx), 2) +
pow((offset.dy - oneDot.offset.dy), 2)) <
eraseRange) {
lines.remove(oneLine);
break;
}
}
}
notifyListeners();
}
}
and the Dotinfo save the information of the lines
DotInfo.dart
class DotInfo {
final Offset offset;
final double size;
final Color color;
DotInfo(this.offset, this.size, this.color);
}
in the WorkbookDrawingPage.dart , CustomPaint.painter method get DrawingPainter Class
DrawingPainter.dart
class DrawingPainter extends CustomPainter {
final List<List<DotInfo>> lines;
DrawingPainter(this.lines);
#override
void paint(Canvas canvas, Size size) {
for (var oneLine in lines) {
Color? color;
double? size;
var path = Path();
var l = <Offset>[];
for (var oneDot in oneLine) {
color ??= oneDot.color;
size ??= oneDot.size;
l.add(oneDot.offset);
}
path.addPolygon(l, false);
canvas.drawPath(
path,
Paint()
..color = color!
..strokeWidth = size!
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..isAntiAlias = true
..strokeJoin = StrokeJoin.round);
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
The Drawing function is related to the provider.
I have no idea how to save the drawn lines into the device. My idea was saving the WorkbookDrawingPage widget itself to the device by using shared preference but, shared preference only can save Int, String, double... so, I tried to encode the DotInfo Class but it cannot be encoded because constructors value is not initialized in the DotInfo Class.
how to save the drawn lines to the device and make them pair with the book?
is saving the widget to the device is possible?? or can you give me any hint or answer?
I have a listview in which the text of items are animated when they first appear - and when they reappear after enough scrolling. When the list grows to certain size and the user scrolls back far enough items are animated again - presumably they've been removed from the widget tree and are now being re-inserted and thus get re-initiated etc. I want to prevent this from happening so that they only animate the first time they appear.
I think this means I need to have state stored somewhere per item that keeps track and tells the individual items whether they should animate on them being built or not. I am not sure where to put and how to connect that though, partly because it seems to overlap between presentation and business logic layers. I think perhaps it should be a variable in the list items contained in the list object that the listview builder is constructing from - or should it somehow be in the actual widgets in the listview?
class _StockListViewBuilderState extends State<StockListViewBuilder> with asc_alertBar {
final ScrollController _scrollController = ScrollController();
late double _scrollPosition;
late double _maxScrollExtent;
late bool isThisTheEnd = false;
_scrollListener() async {
setState(() {
_scrollPosition = _scrollController.position.pixels;
_maxScrollExtent = _scrollController.position.maxScrollExtent;
});
if (!isThisTheEnd && _scrollPosition / _maxScrollExtent > 0.90) {
isThisTheEnd = true;
if (widget.stockListicle.getIsRemoteEmpty()) {
alertBar('No more items available', /* null,*/ context: context);
} else {
await widget.stockListicle.fetch(numberToFetch: 5);
}
}
if (isThisTheEnd && _scrollPosition / _maxScrollExtent <= 0.90) {
isThisTheEnd = false;
}
}
#override
void initState() {
super.initState();
late String? userFullName = GetIt.I.get<Authenticate>().user?.fullName;
developer.log('Authenticated user $userFullName', name: '_StockListViewBuilderState');
developer.log("init ", name: "_StockListViewBuilderState ");
int listCount;
_scrollController.addListener(_scrollListener);
WidgetsBinding.instance.addPostFrameCallback((_) async {
//developer.log("stckLtcl init pf con ");
listCount = widget.stockListicle.items.length;
if (listCount < 10 && !widget.stockListicle.getIsRemoteEmpty()) {
try {
await widget.stockListicle.fetch(numberToFetch: 10);
} catch (e) {
super.setState(() {
//developer.log("Can't load stock:$e");
alertBar(
"Couldn't load from the internet.",
context: context,
backgroundColor: Colors.purple,
);
});
}
}
});
WidgetsBinding.instance.addPostFrameCallback((_) async {
final ConnectionNotifier connectionNotifier = context.read<ConnectionNotifier>();
if (connectionNotifier.isConnected() != true) {
await connectionNotifier.check();
if (connectionNotifier.isConnected() != true) {
alertBar("Please check the internet connection.", context: context);
}
}
});
}
#override
Widget build(BuildContext context) {
return ListView.builder(
scrollDirection: Axis.vertical,
controller: _scrollController,
shrinkWrap: true,
key: widget.theKey,
itemCount: widget.stockListicle.items.length + 1,
itemBuilder: (context, index) {
if (index <= widget.stockListicle.items.length - 1) {
return InkWell(
onTap: (() => Navigator.pushNamed(
context,
'/stocks/stock',
arguments: ScreenArguments(widget.stockListicle.items[index] as Stock),
)),
child: StockListItem(
stock: widget.stockListicle.items[index] as Stock,
));
} else {
return LoadingItemNotifier(
isLoading: widget.stockListicle.getIsBusyLoading(),
);
}
},
);
}
}
//...
Currently StockListItem extends StatelessWidget and returns a 'ListTile' which as its title parameter has ...title: AnimatedText(textContent: stock.title),...
I was trying to keep track of first-time-animation inside AnimatedText widget until I realized from an OOP & Flutter perspective, it's probably wrong place...
class AnimatedText extends StatefulWidget {
final bool doShowMe;
final String textContent;
final Duration hideDuration;
final double durationFactor;
const AnimatedText({
Key? key,
this.doShowMe = true,
this.textContent = '',
this.hideDuration = const Duration(milliseconds: 500),
this.durationFactor = 1,
}) : super(key: key);
#override
State<AnimatedText> createState() => _AnimatedTextState();
}
class _AnimatedTextState extends State<AnimatedText> with SingleTickerProviderStateMixin {
late AnimationController _appearanceController;
late String displayText;
late String previousText;
late double durationFactor;
late Duration buildDuration = Duration(
milliseconds: (widget.textContent.length / 15 * widget.durationFactor * 1000).round());
#override
void initState() {
super.initState();
developer.log('init ${widget.textContent}', name: '_AnimatedTextState');
displayText = '';
previousText = widget.textContent;
_appearanceController = AnimationController(
vsync: this,
duration: buildDuration,
)..addListener(
() => updateText(),
);
if (widget.doShowMe) {
_doShowMe();
}
}
void updateText() {
String payload = widget.textContent;
int numCharsToShow = (_appearanceController.value * widget.textContent.length).ceil();
if (widget.doShowMe) {
// make it grow
displayText = payload.substring(0, numCharsToShow);
// developer.log('$numCharsToShow / ${widget.textContent.length} ${widget.textContent}');
} else {
// make it shrink
displayText = payload.substring(payload.length - numCharsToShow, payload.length);
}
}
#override
void didUpdateWidget(AnimatedText oldWidget) {
super.didUpdateWidget(oldWidget);
if ((widget.doShowMe != oldWidget.doShowMe) || (widget.textContent != oldWidget.textContent)) {
if (widget.doShowMe) {
_doShowMe();
} else {
_doHideMe();
}
}
if (widget.doShowMe && widget.textContent != previousText) {
previousText = widget.textContent;
developer.log('reset');
_appearanceController
..reset()
..forward();
}
}
#override
void dispose() {
_appearanceController.dispose();
displayText = '';
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _appearanceController,
builder: (context, child) {
return Text(displayText);
});
}
void _doShowMe() {
_appearanceController
..duration = buildDuration
..forward();
}
void _doHideMe() {
_appearanceController
..duration = widget.hideDuration
..reverse();
}
}
I have a floating widget which I want to animate moving across the screen on a Bezier path.
I already have a path var which changes according to drag and fling gestures (predefined final offset).
How do I make the widget follow such a path?
Edit: as per request here's the code.
It's mostly a fork of https://pub.dev/packages/pip_view package.
You can look at the _onPadEnd, which is where I'm triggering the animation. Just trying to see how to make it follow a Bezier path.
import 'package:flutter/material.dart';
import 'dismiss_keyboard.dart';
import 'constants.dart';
import 'dart:developer';
import 'dart:math' hide log;
import 'package:arc_animator/arc_animator.dart'; //unused import
double sqrtNegSup(double val){ return val.isNegative ? -(sqrt(-val)) : sqrt(val); }
Map<PIPViewCorner, Offset> _calculateOffsets({Size spaceSize, Size widgetSize, EdgeInsets windowPadding,}) {
Offset getOffsetForCorner(PIPViewCorner corner) {
final spacing = 16;
final left = spacing + windowPadding.left;
final top = spacing + windowPadding.top;
final right = spaceSize.width - widgetSize.width - windowPadding.right - spacing;
final bottom = spaceSize.height - widgetSize.height - windowPadding.bottom - spacing;
switch (corner) {
case PIPViewCorner.topLeft: return Offset(left, top);
case PIPViewCorner.topRight: return Offset(right, top);
case PIPViewCorner.bottomLeft: return Offset(left, bottom);
case PIPViewCorner.bottomRight: return Offset(right, bottom);
default: throw Exception('Not implemented.');
}
}
final corners = PIPViewCorner.values;
final Map<PIPViewCorner, Offset> offsets = {};
for (final corner in corners) {
offsets[corner] = getOffsetForCorner(corner);
}
return offsets;
}
enum PIPViewCorner {
topLeft,
topRight,
bottomLeft,
bottomRight,
}
class _CornerDistance {
final PIPViewCorner corner;
final double distance;
_CornerDistance({this.corner, this.distance,});
}
class PIPView extends StatefulWidget {
final PIPViewCorner initialCorner;
final double floatingWidth;
final double floatingHeight;
final bool avoidKeyboard;
final Widget Function(BuildContext context, bool isFloating, ) builder;
const PIPView({
Key key, #required this.builder,
this.initialCorner = PIPViewCorner.topRight,
this.floatingWidth,
this.floatingHeight,
this.avoidKeyboard = true,
}) : super(key: key);
#override PIPViewState createState() => PIPViewState();
static PIPViewState of(BuildContext context) {return context.findAncestorStateOfType<PIPViewState>();}
}
/// pan functions correspond to callbacks of dragging gestures
/// those functions will update the last drag and offset vars accordingly
///
/// animations are performed independently
/// and are triggered when toggling or dragging is concluded
class PIPViewState extends State<PIPView> with TickerProviderStateMixin {
Widget _bottomView;
AnimationController _toggleFloatingAnimationController; //animation for toggling PIP
AnimationController _dragAnimationController; //animation for dragging PIP
PIPViewCorner _corner;
Offset _dragOffset = Offset.zero;
bool _isDragging = false;
Map<PIPViewCorner, Offset> _offsets = {};
//screenspace
double width;
double height;
//values for refining fling engagement (see usages)
double baseAdditionOfFlingVelocity = 0.75;
double baseMultiplierOfFlingVelocity = 2;
//values for refining fling duration (see usages)
int minimumDuration = 730;
int maximumDuration = 1280;
double durationMultiplier = 1.10;
bool _isAnimating() { return _toggleFloatingAnimationController.isAnimating || _dragAnimationController.isAnimating; }
#override void initState() {
super.initState();
_corner = widget.initialCorner;
_toggleFloatingAnimationController = AnimationController(duration: defaultAnimationDuration, vsync: this,);
_dragAnimationController = AnimationController(duration: defaultAnimationDuration, vsync: this,);
}
void _updateCornersOffsets({ Size spaceSize, Size widgetSize, EdgeInsets windowPadding,}) {_offsets = _calculateOffsets(spaceSize: spaceSize, widgetSize: widgetSize, windowPadding: windowPadding,);}
_CornerDistance _calculateNearestCornerAndDistance({Offset offset, Map<PIPViewCorner, Offset> offsets,}) {
_CornerDistance calculateDistance(PIPViewCorner corner) {
final distance = offsets[corner].translate(-offset.dx, -offset.dy,).distanceSquared;
return _CornerDistance(corner: corner, distance: distance,);
}
final distances = PIPViewCorner.values.map(calculateDistance).toList();
distances.sort((cd0, cd1) => cd0.distance.compareTo(cd1.distance));
return _CornerDistance(corner:distances.first.corner,distance:distances.first.distance);
}
_CornerDistance _calculateNearestCornerAndDistanceWithFling({Offset offsetWithFling, Offset offsetWithoutFling, Map<PIPViewCorner, Offset> offsets,}) {
_CornerDistance calculateDistance(PIPViewCorner corner) {
final distance = offsets[corner].translate(-offsetWithFling.dx, -offsetWithFling.dy,).distanceSquared;
return _CornerDistance(corner: corner, distance: distance,);
}
final distances = PIPViewCorner.values.map(calculateDistance).toList();
distances.sort((cd0, cd1) => cd0.distance.compareTo(cd1.distance));
final actualDistance = offsets[distances.first.corner].translate(-offsetWithoutFling.dx, -offsetWithoutFling.dy).distanceSquared;
return _CornerDistance(corner:distances.first.corner, distance: actualDistance);
}
#override Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
var windowPadding = mediaQuery.padding;
if (widget.avoidKeyboard) {windowPadding += mediaQuery.viewInsets;}
final isFloating = _bottomView != null;
return LayoutBuilder(
builder: (context, constraints) {
width = constraints.maxWidth;
height = constraints.maxHeight;
double floatingWidth = widget.floatingWidth;
double floatingHeight = widget.floatingHeight;
if (floatingWidth == null && floatingHeight != null) {floatingWidth = width / height * floatingHeight;}
floatingWidth ??= 100.0;
if (floatingHeight == null) {floatingHeight = height / width * floatingWidth;}
final floatingWidgetSize = Size(floatingWidth, floatingHeight);
final fullWidgetSize = Size(width, height);
_updateCornersOffsets(spaceSize: fullWidgetSize, widgetSize: floatingWidgetSize, windowPadding: windowPadding,);
final calculatedOffset = _offsets[_corner];
// BoxFit.cover
final widthRatio = floatingWidth / width;
final heightRatio = floatingHeight / height;
final scaledDownScale = widthRatio > heightRatio
? floatingWidgetSize.width / fullWidgetSize.width
: floatingWidgetSize.height / fullWidgetSize.height;
return Stack( children: <Widget>[
if (isFloating) Navigator(onGenerateRoute: (settings) {return MaterialPageRoute(builder: (_) {return _bottomView;});},),
AnimatedBuilder(
animation: Listenable.merge([_toggleFloatingAnimationController, _dragAnimationController,]),
builder: (context, child) {
final animationCurve = CurveTween(curve: Curves.fastLinearToSlowEaseIn,);
// final animationArc =
//region assign corresponding keys of controller and animation
final dragAnimationValue= animationCurve.transform(_dragAnimationController.value,);
final toggleFloatingAnimationValue = animationCurve.transform(_toggleFloatingAnimationController.value,);
//endregion
final floatingOffset = _isDragging ? _dragOffset : Tween<Offset>(begin: _dragOffset, end: calculatedOffset,)
.transform(_dragAnimationController.isAnimating ? dragAnimationValue : toggleFloatingAnimationValue);
final borderRadius = Tween<double>(begin: 0, end: 10,).transform(toggleFloatingAnimationValue);
final width = Tween<double>(begin: fullWidgetSize.width, end: floatingWidgetSize.width,).transform(toggleFloatingAnimationValue);
final height = Tween<double>(begin: fullWidgetSize.height, end: floatingWidgetSize.height,).transform(toggleFloatingAnimationValue);
final scale = Tween<double>(begin: 1, end: scaledDownScale,).transform(toggleFloatingAnimationValue);
return Positioned(
left: floatingOffset.dx,
top: floatingOffset.dy,
child: GestureDetector(
onPanStart: isFloating ? _onPanStart : null,
onPanUpdate: isFloating ? _onPanUpdate : null,
onPanCancel: isFloating ? _onPanCancel : null,
onPanEnd: isFloating ? _onPanEnd : null, //pass floatingWidgetSize
onTap: isFloating ? stopFloating : null,
child: Material(
elevation: 10,
borderRadius: BorderRadius.circular(borderRadius),
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(borderRadius),
),
width: width,
height: height,
child: Transform.scale(
scale: scale,
child: OverflowBox(
maxHeight: fullWidgetSize.height,
maxWidth: fullWidgetSize.width,
child: IgnorePointer(
ignoring: isFloating,
child: child,
),
),
),
),
),
),
);
},
child: Builder(builder: (context) => widget.builder(context, isFloating),),
),
],);
},
);
}
//region padEndLogs
void logFlingDetails(DragEndDetails details){
// details.velocity.pixelsPerSecond.translate(translateX, translateY)
log(
"fling details"
+"\n dx sqrt: " + sqrtNegSup(details.velocity.pixelsPerSecond.dx).toInt().toString()
+"\n dy sqrt: " + sqrtNegSup(details.velocity.pixelsPerSecond.dy).toInt().toString()
+"\n direction: " + details.velocity.pixelsPerSecond.direction.toString()
);
}
void logOffsetDetails(Offset _off, bool beforeApplyingFling){
log(
(beforeApplyingFling ? "offset details BEFORE applying fling params" : "offset details AFTER applying fling params")
+"\n dx: " + _off.dx.toInt().toString()
+"\n dy: " + _off.dy.toInt().toString()
+"\n direction: " + _off.direction.toString()
);
}
void logCornerCalculationDetails(_CornerDistance cornerDistanceDetails, bool withFling){
log(
(withFling?"cornerDistanceDetails AFTER fling":"cornerDistanceDetails BEFORE fling")
+"\n nearest corner: " + cornerDistanceDetails.corner.toString()
+"\n distance: " + cornerDistanceDetails.distance.toInt().toString()
);
}
//endregion
//region toggles & gestures
/*toggle pip on, arg is page over which it will be presented*/ void presentBelow(Widget widget) {
if (_isAnimating() || _bottomView != null) return;
dismissKeyboard(context);
setState(() {_bottomView = widget;});
_toggleFloatingAnimationController.forward();
}
/*toggle pip off, triggered on tap*/ void stopFloating() {
if (_isAnimating() || _bottomView == null) return;
dismissKeyboard(context);
_toggleFloatingAnimationController.reverse().whenCompleteOrCancel(() {if (mounted) {setState(() {_bottomView = null;});}});
}
void _onPanCancel() {
if (!_isDragging) return;
setState(() {
_dragAnimationController.value = 0;
_dragOffset = Offset.zero;
_isDragging = false;
});
}
void _onPanStart(DragStartDetails details) {
if (_isAnimating()) return;
setState(() {_dragOffset = _offsets[_corner]; _isDragging = true;});
}
void _onPanUpdate(DragUpdateDetails details) {
if (!_isDragging) return;
setState(() {_dragOffset = _dragOffset.translate(details.delta.dx, details.delta.dy,);});
}
void _onPanEnd(DragEndDetails details) {
if (!_isDragging) return;
//region fling values
double minimum = min(width, height);
double flingDx = sqrtNegSup(details.velocity.pixelsPerSecond.dx) * ((width / minimum) + baseAdditionOfFlingVelocity) * baseMultiplierOfFlingVelocity;
double flingDy = sqrtNegSup(details.velocity.pixelsPerSecond.dy) * ((height / minimum) + baseAdditionOfFlingVelocity) * baseMultiplierOfFlingVelocity;
//endregion
Offset offsetWithFling = _dragOffset.translate(flingDx, flingDy);
final nearestCornerToUse = _calculateNearestCornerAndDistanceWithFling(offsetWithFling: offsetWithFling,offsetWithoutFling: _dragOffset,offsets: _offsets );
setState(() { _corner = nearestCornerToUse.corner; _isDragging = false; });
//region duration values
int newDuration = (sqrt(nearestCornerToUse.distance)*durationMultiplier).toInt();
if(newDuration<minimumDuration) newDuration=minimumDuration;
if(newDuration>maximumDuration) newDuration=maximumDuration;
//endregion
log("new duration is: "+newDuration.toString());
_dragAnimationController.duration = Duration(milliseconds: newDuration.toInt());
Path path = Path();
path.moveTo(_dragOffset.dx, _dragOffset.dy);
path.quadraticBezierTo(flingDx, flingDy, _offsets[nearestCornerToUse.corner].dx, _offsets[nearestCornerToUse.corner].dy);
double jDx = calculate(path, _dragAnimationController).dx;
double jDy = calculate(path, _dragAnimationController).dy;
_dragAnimationController.forward().whenCompleteOrCancel(() { _dragAnimationController.value = 0; _dragOffset = Offset.zero; });
}
//endregion
Offset calculate(Path path, value) {
PathMetrics pathMetrics = path.computeMetrics();
PathMetric pathMetric = pathMetrics.elementAt(0);
value = pathMetric.length * value;
Tangent pos = pathMetric.getTangentForOffset(value);
return pos.position;
}
}```
[1]: https://pub.dev/packages/pip_view
I want to create the custom Slider from the deign below with a continuous track which would snap to a specific value which is shown below as a black rectangle.
I was able to recreate the custom Slider by setting defining custom trackShape and thumbShape.
I'm not sure how to snap the thumb when it's very close to the small black rectangle and how to make the black rectangle clickable.
The behavior is very similar to the Slider with discrete divisions but it should be continuous and the small black rectangle must be clickable.
I ended up implementing it by myself with some inspiration from https://github.com/tomwyr/step-slider:
class SnapSlider extends StatefulWidget {
SnapSlider({
Key key,
this.sliderKey,
this.snapValues = const {},
this.value,
this.onSnapValueChanged,
this.snapDistance = 0.05,
this.animCurve: Curves.fastOutSlowIn,
this.animDuration: const Duration(milliseconds: 350),
this.min: 0.0,
this.max: 1.0,
this.label,
this.divisions,
this.onChanged,
this.onChangeEnd,
this.onChangeStart,
this.activeColor,
this.inactiveColor,
this.semanticFormatterCallback,
}) : assert(snapValues != null),
assert(snapValues.every((it) => it >= min && it <= max),
'Each snap value needs to be within slider values range.'),
super(key: key);
final Key sliderKey;
final Set<double> snapValues;
final double value;
final ValueChanged<double> onSnapValueChanged;
final double snapDistance;
final Curve animCurve;
final Duration animDuration;
final double min;
final double max;
final String label;
final int divisions;
final Color activeColor;
final Color inactiveColor;
final ValueChanged<double> onChanged;
final ValueChanged<double> onChangeEnd;
final ValueChanged<double> onChangeStart;
final SemanticFormatterCallback semanticFormatterCallback;
#override
_StepSliderState createState() => _StepSliderState();
}
class _StepSliderState extends State<SnapSlider>
with SingleTickerProviderStateMixin {
AnimationController _animator;
CurvedAnimation _baseAnim;
Animation<double> _animation;
double _lastSnapValue;
#override
void didUpdateWidget(SnapSlider oldWidget) {
super.didUpdateWidget(oldWidget);
_animator.duration = widget.animDuration;
_baseAnim.curve = widget.animCurve;
}
#override
void initState() {
super.initState();
_animator = AnimationController(
vsync: this, duration: widget.animDuration, value: 1.0);
_baseAnim = CurvedAnimation(parent: _animator, curve: widget.animCurve);
_recreateAnimation(widget.value, widget.value);
_animation.addListener(() {
_onSliderChanged(_animation.value);
widget.onChanged?.call(_animation.value);
});
}
#override
void dispose() {
_animator.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animator,
builder: (_, __) => Slider(
key: widget.sliderKey,
min: widget.min,
max: widget.max,
label: widget.label,
divisions: widget.divisions,
activeColor: widget.activeColor,
inactiveColor: widget.inactiveColor,
semanticFormatterCallback: widget.semanticFormatterCallback,
value: widget.value,
onChangeStart: (it) {
_animator.stop();
_onSliderChangeStart(it);
widget.onChangeStart?.call(it);
},
onChangeEnd: (it) {
_onSliderChangeEnd(it);
widget.onChangeEnd?.call(it);
},
onChanged: (it) {
_onSliderChanged(it);
widget.onChanged?.call(it);
},
),
);
}
void _onSliderChangeStart(double value) {
}
void _onSliderChangeEnd(double value) {
double snapValue = _closestSnapValue(value);
var distance = (value - snapValue).abs();
if (snapValue != _lastSnapValue) {
if (distance <= widget.snapDistance) {
_animateTo(widget.value, snapValue, true);
widget.onSnapValueChanged?.call(widget.value);
_lastSnapValue = snapValue;
}
} else {
if (distance > widget.snapDistance) {
_lastSnapValue = null;
}
}
}
double _closestSnapValue(double value) {
return widget.snapValues.reduce((a, b) {
var distanceA = (value - a).abs();
var distanceB = (value - b).abs();
return distanceA < distanceB ? a : b;
});
}
void _onSliderChanged(double value) {
}
void _animateTo(double start, double end, bool restart) {
_recreateAnimation(start, end);
_animator.forward(from: 0.0);
}
void _recreateAnimation(double start, double end) {
_animation = Tween(begin: start ?? end, end: end).animate(_baseAnim);
}
}
Flutter's AnimatedSize class animates its size according to the size of its child. I need to know how to listen for changes to the size, ideally when the resizing has finished.
With my use-case, this widget is contained within a ListView, but I only seem to be able to listen to scroll events on this with a NotificationListener (being able to listen to changes in scrollable height would solve my problem).
Alternatively, being able to listen for when a widget such as a Column changes it's number of children would work too.
There was a widget specifically made for this case. It's called:
SizeChangedLayoutNotifier (https://api.flutter.dev/flutter/widgets/SizeChangedLayoutNotifier-class.html)
You just have to wrap your widget with it and then listen with the NotificationListener widget (https://api.flutter.dev/flutter/widgets/NotificationListener-class.html) for changes.
An Example would be following:
NotificationListener(
onNotification: (SizeChangedLayoutNotification notification){
Future.delayed(Duration(milliseconds: 300),(){setState(() {
print('size changed');
_height++;
});});
return true;
},
child: SizeChangedLayoutNotifier( child: AnimatedContainer(width: 100, height: _height)))
Hope this will help all future people which will find this post.
I believe the last line of your question provides a hint as to what you're trying to do. It sounds like you're displaying a list of things, and you want something to be notified when that list of things changes. If I'm wrong about that, please clarify =).
There are two ways of doing this; one is that you could pass a callback function to the widget containing the list. When you added something to the list you could simply call the callback.
However, that is a little bit fragile and if you have multiple layers in between the place you need to know and the actual list it could get messy.
This is due in part to the fact that in flutter, for the most part, data goes downwards (through children) much easier than it goes up. It sounds like what you might want to do is have a parent widget that holds the list of items, and passes that down to whatever builds the actual list. If there are multiple layers of widgets between the parent and the child, you could use an InheritedWidget to get the information from the child without directly passing it.
EDIT: with clarification from the OP, this answer only provided an sub-optimal alternative to the original goal. See below for an answer to the main query:
I don't think that it is possible to do this with any existing flutter widgets. However, because flutter is open-source it's entirely possible to simply create your own widget based on the flutter one that does do what you need. You just need to dig into the source code a bit.
Please note that the code I'm pasting below contains a slightly modified version of the flutter implementation in rendering animated_size.dart and widgets animated_size.dart, and therefore usage of it must adhere to the flutter LICENSE file at the time of copying. Use of the code is governed by BSD style license, yada yada.
I've created a very slightly modified version of the AnimatedSize widget called NotifyingAnimatedSize (and the corresponding more-interesting NotifyingRenderAnimatedSize) in the code below, which simply calls a callback when it starts animated and when it's done animating. I've removed all of the comments from the source code as they made it even longer.
Look for notificationCallback throughout the code as that's basically all I added.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() => runApp(new MyApp());
enum NotifyingRenderAnimatedSizeState {
start,
stable,
changed,
unstable,
}
enum SizeChangingStatus {
changing,
done,
}
typedef void NotifyingAnimatedSizeCallback(SizeChangingStatus status);
class NotifyingRenderAnimatedSize extends RenderAligningShiftedBox {
NotifyingRenderAnimatedSize({
#required TickerProvider vsync,
#required Duration duration,
Curve curve: Curves.linear,
AlignmentGeometry alignment: Alignment.center,
TextDirection textDirection,
RenderBox child,
this.notificationCallback
}) : assert(vsync != null),
assert(duration != null),
assert(curve != null),
_vsync = vsync,
super(child: child, alignment: alignment, textDirection: textDirection) {
_controller = new AnimationController(
vsync: vsync,
duration: duration,
)..addListener(() {
if (_controller.value != _lastValue) markNeedsLayout();
});
_animation = new CurvedAnimation(parent: _controller, curve: curve);
}
AnimationController _controller;
CurvedAnimation _animation;
final SizeTween _sizeTween = new SizeTween();
bool _hasVisualOverflow;
double _lastValue;
final NotifyingAnimatedSizeCallback notificationCallback;
#visibleForTesting
NotifyingRenderAnimatedSizeState get state => _state;
NotifyingRenderAnimatedSizeState _state = NotifyingRenderAnimatedSizeState.start;
Duration get duration => _controller.duration;
set duration(Duration value) {
assert(value != null);
if (value == _controller.duration) return;
_controller.duration = value;
}
Curve get curve => _animation.curve;
set curve(Curve value) {
assert(value != null);
if (value == _animation.curve) return;
_animation.curve = value;
}
bool get isAnimating => _controller.isAnimating;
TickerProvider get vsync => _vsync;
TickerProvider _vsync;
set vsync(TickerProvider value) {
assert(value != null);
if (value == _vsync) return;
_vsync = value;
_controller.resync(vsync);
}
#override
void detach() {
_controller.stop();
super.detach();
}
Size get _animatedSize {
return _sizeTween.evaluate(_animation);
}
#override
void performLayout() {
_lastValue = _controller.value;
_hasVisualOverflow = false;
if (child == null || constraints.isTight) {
_controller.stop();
size = _sizeTween.begin = _sizeTween.end = constraints.smallest;
_state = NotifyingRenderAnimatedSizeState.start;
child?.layout(constraints);
return;
}
child.layout(constraints, parentUsesSize: true);
assert(_state != null);
switch (_state) {
case NotifyingRenderAnimatedSizeState.start:
_layoutStart();
break;
case NotifyingRenderAnimatedSizeState.stable:
_layoutStable();
break;
case NotifyingRenderAnimatedSizeState.changed:
_layoutChanged();
break;
case NotifyingRenderAnimatedSizeState.unstable:
_layoutUnstable();
break;
}
size = constraints.constrain(_animatedSize);
alignChild();
if (size.width < _sizeTween.end.width || size.height < _sizeTween.end.height) _hasVisualOverflow = true;
}
void _restartAnimation() {
_lastValue = 0.0;
_controller.forward(from: 0.0);
}
void _layoutStart() {
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
_state = NotifyingRenderAnimatedSizeState.stable;
}
void _layoutStable() {
if (_sizeTween.end != child.size) {
_sizeTween.begin = size;
_sizeTween.end = debugAdoptSize(child.size);
_restartAnimation();
_state = NotifyingRenderAnimatedSizeState.changed;
} else if (_controller.value == _controller.upperBound) {
// Animation finished. Reset target sizes.
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
notificationCallback(SizeChangingStatus.done);
} else if (!_controller.isAnimating) {
_controller.forward(); // resume the animation after being detached
}
}
void _layoutChanged() {
if (_sizeTween.end != child.size) {
// Child size changed again. Match the child's size and restart animation.
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
_restartAnimation();
_state = NotifyingRenderAnimatedSizeState.unstable;
} else {
notificationCallback(SizeChangingStatus.changing);
// Child size stabilized.
_state = NotifyingRenderAnimatedSizeState.stable;
if (!_controller.isAnimating) _controller.forward(); // resume the animation after being detached
}
}
void _layoutUnstable() {
if (_sizeTween.end != child.size) {
// Still unstable. Continue tracking the child.
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child.size);
_restartAnimation();
} else {
// Child size stabilized.
_controller.stop();
_state = NotifyingRenderAnimatedSizeState.stable;
}
}
#override
void paint(PaintingContext context, Offset offset) {
if (child != null && _hasVisualOverflow) {
final Rect rect = Offset.zero & size;
context.pushClipRect(needsCompositing, offset, rect, super.paint);
} else {
super.paint(context, offset);
}
}
}
class NotifyingAnimatedSize extends SingleChildRenderObjectWidget {
const NotifyingAnimatedSize({
Key key,
Widget child,
this.alignment: Alignment.center,
this.curve: Curves.linear,
#required this.duration,
#required this.vsync,
this.notificationCallback,
}) : super(key: key, child: child);
final AlignmentGeometry alignment;
final Curve curve;
final Duration duration;
final TickerProvider vsync;
final NotifyingAnimatedSizeCallback notificationCallback;
#override
NotifyingRenderAnimatedSize createRenderObject(BuildContext context) {
return new NotifyingRenderAnimatedSize(
alignment: alignment,
duration: duration,
curve: curve,
vsync: vsync,
textDirection: Directionality.of(context),
notificationCallback: notificationCallback
);
}
#override
void updateRenderObject(BuildContext context, NotifyingRenderAnimatedSize renderObject) {
renderObject
..alignment = alignment
..duration = duration
..curve = curve
..vsync = vsync
..textDirection = Directionality.of(context);
}
}
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => MyAppState();
}
class MyAppState extends State<MyApp> with TickerProviderStateMixin<MyApp> {
double _containerSize = 100.0;
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new SafeArea(
child: new Container(
color: Colors.white,
child: new Column(children: [
new RaisedButton(
child: new Text("Press me to make the square change size!"),
onPressed: () => setState(
() {
if (_containerSize > 299.0)
_containerSize = 100.0;
else
_containerSize += 100.0;
},
),
),
new NotifyingAnimatedSize(
duration: new Duration(seconds: 2),
vsync: this,
child: new Container(
color: Colors.blue,
width: _containerSize,
height: _containerSize,
),
notificationCallback: (state) {
print("State is $state");
},
)
]),
),
),
);
}
}
This is not possible. Widgets have no clue about the size of their children. The only thing they do is apply constraints on them, but that's unrelated to the final size.
Here I repost rmtmckenzie's (credits to him) answer but with null safety. I decided not to edit his answer to offer with his and mine both answers with and without null safety. You can just use in your code the NotifyingAnimatedSize.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
enum SizeChangingStatus {
changing,
done,
}
enum NotifyingRenderAnimatedSizeState {
start,
stable,
changed,
unstable,
}
typedef NotifyingAnimatedSizeCallback = void Function(SizeChangingStatus status);
class NotifyingRenderAnimatedSize extends RenderAligningShiftedBox {
NotifyingRenderAnimatedSize({
required TickerProvider vsync,
required Duration duration,
Duration? reverseDuration,
Curve curve = Curves.linear,
AlignmentGeometry alignment = Alignment.center,
required TextDirection textDirection,
RenderBox? child,
Clip clipBehavior = Clip.hardEdge,
required this.notificationCallback,
})
: _vsync = vsync,
_clipBehavior = clipBehavior,
super(textDirection: textDirection, alignment: alignment, child: child) {
_controller = AnimationController(
vsync: vsync,
duration: duration,
reverseDuration: reverseDuration,
)
..addListener(() {
if (_controller.value != _lastValue) {
markNeedsLayout();
}
});
_animation = CurvedAnimation(
parent: _controller,
curve: curve,
);
}
late final AnimationController _controller;
late final CurvedAnimation _animation;
final SizeTween _sizeTween = SizeTween();
late bool _hasVisualOverflow;
double? _lastValue;
final NotifyingAnimatedSizeCallback notificationCallback;
/// The state this size animation is in.
///
/// See [RenderAnimatedSizeState] for possible states.
#visibleForTesting
NotifyingRenderAnimatedSizeState get state => _state;
NotifyingRenderAnimatedSizeState _state = NotifyingRenderAnimatedSizeState.start;
/// The duration of the animation.
Duration get duration => _controller.duration!;
set duration(Duration value) {
if (value == _controller.duration) {
return;
}
_controller.duration = value;
}
/// The duration of the animation when running in reverse.
Duration? get reverseDuration => _controller.reverseDuration;
set reverseDuration(Duration? value) {
if (value == _controller.reverseDuration) {
return;
}
_controller.reverseDuration = value;
}
/// The curve of the animation.
Curve get curve => _animation.curve;
set curve(Curve value) {
if (value == _animation.curve) {
return;
}
_animation.curve = value;
}
/// {#macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge], and must not be null.
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior = Clip.hardEdge;
set clipBehavior(Clip value) {
if (value != _clipBehavior) {
_clipBehavior = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
}
/// Whether the size is being currently animated towards the child's size.
///
/// See [RenderAnimatedSizeState] for situations when we may not be animating
/// the size.
bool get isAnimating => _controller.isAnimating;
/// The [TickerProvider] for the [AnimationController] that runs the animation.
TickerProvider get vsync => _vsync;
TickerProvider _vsync;
set vsync(TickerProvider value) {
if (value == _vsync) {
return;
}
_vsync = value;
_controller.resync(vsync);
}
#override
void attach(PipelineOwner owner) {
super.attach(owner);
switch (state) {
case NotifyingRenderAnimatedSizeState.start:
case NotifyingRenderAnimatedSizeState.stable:
break;
case NotifyingRenderAnimatedSizeState.changed:
case NotifyingRenderAnimatedSizeState.unstable:
// Call markNeedsLayout in case the RenderObject isn't marked dirty
// already, to resume interrupted resizing animation.
markNeedsLayout();
break;
}
}
#override
void detach() {
_controller.stop();
super.detach();
}
Size? get _animatedSize => _sizeTween.evaluate(_animation);
#override
void performLayout() {
_lastValue = _controller.value;
_hasVisualOverflow = false;
final BoxConstraints constraints = this.constraints;
if (child == null || constraints.isTight) {
_controller.stop();
size = _sizeTween.begin = _sizeTween.end = constraints.smallest;
_state = NotifyingRenderAnimatedSizeState.start;
child?.layout(constraints);
return;
}
child!.layout(constraints, parentUsesSize: true);
switch (_state) {
case NotifyingRenderAnimatedSizeState.start:
_layoutStart();
break;
case NotifyingRenderAnimatedSizeState.stable:
_layoutStable();
break;
case NotifyingRenderAnimatedSizeState.changed:
_layoutChanged();
break;
case NotifyingRenderAnimatedSizeState.unstable:
_layoutUnstable();
break;
}
size = constraints.constrain(_animatedSize!);
alignChild();
if (size.width < _sizeTween.end!.width || size.height < _sizeTween.end!.height) {
_hasVisualOverflow = true;
}
}
#override
Size computeDryLayout(BoxConstraints constraints) {
if (child == null || constraints.isTight) {
return constraints.smallest;
}
// This simplified version of performLayout only calculates the current
// size without modifying global state. See performLayout for comments
// explaining the rational behind the implementation.
final Size childSize = child!.getDryLayout(constraints);
switch (_state) {
case NotifyingRenderAnimatedSizeState.start:
return constraints.constrain(childSize);
case NotifyingRenderAnimatedSizeState.stable:
if (_sizeTween.end != childSize) {
return constraints.constrain(size);
} else if (_controller.value == _controller.upperBound) {
return constraints.constrain(childSize);
}
break;
case NotifyingRenderAnimatedSizeState.unstable:
case NotifyingRenderAnimatedSizeState.changed:
if (_sizeTween.end != childSize) {
return constraints.constrain(childSize);
}
break;
}
return constraints.constrain(_animatedSize!);
}
void _restartAnimation() {
_lastValue = 0.0;
_controller.forward(from: 0.0);
}
/// Laying out the child for the first time.
///
/// We have the initial size to animate from, but we do not have the target
/// size to animate to, so we set both ends to child's size.
void _layoutStart() {
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size);
_state = NotifyingRenderAnimatedSizeState.stable;
}
/// At this state we're assuming the child size is stable and letting the
/// animation run its course.
///
/// If during animation the size of the child changes we restart the
/// animation.
void _layoutStable() {
if (_sizeTween.end != child!.size) {
_sizeTween.begin = size;
_sizeTween.end = debugAdoptSize(child!.size);
_restartAnimation();
_state = NotifyingRenderAnimatedSizeState.changed;
} else if (_controller.value == _controller.upperBound) {
// Animation finished. Reset target sizes.
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size);
notificationCallback(SizeChangingStatus.done);
} else if (!_controller.isAnimating) {
_controller.forward(); // resume the animation after being detached
}
}
/// This state indicates that the size of the child changed once after being
/// considered stable.
///
/// If the child stabilizes immediately, we go back to stable state. If it
/// changes again, we match the child's size, restart animation and go to
/// unstable state.
void _layoutChanged() {
if (_sizeTween.end != child!.size) {
// Child size changed again. Match the child's size and restart animation.
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size);
_restartAnimation();
_state = NotifyingRenderAnimatedSizeState.unstable;
} else {
notificationCallback(SizeChangingStatus.changing);
// Child size stabilized.
_state = NotifyingRenderAnimatedSizeState.stable;
if (!_controller.isAnimating) {
// Resume the animation after being detached.
_controller.forward();
}
}
}
/// The child's size is not stable.
///
/// Continue tracking the child's size until is stabilizes.
void _layoutUnstable() {
if (_sizeTween.end != child!.size) {
// Still unstable. Continue tracking the child.
_sizeTween.begin = _sizeTween.end = debugAdoptSize(child!.size);
_restartAnimation();
} else {
// Child size stabilized.
_controller.stop();
_state = NotifyingRenderAnimatedSizeState.stable;
}
}
#override
void paint(PaintingContext context, Offset offset) {
if (child != null && _hasVisualOverflow && clipBehavior != Clip.none) {
final Rect rect = Offset.zero & size;
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
rect,
super.paint,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer.layer = null;
super.paint(context, offset);
}
}
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
#override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
}
class NotifyingAnimatedSize extends StatefulWidget {
/// Creates a widget that animates its size to match that of its child.
///
/// The [curve] and [duration] arguments must not be null.
const NotifyingAnimatedSize({
required this.child,
this.alignment = Alignment.center,
this.curve = Curves.linear,
required this.duration,
this.reverseDuration,
required this.notificationCallback,
this.clipBehavior = Clip.hardEdge,
});
/// The widget below this widget in the tree.
///
/// {#macro flutter.widgets.ProxyWidget.child}
final Widget child;
/// The alignment of the child within the parent when the parent is not yet
/// the same size as the child.
///
/// The x and y values of the alignment control the horizontal and vertical
/// alignment, respectively. An x value of -1.0 means that the left edge of
/// the child is aligned with the left edge of the parent whereas an x value
/// of 1.0 means that the right edge of the child is aligned with the right
/// edge of the parent. Other values interpolate (and extrapolate) linearly.
/// For example, a value of 0.0 means that the center of the child is aligned
/// with the center of the parent.
///
/// Defaults to [Alignment.center].
///
/// See also:
///
/// * [Alignment], a class with convenient constants typically used to
/// specify an [AlignmentGeometry].
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
/// relative to text direction.
final AlignmentGeometry alignment;
/// The animation curve when transitioning this widget's size to match the
/// child's size.
final Curve curve;
/// The duration when transitioning this widget's size to match the child's
/// size.
final Duration duration;
/// The duration when transitioning this widget's size to match the child's
/// size when going in reverse.
///
/// If not specified, defaults to [duration].
final Duration? reverseDuration;
/// {#macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge], and must not be null.
final Clip clipBehavior;
/// Callback to trigger when animation ends
final NotifyingAnimatedSizeCallback notificationCallback;
#override
State<NotifyingAnimatedSize> createState() => _NotifyingAnimatedSizeState();
}
class _NotifyingAnimatedSizeState extends State<NotifyingAnimatedSize> with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) =>
_NotifyingAnimatedSize(
alignment: widget.alignment,
curve: widget.curve,
duration: widget.duration,
vsync: this,
notificationCallback: widget.notificationCallback,
child: widget.child,
);
}
class _NotifyingAnimatedSize extends SingleChildRenderObjectWidget {
const _NotifyingAnimatedSize({
Key? key,
required Widget child,
this.alignment = Alignment.center,
this.curve = Curves.linear,
required this.duration,
required this.vsync,
required this.notificationCallback,
}) : super(key: key, child: child);
final AlignmentGeometry alignment;
final Curve curve;
final Duration duration;
final TickerProvider vsync;
final NotifyingAnimatedSizeCallback notificationCallback;
#override
NotifyingRenderAnimatedSize createRenderObject(BuildContext context) =>
NotifyingRenderAnimatedSize(
alignment: alignment,
duration: duration,
curve: curve,
vsync: vsync,
textDirection: Directionality.of(context),
notificationCallback: notificationCallback);
#override
void updateRenderObject(BuildContext context, NotifyingRenderAnimatedSize renderObject) {
renderObject
..alignment = alignment
..duration = duration
..curve = curve
..vsync = vsync
..textDirection = Directionality.of(context);
}
}
User the widget like so:
NotifyingAnimatedSize(
duration: const Duration(milliseconds: 200),
notificationCallback: (status) {
if (status == SizeChangingStatus.done) {
//do something
}
},
child: Container(height: 50, width: 50, color: Colors.red),
);