I am trying to make a Sliver list whose elements are rotated at 90' at first but when tapped they rotate back to 0' and vice verse. The animation itself is working fine but the problem is the scroll only work when the items are at 0'.
Can someone please tell me how can I fix this?
I tried wrapping a container around the list item and observed that the sliver only accounts for the item at 0' but not the item at 90'. I even tried changing the height of the container itself but it gives weird results.
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
Animation<double> animation;
Animation<double> animationOffset;
AnimationController controller;
#override
void initState() {
super.initState();
controller = AnimationController(vsync: this, duration: const Duration(seconds: 2));
animation = Tween<double>(begin: -Math.pi / 2, end: 0).animate(controller)
..addStatusListener((status){
if (status == AnimationStatus.completed){
controller.reverse();
}else if (status == AnimationStatus.dismissed){
controller.forward();
}
});
animationOffset = Tween<double>(begin: 87, end: 0).animate(controller)
..addStatusListener(
(status){
print('$status');
print('value: ${animationOffset.value}');
}
);
}
#override
Widget build(BuildContext context) => GestureDetector(
child: CustomScrollView(
slivers: <Widget>[
SliverPadding(
padding: EdgeInsets.all(0),
sliver: SliverList(
delegate: SliverChildListDelegate([
Container(color: Colors.green, child: AnimatedBuilder(animation: animationOffset, builder:(context, child) => Transform.translate(offset: Offset(0, 0),child: RotateTransition(child: TextWidget(), animation: animation)))),
]),
),
),
],
),
onTap: () {
if(animation.status == AnimationStatus.completed){
controller.reverse();
}
else if (animation.status == AnimationStatus.dismissed){
controller.forward();
}
},
);
#override
void dispose() {
// TODO: implement dispose
controller.dispose();
super.dispose();
}
}
I'm not sure but I tried using SliverAnimatedOpacity() for animating opacity and it worked fine. Hope it helps someone
Related
I am trying to have some text show (or quickly fade in) as a result of user action (e.g. button click), and after a second fade out without user action. I understand that fading is accomplished by AnimatedOpacity class with opacity being set by a state variable, however it's not clear to me how to accomplish this particular scenario. Many thanks.
first, text its show it self while not fade its can be considered animated too, or create some async function
check this code :
import 'package:flutter/material.dart';
class FadeTextAuto extends StatefulWidget {
const FadeTextAuto({Key? key}) : super(key: key);
#override
_FadeTextAutoState createState() => _FadeTextAutoState();
}
class _FadeTextAutoState extends State<FadeTextAuto> with SingleTickerProviderStateMixin{
late Animation _animationFadeInOut;
late AnimationController _animationController;
bool _textShouldPlay = false;
late Animation _animationText;
String info = "This text appear in";
#override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 2000));
_animationFadeInOut = Tween<double>(
begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _animationController, curve: Curves.linear));
_animationText = StepTween(
begin: 1, end: 3
).animate(CurvedAnimation(parent: _animationController, curve: Curves.linear));
}
#override
Widget build(BuildContext context) {
return Material(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(onPressed: (){
setState(() {
info = "This text appear in";
_textShouldPlay = false;
});
print(_animationText.value);
_animationController.forward().then((value){
if(mounted){
setState(() {
_textShouldPlay = true;
info = "Hey read this Information";
});
Future.delayed(const Duration(milliseconds: 2000),(){
//wait a little for text can be read
if(mounted){
setState(() {
_textShouldPlay = false;
info = "this Text will be disappear in";
});
}
}).then((value){
if(mounted){
_animationController.reverse();
}
});
}
});
}, child: const Text("User Click")),
AnimatedBuilder(
animation: _animationController,
builder: (context, child){
return Opacity(
opacity: _animationFadeInOut.value,
child: Text(
"$info ${_textShouldPlay?"":_animationText.value} ", style: const TextStyle(color: Colors.cyan, fontSize: 20),
),
);
},
),
],
),
);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
result :
I have multiple widgets I want to fade in one after another but without needing a button. Every tutorial I have seen has required the use of a button in order to transition to the next widget. The widgets I want to transition take up the whole screen .
How do you want to trigger the transition?
If time is a suitable trigger.
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
late Timer timer;
int count = 0;
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 1), (_) => setState(() => count += 1));
}
#override
void dispose() {
super.dispose();
timer.cancel();
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(child: child, scale: animation);
},
child: Text(
'$count',
key: ValueKey<int>(count),
style: Theme.of(context).textTheme.headline4,
),
),
],
),
);
}
}
I'm trying to make a button where when user hold it, there will be a progress, but if the user unpress the button before it finish, the progress will decrease. somthing like the one on the picture
As said by Arnold Parge, you can use the GestureDetector and listen to onTapDown and onTapUp. To create your desired LoadingButton, you can use the following Widget Structure:
- GetureDetector
- Stack
- CircularProgressIndicator // background circle
- CircularProgressIndicator // foreground circle
- Icon
To create the animation, you can add an AnimationController and bind the value of the foreground CircularProgressIndicator to AnimationController.value. Now you'll just have to the add the two listeners to to the GestureDetector:
onTapDown: When tapping on the button, the animation should start. Therefore we call AnimationController.forward()
onTapUp: When releasing the press, we want to check if the animation is already finished. We can use AnimationController.status to check the status. If it is AnimationStatus.forward it is still running. Hence we want to reverse the animation with calling AnimationController.reverse().
Here is the resulting button:
And the complete source code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(),
home: Scaffold(
body: Center(child: LoadingButton()),
),
);
}
}
class LoadingButton extends StatefulWidget {
#override
LoadingButtonState createState() => LoadingButtonState();
}
class LoadingButtonState extends State<LoadingButton>
with SingleTickerProviderStateMixin {
AnimationController controller;
#override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: Duration(seconds: 1));
controller.addListener(() {
setState(() {});
});
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => controller.forward(),
onTapUp: (_) {
if (controller.status == AnimationStatus.forward) {
controller.reverse();
}
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
CircularProgressIndicator(
value: 1.0,
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey),
),
CircularProgressIndicator(
value: controller.value,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
Icon(Icons.add)
],
),
);
}
}
Use GestureDetector.
Start the progress in onTapDown and reverse the progress in onTapUp if the progress is not complete or whatever your conditions are.
I'm looking for a way to insert new items into a list view, while maintaining the scroll offset of the user. Basically like a twitter feed after pulling to refresh: the new items get added on top, while the scroll position is maintained. The user can then just scroll up to see the newly added items.
If I just rebuild the list/scroll widget with a couple of new items at the beginning, it -of course- jumps, because the height of the scroll view content increased. Just estimating the height of those new items to correct the jump is not an option, because the content of the new items is variable.
Even the AnimatedList widget which provides methods to dynamically insert items at an arbitrary position jumps when inserting at index 0.
Any ideas on how to approach this? Perhaps calculating the height of the new items beforehand using an Offstage widget?
Ran into this problem recently: I have a chat scroll that async loads previous or next messages depending on the direction of the scroll. This solution worked out for me.
The idea of the solution is the following. You create two SliverLists and put them inside CustomScrollView.
CustomScrollView(
center: centerKey,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
// Here we render elements from the upper group
child: top[index]
)
}
),
SliverList(
// Key parameter makes this list grow bottom
key: centerKey,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
// Here we render elements from the bottom group
child: bottom[index]
)
}
),
)
The first list scrolls upwards while the second list scrolls downwards. Their offset zero points are fixed at the same point and never move. If you need to prepend an item you push it to the top list, otherwise, you push it to the bottom list. That way their offsets don't change and your scroll view does not jump.
You can find the solution prototype in the following dartpad example.
UPD: Fixed null safety issue in this dartpad example.
I don't know if you managed to solve it... Marcin Szalek has posted a very nice solution on his blog about implementing an infinite dynamic list. I tried it and works like a charm with a ListView. I then tried to do it with an AnimatedList, but experienced the same issue that you reported (jumping to the top after each refresh...). Anyway, a ListView is quite powerful and should do the trick for you!
The code is:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
theme: new ThemeData(primarySwatch: Colors.blue),
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<int> items = List.generate(10, (i) => i);
ScrollController _scrollController = new ScrollController();
bool isPerformingRequest = false;
#override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_getMoreData();
}
});
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
_getMoreData() async {
if (!isPerformingRequest) {
setState(() => isPerformingRequest = true);
List<int> newEntries = await fakeRequest(
items.length, items.length + 10); //returns empty list
if (newEntries.isEmpty) {
double edge = 50.0;
double offsetFromBottom = _scrollController.position.maxScrollExtent -
_scrollController.position.pixels;
if (offsetFromBottom < edge) {
_scrollController.animateTo(
_scrollController.offset - (edge - offsetFromBottom),
duration: new Duration(milliseconds: 500),
curve: Curves.easeOut);
}
}
setState(() {
items.addAll(newEntries);
isPerformingRequest = false;
});
}
}
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: isPerformingRequest ? 1.0 : 0.0,
child: new CircularProgressIndicator(),
),
),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text("Infinite ListView"),
),
body: ListView.builder(
itemCount: items.length + 1,
itemBuilder: (context, index) {
if (index == items.length) {
return _buildProgressIndicator();
} else {
return ListTile(title: new Text("Number $index"));
}
},
controller: _scrollController,
),
);
}
}
/// from - inclusive, to - exclusive
Future<List<int>> fakeRequest(int from, int to) async {
return Future.delayed(Duration(seconds: 2), () {
return List.generate(to - from, (i) => i + from);
});
}
A gist containing whole class can be found here.
I think reverse + lazyLoading will help you.
Reverse a list:
ListView.builder(reverse: true, ...);
for lazyLoading refer here.
I would like to draw a user's attention to a submit button anytime a radio button is selected and I would like to know if there's anyway to implement this in flutter. I have looked at the RaisedButton docs but there doesn't seem to be any property that flashes or shakes the button. The code below for instance initially has no radio button selected so the button is grayed out, once a choice is made amongst multiple radio buttons, the submit RaisedButton onPressed property value is no longer null but replaced with the action required; however I also want a situation where if a different radio button is selected, there is some way to add some motion (flashing/shaking button) to the submit button but not change the onPressed property
new Radio<int>(
value: 1,
groupValue: 0,
onChanged: handleRadioValue
)
new RaisedButton(
child: const Text('SUBMIT')
onPressed: selected
)
handleRadioValue(){
selected = groupValue == 0 ? null : submitButton();
//change Raised button to attract attention}
You can attract attention by animating the color of the RaisedButton. This example draws attention to the RaisedButton when the radio button selection changes by changing its color to the current theme's disabledColor and back.
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
createState() => new MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
AnimationController _controller;
int _radioValue;
#override initState() {
_controller = new AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
)..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed)
_controller.reverse();
});
super.initState();
}
void _handleRadioValue(int value) {
// Don't animate the first time that the radio value is set
if (_radioValue != null)
_controller.forward();
setState(() {
_radioValue = value;
});
}
#override
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
return new Scaffold(
body: new Center(
child: new Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
new Radio<int>(
value: 0,
groupValue: _radioValue,
onChanged: _handleRadioValue
),
new Radio<int>(
value: 1,
groupValue: _radioValue,
onChanged: _handleRadioValue
),
new AnimatedBuilder(
child: const Text('SUBMIT'),
animation: _controller,
builder: (BuildContext context, Widget child) {
return new RaisedButton(
color: new ColorTween(
begin: theme.primaryColor,
end: theme.disabledColor,
).animate(_controller).value,
colorBrightness: Brightness.dark,
child: child,
onPressed: _radioValue == null ?
null :
() => print('submit')
);
}
)
],
),
),
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
}