How to run fade transition animation from beginning every time in flutter? - flutter

In page view, there is a header that should be changed whenever the page is changed and there is a button to perform this operation. There is a list of texts named headers, and every time the button is pressed the text should be changed with another one with a fade transition animation. Now what I really want to achieve is that when the button is pressed the next text should replace the current text with 0 opacity and within a second its opacity should go up to 1 and it should be performed without any flaws and smoothly every time the page changes.
What I'm struggling with is that I can't set the animation value to 0 when the animation is completed without any side effects to the UI, it shouldn’t be noticeable.
In short, I want the animation value to start from zero every time the button is pressed.
You can watch how the animation is working via the link https://youtube.com/shorts/3aIAWjhZ2AM?feature=share.
The controller.forward(from: 0); is somehow working not as expected...
Here is my business logic:
class AnnouncementController extends ChangeNotifier {
late AnimationController controller;
late Tween<double> tween;
late Animation<double> opacityAnimation;
PageController pageController = PageController();
List<String> headers = [
'Welcome back, Jasurbek',
'What do you offer?',
'What kind of accommodation do you have?',
'Tell the guests about the advantages of your dwelling',
'Add photos of the property',
'Let\'s think of a bright title',
'Let\'s set the price',
];
navigate(int index){
currentPageIndex = index;
notifyListeners();
controller.forward(from: 0);
pageController.animateToPage(
index,
duration: const Duration(milliseconds: 500),
curve: Curves.ease,
);
}
}
Here is my main class:
class _AnnouncementState extends State<Announcement> with SingleTickerProviderStateMixin {
final AnnouncementController viewModel = AnnouncementController();
#override
void initState() {
viewModel.controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
viewModel.tween = Tween(begin: 0.0, end: 1.0);
viewModel.opacityAnimation = viewModel.tween.animate(viewModel.controller);
viewModel.controller.forward();
super.initState();
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context) => viewModel,
child: Consumer<AnnouncementController>(builder: (ctx, model, index) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
height: 250,
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(20),
child: FadeTransition(
opacity: viewModel.opacityAnimation,
child: Text(
viewModel.headers[viewModel.currentPageIndex],
style: TextStyle(
color: Colors.black,
fontSize: 26.sp,
fontWeight: FontWeight.bold,
),
),
)
),
Expanded(
child: PageView(
controller: viewModel.pageController,
physics: const NeverScrollableScrollPhysics(),
children: [
FirstPage(viewModel: viewModel),
SecondPage(viewModel: viewModel),
ThirdPage(viewModel: viewModel),
FourthPage(viewModel: viewModel),
FifthPage(viewModel: viewModel),
],
onPageChanged: (int index) {
viewModel.navigate(index);
},
),
),
],
),
);
}),
);
}
}
Showcase
https://youtube.com/shorts/3aIAWjhZ2AM?feature=share

Please use animated container and you can add your animation in that and put your view inside as child
Document link- https://docs.flutter.dev/cookbook/animation/animated-container

Related

How can I Scroll to the first occurrence of a given string in a Text Widget

