Skip to content

Instantly share code, notes, and snippets.

@loic-sharma
Last active January 2, 2024 17:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save loic-sharma/2b07d54b06f65bf4a240eee13dce0250 to your computer and use it in GitHub Desktop.
Save loic-sharma/2b07d54b06f65bf4a240eee13dce0250 to your computer and use it in GitHub Desktop.
Windows compositor animation regression
// The Windows compositor changes regresses the animation in
// the `material_floating_search_bar_2` package:
//
// https://pub.dev/packages/material_floating_search_bar_2/versions/0.5.0
//
// Below is a minimal repro of the broken animation. This has two pieces:
//
// 1. The background fades to a grey color
// 2. A box is "revealed" using a custom clipper
//
// On framework commit b417fb828b332b0a4b0c80b742d86aa922de2649 this animation is broken on Windows.
// On framework commit 9c2a7560096223090d38bbd5b4c46760be396bc1 this animation works as expected on Windows.
//
// Good gif: https://publish-01.obsidian.md/access/b48ac8ca270cd9dac18c4a64d11b1c02/assets/2023-12-28-compositor_animation_regression_good.gif
// Bad gif: https://publish-01.obsidian.md/access/b48ac8ca270cd9dac18c4a64d11b1c02/assets/2023-12-28-compositor_animation_regression_bad.gif
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Not using `MaterialApp` is necessary to reproduce:
return Container(
color: Colors.white,
child: const Directionality(
textDirection: TextDirection.ltr,
child: FloatingSearchBar(),
),
);
// Switching to `MaterialApp` fixes the issue:
// return const MaterialApp(
// home: Scaffold(
// body: FloatingSearchBar(),
// ),
// );
}
}
class FloatingSearchBar extends StatefulWidget {
const FloatingSearchBar({super.key});
@override
FloatingSearchBarState createState() => FloatingSearchBarState();
}
class FloatingSearchBarState extends State<FloatingSearchBar> with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _animate() {
if (_controller.isDismissed || _controller.status == AnimationStatus.reverse) {
_controller.forward();
} else {
_controller.reverse();
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, _) {
return Stack(
children: <Widget>[
if (!_controller.isDismissed)
FadeTransition(
opacity: _controller,
child: const SizedBox.expand(
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.black26),
),
),
),
_buildSearchBar(),
],
);
},
);
}
Widget _buildSearchBar() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// This is where the search text input would go...
GestureDetector(
onTap: () => _animate(),
child: Text(
switch (_controller.status) {
AnimationStatus.forward || AnimationStatus.completed => 'Click to close',
AnimationStatus.reverse || AnimationStatus.dismissed => 'Click to open',
},
style: const TextStyle(color: Colors.black),
),
),
// Below are where the search results would be. Clicking on the search
// input above reveals the results below.
// Removing this fixes the background's fade transition.
ClipOval(
clipper: _CircularRevealClipper(
fraction: _controller.value,
),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
// Removing this line fixes the background's fade transition.
borderRadius: BorderRadius.circular(16.0),
),
child: const Padding(
padding: EdgeInsets.all(64.0),
child: Text(
'Hello world',
style: TextStyle(color: Colors.black),
),
),
),
),
],
);
}
}
class _CircularRevealClipper extends CustomClipper<Rect> {
const _CircularRevealClipper({required this.fraction});
final double fraction;
@override
Rect getClip(Size size) {
final double halfWidth = size.width * 0.5;
final double maxRadius = sqrt(halfWidth * halfWidth + size.height * size.height);
final double radius = lerpDouble(0.0, maxRadius, fraction) ?? 0;
return Rect.fromCircle(
center: Offset(halfWidth, 0),
radius: radius,
);
}
@override
bool shouldReclip(CustomClipper<Rect> oldClipper) {
if (oldClipper is _CircularRevealClipper) {
return oldClipper.fraction != fraction;
}
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment