您最多选择25个主题 主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

309 行
11 KiB

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<object> 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<object> 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<ChatList>
{
public AnimationController _controller ;
public Animation<float> _animation;
public readonly GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>.key();
public readonly ScrollController _scrollController = new ScrollController();
private bool _isNextPageLoading;
public List<object> _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<object>();
_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<object> oldList)
{
_oldData = new List<object>(widget.items);
/*var diffResult = calculateListDiff<object>(
oldList,
widget.items,
equalityChecker: (item1, item2) =>
{
if (item1 is Dictionary<string, object> && item2 is Dictionary<string, object>)
{
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<object>(widget.items);
foreach (var item1 in oldList)
{
foreach (var item2 in widget.items)
{
if (item1 is Dictionary<string, object> && item2 is Dictionary<string, object>)
{
var message1 = ((Dictionary<string, object>)item1)["message"]! as ChatComponents.Message;
var message2 = ((Dictionary<string, object>)item2)["message"]! as ChatComponents.Message;
return message1.id == message2.id;
}
return item1 == item2;
}
}
}*/
}
// Hacky solution to reconsider
private void _scrollToBottomIfNeeded(List<object> 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<string, object>)
{
var message = ((Dictionary<string, object>) 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<float> 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<float> 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<ScrollNotification>(
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<Widget>
{
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<Color>(
InheritedChatTheme.of(context).theme.primaryColor
)
)
)
)
)
)
)
)
}
)
);
}
}
}