I have a GridView that contains draggable items. When an item is dragged to the top/bottom of the screen I want to scroll the GridView in that direction.
Currently I wrapped each draggable item in a Listener like so:
Listener(
child: _wrap(widget.children[i], i),
onPointerMove: (PointerMoveEvent event) {
if (event.position.dy >= MediaQuery.of(context).size.height - 100) {
// 120 is height of your draggable.
widget.scrollController.animateTo(
widget.scrollController.offset + 120,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 200));
}if (event.position.dy <= kToolbarHeight + MediaQueryData.fromWindow(window).padding.top + 100) {
// 120 is height of your draggable.
widget.scrollController.animateTo(
widget.scrollController.offset - 120,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 200));
}
}
)
It works, but the scroll is not smooth at all and looks kind of laggy.
I would need it to work on web too.
Does anyone have a better solution for this?
Here's how I'm solving it. Using TickerProviderStateMixin, you can obtain a Ticker that invokes a callback once per frame, where you can adjust the scroll offset by a small amount for a smooth scroll. I used a Stack to add dummy DragTargets to the top and bottom of the list area which control the tickers. I used two per edge, to allow different scrolling speeds. You could probably use a Listener to interpolate the speed using the cursor position if you want finer-grained control.
https://www.dartpad.dev/acb83fdbbbbb0fd765cd5afa414a8942
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Stack(
children: [
ListView.separated(
controller: controller,
itemCount: 50,
itemBuilder: (context, index) {
return buildLongPressDraggable(index);
},
separatorBuilder: (context, index) {
return Divider();
},
),
Positioned(
top: 0, left: 0, right: 0, height: 25, child: buildEdgeScroller(-10)),
Positioned(
top: 25, left: 0, right: 0, height: 25, child: buildEdgeScroller(-5)),
Positioned(
bottom: 25, left: 0, right: 0, height: 25, child: buildEdgeScroller(5)),
Positioned(
bottom: 0, left: 0, right: 0, height: 25, child: buildEdgeScroller(10)),
],
),
);
}
Widget buildEdgeScroller(double offsetPerFrame) {
return DragTarget<int>(
builder: (context, candidateData, rejectedData) => Container(),
onWillAccept: (data) {
scrollTicker = this.createTicker((elapsed) {
if (!controller.hasClients) {
return;
}
final position = controller.position;
if ((offsetPerFrame < 0 && position.pixels <= position.minScrollExtent) ||
(offsetPerFrame > 0 && position.pixels >= position.maxScrollExtent)) {
scrollTicker.stop();
scrollTicker.dispose();
scrollTicker = null;
} else {
controller.jumpTo(controller.offset + offsetPerFrame);
}
});
scrollTicker.start();
return false;
},
onLeave: (data) {
scrollTicker?.stop();
scrollTicker?.dispose();
scrollTicker = null;
},
);
}
Related
I have a chatbot screen that I built with a listview and there is a strange behavior that I don't understand the origin of.
From time to time, some of the elements overlap each other, then if I scroll or change screen and come back they are back to normal. I can't seem to find the problem...
import 'package:async_redux/async_redux.dart';
import 'package:flutter/material.dart';
import '../../../app_theme.dart';
import '../../../data/models/models.dart';
import '../../../helpers.dart';
import 'action_message.dart';
import 'conversation_bar.dart';
import 'image_message.dart';
import 'text_message.dart';
import 'typing_indicator.dart';
class ConversationFlow extends StatelessWidget {
// Max number of messages to show
final int maxMessages = 50;
Widget _buildConversationMessage(BuildContext context, message, next) {
// Detects which message type to build as child
Widget child = Container();
if (message.text.isEmpty) {
return child;
}
if (message.attachment == null) {
child = TextMessage(message: message);
} else if (message.attachment.type == 'image') {
child = ImageMessage(message: message);
}
// General structure of each message
var topSpacing = App.size(15, context);
// Between same speaker messages
if (next != null && next.speaker == message.speaker) {
topSpacing = App.size(1.5, context);
}
// Last one from conversation
if (next == null) {
topSpacing = App.size(10, context);
}
return Row(
mainAxisAlignment: (message.speaker == 'coach') ? MainAxisAlignment.start : MainAxisAlignment.end,
children: [
Container(
constraints: BoxConstraints(
maxWidth: App.isLandscape(context)
? MediaQuery.of(context).size.width * 0.42
: MediaQuery.of(context).size.width * 0.7,
),
padding: EdgeInsets.symmetric(
vertical: App.size(8, context),
horizontal: App.size(10, context),
),
margin: EdgeInsets.only(
left: (message.speaker == 'coach') ? App.margin(context) : 0,
top: topSpacing,
right: (message.speaker == 'coach') ? 0 : App.margin(context),
),
decoration: BoxDecoration(
color: (message.speaker == 'coach') ? AppTheme.colorGrey[100] : AppTheme.colorAccent,
borderRadius: BorderRadius.circular(App.radius()),
),
child: child,
),
(message.speaker == 'coach' && message.action != null) ? ActionMessage(action: message.action) : Container(),
],
);
}
#override
Widget build(BuildContext context) {
return StoreConnector(
converter: (Store<AppState> store) => store.state.bot,
builder: (BuildContext context, Bot bot) {
int itemsCount = bot.conversation.isNotEmpty
? bot.conversation.length > maxMessages
? maxMessages
: bot.conversation.length + 2
: 1;
return Align(
alignment: Alignment.bottomCenter,
child: ListView.builder(
shrinkWrap: true,
reverse: true,
scrollDirection: Axis.vertical,
physics: BouncingScrollPhysics(),
itemCount: itemsCount,
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return ConversationBar();
}
// Typing indicator Row
if (index == 1) {
return bot.typing ? TypingIndicator() : Container();
}
// Correct Typing index shift
index = index - 2;
// Reduces opacity as conversation fades away like memories
double opacity = 1.0;
(index > maxMessages - 10) ? opacity = 1.0 - ((index - maxMessages + 10) * 0.1) : 0.0;
if (opacity < 0.0) {
opacity = 0.0;
} else if (opacity > 1.0) {
opacity = 1.0;
}
if (index < maxMessages) {
// Conversation bubbles
return Opacity(
opacity: opacity,
child: _buildConversationMessage(
context,
bot.conversation[index],
bot.conversation.asMap().containsKey(index + 1) ? bot.conversation[index + 1] : null,
),
);
}
return Container();
},
),
);
},
);
}
}
Any idea of what could cause this?
apply itemExtent: value into Listview.builder
Use readmore package if you have different size and wrap your text into ReadMoreText
ReadMoreText(
'your message',
trimLines: 2,
trimMode: TrimMode.Line,
trimCollapsedText: 'Read more',
trimExpandedText: 'Read less',
),
use trimLines for line of text you want to show
I have implemented a standard ListViewBuilder. My objective is to set some properties according to the index, for example if the index is equal to (item count - 1), i.e end of the list, i want Text to be displayed, i haven't been able to do that. Any help is appreciated. Here is my Code:
Container(
height: 500,
width: double.infinity,
child: ListView.builder(
itemCount: 6,
padding: EdgeInsets.symmetric(vertical: 0.0),
itemBuilder: (context, index) {
if (index <= 5) {
return ListCard().buildListCard();
}
return Container(
child: Text("FIN"),
height: 50,
width: double.infinity);
},
),
)
Output Image
Let's going your example, if you want to show the Text widget on the list end you can follow below lines;
Container(
height: 500,
width: double.infinity,
child: ListView.builder(
itemCount: dynamicList.length,
padding: EdgeInsets.symmetric(vertical: 0.0),
itemBuilder: (context, index) {
if (index <= 5) {
return ListCard().buildListCard();
}
if (index == dynamicList.length - 1) { // <-- this line your want logic
return Text('Your text widget');
}
return Container(
child: Text("FIN"),
height: 50,
width: double.infinity);
},
),
)
You have incorrectly logic in your code. Remember that index is starting from 0. So, the following code:
// From index 0 to index 5 (6 item)
if (index <= 5) {
}
will always executed because it is included all your items which is 6 in length.
So, change it to:
// From index 0 to index 4 excluding the 5.
if (index < 5) {
}
I'm trying to implement the cubical swipe as outlined by Marcin Szalek in his Flutter talk for the different pages in my app. I was able to implement it for the left side, as he had shown. But if I want to implement it for the right hand side of the main page, or if I want to add a Gesture detector for another action, how can I stack these GDs on top of one another, if at all possible? Or should I restrict the GDs to certain sections of the screen? Thanks in advance.
GestureDetector(
onHorizontalDragStart: _onDragStart,
onHorizontalDragUpdate: _onDragUpdate,
onHorizontalDragEnd: _onDragEnd,
behavior: HitTestBehavior.translucent,
onTap: toggle,
child: AnimatedBuilder(
animation: animationController,
builder: (context, _) {
return Material(
color: Colors.black,
child: Stack(
children: <Widget>[
Transform.translate(
offset: Offset(maxSlide * (animationController.value - 1), 0),
child: Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(math.pi / 2 * (1 - animationController.value)),
alignment: Alignment.centerRight,
child: SettingsDrawer(),
),
),
Transform.translate(
offset: Offset(maxSlide * animationController.value, 0),
child: Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(-math.pi * animationController.value / 2),
alignment: Alignment.centerLeft,
child: widget.child,
),
),
],
),
);
},
),
),
]);
}
void _onDragStart(DragStartDetails details) {
bool isDragOpenFromLeft = animationController.isDismissed;
bool isDragCloseFromRight = animationController.isCompleted;
_canBeDragged = isDragOpenFromLeft || isDragCloseFromRight;
}
void _onDragUpdate(DragUpdateDetails details) {
if (_canBeDragged) {
double delta = details.primaryDelta / maxSlide;
animationController.value += delta;
}
}
void _onDragEnd(DragEndDetails details) {
//I have no idea what it means, copied from Drawer
double _kMinFlingVelocity = 365.0;
if (animationController.isDismissed || animationController.isCompleted) {
return;
}
if (details.velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) {
double visualVelocity = details.velocity.pixelsPerSecond.dx /
MediaQuery.of(context).size.width;
animationController.fling(velocity: visualVelocity);
} else if (animationController.value < 0.5) {
animationController.reverse();
} else {
animationController.forward();
}
}
I'm currently stuck with setting the index here. I've tried various ways to set the index but the index's value cant be changed. This method is comes from the package :
https://pub.dev/packages/flutter_tindercard
Since I'm building cards, I might need to go back in index to properly sync with the back button which brings back the card. But the builder's index just keeps adding even if I perform a decrement inside the builder. This is the code I have of the builder:
Widget _cardBuild() {
return TinderSwapCard(
orientation: AmassOrientation.BOTTOM,
totalNum: articleList.length != null ? articleList.length + backCount : 0,
stackNum: 3,
swipeEdge: 1.0,
animDuration: 50,
maxWidth: MediaQuery.of(context).size.width * 0.9,
maxHeight: 410.1,
minWidth: MediaQuery.of(context).size.width * 0.8,
minHeight: 410,
cardBuilder: (context, index) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
child: Conditional.single(
context: context,
conditionBuilder: (BuildContext context) => pendingBack > 0,
widgetBuilder: (BuildContext context) {
return _generateCards(cardIndex);
},
fallbackBuilder: (BuildContext context) {
return _generateCards(index);
},
),
);
},
cardController: controller = CardController(),
swipeUpdateCallback: (DragUpdateDetails details, Alignment align) {
/// Get swiping card's alignment
if (align.x < 0) {
//Card is LEFT swiping
} else if (align.x > 0) {
//Card is RIGHT swiping
}
},
swipeCompleteCallback: (CardSwipeOrientation orientation, int index) {
if (index == articleList.length) {
setState(() {
isOutOfCards = true;
});
} else {
setState(() {
cardIndex++;
});
}
},
);
}
A sample screenshot of the app:
[
I have a Draggable on a DragTarget as part of a Stack. Inside is another Stack with Draggables, again on DragTargets and so on... (Stack over Stack over Stack etc.).
The Draggable is a Positioned with a Listener telling where to be placed.
homeView.dart
body: Stack(children: [
DraggableWidget(parentKey, Offset(0, 0)),
]),
draggableWidget.dart
class DraggableWidget extends StatefulWidget {
final Key itemKey;
final Offset itemPosition;
DraggableWidget(this.itemKey, this.itemPosition);
#override
_DraggableWidgetState createState() => _DraggableWidgetState();
}
class _DraggableWidgetState extends State<DraggableWidget> {
Offset tempDelta = Offset(0, 0);
Window<List<Key>> item;
List<DraggableWidget> childList = [];
Map<Key, Window<List>> structureMap;
initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
structureMap = Provider.of<Data>(context).structureMap;
if (structureMap[widget.itemKey] != null) {
structureMap[widget.itemKey].childKeys.forEach(
(k) => childList.add(
DraggableWidget(k, item.position),
),
);
} else {
structureMap[widget.itemKey] = Window<List<Key>>(
title: 'App',
key: widget.itemKey,
size: Size(MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height),
position: Offset(0, 0),
color: Colors.blue,
childKeys: []);
}
item = Provider.of<Data>(context).structureMap[widget.itemKey];
return Positioned(
top: item.position.dx,
left: item.position.dy,
child: DragTarget(
builder:
(buildContext, List<Window<List<Key>>> candidateData, rejectData) {
return Listener(
onPointerDown: (PointerDownEvent event) {},
onPointerUp: (PointerUpEvent event) {
setState(() {
item.position = Offset(item.position.dx + tempDelta.dx,
item.position.dy + tempDelta.dy);
tempDelta = Offset(0, 0);
});
},
onPointerMove: (PointerMoveEvent event) {
tempDelta = Offset((event.delta.dy + tempDelta.dx),
(event.delta.dx + tempDelta.dy));
},
child: Draggable(
childWhenDragging: Container(),
feedback: Container(
color: item.color,
height: item.size.height,
width: item.size.width,
),
child: Column(children: [
Text(item.title),
Container(
color: item.color,
height: item.size.height,
width: item.size.width,
child: ItemStackBuilder(widget.itemKey, item.position),
),
]),
data: item),
);
},
),
);
}
}
itemStackBuilder.dart
class ItemStackBuilder extends StatelessWidget {
final Key itemKey;
final Offset itemPosition;
ItemStackBuilder(this.itemKey, this.itemPosition);
#override
Widget build(BuildContext context) {
Map<Key, Window<List<Key>>> structureMap =
Provider.of<Data>(context).structureMap;
if (structureMap[itemKey] == null) {
structureMap[itemKey] = Window(size: Size(20, 20), childKeys: []);
}
return Stack(overflow: Overflow.visible, children: [
...stackItems(context),
Container(
height: structureMap[itemKey].size.height,
width: structureMap[itemKey].size.width,
color: Colors.transparent),
]);
}
List<Widget> stackItems(BuildContext context) {
List<Key> childKeyList =
Provider.of<Data>(context).structureMap[itemKey].childKeys;
var stackItemDraggable;
List<Widget> stackItemsList = [];
if (childKeyList == null || childKeyList.length < 1) {
stackItemsList = [Container()];
} else {
for (int i = 0; i < childKeyList.length; i++) {
stackItemDraggable = DraggableWidget(childKeyList[i], itemPosition);
stackItemsList.add(stackItemDraggable);
}
}
return stackItemsList;
}
}
When I want to move the Draggable item on top, the underlying Stack moves.
I tried it with a Listener widget and was able to detect all RenderBoxes inside the Stack.
But how can I select the specific Draggable and/or disable all the other layers? Is it a better idea to forget about Draggables and do it all with Positioned and GestureDetector?
Ok, it was my mistake not of the framework:
on itemStackBuilder.dart I used an additional Container to size the Stack. I was not able to recognise, because color was transparent:
Container(
height: structureMap[itemKey].size.height,
width: structureMap[itemKey].size.width,
color: Colors.transparent),
]);
}
After deleting this part, all works fine for now.