Flutter ListView scroll animation overlapping sibling widget - flutter

Hoping someone can help with this and it's not a bug and it's just me being silly.
There is very strange behavior from listview when it's not taking the full length of the screen and in a column.
When you scroll down, the animation at max extent persists and overlaps. I'm assuming this is a bug and not by design.
Here's the simple code to reproduce.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp(
items: List<MessageItem>.generate(
33,
(i) => MessageItem("Sender $i", "Message body $i"),
),
));
}
class MyApp extends StatelessWidget {
final List<MessageItem> items;
MyApp({Key key, #required this.items}) : super(key: key);
#override
Widget build(BuildContext context) {
final title = 'Mixed List';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Column(
children: [
Expanded(
child: Container(),
),
Expanded(
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: item.buildTitle(context),
subtitle: item.buildSubtitle(context),
);
},
),
),
],
),
),
);
}
}
/// A ListItem that contains data to display a message.
class MessageItem {
final String sender;
final String body;
MessageItem(this.sender, this.body);
Widget buildTitle(BuildContext context) => Text(sender);
Widget buildSubtitle(BuildContext context) => Text(body);
}

So final code will be. I have added the scroll phisycs BouncingScrollPhysics.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp(
items: List<MessageItem>.generate(
33,
(i) => MessageItem("Sender $i", "Message body $i"),
),
));
}
class MyApp extends StatelessWidget {
final List<MessageItem> items;
MyApp({Key key, #required this.items}) : super(key: key);
#override
Widget build(BuildContext context) {
final title = 'Mixed List';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Column(
children: [
Expanded(
child: Container(
),
),
Expanded(
child: ListView.builder(
physics: BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
itemCount: 50,
itemBuilder: (context, index) {
return ListTile(
title: Text("${index + 1}"),
subtitle: Text("${index + 1}"),
);
},
),
),
],
),
),
);
}
}
/// A ListItem that contains data to display a message.
class MessageItem {
final String sender;
final String body;
MessageItem(this.sender, this.body);
Widget buildTitle(BuildContext context) => Text(sender);
Widget buildSubtitle(BuildContext context) => Text(body);
}

I'm not sure if this is a bug or not. Or if my solution is the correct way of doing it, or not. But this work
#override
Widget build(BuildContext context) {
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
backgroundColor: Colors.white,
toolbarHeight: 200,
pinned: true,
forceElevated: innerBoxIsScrolled,
),
),
];
},
body: Container(
child: SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<String>("name"),
slivers: <Widget>[
SliverOverlapInjector(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 30,
),
),
),
],
);
},
),
)),
);
}
The reason I don't like this is that I'm putting non-bar content in an AppBar.
If anyone has a better solution please let me know.

Related

Expansion Tile Flutter

I want when I click on one of the Expansion Tiles, the rest of the items are closed, how can I do that?
Refer to below example code :
import 'package:flutter/material.dart';
class PickPlanPage extends StatefulWidget {
const PickPlanPage({Key? key}) : super(key: key);
#override
State<PickPlanPage> createState() => _PickPlanPageState();
}
class _PickPlanPageState extends State<PickPlanPage> {
bool initiallyExpanded = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView.separated(
itemCount: 5,
separatorBuilder: (context, index) => const SizedBox(height: 20),
itemBuilder: (context, index) => ExpansionTile(
initiallyExpanded: false,
title: const Text('title'),
onExpansionChanged: (value) => setState(() => initiallyExpanded = value),
children: const [
Text('Description'),
],
),
),
);
}
}
You can archive in your existing code with small modification.
make sure you need to add key in ListView & ExpansionTile widget. try bellow code.
class _PickPlanPageState extends State<PickPlanPage> {
int expandIndex = -1; // to handle only one expansion at a time.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView.separated(
key: Key(expandIndex.toString()), //<-- must add
itemCount: 5,
separatorBuilder: (context, index) => const SizedBox(height: 20),
itemBuilder: (context, index) => ExpansionTile(
key: Key(index.toString()), //<-- must add
initiallyExpanded: (index == expandIndex), //<-- must add
title: const Text('title'),
onExpansionChanged: (value) {
setState(() {
expandIndex = value ? index : -1;
});
},
children: const [
Text('Description'),
],
),
),
);
}
}
You can use ExpansionPanelList and track the selected item tap event.
class PickPlanPage extends StatefulWidget {
const PickPlanPage({Key? key}) : super(key: key);
#override
State<PickPlanPage> createState() => _PickPlanPageState();
}
class _PickPlanPageState extends State<PickPlanPage> {
int? openIndex;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: [
ExpansionPanelList(
children: [
for (int index = 0; index < 5; index++)
ExpansionPanel(
headerBuilder: (context, isExpanded) => GestureDetector(
onTap: () {
openIndex = index;
setState(() {});
},
child: const Text('title'),
),
isExpanded: index == openIndex,
body: Column(
children: [
Text('Description'),
],
),
),
],
),
],
),
);
}
}