say I have a song lyric app and there is just one Scaffold with a Text widget that displays the entire lyric and the lyrics are written in the format
....
Chorus:
...
....
....
and I have a FAB, onClick of which I need the text to auto scroll to the text "Chorus:", this text is literally in every song, but when the verses are a about 4+, they usually go off screen, so, user usually has to manually scroll to the chorus again after each verse that's beyond the screen height, but I need this to be done automatically at the tap of a button
scroll up till the string "chorus" is in view, how would I do this in flutter
TEXT
const kTheAstronomers = '''1. Yeah, you woke up in London
At least that's what you said
I sat here scrollin'
And I saw you with your aunt
A demon on your left
An angel on your right
But you're hypocritical
Always political
Chorus:
Say you mean one thing
But you go ahead and lie
Oh, you lie-lie, lie-lie
And you say you're not the bad type
2. Oh, you posted on Twitter
Said you had other plans
But your mother, she called me
Said, "Come have lunch with the fam"
Guess you didn't tell her that
You should've called me back
I guess you changed your number or somethin\' '''
LYRIC SCREEN
#override
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
body: SafeArea(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10),
child: Text(
kTheAstronomers,
style: const TextStyle(
fontSize: 30,
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
),
),
),
),
)
floatingActionButton: FAB(onPressed: autoScrollToChorus),
,
You can create a GlobalKey and use the currentContext to scroll to the Chorus part.
final _key = GlobalKey()
Inside the autoScrollToChorus method you can add:
final context = _key.currentContext!;
await Scrollable.ensureVisible(context)
I found a way.
I had to change the way I displayed the text, instead of using one text widget, I used a ListView builder to display two texts, but before that, in initState, when my page receives the text, I split the text into a list of two separate texts, one containing the first part and the other containing from the Chorus down, then I give this list to the listview builder (you could also just use a column and create two separate widgets and just pass the scroll key to the second text, knowing it's the second part of the text)
final GlobalKey _key = GlobalKey();
void _autoScrollToChorus() async {
BuildContext context = _key.currentContext!;
await Scrollable.ensureVisible(context);
}
late List<String> lyricList;
#override
initState() {
lyricList =
kTheAstronomers.split(RegExp("(?=chorus)", caseSensitive: false));
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ListView.builder(
itemCount: lyricList.length,
itemBuilder: (context, idx) {
return Text(
key: idx == 1 ? _key : null,
lyricList[idx],
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 30,
),
);
}),
),
floatingActionButton: lyricList.length > 1 ? FloatingActionButton(
onPressed: _autoScrollToChorus,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text("Chorus"),
),
) : null,
);
}
Thanks to #Priyaank I knew to use the key and scroll to a particular widget
a more advanced solution that makes it possible to hide the button when the chorus is in view USING THE SCROLLABLE_POSITIONED_LIST PACKAGE
final GlobalKey _key = GlobalKey();
final ItemScrollController _itemScrollController = ItemScrollController();
final ItemPositionsListener _itemListener = ItemPositionsListener.create();
late List<String> lyricList;
bool chorusIsVisible = true;
void _autoScrollToChorus() {
// BuildContext context = _key.currentContext!;
// await Scrollable.ensureVisible(context);
_itemScrollController.scrollTo(
index: 1,
duration: const Duration(milliseconds: 500),
alignment: 0.5
);
}
#override
initState() {
lyricList =
kTheAstronomers.split(RegExp("(?=chorus)", caseSensitive: false));
super.initState();
if(lyricList.length > 1) {
_itemListener.itemPositions.addListener(() {
chorusIsVisible = _itemListener.itemPositions.value.where((item) {
final isTopVisible = item.itemLeadingEdge >= 0;
return isTopVisible;
}
).map((item) => item.index).toList().contains(1);
setState(() {});
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ScrollablePositionedList.builder(
itemScrollController: _itemScrollController,
itemPositionsListener: _itemListener,
itemCount: lyricList.length,
itemBuilder: (context, idx) {
return Text(
lyricList[idx],
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 30,
),
);
}),
),
floatingActionButton: lyricList.length > 1 && !chorusIsVisible ? FloatingActionButton(
onPressed: _autoScrollToChorus,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text("Chorus"),
),
) : null,
);
}
}

Flutter - how to add animation to a Widget so that it slides into the view when a button is pressed?

Let's make a simple example, given a Column(), I have 2 Containers in it, and a button.
Column(
children: [
MyButton(
label: "Expand me"
onTap: () => setState(() => isOpen = !isOpen)
),
Container(
child: Text("Container 1"),
height: 200
),
if (isOpen)
Container(
child: Text("Container 2"),
height: 150
)
]
)
so basically, if we press the button, the second Container will appear right under the first one, like an expansion panel.
Now I want to add an animation, and I'm having a hard time finding the best fit for my use case, as most solutions look really complex for such a simple task.
The animation is really simple, instead of making the Container 2 appear out of nowhere under the first one, it would be nice if the Container 2 would start behind Container 1, and then slide towards the bottom, until in position.
What is the cleanest way to achieve this in flutter?
import 'package:flutter/material.dart';
// ignore: must_be_immutable
class EasyAnimatedOffset extends StatefulWidget {
EasyAnimatedOffset();
#override
_EasyAnimatedOffsetState createState() => _EasyAnimatedOffsetState();
}
class _EasyAnimatedOffsetState extends State<EasyAnimatedOffset>
with SingleTickerProviderStateMixin {
//Notice the "SingleTickerProviderStateMixin" above
//Must add "AnimationController"
late AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
//change the animation duration for a slower or faster animation.
//For example, replacing 1000 with 5000 will give you a 5x slower animation.
duration: Duration(milliseconds: 1000),
);
}
animateForward() {
_animationController.forward();
//this controller will move the animation forward
//you can also create a reverse animation using "_animationController.reverse()"
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
//the offset has a x value and y value.
//changing the y axis value moves the animation vertically
//changing the x axis value moves the animation horizantaly
double xAxisValue = 0;
double yAxisValue = 10;
return AnimatedBuilder(
animation: _animationController,
// child: widget.child,
builder: (context, child) {
return Transform.translate(
offset: Offset(_animationController.value * xAxisValue,
_animationController.value * yAxisValue),
//add your button or widget here
child: InkWell(
onTap: () {
animateForward();
},
child: Center(
child: Container(
height: 100,
width: 200,
color: Colors.amber,
child: Center(
child: Text(
"Animate Me",
style: TextStyle(
color: Colors.black,
fontSize: 20,
),
),
)),
)));
});
}
}

