Flutter: How to create bidirectional scrolling ListView with fixed portion of on left and bottom - flutter

How can I make a scrolling view in Flutter in which a left and bottom portion of the screen is fixed (axes), then the rest of the screen can be scrolled horizontally to the right, or vertically upward. (imagine scrolling a graph with two axes .. see image)

Very interesting question. After looking through the docs I couldn't find a widget that would fit this scenario. So I decided to search a bit on pub.dev for a plugin that could make this happen.
Found it: https://pub.dev/packages/bidirectional_scroll_view
The plugin does a fairly good job of scrolling content on both axis, but to get what you are looking for ("fixed portion of on left and bottom") you are gonna have to structure your page accordingly. I decided to go with Stack and Align widgets, here is what it looks like:
See the full code on a working DartPad: https://dartpad.dev/10573c0e9bfa7f1f8212326b795d8628
Or take a look at the code bellow (don't forget to include bidirectional_scroll_view in your project):
void main() {
runApp(new MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
BidirectionalScrollViewPlugin _plugin;
double fixedAxisSpace = 100.0;
double biDirectContentWidth = 4096.0;
double biDirectContentHeight = 4096.0;
#override
void initState() {
super.initState();
_plugin = new BidirectionalScrollViewPlugin(
child: _buildWidgets(),
velocityFactor: 0.0,
);
}
void _snapToZeroZero(BuildContext context){
double yOffset = biDirectContentHeight + fixedAxisSpace - context.size.height;
_plugin.offset = new Offset(0, yOffset);
}
#override
Widget build(BuildContext context) {
final btnSnapToZeroZero = Padding(
padding: EdgeInsets.all(10.0),
child:FlatButton(
color: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: new BorderRadius.circular(12.0),
),
onPressed: () { _snapToZeroZero(context); },
child: Text(
"Snap to 0.0",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white),
),
)
);
return new MaterialApp(
debugShowCheckedModeBanner: false,
home: new Scaffold(
body: Stack(
children: <Widget>[
_plugin, // BidirectionalScrollViewPlugin, goes 1st because we want it to sit on the bottom layer
Align( // Y Axis goes over _plugin, it is aligned to topLeft
alignment: Alignment.topLeft,
child: Column(
children: <Widget> [
Expanded(
child: Container(
width: fixedAxisSpace,
decoration: BoxDecoration(
color: Colors.white, // change to Colors.white70 too se what is going on "behind the scene"
border: Border(
right: BorderSide(width: 1.0, color: Colors.black),
),
),
child: Center(child: VerticalTextWidget("FIXED _ Y AXIS", 22))
),
),
SizedBox(height: fixedAxisSpace),
]
),
),
Align( // X Axis goes over _plugin and Y Axis, it is aligned to bottomLeft
alignment: Alignment.bottomLeft,
child: Row(
children: <Widget> [
SizedBox(width: fixedAxisSpace),
Expanded(
child: Container(
height: fixedAxisSpace,
decoration: BoxDecoration(
color: Colors.white, // change to Colors.white70 too se what is going on "behind the scene"
border: Border(
top: BorderSide(width: 1.0, color: Colors.black),
),
),
child: Center(child: Text("FIXED | X AXIS", style: TextStyle(fontSize: 22)))
),
),
]
),
),
Align( // this little square is optional, I use it to put a handy little button over everything else at the bottom left corner.
alignment: Alignment.bottomLeft,
child: Container(
color: Colors.white, // change to Colors.white70 too se what is going on "behind the scene"
height: fixedAxisSpace,
width: fixedAxisSpace,
child: btnSnapToZeroZero
),
),
],
)
)
);
}
// put your large bidirectional content here
Widget _buildWidgets() {
return new Padding(
padding: EdgeInsets.fromLTRB(100, 0, 0, 100),
child: SizedBox(
width: biDirectContentWidth,
height: biDirectContentHeight,
child: Image.network(
'https://i.stack.imgur.com/j1ItQ.png?s=328&g=1',
repeat: ImageRepeat.repeat,
alignment: Alignment.bottomLeft
),
)
);
}
}
VerticalTextWidget:
class VerticalTextWidget extends StatelessWidget {
final String text;
final double size;
const VerticalTextWidget(this.text, this.size);
#override
Widget build(BuildContext context) {
return Wrap(
direction: Axis.vertical,
alignment: WrapAlignment.center,
children: text.split("").map((string) => Text(string, style: TextStyle(fontSize: size))).toList(),
);
}
}

