Flutter: How to add BackdropFilter to SliverAppBar - flutter

I want to add a BackdropFilter() to a SliverAppbar().
I want it to look something like the iOS App Library App Bar: https://cln.sh/eP8wfY.
Header sliver not floating over the list in a NestedScrollView does so but only to the header, I want the title and the actions to be visible while the background is blurred.
Thanks!
Edit
What the pages look like: https://cln.sh/vcCY4j.
Github Gist with my code: https://gist.github.com/HadyMash/21e7bd2f7e202de02837505e1c7363e9.

NOTE: getting hard time on color, even after spending hours of time.
you need to change colors
of you find some area problem that maybe because of safeArea or CupertinoNavBar.
you can remove/change color on shadow, i'm giving too much on test purpose.
All you have to play with Colors and LinearGradient
OutPut
Here is my concept:
Stack
- backgroundImage
- Container with white.3
- CustomScrollView
- SliverToBoxAdapter 2x kToolbarHeight for extra height for GridList,
- SliverGrid
- LinearGradient 2xkToolbarHeight for fadeEffect on upper scroll
- our widget TextField or anything
Demo
class Body extends StatelessWidget {
const Body({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) => Stack(
children: [
Container(
decoration: BoxDecoration(
// color: Colors.white.withOpacity(.3),
image: DecorationImage(
image: AssetImage("assets/me.jpg"),
fit: BoxFit.cover,
),
),
child: Container(),
),
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(.3),
),
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: SizedBox(
height: kToolbarHeight * 2,
),
),
SliverPadding(
padding: EdgeInsets.all(20),
sliver: SliverGrid.count(
crossAxisCount: 2,
mainAxisSpacing: 20,
crossAxisSpacing: 20,
children: [
...List.generate(
12,
(index) => Container(
decoration: BoxDecoration(
color: index % 3 == 0
? Colors.deepPurple
: index % 3 == 1
? Colors.deepOrange
: Colors.amberAccent,
borderRadius: BorderRadius.circular(12),
),
))
],
),
)
],
),
),
Align(
alignment: Alignment.topCenter,
child: Container(
height: kToolbarHeight * 2,
width: constraints.maxWidth,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.grey,
Colors.white.withOpacity(.7),
],
),
),
child: Text(""),
),
),
Positioned(
top: kTextTabBarHeight * 1.122,
/// need to tweek
left: 20,
right: 20,
child: Container(
height: kToolbarHeight,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white70,
boxShadow: [
BoxShadow(
blurRadius: 12,
spreadRadius: 6,
color: Colors.black54,
offset: Offset(0, 12))
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () {
print("boosm");
},
child: Text("Tap")),
],
),
),
),
],
),
),
);
}
}

