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