I have 2 button, to control a scrolling of InteractiveViewer widget, left and right, hopefully I can add up and down later own my own after i have better understanding of matrix4Tween
right now the button is as such
Row(
children: [
ElevatedButton(
onPressed: () {
controller.value = Matrix4.identity()..translate(0.0, 0.0);
},
child: Text('<'),
),
ElevatedButton(
onPressed: () {
controller.value = Matrix4.identity()..translate(-(width), 0.0);
},
child: Text('>'),
),
],
),
and below it is a interactiveViewer widget that contains 4 gridview
Container(
color: Colors.grey,
width: gridboxwidth,
height: gridboxheight,
child: InteractiveViewer(
alignPanAxis: true,
constrained: false,
transformationController: controller,
scaleEnabled: true,
minScale: 0.1,
maxScale: 1,
child: Column(
children: [
Row(
children: [
grid1(size),
grid3(size),
],
),
Row(
children: [
grid2(size),
grid4(size),
],
),
],
),
),
),
and it works just fine, on tap of the button, the grid moves into view, but I don't think it is intuitive enough and some animation might need to be added to show that the grid has changed.
Any help and guidance is greatly appreciated.
this is a sample widget that uses Matrix4Tween, the important lines of code are marked with // NOTE: comments:
class FooInteractiveViewer extends StatefulWidget {
#override
_FooInteractiveViewerState createState() => _FooInteractiveViewerState();
}
class _FooInteractiveViewerState extends State<FooInteractiveViewer> with TickerProviderStateMixin {
AnimationController _ctrl;
Animation<Matrix4> _matrixAnimation = AlwaysStoppedAnimation(Matrix4.identity());
final _transformationController = TransformationController();
#override
void initState() {
super.initState();
_ctrl = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
)
// NOTE: add listener to be called each time _ctrl changes
..addListener(_listener);
}
void _listener() {
print(MatrixUtils.transformPoint(_matrixAnimation.value, Offset.zero));
// NOTE: this is the most important part of this code:
_transformationController.value = _matrixAnimation.value;
}
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return InteractiveViewer(
// panEnabled: false,
alignPanAxis: true,
constrained: false,
transformationController: _transformationController,
child: SizedBox(
width: constraints.maxWidth * 2,
height: constraints.maxHeight * 2,
child: Column(
children: [
Expanded(
child: Row(
children: [
button(Colors.red, constraints.biggest, 1, 0, 'green'),
button(Colors.green, constraints.biggest, 0, 1, 'blue'),
],
),
),
Expanded(
child: Row(
children: [
button(Colors.blue, constraints.biggest, 1, 1, 'orange'),
button(Colors.orange, constraints.biggest, 0, 0, 'red'),
],
),
),
],
),
)
);
},
);
}
Widget button(Color color, Size size, int x, int y, String name) {
return Expanded(
child: Material(
color: color,
child: InkWell(
onTap: () async {
// timeDilation = 10;
// NOTE: create new Animation<Matrix4> that will be used inside _listener
_matrixAnimation = Matrix4Tween(
begin: _transformationController.value,
end: Matrix4.translationValues(-size.width * x, -size.height * y, 0)
).chain(CurveTween(curve: Curves.decelerate)).animate(_ctrl);
// NOTE: lets start the show
await _ctrl.forward(from: 0);
print('### animation finished ###');
},
child: Center(child: Text('go to $name', textScaleFactor: 2)),
),
),
);
}
#override
void dispose() {
super.dispose();
_ctrl.dispose();
}
}
Related
I'm trying to add a custom dropdown menu whose items are just links to other pages
I tried using DropdownButton
But I failed to make its elements as a link and it requires a value, and I do not have a value to pass to it
thank you
You can use OverlayEntry for this case. Below is a simple working example of a dropdown using OverlayEntry:
class TestDropdownWidget extends StatefulWidget {
TestDropdownWidget({Key? key}) : super(key: key);
#override
_TestDropdownWidgetState createState() => _TestDropdownWidgetState();
}
class _TestDropdownWidgetState extends State<TestDropdownWidget>
with TickerProviderStateMixin {
final LayerLink _layerLink = LayerLink();
late OverlayEntry _overlayEntry;
bool _isOpen = false;
//Controller Animation
late AnimationController _animationController;
late Animation<double> _expandAnimation;
#override
void dispose() {
super.dispose();
_animationController.dispose();
}
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_expandAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}
#override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _layerLink,
child: InkWell(
onTap: _toggleDropdown,
child: Text('Click Me'), //Define your child here
),
);
}
OverlayEntry _createOverlayEntry() {
return OverlayEntry(
builder: (context) => GestureDetector(
onTap: () => _toggleDropdown(close: true),
behavior: HitTestBehavior.translucent,
// full screen container to register taps anywhere and close drop down
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Stack(
children: [
Positioned(
left: 100,
top: 100.0,
width: 250,
child: CompositedTransformFollower(
//use offset to control where your dropdown appears
offset: Offset(0, 20),
link: _layerLink,
showWhenUnlinked: false,
child: Material(
elevation: 2,
borderRadius: BorderRadius.circular(6),
borderOnForeground: true,
color: Colors.white,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.grey),
),
child: SizeTransition(
axisAlignment: 1,
sizeFactor: _expandAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
//These are the options that appear in the dropdown
Text('Option 1'),
Text('Option 2'),
Text('Option 3'),
Text('Option 4'),
Text('Option 5'),
],
),
),
),
),
),
),
],
),
),
),
);
}
void _toggleDropdown({
bool close = false,
}) async {
if (_isOpen || close) {
_animationController.reverse().then((value) {
_overlayEntry.remove();
if (mounted) {
setState(() {
_isOpen = false;
});
}
});
} else {
_overlayEntry = _createOverlayEntry();
Overlay.of(context)!.insert(_overlayEntry);
setState(() => _isOpen = true);
_animationController.forward();
}
}
}
Here's a gif to show the ui:
I am trying to build a list view with cards with different animation behaviors depending on user interactions like hovering. The hovered card needs to be scaled up and the remaining cards in the list need to be scaled down and made less opaque.
Like:
Expected smoothness
View code:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:simple_animations/multi_tween/multi_tween.dart';
import 'package:simple_animations/simple_animations.dart';
import 'package:simple_animations/stateless_animation/custom_animation.dart';
import '../controllers/work_controller.dart';
class WorkView extends GetView<WorkController> {
#override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
flex: 1,
child: Container(color: Colors.blueGrey),
),
Expanded(
flex: 2,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
10,
(index) {
return MouseRegion(
onExit: (event) => controller.hoverIndex.value = -1,
onEnter: ((event) {
print("Setting index: $index");
controller.hoverIndex.value = index;
}),
child: AnimatedWorkCard(
index: index,
));
},
),
),
))
],
);
}
}
class AnimatedWorkCard extends StatefulWidget {
final int index;
const AnimatedWorkCard({
Key? key,
required this.index,
}) : super(key: key);
#override
State<AnimatedWorkCard> createState() => _AnimatedWorkCardState();
}
enum CardAnimationProps {
isNotHoveredOpacity,
isHoveredImgScale,
isNotHoveredTranslateX,
isHoveredTextTranslateY,
isHoveredTextOpacity,
}
class _AnimatedWorkCardState extends State<AnimatedWorkCard> {
final WorkController controller = Get.find();
var animationControl = CustomAnimationControl.play;
#override
Widget build(BuildContext context) {
return Obx(() {
var isHovered = controller.hoverIndex.value == widget.index;
if (!isHovered) {
return CustomAnimation<TimelineValue<CardAnimationProps>>(
control: animationControl,
tween: createNotHoveredTween(),
builder: (context, child, value) {
return Transform.translate(
offset: Offset(
value.get(CardAnimationProps.isNotHoveredTranslateX), 0),
child: child,
);
},
child: Card(
color: Colors.amber,
child: Container(
width: Get.width * 0.2,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Work ${widget.index}',
style: TextStyle(
fontSize: Get.width * 0.03,
),
),
ElevatedButton(
child: Text('Hovering: ${controller.hoverIndex.value}'),
onPressed: () {
Get.toNamed('/work/${widget.index}');
},
),
],
),
),
),
);
} else {
return CustomAnimation<TimelineValue<CardAnimationProps>>(
control: animationControl,
tween: createTween(controller.hoverIndex.value, widget.index),
builder: (context, child, value) {
return Transform.scale(
scale: value.get(CardAnimationProps.isHoveredImgScale),
child: child ?? Container());
},
child: Card(
color: Colors.amber,
child: Container(
width: Get.width * 0.2,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Work ${widget.index}',
style: TextStyle(
fontSize: Get.width * 0.03,
),
),
ElevatedButton(
child: Text('Hovering: ${controller.hoverIndex.value}'),
onPressed: () {
Get.toNamed('/work/${widget.index}');
},
),
],
),
),
),
);
}
});
}
createTween(int hoverIndex, int index) {
TimelineTween timelineTween = TimelineTween<CardAnimationProps>();
var scene =
timelineTween.addScene(begin: 0.seconds, end: 2000.milliseconds);
scene.animate(
CardAnimationProps.isHoveredImgScale,
tween: Tween<double>(begin: 1, end: 1.3),
);
scene.animate(
CardAnimationProps.isHoveredTextOpacity,
tween: Tween<double>(begin: 0, end: 1),
);
scene.animate(
CardAnimationProps.isHoveredTextTranslateY,
tween: Tween<double>(begin: 20, end: 0),
);
return timelineTween;
}
createNotHoveredTween() {
TimelineTween timelineTween = TimelineTween<CardAnimationProps>();
var scene =
timelineTween.addScene(begin: 0.seconds, end: 2000.milliseconds);
scene.animate(
CardAnimationProps.isNotHoveredOpacity,
tween: Tween<double>(begin: 1, end: 0.2),
);
scene.animate(
CardAnimationProps.isNotHoveredTranslateX,
tween: Tween<double>(
begin: 0,
end: 20,
),
);
return timelineTween;
}
}
Controller code:
import 'package:get/get.dart';
class WorkController extends GetxController {
RxInt hoverIndex = (-1).obs;
#override
void onReady() {
super.onReady();
}
#override
void onClose() {}
}
But the animation is not smooth and it's just jumping from the states.
Any idea how this can made smoother or any other way this can be thought of implementing?
Animation demo
I want to slide out my first widget from right and slide in second from left of screen.
I'm trying to use AnimatedSwitcher with SlideTransition
my current code bug is that first widget doesn't slide out and just vanishes
here is my complete code snippet.
Any help would be appriciated
class LoginPage extends StatefulWidget {
LoginPage({Key? key}) : super(key: key);
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage>
with SingleTickerProviderStateMixin {
static const int PIN_CODE_LENGTH = 4;
final TextEditingController _mobileController = TextEditingController();
final TextEditingController _pinController = TextEditingController();
final UniqueKey _mobileKey = UniqueKey();
final UniqueKey _pinKey = UniqueKey();
bool _submittable = false;
bool _isLoginStepOne = true;
String _buttonText = Strings.next;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Directionality(
textDirection: TextDirection.rtl,
child: SingleChildScrollView(
child: SizedBox(
height: SizePercentConfig.screenHeight,
child: Column(
children: [
_buildHeader(),
Expanded(
child: _buildForm(),
),
],
),
),
),
),
);
}
Widget _buildHeader() {
return Container(
height: SizePercentConfig.safeBlockVertical * 60,
child: Stack(
children: [
Positioned(
bottom: 0,
right: SizePercentConfig.blockSizeHorizontal * 30,
left: SizePercentConfig.blockSizeHorizontal * 30,
child: Image.asset(
Assets.logo,
fit: BoxFit.fitWidth,
),
),
Container(
height: SizePercentConfig.safeBlockVertical * 50,
child: Stack(
children: [
Positioned(
bottom: 0,
child: Image.asset(
Assets.loginHeader,
width: SizePercentConfig.screenWidth,
fit: BoxFit.fitWidth,
),
),
],
),
),
],
),
);
}
Widget _buildForm() {
return Form(
onChanged: _validate,
child: Padding(
padding: const EdgeInsets.all(Dimens.unitX2),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: const Duration(seconds: 1),
transitionBuilder: (Widget child, Animation<double> animation) {
final inAnimation = Tween<Offset>(
begin: Offset(1.0, 0.0), end: Offset(0.0, 0.0))
.animate(animation);
final outAnimation = Tween<Offset>(
begin: Offset(-1.0, 0.0), end: Offset(0.0, 0.0))
.animate(animation);
print('** child key: ${child.key}');
print('** mobile key: $_mobileKey');
print('** pin key: $_pinKey');
if (child.key == _mobileKey) {
// in animation
print('>>>>>>> first statement');
return ClipRect(
child: SlideTransition(
position: inAnimation,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: child,
),
),
);
} else {
// out animation
print('>>>>>>> second statement');
return ClipRect(
child: SlideTransition(
position: outAnimation,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: child,
),
),
);
}
},
layoutBuilder:
(Widget? currentChild, List<Widget> previousChildren) {
return currentChild!;
},
child: _isLoginStepOne
? AppTextField(
key: _mobileKey,
controller: _mobileController,
hint: Strings.mobileNumber,
textInputType: TextInputType.phone,
)
: _buildPinCode()),
SizedBox(height: Dimens.unitX2),
AppSolidButton(
onPressed: _buttonAction,
text: _buttonText,
width: SizePercentConfig.screenWidth,
enabled: _submittable,
),
SizedBox(height: Dimens.unitX2),
],
),
),
);
}
void _validate() {
if (_isLoginStepOne) {
if (Regex.mobileRegex.hasMatch(_mobileController.value.text) !=
_submittable)
setState(() {
print('--> setState called in _validate');
_submittable = !_submittable;
});
} else {
if ((_pinController.value.text.length == 4) != _submittable)
setState(() {
print('--> setState called in _validate');
_submittable = !_submittable;
});
}
}
void _buttonAction() {
if (_submittable) {
setState(() {
print('--> setState called in _buttonPressed');
_isLoginStepOne = false;
_submittable = false;
_buttonText = Strings.login;
});
} else {}
}
Widget _buildPinCode() {
return Directionality(
textDirection: TextDirection.ltr,
child: PinCodeTextField(
key: _pinKey,
controller: _pinController,
appContext: context,
length: PIN_CODE_LENGTH,
onChanged: (_) {},
enablePinAutofill: true,
enableActiveFill: true,
textStyle: TextStyle(color: Palette.scorpion),
pinTheme: PinTheme(
shape: PinCodeFieldShape.circle,
fieldHeight: SizePercentConfig.safeBlockHorizontal * 20,
fieldWidth: SizePercentConfig.safeBlockHorizontal * 20,
activeFillColor: Palette.concrete,
inactiveFillColor: Palette.concrete,
selectedFillColor: Palette.roseBud,
activeColor: Palette.concrete,
disabledColor: Palette.concrete,
inactiveColor: Palette.concrete,
selectedColor: Palette.roseBud,
),
cursorColor: Palette.transparent,
keyboardType: TextInputType.number,
),
);
}
}
Give your ClipRect widgets unique keys:
If the "new" child is the same widget type and key as the "old" child, but with different parameters, then AnimatedSwitcher will not do a transition between them, since as far as the framework is concerned, they are the same widget and the existing widget can be updated with the new parameters. To force the transition to occur, set a Key on each child widget that you wish to be considered unique (typically a ValueKey on the widget data that distinguishes this child from the others).
I have a simple Card with an "Expand" button that toggles visibility of an _expandedCardBody method that returns a Column.
bool expandedCard = false;
Widget _expandedCardBody() {
return Column(
children: <Widget>[
Row(...),
Row(...),
Row(...),
]);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: EdgeInsets.all(10),
child: ListView(
children: <Widget>[
Text("Setup my budget", style: kTitleStyle),
Card(
color: Colors.white,
child: Column(
children: <Widget>[
ListTile(
leading: SvgPicture.asset(
"assets/images/icon-eating-out.svg",
width: 65),
title: Text('Eating out', style: kNormalStyle),
subtitle:
Text('AED 1,245 this month', style: kSmallStyle),
trailing: SizedBox(
width: 75,
child: TextFormField(
keyboardType: TextInputType.number,
decoration: InputDecoration(hintText: "AED.."),
style: kNormalStyle,
),
),
),
expandedCard ? _expandedCardBody() : SizedBox(),
Divider(),
Container(
alignment: Alignment.topLeft,
child: FlatButton(
child: Text(expandedCard ? 'COLLAPSE' : 'EXPAND'),
onPressed: () {
setState(() {
expandedCard = !expandedCard;
});
},
),
),
],
),
)
],
)));
}
It works, but this is what it looks like:
Instead of simply showing/hiding _expandedCardBody, I'd like to animate it's height.
I've tried using AnimatedContainer like so, but it requires knowing the height of the _expandedCardBody (which I do not).
AnimatedContainer(
child: _expandedCardBody(),
height: expandedCard ? 200 : 0, // 🤔 don't know what the height is.
duration: Duration(milliseconds: 250),
),
How can I animate the height of the Card body?
Use SingleTickerProvider with your state class
for eg like this :
class YourClass extends State<YourClass>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
Animation<double> animation;
var isDetailOpened = false;
#override
void initState() {
_animationController =
AnimationController(duration: Duration(milliseconds: 200), vsync: this);
_animationController.addListener(() {
setState(() {});
});
seeMoreEnabled = false;
animation =
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut);
super.initState();
}
#override
dispose() {
_animationController.dispose();
super.dispose();
}
then Replace your Text (Expand or collapse) with flatButton or CupertinoButton
after doing that onPress or onClick method
onPressed: () {
if (isDetailOpened) {
_animationController.reverse();
} else {
_animationController.forward();
}
isDetailOpened = !isDetailOpened;
},
after that Put your widgets in SizeTransition
SizeTransition(
axisAlignment: 1.0,
sizeFactor: animation,
(your other widgets)
This is an example for an ideal purpose (in simply way)
I'm new to flutter and I want to implement something like this: (klook app)
It's basically a button being shown when the user scrolls a bit.
I tried different things with a SliverAppBar and SliverStickyHeader, but I can't make it work like this. I also played with Opacity and Visibility but it moves my hole view and does not 'overlap' my banner/searchby widget.
My code so far:
class _ExplorePageState extends State<ExplorePage> {
ScrollController _scrollController;
bool lastStatus = true;
_scrollListener() {
if (isShrink != lastStatus) {
print("listen");
setState(() {
lastStatus = isShrink;
});
}
}
bool get isShrink {
return _scrollController.hasClients &&
_scrollController.offset > (400 - kToolbarHeight);
}
#override
void initState() {
_scrollController = ScrollController();
_scrollController.addListener(_scrollListener);
super.initState();
}
#override
void dispose() {
_scrollController.removeListener(_scrollListener());
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverStickyHeader(
header: Visibility(
child: Container(
color: Colors.red,
height: isShrink ? 100 : 0,
child: Text('Header 1'),
),
visible: isShrink ? true : false,
maintainState: true,
maintainSize: true,
maintainAnimation: true,
),
sliver: SliverList(
delegate: SliverChildListDelegate(
[
BannerWidget(),
ButtonWidget(),
],
),
),
),
],
),
);
}
}
The BannerWidget and ButtomWidget are two Containers similar to the app shown above.
I hope you can help me or tell me maybe what this behaviour is called.
Thank you!
If you're ok with using CustomScrollView, you could use SliverPersistentHeader with your own delegate. It will allow you to access current header scroll state and make your own layout depending on how much space you have left.
const double _kSearchHeight = 50.0;
const double _kHeaderHeight = 250.0;
class _ExplorePageState extends State<ExplorePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
delegate: DelegateWithSearchBar(),
pinned: true,
),
SliverList(
delegate: SliverChildListDelegate(
[
for (int i = 0; i < 4; i++)
Container(
height: 200,
child: Text('test'),
color: Colors.black26
),
],
),
)
],
),
),
);
}
}
class DelegateWithSearchBar extends SliverPersistentHeaderDelegate {
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final showSearchBar = shrinkOffset > _kHeaderHeight - _kSearchHeight;
return Stack(
children: <Widget>[
AnimatedOpacity(
opacity: !showSearchBar ? 1 : 0,
duration: Duration(milliseconds: 100),
child: LayoutBuilder(
builder: (context, constraints) {
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage('xxx'),
fit: BoxFit.cover
)
),
height: constraints.maxHeight,
child: SafeArea(
child: Container(
padding: EdgeInsets.only(left: 20, bottom: 20),
alignment: Alignment.bottomLeft,
child: Text(
'Sample Text',
style: TextStyle(color: Colors.white, fontSize: 22)
),
),
),
);
}
),
),
AnimatedOpacity(
opacity: showSearchBar ? 1 : 0,
duration: Duration(milliseconds: 100),
child: Container(
height: _kSearchHeight,
color: Colors.white,
alignment: Alignment.center,
child: Text('search bar')
),
),
],
);
}
#override
bool shouldRebuild(SliverPersistentHeaderDelegate _) => true;
#override
double get maxExtent => _kHeaderHeight;
#override
double get minExtent => _kSearchHeight;
}