Performant blur/opacity in Flutter? - performance

I love the blurry frost effect using a BackdropFilter (see this).
However, because the BackdropFilter has Opacity and because the widget I'm blurring also has Opacity, the performance is horrendous. This is also because I redraw my widgets a few times a second, but that shouldn't be an issue given Flutter can go 60fps?
I turned on checkerboardOffscreenLayers and see checkerboards for miles. :O
The checkerboards happen due to blurScreen, not due to widgetToBlur but widgetToBlur does slow down performance probably because (in my real code, not this example) it's calling setState() multiple times a second.
Is there a more performant way to make blurs/opacities? The link above says to apply opacity to widgets individually. I can't do that with the blur though (blurScreen below), because the BackdropFilter has to be stacked on top of my widget-that-does-the-redrawing.
I removed the blur effect and my performance is way better (no checkerboards, app doesn't crash).
build() code in question:
final widgetToBlur = Container(
child: Opacity(
opacity: 0.3,
// In my actual code, this is a Stateful widget.
child: Text('Spooky blurry semi-transparent text!'),
),
);
final blurScreen = BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
child: Container(
decoration: BoxDecoration(
color: _backgroundColor.withOpacity(0.3),
),
),
);
return Container(
child: Stack(
children: <Widget>[
widgetToBlur,
blurScreen,
Text('This is in front of the blurred background'),
],
),
);

I ended up drawing the widgetToBlur once, blurred, with opacity, using Paint and Canvas.
This means it only runs the blur and opacity operations once, in initState(), and never has to be re-rendered-with-blur throughout the lifecycle of the widget.
If anyone else ends up stuck with this, you can leave a comment and I can help out more.

I have a similar problem and it also boils down to using canvas and paint.
The only problem now is, if I apply a MaskFilter to the image, not much happens despite the enormously high sigma... Only the edge is a little blurred.
Now the question is why is that so? How did you solved this issue?
canvas.drawImageRect(
image,
Offset(0, 0) & srcSize,
Offset(-delta, 0) & dstSize,
Paint()..maskFilter = MaskFilter.blur(
BlurStyle.normal, 100.0
)
);
For those of you who are interested, I have loaded the image in the init function as follows:
rootBundle.load("assets/gift_1.png").then((bd) {
Uint8List lst = new Uint8List.view(bd.buffer);
Ui.instantiateImageCodec(lst).then((codec) {
codec.getNextFrame().then((frameInfo) {
image = frameInfo.image;
});
});
});
P.s. Unfortunately, I can't write any comments yet; therefore here as a contribution, including solution proposal

you can use this. but I do not know speed.
var pr = ui.PictureRecorder();
var canvas = ui.Canvas(pr);
canvas.drawImage(
img,
ui.Offset(0, 0),
ui.Paint()..maskFilter = ui.MaskFilter.blur(
BlurStyle.normal, 100.0
)
);
var pic = pr.endRecording();
return pic.toImage(img.width, img.height);

Related

Pin a one part on a Row to the screen while scroll the complete screen in Flutter

I am trying to build a layout explicit for Windows and Web. If the App becomes to small there will be another Layout.
The basic Idea is this Layout:
The green and the blue part are potential to big for the screen. And the red part is a header with some selection options.
My goal is, that the user can scroll the complete page down to the end of the green part. So that the red header will be disappear through scrolling down, the green part will be continued to be scrollable. So far I'd just use a Column in a SingleChildScrollView.
The tricky part is the blue part. I want it to get scrolled with the header to the top of the screen but then stays pinned there while the green part continues to be scrolled. Ideally it uses the complete height of the screen when the header is out of view.
Has somebody an idea how to achieve this?
Is there a ready solution I just don't know or would I need to exchange the layout when the header disappears?
If it helps. here is the code to make the basic layout:
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
//hides when scrolled down
_buildHeader(),
//some small divider
Container(height: 5, color: Colors.black,),
_mainContent(),
],
),
);
}
Widget _mainContent(){
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//Container on the left is Fixed under header if header visible and uses complete screen if header is not visible
Expanded(child: _buildLeftContainer()),
//a Container with unknown hieght
Expanded(child: _buildRightContainer())
],
);
}
Widget _buildHeader(){
//some Content
return Container(
height: 150,
color: Colors.red,
);
}
Widget _buildRightContainer(){
//could be any height from not shown to 5 times the screens
return Container(
height: 1000,
color: Colors.green,
);
}
Widget _buildLeftContainer(){
// will contain a list of selectables with in specific sized pages
// height is for display, it should use the available space between on the left half of the screen
return Container(
height: 400,
color: Colors.blue,
);
}
Have you tried using Sliver widgets for this?
I haven't done exactly this, but I am pretty sure a combination of SliverList and SliverFillRemaining would work well for this.
You could have a large SliverList to handle the header disappearing, and put all the rest inside a SliverFillRemaining as a child of the large SliverList.
A video about SliverList, SliverGrid and SliverFillRemaining:
https://www.youtube.com/watch?v=k2v3gxtMlDE

