Flutter scroll screen when pointer reaches edge - flutter

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

How to avoid ListView element overlap in Flutter?

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

Assigning specific items in ListViewBuilder for certain values of index

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) {
}

Animating cubical perspective in flutter

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();
}
}

Flutter: How to manually set index for builder?

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:
[

Flutter: Drag Draggable Stack item inside a Draggable Stack Item

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.