flutter scollable.ensurevisible not working in reverse direction - flutter

Please check the video / gif:
I have a pageview that will make the current tab active. I need to ensure the active tab is always visible even if the user swipes the screen multiple times. It is working from the left to right. But when we try back from right to left it's not behaving as expected.
PageView file
PageView(
controller: pageController,
onPageChanged: (int page) {
_duaWidgetState.currentState.updateBtn(page + 1);
Scrollable.ensureVisible(
_duaWidgetState.currentState.activeBtn.currentContext);
},
children: loaded
TabBarWidget with scroll view file
GlobalKey activeBtn = GlobalKey();
var _selectedTab = 1;
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 20),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(widget.numberOfTab,
(index) => tabBarItem(index + 1, widget.numberOfTab, activeBtn)),
),
),
);
}
Container that uses the Key
Container(
key: activeBtn,
margin: EdgeInsets.only(left: 20),
padding: EdgeInsets.symmetric(
vertical: 10,
horizontal: 24,
),

No One answered my Question. But, after a lot of effort and time. I found a workaround to fix this problem. Hope this is a bug and will fix by the flutter team in the coming updates.
Scrollable.ensureVisible(_duaWidgetState.currentState.activeBtn.currentContext);
This function has an optional argument called alignment which can be adjusted to make sure the selected element/button is in the center of the viewport. (In my case horizontally center).
My widgets are horizontally scrollable, so I need to update the page number according to the result from PageView(), Here when I used
alignment: 0
Its working fine from left to right swipes of page view. But with this alignment when i swipe page from right to left to go to previous page in the PageView widget, the ensureVisible is not working like expected. The selected element/button is out of the view. I experimented and found that when I used
aligment: 1
The ensureVisible is working fine from the swipes from right to left but at the same time. when I scroll left to right the same problem occured.
Finally, so I managed to do some workaround to fix this behavior. The solution is to store last page index in a variable and check whether the last page is greater than the new page then alignment: 0 and if the last page less than the new page aligment:1 like that.
PageView(
onPageChanged: (int page) {
if (lastPage < page) {
Scrollable.ensureVisible(
_duaWidgetState.currentState.activeBtn.currentContext,
alignment: -0.0100,
);
} else {
Scrollable.ensureVisible(
_duaWidgetState.currentState.activeBtn.currentContext,
(Perfect For font size 24)
alignment: 1.005,
);
}}
Hope my answer will help someone in the future.

I modified the original ensureVisible() a bit and this works in both directions in my case:
// `contentAnchor` is the global key of the widget you want to scroll to
final context = contentAnchor.currentContext!;
final ScrollableState scrollable = Scrollable.of(context)!;
scrollable.position.ensureVisible(
context.findRenderObject()!,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);

Related

Reducing Jank in Flutter progressive web app

Woah! I've spent several hours refactoring nested ListViews to a parent CustomScrollView & child Slivers. The errors Slivers produced were opaque and frightening, with nothing illuminating in Logcat; sleuthing devoured much of the time.
Anyway, that's solved. I find I still have jank scrolling a 15-item list. OK, each item can involve further numerous widgets {Padding, Alignment, Elevated button, Row, Text, SizedBox, Icon}. So my 15-item list ends up being multiple more Widgets.
I've now swapped out my SliverChildListDelegate for SliverChildBuilderDelegates, so a Builder builds the Widget List lazily. Having done this, it seems quite inefficient because it's increased the Widgets in the Widget tree. Each of the builders' buildItem() calls needs an extra Column Widget to wrap that sub-chunk of the total list.
It may be a lot of Widgets scrolling but it's only a 15 item list. I wish I knew what to optimise. Any thoughts on how to best reduce jank on Lists for mobile web?
The Flutter team says Flutter works best for computational-centred apps rather than text heavy informational apps. In future would it be better just to use webView Widgets? I always thought embedding Webviews would be clunky and slow but Lists of native Flutter Widgets, even as SliverLists, give jank.
Here is the janky list complete with builder:
Widget buildLocationDescriptionWidgets(LocationDetails presentLocation) {
print(LOG + "buildLocationDescriptionWidgets");
if (presentLocation.descriptionLinkUrls.isEmpty)
return SliverToBoxAdapter(child:
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Text(presentLocation.description[0])));
int numDescriptionBlocks = presentLocation.description.length;
double paddingBottom = 16;
if (presentLocation.descriptionLinkUrls.length >= numDescriptionBlocks) {
paddingBottom = 0;
}
return SliverPadding(
padding: EdgeInsets.fromLTRB(16, 16, 16, paddingBottom), sliver:
SliverList(
key: Key("descriptionSliverList"),
delegate: SliverChildBuilderDelegate((context, index) =>
buildDescriptionBlock(context, index),
childCount: presentLocation.description.length
),
));
}
Column buildDescriptionBlock(BuildContext context, int index) {
List<Widget> colChildWidget = [];
colChildWidget.add(Text(
widget.presentLocation.description[index],
textAlign: TextAlign.left,
));
if (index < widget.presentLocation.descriptionLinkUrls.length) {
colChildWidget.add(Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Align(
alignment: Alignment.center,
child: index >=
widget.presentLocation.descriptionButtonIcons.length
? ElevatedButton(
child: Text(
widget.presentLocation.descriptionButtonText[index]),
onPressed: () {
_launchURL(
widget.presentLocation.descriptionLinkUrls[index]);
})
: ElevatedButton(
child:
Row(mainAxisSize: MainAxisSize.min, children: [
Text(
widget.presentLocation.descriptionButtonText[index]),
SizedBox(width: 8),
FaIcon(
buttonIconMap[widget.presentLocation
.descriptionButtonIcons[index]],
size: 16)
]),
onPressed: () {
_launchURL(
widget.presentLocation.descriptionLinkUrls[index]);
}))));
}
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: colChildWidget);
}
Should I regress from a builder to a conventional SliverList?
Other things I've tried: I eliminated jank in my app Drawer list by putting const everywhere possible, lots of Dividers were made Const. But when you style text using Theme.of(context).textTheme.bodyText2 etc. it doesn't allow you to set textboxes to const. If you want to use const you can't style the app globally, you'd have to hard code. Is it worth forsaking abstraction for hard coding Text widget styles?
Here is the web app: Love Edinburgh
For the clearest example of jank
Open the App Drawer
Scroll to WONDER
Tap Arthur's Seat
Open the panel to full screen - slide it up
Scroll down the panel.
It doesn't show on a desktop browser which is compiled using Skia / Webkit. It's a bit fiddly to get scroll working on a desktop browser, you need to click amongst the text, then attempt to scroll. It's meant for mobile use so I'm resigned to that.
Not sure how to help you out. Would rather put this on a comment rather than answer but I don't have the points to do a comment.
Anyway, I wish I could replicate your problem. On my personal experience, for a 15 item list with numerous child widgets, it shouldn't be janky unless it has probably big sized images or really too much extra widgets.
On my case, I made sure to "isolate" / "compute" my heavy computations and showed a loading screen while preparing the list.
You may read on:
Isolates : https://dart.dev/guides/language/concurrency
Compute:
https://api.flutter.dev/flutter/foundation/compute-constant.html
Hope that helped!

