how to make profile appbar like telegram in flutter? - flutter

Since i am new to flutter i want the complete implementation of this animation.

/// Flutter code sample for SliverAppBar
// This sample shows a [SliverAppBar] and it's behavior when using the
// [pinned], [snap] and [floating] parameters.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
/// This is the main application widget.
class MyApp extends StatelessWidget {
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget();
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool _pinned = true;
bool _snap = false;
bool _floating = false;
// [SliverAppBar]s are typically used in [CustomScrollView.slivers], which in
// turn can be placed in a [Scaffold.body].
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
pinned: _pinned,
snap: _snap,
floating: _floating,
expandedHeight: 160.0,
flexibleSpace: const FlexibleSpaceBar(
title: Text('SliverAppBar'),
background: FlutterLogo(),
),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 20,
child: Center(
child: Text('Scroll to see the SliverAppBar in effect.'),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: index.isOdd ? Colors.white : Colors.black12,
height: 100.0,
child: Center(
child: Text('$index', textScaleFactor: 5),
),
);
},
childCount: 20,
),
),
],
),
bottomNavigationBar: BottomAppBar(
child: ButtonBar(
alignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
children: <Widget>[
const Text('pinned'),
Switch(
onChanged: (bool val) {
setState(() {
_pinned = val;
});
},
value: _pinned,
),
],
),
Row(
children: <Widget>[
const Text('snap'),
Switch(
onChanged: (bool val) {
setState(() {
_snap = val;
// Snapping only applies when the app bar is floating.
_floating = _floating || _snap;
});
},
value: _snap,
),
],
),
Row(
children: <Widget>[
const Text('floating'),
Switch(
onChanged: (bool val) {
setState(() {
_floating = val;
_snap = _snap && _floating;
});
},
value: _floating,
),
],
),
],
),
),
);
}
}
Source - https://api.flutter.dev/flutter/material/SliverAppBar-class.html

This is exact implementation of this AppBar:
class TransitionAppBar extends StatelessWidget {
TransitionAppBar({
required this.avatar,
required this.title,
this.extent = 250,
Key? key,
}) : super(key: key);
final Widget avatar;
final double extent;
final String title;
#override
Widget build(BuildContext context) {
return SliverPersistentHeader(
pinned: true,
delegate: _TransitionAppBarDelegate(
avatar: avatar,
title: title,
extent: extent > 200 ? extent : 200,
),
);
}
}
class _TransitionAppBarDelegate extends SliverPersistentHeaderDelegate {
_TransitionAppBarDelegate({required this.avatar, required this.title, this.extent = 250})
: assert(extent >= 200, '');
final Widget avatar;
final double extent;
final String title;
final _avatarAlignTween = AlignmentTween(begin: Alignment.bottomCenter, end: Alignment.topLeft);
final _avatarMarginTween = EdgeInsetsTween(
end: EdgeInsets.only(left: 14, top: 36),
);
final _iconAlignTween = AlignmentTween(begin: Alignment.bottomRight, end: Alignment.topRight);
final _titleMarginTween = EdgeInsetsTween(
begin: EdgeInsets.only(bottom: 20),
end: EdgeInsets.only(left: 64, top: 45),
);
#override
double get maxExtent => extent;
#override
double get minExtent => 80;
#override
bool shouldRebuild(_TransitionAppBarDelegate oldDelegate) {
return avatar != oldDelegate.avatar || title != oldDelegate.title;
}
#override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
final tempVal = maxExtent * 72 / 100;
final progress = shrinkOffset > tempVal ? 1.0 : shrinkOffset / tempVal;
final avatarMargin = _avatarMarginTween.lerp(progress);
final titleMargin = _titleMarginTween.lerp(progress);
final avatarAlign = _avatarAlignTween.lerp(progress);
final iconAlign = _iconAlignTween.lerp(progress);
final avatarSize = (1 - progress) * 200 + 32;
return Stack(
children: <Widget>[
AnimatedContainer(
duration: Duration(milliseconds: 100),
height: 80,
constraints: BoxConstraints(maxHeight: minExtent),
color: Colors.white,
),
Padding(
padding: avatarMargin,
child: Align(
alignment: avatarAlign,
child: SizedBox(
height: avatarSize,
width: avatarSize,
child: avatar,
),
),
),
Align(
alignment: Alignment.bottomRight,
child: Container(
height: progress < 0.4 ? 100 * (1 - progress) * 1.5 : 0,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.pink[200]!.withOpacity(0.05),
Colors.pink[400]!.withOpacity(0.8),
],
),
),
),
),
Padding(
padding: titleMargin,
child: Align(
alignment: avatarAlign,
child: Text(
title,
),
),
),
Padding(
padding: titleMargin,
child: Align(
alignment: iconAlign,
child: Padding(
padding: const EdgeInsets.only(right: 16),
child: GestureDetector(
onTap: () {},
child: Icon(
Icons.search,
size: 30,
color: progress < 0.4 ? Colors.white : Colors.black,
),
),
),
),
),
],
);
}
}
Also this answer might be helpful

Related

How to separate different widgets alongside another widget?

