Recently I installed a new app called Chanel Fashion, on it's home page there is a very strange type of scrolling, which you can see it from below GIF, I highly doubt it's a customized scroller of anytype, I think it's a pageview, any hints on how can I implement such a thing in flutter?
P.s this blog tried to make something like that in android but it's different in many ways.
P.s 2 this SO question tried to implement it on IOS.
This is my demo
demo chanel scroll
library in demo: interpolate: ^1.0.2+2
main.dart
import 'package:chanel_scroll_animation/chanel1/chanel1_page.dart';
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(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: Chanel1Page(),
);
}
}
chanel1_page.dart
import 'package:chanel_scroll_animation/chanel1/item.dart';
import 'package:chanel_scroll_animation/chanel1/snapping_list_view.dart';
import 'package:chanel_scroll_animation/models/model.dart';
import 'package:flutter/material.dart';
class Chanel1Page extends StatefulWidget {
#override
_Chanel1PageState createState() => _Chanel1PageState();
}
class _Chanel1PageState extends State<Chanel1Page> {
ScrollController _scrollController;
double y=0;
double maxHeight=0;
#override
void initState() {
// TODO: implement initState
super.initState();
_scrollController=new ScrollController();
_scrollController.addListener(() {
print("_scrollController.offset.toString() "+_scrollController.offset.toString());
setState(() {
y=_scrollController.offset;
});
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final Size size=MediaQuery.of(context).size;
setState(() {
maxHeight=size.height/2;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: maxHeight!=0?SnappingListView(
controller: _scrollController,
snapToInterval: maxHeight,
scrollDirection: Axis.vertical,
children: [
Container(
height: ( models.length +1) * maxHeight,
child: Column(
children: [
for (int i = 0; i < models.length; i++)
Item(item: models[i],index: i,y: y,)
],
),
)
],
):Container(),
),
);
}
}
item.dart
import 'package:chanel_scroll_animation/models/model.dart';
import 'package:flutter/material.dart';
import 'package:interpolate/interpolate.dart';
const double MIN_HEIGHT = 128;
class Item extends StatefulWidget {
final Model item;
final int index;
final double y;
Item({this.item,this.index,this.y});
#override
_ItemState createState() => _ItemState();
}
class _ItemState extends State<Item> {
Interpolate ipHeight;
double maxHeight=0;
#override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final Size size=MediaQuery.of(context).size;
maxHeight=size.height/2;
initInterpolate();
});
}
initInterpolate()
{
ipHeight=Interpolate(
inputRange: [(widget.index-1)*maxHeight,widget.index*maxHeight],
outputRange: [MIN_HEIGHT,maxHeight],
extrapolate: Extrapolate.clamp,
);
}
#override
Widget build(BuildContext context) {
final Size size=MediaQuery.of(context).size;
double height=ipHeight!=null? ipHeight.eval(widget.y):MIN_HEIGHT;
print("height "+height.toString());
return Container(
height: height,
child: Stack(
children: [
Positioned.fill(
child: Image.asset(
widget.item.picture,
fit: BoxFit.cover,
),
),
Positioned(
bottom:40,
left: 30,
right: 30,
child: Column(
children: [
Text(
widget.item.subtitle,
style: TextStyle(fontSize: 16, color: Colors.white),
),
SizedBox(
height: 10,
),
Text(
widget.item.title.toUpperCase(),
style: TextStyle(fontSize: 24, color: Colors.white),
textAlign: TextAlign.center,
),
],
),
)
],
),
);
}
}
snapping_list_view.dart
import "package:flutter/widgets.dart";
import "dart:math";
class SnappingListView extends StatefulWidget {
final Axis scrollDirection;
final ScrollController controller;
final IndexedWidgetBuilder itemBuilder;
final List<Widget> children;
final int itemCount;
final double snapToInterval;
final ValueChanged<int> onItemChanged;
final EdgeInsets padding;
SnappingListView(
{this.scrollDirection,
this.controller,
#required this.children,
#required this.snapToInterval,
this.onItemChanged,
this.padding = const EdgeInsets.all(0.0)})
: assert(snapToInterval > 0),
itemCount = null,
itemBuilder = null;
SnappingListView.builder(
{this.scrollDirection,
this.controller,
#required this.itemBuilder,
this.itemCount,
#required this.snapToInterval,
this.onItemChanged,
this.padding = const EdgeInsets.all(0.0)})
: assert(snapToInterval > 0),
children = null;
#override
createState() => _SnappingListViewState();
}
class _SnappingListViewState extends State<SnappingListView> {
int _lastItem = 0;
#override
Widget build(BuildContext context) {
final startPadding = widget.scrollDirection == Axis.horizontal
? widget.padding.left
: widget.padding.top;
final scrollPhysics = SnappingListScrollPhysics(
mainAxisStartPadding: startPadding, itemExtent: widget.snapToInterval);
final listView = widget.children != null
? ListView(
scrollDirection: widget.scrollDirection,
controller: widget.controller,
children: widget.children,
physics: scrollPhysics,
padding: widget.padding)
: ListView.builder(
scrollDirection: widget.scrollDirection,
controller: widget.controller,
itemBuilder: widget.itemBuilder,
itemCount: widget.itemCount,
physics: scrollPhysics,
padding: widget.padding);
return NotificationListener<ScrollNotification>(
child: listView,
onNotification: (notif) {
if (notif.depth == 0 &&
widget.onItemChanged != null &&
notif is ScrollUpdateNotification) {
final currItem =
(notif.metrics.pixels - startPadding) ~/ widget.snapToInterval;
if (currItem != _lastItem) {
_lastItem = currItem;
widget.onItemChanged(currItem);
}
}
return false;
});
}
}
class SnappingListScrollPhysics extends ScrollPhysics {
final double mainAxisStartPadding;
final double itemExtent;
const SnappingListScrollPhysics(
{ScrollPhysics parent,
this.mainAxisStartPadding = 0.0,
#required this.itemExtent})
: super(parent: parent);
#override
SnappingListScrollPhysics applyTo(ScrollPhysics ancestor) {
return SnappingListScrollPhysics(
parent: buildParent(ancestor),
mainAxisStartPadding: mainAxisStartPadding,
itemExtent: itemExtent);
}
double _getItem(ScrollPosition position) {
return (position.pixels - mainAxisStartPadding) / itemExtent;
}
double _getPixels(ScrollPosition position, double item) {
return min(item * itemExtent, position.maxScrollExtent);
}
double _getTargetPixels(
ScrollPosition position, Tolerance tolerance, double velocity) {
double item = _getItem(position);
if (velocity < -tolerance.velocity)
item -= 0.5;
else if (velocity > tolerance.velocity) item += 0.5;
return _getPixels(position, item.roundToDouble());
}
#override
Simulation createBallisticSimulation(
ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
return super.createBallisticSimulation(position, velocity);
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
if (target != position.pixels)
return ScrollSpringSimulation(spring, position.pixels, target, velocity,
tolerance: tolerance);
return null;
}
#override
bool get allowImplicitScrolling => false;
}
Use a with a SingleChildScrollView with a column as it's child. In order to make the picture small when it's a header, use a FittedBox. Wrap the FittedBox with a SizedBox to control the size of the inside widgets. Use a scroll notifier to cause updates when it is scrolling and track how far the user scrolls. Divide the scroll amount by the max height that you want in order to know the current widget that needs resizing. Resize that widget by finding the remainder and dividing it by the max height and multiplying by the difference of the min and max size then add min size. This will ensure a smooth transition. Then make any widgets above in the column max sized and below minimum sized to make sure lag doesn't ruin the scroller.
Use AnimatedOpacity to allow the description of the header to fade in and out or make a customized animation of how you think it should look.
The following code should work though customize the text widgets with what style you'd like. Enter the custom TitleWithImage(contains widget and two strings) items to be in the list, the maxHeight and minHeight into the custom widget. It likely isn't completely optimized and probably has lots of bugs although I fixed some:
import 'package:flutter/material.dart';
class CoolListView extends StatefulWidget {
final List<TitleWithImage> items;
final double minHeight;
final double maxHeight;
const CoolListView({Key key, this.items, this.minHeight, this.maxHeight}) : super(key: key);
#override
_CoolListViewState createState() => _CoolListViewState();
}
class _CoolListViewState extends State<CoolListView> {
List<Widget> widgets=[];
ScrollController _scrollController = new ScrollController();
#override
Widget build(BuildContext context) {
if(widgets.length == 0){
for(int i = 0; i<widget.items.length; i++){
if(i==0){
widgets.add(ListItem(height: widget.maxHeight, item: widget.items[0],descriptionTransparent: false));
}
else{
widgets.add(
ListItem(height: widget.minHeight, item: widget.items[i], descriptionTransparent: true,)
);
}
}
}
return new NotificationListener<ScrollUpdateNotification>(
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: widgets,
)
),
onNotification: (t) {
if (t!= null && t is ScrollUpdateNotification) {
int currentWidget = (_scrollController.position.pixels/widget.maxHeight).ceil();
currentWidget = currentWidget==-1?0:currentWidget;
setState(() {
if(currentWidget != widgets.length-1){//makes higher index min
for(int i = currentWidget+1; i<=widgets.length-1; i++){
print(i);
widgets[i] = ListItem(height: widget.minHeight, item: widget.items[i],descriptionTransparent: true,);
}
}
if(currentWidget!=0){
widgets[currentWidget] = ListItem(
height: _scrollController.position.pixels%widget.maxHeight/widget.maxHeight*(widget.maxHeight-widget.minHeight)+widget.minHeight,
item: widget.items[currentWidget],
descriptionTransparent: true,
);
for(int i = currentWidget-1; i>=0; i--){
widgets[i] = ListItem(height: widget.maxHeight,
item: widget.items[i],
descriptionTransparent: false,
);
}
}
else{
widgets[0] = ListItem(
height: widget.maxHeight,
item: widget.items[0],
descriptionTransparent: false
);
}
});
}
},
);
}
}
class TitleWithImage
{
final Widget image;
final String title;
final String description;
TitleWithImage(this.image, this.title, this.description);
}
class ListItem extends StatelessWidget {
final double height;
final TitleWithImage item;
final bool descriptionTransparent;
const ListItem({Key key, this.height, this.item, this.descriptionTransparent}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child:Stack(
children: [
SizedBox(
height: height,
width: MediaQuery.of(context).size.width,
child: FittedBox(
fit: BoxFit.none,
child:Align(
alignment: Alignment.center,
child: item.image
)
),
),
SizedBox(
height: height,
width: MediaQuery.of(context).size.width,
child: Column(
children: [
Spacer(),
Text(item.title,),
AnimatedOpacity(
child: Text(
item.description,
style: TextStyle(
color: Colors.black
),
),
opacity: descriptionTransparent? 0.0 : 1.0,
duration: Duration(milliseconds: 500),
),
],
),
),
],
),
);
}
}
Edit here is my main.dart:
import 'package:cool_list_view/CoolListView.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Collapsing List Demo')),
body: CoolListView(
items: [
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
Colors.orange,
Colors.blue,
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(
Container(
height: 1000,
width:1000,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end:
Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
colors: [
const Color(0xffee0000),
const Color(0xffeeee00)
], // red to yellow
tileMode: TileMode.repeated, // repeats the gradient over the canvas
),
),
),
'title',
'description',
),
new TitleWithImage(Container(height: 1000,width:1000,color: Colors.blue), 'title', 'description'),
new TitleWithImage(Container(height: 1000,width:1000, color: Colors.orange), 'title', 'description'),
],
minHeight: 50,
maxHeight: 300,
),
),
);
}
}
You can do that using ScrollController value to change the size of the widget or it's children's, sorry I can't write the code because it's time consuming and requires some computation but watch this video:https://www.youtube.com/watch?v=Cn6VCTaHB-k&t=558s it will gave you the basic idea and help you keep going.
try to use Sliver.
This is an example of what I mean:
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
backgroundColor: Color(0xFF0084C9),
leading: IconButton(
icon: Icon(
Icons.blur_on,
color: Colors.white70,
),
onPressed: () {
Scaffold.of(context).openDrawer();
},
),
expandedHeight: bannerHigh,
floating: true,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text("Your title",
style: TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.w600)),
background: Image.network(
'image url',
fit: BoxFit.cover,
),
),
),
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
],
),
),
],
),
);
Related
I was able to show the pictures as in the video by taking advantage of Johannes Milke's video that I left the link of. But that's not all I want. I need a structure that looks like these images but shows the total number of users. I leave the image of exactly what I want and my related codes.
What I want to achieve; Creating a structure where I can write the total number of users
There are a few packages on pub dev but not what I wanted. Thank you
Video:Source Video
image i want to make:
My stacked widget:
import 'package:flutter/material.dart';
class StackedWidgets extends StatelessWidget {
final List<Widget> items;
final TextDirection direction;
final double size;
final double xShift;
const StackedWidgets({
Key? key,
required this.items,
this.direction = TextDirection.ltr,
this.size = 100,
this.xShift = 20,
}) : super(key: key);
#override
Widget build(BuildContext context) {
final allItems = items
.asMap()
.map((index, item) {
final left = size - xShift;
final value = Container(
width: size,
height: size,
child: item,
margin: EdgeInsets.only(left: left * index),
);
return MapEntry(index, value);
})
.values
.toList();
return Stack(
children: direction == TextDirection.ltr
? allItems.reversed.toList()
: allItems,
);
}
}
Usage my stacked widget:
Widget buildStackedImages({
TextDirection direction = TextDirection.ltr,
}) {
final double size = 100;
final double xShift = 20;
final urlImages = [
'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=633&q=80',
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80',
'https://images.unsplash.com/photo-1616766098956-c81f12114571?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=634&q=80',
];
final items = urlImages.map((urlImage) => buildImage(urlImage)).toList();
return StackedWidgets(
direction: direction,
items: items,
size: size,
xShift: xShift,
);
}
Widget buildImage(String urlImage) {
final double borderSize = 5;
return ClipOval(
child: Container(
padding: EdgeInsets.all(borderSize),
color: Colors.white,
child: ClipOval(
child: Image.network(
urlImage,
fit: BoxFit.cover,
),
),
),
);
}
Add a label also to this class
import 'package:flutter/material.dart';
class StackedWidgets extends StatelessWidget {
final List<Widget> items;
final TextDirection direction;
final double size;
final double xShift;
final String lable;
const StackedWidgets({
Key? key,
required this.items,
this.direction = TextDirection.ltr,
this.size = 100,
this.xShift = 20,
this.label = '',
}) : super(key: key);
#override
Widget build(BuildContext context) {
final allItems = items
.asMap()
.map((index, item) {
final left = size - xShift;
final value = Container(
width: size,
height: size,
child: item,
margin: EdgeInsets.only(left: left * index),
);
return MapEntry(index, value);
})
.values
.toList();
return Row(
children: [
Stack(
children: direction == TextDirection.ltr
? allItems.reversed.toList()
: allItems,
),
Text(label),
]
);
}
}
In items pass the number widget and label too
return StackedWidgets(
direction: direction,
items: [...items, Container(
width: 25,//you can also add padding if required
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.orange,
),
child: Text('+22'))],
size: size,
xShift: xShift,
label: "users are here",
);
I will prefer this way,
Run on dartPad
class InTEST extends StatefulWidget {
const InTEST({Key? key}) : super(key: key);
#override
State<InTEST> createState() => _InTESTState();
}
class _InTESTState extends State<InTEST> {
int maxRenderAvatar = 5;
int numberOfActiveUser = 33;
double size = 100;
double borderSize = 5;
Widget buildStackedImages({
TextDirection direction = TextDirection.ltr,
}) {
List<String> urlImages = List.filled(
numberOfActiveUser, //based on your list
'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=633&q=80');
List<Widget> items = [];
final renderItemCount = numberOfActiveUser > maxRenderAvatar
? maxRenderAvatar
: urlImages.length;
for (int i = 0; i < renderItemCount; i++) {
items.add(
Positioned(
left: (i * size * .8),
child: buildImage(
urlImages[i],
),
),
);
}
// add counter if urlImages.length > maxRenderAvatar
if (numberOfActiveUser > maxRenderAvatar) {
items.add(
Positioned(
left: maxRenderAvatar * size * .8,
child: Container(
width: size,
height: size,
padding: EdgeInsets.all(borderSize),
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 4),
color: Colors.amber,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
"+ ${urlImages.length - maxRenderAvatar}",
style: TextStyle(
fontSize: 23,
),
),
),
),
);
}
return SizedBox(
height: size + (borderSize * 2), //10 for borderSize
width: MediaQuery.of(context).size.width,
child: Stack(
children: items,
),
);
}
Widget buildImage(String urlImage) {
return ClipOval(
child: Container(
padding: EdgeInsets.all(borderSize),
color: Colors.white,
child: ClipOval(
child: Image.network(
urlImage,
width: size,
height: size,
fit: BoxFit.cover,
),
),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Slider(
value: numberOfActiveUser.toDouble(),
min: 0,
max: 55,
onChanged: (v) {
numberOfActiveUser = v.toInt();
setState(() {});
}),
Slider(
value: maxRenderAvatar.toDouble(),
min: 0,
max: 42,
onChanged: (v) {
maxRenderAvatar = v.toInt();
setState(() {});
}),
buildStackedImages(),
],
),
);
}
}
I want to be able to drag an widget to any place on the screen. The first time the widget is dragged and place in a section of the screen. There is no issue. On the second attempt to drag of the widget container, the coordinates to not match and the widget jumps to the top of the screen. I want to have multiple widgets that can be dragged.
Any suggestions welcomed.
import 'package:flutter/gestures.dart';
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(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
// visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'HomePage'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
double ytop = 50.0;
double xleft = 50.0;
GlobalKey blueKey = GlobalKey();
String position = '';
#override
void initState() {
// _getPositions();
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_getPositions();
});
}
#override
Widget build(BuildContext context) {
// _getPositions();
print('$xleft <---> $ytop');
return Scaffold(
// appBar: AppBar(
// title: Text(widget.title),
// ),
body: Container(
child: Stack(
children: <Widget>[
Positioned(
top: 30,
left: 10,
child: Text(
'Filler Text',
),
),
Positioned(
top: ytop,
left: xleft,
child: GestureDetector(
dragStartBehavior: DragStartBehavior.down,
onVerticalDragStart: _onVerticalDragStartHandler,
onVerticalDragUpdate: _onDragUpdateHandler,
onVerticalDragEnd: (details) => {print('On END $details')},
onTap: () {
print('HELLO');
_getPositions();
},
child: Container(
key: blueKey,
width: 100,
height: 100,
decoration: myBlueBoxDecoration,
),
// ),
),
),
],
),
),
// This trailing comma makes auto-formatting nicer for build methods.
);
}
/// Track starting point of a vertical gesture
void _onVerticalDragStartHandler(DragStartDetails details) {
setState(() {
ytop = details.globalPosition.dy;
xleft = details.globalPosition.dx;
// final dd = details.globalPosition.dy;
print('Vertical Start Local Vertical Left: $xleft Top: $ytop');
});
}
void _onDragUpdateHandler(DragUpdateDetails details) {
setState(
() {
ytop = details.localPosition.dy;
xleft = details.localPosition.dx;
xleft = xleft < 0 ? 0 : xleft;
ytop = ytop < 10 ? 10 : ytop;
// xleft = xleft > 259 ? 259 : xleft;
// ytop = ytop > 618 ? 618 : ytop;
print('Drage Update:--> Left: $xleft Top: $ytop\n');
print('');
},
);
}
_getPositions() {
final RenderBox renderBoxRed = blueKey.currentContext.findRenderObject();
final positionRed = renderBoxRed.localToGlobal(Offset.zero);
position = 'POSITION of Red: $positionRed';
print(position);
}
}
BoxDecoration myBlueBoxDecoration = BoxDecoration(
color: const Color(0xff7c94b6),
border: Border.all(color: Colors.green, width: 1),
borderRadius: BorderRadius.circular(12),
);
I fixed the problem by moving to LongPressDraggable. It would be good to reduce the time that makes the widget able to drag. See the code below. Any suggestions on the original implementation welcomed.
Positioned(
top: ytop,
left: xleft,
child: LongPressDraggable(
childWhenDragging: Container(
// key: blueKey,
width: w,
height: h,
decoration: myBlueBoxDecoration,
),
onDragStarted: genericCall,
onDragEnd: (d) {
setState(() {
xleft = d.offset.dx;
ytop = d.offset.dy;
print(d.offset.dx);
});
},
feedback: Container(
// key: blueKey,
width: 100,
height: 100,
decoration: myGreenBoxDecoration,
),
child: Container(
key: blueKey,
width: 100,
height: 100,
decoration: myBlueBoxDecoration,
),
),
),
I guess the old man is thinking to slow. Here is fix I needed.
Positioned(
top: ytop,
left: xleft,
child: Draggable(
childWhenDragging: Container(
// key: blueKey,
width: w,
height: h,
decoration: myBlueBoxDecoration,
),
onDragStarted: genericCall,
onDragEnd: (d) {
setState(() {
xleft = d.offset.dx;
ytop = d.offset.dy;
print(d.offset.dx);
});
},
feedback: Container(
// key: blueKey,
width: 100,
height: 100,
decoration: myGreenBoxDecoration,
),
child: Container(
key: blueKey,
width: 100,
height: 100,
decoration: myBlueBoxDecoration,
),
),
),
I just succeed flutter-flare for my flutter widgets
but I could not find a example to use it for background (Container)
is this possible?
Use official example code https://github.com/2d-inc/Flare-Flutter/tree/stable/example/teddy/lib
with a little modification, you can use Stack and Positioned and change attribute value like top
code snippet
return Scaffold(
backgroundColor: Color.fromRGBO(93, 142, 155, 1.0),
body: Container(
child: Stack(
children: <Widget>[
Positioned(
top: 50,
left:0,
right: 0,
child: Container(
height: 200,
padding:
const EdgeInsets.only(left: 30.0, right: 30.0),
child: FlareActor(
"assets/Teddy.flr",
shouldClip: false,
alignment: Alignment.bottomCenter,
fit: BoxFit.contain,
controller: _teddyController,
)),),
Positioned(
child: SingleChildScrollView(
working demo
full code
import 'package:flutter/material.dart';
import 'package:flare_flutter/flare_actor.dart';
import 'package:flutter/rendering.dart';
import 'dart:async';
import 'dart:math';
import 'dart:ui';
import 'package:flare_flutter/flare.dart';
import 'package:flare_dart/math/mat2d.dart';
import 'package:flare_dart/math/vec2d.dart';
import 'package:flare_flutter/flare_controls.dart';
// Adapted these helpful functions from:
// https://github.com/flutter/flutter/blob/master/packages/flutter/test/material/text_field_test.dart
// Returns first render editable
RenderEditable findRenderEditable(RenderObject root) {
RenderEditable renderEditable;
void recursiveFinder(RenderObject child) {
if (child is RenderEditable) {
renderEditable = child;
return;
}
child.visitChildren(recursiveFinder);
}
root.visitChildren(recursiveFinder);
return renderEditable;
}
List<TextSelectionPoint> globalize(
Iterable<TextSelectionPoint> points, RenderBox box) {
return points.map<TextSelectionPoint>((TextSelectionPoint point) {
return TextSelectionPoint(
box.localToGlobal(point.point),
point.direction,
);
}).toList();
}
Offset getCaretPosition(RenderBox box) {
final RenderEditable renderEditable = findRenderEditable(box);
if (!renderEditable.hasFocus) {
return null;
}
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(renderEditable.selection),
renderEditable,
);
return endpoints[0].point + const Offset(0.0, -2.0);
}
class SigninButton extends StatelessWidget {
final Widget child;
final Gradient gradient;
final double width;
final double height;
final Function onPressed;
const SigninButton({
Key key,
#required this.child,
this.gradient,
this.width = double.infinity,
this.height = 50.0,
this.onPressed,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
width: width,
height: 50.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25.0),
gradient: LinearGradient(
colors: <Color>[
Color.fromRGBO(160, 92, 147, 1.0),
Color.fromRGBO(115, 82, 135, 1.0)
],
)),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
child: Center(
child: child,
)),
),
);
}
}
class TeddyController extends FlareControls {
// Store a reference to our face control node (the "ctrl_look" node in Flare)
ActorNode _faceControl;
// Storage for our matrix to get global Flutter coordinates into Flare world coordinates.
Mat2D _globalToFlareWorld = Mat2D();
// Caret in Flutter global coordinates.
Vec2D _caretGlobal = Vec2D();
// Caret in Flare world coordinates.
Vec2D _caretWorld = Vec2D();
// Store the origin in both world and local transform spaces.
Vec2D _faceOrigin = Vec2D();
Vec2D _faceOriginLocal = Vec2D();
bool _hasFocus = false;
// Project gaze forward by this many pixels.
static const double _projectGaze = 60.0;
String _password;
#override
bool advance(FlutterActorArtboard artboard, double elapsed) {
super.advance(artboard, elapsed);
Vec2D targetTranslation;
if (_hasFocus) {
// Get caret in Flare world space.
Vec2D.transformMat2D(_caretWorld, _caretGlobal, _globalToFlareWorld);
// To make it more interesting, we'll also add a sinusoidal vertical offset.
_caretWorld[1] +=
sin(new DateTime.now().millisecondsSinceEpoch / 300.0) * 70.0;
// Compute direction vector.
Vec2D toCaret = Vec2D.subtract(Vec2D(), _caretWorld, _faceOrigin);
Vec2D.normalize(toCaret, toCaret);
Vec2D.scale(toCaret, toCaret, _projectGaze);
// Compute the transform that gets us in face "ctrl_face" space.
Mat2D toFaceTransform = Mat2D();
if (Mat2D.invert(toFaceTransform, _faceControl.parent.worldTransform)) {
// Put toCaret in local space, note we're using a direction vector
// not a translation so transform without translation
Vec2D.transformMat2(toCaret, toCaret, toFaceTransform);
// Our final "ctrl_face" position is the original face translation plus this direction vector
targetTranslation = Vec2D.add(Vec2D(), toCaret, _faceOriginLocal);
}
} else {
targetTranslation = Vec2D.clone(_faceOriginLocal);
}
// We could just set _faceControl.translation to targetTranslation, but we want to animate it smoothly to this target
// so we interpolate towards it by a factor of elapsed time in order to maintain speed regardless of frame rate.
Vec2D diff =
Vec2D.subtract(Vec2D(), targetTranslation, _faceControl.translation);
Vec2D frameTranslation = Vec2D.add(Vec2D(), _faceControl.translation,
Vec2D.scale(diff, diff, min(1.0, elapsed * 5.0)));
_faceControl.translation = frameTranslation;
return true;
}
// Fetch references for the `ctrl_face` node and store a copy of its original translation.
#override
void initialize(FlutterActorArtboard artboard) {
super.initialize(artboard);
_faceControl = artboard.getNode("ctrl_face");
if (_faceControl != null) {
_faceControl.getWorldTranslation(_faceOrigin);
Vec2D.copy(_faceOriginLocal, _faceControl.translation);
}
play("idle");
}
onCompleted(String name) {
play("idle");
}
// Called by [FlareActor] when the view transform changes.
// Updates the matrix that transforms Global-Flutter-coordinates into Flare-World-coordinates.
#override
void setViewTransform(Mat2D viewTransform) {
Mat2D.invert(_globalToFlareWorld, viewTransform);
}
// Transform the [Offset] into a [Vec2D].
// If no caret is provided, lower the [_hasFocus] flag.
void lookAt(Offset caret) {
if (caret == null) {
_hasFocus = false;
return;
}
_caretGlobal[0] = caret.dx;
_caretGlobal[1] = caret.dy;
_hasFocus = true;
}
void setPassword(String value) {
_password = value;
}
bool _isCoveringEyes = false;
coverEyes(cover) {
if (_isCoveringEyes == cover) {
return;
}
_isCoveringEyes = cover;
if (cover) {
play("hands_up");
} else {
play("hands_down");
}
}
void submitPassword() {
if (_password == "bears") {
play("success");
} else {
play("fail");
}
}
}
typedef void CaretMoved(Offset globalCaretPosition);
typedef void TextChanged(String text);
// Helper widget to track caret position.
class TrackingTextInput extends StatefulWidget {
TrackingTextInput(
{Key key, this.onCaretMoved, this.onTextChanged, this.hint, this.label, this.isObscured = false})
: super(key: key);
final CaretMoved onCaretMoved;
final TextChanged onTextChanged;
final String hint;
final String label;
final bool isObscured;
#override
_TrackingTextInputState createState() => _TrackingTextInputState();
}
class _TrackingTextInputState extends State<TrackingTextInput> {
final GlobalKey _fieldKey = GlobalKey();
final TextEditingController _textController = TextEditingController();
Timer _debounceTimer;
#override
initState() {
_textController.addListener(() {
// We debounce the listener as sometimes the caret position is updated after the listener
// this assures us we get an accurate caret position.
if (_debounceTimer?.isActive ?? false) _debounceTimer.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 100), () {
if (_fieldKey.currentContext != null) {
// Find the render editable in the field.
final RenderObject fieldBox =
_fieldKey.currentContext.findRenderObject();
Offset caretPosition = getCaretPosition(fieldBox);
if (widget.onCaretMoved != null) {
widget.onCaretMoved(caretPosition);
}
}
});
if (widget.onTextChanged != null) {
widget.onTextChanged(_textController.text);
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: TextFormField(
decoration: InputDecoration(
hintText: widget.hint,
labelText: widget.label,
),
key: _fieldKey,
controller: _textController,
obscureText: widget.isObscured,
validator: (value) {}),
);
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
TeddyController _teddyController;
#override
initState() {
_teddyController = TeddyController();
super.initState();
}
#override
Widget build(BuildContext context) {
EdgeInsets devicePadding = MediaQuery.of(context).padding;
return Scaffold(
backgroundColor: Color.fromRGBO(93, 142, 155, 1.0),
body: Container(
child: Stack(
children: <Widget>[
Positioned(
top: 50,
left:0,
right: 0,
child: Container(
height: 200,
padding:
const EdgeInsets.only(left: 30.0, right: 30.0),
child: FlareActor(
"assets/Teddy.flr",
shouldClip: false,
alignment: Alignment.bottomCenter,
fit: BoxFit.contain,
controller: _teddyController,
)),),
Positioned(
child: SingleChildScrollView(
padding: EdgeInsets.only(
left: 20.0, right: 20.0, top: devicePadding.top + 150.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.all(Radius.circular(25.0))),
child: Padding(
padding: const EdgeInsets.all(30.0),
child: Form(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
TrackingTextInput(
label: "Email",
hint: "What's your email address?",
onCaretMoved: (Offset caret) {
_teddyController.lookAt(caret);
}),
TrackingTextInput(
label: "Password",
hint: "Try 'bears'...",
isObscured: true,
onCaretMoved: (Offset caret) {
_teddyController.coverEyes(caret != null);
_teddyController.lookAt(null);
},
onTextChanged: (String value) {
_teddyController.setPassword(value);
},
),
SigninButton(
child: Text("Sign In",
style: TextStyle(
fontFamily: "RobotoMedium",
fontSize: 16,
color: Colors.white)),
onPressed: () {
_teddyController.submitPassword();
})
],
)),
)),
])),
),
],
)),
);
}
}
I'm trying to create a drag and drop game. I would like to make sure that the Draggable widgets don't get out of the screen when they are dragged around.
I couldn't find an answer to this specific question. Someone asked something similar about constraining draggable area Constraining Draggable area but the answer doesn't actually make use of Draggable.
To start with I tried to implement a limit on the left-hand side.
I tried to use a Listener with onPointerMove. I've associated this event with a limitBoundaries method to detect when the Draggable exits from the left side of the screen. This part is working as it does print in the console the Offset value when the Draggable is going out (position.dx < 0). I also associated a setState to this method to set the position of the draggable to Offset(0.0, position.dy) but this doesn't work.
Could anybody help me with this?
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Draggable Test',
home: GamePlay(),
);
}
}
class GamePlay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
Row(
children: [
Container(
width: 360,
height: 400,
decoration: BoxDecoration(
color: Colors.lightGreen,
border: Border.all(
color: Colors.green,
width: 2.0,
),
),
),
Container(
width: 190,
height: 400,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.purple,
width: 2.0,
),
),
),
],
),
DragObject(
key: GlobalKey(),
initPos: Offset(365, 0.0),
id: 'Item 1',
itmColor: Colors.orange),
DragObject(
key: GlobalKey(),
initPos: Offset(450, 0.0),
id: 'Item 2',
itmColor: Colors.pink,
),
],
),
);
}
}
class DragObject extends StatefulWidget {
final String id;
final Offset initPos;
final Color itmColor;
DragObject({Key key, this.id, this.initPos, this.itmColor}) : super(key: key);
#override
_DragObjectState createState() => _DragObjectState();
}
class _DragObjectState extends State<DragObject> {
GlobalKey _key;
Offset position;
Offset posOffset = Offset(0.0, 0.0);
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
_key = widget.key;
position = widget.initPos;
super.initState();
}
void _getRenderOffsets() {
final RenderBox renderBoxWidget = _key.currentContext.findRenderObject();
final offset = renderBoxWidget.localToGlobal(Offset.zero);
posOffset = offset - position;
}
void _afterLayout(_) {
_getRenderOffsets();
}
void limitBoundaries(PointerEvent details) {
if (details.position.dx < 0) {
print(details.position);
setState(() {
position = Offset(0.0, position.dy);
});
}
}
#override
Widget build(BuildContext context) {
return Positioned(
left: position.dx,
top: position.dy,
child: Listener(
onPointerMove: limitBoundaries,
child: Draggable(
child: Container(
width: 80,
height: 80,
color: widget.itmColor,
),
feedback: Container(
width: 82,
height: 82,
color: widget.itmColor,
),
childWhenDragging: Container(),
onDragEnd: (drag) {
setState(() {
position = drag.offset - posOffset;
});
},
),
),
);
}
}
Try this. I tweaked this from: Constraining Draggable area .
ValueNotifier<List<double>> posValueListener = ValueNotifier([0.0, 0.0]);
ValueChanged<List<double>> posValueChanged;
double _horizontalPos = 0.0;
double _verticalPos = 0.0;
#override
void initState() {
super.initState();
posValueListener.addListener(() {
if (posValueChanged != null) {
posValueChanged(posValueListener.value);
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
_buildDraggable(),
]));
}
_buildDraggable() {
return SafeArea(
child: Container(
margin: EdgeInsets.only(bottom: 100),
color: Colors.green,
child: Builder(
builder: (context) {
final handle = GestureDetector(
onPanUpdate: (details) {
_verticalPos =
(_verticalPos + details.delta.dy / (context.size.height))
.clamp(.0, 1.0);
_horizontalPos =
(_horizontalPos + details.delta.dx / (context.size.width))
.clamp(.0, 1.0);
posValueListener.value = [_horizontalPos, _verticalPos];
},
child: Container(
child: Container(
margin: EdgeInsets.all(12),
width: 110.0,
height: 170.0,
child: Container(
color: Colors.black87,
),
decoration: BoxDecoration(color: Colors.black54),
),
));
return ValueListenableBuilder<List<double>>(
valueListenable: posValueListener,
builder:
(BuildContext context, List<double> value, Widget child) {
return Align(
alignment: Alignment(value[0] * 2 - 1, value[1] * 2 - 1),
child: handle,
);
},
);
},
),
),
);
}
I've found a workaround for this issue. It's not exactly the output I was looking for but I thought this could be useful to somebody else.
Instead of trying to control the drag object during dragging, I just let it go outside of my screen and I placed it back to its original position in case it goes outside of the screen.
Just a quick note if someone tries my code, I forgot to mention that I'm trying to develop a game for the web. The output on a mobile device might be a little bit odd!
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Draggable Test',
home: GamePlay(),
);
}
}
class GamePlay extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
Row(
children: [
Container(
width: 360,
height: 400,
decoration: BoxDecoration(
color: Colors.lightGreen,
border: Border.all(
color: Colors.green,
width: 2.0,
),
),
),
Container(
width: 190,
height: 400,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.purple,
width: 2.0,
),
),
),
],
),
DragObject(
key: GlobalKey(),
initPos: Offset(365, 0.0),
id: 'Item 1',
itmColor: Colors.orange),
DragObject(
key: GlobalKey(),
initPos: Offset(450, 0.0),
id: 'Item 2',
itmColor: Colors.pink,
),
],
),
);
}
}
class DragObject extends StatefulWidget {
final String id;
final Offset initPos;
final Color itmColor;
DragObject({Key key, this.id, this.initPos, this.itmColor}) : super(key: key);
#override
_DragObjectState createState() => _DragObjectState();
}
class _DragObjectState extends State<DragObject> {
GlobalKey _key;
Offset position;
Offset posOffset = Offset(0.0, 0.0);
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
_key = widget.key;
position = widget.initPos;
super.initState();
}
void _getRenderOffsets() {
final RenderBox renderBoxWidget = _key.currentContext.findRenderObject();
final offset = renderBoxWidget.localToGlobal(Offset.zero);
posOffset = offset - position;
}
void _afterLayout(_) {
_getRenderOffsets();
}
#override
Widget build(BuildContext context) {
return Positioned(
left: position.dx,
top: position.dy,
child: Listener(
child: Draggable(
child: Container(
width: 80,
height: 80,
color: widget.itmColor,
),
feedback: Container(
width: 82,
height: 82,
color: widget.itmColor,
),
childWhenDragging: Container(),
onDragEnd: (drag) {
setState(() {
if (drag.offset.dx > 0) {
position = drag.offset - posOffset;
} else {
position = widget.initPos;
}
});
},
),
),
);
}
}
I'm still interested if someone can find a proper solution to the initial issue :-)
you could use the property onDragEnd: of the widget Draggable and before setting the new position compare it with the height or width of your device using MediaQuery and update only if you didn't pass the limits of your screen, else set the new position to the initial one.
Example bellow :
Positioned(
left: position.dx,
top: position.dy,
child: Draggable(
maxSimultaneousDrags: 1,
childWhenDragging:
Opacity(opacity: .2, child: rangeEvent(context)),
feedback: rangeEvent(context),
axis: Axis.vertical,
affinity: Axis.vertical,
onDragEnd: (details) => updatePosition(details.offset),
child: Transform.scale(
scale: scale,
child: rangeEvent(context),
),
),
)
In the method updatePosition, you verify the new position before updating:
void updatePosition(Offset newPosition) => setState(() {
if (newPosition.dy > 10 &&
newPosition.dy < MediaQuery.of(context).size.height * 0.9) {
position = newPosition;
} else {
position = const Offset(0, 0);// initial possition
}
});
How to implement a table with fixed vertical and horizontal headers in Flutter? For example, the horizontal header should only scroll horizontally and the vertical header vertically. Both headers should always be visible. How to set-up the layout?
Example
I already tried to use a Row with two nested Columns to set-up the overall 2x2 layout: (0, 0) empty; (0, 1) vertical header; (1,0) horizontal header, and (1, 1) data. To visualize the actual data I used GridViews for the two headers and the data. Moreover, I want to use the scroll controller to achieve the scroll behavior.
Row
Column: (0) empty, (1) GridView
Column: (0) GridView (1) GridView
Another solution I thought about was to have nested GridViews instead of the Row and the two Columns.
This code shows the first column:
Widget build(BuildContext context) {
return Container(
width: double.maxFinite,
child: Row(
children: <Widget>[
Column(
children: <Widget>[
Text("empty"), // (0,0)
Container( // (0, 1)
child: Flexible(
child: GridView.count(
controller: _vScrollController1,
crossAxisCount: 1,
childAspectRatio: 3.0,
children: List.generate(
widget.data.length,
(index) => Text("my cell")
),
),
),
),
],
),
],
),
);
However, it produces the following error message:
════════ Exception caught by rendering library
The method '>' was called on null.
Receiver: null.
Tried calling: >(1e-10).
User-created ancestor of the error-causing widget was Container.
═════════════════════════════════
Probably, some width/height properties are not properly set? How would you achieve this table layout? Thanks for your help!
There are actually existing Flutter plugins for this. Consider using one.
Here is an example taken from horizontal_data_table:
import 'package:flutter/material.dart';
import 'package:horizontal_data_table/horizontal_data_table.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
HDTRefreshController _hdtRefreshController = HDTRefreshController();
static const int sortName = 0;
static const int sortStatus = 1;
bool isAscending = true;
int sortType = sortName;
#override
void initState() {
user.initData(100);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: _getBodyWidget(),
);
}
Widget _getBodyWidget() {
return Container(
child: HorizontalDataTable(
leftHandSideColumnWidth: 100,
rightHandSideColumnWidth: 600,
isFixedHeader: true,
headerWidgets: _getTitleWidget(),
leftSideItemBuilder: _generateFirstColumnRow,
rightSideItemBuilder: _generateRightHandSideColumnRow,
itemCount: user.userInfo.length,
rowSeparatorWidget: const Divider(
color: Colors.black54,
height: 1.0,
thickness: 0.0,
),
leftHandSideColBackgroundColor: Color(0xFFFFFFFF),
rightHandSideColBackgroundColor: Color(0xFFFFFFFF),
verticalScrollbarStyle: const ScrollbarStyle(
thumbColor: Colors.yellow,
isAlwaysShown: true,
thickness: 4.0,
radius: Radius.circular(5.0),
),
horizontalScrollbarStyle: const ScrollbarStyle(
thumbColor: Colors.red,
isAlwaysShown: true,
thickness: 4.0,
radius: Radius.circular(5.0),
),
enablePullToRefresh: true,
refreshIndicator: const WaterDropHeader(),
refreshIndicatorHeight: 60,
onRefresh: () async {
//Do sth
await Future.delayed(const Duration(milliseconds: 500));
_hdtRefreshController.refreshCompleted();
},
htdRefreshController: _hdtRefreshController,
),
height: MediaQuery.of(context).size.height,
);
}
List<Widget> _getTitleWidget() {
return [
TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
child: _getTitleItemWidget(
'Name' + (sortType == sortName ? (isAscending ? '↓' : '↑') : ''),
100),
onPressed: () {
sortType = sortName;
isAscending = !isAscending;
user.sortName(isAscending);
setState(() {});
},
),
TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
child: _getTitleItemWidget(
'Status' +
(sortType == sortStatus ? (isAscending ? '↓' : '↑') : ''),
100),
onPressed: () {
sortType = sortStatus;
isAscending = !isAscending;
user.sortStatus(isAscending);
setState(() {});
},
),
_getTitleItemWidget('Phone', 200),
_getTitleItemWidget('Register', 100),
_getTitleItemWidget('Termination', 200),
];
}
Widget _getTitleItemWidget(String label, double width) {
return Container(
child: Text(label, style: TextStyle(fontWeight: FontWeight.bold)),
width: width,
height: 56,
padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
);
}
Widget _generateFirstColumnRow(BuildContext context, int index) {
return Container(
child: Text(user.userInfo[index].name),
width: 100,
height: 52,
padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
);
}
Widget _generateRightHandSideColumnRow(BuildContext context, int index) {
return Row(
children: <Widget>[
Container(
child: Row(
children: <Widget>[
Icon(
user.userInfo[index].status
? Icons.notifications_off
: Icons.notifications_active,
color:
user.userInfo[index].status ? Colors.red : Colors.green),
Text(user.userInfo[index].status ? 'Disabled' : 'Active')
],
),
width: 100,
height: 52,
padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
),
Container(
child: Text(user.userInfo[index].phone),
width: 200,
height: 52,
padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
),
Container(
child: Text(user.userInfo[index].registerDate),
width: 100,
height: 52,
padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
),
Container(
child: Text(user.userInfo[index].terminationDate),
width: 200,
height: 52,
padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
),
],
);
}
}
User user = User();
class User {
List<UserInfo> userInfo = [];
void initData(int size) {
for (int i = 0; i < size; i++) {
userInfo.add(UserInfo(
"User_$i", i % 3 == 0, '+001 9999 9999', '2019-01-01', 'N/A'));
}
}
///
/// Single sort, sort Name's id
void sortName(bool isAscending) {
userInfo.sort((a, b) {
int aId = int.tryParse(a.name.replaceFirst('User_', '')) ?? 0;
int bId = int.tryParse(b.name.replaceFirst('User_', '')) ?? 0;
return (aId - bId) * (isAscending ? 1 : -1);
});
}
///
/// sort with Status and Name as the 2nd Sort
void sortStatus(bool isAscending) {
userInfo.sort((a, b) {
if (a.status == b.status) {
int aId = int.tryParse(a.name.replaceFirst('User_', '')) ?? 0;
int bId = int.tryParse(b.name.replaceFirst('User_', '')) ?? 0;
return (aId - bId);
} else if (a.status) {
return isAscending ? 1 : -1;
} else {
return isAscending ? -1 : 1;
}
});
}
}
class UserInfo {
String name;
bool status;
String phone;
String registerDate;
String terminationDate;
UserInfo(this.name, this.status, this.phone, this.registerDate,
this.terminationDate);
}
Actual output:
You can check other existing plugins.
table_sticky_headers
linked_scroll_controller
Also, here are some answered SO questions related to your:
Fixed column and row header for DataTable on Flutter Dart
How to create a horizontally scrolling table with fixed column in Flutter?
For other reference, you can check the blog "Flutter: Creating a two direction scrolling table with fixed head and column"