Related

Adjust Width from the Left Side of Container

Changing the width of a container defaults to resizing from the right side.
The pseudocode below has an example where dx is a variable that can change. When it increases or decreases, the container will always grow or shrink from the right side.
Is there a simple way to switch the direction so that the width will increase or decrease from the left side instead of the right side?
Container(
width: dx,
height:200
)
Here is a dartpad gist that shows how the right side of the container's width changes when dragged. What I'm asking is if there is a quick and simple way to make the left side expand/contract without having to animate the position of the container: https://dartpad.dev/?id=ebbe57041bf950018fe5733674c68b20
I checked out your dartpad code. To achieve what you want, I suggest you put two empty Containers on either side of your handles and decrease their size when the handles are dragged (your center Container should also be inside an Expanded widget to take up all the allowed space). here is the example code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
// Application name
title: 'Flutter Stateful Clicker Counter',
theme: ThemeData(
// Application theme data, you can set the colors for the application as
// you want
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Clicker Counter Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({Key? key, required this.title}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
double? rightContainerWidth, leftContainerWidth;
#override
Widget build(BuildContext context) {
rightContainerWidth ??= MediaQuery.of(context).size.width / 2 - 20;
leftContainerWidth ??= MediaQuery.of(context).size.width / 2 - 20;
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Wrap(
children: <Widget>[
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
// left handle
Container(
width: leftContainerWidth,
),
GestureDetector(
onHorizontalDragUpdate: (DragUpdateDetails details) {
setState(() {
leftContainerWidth = details.globalPosition.dx;
});
},
child: Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
bottomLeft: Radius.circular(20),
)),
width: 10,
height: 200)),
Expanded(
child: Container(
// padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: ClipRect(
child: Container(
// width: _counter+0.2,
height: 200,
color: Colors.green,
))),
),
GestureDetector(
onHorizontalDragStart: (DragStartDetails details) {
print("st: ${details.localPosition.dx}");
// dx for start is the x offset of the mouse relative to the container
// changeX = (_counter as double) - details.localPosition.dx.floor();
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
setState(() {
// print(details.localPosition.dx);
rightContainerWidth = MediaQuery.of(context).size.width -
details.globalPosition.dx;
});
},
child: Container(
width: 10,
height: 200,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
bottomRight: Radius.circular(20),
)))),
Container(
width: rightContainerWidth,
),
])
],
),
),
);
}
}
Caution: I did not add conditional statements to prevent overflows, make sure you also add them!
Use Wrap Widget
A widget that displays its children in multiple horizontal or vertical runs.
A Wrap lays out each child and attempts to place the child adjacent to the previous child in the main axis, given by direction, leaving spacing space in between. If there is not enough space to fit the child, Wrap creates a new run adjacent to the existing children in the cross axis.
After all the children have been allocated to runs, the children within the runs are positioned according to the alignment in the main axis and according to the crossAxisAlignment in the cross axis.
The runs themselves are then positioned in the cross axis according to the runSpacing and runAlignment.
Example:
Wrap(
spacing: 8.0, // gap between adjacent chips
runSpacing: 4.0, // gap between lines
children: <Widget>[
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue.shade900, child: const Text('AH')),
label: const Text('Hamilton'),
),
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue.shade900, child: const Text('ML')),
label: const Text('Lafayette'),
),
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue.shade900, child: const Text('HM')),
label: const Text('Mulligan'),
),
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue.shade900, child: const Text('JL')),
label: const Text('Laurens'),
),
],
)

