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

299 行
11 KiB

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<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 readonly GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>.key();
public readonly ScrollController _scrollController = new ScrollController();
public Animation<float> _animation;
public AnimationController _controller;
public bool _isNextPageLoading;
public List<object> _oldData;
public List<string> _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<object>();
_oldData.AddRange(widget.items);
_oldIndex = new List<string>();
}
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)
{
foreach (var item in widget.items)
{
if (item is Dictionary<string, object> )
{
var message1 = ((Dictionary<string, object>)item)["message"] as ChatComponents.Message;
int test = widget.items.IndexOf(item);
if (message1 != null)
{
if (_oldIndex != null && !_oldIndex.Contains(message1.id))
{
_listKey.currentState?.insertItem(0);
}
}
}
}
_scrollToBottomIfNeeded(oldList);
_oldData = new List<object>(widget.items);
List<string> _newIndex = new List<string>();
foreach (var item in widget.items)
{
if (item is Dictionary<string, object> )
{
var message1 = ((Dictionary<string, object>)item)["message"] as ChatComponents.Message;
if (message1 != null)
_newIndex.Add(message1.id);
}
}
_oldIndex = _newIndex;
}
// 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;
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<Widget>
{
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<Color>(
InheritedChatTheme.of(context).theme.primaryColor
)
)
)
)
)
)
)
)
}
)
);
}
}
}