您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
309 行
11 KiB
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
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
}
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|