Parallax-style header scrolling performance in flutter

I'm developing a parallax-style header/background block in my flutter application, which scrolls upwards at around 1/3 the speed of the foreground content. All parts in the foreground are within the same customScrollView and the background header is in a positioned container at the top of the stack.
I'm using a listener on the customscrollview to update a y-offset integer, and then using that integer to update the top position on the element inside my stack.
While this works as expected, the issue I'm facing is a large amount of repainting takes place on scroll, which in the future may impact performance. I'm sure there may be a more efficient way to achieve this - such as placing the entire background in a separate child widget and passing the controller down to it from the parent widget - however I am struggling to find any information on doing so, or if this is the correct approach.
Can someone point me in the right direction for refactoring this in such a way as to disconnect the scrolling background from the foreground, so that the foreground doesn't repaint constantly?
class ScrollingWidgetList extends StatefulWidget {
ScrollingWidgetList();
#override
State<StatefulWidget> createState() {
return _ScrollingWidgetList();
}
}
class _ScrollingWidgetList extends State<ScrollingWidgetList> {
ScrollController _controller;
double _offsetY = 0.0;
_scrollListener() {
setState(() {
_offsetY = _controller.offset;
});
}
#override
void initState() {
_controller = ScrollController();
_controller.addListener(_scrollListener);
super.initState();
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
top: -(_offsetY / 3),
child: ConstrainedBox(
constraints: new BoxConstraints(
maxHeight: 300.0,
minHeight: MediaQuery.of(context).size.width * 0.35),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [
Theme.of(context).primaryColorDark,
Colors.blueGrey[900].withOpacity(0.8)
],
)),
height: MediaQuery.of(context).size.width * 0.35)),
width: MediaQuery.of(context).size.width,
),
CustomScrollView(controller: _controller, slivers: [
SliverList(
delegate: SliverChildListDelegate([
Padding(
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0),
child: ListTile(
title: Padding(
padding: const EdgeInsets.only(top: 6.0),
child: Text('Header text',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Colors.white)),
),
subtitle: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text('Subtitle text',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white)),
),
))
])),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return FakeItem(
executing: false,
delay: index.isOdd,
complete: false,
cancelled: false);
},
childCount: 30,
)),
])
],
);
}
}
A great solution was added by #pskink in the comments, however they seemed to have removed it. For anyone searching for an elegant solution, this is the basics of what was settled on.
You can see in the below code there is two layouts that are being handled by CustomMultiChildLayout. Hopefully this helps anyone searching for a similar solution
class ScrollList extends StatelessWidget {
final ScrollController _controller = ScrollController();
#override
Widget build(BuildContext context) {
return CustomMultiChildLayout(
delegate: ScrollingChildComponentDelegate(_controller),
children: <Widget>[
// background element layout
LayoutId(
id: 'background',
child: DecoratedBox(
decoration: BoxDecoration(
// box decoration
),
),
),
// foreground element layout
LayoutId(
id: 'scrollview',
child: CustomScrollView(
controller: _controller,
physics: AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: ListTile(
title: Text('TitleText'),
),
subtitle: Text('SubtitleText'),
)),
),
SliverList(
delegate: SliverChildBuilderDelegate(itemBuilder,
childCount: 100),
),
],
)),
],
);
}
}
// itembuilder for child components
Widget itemBuilder(BuildContext context, int index) {
return Card(
margin: EdgeInsets.all(6),
child: ClipPath(
clipper: ShapeBorderClipper(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
child: Container(
// child element content
)));
}
// controller for the animation
class ScrollingChildComponentDelegate extends MultiChildLayoutDelegate {
final ScrollController _controller;
ScrollingChildComponentDelegate(this._controller) : super(relayout: _controller);
#override
void performLayout(Size size) {
positionChild('background', Offset(0, -_controller.offset / 3));
layoutChild('background',
BoxConstraints.tightFor(width: size.width, height: size.height * 0.2));
positionChild('scrollview', Offset.zero);
layoutChild('scrollview', BoxConstraints.tight(size));
}
#override
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => true;
}

Why does my image disappear when I scroll down?

I have 4 cards to take user inputs in a page, this 4 card widgets is showing in the listView, when user tap AddPhoto card, user can add photo from camera or gallery successfully, but after user added photo and scrolling page to give in inputs to the other cards, when AddPhoto card disappears because of sliding, after user turn back to the initial point of the page, the picture that user added is showing nothing.
How can I solve it?
class AddPhotoCard extends StatefulWidget {
AddPhotoCard({this.subCategoryCardId,this.subCategoryId});
final int subCategoryId;
final int subCategoryCardId;
#override
_AddPhotoCardState createState() => _AddPhotoCardState();
}
class _AddPhotoCardState extends State<AddPhotoCard> {
String path;
#override
Widget build(BuildContext context) {
return AnimatedPadding(
duration: Duration(milliseconds: 500),
padding: path==null?EdgeInsets.only(top: 7.5,left: 30,right: 30,bottom:7.5):
EdgeInsets.only(top: 7.5,left: 7.5,right: 7.5,bottom:7.5),
child: GestureDetector(
onTap: (){
_showOptions(context);
},
child: AnimatedContainer(
duration: Duration(milliseconds:500),
height: path==null?200:400,
child: Container(
decoration: BoxDecoration(
border: Border.all(style: BorderStyle.solid, width: 1),
borderRadius: BorderRadius.circular(30),
color:categoryModels[widget.subCategoryId].subCategoryModels[widget.subCategoryCardId].categoryColor.withOpacity(0.5),
),
child: Padding(
padding:EdgeInsets.all(5.0),
child: Column(
children: [
Expanded(
child: AspectRatio(
aspectRatio: 600/600,
child: ClipRRect(
borderRadius: BorderRadius.circular(30),
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fitWidth,
alignment: FractionalOffset.center,
image: path==null?AssetImage("images/stickerForRecipeScreen.png"):FileImage(File(path)),
)
),
),
),
),
),
path==null?Text(
"${categoryModels[widget.subCategoryId].subCategoryModels[widget.subCategoryCardId]
.subCategoryName} tarifiniz için bir resim çekin",
style: TextStyle(
color: Colors.black,
fontFamily: "OpenSans",
fontWeight: FontWeight.bold,
fontSize: 20),): SizedBox.shrink(),
path==null?Icon(
Icons.camera_alt_rounded,
color: Colors.black,size: 70,): SizedBox.shrink(),
path==null?SizedBox(height: 20): SizedBox.shrink(),
],
),
),
),
),
),
);
}
}
This problem occurs because the ListView automatically destroys any widgets that are "scrolled away". You need to keep them alive or increase the cache:
Solution 1: Add this to your ListView:
cacheExtent: 4 //the number of widgets you have and want to keepAlive
Solution 2: Make your widgets a keepAlive:
//add this to the AddPhoto build method
super.build(context);
//add this to the end of the AddPhoto state, outside the build method
#override
bool get wantKeepAlive => true;
//add this then to your ListView
addAutomaticKeepAlives: true