SliverAppBar doesn't follow for user's gesture even if floating: true

When I scroll the list, not from the top SliverAppBar doesn't show until I reach the top of the list, but in the Telegram app, AppBar shows right after I start to scroll down.
I want to reach the same behavior as AppBar in the Telegram app.
I also should to mentioned that snap: true gives close but not exactly the same behavior that I need
Here is my code now (Live example):
import 'package:flutter/material.dart';
class TestPage extends StatefulWidget {
TestPage({Key? key, this.title}) : super(key: key);
final String? title;
#override
_TestPageState createState() => _TestPageState();
}
class TabElement {
String id;
int? count;
String? label;
TabElement({required this.id, this.count, this.label}) {
label ??= id;
}
}
class Element {
late String id;
}
class _TestPageState extends State<TestPage> {
final Map<String, List<Element>?> _orders = {
'step1': null,
'step2': null,
'step3': null,
'step4': null,
'step5': null,
'step6': null,
'step7': null,
'step8': null,
'step9': null,
'step10': null,
'step11': null,
'step12': null,
'step13': null,
'step14': null,
};
List<TabElement> get _tabs => _orders.entries
.map((el) => TabElement(id: el.key, count: el.value?.length))
.toList();
#override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
length: _tabs.length,
child: _nestedScrollView(widget, _tabs, _orders),
),
);
}
}
Widget _tab(TabElement tab) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (tab.label != null)
Text(
tab.label as String,
style: TextStyle(color: Colors.black),
),
],
);
}
Widget _nestedScrollView(TestPage widget, List<TabElement> tabs, orders) {
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: _sliverAppBar(context, widget, innerBoxIsScrolled, tabs),
),
];
},
body: _tabBarView(tabs, orders),
);
}
Widget _sliverAppBar(BuildContext context, TestPage widget,
bool innerBoxIsScrolled, List<TabElement> tabs) {
return SliverAppBar(
title: Text(
widget.title as String,
style: TextStyle(color: Colors.black),
),
centerTitle: false,
backgroundColor: Colors.white,
pinned: true,
floating: true,
snap: false,
forceElevated: innerBoxIsScrolled,
bottom: TabBar(
isScrollable: true,
tabs:
tabs.map<Widget>((TabElement tab) => Tab(child: _tab(tab))).toList(),
),
);
}
Widget _tabBarView(List<TabElement> tabs, orders) {
return TabBarView(
children: tabs
.map<Widget>((tab) => SafeArea(
top: false,
bottom: false,
child: Builder(builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<String>(tab.id),
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
),
SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 80,
),
),
],
);
})))
.toList());
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: TestPage(title: 'Test'),
);
}
}
The answer was surprisingly simple. NestedScrollView has a floatHeaderSlivers method that adds the required behavior.
NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[];
},
body: _tabBarView(tabs, orders),
)

Dart / Flutter - How do I convert a passed string to a lists name?