This is my problem:
I want to achieve this design:
How can I separate one to each other, first the homePageTimerUI and below the countDownTimer
?
This is my code:
homePageTimerUI.dart
class HomePageTimerUI extends StatefulWidget {
const HomePageTimerUI({Key? key}) : super(key: key);
#override
State<HomePageTimerUI> createState() => _HomePageTimerUIState();
}
class _HomePageTimerUIState extends State<HomePageTimerUI> {
#override
Widget build(BuildContext context) {
return SizedBox(
height: double.maxFinite,
width: double.infinity,
child: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(40),
child: Container(
color: Colors.transparent,
child: SafeArea(
child: Column(
children: <Widget>[
TabBar(
indicator: const UnderlineTabIndicator(
borderSide: BorderSide(
color: Color(0xff3B3B3B), width: 4.0),
insets:
EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 11.0)),
indicatorWeight: 15,
indicatorSize: TabBarIndicatorSize.label,
labelColor: const Color(0xff3B3B3B),
labelStyle: const TextStyle(
fontSize: 12,
letterSpacing: 1.3,
fontWeight: FontWeight.w500),
unselectedLabelColor: const Color(0xffD7D7D7),
tabs: const [
Tab(
text: "POMODORO",
icon: Icon(Icons.work_history, size: 40),
),
Tab(
text: "SHORT BREAK",
icon: Icon(Icons.ramen_dining, size: 40),
),
Tab(
text: "LONG BREAK",
icon: Icon(Icons.battery_charging_full_rounded,
size: 40),
),
])
],
),
),
),
),
),
body: TabBarView(
children: const <Widget>[
// Center(
// child: StartPomodoro(),
// ),
// Center(
// child: ShortBreak(),
// ),
// Center(child: LongBreak()),
],
),
),
),
);
}
}
countDownTimer.dart
class CountDownTimer extends StatefulWidget {
#override
_CountDownTimerState createState() => _CountDownTimerState();
}
class _CountDownTimerState extends State<CountDownTimer>
with TickerProviderStateMixin {
late AnimationController controller;
String get timerString {
Duration duration = controller.duration! * controller.value;
return '${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}';
}
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 5),
);
}
#override
Widget build(BuildContext context) {
ThemeData themeData = Theme.of(context);
return Scaffold(
backgroundColor: Colors.white10,
body: AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Stack(
children: <Widget>[
Align(
alignment: Alignment.bottomCenter,
child: Container(
color: Colors.amber,
height:
controller.value * MediaQuery.of(context).size.height,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Align(
alignment: FractionalOffset.center,
child: Stack(
children: <Widget>[
CustomPaint(
painter: CustomTimerPainter(
animation: controller,
backgroundColor: Colors.white,
color: themeData.indicatorColor,
)),
Align(
alignment: FractionalOffset.center,
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
crossAxisAlignment:
CrossAxisAlignment.center,
children: <Widget>[
Text(
timerString,
style: TextStyle(
fontSize: 112.0,
color: Colors.white),
),
],
),
),
],
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AnimatedBuilder(
animation: controller,
builder: (context, child) {
return FloatingActionButton.extended(
onPressed: () {
if (controller.isAnimating)
controller.stop();
else {
controller.reverse(
from: controller.value == 0.0
? 1.0
: controller.value);
}
},
icon: Icon(controller.isAnimating
? Icons.pause
: Icons.play_arrow),
label: Text(
controller.isAnimating ? "Pause" : "Play"));
}),
],
),
],
),
),
],
);
}),
);
}
}
class CustomTimerPainter extends CustomPainter {
CustomTimerPainter({
required this.animation,
required this.backgroundColor,
required this.color,
}) : super(repaint: animation);
final Animation<double> animation;
final Color backgroundColor, color;
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = backgroundColor
..strokeWidth = 10.0
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke;
canvas.drawCircle(size.center(Offset.zero), size.width / 2.0, paint);
paint.color = color;
double progress = (1.0 - animation.value) * 2 * math.pi;
canvas.drawArc(Offset.zero & size, math.pi * 1.5, -progress, false, paint);
}
#override
bool shouldRepaint(CustomTimerPainter old) {
return animation.value != old.animation.value ||
color != old.color ||
backgroundColor != old.backgroundColor;
}
}
This is my own attempt to solve the issue, but didn't work
class StackedPages extends StatelessWidget {
const StackedPages({super.key});
#override
Widget build(BuildContext context) {
return Column(
children: [
HomePageTimerUI(),
CountDownTimer(),
],
);
}
}
How can achieve this design?
Thank you for any help you can offer
Are you asking for Stack() widget?
You want to replace one for another?
You could use AnimatedSwitcher().
AnimatedSwitcher(
duration: const Duration(seconds: 1), child: MyWidget());
Where MyWidget() is asigned to the widget you want to show via SetState(). The widgets are switched with a nice animation.
If you need to have one Widget placed one above the other in the vertical axis, you should try to remove the scaffold and the SizedBox with infity values.
if this class can't stack.
Just using static type.
add these codes at the same file with CountDownTimer.
static const Widget countDownTimer = CountDownTimer();
...
// class CountDownTimer
Then, you must import this file and call this widget by this parameter.
example
#override
Widget build(BuildContext context) {
return Column(
children: [
HomePageTimerUI(),
countDownTimer,
],
);
}
but you don't need to use StackPages just put countDownTimer in your pages.

Hero widget transition conflict with other animations