TL;DR I fixed the problem by using a normal AppBar() since I didn't need a SliverAppBar(). I made a custom app bar to fix the problem (see code at the end of the question).
I realised I didn't need a SilverAppBar() because it will just stay floating and pinned. This made my life a whole lot easier since I could use an AppBar() and set the extendBodyBehindAppBar to true in the Scaffold(). This made it so that I wouldn't have to make a custom sliver widget as I am not familiar with making them.
My solution was to make a custom AppBar(). I would have a Stack() then put the blur effect and the AppBar() above it.
https://github.com/flutter/flutter/issues/48212 shows that you can't use ShaderMasks() with BackdropFilter()s. To work around this I made a column with a bunch of BackdropFilter()s. They would have decreasing sigma values to create the gradient effect I was looking for. This isn't very performant, however, and in heavier apps wouldn't work well. Making each block have the length of a single logical pixel was too heavy so I made it 2 logical pixels.
It can also be easily expanded, for example, by adding a fade effect as I did.
Here is what the result looks like.
Before: https://cln.sh/vcCY4j
After:
https://cln.sh/vNZVJW (Includes the opacity effect (0.8))
https://cln.sh/jfH7OP (Reduced opacity effect (0.5))
https://cln.sh/nXzPLe (No opacity effect)
Here is the code for the solution:
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
class BlurredAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Widget>? actions;
/// An `AppBar()` which has a blur effect behind it which fades in to hide it
/// until content appears behind it. This has a similar effect to the iOS 14
/// App Library app bar. It also has the possibility of having a fade effect to
/// redude the opacity of widgets behind the `BlurredAppBar()` using a `LinearGradient()`.
const BlurredAppBar({required this.title, this.actions, Key? key})
: super(key: key);
/// The height of the `AppBar()`
final double height = 56;
/// Returns a `List<Widget>` of `BackdropFilter()`s which have decreasing blur values.
/// This will create the illusion of a gradient blur effect as if a `ShaderMask()` was used.
List<Widget> _makeBlurGradient(double height, MediaQueryData mediaQuery) {
List<Widget> widgets = [];
double length = height + mediaQuery.padding.top;
for (int i = 1; i <= (length / 2); i++) {
widgets.add(
ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: max(((length / 2) - i.toDouble()) / 2, 0),
sigmaY: min(5, max(((length / 2) - i.toDouble()) / 2, 0)),
),
child: SizedBox(
height: 2,
width: mediaQuery.size.width,
),
),
),
);
}
return widgets;
}
#override
Widget build(BuildContext context) {
final MediaQueryData mediaQuery = MediaQuery.of(context);
return Stack(
children: [
// BackdropFilters
SizedBox(
height: height + mediaQuery.padding.top,
child: Column(
children: _makeBlurGradient(height, mediaQuery),
),
),
// Fade effect.
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: [0.5, 1],
colors: [
Colors.white.withOpacity(0.8),
Colors.white.withOpacity(0),
],
),
),
),
// AppBar
AppBar(
title: Text(
title,
style: Theme.of(context).textTheme.headline3,
),
automaticallyImplyLeading: false,
actions: actions,
),
],
);
}
#override
Size get preferredSize => Size.fromHeight(height);
}

SliverAppBar(
primary: false,
toolbarHeight: kToolbarHeight * 1.5,
floating: true,
snap: true,
backgroundColor: Colors.black45,
titleSpacing: 0.0,
title: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 36.0, sigmaY: 36.0),
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: kToolbarHeight * 1.5,
),
),
)
You can change toolbarHeight to your liking, but the child of the BackdropFilter must have the same height.

Related

How to split a container up into various colors in Flutter?

I want to split a container into multiple colors at various different points (which can change dynamically).
I get this effect almost by using LinearGradient - however, I want sharp borders (e.g. no blending of colors when they change). How can I achieve this?
final size = MediaQuery.of(context).size;
var colors = [Colors.red, Colors.blue, Colors.green, Colors.orange];
var stops = [0.0, 0.15, 0.60, 1.00];
return SizedBox(
child: Container(
width: size.width,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: colors,
stops: stops,
),
),
),
);
you can use this to have a box with different colors:
final Size size = MediaQuery.of(context).size;
final List<double> stops = <double>[0, 0.15, 0.60, 1];
final List<Color> colors = <Color>[
Colors.red,
Colors.blue,
Colors.green,
Colors.orange,
];
return SizedBox(
width: size.width,
child: Row(
children: List<Widget>.generate(
colors.length,
(int index) => Container(
width: size.width *
(stops[index] - (index == 0 ? 0 : stops[index - 1])),
color: colors[index],
),
),
),
);
If you just want to display your colors with no gradient, here is a way:
class MyWidget extends StatelessWidget {
const MyWidget();
#override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
flex: 7,
child: Container(
color: Colors.red,
),
),
Expanded(
flex: 31,
child: Container(
color: Colors.blue,
),
),
Expanded(
flex: 42,
child: Container(
color: Colors.green,
),
),
Expanded(
flex: 20,
child: Container(
color: Colors.orange,
),
),
],
);
}
}
The result:
You can make it more dynamic by creating the row from a list.
Changing the flex values with change the size of the corresponding color.