Flutter List.generate but in reverse?

I have a container that I am trying to print out items form a list, minus the last item in the list. I am using list.generate:
child: Container(
width: double.infinity,
color: Colors.white,
child: ListView(
padding: const EdgeInsets.only(top: 10.0),
children: List.generate(
Provider.of<WeekList>(context).listOfWeeks.toList().length -
1,
(index) => ListTileCustom(
index: index,
),
),
),
),
My problem is when it prints out my custom ListTileCustom widget each new item added to the list is added to the bottom and not the top in the view.
In the picture above the order should be:
Red Bar
Red / Green Bar
Green bar
and when I add new items by pressing the button they should be added to the top not the bottom.
I tried adding reverse: true and that gets the order right but moves everything to the bottom and adds a ton of white space above them... Also not sure the scroll will work in the right direction at that point either.
To reverse a list in Dart, you can use ..reversed method. In the code you posted, that would be: children: List.generate(/* omitted */).reversed.toList().
Another (probably better) solution is to use for loop directly in children, for example:
ListView(
children: [
for (int i = 0; i < 5; i++) FlutterLogo(),
],
)
With this approach you can easily invert the list (and omit one element, like you desired) by changing the loop, e.g. for (int i = 10; i > 1; i--) or something.

Best way to allow a bit of overscroll in a CustomScrollView