How to drag elements inside a zoomable content widget?

I'm trying to create an editable node diagram in a draggable/zoomable viewport (kind of like the node system in Blender). A user should be able to edit and drag the nodes.
I can't get the PhotoView (which I use as a viewport) to stay still when I actually want to drag a node within it. All of the PhotoView's children are moving when only one widget - the node - should do so.
I've tried placing boxes listening to pointer events (to make them draggable) inside a PhotoView, but somehow, anything outside a centered area the size of the screen doesn't receive any touches.
Minimal code so far:
Creating the PhotoViewController (inside a State):
double scale = 1;
PhotoViewController controller;
#override
void initState() {
super.initState();
controller = PhotoViewController()
..outputStateStream.listen(listener);
}
void listener(PhotoViewControllerValue value) {
setState(() {
// store the scale in a local variable to drag widgets in relation to the zoom
scale = value.scale;
});
}
Building the viewport (also part of the State):
Offset position = Offset(0, 0);
#override
Widget build(BuildContext context) {
return Center(
child: PhotoView.customChild(
child: Stack(
children: <Widget>[
// vertical line
Center(
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black
),
child: Container(
width: 1,
height: 1000,
),
),
),
// horizontal line
Center(
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black
),
child: Container(
width: 1000,
height: 1,
),
),
),
// box to debug the initial screen size
Center(
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.black12
),
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
),
),
),
// stack containing all nodes (only one in this case)
Stack(
children: <Widget>[
// build a node...
Center(
child: Transform.translate( // offset the node
offset: position,
child: Listener( // make it a listener
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.red
),
child: Container(
width: 130,
height: 100,
child: Column(
children: <Widget>[
Text("Node",
style: TextStyle(
color: Colors.white,
fontSize: 18.0
),
)
],
),
),
),
// make the node listen to touch movements and eventually change its position
onPointerMove: (event) =>
{
setState(() =>
{
position +=
event.delta.scale(1 / scale, 1 / scale)
})
},
)
)
),
]
),
],
),
childSize: Size(10000, 10000),
backgroundDecoration: BoxDecoration(color: Colors.white),
initialScale: 1.0,
controller: controller,
),
);
}
At the moment, I kind of fixed the unintentional viewport drag by toggling a bool (whenever a node is touched/released) and overwriting the PhotoView's value.
Switching from the photo_view package to pskink's matrix_gesture_detector solved my issues. Though I now follow a different system: for every node, a new matrix is created (deriving from a main matrix, translated by the node's position). Matrices may be used inside a Transform widget to transform its child.
I achieved building a translatable viewport in which there is a draggable node carrying a clickable checkbox.
Some working minimal code:
import 'package:matrix_gesture_detector/matrix_gesture_detector.dart';
import 'package:vector_math/vector_math_64.dart' as vector;
...
// following code is executed inside a State
Matrix4 matrix = Matrix4.identity();
ValueNotifier<int> notifier = ValueNotifier(0);
vector.Vector3 nodePosition = vector.Vector3(50, 0, 0);
bool check = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Node Diagram Demo'),
),
body: LayoutBuilder(
builder: (ctx, constraints) {
return MatrixGestureDetector(
shouldRotate: false,
onMatrixUpdate: (m, tm, sm, rm) {
matrix = MatrixGestureDetector.compose(matrix, tm, sm, null);
notifier.value++;
},
child: Container(
width: double.infinity,
height: double.infinity,
alignment: Alignment.topLeft,
color: Color(0xff444444),
child: AnimatedBuilder(
animation: notifier,
builder: (ctx, child) {
return Container(
width: double.infinity,
height: double.infinity,
child: Stack( // a stack in which all nodes are built
children: <Widget>[
buildCenter(),
buildNode()
],
)
);
},
),
),
);
},
),
);
}
// I made this into a method so the transformed matrix can be calculated at runtime
Widget buildNode() {
// create a clone of the main matrix and translate it by the node's position
Matrix4 ma = matrix.clone();
ma.translate(nodePosition.x, nodePosition.y);
return Transform(
transform: ma, // transform the node using the new (translated) matrix
child: MatrixGestureDetector(
shouldRotate: false,
shouldScale: false,
onMatrixUpdate: (m, tm, sm, rm) {
Matrix4 change = tm;
// move the node (in relation to the viewport zoom) when it's being dragged
double sc = MatrixGestureDetector.decomposeToValues(matrix).scale;
nodePosition += change.getTranslation() / sc;
notifier.value++; // refresh view
},
// design a node holding a bool variable ('check')...
child: Container(
decoration: BoxDecoration(
color: Colors.blue
),
child: Container(
width: 200,
height: 100,
child: Column(
children: <Widget>[
Text("Node",
style: TextStyle(
color: Colors.white,
fontSize: 18.0
),
),
Checkbox(
onChanged: (v) =>
{
check = v,
notifier.value++ // refresh view
},
value: check,
)
],
),
)
)
)
);
}
// build two lines to indicate a matrix origin
Widget buildCenter() {
return Transform(
transform: matrix,
child: Stack(
children: <Widget>[
// vertical line
Center(
child: Container(
width: 1,
height: 250,
decoration: BoxDecoration(color: Colors.white)
)
),
// horizontal line
Center(
child: Container(
width: 250,
height: 1,
decoration: BoxDecoration(color: Colors.white)
)
),
],
)
);
}