I'm trying to achieve the Hero & shake upright animation result attached gif here.
This is the result I got so far.
Seems like the Hero widget conflicts with the animation I've applied. It seems to be working for the 200 & 300 card. But when 100 is tapped, it seems to be working differently. Attached below are the code for the demo above.
Tried using WidgetsBinding.instance.addPostFrameCallback and SchedulerBinding.instance.addPersistentFrameCallback.
Is there any way to get the expected result instead of using the code I've used?
dummy_data.dart
class _DummyData {
final IconData icons;
final Color colors;
final String backText;
const _DummyData(this.icons, this.colors, this.backText);
}
const List<_DummyData> _datas = [
_DummyData(Icons.abc, Colors.blue, '100'),
_DummyData(Icons.alarm, Colors.red, '200'),
_DummyData(Icons.shop, Colors.green, '300'),
];
playground_list.dart
class PlayGroundList extends StatelessWidget {
const PlayGroundList({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: CustomScrollView(
slivers: [
const SliverToBoxAdapter(child: SizedBox(height: 50)),
SliverList(
delegate: SliverChildBuilderDelegate(
childCount: _datas.length,
(context, index) => PlayGroundCardWidget(dummy: _datas[index]),
),
),
],
),
);
}
}
playground_card_widget.dart
class PlayGroundCardWidget extends StatelessWidget {
final _DummyData dummy;
const PlayGroundCardWidget({super.key, required this.dummy});
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 1.5.h),
height: 350,
child: GestureDetector(
onTap: () => Navigator.push(
context,
PageRouteBuilder(
transitionsBuilder: (context, animation, _, child) =>
FadeTransition(
opacity: Tween(
begin: 0.0,
end: 1.0,
).chain(CurveTween(curve: Curves.ease)).animate(animation),
child: child,
),
pageBuilder: (context, _, __) => PlayGroundDetail(dummy: dummy),
),
),
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Hero(
tag: dummy.colors.value,
child: Material(color: dummy.colors),
),
),
Align(
alignment: Alignment.topCenter,
child: Hero(
tag: dummy.backText,
child: Material(
color: Colors.transparent,
child: Text(
dummy.backText,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black,
fontSize: 140.0,
),
),
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 3.h),
Expanded(
flex: 12,
child: Hero(
tag: dummy.icons,
child: Icon(dummy.icons, size: 60.0),
),
),
],
),
],
),
),
);
}
}
playground_detail.dart
const _shakeDuration = Duration(milliseconds: 900);
class PlayGroundDetail extends StatefulWidget {
final _DummyData dummy;
const PlayGroundDetail({super.key, required this.dummy});
#override
State<PlayGroundDetail> createState() => _PlayGroundDetailState();
}
class _PlayGroundDetailState extends State<PlayGroundDetail>
with TickerProviderStateMixin {
late final PageController _pageController;
#override
void initState() {
super.initState();
_pageController = PageController();
}
#override
void dispose() {
super.dispose();
_pageController.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
CustomScrollView(
slivers: [
const SliverToBoxAdapter(child: SizedBox(height: 50)),
SliverToBoxAdapter(
child: SizedBox(
height: 350,
child: Stack(
children: [
Positioned.fill(
child: Hero(
tag: widget.dummy.colors.value,
child: Material(color: widget.dummy.colors),
),
),
Align(
alignment: Alignment.topCenter,
child: Hero(
tag: widget.dummy.backText,
child: Material(
color: Colors.transparent,
child: ShakeTransitionWidget(
axis: Axis.vertical,
duration: _shakeDuration,
offset: 30,
child: Text(
widget.dummy.backText,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black,
fontSize: 140.0,
),
),
),
),
),
),
Center(
child: Hero(
tag: widget.dummy.icons,
child: ShakeTransitionWidget(
axis: Axis.vertical,
offset: 5,
duration: _shakeDuration,
child: Icon(widget.dummy.icons, size: 60.0),
),
),
),
],
),
),
),
],
),
],
),
);
}
}
shake_transition_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class ShakeTransitionWidget extends StatefulWidget {
final Widget child;
final Duration duration;
final double offset;
final Axis axis;
const ShakeTransitionWidget({
super.key,
required this.child,
required this.duration,
required this.offset,
required this.axis,
});
#override
State<ShakeTransitionWidget> createState() => _ShakeTransitionWidgetState();
}
class _ShakeTransitionWidgetState extends State<ShakeTransitionWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation _animation;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: widget.duration,
);
_animation = Tween(
begin: 1.0,
end: 0.0,
).chain(CurveTween(curve: Curves.elasticOut)).animate(_controller);
// Tried this.
// WidgetsBinding.instance.addPostFrameCallback((_) => _controller.forward());
if (SchedulerBinding.instance.schedulerPhase ==
SchedulerPhase.persistentCallbacks) {
_controller.forward();
}
SchedulerBinding.instance.addPersistentFrameCallback((timeStamp) {});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (ctx, _) => Transform.translate(
offset: widget.axis == Axis.horizontal
? Offset(_animation.value * widget.offset, 0.0)
: Offset(0.0, _animation.value * widget.offset),
child: widget.child,
),
);
}
}
Wrap your Hero widget with ShakeTransitionWidget instead of the other way around:
// ... Hero is the child, not the parent
ShakeTransitionWidget(
axis: Axis.vertical,
duration: _shakeDuration,
offset: 30,
child: Hero(
tag: widget.dummy.backText,
// ...
Now inside the initState() of _ShakeTransitionWidgetState simply call _controller.forward() without any SchedulerBinding or WidgetsBinding.
As a rule of thumb, usually the Hero and its children should not change from page to page. Instead add modifications to the parent widgets.
See gif.

How to zoom an item of a list on mouse-over, keeping it always visible (Flutter on Web platform)

The problem is described like this.
In a web environment, I have to build a horizontal list of images (like in Netflix) which should increase the size of the element when the user positions the mouse cursor over them. To achieve this, I'm using a Stack (with clipBehavior equals to Clip.none) to render each item in the list, when I detect the mouse-over event I add a new Container (larger than the size of the original item) to draw an AnimatedContainer inside which will grow to fill it.
The animation works great, but the container gets positioned down to the next right item on the list, however, I need it above the item.
Here is the code:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
final double zoomTargetHeight = 320;
final double zoomTargetWidth = 500;
final double zoomOriginalHeight = 225;
final double zoomOriginalWidth = 400;
double _zoomHeight = 225;
double _zoomWidth = 400;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: SingleChildScrollView(
child: Column(
children: [
Image.network("https://source.unsplash.com/random/1600x900?cars"),
Container(
color: Colors.black87,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 12,
),
const Text(
"List of items",
style: TextStyle(color: Colors.white),
),
const SizedBox(
height: 12,
),
SizedBox(
height: 235,
child: ListView.separated(
clipBehavior: Clip.none,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return buildCard(index);
},
separatorBuilder: (context, index) {
return const SizedBox(
width: 12,
);
},
itemCount: 10,
),
),
const SizedBox(
height: 200,
),
],
),
),
),
],
),
),
);
}
Map _showZoom = {};
Widget buildCard(int index) {
Stack stack = Stack(
clipBehavior: Clip.none,
children: [
MouseRegion(
onEnter: (event) {
setState(() {
_showZoom["$index"] = true;
});
},
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
Image.network(
"https://source.unsplash.com/random/400x225?sig=$index&cars"),
Container(
color: Colors.black.withAlpha(100),
height: zoomOriginalHeight,
width: zoomOriginalWidth,
),
],
),
),
),
if (_showZoom["$index"] != null && _showZoom["$index"]!)
Positioned(
left: (zoomOriginalWidth - zoomTargetWidth) / 2,
top: (zoomOriginalHeight - zoomTargetHeight) / 2,
child: MouseRegion(
onHover: (_) {
setState(() {
_zoomHeight = zoomTargetHeight;
_zoomWidth = zoomTargetWidth;
});
},
onExit: (event) {
setState(() {
_showZoom["$index"] = false;
_zoomHeight = zoomOriginalHeight;
_zoomWidth = zoomOriginalWidth;
});
},
child: SizedBox(
width: zoomTargetWidth,
height: zoomTargetHeight,
child: Center(
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
width: _zoomWidth,
height: _zoomHeight,
// color: Colors.green.withAlpha(100),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
image: DecorationImage(
image: NetworkImage(
"https://source.unsplash.com/random/400x225?sig=$index&cars"),
fit: BoxFit.cover,
),
),
),
),
),
),
),
],
);
return stack;
}
}
Remember flutter config --enable-web
I think this is precisely what you are looking for (Check also the live demo on DartPad):
The solution is:
Use an outer Stack that wraps the ListView;
Add another ListView in front of it in the Stack with the same number of items and same item sizes;
Then, ignore the pointer-events with IgnorePointer on this new ListView so the back one will receive the scroll/tap/click events;
Synchronize the scroll between the back ListView and the front one by listening to scroll events with NotificationListener<ScrollNotification>;
Here's the code
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
final double zoomTargetHeight = 320;
final double zoomTargetWidth = 500;
final double zoomOriginalHeight = 225;
final double zoomOriginalWidth = 400;
late final ScrollController _controllerBack;
late final ScrollController _controllerFront;
#override
void initState() {
super.initState();
_controllerBack = ScrollController();
_controllerFront = ScrollController();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
Image.network("https://source.unsplash.com/random/1600x900?cars"),
Container(
color: Colors.black87,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 12,
),
const Text(
"List of items",
style: TextStyle(color: Colors.white),
),
const SizedBox(
height: 12,
),
SizedBox(
height: 225,
child: NotificationListener<ScrollNotification>(
onNotification: (notification) {
_controllerFront.jumpTo(_controllerBack.offset);
return true;
},
child: Stack(
clipBehavior: Clip.none,
children: [
ListView.separated(
controller: _controllerBack,
clipBehavior: Clip.none,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return buildBackCard(index);
},
separatorBuilder: (context, index) {
return const SizedBox(
width: 12,
);
},
itemCount: 10,
),
IgnorePointer(
child: ListView.separated(
controller: _controllerFront,
clipBehavior: Clip.none,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return buildFrontCard(index);
},
separatorBuilder: (context, index) {
return const SizedBox(
width: 12,
);
},
itemCount: 10,
),
),
],
),
),
),
const SizedBox(
height: 200,
),
],
),
),
),
],
),
),
);
}
final Map _showZoom = {};
Widget buildBackCard(int index) {
return MouseRegion(
onEnter: (event) {
setState(() {
_showZoom["$index"] = true;
});
},
onExit: (event) {
setState(() {
_showZoom["$index"] = false;
});
},
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
Image.network(
"https://source.unsplash.com/random/400x225?sig=$index&cars",
),
Container(
color: Colors.black.withAlpha(100),
height: zoomOriginalHeight,
width: zoomOriginalWidth,
),
],
),
),
);
}
Widget buildFrontCard(int index) {
Widget child;
double scale;
if (_showZoom["$index"] == null || !_showZoom["$index"]!) {
scale = 1;
child = SizedBox(
height: zoomOriginalHeight,
width: zoomOriginalWidth,
);
} else {
scale = zoomTargetWidth / zoomOriginalWidth;
child = Stack(
clipBehavior: Clip.none,
children: [
Container(
height: zoomOriginalHeight,
width: zoomOriginalWidth,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
image: DecorationImage(
image: NetworkImage(
"https://source.unsplash.com/random/400x225?sig=$index&cars"),
fit: BoxFit.cover,
),
),
),
],
);
}
return AnimatedScale(
duration: const Duration(milliseconds: 400),
scale: scale,
child: child,
);
}
}
I'd do something different. Instead of Stacking the zoomed-out and zoomed-in images it could be just one image with a AnimatedScale to do the transitions.
Check the code below:
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
AnimatedScale(
duration: const Duration(milliseconds: 400),
scale: _showZoom["$index"] == true
? zoomTargetWidth / zoomOriginalWidth
: 1,
child: Image.network(
"https://source.unsplash.com/random/400x225?sig=$index&cars"),
),
if (_showZoom["$index"] == null || _showZoom["$index"] == false)
Container(
color: Colors.black.withAlpha(100),
height: zoomOriginalHeight,
width: zoomOriginalWidth,
),
],
),
),
Check out the screenshot and the live demo on DartPad:
All source
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
final double zoomTargetHeight = 320;
final double zoomTargetWidth = 500;
final double zoomOriginalHeight = 225;
final double zoomOriginalWidth = 400;
double _zoomHeight = 225;
double _zoomWidth = 400;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
Image.network("https://source.unsplash.com/random/1600x900?cars"),
Container(
color: Colors.black87,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 12,
),
const Text(
"List of items",
style: TextStyle(color: Colors.white),
),
const SizedBox(
height: 12,
),
SizedBox(
height: 235,
child: ListView.separated(
clipBehavior: Clip.none,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return buildCard(index);
},
separatorBuilder: (context, index) {
return const SizedBox(
width: 12,
);
},
itemCount: 10,
),
),
const SizedBox(
height: 200,
),
],
),
),
),
],
),
),
);
}
Map _showZoom = {};
Widget buildCard(int index) {
Stack stack = Stack(
clipBehavior: Clip.none,
children: [
MouseRegion(
onEnter: (event) {
setState(() {
_showZoom["$index"] = true;
});
},
onExit: (event) {
setState(() {
_showZoom["$index"] = false;
});
},
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
AnimatedScale(
duration: const Duration(milliseconds: 400),
scale: _showZoom["$index"] == true
? zoomTargetWidth / zoomOriginalWidth
: 1,
child: Image.network(
"https://source.unsplash.com/random/400x225?sig=$index&cars"),
),
if (_showZoom["$index"] == null || _showZoom["$index"] == false)
Container(
color: Colors.black.withAlpha(100),
height: zoomOriginalHeight,
width: zoomOriginalWidth,
),
],
),
),
),
],
);
return stack;
}
}

