i am developing a chatbot and i want to add a typing indicator before the bot reply to the user, i have tried switching between widgets after the user inputs a message using future delayed, but it is not working at all.
the following code demonstrates how i tried using the future delayed:
bool _nextWidget = false;
#override
void initState() {
super.initState();
}
void myMethod() {
Future.delayed(
const Duration(
seconds: 5,
milliseconds: 500,
),
() {
if (this.mounted) {
setState(() {
_nextWidget = true;
});
}
});
}
Widget bot(String message) {
myMethod();
return _nextWidget ? botMessage(message) : botInd();
}
Widget botInd() {
return Container(
alignment: Alignment.bottomLeft,
margin: EdgeInsets.only(top: 20),
child: Container(
constraints: BoxConstraints(maxWidth: 75, maxHeight: 100),
child: JumpingDotsProgressIndicator(fontSize: 50.0, color: Colors.white)
)
);
}
Widget botMessage(String message) {
return ChatBubble(
clipper: ChatBubbleClipper2(type: BubbleType.receiverBubble),
alignment: Alignment.bottomLeft,
margin: EdgeInsets.only(top: 20),
backGroundColor: Colors.white,
child: Container(
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.7),
child: Text(
message,
style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold)
)
)
);
}
Have a look at https://flutter.dev/docs/cookbook/effects/typing-indicator it explains all necessary steps how to create a typing indicator with code examples.
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(
const MaterialApp(
home: ExampleIsTyping(),
debugShowCheckedModeBanner: false,
),
);
}
const _backgroundColor = Color(0xFF333333);
class ExampleIsTyping extends StatefulWidget {
const ExampleIsTyping({
Key? key,
}) : super(key: key);
#override
_ExampleIsTypingState createState() => _ExampleIsTypingState();
}
class _ExampleIsTypingState extends State<ExampleIsTyping> {
bool _isSomeoneTyping = false;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _backgroundColor,
appBar: AppBar(
title: const Text('Typing Indicator'),
),
body: Column(
children: [
Expanded(
child: _buildMessages(),
),
Align(
alignment: Alignment.bottomLeft,
child: TypingIndicator(
showIndicator: _isSomeoneTyping,
),
),
_buildIsTypingSimulator(),
],
),
);
}
Widget _buildMessages() {
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8.0),
itemCount: 25,
reverse: true,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(left: 100.0),
child: FakeMessage(isBig: index.isOdd),
);
},
);
}
Widget _buildIsTypingSimulator() {
return Container(
color: Colors.grey,
padding: const EdgeInsets.all(16),
child: Center(
child: CupertinoSwitch(
onChanged: (newValue) {
setState(() {
_isSomeoneTyping = newValue;
});
},
value: _isSomeoneTyping,
),
),
);
}
}
class TypingIndicator extends StatefulWidget {
const TypingIndicator({
Key? key,
this.showIndicator = false,
this.bubbleColor = const Color(0xFF646b7f),
this.flashingCircleDarkColor = const Color(0xFF333333),
this.flashingCircleBrightColor = const Color(0xFFaec1dd),
}) : super(key: key);
final bool showIndicator;
final Color bubbleColor;
final Color flashingCircleDarkColor;
final Color flashingCircleBrightColor;
#override
_TypingIndicatorState createState() => _TypingIndicatorState();
}
class _TypingIndicatorState extends State<TypingIndicator>
with TickerProviderStateMixin {
late AnimationController _appearanceController;
late Animation<double> _indicatorSpaceAnimation;
late Animation<double> _smallBubbleAnimation;
late Animation<double> _mediumBubbleAnimation;
late Animation<double> _largeBubbleAnimation;
late AnimationController _repeatingController;
final List<Interval> _dotIntervals = const [
Interval(0.25, 0.8),
Interval(0.35, 0.9),
Interval(0.45, 1.0),
];
#override
void initState() {
super.initState();
_appearanceController = AnimationController(
vsync: this,
)..addListener(() {
setState(() {});
});
_indicatorSpaceAnimation = CurvedAnimation(
parent: _appearanceController,
curve: const Interval(0.0, 0.4, curve: Curves.easeOut),
reverseCurve: const Interval(0.0, 1.0, curve: Curves.easeOut),
).drive(Tween<double>(
begin: 0.0,
end: 60.0,
));
_smallBubbleAnimation = CurvedAnimation(
parent: _appearanceController,
curve: const Interval(0.0, 0.5, curve: Curves.elasticOut),
reverseCurve: const Interval(0.0, 0.3, curve: Curves.easeOut),
);
_mediumBubbleAnimation = CurvedAnimation(
parent: _appearanceController,
curve: const Interval(0.2, 0.7, curve: Curves.elasticOut),
reverseCurve: const Interval(0.2, 0.6, curve: Curves.easeOut),
);
_largeBubbleAnimation = CurvedAnimation(
parent: _appearanceController,
curve: const Interval(0.3, 1.0, curve: Curves.elasticOut),
reverseCurve: const Interval(0.5, 1.0, curve: Curves.easeOut),
);
_repeatingController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
if (widget.showIndicator) {
_showIndicator();
}
}
#override
void didUpdateWidget(TypingIndicator oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.showIndicator != oldWidget.showIndicator) {
if (widget.showIndicator) {
_showIndicator();
} else {
_hideIndicator();
}
}
}
#override
void dispose() {
_appearanceController.dispose();
_repeatingController.dispose();
super.dispose();
}
void _showIndicator() {
_appearanceController
..duration = const Duration(milliseconds: 750)
..forward();
_repeatingController.repeat();
}
void _hideIndicator() {
_appearanceController
..duration = const Duration(milliseconds: 150)
..reverse();
_repeatingController.stop();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _indicatorSpaceAnimation,
builder: (context, child) {
return SizedBox(
height: _indicatorSpaceAnimation.value,
child: child,
);
},
child: Stack(
children: [
_buildAnimatedBubble(
animation: _smallBubbleAnimation,
left: 8,
bottom: 8,
bubble: _buildCircleBubble(8),
),
_buildAnimatedBubble(
animation: _mediumBubbleAnimation,
left: 10,
bottom: 10,
bubble: _buildCircleBubble(16),
),
_buildAnimatedBubble(
animation: _largeBubbleAnimation,
left: 12,
bottom: 12,
bubble: _buildStatusBubble(),
),
],
),
);
}
Widget _buildAnimatedBubble({
required Animation<double> animation,
required double left,
required double bottom,
required Widget bubble,
}) {
return Positioned(
left: left,
bottom: bottom,
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.scale(
scale: animation.value,
alignment: Alignment.bottomLeft,
child: child,
);
},
child: bubble,
),
);
}
Widget _buildCircleBubble(double size) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.bubbleColor,
),
);
}
Widget _buildStatusBubble() {
return Container(
width: 85,
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(27),
color: widget.bubbleColor,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFlashingCircle(0),
_buildFlashingCircle(1),
_buildFlashingCircle(2),
],
),
);
}
Widget _buildFlashingCircle(int index) {
return AnimatedBuilder(
animation: _repeatingController,
builder: (context, child) {
final circleFlashPercent =
_dotIntervals[index].transform(_repeatingController.value);
final circleColorPercent = sin(pi * circleFlashPercent);
return Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Color.lerp(widget.flashingCircleDarkColor,
widget.flashingCircleBrightColor, circleColorPercent),
),
);
},
);
}
}
#immutable
class FakeMessage extends StatelessWidget {
const FakeMessage({
Key? key,
required this.isBig,
}) : super(key: key);
final bool isBig;
#override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0),
height: isBig ? 128.0 : 36.0,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: Colors.grey.shade300,
),
);
}
}
Related
I am implementing FAB as Expandable with little dark overlay when FAB is clicked.
My problem is there is right margin in Scaffold and my overlay is not filling whole view.
How to remove that right margin?
Here is how it's looking:
Here is my Scaffold code:
return Scaffold(
resizeToAvoidBottomInset: true,
floatingActionButton: !isSearchBarVisible ? SizedBox.expand(
child: ExpandableFab(
//key: _key,
distance: size.height * 0.09,
children: [
/* not needed to show problem */
],
),
) : null,
body: /* some body */
and here is ExpandableFab class
class ExpandableFab extends StatefulWidget {
const ExpandableFab({
Key? key,
this.initialOpen,
required this.distance,
required this.children,
}) : super(key: key);
final bool? initialOpen;
final double distance;
final List<Widget> children;
#override
_ExpandableFabState createState() => _ExpandableFabState();
}
class _ExpandableFabState extends State<ExpandableFab>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
Animation<double>? _expandAnimation;
bool _open = false;
#override
void initState() {
super.initState();
_open = widget.initialOpen ?? false;
_controller = AnimationController(
value: _open ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
vsync: this,
);
_expandAnimation = CurvedAnimation(
curve: Curves.fastOutSlowIn,
reverseCurve: Curves.easeOutQuad,
parent: _controller,
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggle() {
setState(() {
_open = !_open;
if (_open) {
_controller.forward();
} else {
_controller.reverse();
}
});
}
#override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return GestureDetector(
onTap: () => _toggle(),
child: Stack(
alignment: Alignment.bottomRight,
clipBehavior: Clip.none,
children: [
IgnorePointer(
ignoring: !_open,
child: TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.0, end: _open ? 1.0 : 0.0),
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
builder: (_, value, child) {
if (value < 0.001) {
return child!;
}
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: value, sigmaY: value),
child: child,
);
},
child: Container(color: Colors.transparent),
),
),
IgnorePointer(
ignoring: !_open,
child: AnimatedOpacity(
duration: Duration(milliseconds: 500),
opacity: _open ? 1 : 0,
curve: Curves.easeInOut,
child: Container(
color: Colors.black12,
),
),
),
Transform.translate(
offset: Offset(0, 0),
child: Stack(
alignment: Alignment.bottomRight,
children: [
Positioned(
bottom: size.height * 0.14,
child: _buildTapToCloseFab(size)
),
Positioned(
bottom: size.height * 0.14,
child: _buildTapToOpenFab(size)
),
..._buildExpandingActionButtons(),
],
),
),
],
),
);
}
Widget _buildTapToCloseFab(Size size) {
return SizedBox(
width: 56.0,
height: 56.0,
child: Center(
child: Material(
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
elevation: 4.0,
child: InkWell(
onTap: _toggle,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.close,
color: Theme.of(context).primaryColor,
),
),
),
),
),
);
}
Widget _buildTapToOpenFab(Size size) {
return IgnorePointer(
ignoring: _open,
child: AnimatedContainer(
transformAlignment: Alignment.center,
transform: Matrix4.diagonal3Values(
_open ? 0.7 : 1.0,
_open ? 0.7 : 1.0,
1.0,
),
duration: const Duration(milliseconds: 250),
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
child: AnimatedOpacity(
opacity: _open ? 0.0 : 1.0,
curve: const Interval(0.25, 1.0, curve: Curves.easeInOut),
duration: const Duration(milliseconds: 250),
child: FloatingActionButton(
onPressed: _toggle,
child: Icon(
Icons.add,
),
),
),
),
);
}
List<Widget> _buildExpandingActionButtons() {
final children = <Widget>[];
final count = widget.children.length;
final step = 90.0 / (count - 1);
var dist;
for (var i = 0, angleInDegrees = 0.0;
i < count;
i++, angleInDegrees += step) {
if (i == 0) {
dist = (widget.distance) * (i + 1);
}
else {
dist = (widget.distance) * (i + 1);
}
children.add(
_ExpandingActionButton(
directionInDegrees: 90,
maxDistance: dist,
progress: _expandAnimation,
child: widget.children[i],
),
);
}
return children;
}
}
class _ExpandingActionButton extends StatelessWidget {
const _ExpandingActionButton({
Key? key,
required this.directionInDegrees,
required this.maxDistance,
required this.progress,
required this.child,
}) : super(key: key);
final double directionInDegrees;
final double maxDistance;
final Animation<double>? progress;
final Widget child;
#override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return AnimatedBuilder(
animation: progress!,
builder: (BuildContext context, Widget? child) {
final offset = Offset.fromDirection(
directionInDegrees * (math.pi / 180.0),
progress!.value * maxDistance,
);
return Positioned(
right: 4.0 + offset.dx,
bottom: (size.height * 0.14) + 4.0 + offset.dy,
child: Transform.rotate(
angle: (1.0 - progress!.value) * math.pi / 2,
child: child,
),
);
},
child: FadeTransition(
opacity: progress!,
child: child,
),
);
}
}
It's mostly code from Flutter tutorial: https://docs.flutter.dev/cookbook/effects/expandable-fab with some little changes like vertical expand or overlay when FAB is expanded.
Use this structure:
Scaffold(
resizeToAvoidBottomInset: true,
floatingActionButton: !isSearchBarVisible ? Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox.expand(
child: ExpandableFab(
//key: _key,
distance: size.height * 0.09,
children: [
/* not needed to show problem */
],
),
),
) : null,
body:
I have got a stack of cards and scroll wheel. I am trying to animate the front card, move it to the right, then bring it back in a way it goes to the back of the cards.
I have used a future function to specify witch part of code should be done first. But what I get is; it changes the index of the card first the animation takes place. Here is my code:
class AnimationsPractice extends StatefulWidget {
static const String id = 'test_screen';
#override
_AnimationsPracticeState createState() => _AnimationsPracticeState();
}
class _AnimationsPracticeState extends State<AnimationsPractice>
with SingleTickerProviderStateMixin {
FixedExtentScrollController _scrollController =
FixedExtentScrollController(initialItem: 0);
AnimationController _controller;
Animation<Offset> _offsetAnimation;
int selected;
List<Widget> sampleCard;
Animation<Offset> _offsetAnimation2;
bool halfWayAnimation;
#override
void initState() {
super.initState();
_controller =
AnimationController(duration: const Duration(seconds: 1), vsync: this)
..repeat();
_offsetAnimation = Tween<Offset>(
begin: Offset.zero,
end: const Offset(1.5, 0.0),
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
0.0,
0.5,
curve: Curves.elasticIn,
),
),
);
_offsetAnimation2 = Tween<Offset>(
begin: const Offset(1.5, 0.0),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
0.5,
1.0,
curve: Curves.elasticIn,
),
),
);
halfWayAnimation = false;
_controller.stop();
sampleCard = [
Container(
height: 60,
width: 40,
color: Colors.red,
),
Transform.rotate(
angle: 10 * (pi / 180),
child: Container(
height: 60,
width: 40,
color: Colors.blueGrey,
)),
SlideTransition(
position: halfWayAnimation ? _offsetAnimation2 : _offsetAnimation,
child: Container(
height: 60,
width: 40,
color: Colors.yellow,
),
),
];
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
Future<void> _playAnimation() async {
try {
await _controller.forward().orCancel;
await siftingIndex();
await _controller.reverse().orCancel;
} on TickerCanceled {
// the animation got canceled, probably because it was disposed of
}
}
Future<void> siftingIndex() {
return Future.delayed(const Duration(microseconds: 200), () {
sampleCard.insert(0, sampleCard[sampleCard.length - 1]);
sampleCard.removeLast();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(180.0),
child: SafeArea(
child: TextButton(
child: Text('back to login'),
onPressed: () {
Navigator.pushNamed(context, LoginScreen.id);
},
),
),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Stack(children: sampleCard),
CustomScrollWheel(
onItemChange: (x) {
setState(() {
_playAnimation();
halfWayAnimation = true;
});
},
scrollController: _scrollController,
),
],
),
);
}
}
enter image description here
In Flutter you suppose I have a simple Container and I would like to change the size of that to up, for example in this simple screenshot I want to change top container in section 1 to up to have a top container in section 2
and top container in section 1 should behave only 100.0 after size to up
container B in section 1 and section 2 are in the same axis without change position when container A will be resized to up
for example, this is what I want to have
I'm not sure with which one animation I can have this feature
this code work, but this is not what I want to have.
i want to have draggable bottom sheet with changing border radius when bottom sheet arrived to top of screen like with pastes sample video screen and fade0n/out widget inside appbar which that inside top of bottom sheet, which that visible when bottom sheet arrived top or hide when bottom sheet don't have maximum size
import 'package:flutter/material.dart';
void main()=>runApp(SizeUp());
class SizeUp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'test',
home: SizeUpAnim(),
);
}
}
class SizeUpAnim extends StatefulWidget {
#override
State<StatefulWidget> createState() =>_SizeUpAnim();
}
class _SizeUpAnim extends State with SingleTickerProviderStateMixin {
AnimationController _controller;
// ignore: constant_identifier_names
static const _PANEL_HEADER_HEIGHT = 32.0;
bool get _isPanelVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 100), value: 1.0, vsync: this);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 8.0,
title: const Text("Step4"),
leading: IconButton(
onPressed: () {
_controller.fling(velocity: _isPanelVisible ? -1.0 : 1.0);
},
icon: AnimatedIcon(
icon: AnimatedIcons.close_menu,
progress: _controller.view,
),
),
),
body: Column(
children: <Widget>[
Expanded(
child: LayoutBuilder(
builder: _buildStack,
),
),
Text('aaa'),
],
),
);
}
Animation<RelativeRect> _getPanelAnimation(BoxConstraints constraints) {
final double height = constraints.biggest.height;
final double top = height - _PANEL_HEADER_HEIGHT;
const double bottom = -_PANEL_HEADER_HEIGHT;
return RelativeRectTween(
begin: RelativeRect.fromLTRB(0.0, top, 0.0, bottom),
end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate( CurvedAnimation(parent: _controller, curve: Curves.linear));
}
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
final Animation<RelativeRect> animation = _getPanelAnimation(constraints);
final ThemeData theme = Theme.of(context);
return Container(
color: theme.primaryColor,
child: Stack(
children: <Widget>[
const Center(
child: Text("base"),
),
PositionedTransition(
rect: animation,
child: Material(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0)),
elevation: 12.0,
child: Container(
height: _PANEL_HEADER_HEIGHT,
child: const Center(child: Text("panel")),
),
),
),
],
),
);
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
}
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool isLong = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First'),
),
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('hey'),
RaisedButton(
onPressed: () {
setImages();
setState(() {
isLong = !isLong;
});
},
child: Text(isLong ? 'long' : 'short'),
),
RaisedButton(
onPressed: _onPressed,
child: Text('open'),
)
],
),
),
);
}
_onPressed() {
Navigator.of(context)
.push(TransparentRoute(builder: (context) => NewWidget(images)));
}
List<String> images = List.generate(
5,
(_) => 'http://placeimg.com/100/100/any',
);
void setImages() {
images = List.generate(
isLong ? 5 : 25,
(_) => 'http://placeimg.com/100/100/any',
);
}
}
class NewWidget extends StatefulWidget {
const NewWidget(this.images, {Key key}) : super(key: key);
final List<String> images;
#override
_NewWidgetState createState() => _NewWidgetState();
}
class _NewWidgetState extends State<NewWidget> {
bool isBig = false;
bool isStack = false;
bool isBounsing = true;
final double topOffset = 200;
final double miniHandleHeigh = 30;
double safeAreaPadding = 0;
double startBigAnimationOffset;
double startStickyOffset;
double backgroundHeight = 0;
double get savedAppBarHeigh => safeAreaPadding + kToolbarHeight;
final ScrollController controller = ScrollController();
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
super.initState();
}
void _afterLayout(_) {
var media = MediaQuery.of(context);
safeAreaPadding = media.padding.top;
startBigAnimationOffset = topOffset - savedAppBarHeigh;
startStickyOffset = startBigAnimationOffset + 20;
backgroundHeight = media.size.height - miniHandleHeigh - topOffset;
var canScroll = !_isImageSizeBiggerThenBottomSheetSize(
media.size.width,
media.size.height,
);
controller.addListener(
canScroll ? withoutScrolling : withScrolling,
);
}
void withoutScrolling() {
var offset = controller.offset;
if (offset < 0) {
goOut();
} else {
controller.animateTo(
0,
duration: Duration(milliseconds: 100),
curve: Curves.easeIn,
);
}
}
void withScrolling() {
var offset = controller.offset;
if (offset < 0) {
goOut();
} else if (offset < startBigAnimationOffset && isBig) {
setState(() {
isBig = false;
});
} else if (offset > startBigAnimationOffset && !isBig) {
setState(() {
isBig = true;
});
} else if (offset > startStickyOffset && !isStack) {
setState(() {
isStack = true;
});
} else if (offset < startStickyOffset && isStack) {
setState(() {
isStack = false;
});
}
if (offset < topOffset && !isBounsing) {
setState(() => isBounsing = true);
} else if (offset > topOffset && isBounsing) {
setState(() => isBounsing = false);
}
}
void goOut() {
controller.dispose();
Navigator.of(context).pop();
}
_isImageSizeBiggerThenBottomSheetSize(
double screenWidth,
double screenHeight,
) {
// padding: 10, 3 in row;
print(screenHeight);
var itemHeight = (screenWidth - 20) / 3;
print(itemHeight);
var gridMaxHeight = screenHeight - topOffset - miniHandleHeigh;
print(gridMaxHeight);
return (widget.images.length / 3).floor() * itemHeight > gridMaxHeight;
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: isStack ? Colors.white : Colors.transparent,
body: Stack(
children: [
Positioned(
bottom: 0,
right: 0,
left: 0,
child: Container(
constraints: BoxConstraints(minHeight: backgroundHeight),
decoration: BoxDecoration(
color: Colors.white,
),
),
),
ListView(
physics:
isBounsing ? BouncingScrollPhysics() : ClampingScrollPhysics(),
controller: controller,
children: <Widget>[
Container(
alignment: Alignment.bottomCenter,
height: topOffset,
child: TweenAnimationBuilder(
tween: Tween(begin: 0.0, end: isBig ? 1.0 : 0.0),
duration: Duration(milliseconds: 500),
child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(top: 15),
child: Container(
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.circular(2),
),
height: 5,
width: 60,
),
),
),
builder: (_, number, child) {
return Container(
height: savedAppBarHeigh * number + miniHandleHeigh,
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
top: Radius.circular((1 - number) * 20)),
color: Colors.white,
),
child: Opacity(opacity: 1 - number, child: child),
);
}),
),
Container(
padding: EdgeInsets.all(10),
constraints: BoxConstraints(
minHeight:
MediaQuery.of(context).size.height - savedAppBarHeigh),
decoration: BoxDecoration(
color: Colors.white,
),
child: getGrid(),
)
],
),
if (isStack)
_AppBar(
title: 'Gallery',
)
],
),
);
}
Widget getGrid() {
return GridView.count(
crossAxisSpacing: 10,
mainAxisSpacing: 10,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
crossAxisCount: 3,
children: widget.images.map((url) {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.blueAccent,
),
),
child: Image(
image: NetworkImage(url),
),
);
}).toList(),
);
}
}
class _AppBar extends StatelessWidget {
const _AppBar({Key key, this.title}) : super(key: key);
final Color backgroundColor = Colors.white;
final Color color = Colors.grey;
final String title;
#override
Widget build(BuildContext context) {
return Material(
elevation: 5,
child: Container(
height: kToolbarHeight + MediaQuery.of(context).padding.top,
color: backgroundColor,
child: OverflowBox(
alignment: Alignment.topCenter,
maxHeight: 200,
child: SafeArea(
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: kToolbarHeight),
child: AppBar(
centerTitle: false,
backgroundColor: backgroundColor,
iconTheme: IconThemeData(
color: color, //change your color here
),
primary: false,
title: Text(
title,
style: TextStyle(color: color),
),
elevation: 0,
),
),
),
),
),
);
;
}
}
class TransparentRoute extends PageRoute<void> {
TransparentRoute({
#required this.builder,
RouteSettings settings,
}) : assert(builder != null),
super(settings: settings, fullscreenDialog: false);
final WidgetBuilder builder;
#override
bool get opaque => false;
#override
Color get barrierColor => null;
#override
String get barrierLabel => null;
#override
bool get maintainState => true;
#override
Duration get transitionDuration => Duration(milliseconds: 350);
#override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
final result = builder(context);
return Container(
color: Colors.black.withOpacity(0.5),
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeIn,
)),
child: result,
),
);
}
}
I think you may try this library, sliding_sheet
when you detect the expand status by controller, then you animate/enlarge the container A.
I've started playing with Flutter and now thinking about the best way how I can implement a card's flipping animation.
I found this flip_card package and I'm trying to adjust its source code to my needs.
Here is the app which I have now:
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(FlipAnimationApp());
class FlipAnimationApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Flip animation"),
),
body: Center(
child: Container(
width: 200,
height: 200,
child: WidgetFlipper(
frontWidget: Container(
color: Colors.green[200],
child: Center(
child: Text(
"FRONT side.",
),
),
),
backWidget: Container(
color: Colors.yellow[200],
child: Center(
child: Text(
"BACK side.",
),
),
),
),
),
),
),
);
}
}
class WidgetFlipper extends StatefulWidget {
WidgetFlipper({
Key key,
this.frontWidget,
this.backWidget,
}) : super(key: key);
final Widget frontWidget;
final Widget backWidget;
#override
_WidgetFlipperState createState() => _WidgetFlipperState();
}
class _WidgetFlipperState extends State<WidgetFlipper> with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> _frontRotation;
Animation<double> _backRotation;
bool isFrontVisible = true;
#override
void initState() {
super.initState();
controller = AnimationController(duration: Duration(milliseconds: 500), vsync: this);
_frontRotation = TweenSequence(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween(begin: 0.0, end: pi / 2).chain(CurveTween(curve: Curves.linear)),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(pi / 2),
weight: 50.0,
),
],
).animate(controller);
_backRotation = TweenSequence(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(pi / 2),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: Tween(begin: -pi / 2, end: 0.0).chain(CurveTween(curve: Curves.linear)),
weight: 50.0,
),
],
).animate(controller);
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
AnimatedCard(
animation: _backRotation,
child: widget.backWidget,
),
AnimatedCard(
animation: _frontRotation,
child: widget.frontWidget,
),
_tapDetectionControls(),
],
);
}
Widget _tapDetectionControls() {
return Stack(
fit: StackFit.expand,
children: <Widget>[
GestureDetector(
onTap: _leftRotation,
child: FractionallySizedBox(
widthFactor: 0.5,
heightFactor: 1.0,
alignment: Alignment.topLeft,
child: Container(
color: Colors.transparent,
),
),
),
GestureDetector(
onTap: _rightRotation,
child: FractionallySizedBox(
widthFactor: 0.5,
heightFactor: 1.0,
alignment: Alignment.topRight,
child: Container(
color: Colors.transparent,
),
),
),
],
);
}
void _leftRotation() {
_toggleSide();
}
void _rightRotation() {
_toggleSide();
}
void _toggleSide() {
if (isFrontVisible) {
controller.forward();
isFrontVisible = false;
} else {
controller.reverse();
isFrontVisible = true;
}
}
}
class AnimatedCard extends StatelessWidget {
AnimatedCard({
this.child,
this.animation,
});
final Widget child;
final Animation<double> animation;
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
var transform = Matrix4.identity();
transform.setEntry(3, 2, 0.001);
transform.rotateY(animation.value);
return Transform(
transform: transform,
alignment: Alignment.center,
child: child,
);
},
child: child,
);
}
}
Here is how it looks like:
What I'd like to achieve is to make the card flip over its right side if it was tapped on its right half and over its left side if it was tapped on its left half. If it is tapped several times in a row on the same half it should flip over the same side (not back and forth as it is doing now).
So the desired animation should behave as the following one from Quizlet app.
You should know when you tap on the right or left to change the animations dynamically, for that you could use a flag isRightTap. Then, you should invert the values of the Tweens if it has to rotate to one side or to the other.
And the side you should rotate would be:
Rotate to left if the front is visible and you tapped on the left, or, because the back animation is reversed, if the back is is visible and you tapped on the right
Otherwise, rotate to right
Here are the things I changed in _WidgetFlipperState from the code in the question:
_updateRotations(bool isRightTap) {
setState(() {
bool rotateToLeft = (isFrontVisible && !isRightTap) || !isFrontVisible && isRightTap;
_frontRotation = TweenSequence(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween(begin: 0.0, end: rotateToLeft ? (pi / 2) : (-pi / 2))
.chain(CurveTween(curve: Curves.linear)),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(rotateToLeft ? (-pi / 2) : (pi / 2)),
weight: 50.0,
),
],
).animate(controller);
_backRotation = TweenSequence(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(rotateToLeft ? (pi / 2) : (-pi / 2)),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: Tween(begin: rotateToLeft ? (-pi / 2) : (pi / 2), end: 0.0)
.chain(CurveTween(curve: Curves.linear)),
weight: 50.0,
),
],
).animate(controller);
});
}
#override
void initState() {
super.initState();
controller =
AnimationController(duration: Duration(milliseconds: 500), vsync: this);
_updateRotations(true);
}
void _leftRotation() {
_toggleSide(false);
}
void _rightRotation() {
_toggleSide(true);
}
void _toggleSide(bool isRightTap) {
_updateRotations(isRightTap);
if (isFrontVisible) {
controller.forward();
isFrontVisible = false;
} else {
controller.reverse();
isFrontVisible = true;
}
}
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage();
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _toggler = true;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(actions: [
TextButton(
onPressed: _onFlipCardPressed,
child: const Text('change', style: TextStyle(color: Colors.white)),
)
]),
body: Center(
child: SizedBox.square(
dimension: 140,
child: FlipCard(
toggler: _toggler,
frontCard: AppCard(title: 'Front'),
backCard: AppCard(title: 'Back'),
),
),
),
);
}
void _onFlipCardPressed() {
setState(() {
_toggler = !_toggler;
});
}
}
class AppCard extends StatelessWidget {
final String title;
const AppCard({
required this.title,
});
#override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
color: Colors.deepPurple[400],
),
child: Center(
child: Text(
title,
style: const TextStyle(
fontSize: 40.0,
color: Colors.white,
),
textAlign: TextAlign.center,
),
),
);
}
}
class FlipCard extends StatelessWidget {
final bool toggler;
final Widget frontCard;
final Widget backCard;
const FlipCard({
required this.toggler,
required this.backCard,
required this.frontCard,
});
#override
Widget build(BuildContext context) {
return GestureDetector(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 800),
transitionBuilder: _transitionBuilder,
layoutBuilder: (widget, list) => Stack(children: [widget!, ...list]),
switchInCurve: Curves.ease,
switchOutCurve: Curves.ease.flipped,
child: toggler
? SizedBox(key: const ValueKey('front'), child: frontCard)
: SizedBox(key: const ValueKey('back'), child: backCard),
),
);
}
Widget _transitionBuilder(Widget widget, Animation<double> animation) {
final rotateAnimation = Tween(begin: pi, end: 0.0).animate(animation);
return AnimatedBuilder(
animation: rotateAnimation,
child: widget,
builder: (context, widget) {
final isFront = ValueKey(toggler) == widget!.key;
final rotationY = isFront ? rotateAnimation.value : min(rotateAnimation.value, pi * 0.5);
return Transform(
transform: Matrix4.rotationY(rotationY)..setEntry(3, 0, 0),
alignment: Alignment.center,
child: widget,
);
},
);
}
}
Try this code I've made some changes to your code, now the GestureDetector is divided equally in width on widget so when you tap on the left side of the box it will reverse the animation and if you tap on right side part it will forward the animation.
Widget _tapDetectionControls() {
return Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
flex: 1,
child: GestureDetector(
onTap: _leftRotation,
),
),
Expanded(
flex: 1,
child: GestureDetector(
onTap: _rightRotation,
),
),
],
);
}
void _leftRotation() {
controller.reverse();
}
void _rightRotation() {
controller.forward();
}
I'm trying to figure out how to notify user with alert that comes from top of the screen like normal push notification does.
How can I alert user from top of the screen.
AlertDialog is not customizable so I'm stuck with this. Is there any way to show something like alert or snack bar from top of the screen?
Flutter gives you the possiblity to create notifications with the help of the class Overlay. To animate these entering the screen from the top you can use the SlideTransition in combination with an AnimationController. Here is an example application I created:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: Home());
}
}
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton.icon(
icon: Icon(Icons.notifications_active),
label: Text('Notify!'),
onPressed: () {
Navigator.of(context)
.overlay
.insert(OverlayEntry(builder: (BuildContext context) {
return FunkyNotification();
}));
},
),
),
);
}
}
class FunkyNotification extends StatefulWidget {
#override
State<StatefulWidget> createState() => FunkyNotificationState();
}
class FunkyNotificationState extends State<FunkyNotification>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<Offset> position;
#override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 750));
position = Tween<Offset>(begin: Offset(0.0, -4.0), end: Offset.zero)
.animate(
CurvedAnimation(parent: controller, curve: Curves.bounceInOut));
controller.forward();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Material(
color: Colors.transparent,
child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(top: 32.0),
child: SlideTransition(
position: position,
child: Container(
decoration: ShapeDecoration(
color: Colors.deepPurple,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0))),
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text(
'Notification!',
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
),
),
),
),
);
}
}
Here you can dismiss notifications using the swipe up or down. This is the perfect notification for promotion in-app.
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> with TickerProviderStateMixin {
bool _fromTop = true;
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.fireplace_outlined),
onPressed: () {
showGeneralDialog(
barrierLabel: "Label",
barrierDismissible: true,
barrierColor: Colors.transparent,
transitionDuration: Duration(milliseconds: 700),
context: context,
pageBuilder: (context, anim1, anim2) {
return GestureDetector(
onVerticalDragUpdate: (dragUpdateDetails) {
Navigator.of(context).pop();
},
child: Column(
children: [
SizedBox(height: 40),
Card(
margin:
EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: Container(
height: 100,
child: Image.asset('lib/model/promo.png',
fit: BoxFit.fill),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(40),
),
),
),
],
),
);
},
transitionBuilder: (context, anim1, anim2, child) {
return SlideTransition(
position: anim1.drive(Tween(
begin: Offset(0, _fromTop ? -1 : 1), end: Offset(0, 0))
.chain(CurveTween(curve: Sprung()))),
child: child,
);
},
);
},
),
);
}
}
class Sprung extends Curve {
factory Sprung([double damping = 20]) => Sprung.custom(damping: damping);
Sprung.custom({
double damping = 20,
double stiffness = 180,
double mass = 1.0,
double velocity = 0.0,
}) : this._sim = SpringSimulation(
SpringDescription(
damping: damping,
mass: mass,
stiffness: stiffness,
),
0.0,
1.0,
velocity,
);
final SpringSimulation _sim;
#override
double transform(double t) => _sim.x(t) + t * (1 - _sim.x(1.0));
}