-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Closed
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listc: renderingUI glitches reported at the engine/skia or impeller rendering levelUI glitches reported at the engine/skia or impeller rendering levele: device-specificOnly manifests on certain devicesOnly manifests on certain devicese: impellerImpeller rendering backend issues and features requestsImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.flutter/engine related. See also e: labels.platform-androidAndroid applications specificallyAndroid applications specificallyr: fixedIssue is closed as already fixed in a newer versionIssue is closed as already fixed in a newer versionslimpellerEngine binary size reduction. go/slimpellerEngine binary size reduction. go/slimpellerteam-engineOwned by Engine teamOwned by Engine teamtriaged-engineTriaged by Engine teamTriaged by Engine team
Description
Steps to reproduce
- Launch the app with Impeller rendering enabled (reproduced on Samsung S25 Ultra)
- On the main screen, scroll up and down repeatedly
- Open the second page
Expected results
Absence of visual glitches, matching the behaviour observed with Impeller disabled
Actual results
UI glitches occur while scrolling or overscrolling, and also when opening the second screen
Code sample
Code sample
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) {
return MaterialApp(
title: 'Impeller Glitch Reproduction',
theme: ThemeData(primarySwatch: Colors.blue),
home: const ChatPage(),
);
}
}
class ChatPage extends StatefulWidget {
const ChatPage({super.key});
@override
State<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
final List<MessageData> messages = [];
final ScrollController _scrollController = ScrollController();
final Set<String> _placedItems = {};
@override
void initState() {
super.initState();
for (var i = 0; i < 50; i++) {
_addMessage();
}
}
void _addMessage() {
final newMessage = MessageData(
id: 'msg_${messages.length}',
text: 'Message ${messages.length + 1}',
isFromUser: messages.length % 2 == 0,
);
setState(() {
messages.insert(0, newMessage);
});
}
void _onItemPlaced(String itemId) {
setState(() {
_placedItems.add(itemId);
});
}
void _navigateToBlurScreen() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => const BlurEffectsScreen(),
transitionDuration: const Duration(milliseconds: 800),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return Stack(
children: [
BackdropFilter(
filter: ImageFilter.blur(
sigmaX: animation.value * 15,
sigmaY: animation.value * 15,
),
child: Container(color: Colors.black.withValues(alpha: animation.value * 0.3)),
),
SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: child),
),
],
);
},
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey,
appBar: AppBar(
title: const Text('Impeller Glitch Test'),
actions: [IconButton(icon: const Icon(Icons.blur_on), onPressed: _navigateToBlurScreen)],
),
body: Stack(
children: [
// Essential shader mask with gradient
Padding(
padding: const EdgeInsets.only(top: 48, bottom: 60),
child: ShaderMask(
shaderCallback: (bounds) => LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black,
Colors.black.withValues(alpha: 0.6),
Colors.black.withValues(alpha: 0.2),
Colors.black.withValues(alpha: 0),
],
stops: const [0, 0.5, 0.65, 0.9],
).createShader(Rect.fromLTWH(0, 0, bounds.width, 130)),
blendMode: BlendMode.dstOut,
child: ShaderMask(
shaderCallback: (bounds) => LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black,
Colors.black.withValues(alpha: 0.6),
Colors.black.withValues(alpha: 0.2),
Colors.black.withValues(alpha: 0),
],
stops: const [0, 0.5, 0.65, 0.9],
).createShader(Rect.fromLTRB(0, bounds.height - 24, bounds.width, bounds.height)),
blendMode: BlendMode.dstOut,
child: ListView.builder(
controller: _scrollController,
reverse: true,
padding: const EdgeInsets.only(top: 154, bottom: 60),
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
final isPlaced = _placedItems.contains(message.id);
final isLast = index == 0;
if (!isPlaced && mounted) {
Future.delayed(Duration(milliseconds: 100 + (index * 50)), () {
if (mounted) _onItemPlaced(message.id);
});
}
return MessageBubble(
key: ValueKey(message.id),
message: message,
maxWidth: MediaQuery.of(context).size.width * 0.75,
placed: isPlaced,
last: isLast,
);
},
),
),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _addMessage,
child: const Icon(Icons.add),
),
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
class BlurEffectsScreen extends StatefulWidget {
const BlurEffectsScreen({super.key});
@override
State<BlurEffectsScreen> createState() => _BlurEffectsScreenState();
}
class _BlurEffectsScreenState extends State<BlurEffectsScreen> with TickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: const Duration(seconds: 2), vsync: this)..repeat();
_animation = Tween<double>(begin: 0, end: 1).animate(_controller);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Essential backdrop filter that triggers glitches
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 15 + (_animation.value * 10),
sigmaY: 15 + (_animation.value * 10),
),
child: Container(color: Colors.white.withValues(alpha: 0.1)),
);
},
),
Center(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.auto_awesome, color: Colors.white, size: 60),
const SizedBox(height: 20),
const Text(
'Blur Effects Screen',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Back to Chat'),
),
],
),
);
},
),
),
Positioned(
top: MediaQuery.of(context).padding.top + 10,
right: 16,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 30),
onPressed: () => Navigator.of(context).pop(),
),
),
],
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
class MessageData {
final String id;
final String text;
final bool isFromUser;
MessageData({required this.id, required this.text, required this.isFromUser});
}
class MessageBubble extends StatefulWidget {
const MessageBubble({
required this.message,
required this.maxWidth,
required this.placed,
required this.last,
super.key,
});
final MessageData message;
final double maxWidth;
final bool placed;
final bool last;
@override
State<MessageBubble> createState() => _MessageBubbleState();
}
class _MessageBubbleState extends State<MessageBubble> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
lowerBound: widget.placed ? 1 : 0.000001,
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = _animationController.drive(CurveTween(curve: Curves.linearToEaseOut));
Future.delayed(const Duration(milliseconds: 100)).whenComplete(() {
if (mounted) {
if (!widget.placed) {
_animationController.forward();
} else {
_animationController.value = 1;
}
}
});
}
@override
Widget build(BuildContext context) {
final alignment = widget.message.isFromUser ? Alignment.centerRight : Alignment.centerLeft;
final color = widget.message.isFromUser
? Colors.blue.withValues(alpha: 0.9)
: Colors.grey.shade200;
return Align(
alignment: alignment,
child: AnimatedBuilder(
animation: _animation,
builder: (context, _) {
return Padding(
padding: EdgeInsets.only(top: 5, bottom: widget.last ? 20 * _animation.value : 0),
child: FittedBox(
fit: BoxFit.scaleDown,
child: SizeTransition(
sizeFactor: _animation,
child: ScaleTransition(
scale: _animation,
alignment: alignment,
child: FadeTransition(
opacity: _animation,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 0.5, sigmaY: 0.5),
child: _BubbleContent(
message: widget.message,
color: color,
maxWidth: widget.maxWidth,
),
),
),
),
),
),
);
},
),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
class _BubbleContent extends StatelessWidget {
const _BubbleContent({required this.message, required this.color, required this.maxWidth});
final MessageData message;
final Color color;
final double maxWidth;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
right: message.isFromUser ? 15 : 10,
left: message.isFromUser ? 14 : 15,
),
child: ClipPath(
clipper: BubbleClipper(fromRobot: !message.isFromUser),
child: DecoratedBox(
decoration: BoxDecoration(color: color),
child: Container(
constraints: BoxConstraints(maxWidth: maxWidth * 0.8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text(
message.text,
style: TextStyle(color: message.isFromUser ? Colors.white : Colors.black),
),
),
),
),
);
}
}
class BubbleClipper extends CustomClipper<Path> {
const BubbleClipper({required this.fromRobot});
final bool fromRobot;
@override
Path getClip(Size size) {
final path = Path();
const radius = 20.0;
const tailSize = 8.0;
if (fromRobot) {
path.addRRect(
RRect.fromLTRBAndCorners(
tailSize,
0,
size.width,
size.height,
topLeft: const Radius.circular(radius),
topRight: const Radius.circular(radius),
bottomLeft: const Radius.circular(radius),
bottomRight: const Radius.circular(radius),
),
);
path.moveTo(0, size.height - tailSize);
path.lineTo(tailSize, size.height - tailSize);
path.lineTo(tailSize, size.height);
path.close();
} else {
path.addRRect(
RRect.fromLTRBAndCorners(
0,
0,
size.width - tailSize,
size.height,
topLeft: const Radius.circular(radius),
topRight: const Radius.circular(radius),
bottomLeft: const Radius.circular(radius),
bottomRight: const Radius.circular(radius),
),
);
path.moveTo(size.width - tailSize, size.height - tailSize);
path.lineTo(size.width - tailSize, size.height);
path.lineTo(size.width, size.height - tailSize);
path.close();
}
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}Screenshots or Video
Screenshots / Video demonstration
Impeller disabled:telegram-cloud-document-2-5433647737025362042.mp4
Impeller enabled
telegram-cloud-document-2-5433647737025362045.mp4
Logs
Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.32.5, on macOS 15.5 24F74 darwin-arm64, locale en-RU)
[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
[!] Xcode - develop for iOS and macOS (Xcode 16.4)
! CocoaPods 1.15.0 out of date (1.16.2 is recommended).
CocoaPods is a package manager for iOS or macOS platform code.
Without CocoaPods, plugins will not work on iOS or macOS.
For more info, see https://flutter.dev/to/platform-plugins
To update CocoaPods, see
https://guides.cocoapods.org/using/getting-started.html#updating-cocoapods
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.3)
[✓] Android Studio (version 2024.1)
[✓] Android Studio (version 2024.2.2)
[✓] IntelliJ IDEA Community Edition (version 2024.2.3)
[✓] VS Code (version 1.101.2)
[✓] Connected device (2 available)
[✓] Network resourceszarkasy and wosika
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listc: renderingUI glitches reported at the engine/skia or impeller rendering levelUI glitches reported at the engine/skia or impeller rendering levele: device-specificOnly manifests on certain devicesOnly manifests on certain devicese: impellerImpeller rendering backend issues and features requestsImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.flutter/engine related. See also e: labels.platform-androidAndroid applications specificallyAndroid applications specificallyr: fixedIssue is closed as already fixed in a newer versionIssue is closed as already fixed in a newer versionslimpellerEngine binary size reduction. go/slimpellerEngine binary size reduction. go/slimpellerteam-engineOwned by Engine teamOwned by Engine teamtriaged-engineTriaged by Engine teamTriaged by Engine team
Type
Projects
Status
Done