using System; using System.Collections.Generic; using uiwidgets; using Unity.UIWidgets.animation; using Unity.UIWidgets.async; using Unity.UIWidgets.foundation; using Unity.UIWidgets.material; using Unity.UIWidgets.painting; using Unity.UIWidgets.ui; using Unity.UIWidgets.widgets; namespace UIWidgetsSample { public delegate Widget ItemBuilder(object _object, int? index); public class ChatList : StatefulWidget { /// Used for pagination (infinite scroll) together with [onEndReached]. /// When true, indicates that there are no more pages to load and /// pagination will not be triggered. public readonly bool? isLastPage; /// Item builder public readonly ItemBuilder itemBuilder; /// Items to build public readonly List items; /// Used for pagination (infinite scroll). Called when user scrolls /// to the very end of the list (minus [onEndReachedThreshold]). public readonly OnEndReached onEndReached; /// Used for pagination (infinite scroll) together with [onEndReached]. /// Can be anything from 0 to 1, where 0 is immediate load of the next page /// as soon as scroll starts, and 1 is load of the next page only if scrolled /// to the very end of the list. Default value is 0.75, e.g. start loading /// next page when scrolled through about 3/4 of the available content. public readonly float? onEndReachedThreshold; /// Creates a chat list widget public ChatList( ItemBuilder itemBuilder, List items, OnEndReached onEndReached = null, float? onEndReachedThreshold = null, Key key = null, bool? isLastPage = null ) : base(key) { this.itemBuilder = itemBuilder; this.items = items; this.onEndReached = onEndReached; this.onEndReachedThreshold = onEndReachedThreshold; this.isLastPage = isLastPage; } public override State createState() { return new _ChatListState(); } } /// [ChatList] widget state public class _ChatListState : SingleTickerProviderStateMixin { public AnimationController _controller ; public Animation _animation; public readonly GlobalKey _listKey = GlobalKey.key(); public readonly ScrollController _scrollController = new ScrollController(); private bool _isNextPageLoading; public List _oldData ; public override void initState() { base.initState(); didUpdateWidget(widget); _controller = new AnimationController(vsync: this); _animation = new CurvedAnimation( curve: Curves.easeOutQuad, parent: _controller ); _oldData = new List(); _oldData.AddRange(widget.items); } public override void didUpdateWidget(StatefulWidget oldWidget) { oldWidget = (ChatList) oldWidget; base.didUpdateWidget(oldWidget); _calculateDiffs(((ChatList) oldWidget).items); } public override void dispose() { base.dispose(); _controller.dispose(); _scrollController.dispose(); } private void _calculateDiffs(List oldList) { _oldData = new List(widget.items); /*var diffResult = calculateListDiff( oldList, widget.items, equalityChecker: (item1, item2) => { if (item1 is Dictionary && item2 is Dictionary) { var message1 = item1["message"]! as ChatComponents.Message; var message2 = item2["message"]! as ChatComponents.Message; return message1.id == message2.id; } return item1 == item2; } ); foreach (var update in diffResult.getUpdates(batch: false)) update.when( insert: (pos, count) => { _listKey.currentState?.insertItem(pos); }, remove: (pos, count) => { var item = oldList[pos]; _listKey.currentState?.removeItem( pos, (_, animation) => _buildRemovedMessage(item, animation) ); }, change: (pos, payload) => { }, move: (from, to) => { } ); _scrollToBottomIfNeeded(oldList); _oldData = new List(widget.items); foreach (var item1 in oldList) { foreach (var item2 in widget.items) { if (item1 is Dictionary && item2 is Dictionary) { var message1 = ((Dictionary)item1)["message"]! as ChatComponents.Message; var message2 = ((Dictionary)item2)["message"]! as ChatComponents.Message; return message1.id == message2.id; } return item1 == item2; } } }*/ } // Hacky solution to reconsider private void _scrollToBottomIfNeeded(List oldList) { try { // Take index 1 because there is always a spacer on index 0 var oldItem = oldList[1]; var item = widget.items[1]; // Compare items to fire only on newly added messages if (oldItem != item && item is Dictionary) { var message = ((Dictionary) item)["message"] as ChatComponents.Message; // Run only for sent message if (message.author.id == InheritedUser.of(context).user.id) // Delay to give some time for Flutter to calculate new // size after new message was added Future.delayed(TimeSpan.FromMilliseconds(100), () => { _scrollController.animateTo( 0, TimeSpan.FromMilliseconds(200), Curves.easeInQuad ); return default; }); } } catch (Exception e) { // Do nothing if there are no items } } private Widget _buildRemovedMessage(object item, Animation animation) { return new SizeTransition( axisAlignment: -1, sizeFactor: animation.drive(new CurveTween(Curves.easeInQuad)), child: new FadeTransition( opacity: animation.drive(new CurveTween(Curves.easeInQuad)), child: widget.itemBuilder(item, null) ) ); } private Widget _buildNewMessage(int index, Animation animation) { try { var item = _oldData[index]; return new SizeTransition( axisAlignment: -1, sizeFactor: animation.drive(new CurveTween(Curves.easeOutQuad)), child: widget.itemBuilder(item, index) ); } catch (Exception e) { return new SizedBox(); } } public override Widget build(BuildContext context) { return new NotificationListener( onNotification: notification => { if (widget.onEndReached == null || widget.isLastPage == true) return false; if (notification.metrics.pixels >= notification.metrics.maxScrollExtent * (widget.onEndReachedThreshold ?? 0.75)) { if (widget.items.isEmpty() || _isNextPageLoading) return false; _controller.duration = new TimeSpan(); _controller.forward(); setState(() => { _isNextPageLoading = true; }); widget.onEndReached().whenComplete(() => { _controller.duration = TimeSpan.FromMilliseconds(300); _controller.reverse(); setState(() => { _isNextPageLoading = false; }); }); } return false; }, child: new CustomScrollView( controller: _scrollController, reverse: true, slivers: new List { new SliverPadding( padding: EdgeInsets.only(bottom: 4), sliver: new SliverAnimatedList( initialItemCount: widget.items.Count, key: _listKey, itemBuilder: (_, index, animation) => _buildNewMessage(index, animation) ) ), new SliverPadding( padding: EdgeInsets.only( top: 16 ), sliver: new SliverToBoxAdapter( child: new SizeTransition( axisAlignment: 1, sizeFactor: _animation, child: new Center( child: new Container( alignment: Alignment.center, height: 32, width: 32, child: new SizedBox( height: 16, width: 16, child: new CircularProgressIndicator( backgroundColor: Colors.transparent, strokeWidth: 2, valueColor: new AlwaysStoppedAnimation( InheritedChatTheme.of(context).theme.primaryColor ) ) ) ) ) ) ) ) } ) ); } } }