The UI I'm making usually starts with the bottom sliver scrolled all the way in, so that its top is at the top of the view. But also:
Needs an extra empty space at the top, in case the user wants to pull the content down so that they can reach it without moving their hand from the bottom of the phone (this is a really basic ergonomic feature and I think we should expect to see it become pretty common soon, first led by apps moving more of their key functionality to the bottom of the screen, EG, Firefox's url bar.) (Currently, I'm using the appBar sliver for this, but I can imagine a full solution not using that)
Might need extra empty space at the bottom, whenever the content in that bottom sliver wont be long enough to allow it to be scrolled in all the way. It will seem buggy and irregular otherwise. Ideally I'd impose a minHeight so that the bottom sliver will always at least as tall as the screen, but it's a sliver, so I'm pretty sure that's not possible/ugly-difficult.
The avenue I'm considering right now is, ScrollPhysics wrapper that modifies its ScrollMetrics so that maxExtent and minExtent are larger. As far as I can tell, this will allow the CustomScrollView (given this ScrollPhysics) to overscroll. It feels kinda messy though. It would be nice to know what determines maxExtent and minExtent in the first place and alter that.
Lacking better options, I went ahead with the plan, and made my own custom ScrollPhysics class that allows overscroll by the given amount, extra.
return CustomScrollView(
physics: _ExtraScrollPhysics(extra: 100 * MediaQuery.of(context).devicePixelRatio),
...
And _ExtraScrollPhysics is basically just an extended AlwaysScrollable with all of the methods that take ScrollMetrics overloaded to copy its contents into a ScrollMetric with a minScrollExtent that has been decreased by -extra, then passing it along to the superclass's version of the method. It turns out that adjusting the maxScrollExtent field wasn't necessary for the usecase I described!
This has one drawback, the overscroll glow indicator, on top, appears at the top of the content, rather than the top of the scroll view, which looks pretty bad. It looks like this might be fixable, but I'd far prefer a method where this wasn't an issue.
mako's solution is a good starting point but it does not work for mouse wheel scrolling, only includes overscroll at the top, and did not implement the solution to the glow indicator problem.
A more general solution
For web, use a Listener to detect PointerSignalEvents, and manually scroll the list with a ScrollController.
For mobile, listening for events is not needed.
Extend a ScrollPhysics class as mako suggested but use NeverScrollableScrollPhysics for web to prevent the physics from interfering with the manual scrolling. To fix the glow indicator problem for mobile, wrap your CustomScrollView in a ScrollConfiguration as provided by nioncode.
Add overscroll_physics.dart from the gist.
Add custom_glowing_overscroll_indicator.dart from the other gist.
GestureBinding.instance.pointerSignalResolver.register is used to prevent the scroll event from propogating up the widget tree.
Example
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:my_project/custom_glowing_overscroll_indicator.dart';
import 'package:my_project/overscroll_physics.dart';
class OverscrollList extends StatelessWidget {
final ScrollController _scrollCtrl = ScrollController();
final double _topOverscroll = 200;
final double _bottomOverscroll = 200;
void _scrollList(Offset offset) {
_scrollCtrl.jumpTo(
_scrollCtrl.offset + offset.dy,
);
}
#override
Widget build(BuildContext context) {
return Container(
height: 300,
decoration: BoxDecoration(border: Border.all(width: 1)),
child: Listener(
onPointerSignal: (PointerSignalEvent event) {
if (kIsWeb) {
GestureBinding.instance.pointerSignalResolver.register(event, (event) {
_scrollList((event as PointerScrollEvent).scrollDelta);
});
}
},
child: ScrollConfiguration(
behavior: OffsetOverscrollBehavior(
leadingPaintOffset: -_topOverscroll,
trailingPaintOffset: -_bottomOverscroll,
),
child: CustomScrollView(
controller: _scrollCtrl,
physics: kIsWeb
? NeverScrollableOverscrollPhysics(
overscrollStart: _topOverscroll,
overscrollEnd: _bottomOverscroll,
)
: AlwaysScrollableOverscrollPhysics(
overscrollStart: _topOverscroll,
overscrollEnd: _bottomOverscroll,
),
slivers: [
SliverToBoxAdapter(
child: Container(width: 400, height: 100, color: Colors.blue),
),
SliverToBoxAdapter(
child: Container(width: 400, height: 100, color: Colors.yellow),
),
SliverToBoxAdapter(
child: Container(width: 400, height: 100, color: Colors.red),
),
SliverToBoxAdapter(
child: Container(width: 400, height: 100, color: Colors.orange),
),
],
),
),
),
);
}
}
dartpad demo
Mobile result:

Make first item (or padding) of SingleChildScrollView not scrollable and delegate the touch events

I have a Stack with two Columns.
First column contains contains only one child (1) with InteractiveViewer which has defined height (for example 250). It changes its content's zoom and translation based on scroll offset of SingleChildScrollView from second column. It shouldn't be pushed out of the screen while scrolling, so it's under the scroll view in a stack.
The second column have SingleChildScrollView with top padding (2) OR first view (3) that matches the (1) height.
Now I'd like to make the top padding (2) or view (3) not scroll the SingleChildScrollView but pass those touch events to InteractiveViewer. Doesn't matter whether the solution use padding or empty view, I just wanted to note here than what I want can be achieved with padding or view. I tried both but failed.
I tried the GestureDetector, IgnorePointer and AbsorbPointer, but seems like the SingleChildScrollView always get the touch events. I haven't found a way to make the top padding not scrollable too.
Basically I'd like to achieve something similar to the attached gif, except that I don't need the "Collapsing Header" text animation and app bar. Just pay attention to the mountains that hide below the scroll view. The scroll view should take entire screen once the scroll offset is equal padding/view height (250 in this example).
Is that possible somehow? My logic behind InteractiveViewer is way more complicated than the example provided below, I just simplified it to make the question easier to understand.
Stack(children: [
Column(
children: [
Container( // (1) Widget that should get the touch events
height: 250,
child: InteractiveViewer(...)
),
],
),
Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.only(top: 250), // (2) Either not scrollable padding
child: Column(
children: [
Container(height: 250), // (3) or not scrollable first item
Container(...)
],
),
),
),
],
),
]);

SlideTransition in flutter takes space before it slides in

I implemented SlideTransition for a widget in flutter, and it slides in as expected. The issue is that before the animation is called, the space where the slider is going to be displayed later is empty.
How can I make the parent to give this space to other widgets in the layout until the moment that a slider comes into view?
I was thinking about giving a slider the initial height of zero, but then the widgets inside the slider would act funny as the height changes in sync with sliding. I wonder if there is a simpler way.
The parent is:
new Scaffold(
appBar: _appBar,
body: new Column(
children: <Widget>[
new Expanded(
child: _body;
}),
),
new SlideTransition(
position: _sliderPosition, //
child: _slider,
),
],
);
And the position is defined as:
_sliderPosition = new MaterialPointArcTween(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).animate(_animationController);
I solved this issue by replacing _slider with _buildSlider() which returns null until a slider is needed.
This is also better for performance as there is no need to render a slider if it's hidden.
Bumped into the same issue with a SlideTransition taking the full height during a vertical slide. In the end I achieved the slide effect by using a SizeTransition instead, and setting its axisAlignment property accordingly (-1, 0, 1 for top/center/bottom in my case).