i am using this plugin for swiping. one of the flow of the app is to,
swipe up to view the user Profile.
but if i swipe up to view the profile it will remove the user from the list, which is not what i want...
i want it that, if i swipe up it should not remove from the list. i have try to disable swipe up but not navigating to profile page.
i have download the plugin to edit but i cant figure out where is removing from the list..
here is the plugin code
library flutter_tindercard;
import 'dart:math';
import 'package:flutter/material.dart';
enum TriggerDirection { none, right, left, up, down }
/// A Tinder-Like Widget.
class TinderSwapCard extends StatefulWidget {
final CardBuilder _cardBuilder;
final int _totalNum;
final int _stackNum;
final int _animDuration;
final double _swipeEdge;
final double _swipeEdgeVertical;
final bool _swipeUp;
final bool _swipeDown;
final bool _allowVerticalMovement;
final CardSwipeCompleteCallback? swipeCompleteCallback;
final CardDragUpdateCallback? swipeUpdateCallback;
final CardController? cardController;
final List<Size> _cardSizes = [];
final List<Alignment> _cardAligns = [];
#override
_TinderSwapCardState createState() => _TinderSwapCardState();
/// Constructor requires Card Widget Builder [cardBuilder] and
/// your card count [totalNum]
/// option includes:
/// stack orientation [orientation], number of card display
/// in same time [stackNum], [swipeEdge] is the edge to determine
/// action(recover or swipe) when you release your swiping card it is the
/// value of alignment, 0.0 means middle, so it need bigger than zero.
/// and size control params;
TinderSwapCard({
required CardBuilder cardBuilder,
required int totalNum,
AmassOrientation orientation = AmassOrientation.bottom,
int stackNum = 3,
int animDuration = 800,
double swipeEdge = 3.0,
double swipeEdgeVertical = 8.0,
bool swipeUp = false,
bool swipeDown = false,
double? maxWidth,
double? maxHeight,
double? minWidth,
double? minHeight,
bool allowVerticalMovement = true,
this.cardController,
this.swipeCompleteCallback,
this.swipeUpdateCallback,
}) : assert(stackNum > 1),
assert(swipeEdge > 0),
assert(swipeEdgeVertical > 0),
assert(maxWidth! > minWidth! && maxHeight! > minHeight!),
_cardBuilder = cardBuilder,
_totalNum = totalNum,
_stackNum = stackNum,
_animDuration = animDuration,
_swipeEdge = swipeEdge,
_swipeEdgeVertical = swipeEdgeVertical,
_swipeUp = swipeUp,
_swipeDown = swipeDown,
_allowVerticalMovement = allowVerticalMovement {
final widthGap = maxWidth! - minWidth!;
final heightGap = maxHeight! - minHeight!;
for (var i = 0; i < _stackNum; i++) {
_cardSizes.add(
Size(minWidth + (widthGap / _stackNum) * i,
minHeight + (heightGap / _stackNum) * i),
);
switch (orientation) {
case AmassOrientation.bottom:
_cardAligns.add(
Alignment(
0.0,
(0.5 / (_stackNum - 1)) * (stackNum - i),
),
);
break;
case AmassOrientation.top:
_cardAligns.add(
Alignment(
0.0,
(-0.5 / (_stackNum - 1)) * (stackNum - i),
),
);
break;
case AmassOrientation.left:
_cardAligns.add(
Alignment(
(-0.5 / (_stackNum - 1)) * (stackNum - i),
0.0,
),
);
break;
case AmassOrientation.right:
_cardAligns.add(
Alignment(
(0.5 / (_stackNum - 1)) * (stackNum - i),
0.0,
),
);
break;
}
}
}
}
class _TinderSwapCardState extends State<TinderSwapCard>
with TickerProviderStateMixin {
late Alignment frontCardAlign;
late AnimationController _animationController;
late int _currentFront;
static TriggerDirection? _trigger;
Widget _buildCard(BuildContext context, int realIndex) {
if (realIndex < 0) {
return Container();
}
final index = realIndex - _currentFront;
if (index == widget._stackNum - 1) {
return Align(
alignment: _animationController.status == AnimationStatus.forward
? frontCardAlign = CardAnimation.frontCardAlign(
_animationController,
frontCardAlign,
widget._cardAligns[widget._stackNum - 1],
widget._swipeEdge,
widget._swipeUp,
widget._swipeDown,
).value
: frontCardAlign,
child: Transform.rotate(
angle: (pi / 180.0) *
(_animationController.status == AnimationStatus.forward
? CardAnimation.frontCardRota(
_animationController, frontCardAlign.x)
.value
: frontCardAlign.x),
child: SizedBox.fromSize(
size: widget._cardSizes[index],
child: widget._cardBuilder(
context,
widget._totalNum - realIndex - 1,
),
),
),
);
}
return Align(
alignment: _animationController.status == AnimationStatus.forward &&
(frontCardAlign.x > 3.0 ||
frontCardAlign.x < -3.0 ||
frontCardAlign.y > 3 ||
frontCardAlign.y < -3)
? CardAnimation.backCardAlign(
_animationController,
widget._cardAligns[index],
widget._cardAligns[index + 1],
).value
: widget._cardAligns[index],
child: SizedBox.fromSize(
size: _animationController.status == AnimationStatus.forward &&
(frontCardAlign.x > 3.0 ||
frontCardAlign.x < -3.0 ||
frontCardAlign.y > 3 ||
frontCardAlign.y < -3)
? CardAnimation.backCardSize(
_animationController,
widget._cardSizes[index],
widget._cardSizes[index + 1],
).value
: widget._cardSizes[index],
child: widget._cardBuilder(
context,
widget._totalNum - realIndex - 1,
),
),
);
}
List<Widget> _buildCards(BuildContext context) {
final cards = <Widget>[];
for (var i = _currentFront; i < _currentFront + widget._stackNum; i++) {
cards.add(_buildCard(context, i));
}
cards.add(SizedBox.expand(
child: GestureDetector(
onPanUpdate: (final details) {
setState(() {
if (widget._allowVerticalMovement == true) {
frontCardAlign = Alignment(
frontCardAlign.x +
details.delta.dx * 20 / MediaQuery.of(context).size.width,
frontCardAlign.y +
details.delta.dy * 30 / MediaQuery.of(context).size.height,
);
} else {
frontCardAlign = Alignment(
frontCardAlign.x +
details.delta.dx * 20 / MediaQuery.of(context).size.width,
0,
);
if (widget.swipeUpdateCallback != null) {
widget.swipeUpdateCallback!(details, frontCardAlign);
}
}
if (widget.swipeUpdateCallback != null) {
widget.swipeUpdateCallback!(details, frontCardAlign);
}
});
},
onPanEnd: (final details) {
animateCards(TriggerDirection.none);
},
),
));
return cards;
}
void animateCards(TriggerDirection trigger) {
if (_animationController.isAnimating ||
_currentFront + widget._stackNum == 0) {
return;
}
_trigger = trigger;
_animationController.stop();
_animationController.value = 0.0;
_animationController.forward();
}
void triggerSwap(TriggerDirection trigger) {
animateCards(trigger);
}
// support for asynchronous data events
#override
void didUpdateWidget(covariant TinderSwapCard oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget._totalNum != oldWidget._totalNum) {
_initState();
}
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
_initState();
}
void _initState() {
_currentFront = widget._totalNum - widget._stackNum;
frontCardAlign = widget._cardAligns[widget._cardAligns.length - 1];
_animationController = AnimationController(
vsync: this,
duration: Duration(
milliseconds: widget._animDuration,
),
);
_animationController.addListener(() => setState(() {}));
_animationController.addStatusListener(
(final status) {
final index = widget._totalNum - widget._stackNum - _currentFront;
if (status == AnimationStatus.completed) {
CardSwipeOrientation orientation;
if (frontCardAlign.x < -widget._swipeEdge) {
orientation = CardSwipeOrientation.left;
} else if (frontCardAlign.x > widget._swipeEdge) {
orientation = CardSwipeOrientation.right;
} else if (frontCardAlign.y < -widget._swipeEdgeVertical) {
orientation = CardSwipeOrientation.up;
} else if (frontCardAlign.y > widget._swipeEdgeVertical) {
orientation = CardSwipeOrientation.down;
} else {
frontCardAlign = widget._cardAligns[widget._stackNum - 1];
orientation = CardSwipeOrientation.recover;
}
if (widget.swipeCompleteCallback != null) {
widget.swipeCompleteCallback!(orientation, index);
}
if (orientation != CardSwipeOrientation.recover) changeCardOrder();
}
},
);
}
#override
Widget build(BuildContext context) {
widget.cardController?.addListener(triggerSwap);
return Stack(children: _buildCards(context));
}
void changeCardOrder() {
setState(() {
_currentFront--;
frontCardAlign = widget._cardAligns[widget._stackNum - 1];
});
}
}
typedef CardBuilder = Widget Function(BuildContext context, int index);
enum CardSwipeOrientation { left, right, recover, up, down }
/// swipe card to [CardSwipeOrientation.left] or [CardSwipeOrientation.right]
/// , [CardSwipeOrientation.recover] means back to start.
typedef CardSwipeCompleteCallback = void Function(
CardSwipeOrientation orientation, int index);
/// [DragUpdateDetails] of swiping card.
typedef CardDragUpdateCallback = void Function(
DragUpdateDetails details, Alignment align);
enum AmassOrientation { top, bottom, left, right }
class CardAnimation {
static Animation<Alignment> frontCardAlign(
AnimationController controller,
Alignment beginAlign,
Alignment baseAlign,
double swipeEdge,
bool swipeUp,
bool swipeDown,
) {
double endX, endY;
if (_TinderSwapCardState._trigger == TriggerDirection.none) {
endX = beginAlign.x > 0
? (beginAlign.x > swipeEdge ? beginAlign.x + 10.0 : baseAlign.x)
: (beginAlign.x < -swipeEdge ? beginAlign.x - 10.0 : baseAlign.x);
endY = beginAlign.x > 3.0 || beginAlign.x < -swipeEdge
? beginAlign.y
: baseAlign.y;
if (swipeUp || swipeDown) {
if (beginAlign.y < 0) {
if (swipeUp) {
endY =
beginAlign.y < -swipeEdge ? beginAlign.y - 10.0 : baseAlign.y;
}
} else if (beginAlign.y > 0) {
if (swipeDown) {
endY = beginAlign.y > swipeEdge ? beginAlign.y + 10.0 : baseAlign.y;
}
}
}
} else if (_TinderSwapCardState._trigger == TriggerDirection.left) {
endX = beginAlign.x - swipeEdge;
endY = beginAlign.y + 0.5;
}
/* Trigger Swipe Up or Down */
else if (_TinderSwapCardState._trigger == TriggerDirection.up ||
_TinderSwapCardState._trigger == TriggerDirection.down) {
var beginY =
_TinderSwapCardState._trigger == TriggerDirection.up ? -10 : 10;
endY = beginY < -swipeEdge ? beginY - 10.0 : baseAlign.y;
endX = beginAlign.x > 0
? (beginAlign.x > swipeEdge ? beginAlign.x + 10.0 : baseAlign.x)
: (beginAlign.x < -swipeEdge ? beginAlign.x - 10.0 : baseAlign.x);
} else {
endX = beginAlign.x + swipeEdge;
endY = beginAlign.y + 0.5;
}
return AlignmentTween(
begin: beginAlign,
end: Alignment(endX, endY),
).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOut,
),
);
}
static Animation<double> frontCardRota(
AnimationController controller, double beginRot) {
return Tween(begin: beginRot, end: 0.0).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOut,
),
);
}
static Animation<Size?> backCardSize(
AnimationController controller,
Size beginSize,
Size endSize,
) {
return SizeTween(begin: beginSize, end: endSize).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOut,
),
);
}
static Animation<Alignment> backCardAlign(
AnimationController controller,
Alignment beginAlign,
Alignment endAlign,
) {
return AlignmentTween(begin: beginAlign, end: endAlign).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOut,
),
);
}
}
typedef TriggerListener = void Function(TriggerDirection trigger);
class CardController {
late TriggerListener? _listener;
void triggerLeft() {
if (_listener != null) {
_listener!(TriggerDirection.left);
}
}
void triggerRight() {
if (_listener != null) {
_listener!(TriggerDirection.right);
}
}
void triggerUp() {
if (_listener != null) {
_listener!(TriggerDirection.up);
}
}
void triggerDown() {
if (_listener != null) {
_listener!(TriggerDirection.down);
}
}
// ignore: use_setters_to_change_properties
void addListener(final TriggerListener listener) {
_listener = listener;
}
void removeListener() {
_listener = null;
}
}
i pass in the current index to swipeUpdateCallback then set SwipeUp to false...
here is the full sample code
import 'dart:async';
import 'dart:math';
import 'package:card/card.dart';
import 'package:card/up.dart';
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
HomePage({Key? key}) : super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
late StreamController<List<String>> _streamController;
class _HomePageState extends State<HomePage> {
List<String> welcomeImages = [
"assets/welcome0.png",
"assets/welcome1.png",
"assets/welcome2.png",
];
#override
initState() {
_streamController = StreamController<List<String>>();
super.initState();
}
void _addToStream() {
Random random = Random();
int index = random.nextInt(3);
welcomeImages.add('assets/welcome$index.png');
_streamController.add(welcomeImages);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("asynchronous data events demo"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Added image appears on top:',
),
StreamBuilder<List<String>>(
stream: _streamController.stream,
initialData: welcomeImages,
builder:
(BuildContext context, AsyncSnapshot<List<String>> snapshot) {
print('snapshot.data.length: ${snapshot.data!.length}');
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('Add image');
case ConnectionState.waiting:
case ConnectionState.active:
return _asyncDataExample(
context,
snapshot.data!,
(CardSwipeOrientation orientation) {
// welcomeImages[0] is the swiped card
// you can send to backend service in here
welcomeImages.removeAt(0);
_streamController.add(welcomeImages);
},
);
case ConnectionState.done:
return Text('\$${snapshot.data} (closed)');
}
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _addToStream,
tooltip: 'Add image',
child: Icon(Icons.add),
),
);
}
Widget _asyncDataExample(
BuildContext context, List<String> imageList, Function onSwipe) {
CardController controller; //Use this to trigger swap.
return Center(
child: Container(
height: MediaQuery.of(context).size.height * 0.6,
child: TinderSwapCard(
orientation: AmassOrientation.bottom,
totalNum: imageList.length,
stackNum: 4,
swipeEdge: 4.0,
maxWidth: MediaQuery.of(context).size.width * 0.9,
maxHeight: MediaQuery.of(context).size.width * 0.9,
minWidth: MediaQuery.of(context).size.width * 0.8,
minHeight: MediaQuery.of(context).size.width * 0.8,
cardBuilder: (context, index) => Card(
child: Image.asset('${imageList[index]}'),
),
cardController: controller = CardController(),
swipeUpdateCallback:
(DragUpdateDetails details, Alignment align, int index) {
/// Get swiping card's alignment
if (align.y < -10) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => SwipeUp()));
}
},
swipeCompleteCallback: (CardSwipeOrientation orientation, int index) {
if (orientation != CardSwipeOrientation.recover) {
onSwipe(orientation);
}
if (orientation == CardSwipeOrientation.up) {}
},
),
),
);
}
}
Related
That's what you read. I don't want to hide AppBar when scrolling, there's a lot of info on that.
What I want is the exact opposite. I want my homepage to open with no AppBar and then, when the user starts scrolling, the appbar will be displayed.
This website does exactly what I want to reproduce: https://www.kirschnerbrasil.cc/ (in the desktop version).
I guess I need do use the SliverAppBar, but I haven't manage to do so yet. Can anyone help?
Thanks!
In that case, you will have to make your custom widget the appbar. Please have a look into below code, it will help you to understand the procedure:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// Height of your Container
static final _containerHeight = 100.0;
// You don't need to change any of these variables
var _fromTop = -_containerHeight;
var _controller = ScrollController();
var _allowReverse = true, _allowForward = true;
var _prevOffset = 0.0;
var _prevForwardOffset = -_containerHeight;
var _prevReverseOffset = 0.0;
#override
void initState() {
super.initState();
_controller.addListener(_listener);
}
// entire logic is inside this listener for ListView
void _listener() {
double offset = _controller.offset;
var direction = _controller.position.userScrollDirection;
if (direction == ScrollDirection.reverse) {
_allowForward = true;
if (_allowReverse) {
_allowReverse = false;
_prevOffset = offset;
_prevForwardOffset = _fromTop;
}
var difference = offset - _prevOffset;
_fromTop = _prevForwardOffset + difference;
if (_fromTop > 0) _fromTop = 0;
} else if (direction == ScrollDirection.forward) {
_allowReverse = true;
if (_allowForward) {
_allowForward = false;
_prevOffset = offset;
_prevReverseOffset = _fromTop;
}
var difference = offset - _prevOffset;
_fromTop = _prevReverseOffset + difference;
if (_fromTop < -_containerHeight) _fromTop = -_containerHeight;
}
setState(() {}); // for simplicity I'm calling setState here, you can put bool values to only call setState when there is a genuine change in _fromTop
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("ListView")),
body: Stack(
children: <Widget>[
_yourListView(),
Positioned(
top: _fromTop,
left: 0,
right: 0,
child: _yourContainer(),
)
],
),
);
}
Widget _yourListView() {
return ListView.builder(
itemCount: 100,
controller: _controller,
itemBuilder: (_, index) => ListTile(title: Text("Item $index")),
);
}
Widget _yourContainer() {
return Opacity(
opacity: 1 - (-_fromTop / _containerHeight),
child: Container(
height: _containerHeight,
color: Colors.red,
alignment: Alignment.center,
child: Text("Your Container", style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white)),
),
);
}
}
Heres the code i edit from #Gourango Sutradhar answer, if you want the top to disappear only when it reach the top
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// Height of your Container
static final _containerHeight = 100.0;
// You don't need to change any of these variables
var _fromTop = -_containerHeight;
var _controller = ScrollController();
var _allowReverse = true, _allowForward = true;
var _prevOffset = 0.0;
var _prevForwardOffset = -_containerHeight;
var _prevReverseOffset = 0.0;
#override
void initState() {
super.initState();
_controller.addListener(_listener);
}
// entire logic is inside this listener for ListView
void _listener() {
double offset = _controller.offset;
var direction = _controller.position.userScrollDirection;
if (direction == ScrollDirection.reverse) {
_allowForward = true;
if (_allowReverse) {
_allowReverse = false;
_prevOffset = offset;
_prevForwardOffset = _fromTop;
}
var difference = offset - _prevOffset;
_fromTop = _prevForwardOffset + difference;
if (_fromTop > 0) _fromTop = 0;
} else if (direction == ScrollDirection.forward) {
_allowReverse = true;
if (_allowForward) {
_allowForward = false;
_prevReverseOffset = _fromTop;
}
var difference = offset - _prevOffset;
if (offset > 100.0) {
_prevOffset = offset;
}
if (offset < 100.0) {
_fromTop = _prevReverseOffset + difference;
if (_fromTop < -_containerHeight) _fromTop = -_containerHeight;
}
}
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("ListView")),
body: Stack(
children: <Widget>[
_yourListView(),
Positioned(
top: _fromTop,
left: 0,
right: 0,
child: _yourContainer(),
)
],
),
);
}
Widget _yourListView() {
return ListView.builder(
itemCount: 100,
controller: _controller,
itemBuilder: (_, index) => ListTile(title: Text("Item $index")),
);
}
Widget _yourContainer() {
return Opacity(
opacity: 1,
child: Container(
height: _containerHeight,
color: Colors.red,
alignment: Alignment.center,
child: Text("Your Container", style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white)),
),
);
}
}
I want to create condition that if first api didn't finish load when click on bottom navigation tab it will not navigate to other tab. So I want to pass boolean that will change to true when api is loaded, the problem is I don't know how to pass this dynamic value to my custom Bottom Navigation Bar. Is there an other way instead of using global variable?
This is my code.
landingPage.dart
// This global variable is change to true when receive callback from api
bool isComplete = false;
class _NavigationItem {
_NavigationItem(this.iconFile, this.caption, this.page);
final String iconFile;
final String caption;
final Widget page;
}
class LandingPage extends StatefulWidget {
final String username;
LandingPage({Key key, this.username}) : super(key: key);
#override
_LandingPageState createState() => _LandingPageState();
}
class _LandingPageState extends State<LandingPage> {
int _selectedIndex = 0;
List<_NavigationItem> _navItems = [];
double _bottomNavBarHeight = 46;
double _iconSize = 26;
double _circleSize = 52;
int _animationDuration = 300;
double _circleStrokeWidth = 0;
BottomNavigationController _navigationController;
#override
void didChangeDependencies() {
// Context of a state is available to us from the moment the State loads its dependencies
// Since we need context object so it need to be accessed inside this overridden method.
_navItems = [
_NavigationItem(
"assets/icons/home.svg",
S.of(context).landing_nav_home,
HomePage(
username: widget.username,
callback: (value) { isComplete = value; }, // receiving callback data
)),
_NavigationItem("assets/icons/friend.svg",
S.of(context).landing_nav_friend, FriendPage()),
_NavigationItem(
"assets/icons/chat.svg", S.of(context).landing_nav_chat, ChatPage()),
_NavigationItem("assets/icons/widget.svg",
S.of(context).landing_nav_widget, WidgetPage()),
_NavigationItem("assets/icons/more.svg",
S.of(context).landing_nav_setting, SettingPage()),
];
super.didChangeDependencies();
}
#override
void initState() {
super.initState();
_navigationController = new BottomNavigationController(_selectedIndex);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Container(
padding: EdgeInsets.only(bottom: 90),
child: _navItems.elementAt(_selectedIndex).page,
),
_createBottomNavigationBar()
],
)
);
}
void _onNavigationBarItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
Widget _createBottomNavigationBar() {
return Align(alignment: Alignment.bottomCenter, child: _bottomNav());
}
Widget _bottomNav() {
List<TabItem> tabItems = List.of([
new TabItem(Icons.home_outlined, "", CommonColor.accent,
asset: "assets/icons/home.svg"),
new TabItem(Icons.person, "", CommonColor.accent,
asset: "assets/icons/friends.svg"),
new TabItem(Icons.chat_bubble_outline, "", CommonColor.accent),
new TabItem(Icons.widgets_outlined, "", CommonColor.accent),
new TabItem(Icons.more_horiz, "", CommonColor.accent),
]);
return BottomNavigation(
tabItems,
controller: _navigationController,
barHeight: _bottomNavBarHeight,
iconsSize: _iconSize,
circleSize: _circleSize,
selectedIconColor: Colors.white,
normalIconColor: CommonColor.accent,
circleStrokeWidth: _circleStrokeWidth,
barBackgroundColor: Colors.white,
animationDuration: Duration(milliseconds: _animationDuration),
selectedCallback: (int selectedPos) {
print("selected: $_selectedIndex");
_onNavigationBarItemTapped(selectedPos);
},
);
}
#override
void dispose() {
super.dispose();
_navigationController.dispose();
}
}
BottomNavigation.dart
typedef BottomNavSelectedCallback = Function(int selectedPos);
class BottomNavigation extends StatefulWidget {
final List<TabItem> tabItems;
final int selectedPos;
final double barHeight;
final double padding;
final Color barBackgroundColor;
final double circleSize;
final double circleStrokeWidth;
final double iconsSize;
final Color selectedIconColor;
final Color normalIconColor;
final Duration animationDuration;
final BottomNavSelectedCallback selectedCallback;
final BottomNavigationController controller;
BottomNavigation(this.tabItems,
{this.selectedPos = 0,
this.barHeight = 60,
this.barBackgroundColor = Colors.white,
this.circleSize = 58,
this.circleStrokeWidth = 4,
this.iconsSize = 32,
this.padding = 16,
this.selectedIconColor = Colors.white,
this.normalIconColor = Colors.deepPurpleAccent,
this.animationDuration = const Duration(milliseconds: 300),
this.selectedCallback,
this.controller})
: assert(tabItems != null && tabItems.length >= 2 && tabItems.length <= 5,
"tabItems is required");
#override
State<StatefulWidget> createState() => _BottomNavigationState();
}
class _BottomNavigationState extends State<BottomNavigation>
with TickerProviderStateMixin {
Curve _animationsCurve = Cubic(0.27, 1.21, .77, 1.09);
AnimationController itemsController;
Animation<double> selectedPosAnimation;
Animation<double> itemsAnimation;
List<double> _itemsSelectedState;
int selectedPos;
int previousSelectedPos;
BottomNavigationController _controller;
#override
void initState() {
super.initState();
if (widget.controller != null) {
_controller = widget.controller;
previousSelectedPos = selectedPos = _controller.value;
} else {
previousSelectedPos = selectedPos = widget.selectedPos;
_controller = BottomNavigationController(selectedPos);
}
_controller.addListener(_newSelectedPosNotify);
_itemsSelectedState = List.generate(widget.tabItems.length, (index) {
return selectedPos == index ? 1.0 : 0.0;
});
itemsController = new AnimationController(
vsync: this, duration: widget.animationDuration);
itemsController.addListener(() {
setState(() {
_itemsSelectedState.asMap().forEach((i, value) {
if (i == previousSelectedPos) {
_itemsSelectedState[previousSelectedPos] =
1.0 - itemsAnimation.value;
} else if (i == selectedPos) {
_itemsSelectedState[selectedPos] = itemsAnimation.value;
} else {
_itemsSelectedState[i] = 0.0;
}
});
});
});
selectedPosAnimation = makeSelectedPosAnimation(
selectedPos.toDouble(), selectedPos.toDouble());
itemsAnimation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: itemsController, curve: _animationsCurve));
}
Animation<double> makeSelectedPosAnimation(double begin, double end) {
return Tween(begin: begin, end: end).animate(
CurvedAnimation(parent: itemsController, curve: _animationsCurve));
}
void onSelectedPosAnimate() {
setState(() {});
}
void _newSelectedPosNotify() {
_setSelectedPos(widget.controller.value);
}
#override
Widget build(BuildContext context) {
double fullWidth = MediaQuery.of(context).size.width;
double fullHeight =
widget.barHeight + (widget.circleSize / 2) + widget.circleStrokeWidth;
double sectionsWidth = (fullWidth / 1.2) / widget.tabItems.length;
//Create the boxes Rect
List<Rect> boxes = List();
widget.tabItems.asMap().forEach((i, tabItem) {
double left = (i + 0.5) * sectionsWidth;
double top = fullHeight - widget.barHeight;
double right = left + sectionsWidth;
double bottom = fullHeight;
boxes.add(Rect.fromLTRB(left, top, right, bottom));
});
List<Widget> children = List();
// This is the full view transparent background (have free space for circle)
children.add(Padding(
padding: EdgeInsets.only(bottom: widget.padding),
child: Container(
width: fullWidth,
height: fullHeight,
)));
// This is the bar background (bottom section of our view)
children.add(Positioned.fill(
child: Padding(
padding: EdgeInsets.only(
left: widget.padding,
right: widget.padding,
bottom: widget.padding),
child: Container(
width: MediaQuery.of(context).size.width,
height: widget.barHeight,
decoration: BoxDecoration(
color: widget.barBackgroundColor,
borderRadius: BorderRadius.all(Radius.circular(20)),
boxShadow: [
new BoxShadow(color: Colors.black12, blurRadius: 8.0)
]),
),
),
top: fullHeight - widget.barHeight,
));
// This is the circle handle on selected
children.add(new Positioned(
child: Container(
width: widget.circleSize,
height: widget.circleSize,
child: Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(14)),
color: widget.barBackgroundColor),
),
Container(
margin: EdgeInsets.all(widget.circleStrokeWidth),
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(14)),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
CommonColor.primary,
widget.tabItems[selectedPos].circleColor,
CommonColor.slipBg,
])),
),
],
),
),
left: boxes[selectedPos].center.dx - (widget.circleSize / 2),
top: 0,
));
//Here are the Icons and texts of items
boxes.asMap().forEach((int pos, Rect r) {
// Icon
Color iconColor = pos == selectedPos
? widget.selectedIconColor
: widget.normalIconColor;
double scaleFactor = pos == selectedPos ? 1.2 : 1.0;
children.add(
Positioned(
child: Transform.scale(
scale: scaleFactor,
child: widget.tabItems[pos].asset != null
? SvgPicture.asset(widget.tabItems[pos].asset,
color: iconColor,
width: widget.iconsSize,
height: widget.iconsSize,
fit: BoxFit.cover)
: Icon(
widget.tabItems[pos].icon,
size: widget.iconsSize,
color: iconColor,
),
),
left: r.center.dx - (widget.iconsSize / 2),
top: r.center.dy -
(widget.iconsSize / 2) -
(_itemsSelectedState[pos] *
((widget.barHeight / 2) + widget.circleStrokeWidth)),
),
);
if (pos != selectedPos) {
children.add(Positioned.fromRect(
child: GestureDetector(
onTap: () {
_controller.value = pos;
},
),
rect: r,
));
}
});
return Stack(
children: children,
);
}
void _setSelectedPos(int pos) {
previousSelectedPos = selectedPos;
selectedPos = pos;
itemsController.forward(from: 0.0);
selectedPosAnimation = makeSelectedPosAnimation(
previousSelectedPos.toDouble(), selectedPos.toDouble());
selectedPosAnimation.addListener(onSelectedPosAnimate);
if (widget.selectedCallback != null) {
widget.selectedCallback(selectedPos);
}
}
#override
void dispose() {
super.dispose();
itemsController.dispose();
_controller.removeListener(_newSelectedPosNotify);
}
}
class BottomNavigationController extends ValueNotifier<int> {
BottomNavigationController(int value) : super(value);
}
You should use a FutureBuilder/ StreamBuilder to listen to an event after your api call is finished. Something like this:
Future<dynamic> yourApiCall() async {
// Execute your functions
return someValue;
}
Here in the UI, you can listen to the output of this function with:
FutureBuilder(
future: yourApiCall(),
builder: (context, snapshot) {
return BottomNavigation(
tabItems,
// ...other properties
selectedCallback: (int selectedPos) {
print("selected: $_selectedIndex");
if (snapshot.hasData) // Here you check if the snapshot.data is different from null. That means your api has finished and returned some value
_onNavigationBarItemTapped(selectedPos); // Only then, you notify the callback function
},
);
});
I would like to animate between the background colors of two pages in flutter. I am talking about page transitions, but instead of transitioning the whole page I just want to change the background color of the new page (from the previous page's bg color to a new color), while the rest of the foreground content fades in (or any other type of transition).
If you want more clarification, I want something like a Hero transition, but with background color of the pages.
The background color need not be just a color property of some container.
edit: to answer easily, let say my page is callable by specifying a color to its constructor. So the page is something like the following,
class MyWidget extends StatelessWidget {
final Color bgColor;
const MyWidget(this.bgColor);
#override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
Container(color: bgColor),
// foreground widgets...
],
);
}
}
What should be my approach? Should I create something like a custom transition? In that case, how can I animate the background color?
Modify Hero with ColoredBox, plus add new NavigatorObserver to navigatorObservers of MaterialApp
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
const App({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
navigatorObservers: [ColorHeroController()],
home: Screen1(),
);
}
}
class Screen1 extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('This is color hero'),
Padding(
padding: EdgeInsets.only(bottom: 50, left: 50),
child: ColorHero(
color: Colors.blue,
tag: 'tag',
child: Container(
height: 50,
width: 100,
),
),
),
RaisedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Screen2(),
),
);
},
child: Text('go'),
),
],
),
),
);
}
}
class Screen2 extends StatelessWidget {
const Screen2({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('page2')),
body: Center(
child: Row(
children: [
Text('Page2'),
ColorHero(
tag: 'tag',
color: Colors.red,
child: Container(
height: 100,
width: 100,
),
),
],
),
),
);
}
}
typedef CreateRectTween = Tween<Rect> Function(Rect begin, Rect end);
typedef HeroPlaceholderBuilder = Widget Function(
BuildContext context,
Size heroSize,
Widget child,
);
typedef HeroFlightShuttleBuilder = Widget Function(
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
);
typedef _OnFlightEnded = void Function(_HeroFlight flight);
enum HeroFlightDirection { push, pop }
Rect _boundingBoxFor(BuildContext context, [BuildContext ancestorContext]) {
final RenderBox box = context.findRenderObject() as RenderBox;
assert(box != null && box.hasSize);
return MatrixUtils.transformRect(
box.getTransformTo(ancestorContext?.findRenderObject()),
Offset.zero & box.size,
);
}
class ColorHero extends StatefulWidget {
const ColorHero({
#required this.color,
Key key,
#required this.tag,
this.createRectTween,
this.flightShuttleBuilder,
this.placeholderBuilder,
this.transitionOnUserGestures = false,
#required this.child,
}) : assert(tag != null),
assert(transitionOnUserGestures != null),
assert(child != null),
super(key: key);
final Object tag;
final Color color;
final CreateRectTween createRectTween;
final Widget child;
final HeroFlightShuttleBuilder flightShuttleBuilder;
final HeroPlaceholderBuilder placeholderBuilder;
final bool transitionOnUserGestures;
static Map<Object, _ColorHeroState> _allHeroesFor(
BuildContext context,
bool isUserGestureTransition,
NavigatorState navigator,
) {
assert(context != null);
assert(isUserGestureTransition != null);
assert(navigator != null);
final Map<Object, _ColorHeroState> result = <Object, _ColorHeroState>{};
void inviteHero(StatefulElement hero, Object tag) {
assert(() {
if (result.containsKey(tag)) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'There are multiple heroes that share the same tag within a subtree.'),
ErrorDescription(
'Within each subtree for which heroes are to be animated (i.e. a PageRoute subtree), '
'each Hero must have a unique non-null tag.\n'
'In this case, multiple heroes had the following tag: $tag\n'),
DiagnosticsProperty<StatefulElement>(
'Here is the subtree for one of the offending heroes', hero,
linePrefix: '# ', style: DiagnosticsTreeStyle.dense),
]);
}
return true;
}());
final ColorHero heroWidget = hero.widget as ColorHero;
final _ColorHeroState heroState = hero.state as _ColorHeroState;
if (!isUserGestureTransition || heroWidget.transitionOnUserGestures) {
result[tag] = heroState;
} else {
heroState.ensurePlaceholderIsHidden();
}
}
void visitor(Element element) {
final Widget widget = element.widget;
if (widget is ColorHero) {
final StatefulElement hero = element as StatefulElement;
final Object tag = widget.tag;
assert(tag != null);
if (Navigator.of(hero) == navigator) {
inviteHero(hero, tag);
} else {
final ModalRoute<dynamic> heroRoute = ModalRoute.of(hero);
if (heroRoute != null &&
heroRoute is PageRoute &&
heroRoute.isCurrent) {
inviteHero(hero, tag);
}
}
}
element.visitChildren(visitor);
}
context.visitChildElements(visitor);
return result;
}
#override
_ColorHeroState createState() => _ColorHeroState();
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Object>('tag', tag));
}
}
class _ColorHeroState extends State<ColorHero> {
final GlobalKey _key = GlobalKey();
Size _placeholderSize;
bool _shouldIncludeChild = true;
void startFlight({bool shouldIncludedChildInPlaceholder = false}) {
_shouldIncludeChild = shouldIncludedChildInPlaceholder;
assert(mounted);
final RenderBox box = context.findRenderObject() as RenderBox;
assert(box != null && box.hasSize);
setState(() {
_placeholderSize = box.size;
});
}
void ensurePlaceholderIsHidden() {
if (mounted) {
setState(() {
_placeholderSize = null;
});
}
}
void endFlight({bool keepPlaceholder = false}) {
if (!keepPlaceholder) {
ensurePlaceholderIsHidden();
}
}
#override
Widget build(BuildContext context) {
assert(context.findAncestorWidgetOfExactType<ColorHero>() == null,
'A Hero widget cannot be the descendant of another Hero widget.');
final bool showPlaceholder = _placeholderSize != null;
if (showPlaceholder && widget.placeholderBuilder != null) {
return widget.placeholderBuilder(context, _placeholderSize, widget.child);
}
if (showPlaceholder && !_shouldIncludeChild) {
return SizedBox(
width: _placeholderSize.width,
height: _placeholderSize.height,
);
}
return SizedBox(
width: _placeholderSize?.width,
height: _placeholderSize?.height,
child: Offstage(
offstage: showPlaceholder,
child: TickerMode(
enabled: !showPlaceholder,
child: KeyedSubtree(
key: _key,
child: ColoredBox(
color: widget.color,
child: widget.child,
),
),
),
),
);
}
}
class _HeroFlightManifest {
_HeroFlightManifest({
#required this.type,
#required this.overlay,
#required this.navigatorRect,
#required this.fromRoute,
#required this.toRoute,
#required this.fromHero,
#required this.toHero,
#required this.createRectTween,
#required this.shuttleBuilder,
#required this.isUserGestureTransition,
#required this.isDiverted,
}) : assert(fromHero.widget.tag == toHero.widget.tag);
final HeroFlightDirection type;
final OverlayState overlay;
final Rect navigatorRect;
final PageRoute<dynamic> fromRoute;
final PageRoute<dynamic> toRoute;
final _ColorHeroState fromHero;
final _ColorHeroState toHero;
final CreateRectTween createRectTween;
final HeroFlightShuttleBuilder shuttleBuilder;
final bool isUserGestureTransition;
final bool isDiverted;
Object get tag => fromHero.widget.tag;
Animation<double> get animation {
return CurvedAnimation(
parent: (type == HeroFlightDirection.push)
? toRoute.animation
: fromRoute.animation,
curve: Curves.fastOutSlowIn,
reverseCurve: isDiverted ? null : Curves.fastOutSlowIn.flipped,
);
}
#override
String toString() {
return '_HeroFlightManifest($type tag: $tag from route: ${fromRoute.settings} '
'to route: ${toRoute.settings} with hero: $fromHero to $toHero)';
}
}
class _HeroFlight {
_HeroFlight(this.onFlightEnded) {
_proxyAnimation = ProxyAnimation()
..addStatusListener(_handleAnimationUpdate);
}
final _OnFlightEnded onFlightEnded;
Tween<Rect> heroRectTween;
Tween<Color> colorTween;
Widget shuttle;
Animation<double> _heroOpacity = kAlwaysCompleteAnimation;
ProxyAnimation _proxyAnimation;
_HeroFlightManifest manifest;
OverlayEntry overlayEntry;
bool _aborted = false;
Tween<Rect> _doCreateRectTween(Rect begin, Rect end) {
final CreateRectTween createRectTween =
manifest.toHero.widget.createRectTween ?? manifest.createRectTween;
if (createRectTween != null) return createRectTween(begin, end);
return RectTween(begin: begin, end: end);
}
static final Animatable<double> _reverseTween =
Tween<double>(begin: 1.0, end: 0.0);
Widget _buildOverlay(BuildContext context) {
assert(manifest != null);
shuttle ??= manifest.shuttleBuilder(
context,
manifest.animation,
manifest.type,
manifest.fromHero.context,
manifest.toHero.context,
);
assert(shuttle != null);
return AnimatedBuilder(
animation: _proxyAnimation,
child: shuttle,
builder: (BuildContext context, Widget child) {
final RenderBox toHeroBox =
manifest.toHero.context?.findRenderObject() as RenderBox;
if (_aborted || toHeroBox == null || !toHeroBox.attached) {
if (_heroOpacity.isCompleted) {
_heroOpacity = _proxyAnimation.drive(
_reverseTween.chain(
CurveTween(curve: Interval(_proxyAnimation.value, 1.0))),
);
}
} else if (toHeroBox.hasSize) {
final RenderBox finalRouteBox =
manifest.toRoute.subtreeContext?.findRenderObject() as RenderBox;
final Offset toHeroOrigin =
toHeroBox.localToGlobal(Offset.zero, ancestor: finalRouteBox);
if (toHeroOrigin != heroRectTween.end.topLeft) {
final Rect heroRectEnd = toHeroOrigin & heroRectTween.end.size;
heroRectTween =
_doCreateRectTween(heroRectTween.begin, heroRectEnd);
}
}
final Rect rect = heroRectTween.evaluate(_proxyAnimation);
final Size size = manifest.navigatorRect.size;
final RelativeRect offsets = RelativeRect.fromSize(rect, size);
final color = ColorTween(
begin: manifest.fromHero.widget.color,
end: manifest.toHero.widget.color,
).evaluate(_proxyAnimation);
return Positioned(
top: offsets.top,
right: offsets.right,
bottom: offsets.bottom,
left: offsets.left,
child: IgnorePointer(
child: RepaintBoundary(
child: Opacity(
opacity: _heroOpacity.value,
child: ColoredBox(
color: color,
child: child,
),
),
),
),
);
},
);
}
void _handleAnimationUpdate(AnimationStatus status) {
if (manifest.fromRoute?.navigator?.userGestureInProgress == true) return;
if (status == AnimationStatus.completed ||
status == AnimationStatus.dismissed) {
_proxyAnimation.parent = null;
assert(overlayEntry != null);
overlayEntry.remove();
overlayEntry = null;
manifest.fromHero
.endFlight(keepPlaceholder: status == AnimationStatus.completed);
manifest.toHero
.endFlight(keepPlaceholder: status == AnimationStatus.dismissed);
onFlightEnded(this);
}
}
void start(_HeroFlightManifest initialManifest) {
assert(!_aborted);
assert(() {
final Animation<double> initial = initialManifest.animation;
assert(initial != null);
final HeroFlightDirection type = initialManifest.type;
assert(type != null);
switch (type) {
case HeroFlightDirection.pop:
return initial.value == 1.0 && initialManifest.isUserGestureTransition
? initial.status == AnimationStatus.completed
: initial.status == AnimationStatus.reverse;
case HeroFlightDirection.push:
return initial.value == 0.0 &&
initial.status == AnimationStatus.forward;
}
return null;
}());
manifest = initialManifest;
if (manifest.type == HeroFlightDirection.pop)
_proxyAnimation.parent = ReverseAnimation(manifest.animation);
else
_proxyAnimation.parent = manifest.animation;
manifest.fromHero.startFlight(
shouldIncludedChildInPlaceholder:
manifest.type == HeroFlightDirection.push);
manifest.toHero.startFlight();
heroRectTween = _doCreateRectTween(
_boundingBoxFor(
manifest.fromHero.context, manifest.fromRoute.subtreeContext),
_boundingBoxFor(manifest.toHero.context, manifest.toRoute.subtreeContext),
);
overlayEntry = OverlayEntry(builder: _buildOverlay);
manifest.overlay.insert(overlayEntry);
}
void divert(_HeroFlightManifest newManifest) {
assert(manifest.tag == newManifest.tag);
if (manifest.type == HeroFlightDirection.push &&
newManifest.type == HeroFlightDirection.pop) {
assert(newManifest.animation.status == AnimationStatus.reverse);
assert(manifest.fromHero == newManifest.toHero);
assert(manifest.toHero == newManifest.fromHero);
assert(manifest.fromRoute == newManifest.toRoute);
assert(manifest.toRoute == newManifest.fromRoute);
_proxyAnimation.parent = ReverseAnimation(newManifest.animation);
heroRectTween = ReverseTween<Rect>(heroRectTween);
} else if (manifest.type == HeroFlightDirection.pop &&
newManifest.type == HeroFlightDirection.push) {
assert(newManifest.animation.status == AnimationStatus.forward);
assert(manifest.toHero == newManifest.fromHero);
assert(manifest.toRoute == newManifest.fromRoute);
_proxyAnimation.parent = newManifest.animation.drive(
Tween<double>(
begin: manifest.animation.value,
end: 1.0,
),
);
if (manifest.fromHero != newManifest.toHero) {
manifest.fromHero.endFlight(keepPlaceholder: true);
newManifest.toHero.startFlight();
heroRectTween = _doCreateRectTween(
heroRectTween.end,
_boundingBoxFor(
newManifest.toHero.context, newManifest.toRoute.subtreeContext),
);
} else {
heroRectTween =
_doCreateRectTween(heroRectTween.end, heroRectTween.begin);
}
} else {
assert(manifest.fromHero != newManifest.fromHero);
assert(manifest.toHero != newManifest.toHero);
heroRectTween = _doCreateRectTween(
heroRectTween.evaluate(_proxyAnimation),
_boundingBoxFor(
newManifest.toHero.context, newManifest.toRoute.subtreeContext),
);
shuttle = null;
if (newManifest.type == HeroFlightDirection.pop)
_proxyAnimation.parent = ReverseAnimation(newManifest.animation);
else
_proxyAnimation.parent = newManifest.animation;
manifest.fromHero.endFlight(keepPlaceholder: true);
manifest.toHero.endFlight(keepPlaceholder: true);
newManifest.fromHero.startFlight(
shouldIncludedChildInPlaceholder:
newManifest.type == HeroFlightDirection.push);
newManifest.toHero.startFlight();
overlayEntry.markNeedsBuild();
}
_aborted = false;
manifest = newManifest;
}
void abort() {
_aborted = true;
}
#override
String toString() {
final RouteSettings from = manifest.fromRoute.settings;
final RouteSettings to = manifest.toRoute.settings;
final Object tag = manifest.tag;
return 'HeroFlight(for: $tag, from: $from, to: $to ${_proxyAnimation.parent})';
}
}
class ColorHeroController extends NavigatorObserver {
ColorHeroController({this.createRectTween});
final CreateRectTween createRectTween;
final Map<Object, _HeroFlight> _flights = <Object, _HeroFlight>{};
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
assert(navigator != null);
assert(route != null);
_maybeStartHeroTransition(
previousRoute, route, HeroFlightDirection.push, false);
}
#override
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
assert(navigator != null);
assert(route != null);
if (!navigator.userGestureInProgress)
_maybeStartHeroTransition(
route, previousRoute, HeroFlightDirection.pop, false);
}
#override
void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
assert(navigator != null);
if (newRoute?.isCurrent == true) {
_maybeStartHeroTransition(
oldRoute, newRoute, HeroFlightDirection.push, false);
}
}
#override
void didStartUserGesture(Route<dynamic> route, Route<dynamic> previousRoute) {
assert(navigator != null);
assert(route != null);
_maybeStartHeroTransition(
route, previousRoute, HeroFlightDirection.pop, true);
}
#override
void didStopUserGesture() {
if (navigator.userGestureInProgress) return;
bool isInvalidFlight(_HeroFlight flight) {
return flight.manifest.isUserGestureTransition &&
flight.manifest.type == HeroFlightDirection.pop &&
flight._proxyAnimation.isDismissed;
}
final List<_HeroFlight> invalidFlights =
_flights.values.where(isInvalidFlight).toList(growable: false);
for (final _HeroFlight flight in invalidFlights) {
flight._handleAnimationUpdate(AnimationStatus.dismissed);
}
}
void _maybeStartHeroTransition(
Route<dynamic> fromRoute,
Route<dynamic> toRoute,
HeroFlightDirection flightType,
bool isUserGestureTransition,
) {
if (toRoute != fromRoute &&
toRoute is PageRoute<dynamic> &&
fromRoute is PageRoute<dynamic>) {
final PageRoute<dynamic> from = fromRoute;
final PageRoute<dynamic> to = toRoute;
final Animation<double> animation =
(flightType == HeroFlightDirection.push)
? to.animation
: from.animation;
switch (flightType) {
case HeroFlightDirection.pop:
if (animation.value == 0.0) {
return;
}
break;
case HeroFlightDirection.push:
if (animation.value == 1.0) {
return;
}
break;
}
if (isUserGestureTransition &&
flightType == HeroFlightDirection.pop &&
to.maintainState) {
_startHeroTransition(
from, to, animation, flightType, isUserGestureTransition);
} else {
to.offstage = to.animation.value == 0.0;
WidgetsBinding.instance.addPostFrameCallback((Duration value) {
_startHeroTransition(
from, to, animation, flightType, isUserGestureTransition);
});
}
}
}
void _startHeroTransition(
PageRoute<dynamic> from,
PageRoute<dynamic> to,
Animation<double> animation,
HeroFlightDirection flightType,
bool isUserGestureTransition,
) {
if (navigator == null ||
from.subtreeContext == null ||
to.subtreeContext == null) {
to.offstage = false;
return;
}
final Rect navigatorRect = _boundingBoxFor(navigator.context);
final Map<Object, _ColorHeroState> fromHeroes = ColorHero._allHeroesFor(
from.subtreeContext, isUserGestureTransition, navigator);
final Map<Object, _ColorHeroState> toHeroes = ColorHero._allHeroesFor(
to.subtreeContext, isUserGestureTransition, navigator);
to.offstage = false;
for (final Object tag in fromHeroes.keys) {
if (toHeroes[tag] != null) {
final HeroFlightShuttleBuilder fromShuttleBuilder =
fromHeroes[tag].widget.flightShuttleBuilder;
final HeroFlightShuttleBuilder toShuttleBuilder =
toHeroes[tag].widget.flightShuttleBuilder;
final bool isDiverted = _flights[tag] != null;
final _HeroFlightManifest manifest = _HeroFlightManifest(
type: flightType,
overlay: navigator.overlay,
navigatorRect: navigatorRect,
fromRoute: from,
toRoute: to,
fromHero: fromHeroes[tag],
toHero: toHeroes[tag],
createRectTween: createRectTween,
shuttleBuilder: toShuttleBuilder ??
fromShuttleBuilder ??
_defaultHeroFlightShuttleBuilder,
isUserGestureTransition: isUserGestureTransition,
isDiverted: isDiverted,
);
if (isDiverted)
_flights[tag].divert(manifest);
else
_flights[tag] = _HeroFlight(_handleFlightEnded)..start(manifest);
} else if (_flights[tag] != null) {
_flights[tag].abort();
}
}
for (final Object tag in toHeroes.keys) {
if (fromHeroes[tag] == null) toHeroes[tag].ensurePlaceholderIsHidden();
}
}
void _handleFlightEnded(_HeroFlight flight) {
_flights.remove(flight.manifest.tag);
}
static final HeroFlightShuttleBuilder _defaultHeroFlightShuttleBuilder = (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
final ColorHero toHero = toHeroContext.widget as ColorHero;
return toHero.child;
};
}
I am trying to make a scrollable ListView to the next element so that it is always at the beginning (or center) of the page (as in PageView)
My problem is inertia, the backward movement of an element after scrolling.
How can I implements item's behavior without inertia?
Code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<ScrollController> _horizontalControllers;
ScrollController _verticalController;
var _itemCountHorizontal = 15;
bool _inProgress;
Orientation get isPortrait => MediaQuery.of(context).orientation;
double get _height => MediaQuery.of(context).size.height;
double get _width => MediaQuery.of(context).size.width;
double get horizontalPadding {
double _padd;
if (isPortrait == Orientation.portrait) {
_padd = _width * 0.01;
} else {
_padd = _width * 0.01;
}
return _padd;
}
double get verticalPadding {
double _padd;
if (isPortrait == Orientation.portrait) {
_padd = cardHeight * 0.005;
} else {
_padd = (_height - cardHeight) / 2;
}
return _padd;
}
double get cardHeight {
double cardH;
if (isPortrait == Orientation.portrait) {
cardH = cardWidth * 1.7;
} else {
cardH = _height * 0.9;
}
return cardH;
}
double get cardWidth {
var cardW = _width * 0.99;
if (cardW > _height / 1.7) {
cardW = _height / 1.77;
}
return cardW;
}
#override
void initState() {
_horizontalControllers = [
ScrollController(),
ScrollController(),
ScrollController(),
ScrollController(),
ScrollController(),
];
_verticalController = ScrollController();
_inProgress = false;
super.initState();
}
#override
void dispose() {
_horizontalControllers.forEach((element) {
element.dispose();
});
_verticalController.dispose();
super.dispose();
}
void _onEndScrollVertical(ScrollMetrics metrics) {
print("scroll before = ${metrics.extentBefore}");
print("scroll after = ${metrics.extentAfter}");
print("scroll inside = ${metrics.extentInside}");
print("index = ${metrics.axisDirection}");
print("item HEIGHT => $cardHeight");
final topPadd = MediaQuery.of(context).padding.top;
print('TOPPPPPPPPPP $topPadd');
/* int point = metrics.extentAfter ~/ (_height - topPadd);
var offset = (_height - topPadd) * point;
_inProgress = true;
Future.delayed(Duration(milliseconds: 100), () {
_verticalController.animateTo(offset,
duration: Duration(milliseconds: 1000), curve: Curves.fastOutSlowIn);
});
_inProgress = false;
*/
var halfOfTheHeight = cardHeight / 2;
var offsetOfItem = metrics.extentBefore % cardHeight;
if (offsetOfItem < halfOfTheHeight) {
final offset = metrics.extentBefore - offsetOfItem;
print("offsetOfItem1 = $offsetOfItem offset = $offset");
Future.delayed(Duration(milliseconds: 50), () {
_verticalController.animateTo(offset,
duration: Duration(milliseconds: 1000),
curve: Curves.fastOutSlowIn);
});
} else if (offsetOfItem > halfOfTheHeight) {
final offset = metrics.extentBefore + offsetOfItem;
print("offsetOfItem2 = $offsetOfItem offset = $offset");
Future.delayed(Duration(milliseconds: 50), () {
_verticalController.animateTo(offset,
duration: Duration(milliseconds: 1000),
curve: Curves.fastOutSlowIn);
});
}
}
void _onEndScrollHorizontal(ScrollMetrics metrics, int index) {
print("scroll before = ${metrics.extentBefore}");
print("scroll after = ${metrics.extentAfter}");
print("scroll inside = ${metrics.extentInside}");
print("item WIDTH => $cardWidth");
var halfOfTheWidth = _width / 2;
var offsetOfItem = metrics.extentBefore % _width;
if (offsetOfItem < halfOfTheWidth) {
final offset = metrics.extentBefore - offsetOfItem;
print("offsetOfItem1 = $offsetOfItem offset = $offset");
_inProgress = true;
Future.delayed(Duration(milliseconds: 10), () {
_horizontalControllers[index].animateTo(offset,
duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
});
_inProgress = false;
} else if (offsetOfItem > halfOfTheWidth) {
_inProgress = true;
final offset = metrics.extentBefore + offsetOfItem;
print("offsetOfItem2 = $offsetOfItem offset = $offset");
Future.delayed(Duration(milliseconds: 10), () {
_horizontalControllers[index].animateTo(offset,
duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
});
_inProgress = false;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification &&
scrollNotification.depth == 0) {
if (!_inProgress) {
print('ScrollEndNotification ===> $scrollNotification');
_onEndScrollVertical(scrollNotification.metrics);
}
}
return null;
},
child: buildListViewVertical(),
),
),
);
}
Widget buildListViewVertical() {
return ListView.builder(
itemCount: _itemCountHorizontal,
itemExtent: cardHeight,
controller: _verticalController,
itemBuilder: (BuildContext context, int index) {
return NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification &&
scrollNotification.depth == 0) {
print('ScrollEndNotification ===> $scrollNotification');
_onEndScrollHorizontal(scrollNotification.metrics, index);
}
return null;
},
child: buildListViewHorizontal(index));
},
);
}
Widget buildListViewHorizontal(int index) {
return ListView.builder(
controller: _horizontalControllers[index],
physics: ClampingScrollPhysics(),
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: _itemCountHorizontal + 1,
itemBuilder: (BuildContext context, int index) =>
index < _itemCountHorizontal
? Padding(
padding: EdgeInsets.only(
left: horizontalPadding,
right: horizontalPadding,
top: verticalPadding,
bottom: verticalPadding,
),
child: Container(height: 340, width: 200, color: Colors.red),
)
: SizedBox(
width: 50,
),
);
}
}
Here is a working example for DartPad
Update:
I add CustomScrollPhysics() to ListView, and that solution removed the inertia on reverse motions. Howewer, inertia persisted when moving from index 0 and above...
Code:
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
class CustomScrollPhysics extends ScrollPhysics {
const CustomScrollPhysics({ScrollPhysics parent}) : super(parent: parent);
#override
SpringDescription get spring => SpringDescription(damping: 0.1);
#override
CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomScrollPhysics(parent: buildParent(ancestor));
}
}
you can use DraggableScrollableSheet and SingleChildScrollView Widgets
first you need to change you widget into statefullwidget then you need to use Global key to scroll to the position that you want. and the position that you want to scroll to "widget ex." must have a name .
I'm looking to recreate Snapchat's back-to-back video format in Flutter. Since video_player is lacking callbacks for when the video finishes (and is otherwise prone to callback hell), I was wondering if anyone has some pointers for building something like this.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
void main() {
runApp(MaterialApp(
title: 'My app', // used by the OS task switcher
home: MyHomePage(),
));
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<VideoPlayerController> _controllers = [];
VoidCallback listener;
bool _isPlaying = false;
int _current = 0;
#override
void initState() {
super.initState();
// Add some sample videos
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
this.tick();
// Try refreshing by brute force (this isn't going too well)
new Timer.periodic(Duration(milliseconds: 100), (Timer t) {
int delta = 99999999;
if(_controllers[_current].value != null) {
delta = (_controllers[_current].value.duration.inMilliseconds - _controllers[_current].value.position.inMilliseconds);
}
print("Tick " + delta.toString());
if(delta < 500) {
_current += 1;
this.tick();
}
});
}
void tick() async {
print("Current: " + _current.toString());
await _controllers[_current].initialize();
await _controllers[_current].play();
print("Ready");
setState((){
_current = _current;
});
}
#override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: _controllers[_current].value.aspectRatio,
// Use the VideoPlayer widget to display the video
child: VideoPlayer(_controllers[_current]),
);
}
}
What I have now plays the first video, but there is a very long delay between the first and second. I believe it has to do with my inability to get rid of the listener attached to the 0th item.
Initializing a network VideoPlayerController may take some time to finish. You can initialize the controller of the next video while playing the current. This will take more memory but I don't think it will create huge problems if you prebuffer only one or two videos. Then when the next or previous buttons get pressed, video will be ready to play.
Here is my workaround. It's functional, it prebuffers previous and next videos, skips to the next video when finishes, shows the current position and buffer, pauses and plays on long press.
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
main() {
runApp(MaterialApp(
home: VideoPlayerDemo(),
));
}
class VideoPlayerDemo extends StatefulWidget {
#override
_VideoPlayerDemoState createState() => _VideoPlayerDemoState();
}
class _VideoPlayerDemoState extends State<VideoPlayerDemo> {
int index = 0;
double _position = 0;
double _buffer = 0;
bool _lock = true;
Map<String, VideoPlayerController> _controllers = {};
Map<int, VoidCallback> _listeners = {};
Set<String> _urls = {
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#1',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#2',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#3',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#4',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#5',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#6',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#7',
};
#override
void initState() {
super.initState();
if (_urls.length > 0) {
_initController(0).then((_) {
_playController(0);
});
}
if (_urls.length > 1) {
_initController(1).whenComplete(() => _lock = false);
}
}
VoidCallback _listenerSpawner(index) {
return () {
int dur = _controller(index).value.duration.inMilliseconds;
int pos = _controller(index).value.position.inMilliseconds;
int buf = _controller(index).value.buffered.last.end.inMilliseconds;
setState(() {
if (dur <= pos) {
_position = 0;
return;
}
_position = pos / dur;
_buffer = buf / dur;
});
if (dur - pos < 1) {
if (index < _urls.length - 1) {
_nextVideo();
}
}
};
}
VideoPlayerController _controller(int index) {
return _controllers[_urls.elementAt(index)];
}
Future<void> _initController(int index) async {
var controller = VideoPlayerController.network(_urls.elementAt(index));
_controllers[_urls.elementAt(index)] = controller;
await controller.initialize();
}
void _removeController(int index) {
_controller(index).dispose();
_controllers.remove(_urls.elementAt(index));
_listeners.remove(index);
}
void _stopController(int index) {
_controller(index).removeListener(_listeners[index]);
_controller(index).pause();
_controller(index).seekTo(Duration(milliseconds: 0));
}
void _playController(int index) async {
if (!_listeners.keys.contains(index)) {
_listeners[index] = _listenerSpawner(index);
}
_controller(index).addListener(_listeners[index]);
await _controller(index).play();
setState(() {});
}
void _previousVideo() {
if (_lock || index == 0) {
return;
}
_lock = true;
_stopController(index);
if (index + 1 < _urls.length) {
_removeController(index + 1);
}
_playController(--index);
if (index == 0) {
_lock = false;
} else {
_initController(index - 1).whenComplete(() => _lock = false);
}
}
void _nextVideo() async {
if (_lock || index == _urls.length - 1) {
return;
}
_lock = true;
_stopController(index);
if (index - 1 >= 0) {
_removeController(index - 1);
}
_playController(++index);
if (index == _urls.length - 1) {
_lock = false;
} else {
_initController(index + 1).whenComplete(() => _lock = false);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Playing ${index + 1} of ${_urls.length}"),
),
body: Stack(
children: <Widget>[
GestureDetector(
onLongPressStart: (_) => _controller(index).pause(),
onLongPressEnd: (_) => _controller(index).play(),
child: Center(
child: AspectRatio(
aspectRatio: _controller(index).value.aspectRatio,
child: Center(child: VideoPlayer(_controller(index))),
),
),
),
Positioned(
child: Container(
height: 10,
width: MediaQuery.of(context).size.width * _buffer,
color: Colors.grey,
),
),
Positioned(
child: Container(
height: 10,
width: MediaQuery.of(context).size.width * _position,
color: Colors.greenAccent,
),
),
],
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(onPressed: _previousVideo, child: Icon(Icons.arrow_back)),
SizedBox(width: 24),
FloatingActionButton(onPressed: _nextVideo, child: Icon(Icons.arrow_forward)),
],
),
);
}
}
All of the logic lives inside the state object, therefore makes it dirty. I might turn this into a package in the future.