I'm trying to covert a passed String and convert it to a known List name.
I have a Map with a key value pair of string, string and I have 2 lists. The lists names are the held in the key field of the map. When I pass the the list name to my widget, I want it to show me the list contents, in list tiles, but I can't figure out how to convert that String to the list name. Here is my code:
I send on Line 71 and receive on Line 87.
import 'package:flutter/material.dart';
void main() {
runApp(ListOfHomes());
}
////This is my list of homes this will even eventually come from firestore
Map<String, String> homes = {
'home1': 'The one with the little yellow roof',
'home2': 'The one with the outhouse',
};
//Home one has these rooms this will even eventually come from firestore
List<String> home1 = [
'Room1',
'Room2',
];
//Home2 has these rooms this will even eventually come from firestore
List<String> home2 = [
'Room1',
'Room2',
];
//This is OK for the moment
class ListOfHomes extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Home List'),
),
body: SafeArea(
child: Column(
children: [
ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: homes.length,
itemBuilder: (context, index) {
String key = homes.keys.elementAt(index);
return RowsWithData(roomDecor: key, roomName: homes[key]);
})
],
),
),
));
}
}
class RowsWithData extends StatelessWidget {
RowsWithData({this.roomDecor, this.roomName});
final String roomName;
final String roomDecor;
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
FlatButton(
onPressed: () {
print(roomDecor);
Navigator.push(
context,
MaterialPageRoute(
///this is where my problem lies I'm sending a String
builder: (context) => ListOfRooms(roomlist: roomDecor)));
},
child: ListTile(
title: Text(roomDecor),
subtitle: Text(roomName),
),
),
],
);
}
}
class ListOfRooms extends StatelessWidget {
///I'm receiving the string name here, but I need to return the corresponding List name
///
final String roomlist;
ListOfRooms({this.roomlist});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('list of rooms'),
),
body: SafeArea(
child: Column(
children: [
ListView.builder(
itemCount: roomlist.length,
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemBuilder: (context, position) {
return ListTile(title: Text('${roomlist[position]}'));
}),
],
),
));
}
}
I would like to give you some idea. Try something like this,
class ListOfRooms extends StatelessWidget {
final Map homes = {
'home1': ['Room1', 'Room2'],
'home2': ['Room1', 'Room2'],
};
final String roomlist;
ListOfRooms({this.roomlist});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('list of rooms'),
),
body: SafeArea(
child: Column(
children: [
ListView.builder(
itemCount: homes[roomlist].length,
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemBuilder: (context, position) {
return ListTile(
title: Text('${homes[roomlist][position]}'),
);
},
),
],
),
),
);
}
}
or at the top level, like this,
// Below homes = {}
final Map detailedHomes = {
'home1': home1,
'home2': home2,
};
// Access it with detailedHomes['HOME NAME'][POSITION];
Hope that suits your case!
try to use map like this
import 'package:flutter/material.dart';
void main() {
runApp(ListOfHomes());
}
////This is my list of homes this will even eventually come from firestore
Map<String, String> homes = {
'home1': 'The one with the little yellow roof',
'home2': 'The one with the outhouse',
};
//Home one has these rooms this will even eventually come from firestore
List<String> home1 = [
'Room1',
'Room2',
];
//Home2 has these rooms this will even eventually come from firestore
List<String> home2 = [
'Room1',
'Room2',
];
//This is OK for the moment
class ListOfHomes extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Home List'),
),
body: SafeArea(
child: Column(
children: [
ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: homes.length,
itemBuilder: (context, index) {
String key = homes.keys.elementAt(index);
return RowsWithData(roomDecor: key, roomName: homes[key]);
})
],
),
),
));
}
}
class RowsWithData extends StatelessWidget {
RowsWithData({this.roomDecor, this.roomName});
final String roomName;
final String roomDecor;
Map<String,dynamic> map={
'home1':home1,
'home2':home2
};
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
FlatButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
///this is where my problem lies I'm sending a String
builder: (context) => ListOfRooms(roomlist: map[roomDecor])));
},
child: ListTile(
title: Text(roomDecor),
subtitle: Text(roomName),
),
),
],
);
}
}
class ListOfRooms extends StatelessWidget {
///I'm receiving the string name here, but I need to return the corresponding List name
final List<String> roomlist;
ListOfRooms({this.roomlist});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('list of rooms'),
),
body: SafeArea(
child: Column(
children: [
ListView.builder(
itemCount: roomlist.length,
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemBuilder: (context, position) {
return ListTile(title: Text('${roomlist[position]}'));
}),
],
),
));
}
}

CustomScrollView within ExpansionTile error

