-
Notifications
You must be signed in to change notification settings - Fork 29.2k
Closed
Closed
Copy link
Labels
P1High-priority issues at the top of the work listHigh-priority issues at the top of the work liste: impellerImpeller rendering backend issues and features requestsImpeller rendering backend issues and features requeststeam-engineOwned by Engine teamOwned by Engine teamtriaged-engineTriaged by Engine teamTriaged by Engine team
Description
Steps to reproduce
- Run the following code with Impeller:
code
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.blue,
),
home: const ZoomViewMvp(),
);
}
}
class ZoomViewMvp extends StatefulWidget {
const ZoomViewMvp({super.key});
@override
State<ZoomViewMvp> createState() => _ZoomViewMvpState();
}
class _ZoomViewMvpState extends State<ZoomViewMvp> {
final controller = ScrollController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: ZoomView(
controller: controller,
child: ListView.builder(
controller: controller,
itemBuilder: (context, index) {
return Center(child: Text("simple text ${index.toString()}"));
},
)
),
);
}
}
///Allows a ListView or other Scrollables that implement ScrollPosition and
///jumpTo(offset) in their controller to be zoomed and scrolled.
class ZoomView extends StatefulWidget {
const ZoomView({
super.key,
required this.child,
required this.controller,
this.maxScale = 4.0,
this.minScale = 1.0,
this.scrollAxis = Axis.vertical,
});
final Widget child;
final ScrollController controller;
///scrollAxis must be set to Axis.horizontal if the Scrollable is horizontal
final Axis scrollAxis;
///The maximum scale that the ZoomView can be zoomed to. Set to double.infinity to allow infinite zoom in
final double maxScale;
///The minimum scale that the ZoomView can be zoomed to. Set to 0 to allow infinite zoom out
final double minScale;
@override
State<ZoomView> createState() => _ZoomViewState();
}
class _ZoomViewState extends State<ZoomView> with TickerProviderStateMixin {
@override
void initState() {
if (widget.scrollAxis == Axis.vertical) {
_verticalController = widget.controller;
_horizontalController = ScrollController();
} else {
_verticalController = ScrollController();
_horizontalController = widget.controller;
}
super.initState();
}
///The current scale of the ZoomView
double _scale = 1;
///The scale of the ZoomView before the last scale update event
double _lastScale = 1;
late final ScrollController _verticalController;
late final ScrollController _horizontalController;
///The focal point of pointers at the start of a scale event
late Offset _localFocalPoint;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double height = constraints.maxHeight;
double width = constraints.maxWidth;
return GestureDetector(
behavior: HitTestBehavior.translucent,
onScaleStart: (ScaleStartDetails details) {
if (details.pointerCount != 1) {
_localFocalPoint = details.localFocalPoint;
}
},
onScaleUpdate: (ScaleUpdateDetails details) {
if (details.pointerCount > 1) {
final newScale = _lastScale / details.scale;
final verticalOffset =
_verticalController.position.pixels + (_scale - newScale) * _localFocalPoint.dy;
final horizontalOffset = _horizontalController.position.pixels +
(_scale - newScale) * _localFocalPoint.dx;
//This is the main logic to actually perform the scaling
setState(() {
_scale = newScale;
});
_verticalController.jumpTo(verticalOffset);
_horizontalController.jumpTo(horizontalOffset);
} else {
final correctedDelta = details.focalPointDelta * _scale;
_verticalController.jumpTo(_verticalController.position.pixels - correctedDelta.dy);
_horizontalController.jumpTo(_horizontalController.position.pixels - correctedDelta.dx);
}
},
onScaleEnd: (ScaleEndDetails details) {
_lastScale = _scale;
},
child: Column(
children: [
Expanded(
//When scale decreases, the SizedBox will shrink and the FittedBox
//will scale the child to fit the maximum constraints of the ZoomView
child: FittedBox(
fit: BoxFit.fill,
child: SizedBox(
height: height * _scale,
width: width * _scale,
child: Center(
child: ScrollConfiguration(
behavior: const ScrollBehavior().copyWith(
overscroll: false,
//Disable all inputs on the list as we will handle them
//ourselves using the gesture detector and scroll controllers
dragDevices: <PointerDeviceKind>{},
scrollbars: false,
),
child: ListView(
physics: const ClampingScrollPhysics(),
controller: widget.scrollAxis == Axis.vertical
? _horizontalController
: _verticalController,
scrollDirection: widget.scrollAxis == Axis.vertical
? Axis.horizontal
: Axis.vertical,
children: [
SizedBox(
width: widget.scrollAxis == Axis.vertical ? width : null,
height: widget.scrollAxis == Axis.vertical ? null : height,
child: widget.child,
),
],
),
),
),
),
),
),
],
),
);
},
);
}
}
Expected results
Text is not jittery and is smooth like skia
Actual results
the text jitters and is jagged
Code sample
See above
Screenshots or Video
Screenshots / Video demonstration
Impeller:
screen-20250320-134229.mp4
Skia:
screen-20250320-134348.mp4
I should note skia looked even smoother on device. I think the video encoding might be making it look more jagged there.
another example
screen-20250320-140112.mp4
Logs
Logs
[Paste your logs here]
Flutter Doctor output
Doctor output
[✓] Flutter (Channel master, 3.31.0-1.0.pre.155)
• No issues found!
Metadata
Metadata
Assignees
Labels
P1High-priority issues at the top of the work listHigh-priority issues at the top of the work liste: impellerImpeller rendering backend issues and features requestsImpeller rendering backend issues and features requeststeam-engineOwned by Engine teamOwned by Engine teamtriaged-engineTriaged by Engine teamTriaged by Engine team