How to remove padding from Flutter built-in stepper widget

I want to remove the padding of Flutter's stepper widget in order to create control buttons that don't have any space between them and the horizontal edges of the screen.
What I've tried:
I've found this similar question, where the answer says the only
way to do that would be to create my own version of the stepper
widget, but I did not understand what that person meant by that: should I try
to create from scratch a copy of the built-in widget and adjust it to
fit my needs? It seems like too much time and effort only to change a
small detail like this.
Also, I tried to modify the padding of the built-in
stepper widget (stepper.dart), navigating to it's source code and,
inside the _buildHorizontal() function, changing the value of the
padding property from EdgeInsets.all(24) to EdgeInsets.all(0). It
worked, but would it be a good idea to do this? Modify a bundled
flutter widget?
Furthermore, I've managed to bypass this padding restriction
surrounding the buttons with the "UnconstrainedBox" widget. The only
problem is that, as expected, the child (buttons) overflow it's
parent (the stepper), causing those overflow stripes to be shown
during development. Would it be bad if I left this overflow happen?
Here's my code where the problem appears:
import 'package:flutter/material.dart';
class TestStepperScreen extends StatefulWidget {
const TestStepperScreen({Key? key}) : super(key: key);
#override
_TestStepperScreenState createState() => _TestStepperScreenState();
}
class _TestStepperScreenState extends State<TestStepperScreen> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Icons.arrow_back_ios_new),
),
title: Text(
'Test stepper',
),
),
body: Container(
child: Stepper(
margin: EdgeInsets.all(0),
type: StepperType.horizontal,
controlsBuilder: (BuildContext context, ControlsDetails details) {
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: ElevatedButton(
onPressed: null,
child: Text('Cancel'),
),
),
Expanded(
child: ElevatedButton(
onPressed: null,
child: Text('Continue'),
),
),
],
),
);
},
steps: [
Step(
title: SizedBox(),
content: Column(
children: [
Container(
child: Text('test'),
),
],
),
),
Step(
title: SizedBox(),
content: Column(
children: [
Container(),
],
),
),
Step(
title: SizedBox(),
content: Column(
children: [
Container(),
],
),
),
],
),
),
);
}
}
And here's my code using the UnconstrainedBox (desired behaviour):
import 'package:flutter/material.dart';
class TestStepperScreen extends StatefulWidget {
const TestStepperScreen({Key? key}) : super(key: key);
#override
_TestStepperScreenState createState() => _TestStepperScreenState();
}
class _TestStepperScreenState extends State<TestStepperScreen> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Icons.arrow_back_ios_new),
),
title: Text(
'Test stepper',
),
),
body: Container(
child: Stepper(
margin: EdgeInsets.all(0),
type: StepperType.horizontal,
controlsBuilder: (BuildContext context, ControlsDetails details) {
return UnconstrainedBox(
child: Container(
width: MediaQuery.of(context).size.width,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: ElevatedButton(
onPressed: null,
child: Text('Cancel'),
),
),
Expanded(
child: ElevatedButton(
onPressed: null,
child: Text('Continue'),
),
),
],
),
),
);
},
steps: [
Step(
title: SizedBox(),
content: Column(
children: [
Container(
child: Text('test'),
),
],
),
),
Step(
title: SizedBox(),
content: Column(
children: [
Container(),
],
),
),
Step(
title: SizedBox(),
content: Column(
children: [
Container(),
],
),
),
],
),
),
);
}
}
Any kind of help would be highly appreciated. Thank you for your time.
you can customize the whole stepper widget to delete the padding, follow these steps:
create dart file called custom_stepper for example
put the code at the bottom here in this file(note that it's very big code)
use the CustomStepper widget not Stepper
the result, the next image with Stepper:
the next image with CustomStepper:
import 'package:flutter/material.dart';
typedef ControlsWidgetBuilder = Widget Function(
BuildContext context, ControlsDetails details);
const TextStyle _kStepStyle = TextStyle(
fontSize: 12.0,
color: Colors.white,
);
const Color _kErrorLight = Colors.red;
final Color _kErrorDark = Colors.red.shade400;
const Color _kCircleActiveLight = Colors.white;
const Color _kCircleActiveDark = Colors.black87;
const Color _kDisabledLight = Colors.black38;
const Color _kDisabledDark = Colors.white38;
const double _kStepSize = 24.0;
const double _kTriangleHeight =
_kStepSize * 0.866025;
class CustomStepper extends StatefulWidget {
const CustomStepper({
Key? key,
required this.steps,
this.physics,
this.type = StepperType.vertical,
this.currentStep = 0,
this.onStepTapped,
this.onStepContinue,
this.onStepCancel,
this.controlsBuilder,
this.elevation,
this.margin,
})
: assert(steps != null),
assert(type != null),
assert(currentStep != null),
assert(0 <= currentStep && currentStep < steps.length),
super(key: key);
final List<Step> steps;
final ScrollPhysics? physics;
final StepperType type;
final int currentStep;
final ValueChanged<int>? onStepTapped;
final VoidCallback? onStepContinue;
final VoidCallback? onStepCancel;
final ControlsWidgetBuilder? controlsBuilder;
final double? elevation;
final EdgeInsetsGeometry? margin;
#override
State<CustomStepper> createState() => _CustomStepperState();
}
class _CustomStepperState extends State<CustomStepper>
with TickerProviderStateMixin {
late List<GlobalKey> _keys;
final Map<int, StepState> _oldStates = <int, StepState>{};
#override
void initState() {
super.initState();
_keys = List<GlobalKey>.generate(
widget.steps.length,
(int i) => GlobalKey(),
);
for (int i = 0; i < widget.steps.length; i += 1)
_oldStates[i] = widget.steps[i].state;
}
#override
void didUpdateWidget(CustomStepper oldWidget) {
super.didUpdateWidget(oldWidget);
assert(widget.steps.length == oldWidget.steps.length);
for (int i = 0; i < oldWidget.steps.length; i += 1)
_oldStates[i] = oldWidget.steps[i].state;
}
bool _isFirst(int index) {
return index == 0;
}
bool _isLast(int index) {
return widget.steps.length - 1 == index;
}
bool _isCurrent(int index) {
return widget.currentStep == index;
}
bool _isDark() {
return Theme
.of(context)
.brightness == Brightness.dark;
}
Widget _buildLine(bool visible) {
return Container(
width: visible ? 1.0 : 0.0,
height: 16.0,
color: Colors.grey.shade400,
);
}
Widget _buildCircleChild(int index, bool oldState) {
final StepState state =
oldState ? _oldStates[index]! : widget.steps[index].state;
final bool isDarkActive = _isDark() && widget.steps[index].isActive;
assert(state != null);
switch (state) {
case StepState.indexed:
case StepState.disabled:
return Text(
'${index + 1}',
style: isDarkActive
? _kStepStyle.copyWith(color: Colors.black87)
: _kStepStyle,
);
case StepState.editing:
return Icon(
Icons.edit,
color: isDarkActive ? _kCircleActiveDark : _kCircleActiveLight,
size: 18.0,
);
case StepState.complete:
return Icon(
Icons.check,
color: isDarkActive ? _kCircleActiveDark : _kCircleActiveLight,
size: 18.0,
);
case StepState.error:
return const Text('!', style: _kStepStyle);
}
}
Color _circleColor(int index) {
final ColorScheme colorScheme = Theme
.of(context)
.colorScheme;
if (!_isDark()) {
return widget.steps[index].isActive
? colorScheme.primary
: colorScheme.onSurface.withOpacity(0.38);
} else {
return widget.steps[index].isActive
? colorScheme.secondary
: colorScheme.background;
}
}
Widget _buildCircle(int index, bool oldState) {
return Container(
width: _kStepSize,
height: _kStepSize,
child: AnimatedContainer(
curve: Curves.fastOutSlowIn,
duration: kThemeAnimationDuration,
decoration: BoxDecoration(
color: _circleColor(index),
shape: BoxShape.circle,
),
child: Center(
child: _buildCircleChild(
index, oldState && widget.steps[index].state == StepState.error),
),
),
);
}
Widget _buildTriangle(int index, bool oldState) {
return Container(
width: _kStepSize,
height: _kStepSize,
child: Center(
child: SizedBox(
width: _kStepSize,
height:
_kTriangleHeight,
child: CustomPaint(
painter: _TrianglePainter(
color: _isDark() ? _kErrorDark : _kErrorLight,
),
child: Align(
alignment: const Alignment(
0.0, 0.8),
child: _buildCircleChild(index,
oldState && widget.steps[index].state != StepState.error),
),
),
),
),
);
}
Widget _buildIcon(int index) {
if (widget.steps[index].state != _oldStates[index]) {
return AnimatedCrossFade(
firstChild: _buildCircle(index, true),
secondChild: _buildTriangle(index, true),
firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
sizeCurve: Curves.fastOutSlowIn,
crossFadeState: widget.steps[index].state == StepState.error
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: kThemeAnimationDuration,
);
} else {
if (widget.steps[index].state != StepState.error)
return _buildCircle(index, false);
else
return _buildTriangle(index, false);
}
}
Widget _buildVerticalControls(int stepIndex) {
if (widget.controlsBuilder != null)
return widget.controlsBuilder!(
context,
ControlsDetails(
currentStep: widget.currentStep,
onStepContinue: widget.onStepContinue,
onStepCancel: widget.onStepCancel,
stepIndex: stepIndex,
),
);
final Color cancelColor;
switch (Theme
.of(context)
.brightness) {
case Brightness.light:
cancelColor = Colors.black54;
break;
case Brightness.dark:
cancelColor = Colors.white70;
break;
}
final ThemeData themeData = Theme.of(context);
final ColorScheme colorScheme = themeData.colorScheme;
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
const OutlinedBorder buttonShape = RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(2)));
const EdgeInsets buttonPadding = EdgeInsets.symmetric(horizontal: 16.0);
return Container(
margin: const EdgeInsets.only(top: 16.0),
child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(height: 48.0),
child: Row(
children: <Widget>[
TextButton(
onPressed: widget.onStepContinue,
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
return states.contains(MaterialState.disabled)
? null
: (_isDark()
? colorScheme.onSurface
: colorScheme.onPrimary);
}),
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
return _isDark() ||
states.contains(MaterialState.disabled)
? null
: colorScheme.primary;
}),
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
buttonPadding),
shape: MaterialStateProperty.all<OutlinedBorder>(buttonShape),
),
child: Text(localizations.continueButtonLabel),
),
Container(
margin: const EdgeInsetsDirectional.only(start: 8.0),
child: TextButton(
onPressed: widget.onStepCancel,
style: TextButton.styleFrom(
primary: cancelColor,
padding: buttonPadding,
shape: buttonShape,
),
child: Text(localizations.cancelButtonLabel),
),
),
],
),
),
);
}
TextStyle _titleStyle(int index) {
final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme;
assert(widget.steps[index].state != null);
switch (widget.steps[index].state) {
case StepState.indexed:
case StepState.editing:
case StepState.complete:
return textTheme.bodyText1!;
case StepState.disabled:
return textTheme.bodyText1!.copyWith(
color: _isDark() ? _kDisabledDark : _kDisabledLight,
);
case StepState.error:
return textTheme.bodyText1!.copyWith(
color: _isDark() ? _kErrorDark : _kErrorLight,
);
}
}
TextStyle _subtitleStyle(int index) {
final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme;
assert(widget.steps[index].state != null);
switch (widget.steps[index].state) {
case StepState.indexed:
case StepState.editing:
case StepState.complete:
return textTheme.caption!;
case StepState.disabled:
return textTheme.caption!.copyWith(
color: _isDark() ? _kDisabledDark : _kDisabledLight,
);
case StepState.error:
return textTheme.caption!.copyWith(
color: _isDark() ? _kErrorDark : _kErrorLight,
);
}
}
Widget _buildHeaderText(int index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
AnimatedDefaultTextStyle(
style: _titleStyle(index),
duration: kThemeAnimationDuration,
curve: Curves.fastOutSlowIn,
child: widget.steps[index].title,
),
if (widget.steps[index].subtitle != null)
Container(
margin: const EdgeInsets.only(top: 2.0),
child: AnimatedDefaultTextStyle(
style: _subtitleStyle(index),
duration: kThemeAnimationDuration,
curve: Curves.fastOutSlowIn,
child: widget.steps[index].subtitle!,
),
),
],
);
}
Widget _buildVerticalHeader(int index) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 24.0),
child: Row(
children: <Widget>[
Column(
children: <Widget>[
_buildLine(!_isFirst(index)),
_buildIcon(index),
_buildLine(!_isLast(index)),
],
),
Expanded(
child: Container(
margin: const EdgeInsetsDirectional.only(start: 12.0),
child: _buildHeaderText(index),
),
),
],
),
);
}
Widget _buildVerticalBody(int index) {
return Stack(
children: <Widget>[
PositionedDirectional(
start: 24.0,
top: 0.0,
bottom: 0.0,
child: SizedBox(
width: 24.0,
child: Center(
child: SizedBox(
width: _isLast(index) ? 0.0 : 1.0,
child: Container(
color: Colors.grey.shade400,
),
),
),
),
),
AnimatedCrossFade(
firstChild: Container(height: 0.0),
secondChild: Container(
margin: widget.margin ??
const EdgeInsetsDirectional.only(
start: 60.0,
end: 24.0,
),
child: Column(
children: <Widget>[
widget.steps[index].content,
_buildVerticalControls(index),
],
),
),
firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
sizeCurve: Curves.fastOutSlowIn,
crossFadeState: _isCurrent(index)
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: kThemeAnimationDuration,
),
],
);
}
Widget _buildVertical() {
return ListView(
shrinkWrap: true,
physics: widget.physics,
children: <Widget>[
for (int i = 0; i < widget.steps.length; i += 1)
Column(
key: _keys[i],
children: <Widget>[
InkWell(
onTap: widget.steps[i].state != StepState.disabled
? () {
Scrollable.ensureVisible(
_keys[i].currentContext!,
curve: Curves.fastOutSlowIn,
duration: kThemeAnimationDuration,
);
widget.onStepTapped?.call(i);
}
: null,
canRequestFocus: widget.steps[i].state != StepState.disabled,
child: _buildVerticalHeader(i),
),
_buildVerticalBody(i),
],
),
],
);
}
Widget _buildHorizontal() {
final List<Widget> children = <Widget>[
for (int i = 0; i < widget.steps.length; i += 1) ...<Widget>[
InkResponse(
onTap: widget.steps[i].state != StepState.disabled
? () {
widget.onStepTapped?.call(i);
}
: null,
canRequestFocus: widget.steps[i].state != StepState.disabled,
child: Row(
children: <Widget>[
SizedBox(
height: 72.0,
child: Center(
child: _buildIcon(i),
),
),
Container(
margin: const EdgeInsetsDirectional.only(start: 12.0),
child: _buildHeaderText(i),
),
],
),
),
if (!_isLast(i))
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
height: 1.0,
color: Colors.grey.shade400,
),
),
],
];
final List<Widget> stepPanels = <Widget>[];
for (int i = 0; i < widget.steps.length; i += 1) {
stepPanels.add(
Visibility(
maintainState: true,
visible: i == widget.currentStep,
child: widget.steps[i].content,
),
);
}
return Column(
children: <Widget>[
Material(
elevation: widget.elevation ?? 2,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 24.0),
child: Row(
children: children,
),
),
),
Expanded(
child: ListView(
physics: widget.physics,
padding: const EdgeInsets.symmetric(horizontal: 24.0),
children: <Widget>[
AnimatedSize(
curve: Curves.fastOutSlowIn,
duration: kThemeAnimationDuration,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: stepPanels),
),
_buildVerticalControls(widget.currentStep),
],
),
),
],
);
}
#override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
assert(debugCheckHasMaterialLocalizations(context));
assert(() {
if (context.findAncestorWidgetOfExactType<CustomStepper>() != null)
throw FlutterError(
'Steppers must not be nested.\n'
'The material specification advises that one should avoid embedding '
'steppers within steppers. '
'https:
);
return true;
}());
assert(widget.type != null);
switch (widget.type) {
case StepperType.vertical:
return _buildVertical();
case StepperType.horizontal:
return _buildHorizontal();
}
}
}
class _TrianglePainter extends CustomPainter {
_TrianglePainter({
required this.color,
});
final Color color;
#override
bool hitTest(Offset point) => true;
#override
bool shouldRepaint(_TrianglePainter oldPainter) {
return oldPainter.color != color;
}
#override
void paint(Canvas canvas, Size size) {
final double base = size.width;
final double halfBase = size.width / 2.0;
final double height = size.height;
final List<Offset> points = <Offset>[
Offset(0.0, height),
Offset(base, height),
Offset(halfBase, 0.0),
];
canvas.drawPath(
Path()
..addPolygon(points, true),
Paint()
..color = color,
);
}
}