Tried to fix this reading some documentation and some open issues but was not lucky.. Could someone please help?
I am getting this error:
type 'bool' is not a subtype of type 'double' in type cast
Not sure why though I tried adding container wrapping the component, adding height, adding flexible box etc...
No lucky
`import 'package:flutter/material.dart';
class SampleData {
SampleData(this.title, [this.children = const <SizedBox>[]]);
final String title;
final List<SizedBox> children;
}
final List<SampleData> data = <SampleData>[
SampleData("IT", [
SizedBox(
height: 300,
width: 300,
child: CustomScrollView(
scrollDirection: Axis.horizontal,
slivers: <Widget>[
new SliverToBoxAdapter(
child: Text('fesfefes'),
),
],
),
),
]),
];
class Branch extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Test 123'),
),
body: Container(
width: 500,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) => Item(data[index]),
itemCount: data.length,
),
),
);
}
}
// Displays one Entry. If the entry has children then it's displayed
// with an ExpansionTile.
class Item extends StatelessWidget {
const Item(this.sample);
final SampleData sample;
Widget _buildTiles(SampleData root) {
return SizedBox(
width: 500,
child: ExpansionTile(
key: PageStorageKey<SampleData>(root),
title: Text(root.title),
children: root.children,
),
);
}
#override
Widget build(BuildContext context) {
return _buildTiles(sample);
}
}
`
You can copy paste run full code blow
You can remove
//key: PageStorageKey<SampleData>(root),
working demo
full code
import 'package:flutter/material.dart';
class SampleData {
SampleData(this.title, [this.children = const <SizedBox>[]]);
final String title;
final List<SizedBox> children;
}
final List<SampleData> data = <SampleData>[
SampleData("IT", [
SizedBox(
height: 300,
width: 300,
child: CustomScrollView(
scrollDirection: Axis.horizontal,
slivers: <Widget>[
new SliverToBoxAdapter(
child: Text('fesfefes'),
),
],
),
),
]),
];
class Branch extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Test 123'),
),
body: Container(
width: 500,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) => Item(data[index]),
itemCount: data.length,
),
),
);
}
}
// Displays one Entry. If the entry has children then it's displayed
// with an ExpansionTile.
class Item extends StatelessWidget {
const Item(this.sample);
final SampleData sample;
Widget _buildTiles(SampleData root) {
return SizedBox(
width: 500,
child: ExpansionTile(
//key: PageStorageKey<SampleData>(root),
title: Text(root.title),
children: root.children,
),
);
}
#override
Widget build(BuildContext context) {
return _buildTiles(sample);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Branch(),
);
}
}

How to make scrollbar visible all time?