How do I add a bullet or create a bulleted list in flutter

I have a list and I want to add a bullet to each item (I'm using new Column because I don't want to implement scrolling). How would I create a bulleted list?
I'm thinking maybe an icon but possibly there is a way with the decoration class used in the text style.
To make it as simple as possible, you can use UTF-code.
This's going to be a bullet
String bullet = "\u2022 "
Following widget will create a filled circle shape, So you can call this widget for every item in your column.
class MyBullet extends StatelessWidget{
#override
Widget build(BuildContext context) {
return new Container(
decoration: new BoxDecoration(
color: Colors.black,
shape: BoxShape.circle,
),
);
}
}
Hope this is what you want !
EDIT :
class MyList extends StatelessWidget{
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new ListTile(
leading: new MyBullet(),
title: new Text('My first line'),
),
new ListTile(
leading: new MyBullet(),
title: new Text('My second line'),
)
],
);
}
}
class MyBullet extends StatelessWidget{
#override
Widget build(BuildContext context) {
return new Container(
height: 20.0,
width: 20.0,
decoration: new BoxDecoration(
color: Colors.black,
shape: BoxShape.circle,
),
);
}
}
Simple Answer
If you looking for just a symbol, then use Text('\u2022 Bullet Text')
Detailed Answer
I have created a custom widget for Bullet List of Strings. I am sharing the code so that anyone would find it helpful.
Output:
Code For BulletList Widget
(You can paste this in a separate file like 'bullet_widget.dart' and later import to your screen.)
import 'package:flutter/material.dart';
class BulletList extends StatelessWidget {
final List<String> strings;
BulletList(this.strings);
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.fromLTRB(16, 15, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: strings.map((str) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'\u2022',
style: TextStyle(
fontSize: 16,
height: 1.55,
),
),
SizedBox(
width: 5,
),
Expanded(
child: Container(
child: Text(
str,
textAlign: TextAlign.left,
softWrap: true,
style: TextStyle(
fontSize: 16,
color: Colors.black.withOpacity(0.6),
height: 1.55,
),
),
),
),
],
);
}).toList(),
),
);
}
}
This will Take List of Strings and Output with Bullets. Like This example.
Container(
height: 327,
decoration: BoxDecoration(
color: Constants.agreementBG,
borderRadius: BorderRadius.circular(14)),
child: SingleChildScrollView(
child: BulletList([
'Text 1',
'Text 2',
'Text 3',
]),
),
),
I used the ascii character E.G.
...your widget hierarchy
Text(String.fromCharCode(0x2022)),
...
You can just add an icon.
class MyList extends StatelessWidget{
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new ListTile(
leading: Icon(Icons.fiber_manual_record),
title: new Text('My first line'),
),
new ListTile(
leading: Icon(Icons.fiber_manual_record),
title: new Text('My second line'),
)
],
);
}
}
I might be late to answer this question, but it might be of help to someone who is looking for how to use bullet in a text. It can be done using RichText.
RichText(
text: TextSpan(
text: '• ',
style: TextStyle(color: Colors.lightBlue, fontSize: 18),
children: <TextSpan>[
TextSpan(text: 'Software Developer',style:
GoogleFonts.ptSansNarrow(textStyle: TextStyle(fontSize: 18))),
],
),
)
So, in this case, the color of the bullet can also be changed as you wish!
Here you have the class for bullet text
import 'package:flutter/cupertino.dart';
class BulletText extends StatelessWidget {
late String txt;
BulletText(String t){
txt = t;
}
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('\u2022'),
SizedBox(width: 5),
Expanded(
child: Text(txt)
)
],
);
}
}
You can use CircleAvatar something like below
ListTile(
leading: CircleAvatar(
radius: 6.0,
backgroundColor: Colors.black,
),
title : Text("Timestamp: C0238 - Wheel Speed Mismatch")
),
I got the idea from Tushar Pol. In case you want to display a number on the bullet then you can refer to my code.
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppBullet extends StatelessWidget {
AppBullet({
#required this.width,
#required this.height,
this.order,
}) : assert(width != null),
assert(height != null);
final double width;
final double height;
final int order;
#override
Widget build(BuildContext context) {
return order == null
? _buildBullet(context)
: _buildBulletWithOrder(context);
}
Widget _buildBullet(BuildContext context) {
return new Container(
height: height,
width: width,
decoration: new BoxDecoration(
color: Colors.black,
shape: BoxShape.circle,
),
);
}
Widget _buildBulletWithOrder(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
_buildBullet(context),
Text(
'$order',
style: GoogleFonts.lato(fontSize: 12.0, color: Colors.white),
),
],
);
}
}
Entypo.dot_single from Flutter vector Icons library
import 'package:flutter/material.dart';
import 'package:flutter_vector_icons/flutter_vector_icons.dart';
class MyList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
MyListItem(title: 'First Item'),
MyListItem(title: 'Second Item'),
],
);
}
}
class MyListItem extends StatelessWidget {
final String title;
MyListItem({this.title});
#override
Widget build(BuildContext context) {
return Row(
children: [
Icon(Entypo.dot_single),
Text(title),
],
);
}
}
Screenshot
May be this does not answer this question. I think, this answer can be helpful to other developers.
I use this code to draw a circle of solid color:
CircleAvatar(
radius: 5.0,
backgroundColor: Colors.black,
)
to add extra padding at top, I use Container:
Container(
padding: EdgeInsets.only(top: 3),
child: CircleAvatar(
radius: 5.0,
backgroundColor: Colors.black,
)
)
Also you can use other backgroundColor in CircleAvatar.
Thanks to: #NBM
The solution using flutter widget is to either use the Icon Icon(Icons.circle) or Container or CirleAvatar. There are different solutions. but the one with Icons is easier I think.
You can create a separate class to generate the bullet item that you can further easily modify as per your design. i.e you can use different bullet styles like instead of circle rectangle, triangle, any other icon.
I have just added the option to add the custom padding.
Code:
class MyBulletList extends StatelessWidget {
final String text;
final double vpad;
final double hpad;
MyBulletList({
required this.text,
this.hpad = 24.0,
this.vpad = 8.0,
});
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: hpad, vertical: vpad),
child: Row(
children: [
Icon(
Icons.circle,
size: 6,
color: Colors.grey,
),
SizedBox(
width: 5,
),
Text(
text,
)
],
),
);
}
}
class UL extends StatelessWidget {
final String text;
const UL(this.text, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 14),
child: Icon(
Icons.circle,
size: Theme.of(context).textTheme.bodyText1?.fontSize,
),
),
Text(text, style: Theme.of(context).textTheme.bodyText1),
],
),
);
// return ListTile(
// contentPadding: EdgeInsets.zero,
// minVerticalPadding: 0,
// dense: true,
// visualDensity: VisualDensity(vertical: -4, horizontal: 0),
// leading: Container(
// height: double.infinity,
// child: Icon(
// Icons.circle,
// size: Theme.of(context).textTheme.bodyText1?.fontSize,
// ),
// ),
// title: Text(text, style: Theme.of(context).textTheme.bodyText1),
// );
}
}
You can also pass in padding as an optional parameter to this widget if needed to customize padding