using System; using System.Collections.Generic; using System.Linq; 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.scheduler; using Unity.UIWidgets.widgets; using UnityEngine; using Color = Unity.UIWidgets.ui.Color; 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 readonly GlobalKey _listKey = GlobalKey.key(); public readonly ScrollController _scrollController = new ScrollController(); public Animation _animation; public AnimationController _controller; public bool _isNextPageLoading; public List _oldData; public List _oldIndex; public override void initState() { base.initState(); didUpdateWidget(widget); _controller = new AnimationController(vsync: this); _animation = new CurvedAnimation( curve: Curves.easeOutQuad, parent: _controller ); _isNextPageLoading = false; _oldData = new List(); _oldData.AddRange(widget.items); _oldIndex = new List(); } 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) { foreach (var item in widget.items) { if (item is Dictionary ) { var message1 = ((Dictionary)item)["message"] as ChatComponents.Message; if (message1 != null) { if (oldList != null && oldList.Count != 0 && !oldList.Contains(message1.id)) { _listKey.currentState?.insertItem(oldList.Count); } } } } _scrollToBottomIfNeeded(oldList); _oldData = new List(widget.items); // List _newIndex = new List(); // foreach (var item in widget.items) // { // if (item is Dictionary ) // { // var message1 = ((Dictionary)item)["message"] as ChatComponents.Message; // if (message1 != null) // _newIndex.Add(message1.id); // } // } // // _oldIndex = _newIndex; } // 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; var _onEndReachedThreshold = widget.onEndReachedThreshold == null ? 0.75f : (float) widget.onEndReachedThreshold; if (notification.metrics.pixels >= notification.metrics.maxScrollExtent * _onEndReachedThreshold) { if (widget.items.isEmpty() || _isNextPageLoading) return false; SchedulerBinding.instance.addPostFrameCallback(stamp => { _controller.duration = TimeSpan.Zero; _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) => { return _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 ) ) ) ) ) ) ) ) } ) ); } } }