How do I make a scrollbar visible before user starts scrolling in flutter. Here is the code how I made my list
Scrollbar(
child: ListView.separated(
itemBuilder:
(ctx,index){
return ListTile(
isThreeLine: false,
dense: true,
leading: IconButton(
icon: Icon(Icons.location_on),
onPressed: null,
),
title: Text(bList[index].bName),
onTap: ()=>_homescreen(bList[index].bId,bList[index].bName,index),
);
},
separatorBuilder: (context,index) => Divider(color: Colors.black,),
itemCount: bList.length == null ? 0 : bList.length),
)
Wrapping the List inside Scrollbar I am able to see a scrollbar on scrolling. But is it possible to make scrollbar visible all time? Thanks in advance.
You can wrap your ListView with Scrollbar like below. When isAlwaysShown is true, must pass a controller that is attached to a scroll view
Scrollbar(
controller: ScrollController(),
isAlwaysShown: true,
child: ListView...
Demo: DartPad
You can use ScrollbarPainter. Then use AlwaysStoppedAnimation<double>(1.0) to make it always visible, ScrollNotification to update the scroll position.
MyScrollbar.dart
import 'package:flutter/material.dart';
const double _kScrollbarThickness = 6.0;
class MyScrollbar extends StatefulWidget {
final ScrollableWidgetBuilder builder;
final ScrollController scrollController;
const MyScrollbar({
Key key,
this.scrollController,
#required this.builder,
}) : assert(builder != null),
super(key: key);
#override
_MyScrollbarState createState() => _MyScrollbarState();
}
class _MyScrollbarState extends State<MyScrollbar> {
ScrollbarPainter _scrollbarPainter;
ScrollController _scrollController;
Orientation _orientation;
#override
void initState() {
super.initState();
_scrollController = widget.scrollController ?? ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) {
_updateScrollPainter(_scrollController.position);
});
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
_scrollbarPainter = _buildMaterialScrollbarPainter();
}
#override
void dispose() {
_scrollbarPainter.dispose();
super.dispose();
}
ScrollbarPainter _buildMaterialScrollbarPainter() {
return ScrollbarPainter(
color: Theme.of(context).highlightColor.withOpacity(1.0),
textDirection: Directionality.of(context),
thickness: _kScrollbarThickness,
fadeoutOpacityAnimation: const AlwaysStoppedAnimation<double>(1.0),
padding: MediaQuery.of(context).padding,
);
}
bool _updateScrollPainter(ScrollMetrics position) {
_scrollbarPainter.update(
position,
position.axisDirection,
);
return false;
}
#override
void didUpdateWidget(MyScrollbar oldWidget) {
super.didUpdateWidget(oldWidget);
_updateScrollPainter(_scrollController.position);
}
#override
Widget build(BuildContext context) {
return OrientationBuilder(
builder: (context, orientation) {
_orientation ??= orientation;
if (orientation != _orientation) {
_orientation = orientation;
_updateScrollPainter(_scrollController.position);
}
return NotificationListener<ScrollNotification>(
onNotification: (notification) =>
_updateScrollPainter(notification.metrics),
child: CustomPaint(
painter: _scrollbarPainter,
child: widget.builder(context, _scrollController),
),
);
},
);
}
}
Usage: main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'MyScrollbar.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
);
}
}
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: MyScrollbar(
//scrollController: ctrl, //You can assign your scroll controller here or ignore
builder: (context, scrollController) => ListView.builder(
controller: scrollController, //should scrollController from callback
itemCount: 30,
itemBuilder: (context, index) => ListTile(
title: Text("Index $index"),
),
),
),
);
}
}
Note:
If one the child in ListView widget is Stateful widget and dynamically changes its size, then the MyScrollbar may not get updated.
you can use draggable scrollbar package https://pub.dev/packages/draggable_scrollbar to achieve this.
There are many possabilitys, you can show it always by using alwaysVisibleScrollThumb: true,
Fore example you can do:
DraggableScrollbar.rrect(
controller: myScrollController,
child: ListView.builder(
controller: myScrollController,
itemCount: 1000,
itemExtent: 100.0,
itemBuilder: (context, index) {
return Container(
padding: EdgeInsets.all(8.0),
child: Material(
elevation: 4.0,
borderRadius: BorderRadius.circular(4.0),
color: Colors.green[index % 9 * 100],
child: Center(
child: Text(index.toString()),
),
),
);
},
),
);
Another way of doing it, originally created from #slightfoot: https://gist.github.com/slightfoot/beb74749bf2e743a6da294b37a7dcf8d
You can reach always visible scrollbar with custom scroll paint like in the following example code:
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.indigo,
accentColor: Colors.pinkAccent,
),
home: ExampleScreen(),
),
);
}
class ExampleScreen extends StatefulWidget {
#override
_ExampleScreenState createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('SingleChildScrollView With Scrollbar'),
),
body:
Container(
height: MediaQuery.of(context).size.height * 0.3,
child:
SingleChildScrollViewWithScrollbar(
scrollbarColor: Theme.of(context).accentColor.withOpacity(0.75),
scrollbarThickness: 8.0,
child: Container(
//height: 1500,
child: ListView(
shrinkWrap: true,
children: <Widget>[
ListTile(title: Text('Item 1')),
ListTile(title: Text('Item 2')),
ListTile(title: Text('Item 3')),
ListTile(title: Text('Item 4')),
ListTile(title: Text('Item 5')),
ListTile(title: Text('Item 6')),
ListTile(title: Text('Item 1')),
ListTile(title: Text('Item 2')),
ListTile(title: Text('Item 3')),
ListTile(title: Text('Item 4')),
ListTile(title: Text('Item 5')),
ListTile(title: Text('Item 6')),
],
),
),
),),
);
}
}
class SingleChildScrollViewWithScrollbar extends StatefulWidget {
const SingleChildScrollViewWithScrollbar({
Key key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.padding,
this.primary,
this.physics,
this.controller,
this.child,
this.dragStartBehavior = DragStartBehavior.down,
this.scrollbarColor,
this.scrollbarThickness = 6.0,
}) : super(key: key);
final Axis scrollDirection;
final bool reverse;
final EdgeInsets padding;
final bool primary;
final ScrollPhysics physics;
final ScrollController controller;
final Widget child;
final DragStartBehavior dragStartBehavior;
final Color scrollbarColor;
final double scrollbarThickness;
#override
_SingleChildScrollViewWithScrollbarState createState() => _SingleChildScrollViewWithScrollbarState();
}
class _SingleChildScrollViewWithScrollbarState extends State<SingleChildScrollViewWithScrollbar> {
AlwaysVisibleScrollbarPainter _scrollbarPainter;
#override
void didChangeDependencies() {
super.didChangeDependencies();
rebuildPainter();
}
#override
void didUpdateWidget(SingleChildScrollViewWithScrollbar oldWidget) {
super.didUpdateWidget(oldWidget);
rebuildPainter();
}
void rebuildPainter() {
final theme = Theme.of(context);
_scrollbarPainter = AlwaysVisibleScrollbarPainter(
color: widget.scrollbarColor ?? theme.highlightColor.withOpacity(1.0),
textDirection: Directionality.of(context),
thickness: widget.scrollbarThickness,
);
}
#override
void dispose() {
_scrollbarPainter?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return RepaintBoundary(
child: CustomPaint(
foregroundPainter: _scrollbarPainter,
child: RepaintBoundary(
child: SingleChildScrollView(
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
padding: widget.padding,
primary: widget.primary,
physics: widget.physics,
controller: widget.controller,
dragStartBehavior: widget.dragStartBehavior,
child: Builder(
builder: (BuildContext context) {
_scrollbarPainter.scrollable = Scrollable.of(context);
return widget.child;
},
),
),
),
),
);
}
}
class AlwaysVisibleScrollbarPainter extends ScrollbarPainter {
AlwaysVisibleScrollbarPainter({
#required Color color,
#required TextDirection textDirection,
#required double thickness,
}) : super(
color: color,
textDirection: textDirection,
thickness: thickness,
fadeoutOpacityAnimation: const AlwaysStoppedAnimation(1.0),
);
ScrollableState _scrollable;
ScrollableState get scrollable => _scrollable;
set scrollable(ScrollableState value) {
_scrollable?.position?.removeListener(_onScrollChanged);
_scrollable = value;
_scrollable?.position?.addListener(_onScrollChanged);
_onScrollChanged();
}
void _onScrollChanged() {
update(_scrollable.position, _scrollable.axisDirection);
}
#override
void dispose() {
_scrollable?.position?.removeListener(notifyListeners);
super.dispose();
}
}
Switch to flutter branch master
Add isAlwaysShown: true in Scrollbar
Ref
wrap your widget in Scrollbar widget
For Scrollbar or RawScrollbar visible all time, use thumbVisibility: true (isAlwaysShown: true is deprecated)
By using Scrollbar For Custom Scroll Bar,
**ScrollController controller = ScrollController(); // Mandatory: ScrollController**
Scrollbar(
controller: controller, // Mandatory: ScrollController
// isAlwaysShown: true, // deprecated
thumbVisibility: true, // For always showing Scroll Bar: Use this
thickness: 10, // Optional: Thickness
radius: Radius.circular(5), // Optional: Radius
child: ListView.builder(
itemCount: 100,
controller: controller, // Mandatory: ScrollController
itemBuilder: (context, index){
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.deepOrangeAccent,
child: FlutterLogo(),
),
title: Text('Title-$index'),
subtitle: Text('Subtitle-$index'),
trailing: CircleAvatar(
child: Icon(Icons.navigate_next),
backgroundColor: Colors.deepOrangeAccent,
),
);
},
),
),
By using RawScrollbar For Custom Scroll Bar,
ScrollController controller = ScrollController(); // Mandatory: ScrollController
RawScrollbar(
controller: controller, // Mandatory: ScrollController
// isAlwaysShown: true, // deprecated
thumbVisibility: true, // For always showing Scroll Bar: Use this
thickness: 10, // Optional: Thickness
thumbColor: Colors.greenAccent, // Optional: Color
radius: Radius.circular(5), // Optional: Radius
child: ListView.builder(
itemCount: 100,
controller: controller, // Mandatory: ScrollController
itemBuilder: (context, index){
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.deepOrangeAccent,
child: FlutterLogo(),
),
title: Text('Title-$index'),
subtitle: Text('Subtitle-$index'),
trailing: CircleAvatar(
child: Icon(Icons.navigate_next),
backgroundColor: Colors.deepOrangeAccent,
),
);
},
),
)