ListTile inside ListView is causing an error

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/weather_provider.dart';
class BottomListView extends StatelessWidget {
const BottomListView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final weatherData = Provider.of<WeatherProvider>(context).weatherData;
final isLandscape =
MediaQuery.of(context).orientation == Orientation.landscape;
final height = (MediaQuery.of(context).size.height -
50 -
MediaQuery.of(context).padding.top);
return Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(width: 0.3, color: Colors.white),
),
color: Color.fromRGBO(255, 255, 255, 0.2),
),
height: isLandscape ? height * 0.35 : null,
child: Row(
children: [
Expanded(
child: SizedBox(
width: 200,
child: ListView(
scrollDirection: Axis.horizontal,
children: [ListTile(title: Text('Hello'))]),
),
),
],
));
}
}
I want to have horizontal scrollable listView with ListTiles. But without ListTile it works fine . With ListTile it is causing an error.How to solve it? I tried giving it the width but didn't work.
Error:
BoxConstraints forces an infinite width.
These invalid constraints were provided to RenderParagraph's layout() function by the following function, which probably computed the invalid constraints in question:
ListTiles needs to be defined with width params explicitly using SizedBox or Container.
If you dont define width, it will throw infinite width error.
So Wrap your ListTile inside a SizedBox or Container.
Since, ListView's scrollDirection is set to Horizontal, you dont need to place it inside of a row i.e. it will show children horizontally.
If scrollDirection is set to vertical, it will show children in a Column i.e. Vertically
Also you can't do both, Use expanded and give width to a child of a row.
Using Expanded means the child will take maximum size available to it w.r.t to its parent.
Try the below code snippet
Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(width: 0.3, color: Colors.white),
),
color: Color.fromRGBO(255, 255, 255, 0.2),
),
// height: isLandscape ? height * 0.35 : null,
height: 100,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
SizedBox(
width: 200,
child: ListTile(
title: Text('Hello'),
),
),
SizedBox(
width: 200,
child: ListTile(
title: Text('Hello'),
),
),
SizedBox(
width: 200,
child: ListTile(
title: Text('Hello'),
),
),
SizedBox(
width: 200,
child: ListTile(
title: Text('Hello'),
),
),
],
),
),

When using Flutter Container, everything ok but no ripple effect - but Ink draws beyond its parent