how to show current play time of video when using video_player plugin in flutter?

Currently using the flutter video_player plugin stream video from the given link. Issue is that I had to hide the normal video interactive interface so that user can't skip the video. Now most of the work is done, just need to know how to display duration and current position of the video been played.
videoController.value.duration.inSeconds gives me the duration part, and videoController.value.position gives the position. But how to keep updating the results for theposition` section?
void checkTimer(){
if(playerController.value.position == playerController.value.duration){
setState(() {
Duration duration = Duration(milliseconds: playerController?.value?.position?.inMilliseconds?.round());
nowTime = [duration.inHours, duration.inMinutes, duration.inSeconds]
.map((seg) => seg.remainder(60).toString().padLeft(2, '0'))
.join(':');
});
}
above code was created to update the time as needed. but now the issue is how to update time. should I use setState() or something else, because the above code is not working for me.
Video is not loaded where then screen is loaded. It's loaded when then users click the play button. so till that time, we don't even have a duration value as data is still on the wayt.
How about using ValueListenableBuilder ?
It will listen to the controller's value and update it every time the value changes.
here's the sample :
ValueListenableBuilder(
valueListenable: playerController,
builder: (context, VideoPlayerValue value, child) {
//Do Something with the value.
return Text(value.position.toString());
},
);
use the built-in widget from the video player plugin.
[See more on their example on github][https://github.com/999eagle/plugins/blob/master/packages/video_player/example/lib/main.dart]
VideoProgressIndicator(
_videoController,//controller
allowScrubbing: true,
colors: VideoProgressColors(
playedColor: primary,
bufferedColor: Colors.red,
backgroundColor: black,
),
)
[1]: https://github.com/999eagle/plugins/blob/master/packages/video_player/example/lib/main.dart
Try this :
Create a new Stateful Widget to display the counter for current position,
Pass videoPlayerController as a parameter in the widget ,
Listen to the videoPlayerController in initState and add setSate to the listened value
Here's the code,
const _currentVideoPositionWidth = 38.0;
const _minTwoDigitValue = 10;
class _CurrentVideoPosition extends StatefulWidget {
const _CurrentVideoPosition({
Key? key,
required this.videoPlayerController,
}) : super(key: key);
final VideoPlayerController videoPlayerController;
#override
_CurrentVideoPositionState createState() => _CurrentVideoPositionState();
}
class _CurrentVideoPositionState extends State<_CurrentVideoPosition> {
int currentDurationInSecond = 0;
#override
void initState() {
widget.videoPlayerController.addListener(
() => setState(() => currentDurationInSecond = widget.videoPlayerController.value.position.inSeconds),
);
super.initState();
}
#override
Widget build(BuildContext context) => Container(
width: _currentVideoPositionWidth,
alignment: Alignment.centerRight,
child: Text(
_formatCurrentPosition(),
style: Theme.of(context).textTheme.bodyText1?.copyWith(
color: Colors.white,
),
maxLines: 1,
),
);
String _formatCurrentPosition() =>
currentDurationInSecond < _minTwoDigitValue ? "0 : 0$currentDurationInSecond" : "0 : $currentDurationInSecond";
}
late VideoPlayerController _phenikaaVideoPlayerController;
late Future<void> _initializeVideoPlayerFuture;
#override
void initState() {
super.initState();
_phenikaaVideoPlayerController = VideoPlayerController.network(
"https://assets-phenikaa-website.s3.ap-southeast-
1.amazonaws.com/media/assets/mo-hinh-3-nha.mp4",
);
// Initialize the controller and store the Future for later use.
_initializeVideoPlayerFuture =
_phenikaaVideoPlayerController.initialize();
}
#override
void dispose() {
_phenikaaVideoPlayerController.dispose();
super.dispose();
}
FutureBuilder(
future: _initializeVideoPlayerFuture,
builder: (_, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Column(
children: [
// If the VideoPlayerController has finished initialization, use
// the data it provides to limit the aspect ratio of the video.
AspectRatio(
aspectRatio:
_phenikaaVideoPlayerController.value.aspectRatio,
// Use the VideoPlayer widget to display the video.
child: VideoPlayer(_phenikaaVideoPlayerController),
),
// show the video progress & scrubbing by touch event on it
VideoProgressIndicator(
_phenikaaVideoPlayerController,
allowScrubbing: true,
padding: EdgeInsets.zero,
colors: VideoProgressColors(
backgroundColor: Color(0xFF243771),
playedColor: R.colors.redFF0000,
bufferedColor: R.colors.grayF5F6F8,
),
),
SizedBox(height: R.dimens.smallSpacing),
Row(
children: [
SizedBox(width: R.dimens.smallSpacing2),
InkWell(
onTap: () {
if (_phenikaaVideoPlayerController.value.isPlaying) {
_phenikaaVideoPlayerController.pause();
} else {
_phenikaaVideoPlayerController.play();
}
},
child: ValueListenableBuilder<VideoPlayerValue>(
valueListenable: _phenikaaVideoPlayerController,
builder: (_, _videoPlayerValue, __) {
return Icon(
_videoPlayerValue.isPlaying
? Icons.pause_circle_outline_rounded
: Icons.play_circle_outline_rounded,
);
},
),
),
SizedBox(width: R.dimens.smallSpacing2),
InkWell(
onTap: () {
_phenikaaVideoPlayerController
.seekTo(Duration(seconds: 0));
_phenikaaVideoPlayerController.pause();
},
child: Icon(Icons.stop_circle_outlined),
),
SizedBox(width: R.dimens.smallSpacing2),
// render duration video with current position / total video duration
ValueListenableBuilder<VideoPlayerValue>(
valueListenable: _phenikaaVideoPlayerController,
builder: (_, _videoPlayerValue, __) {
return Text(
"00:${_videoPlayerValue.position.inSeconds.toString().padLeft(2, '0')}",
style: R.styles.titleTextStyleW500S16,
);
},
),
Text(
" / 00:${_phenikaaVideoPlayerController.value.duration.inSeconds.toString()}",
style: R.styles.titleTextStyleW500S16,
),
Spacer(),
//render Volume button
InkWell(
onTap: () {
if (_phenikaaVideoPlayerController.value.volume ==
0.0) {
_phenikaaVideoPlayerController.setVolume(1.0);
} else
_phenikaaVideoPlayerController.setVolume(0.0);
},
child: ValueListenableBuilder<VideoPlayerValue>(
valueListenable: _phenikaaVideoPlayerController,
builder: (_, _videoPlayerValue, __) {
return Icon(
_videoPlayerValue.volume == 0.0
? Icons.volume_off_outlined
: Icons.volume_up_outlined,
);
},
),
),
SizedBox(width: R.dimens.smallSpacing2),
],
),
],
);
} else {
// If the VideoPlayerController is still initializing, show a
// loading spinner.
return Container(
alignment: Alignment.center,
padding: EdgeInsets.only(top: R.dimens.mediumSpacing1),
child: CircularProgressIndicator(
color: Color(0xFF243771),
),
);
}
},
),
Follow my widget tree with the image demo below

AnimatedSwitcher does not animate

I'm trying to make a news section in my app. In this page that's gonna display the news, i want to be able to click anywhere on the page and get the news that is next in my list. So far no problem with that, but i wanted it to have a nice animation so i tried implementing AnimatedSwitcher, but i can't figure out why there is no animation showing.
I tried changing the hierarchy of my code. Putting the gesture detector inside the animated switcher and the other way around. Letting the main container outside or inside of it too. I tried an animation builder that would scale it just in case it wasnt obvious enough but nothing. Tried changing the duration too but that wasn't it.
class ShowNews extends StatefulWidget {
#override
_ShowNewsState createState() => _ShowNewsState();
}
class _ShowNewsState extends State<ShowNews> {
List<News> _news = [
News(title: 'OYÉ OYÉ', desc: 'bla bla bla bla bla'),
News(title: 'another one', desc: 'plus de bout d\'histoire'),
News(title: 'boum', desc: 'attention à l\'accident'),
News(title: 'Lorem ipsum', desc: 'Lorem ipsum in doloris'),
];
int _currentIndex = 0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
if (_currentIndex < _news.length - 1) {
_currentIndex++;
} else {
_currentIndex = 0;
}
});
},
child: Container(
height: 160,
padding: EdgeInsets.all(20.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0),
topRight: Radius.circular(20.0),
),
),
child: AnimatedSwitcher(
duration: Duration(seconds: 5),
child: ColumnArticle(_news, _currentIndex),
),
),
);
}
}
Everything is working fine but the animation.
Edit: I tried adding a key to make it different but still no animation.
class ColumnArticle extends StatelessWidget {
final List<News> _news;
final int _currentIndex;
ColumnArticle(this._news, this._currentIndex);
#override
Widget build(BuildContext context) {
return Column(
key: ValueKey<int>(_currentIndex),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
_news[_currentIndex].title,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 20.0,
),
),
SizedBox(
height: 10.0,
),
Text(
_news[_currentIndex].desc,
style: TextStyle(
fontSize: 14.0,
),
),
],
);
}
}
That happens because the AnimatedSwitcher will add an animation anytime it is rebuilt with a different child reference. However, in your widget lifecycle, you are always using a ColumnArticle as a child, thus, not actually swapping any widget type, that's where the ValueKey comes in play.
You can use the index as the reference for the key, but make sure it actually changes, otherwise it won't work and you also need to pass it to your ColumnArticle base widget (super).
So, your ColumnArticle should look like this:
class ColumnArticle extends StatelessWidget {
final List<News> _news;
final int _currentIndex;
ColumnArticle(this._news, this._currentIndex) : super(key: ValueKey<int>(_currentIndex));
...
}
Passing the same type of widget with different attributes will not trigger an animation since they are the same widgets for the framework. It's also mentioned in the description.
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).
Here is the code from AnimatedSwitcher that checks whether to animate or not:
if (hasNewChild != hasOldChild ||
hasNewChild && !Widget.canUpdate(widget.child, _currentEntry.widgetChild)) {
// Child has changed, fade current entry out and add new entry.
_childNumber += 1;
_addEntryForNewChild(animate: true);
}
This is the static canUpdate method from the framework:
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
To solve this you can set individual keys to your News widgets based on their distinct attributes (eg. text, count, value). ValueKey<T> is just for that.
Column(
children: <Widget>[
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: Text(
'$_count',
// This key causes the AnimatedSwitcher to interpret this as a "new"
// child each time the count changes, so that it will begin its animation
// when the count changes.
key: ValueKey<int>(_count),
),
),
RaisedButton(
child: const Text('Increment'),
onPressed: () {
setState(() {
_count += 1;
});
},
),
])

Widgets sliding from outside the screen in Flutter ? Similar to Android 8 app drawer

I am writing a flashcard app (an extension to the open source AnkiDroid app) in Flutter. The basic workflow is: the app shows me a question and I can reveal the answer. The gesture I want in order to reveal the answer is similar to the Android 8 swipe up from the bottom icon row to reveal the app drawer. A fast swipe (or fling in the android terminology?) can reveal the app list, but a drawn out, slow swipe can control the motion of the apps drawer.
My questions are the following:
What is the proper way to have widgets slide in from outside the screen ? Flutter complains that I'm trying to display widgets outside the screen, suggests I use ClipRect, but I haven't found a way for ClipRect to only display something the size of the screen (it seems to adjust itself to the size of the child)
What is the recommended layout for what I want to do ? Currently I have the question and answer in a Column, and in order to center the question initially and hide the question, I modify the padding. It feels like a bit of a hack.
Is there a helper library that can help me achieve the exact swipe/fling motion that I'm after? It needs to take into account momentum and position in order for the motion to feel just as natural as the android 8 app drawer.
Thank you for any suggestions you may have.
Here are the screens I have so far:
Question screen
Answer screen (after swiping up)
And here's the code:
import 'package:flutter/material.dart';
import 'dart:math';
// Uncomment lines 7 and 10 to view the visual layout at runtime.
//import 'package:flutter/rendering.dart' show debugPaintSizeEnabled;
void main() {
//debugPaintSizeEnabled = true;
runApp(MyApp());
}
/*
* travel around the world
* 環遊世界
* wàan jàu sâi gâai
*/
class Card extends StatefulWidget {
#override
createState() => CardState();
}
class CardState extends State<Card> with SingleTickerProviderStateMixin {
var _dragStartOffset;
Animation<double> questionAnimation;
Animation<double> answerAnimation;
Animation<double> opacityAnimation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(duration: const Duration(milliseconds: 250), vsync: this);
questionAnimation = Tween(begin: 250.0, end: 150.0).animate(controller)
..addListener(() {
setState(() {
// the state that has changed here is the animation object’s value
});
});
answerAnimation = Tween(begin: 200.0, end: 32.0).animate(controller)
..addListener(() {
setState(() {
// the state that has changed here is the animation object’s value
});
});
opacityAnimation = Tween(begin: 0.0, end: 1.0).animate(controller)
..addListener(() {
setState(() {
// the state that has changed here is the animation object’s value
});
});
}
#override
Widget build(BuildContext context) {
Widget question = Container(
padding: EdgeInsets.only(top: questionAnimation.value),
child: Center (
child: Text(
"travel around the world",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 48.0,
),
textAlign: TextAlign.center,
)
),
);
Widget answer = Container(
padding: EdgeInsets.only(top: answerAnimation.value),
child: Opacity(
opacity: opacityAnimation.value,
child: Text(
"wàan jàu sâi gâai 環遊世界",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 48.0,
),
textAlign: TextAlign.center,
)
)
);
var children = [question, answer];
var child = GestureDetector(
onTap: () {
controller.reset();
},
onVerticalDragUpdate: (data) {
// print(data);
var currentOffset = data.globalPosition;
var travel = _dragStartOffset - currentOffset;
// print(travel);
if(travel.dy <0 )
{
return;
}
// cannot be lower than zero
var travelY = max<double>(0.0, travel.dy);
// cannot be higher than 100
travelY = min<double>(200.0, travelY);
var animationPosition = travelY / 200.0;
controller.value = animationPosition;
},
onVerticalDragEnd: (data) {
if(controller.value > 0.50) {
// make the animation continue on its own
controller.forward();
} else {
// go back the other way
controller.reverse();
}
},
onVerticalDragStart: (data) {
//print(data);
_dragStartOffset = data.globalPosition;
},
child: Scaffold(
appBar: AppBar(
title: Text('AnkiReview'),
),
body: Container(
child:Column(
children: children,
)
),
)
);
return child;
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Card(),
);
}
}
I figured out one solution. It involves a Column, the top is just a Container with the question, but the bottom is a PageView which has a blank first page. The user can slide up to reveal the answer.
It solves the clipping issue, and also the physics issue, because PageView has built-in physics and snapping, which would otherwise not be trivial to build (I would probably have to use a CustomScrollView).
code:
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'dart:math';
// Uncomment lines 7 and 10 to view the visual layout at runtime.
//import 'package:flutter/rendering.dart' show debugPaintSizeEnabled;
void main() {
//debugPaintSizeEnabled = true;
runApp(MyApp());
}
/*
* travel around the world
* 環遊世界
* wàan jàu sâi gâai
*/
class Card extends StatefulWidget {
#override
createState() => CardState();
}
class CardState extends State<Card> with SingleTickerProviderStateMixin {
var _dragStartOffset;
var _fontSize = 48.0;
static const _padding = 28.0;
initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
var questionText = Text(
"travel around the world",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: _fontSize,
),
textAlign: TextAlign.center,
);
var answerText = Text(
"wàan jàu sâi gâai 環遊世界",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: _fontSize,
),
textAlign: TextAlign.center
);
Widget question = Container(
padding: EdgeInsets.only(bottom: _padding),
alignment: Alignment.bottomCenter,
child: questionText
);
Widget answer = Container(
padding: EdgeInsets.only(top: _padding),
alignment: Alignment.topCenter,
child: answerText
);
var card = Column(
children: [
Expanded(
child: question,
),
Expanded(
child: PageView(
scrollDirection: Axis.vertical,
children: [
Container(),
answer
]
)
)
]
);
return card;
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('AnkiReview'),
),
body: Container(
child:Card()
),
),
);
}
}