How to fit image into a box of all aspect ratio?

I'm dealing with the template png which has a boxed place where I need to fit any size image within that box. How can fit all dimension image to fit in that box in flutter?. I have attached the image.
I need to add image in that black area which is PNG image and what I tried to implement is here.
body: Stack(
children: [
Positioned(
top: MediaQuery.of(context).size.height / 3.5,
right: MediaQuery.of(context).size.width / 10,
child: imagePicked == null
? SizedBox()
: AspectRatio(
aspectRatio: 1,
child: Image.file(imagePicked, fit: BoxFit.fill),
),
),
const Positioned.fill(
child: Align(
alignment: Alignment.centerRight,
child: Image(image: AssetImage('assets/images/notice.png')))),
],
),
But Here I couldn't manage all dimension images. Please help me sort to fit any dimension image into that black box which is also a template image.
The main issue is here overlaying with stack's widgets.
Widget render prioritize bottom to top level on Stack.
In your code-snippet, place-holder is the bottom widget, and it is being prioritized over the selected image.
That's why putting place-holder as 1st widget on stack solve the issue.

Flutter widget test Finder fails because the widget is outside the bounds of the render tree

Question: What controls the bounds of the "render tree" when running widget tests (flutter_test)?
I ask because I am getting an error on very basic button where it can't find the widget because of its vertical offset being outside the bounds of the "render tree" which seems fixed at 800x600.
I get the message:
Warning: A call to tap() with finder "exactly one widget with text "More Info" (ignoring offstage widgets): Text("More Info", dependencies: [MediaQuery, DefaultTextStyle])" derived an Offset (Offset(400.0, 641.8)) that would not hit test on the specified widget.
Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.
Indeed, Offset(400.0, 641.8) is outside the bounds of the root of the render tree, Size(800.0, 600.0).
The finder corresponds to this RenderBox: RenderParagraph#1b6b1 relayoutBoundary=up27
The hit test result at that offset is: HitTestResult(HitTestEntry#b18dd(RenderView#408c3), HitTestEntry#a2393())
#0 WidgetController._getElementPoint (package:flutter_test/src/controller.dart:953:25)
#1 WidgetController.getCenter (package:flutter_test/src/controller.dart:836:12)
#2 WidgetController.tap (package:flutter_test/src/controller.dart:271:18)
#3 main. (file:///Users/tommy/Repos/surveyapp/survey/test/widget_test.dart:99:18)
The size of the render tree in that error message is 800 x 600. (Not sure how that is set or why. It is curious to me that it is exactly 800x600? So, it is being set somewhere. Is it because I have a web project and it defaults to that size for some reason when running a test?). Any widget that is past 600 on the offset height can't be found in the test. The screen runs fine on iOS emulator and on Chrome under flutter web. When you use devtools, widget inspector, there are not any layout issues. It is just a button with ancestors column, padding, center, safearea and scaffold as part of a simple stateful widget page.
(I have had this happening on Fluter version 2.12 or higher.)
I had the same problem when tapping a button during my tests.
I don't know how to change the 800x600 size but I manage to find a way to scroll to the button, thanks to this answer https://stackoverflow.com/a/67990754/992201
Here's the final code :
final Finder buttonToTap = find.byKey(const Key('keyOfTheButton'));
await tester.dragUntilVisible(
buttonToTap, // what you want to find
find.byType(SingleChildScrollView), // widget you want to scroll
const Offset(0, 50), // delta to move
);
await tester.tap(buttonToTap);
await tester.pump();
This issue was resolved for me by using following snippet
await tester.ensureVisible(find.byKey(Key(key)));
await tester.pumpAndSettle();
Note: This answer does not answer the question about the reason for the 800x600 but rather addresses the problem for fellow googlers.
I don't know why this happens (my best guess is some startup animation that is also simulated in the test), however adding await tester.pumpAndSettle() seems to fix the issue (hardening the assumption that this is caused by some animation).
I could not find any issues on the topic in the Flutter GitHub repository unfortunately.
So in summary, I believe the problem isn't caused by the 800x600 constraints but something else.
I had the same problem today.
The problem comes from the margin:
The following code was the button before fixing the problem:
Container(
color: Colors.green,
child: GestureDetector(
key: Key("resetPsw_back_arrow"),
child: Container(
margin: EdgeInsets.only(
top: myPercent(10, screenHeight),
right: (screenWidth - (myPercent(4.27, screenWidth) +
myPercent(13.1, screenWidth)) >
0
? (screenWidth -
(myPercent(4.27, screenWidth) +
myPercent(13.1, screenWidth))
: 0,
),
width: myPercent(4.27, screenWidth),
height: myPercent(4.27, screenWidth),
decoration: BoxDecoration(
color: Colors.red,
image: DecorationImage(
image: AssetImage("lib/media/img/forwardArrowMain.png"),
fit: BoxFit.contain)),
),
onTap: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
),
),
If you look carefully at the animation during the test, you can see that the tester is not taking into account the margin (it not tap on the button but next to it).
So I wrapped the GestureDetector in a container and the problem was solved.
This was the code after the fix:
Container(
margin: EdgeInsets.only(
top: myPercent(6, screenHeight),
right: (screenWidth - (myPercent(4.27, screenWidth) + myPercent(13.1, screenWidth))) > 0
? (screenWidth - (myPercent(4.27, screenWidth) + myPercent(13.1, screenWidth)))
: 0,
),
child: GestureDetector(
key: Key("resetPsw_back_arrow"),
child: Container(
width: myPercent(4.27, screenWidth),
height: myPercent(4.27, screenWidth),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("lib/media/img/forwardArrowMain.png"),
fit: BoxFit.contain)),
),
onTap: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
),
),

animated opacity and hiding the widget so its not clickable

I'm using AnimatedOpacity to hide a the navbar based on a scroll event. I'm specifically having trouble making it animate in and out well, while also making the widget not clickable.
In order to hide my widget and prevent it from being clicked, I conditionally rendering the my CustomNavBar() component or Container() based on the visible state. However, this cuts my animation abruptly when i fade out. It works fading in, however.
AnimatedOpacity(
opacity: _isVisible ? 1.0 : 0.0,
duration: Duration(milliseconds: 300),
child: _isVisible
? CustomNavBar()
: Container(),
)
This is expected, since the component literally is not existing when _isVisible is false. That kills the whole animation. How do I get around this?
Thanks
There may be other ways to do this but you could use two variables for opacity and visibility. Make use of the AnimatedWidget's onEnd callback to change visibility. Also, you may want to use the Visibility widget for this. It lets you even maintain the child's state if you want.
/// somewhere outside of build in a StatefulWidget
bool _isVisible = true;
bool _isOpaque = true;
/// within the build method
AnimatedOpacity(
opacity: _isOpaque ? 1.0 : 0.0,
onEnd: (){
if(!_isOpaque)
setState((){
_isVisible = false;
});
},
child: Visibility(
child: child,
visible: _isVisible,
),
);
//then in some method to toggle the state...
setState((){
if(!_isVisible){
_isVisible = true;
_isOpaque = true;
}
else{
_isOpaque = false;
}
})
Come to think of it, you also have AnimatedSwitcher which is a LOT easier to do this. I will leave the above code for reference to the onEnd callback. Also, if maintaining state in the child widget is important then the above code would be better suited since you have that option. If it's not important then switch-away.
AnimatedSwitcher(
child: _isVisible ? child : SizedBox.shrink(),
)
IgnorePointer(
child: AnimatedOpacity(
child: child,
opacity: visible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
),
ignoring: !visible,
)
This is working for me like a charm. Basically, it says "ignore hit testing while child is invisible.
I also had a case for AppBar's particular action which I wanted to show / hide with animated opacity. After realizing that Opacity widget leaves child clickable I ended up using AnimatedSwitcher for the action Widget:
AnimatedSwitcher(
duration: Duration(milliseconds: 500),
child: !_visible
? SizedBox()
: IconButton(
icon: ... // more of the related code
),
)
I purposely negate the condition to put the empty SizedBox() to the top for the readability as the main widget can take a lot of lines in the code.
You can use your CustomNavBar instead of my IconButton:
AnimatedSwitcher(
duration: Duration(milliseconds: 500),
child: !_visible
? SizedBox()
: CustomNavBar(
... // more of the related code
),
)

fluttercandies extended_image. How do I zoom in on an image with double tap of 1 finger instead of needing 2?

https://github.com/fluttercandies/extended_image
https://github.com/fluttercandies/extended_image/tree/master/example
https://github.com/fluttercandies/extended_image/blob/master/example/lib/pages/zoom_image_demo.dart
fluttercandies extended_image. How do I zoom in on an image with double tap of 1 finger instead of needing 2? How do I get a sliding image to navigator pop back when it slides off the page?
Trying to implement a zoom/pan image with double tap by 1 finger instead of the need for 2 to expand zoom an image. Have two issues with this code and was wondering if anyone has any ideas. The class is very simple with just 2 strings: image & title passed into it.
1, I need the image to expand on double tap. I would like the user to have to power to expand the image with one finger and not two. Think I need to put this code near the very end.
The good thing is that once it is expanded the double tap works to reduce the image size. How do I get it to do the opposite when it is at normal size?
2, the sliding of the image off the page results in a black screen. Thankfully, this doesn’t freeze or crash the app but it leaves the user with a blank screen and the need to press the system back button. I would like the slide to result in a navigator pop back to the original screen.
Firstly, here’s a sample code of how I’m passing an image and a title into expandimage.dart.
FlatButton(
            child: Image.asset(_kAsset5),
            onPressed: () async {
              Navigator.push(
                context,
                MaterialPageRoute(
                    builder: (context) => ExpandImage(
                          image: _kAsset5,
                          title: "\'go help\' 1",
                        )),
              );
            },
          ),
Here’s the code that I’m using for this ‘expandimage.dart’ and a lot of it is based on the pan/zoom example from flutter candies / extended image example.
import 'package:flutter/material.dart';
import 'package:extended_image/extended_image.dart';
class ExpandImage extends StatelessWidget {
  final String image, title;
  ExpandImage({this.image, this.title});
  #override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.red[900],
        appBar: AppBar(
          backgroundColor: Colors.red[900],
          leading: IconButton(
            icon: Icon(Icons.close),
            onPressed: Navigator.of(context).pop,
          ),
          title: Text(
            title,
            style: TextStyle(
              color: Colors.yellow,
              inherit: true,
              fontWeight: FontWeight.w300,
              fontStyle: FontStyle.italic,
              letterSpacing: 2.0, //1.2
            ),
          ),
          centerTitle: true,
        ),
        body: SizedBox.expand(
          // child: Hero(
          // tag: heroTag,
          child: ExtendedImageSlidePage(
            slideAxis: SlideAxis.both,
            slideType: SlideType.onlyImage,
            child: ExtendedImage(
              //disable to stop image sliding off page && entering dead end without back button.
//setting to false means it won't slide at all.
              enableSlideOutPage: true,
              mode: ExtendedImageMode.gesture,
              initGestureConfigHandler: (state) => GestureConfig(
                minScale: 1.0,
                animationMinScale: 0.8,
                maxScale: 3.0,
                animationMaxScale: 3.5,
                speed: 1.0,
                inertialSpeed: 100.0,
                initialScale: 1.0,
                inPageView: false,
              ),
              // onDoubleTap: ? zoom in on image
              fit: BoxFit.scaleDown,
              image: AssetImage(
                image,
              ),
            ),
          ),
        ),
      ),
    );
  }
}
Here is a sample image passed in. the page turns red when sliding the image around and then it goes black as the image slides off the page.
Hope it will help, if have any doubt ask in the comments.
class _DetailState extends State<Detail> with TickerProviderStateMixin{
#override
Widget build(BuildContext context) {
AnimationController _animationController = AnimationController(duration: Duration(milliseconds: 200),vsync: this);
Function() animationListener = () {};
Animation? _animation;
return Scaffold(
body: Container(
height: MediaQuery.of(context).size.height,
child: ExtendedImage.network(
widget.wallpaper.path,
fit: BoxFit.contain,
mode: ExtendedImageMode.gesture,
initGestureConfigHandler: (state) {
return GestureConfig(
minScale: 0.9,
animationMinScale: 0.7,
maxScale: 3.0,
animationMaxScale: 3.5,
speed: 1.0,
inertialSpeed: 100.0,
initialScale: 1.0,
inPageView: false,
initialAlignment: InitialAlignment.center,
);
},
onDoubleTap: (ExtendedImageGestureState state) {
///you can use define pointerDownPosition as you can,
///default value is double tap pointer down postion.
var pointerDownPosition = state.pointerDownPosition;
double begin = state.gestureDetails!.totalScale!;
double end;
_animation?.removeListener(animationListener);
_animationController.stop();
_animationController.reset();
if (begin == 1) {
end = 1.5;
} else {
end = 1;
}
animationListener = () {
//print(_animation.value);
state.handleDoubleTap(
scale: _animation!.value,
doubleTapPosition: pointerDownPosition);
};
_animation = _animationController
.drive(Tween<double>(begin: begin, end: end));
_animation!.addListener(animationListener);
_animationController.forward();
},
),
),
}
}
If it helps, mark it as right.

Resources