How to make a button return to its initial state

I created a custom button where it expands on the first tap, and on the second tap, it will go to another screen. Now, the problem is, I created three instances of that button. So, the button that I tapped first remains expanded and would not go to its initial size. I would like to make the button return to its initial state whenever the user taps any other widget. Sorry for my code. I'm still practicing. Hope someone help.
Here is the code for the custom button.
import 'package:flutter/material.dart';
double prevHeight = 0;
class CustomRetractableButton extends StatefulWidget {
double height;
double width;
String imagePath;
Color color;
CustomRetractableButton({
required this.height,
required this.width,
required this.color,
required this.imagePath,
}) {
prevHeight = height;
}
#override
_CustomRetractableButtonState createState() =>
_CustomRetractableButtonState();
}
class _CustomRetractableButtonState extends State<CustomRetractableButton>
with TickerProviderStateMixin {
String btnText = '';
Alignment imageAlignment = Alignment.center;
bool isSelected = false;
#override
Widget build(BuildContext context) {
double size = MediaQuery.of(context).size.height;
return AnimatedContainer(
duration: Duration(milliseconds: 150),
decoration: BoxDecoration(
color: widget.color,
borderRadius: BorderRadius.circular(20.0),
image: DecorationImage(
image: AssetImage(widget.imagePath),
alignment: imageAlignment,
fit: BoxFit.fitWidth,
),
),
height: widget.height,
child: TextButton(
onPressed: () {
if (!isSelected) {
isSelected = !isSelected;
setState(() {
widget.height = size;
btnText = 'Send Alert';
imageAlignment = Alignment.topCenter;
});
} else {
//navigates to another screen
}
},
child: Align(
alignment: Alignment.bottomCenter,
child: RotatedBox(
quarterTurns: -1,
child: AnimatedSize(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 150),
child: Padding(
padding: const EdgeInsets.only(left: 100),
child: Text(
btnText,
style: const TextStyle(
color: Colors.white,
fontSize: 25,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
),
);
}
}
Here is the code of main.dart
import 'package:custom_animated_button/custom_retractable_button.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
double height = 250;
String btnText = '';
bool isSelected = true;
Alignment imageAlignment = Alignment.center;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(
flex: 1,
child: Column(
children: [
Container(
width: double.infinity,
child: const Text(
"Request Assistance",
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 20,
),
),
),
Container(
width: double.infinity,
child: const Text(
"Please choose the type of responder",
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 14,
),
),
),
],
),
),
Expanded(
flex: 10,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: CustomRetractableButton(
height: 250,
width: double.infinity,
imagePath: 'assets/police3.png',
color: const Color(0xFF4F70A1),
),
),
const SizedBox(
width: 15,
),
Expanded(
child: CustomRetractableButton(
height: 250,
width: double.infinity,
imagePath: 'assets/medic.png',
color: const Color(0xFF81C6D6),
),
),
const SizedBox(
width: 15,
),
Expanded(
child: CustomRetractableButton(
height: 250,
width: double.infinity,
color: const Color(0xFFE05A45),
imagePath: 'assets/firefighter.png',
),
),
],
),
),
],
),
),
),
),
);
}
}
You want:
allow CustomRetractableButton to return to the initial state when the item is pressed.
You need:
save the initial setting of the CustomRetractableButton.
expose the change and restore the state
TODO:
Make all properties (height, width, imagePath) of CustomRetractableButton final.
Create an activeHeight property under _CustomRetractableButtonState.
Delete your global variable prevHeight, it will not work on multiple instances.
Create a static variable of type _CustomRetractableButtonState that will save the state of the last active CustomRetractableButton.
The complete code of your custom button
import 'package:flutter/material.dart';
class CustomRetractableButton extends StatefulWidget {
final double initialHeight;
final double width;
final String imagePath;
final Color color;
CustomRetractableButton({
required double height,
required this.width,
required this.color,
required this.imagePath,
}):initialHeight = height;
#override
_CustomRetractableButtonState createState() =>
_CustomRetractableButtonState();
}
class _CustomRetractableButtonState extends State<CustomRetractableButton>
with TickerProviderStateMixin {
static _CustomRetractableButtonState? oldActive;
String btnText = '';
Alignment imageAlignment = Alignment.center;
bool isSelected = false;
double activeHeight = 0;
#override
initState(){
activeHeight = widget.initialHeight;
super.initState();
}
revert(){
if(mounted){
setState((){
activeHeight = widget.initialHeight;
isSelected = false;
});
}
}
#override
Widget build(BuildContext context) {
double size = MediaQuery.of(context).size.height;
return AnimatedContainer(
duration: Duration(milliseconds: 150),
decoration: BoxDecoration(
color: widget.color,
borderRadius: BorderRadius.circular(20.0),
image: DecorationImage(
image: AssetImage(widget.imagePath),
alignment: imageAlignment,
fit: BoxFit.fitWidth,
),
),
height: activeHeight,
child: TextButton(
onPressed: () {
if (!isSelected) {
if(oldActive != null){
oldActive!.revert();
}
isSelected = true;
oldActive = this;
setState(() {
activeHeight = size;
btnText = 'Send Alert';
imageAlignment = Alignment.topCenter;
});
} else {
if(oldActive != null){
oldActive!.revert();
oldActive = null;
}
//navigates to another screen
}
},
child: Align(
alignment: Alignment.bottomCenter,
child: RotatedBox(
quarterTurns: -1,
child: AnimatedSize(
vsync: this,
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 150),
child: Padding(
padding: const EdgeInsets.only(left: 100),
child: Text(
btnText,
style: const TextStyle(
color: Colors.white,
fontSize: 25,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
),
);
}
}
Then you can improve the code by using the providers or other flutter state management approach
Wrap the whole body of Scaffold with GestureDetector and make your operations on the on tap method.
and add behavior: HitTestBehavior.opaque, to the gesture detector.