I want switch status with animation in CustomScrollView, but it throw error.
class SliverAnimatedSwitcher extends StatefulWidget {
final state;
const SliverAnimatedSwitcher({Key key, this.state}) : super(key: key);
#override
_SliverAnimatedSwitcherState createState() => _SliverAnimatedSwitcherState();
}
class _SliverAnimatedSwitcherState extends State<SliverAnimatedSwitcher> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: Text('SliverAnimatedSwitcher'),
),
_buildContent(),
],
),
);
}
get state => widget.state;
Widget _buildContent() {
var content;
if (state.isNotEmpty == true) {
content = SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
var item = state.items[index];
return ListTile(
title: Text(item.title),
);
},
childCount: state.items.length,
),
);
} else if (state.isError) {
content = SliverFillRemaining(
key: Key('error'),
child: Container(alignment: Alignment.center, child: Text('Error')),
);
} else if (state.isLoading) {
content = SliverFillRemaining(
key: Key('loading'),
child: Container(alignment: Alignment.center, child: Text('Loading')),
);
} else {
content = SliverFillRemaining(
key: Key('empty'),
child: Container(alignment: Alignment.center, child: Text('Empty')),
);
}
return AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: content,
);
}
}
There's a sliver_tools package on pub.dev that has a SliverAnimatedSwitcher. You'd use it like this:
SliverAnimatedSwitcher(
duration: kThemeAnimationDuration,
child: content,
)
It is because sliver widget is not capable with Stack which is using as layout builder inside AnimatedSwitcher.
I found a temporary solution. Though the result is not equal to the original one (only show the last widget in the result), but I think it is acceptable for me now with such little effort.
First create a SliverAnimatedSwitcher exact the same code as AnimatedSwitcher here
Modify the Stack part to return the currentChild only (defaultLayoutBuilder)
Change FadeTransition to SliverFadeTransition (defaultTransitionBuilder)
.
static Widget defaultLayoutBuilder(Widget currentChild, List<Widget> previousChildren) {
return currentChild;
}
.
static Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return SliverFadeTransition(
opacity: animation,
sliver: child,
);
}
If someone can find a way to Stack Sliver widgets above each other, the result may be more perfect.
Related
I have a problem with Dismissible widget. I use that to close actual page by calling a function:
onDismissed: (_) => Navigator.of(context).pop().
so what I did: wrap whole scaffold by Dismissible.
Everything worked fine until delivered to the page state with bool information about the logged in user (admin or no).
Now i get error like this:
FlutterError (A dismissed Dissmisible widget is still part of the
tree. Make sure to implement the onDismissed handler and to
immediately remove the Dismissible widget from the application once
that has fired.)
Screen: ErrorScreen
I tried "key" changes but it didn't help.
Page code with Dismissible:
class OtherDetailsPage extends StatelessWidget {
const OtherDetailsPage({required this.id, Key? key}) : super(key: key);
final String id;
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
OtherDetailsCubit(OtherRemoteDataSource())..getDetailsWithID(id),
child: BlocBuilder<OtherDetailsCubit, OtherDetailsState>(
builder: (context, details) {
final detail = details.detailsState;
return BlocBuilder<RootCubit, RootState>(
builder: (context, root) {
bool? admin = root.admin;
if (admin == null || detail == null) {
return const LoadingPage();
}
return Dismissible(
key: const Key('key'),
// key: Key(UniqueKey().toString()),
direction: DismissDirection.down,
onDismissed: (_) => Navigator.of(context).pop(),
child: Scaffold(
body: Column(
children: [
Header(detail: detail),
const SizedBox(
height: 20,
),
DetailsDescription(detail: detail),
const SizedBox(
height: 50,
),
admin
? SwitchValueButton(id: id, detail: detail)
: const SizedBox(
height: 1,
),
const AnimatedArrowDown()
],
)),
);
},
);
},
),
);
}
}
I am using a TabBarView in my app like this:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
TabBarView(
controller: _controller,
physics: const NeverScrollableScrollPhysics(),
children: const [
WishlistsView(),
FriendsView(),
EventsView(),
InboxView(),
ProfileView(),
],
),
Align(
alignment: Alignment.bottomCenter,
child: BottomNavBar(
initialIndex: widget.navBarOption.index,
onPageChanged: (index) => _tap(context, index),
),
),
],
),
);
}
void _tap(BuildContext context, int index) => context.go(
'/home/${BottomNavBarOption.values[index].name}',
);
Now the problem is that when I go on another page the first time there is a very short white screen before the page is actually displayed.
Here is a ScreenVideo for a better understanding. This happens on both Web and iOS.
Why is that happening? Can I avoid that? As you can see the views are not very heavy.
All they have is basically a SVGPicture.asset :
class _WishlistsViewState extends State<WishlistsView>
with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: Stack(
children: const [
BackgroundImage(option: BackgroundImageOption.wishlists),
],
),
);
}
}
It is probably taking time to load svg on the first go and caching it for showing it later.. You can probably try OffStage Widget which should load the UI and just not display it.
class _WishlistsViewState extends State<WishlistsView>
with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: Offstage(
offStage: false,
child: Stack(
children: const [
BackgroundImage(option: BackgroundImageOption.wishlists),
],
),
),
);
}
}
as #Kaushik Chandru correctly pointed out: The SVG are the problem. They have to load first. To solve that issue, I used this really useful function precachePicture right before I actually call runApp:
for (BackgroundImageOption imageOption in BackgroundImageOption.values) {
await precachePicture(
ExactAssetPicture(
SvgPicture.svgStringDecoderBuilder,
imageOption.assetPath,
),
null,
);
}
I have CustomScrollView with a SliverGrid with different widget types inside. I have a widget that appears after the stream content ends and other content will load. When this widget appears, I want to shortly pin it on the screen and disable the scrolling for 3 seconds.
To simplify it, I skipped the sliver delegates and summarizes my widget tree like this:
CustomScrollView{
controller: scrollController,
slivers: [
SliverAppBar(),
SliverStreamGrid(
children: [
ProductTile(),
ProductTile(),
ProductTile(),
// End of Available Products
EndOfProductsInfoWidget(), // should be pinned on screen for 3 seconds
SomeOtherProductTile(),
SomeOtherProductTile(),
SomeOtherProductTile(),
]
)
]
}
I am using visibility_detector to detect visibility of the widget and SliverPinnedHeader from sliver_tools package.
The issue lies when our widget is visible I am using a short delay and then disabling scroll event for 3 seconds, you can use global key for this and have more precious output.
CustomScrollView's physics:NeverScrollableScrollPhysics() used to disable scroll event.
class SliverPinTHEx extends StatefulWidget {
const SliverPinTHEx({Key? key}) : super(key: key);
#override
State<SliverPinTHEx> createState() => _SliverPinTHExState();
}
class _SliverPinTHExState extends State<SliverPinTHEx> {
bool? _showPinMessage;
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: _showPinMessage == true
? const NeverScrollableScrollPhysics()
: null,
slivers: [
list(),
if (_showPinMessage != false)
SliverPinnedHeader(
child: VisibilityDetector(
onVisibilityChanged: (info) async {
if (info.visibleFraction == 1.0) {
debugPrint("disable scroll");
await Future.delayed(const Duration(seconds: 1));
setState(() {
_showPinMessage = true;
});
Future.delayed(const Duration(seconds: 3)).then((value) {
setState(() {
_showPinMessage = false;
});
});
}
},
key: const Key('my-widget-key'),
child: Container(
height: 70,
color: Colors.amber,
child: const Text("pinned widget"),
),
)),
list()
],
),
);
}
SliverList list() {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return SizedBox(
height: 50,
child: Text(
index.toString(),
),
);
},
childCount: 55,
),
);
}
}
With the code below
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) => MaterialApp(
home: const MyHomePage(),
);
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) => DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Center(
child: Text('use the mouse wheel to scroll')),
bottom: TabBar(
tabs: const [
Center(child: Text('ScrollView')),
Center(child: Text('PageView'))
],
),
),
body: TabBarView(
children: [
SingleChildScrollView(
child: Column(
children: [
for (int i = 0; i < 10; i++)
Container(
height: MediaQuery.of(context).size.height,
child: const Center(
child: FlutterLogo(size: 80),
),
),
],
),
),
PageView(
scrollDirection: Axis.vertical,
children: [
for (int i = 0; i < 10; ++i)
const Center(
child: FlutterLogo(size: 80),
),
],
),
],
),
),
);
}
You can see, running it on dartpad or from this video,
that using the mouse wheel to scroll a PageView provides a mediocre experience (at best),
This is a known issue #35687 #32120, but I'm trying to find a workaround
to achieve either smooth scrolling for the PageView or at least prevent the "stutter".
Can someone help me out or point me in the right direction?
I'm not sure the issue is with PageScrollPhysics;
I have a gut feeling that the problem might be with WheelEvent
since swiping with multitouch scroll works perfectly
The problem arises from chain of events:
user rotate mouse wheel by one notch,
Scrollable receives PointerSignal and calls jumpTo method,
_PagePosition's jumpTo method (derived from ScrollPositionWithSingleContext) updates scroll position and calls goBallistic method,
requested from PageScrollPhysics simulation reverts position back to initial value, since produced by one notch offset is too small to turn the page,
another notch and process repeated from step (1).
One way to fix issue is perform a delay before calling goBallistic method. This can be done in _PagePosition class, however class is private and we have to patch the Flutter SDK:
// <FlutterSDK>/packages/flutter/lib/src/widgets/page_view.dart
// ...
class _PagePosition extends ScrollPositionWithSingleContext implements PageMetrics {
//...
// add this code to fix issue (mostly borrowed from ScrollPositionWithSingleContext):
Timer timer;
#override
void jumpTo(double value) {
goIdle();
if (pixels != value) {
final double oldPixels = pixels;
forcePixels(value);
didStartScroll();
didUpdateScrollPositionBy(pixels - oldPixels);
didEndScroll();
}
if (timer != null) timer.cancel();
timer = Timer(Duration(milliseconds: 200), () {
goBallistic(0.0);
timer = null;
});
}
// ...
}
Another way is to replace jumpTo with animateTo. This can be done without patching Flutter SDK, but looks more complicated because we need to disable default PointerSignalEvent listener:
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class PageViewLab extends StatefulWidget {
#override
_PageViewLabState createState() => _PageViewLabState();
}
class _PageViewLabState extends State<PageViewLab> {
final sink = StreamController<double>();
final pager = PageController();
#override
void initState() {
super.initState();
throttle(sink.stream).listen((offset) {
pager.animateTo(
offset,
duration: Duration(milliseconds: 200),
curve: Curves.ease,
);
});
}
#override
void dispose() {
sink.close();
pager.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Mouse Wheel with PageView'),
),
body: Container(
constraints: BoxConstraints.expand(),
child: Listener(
onPointerSignal: _handlePointerSignal,
child: _IgnorePointerSignal(
child: PageView.builder(
controller: pager,
scrollDirection: Axis.vertical,
itemCount: Colors.primaries.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(color: Colors.primaries[index]),
);
},
),
),
),
),
);
}
Stream<double> throttle(Stream<double> src) async* {
double offset = pager.position.pixels;
DateTime dt = DateTime.now();
await for (var delta in src) {
if (DateTime.now().difference(dt) > Duration(milliseconds: 200)) {
offset = pager.position.pixels;
}
dt = DateTime.now();
offset += delta;
yield offset;
}
}
void _handlePointerSignal(PointerSignalEvent e) {
if (e is PointerScrollEvent && e.scrollDelta.dy != 0) {
sink.add(e.scrollDelta.dy);
}
}
}
// workaround https://github.com/flutter/flutter/issues/35723
class _IgnorePointerSignal extends SingleChildRenderObjectWidget {
_IgnorePointerSignal({Key key, Widget child}) : super(key: key, child: child);
#override
RenderObject createRenderObject(_) => _IgnorePointerSignalRenderObject();
}
class _IgnorePointerSignalRenderObject extends RenderProxyBox {
#override
bool hitTest(BoxHitTestResult result, {Offset position}) {
final res = super.hitTest(result, position: position);
result.path.forEach((item) {
final target = item.target;
if (target is RenderPointerListener) {
target.onPointerSignal = null;
}
});
return res;
}
}
Here is demo on CodePen.
Quite similar but easier to setup:
add smooth_scroll_web ^0.0.4 to your pubspec.yaml
...
dependencies:
...
smooth_scroll_web: ^0.0.4
...
Usage:
import 'package:smooth_scroll_web/smooth_scroll_web.dart';
import 'package:flutter/material.dart';
import 'dart:math'; // only for demo
class Page extends StatefulWidget {
#override
PageState createState() => PageState();
}
class PageState extends State<Page> {
final ScrollController _controller = new ScrollController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("SmoothScroll Example"),
),
body: SmoothScrollWeb(
controller: controller,
child: Container(
height: 1000,
child: ListView(
physics: NeverScrollableScrollPhysics(),
controller: _controller,
children: [
// Your content goes here, thoses children are only for demo
for (int i = 0; i < 100; i++)
Container(
height: 60,
color: Color.fromARGB(1,
Random.secure().nextInt(255),
Random.secure().nextInt(255),
Random.secure().nextInt(255)),
),
],
),
),
),
);
}
}
Thanks you hobbister !
Refer to flutter's issue #32120 on Github.
I know that it has been almost 1.5 year from this question, but I found a way that works smoothly. Maybe this will be very helpful whoever read it. Add a listener to your pageview controller with this code (You can make adjustments on duration or nextPage/animateToPage/jumpToPage etc.):
pageController.addListener(() {
if (pageController.position.userScrollDirection == ScrollDirection.reverse) {
pageController.nextPage(duration: const Duration(milliseconds: 60), curve: Curves.easeIn);
} else if (pageController.position.userScrollDirection == ScrollDirection.forward) {
pageController.previousPage(duration: const Duration(milliseconds: 60), curve: Curves.easeIn);
}
});
The issue is with the user settings, how the end-user has set the scrolling to happen with his mouse. I have a Logitech mouse that allows me to turn on or off the smooth scrolling capability via Logitech Options. When I enable smooth scrolling it works perfectly and scrolls as required but in case of disabling the smooth scroll it gets disabled on the project as well. The behavior is as set by the end-user.
Still, if there's a requirement to force the scroll to smooth scroll than can only be done by setting relevant animations. There's no direct way as of now.
Hello fellow developers,
For my first Flutter project I need to use a list with sticky headers and infinite scroll. I found a very nice library for this purpose.
https://pub.dev/packages/flutter_sticky_header
Final goal is to fetch new items into the list from my database by scrolling further down.
For testing purposes I added a button to add a random item to my list. However the UI is not updated when the function is called. I am very new to Flutter. Below my code. How can I update the UI every time an item is added to the list without recreating the widget.
class AppScaffold2 extends StatefulWidget {
#override
_AppScaffold2State createState() => _AppScaffold2State();
}
class _AppScaffold2State extends State<AppScaffold2> {
final CustomScrollView x = CustomScrollView(
slivers: new List<Widget>(),
reverse: false,
);
int counter = 0;
add(Widget w){
x.slivers.add(w);
}
#override
Widget build(BuildContext context) {
return DefaultStickyHeaderController(
child: Scaffold(
body: Container(child: Column(children: <Widget>[
Expanded(child: x),
MaterialButton(
onPressed: () => fetch(),
child: Text('add to list')
)
],),)
),
);
}
fetch() {
x.slivers.add(_StickyHeaderList(index: counter));
counter++;
}
}
class _StickyHeaderList extends StatelessWidget {
const _StickyHeaderList({
Key key,
this.index,
}) : super(key: key);
final int index;
#override
Widget build(BuildContext context) {
return SliverStickyHeader(
header: Header(index: index),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) => ListTile(
leading: CircleAvatar(
child: Text('$index'),
),
title: Image.network(
"https://i.ytimg.com/vi/ixkoVwKQaJg/hqdefault.jpg?sqp=-oaymwEZCNACELwBSFXyq4qpAwsIARUAAIhCGAFwAQ==&rs=AOn4CLDrYjizQef0rnqvBc0mZyU3k13yrg",
),
),
childCount: 6,
),
),
);
}
}
Try using setState() in fetch Method.
like this.
fetch() {
x.slivers.add(_StickyHeaderList(index: counter));
setState(() {
_counter++;
});
}```
Update state using setState.
fetch() {
x.slivers.add(_StickyHeaderList(index: counter));
setState(() {
_counter++;
});
}