I want to provide a few buttons inside a singlechildscrollview
Column(
children: < Widget > [
SizedBox(height: constraints.maxHeight / 8.0),
AnimationConfiguration.staggeredList(
position: 1,
duration: const Duration(milliseconds: 2000),
child: SlideAnimation(
verticalOffset: constraints.maxHeight / 10,
child: FadeInAnimation(
child: Image.asset('images/mylive.png'),
),
),
),
Flexible(
child: Padding(
padding: EdgeInsets.fromLTRB(
50, 20, 50, constraints.maxHeight / 7),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(10),
child: Wrap(
spacing: 25,
runSpacing: 25,
children: const < Widget > [
ButtonCard(
name: "My News",
imgpath: "open-email.png",
count: 0),
and this is the build method for the ButtonCard:
Widget build(BuildContext context) {
final double width = MediaQuery.of(context).size.width;
final double height = MediaQuery.of(context).size.height;
return InkWell(
onTap: () {},
child: Container( <<--->> Ink(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.white,
boxShadow: const [
BoxShadow(
color: Colors.black38,
offset: Offset(0, 2),
blurRadius: 7,
),
],
),
child: Column(
children: [
Stack(
children: [
Image.asset(
"assets/images/$imgpath",
width: 60,
),
],
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
),
],
),
),
);
}
When I use the container in ButtonCard, then everything is okay, but the InkWell does not show the ripple effect (because of the BoxDecation color set)
This results in the following, correct scroll view:
But when I change the Container to Ink - I get the beautiful ripple effect, which I want to have. But then the following error occurs during scolling:
As you can see, the Ink with its boxdecoration paints over the parents border. Is this a bug in Ink or does anyone know what the problem is here? Thanks!
In General cases:
Wrap the Container with Inkwell
Wrap the Inkwell with Material
Show needed color with Material
Set color to the Container as transparent
With the above settings, you can have a ripple effect with Inkwell. But very difficult to achieve when you're having gradient colors.
Ref: https://flutteragency.com/inkwell-not-showing-ripple-effect-in-flutter/
You must have Material -> Inkwell -> Ink

stack children is overflowing on image

I clip to hardedge in stack even after this black container is overflowing how can i fix this if i do fit: BoxFit.fill the image get stretched. i tried wrapping the stack in a container or card and then clip to haredge that didnt work too
here is the code i tried
Stack(
children: [
Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
image: DecorationImage(
image: AssetImage(widget.image), fit: BoxFit.fill),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: 43,
width: 162,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.35),
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(15),bottomRight: Radius.circular(15)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(widget.info,style: RecentChats.likedetails,),
Padding(
padding: const EdgeInsets.only(left: 3),
child: Container(
height: 4,
width: 4,
decoration: const BoxDecoration(
color: DatingColoCodes.blue,
shape: BoxShape.circle,
),
),
),
],
),
Text(widget.view,style: RecentChats.viewprofile,),
],
)),
)
],
);
Can't reproduce with the given snippet, it works just fine on me when I run it, maybe try to put the complete snippet?
But anyways, you can achieve the same behavior without the Stack widget, just put the Texts as a child of the Container whose hold the image.
Here is the working example, you can copy paste and run in DartPad
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: const EdgeInsets.all(30),
child: GridView.count(
crossAxisCount: 2,
crossAxisSpacing: 20,
mainAxisSpacing: 8,
childAspectRatio: 0.8,
children: [
for (int i=0; i<20; i++)
Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(20),
image: DecorationImage(
image: NetworkImage('https://source.unsplash.com/random'),
fit: BoxFit.cover,
),
),
/// For some reason, GridView / Container are stretching
/// the height, forcing it to be in full height so we gotta
/// do some hack, we put a Column, and then put the Container
/// with the texts inside the Column as well and make the height
/// as height as the item height by using `MainAxisSize.min`.
///
/// The first Column is to position the Container inside it to
/// the bottom of the item (see: `MainAxisAlignment.end`) and force
/// the width to be full width by using `CrossAxisAlignment.stretch`
///
/// Lastly, we do a `Clip.hardEdge` to prevent the text container
/// to apear sharp-squared and follow the roundness of the parent
/// container instead.
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
color: Colors.red.withOpacity(0.70),
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Hello item $i'),
Text('Washington DC'),
],
),
),
],
),
),
],
),
),
);
}
}
Sidenote: when I say "forcing it to be full height", it means adding height property will simply just not work, that is why I put a column instead.

How to avoid the conflict between the blur effect of Backdrop and the splash effect of TabBar in Flutter?

class Sample extends StatelessWidget {
List<Tab> get tabs => ["Intro", "Flow", "Market"]
.map((e) => Tab(
text: e,
))
.toList(growable: false);
#override
Widget build(BuildContext context) {
return Layout(
title: "Title",
body: CustomScrollView(
slivers: [
_buildHeader(),
_buildStickyBar(),
],
),
);
}
Widget _buildHeader() {
return SliverToBoxAdapter(
child: Stack(
children: [
Positioned(child: Image.assets("some.jpg"), top: 40, bottom: -20, right: -20),
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 40.0, sigmaY: 40.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.white.withOpacity(0.06),
),
child: Text("effect")),
),
],
),
);
}
Widget _buildStickyBar() {
return SliverPersistentHeader(
pinned: true,
floating: true,
delegate: SliverPersistentHeaderEx(//some simple impl
minHeight: 32,
maxHeight: 32,
child: TabBar(
controller: tabController, //it's not the point
tabs: tabs),
),
);
}
}
The theme used is a black theme, and the splash is white.
So, the blur effect and splash effect will conflict. In detail, the splash effect of TabBar will spread to the upper widget(stack, in this case). If you remove withOpacity, the blur effect will be lost, but the diffusion problem will be solved.
I want to keep those effects. How can I limit the splash effect of the TaBbar to only appear in the TabBar?
effect
As documentation says:
The filter will be applied to all the area within its parent or
ancestor widget's clip. If there's no clip, the filter will be applied
to the full screen.
Try to wrap your BackdropFilter with ClipRect
ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 40.0, sigmaY: 40.0),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.white.withOpacity(0.06),
),
child: Text("effect")),
),
),