Flutter チャット インターフェイス - チャット リストをプルダウンして、より多くの履歴メッセージをロードします
これまでに、フラッターチャットインターフェースのリッチテキスト表示内容、カスタム絵文字キーボードの実現、カメラやフォトアルバムなどの操作パネルを展開するプラス記号[➕]、メッセージバブル表示などを実現し、フレキシブルを実現しました。ここで、実装されたチャット インターフェイスのスライド リストを記録し、プルダウンしてさらに履歴メッセージをロードします。
チャットインターフェースのリストはListViewを使用します。
1. レンダリング
二、ListView
ListView は、すべてのサブコンポーネントを一方向に直線的に配置できるスクロール コンポーネントであり、リスト項目 (必要な場合にのみ作成される) の遅延読み込みもサポートします。
ListView({
...
//可滚动widget公共参数
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController? controller,
bool? primary,
ScrollPhysics? physics,
EdgeInsetsGeometry? padding,
//ListView各个构造函数的共同参数
double? itemExtent,
Widget? prototypeItem, //列表项原型,后面解释
bool shrinkWrap = false,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double? cacheExtent, // 预渲染区域长度
//子widget列表
List<Widget> children = const <Widget>[],
})
フォローアップ チャット インターフェイスでは、リバース、物理、コントローラーなどが使用されます。
3. チャットインターフェースメッセージリスト
チャット インターフェイスのリストのスクロールには ListView.builder を使用します。
シュリンクラップを設定する必要があります
ShrinWrap: この属性は、サブコンポーネントの合計の長さに応じて ListView の長さを設定するかどうかを示します。デフォルト値は false です。デフォルトでは、ListView はスクロール方向に可能な限り多くのスペースを占有します。ListView がボーダーレス (スクロール方向) コンテナー内にある場合は、shrinkWrap が true である必要があります。
reverse: reverse を true に設定すると、コンテンツが上下逆に表示されます。
3.1. チャットリスト
// 聊天列表
Widget buildScrollConfiguration(
ChatContainerModel model, BuildContext context) {
return ListView.builder(
physics: AlwaysScrollableScrollPhysics(),
key: chatListViewKey,
shrinkWrap: true,
addRepaintBoundaries: false,
controller: scrollController,
padding:
const EdgeInsets.only(left: 0.0, right: 0.0, bottom: 0.0, top: 0.0),
itemCount: messageList.length + 1,
reverse: true,
clipBehavior: Clip.none,
itemBuilder: (BuildContext context, int index) {
if (index == messageList.length) {
if (historyMessageList != null && historyMessageList!.isEmpty) {
return const ChatNoMoreIndicator();
}
return const ChatLoadingIndicator();
} else {
CommonChatMessage chatMessage = messageList[index];
return ChatCellElem(
childElem: MessageElemHelper.layoutCellElem(chatMessage),
chatMessage: chatMessage,
onSendFailedIndicatorPressed: (CommonChatMessage chatMessage) {
onSendFailedIndicatorPressed(context, chatMessage);
},
onBubbleTapPressed: (CommonChatMessage chatMessage) {
onBubbleTapPressed(context, chatMessage);
},
onBubbleDoubleTapPressed: (CommonChatMessage chatMessage) {
onBubbleDoubleTapPressed(context, chatMessage);
},
onBubbleLongPressed: (CommonChatMessage chatMessage,
LongPressStartDetails details,
ChatBubbleFrame? chatBubbleFrame) {
onBubbleLongPressed(
context, chatMessage, details, chatBubbleFrame);
},
);
}
},
);
}
3.2. チャット インターフェイスに項目が少ない場合、iOS のスクロール範囲が小さく、元に戻すことができません
この問題に対して、ここではネストされた ListView に CustomScrollView が使用されています。CustomScrollView の主な機能は、複数の Sliver を組み合わせるために共通の Scrollable と Viewport を提供することです。
具体的な実装コード
// 嵌套的customScrollView
Widget buildCustomScrollView(ChatContainerModel model, BuildContext context) {
return LayoutBuilder(
builder: (BuildContext lbContext, BoxConstraints constraints) {
double layoutHeight = constraints.biggest.height;
return CustomScrollView(
slivers: <Widget>[
SliverPadding(
padding: EdgeInsets.all(0.0),
sliver: SliverToBoxAdapter(
child: Container(
alignment: Alignment.topCenter,
height: layoutHeight,
child: buildScrollConfiguration(model, context),
),
),
),
],
);
});
}
3.3. ListView の逆が true の場合、項目数が少なすぎると下から上に表示され、上部に大きな空白が表示されます。
項目が少なすぎる場合は、下から上に表示されます。上部に大きな空白領域があるのは、その下のインターフェイスと絵文字キーボードと入力ボックスが列コントロールを使用しているためです。そのため、Expanded を使用して埋めることができ、Expanded コンポーネントはサブコンポーネントに利用可能なスペースを強制的に埋めさせ、Expanded は残りの空白スペースを強制的に埋めます。
Widget buildListContainer(ChatContainerModel model, BuildContext context) {
return Expanded(
child: Container(
decoration: BoxDecoration(
color: ColorUtil.hexColor(0xf7f7f7),
),
clipBehavior: Clip.hardEdge,
alignment: Alignment.topCenter,
child: isNeedDismissPanelGesture
? GestureDetector(
onPanDown: handlerGestureTapDown,
child: buildCustomScrollView(model, context),
)
: buildCustomScrollView(model, context),
),
);
}
// インターフェイスと次の絵文字キーボード、入力ボックスなどは Column コントロールを使用します
return Container(
key: chatContainerKey,
width: double.infinity,
height: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
buildChatStatisticsBar(model),
ChatAnnouncementBar(
announcementNotice: model.announcementNotice,
onAnnouncementPressed: () {
onAnnouncementPressed(model.announcementNotice);
},
),
buildListContainer(model, context),
ChatNavigatorBar(
onNavigatorItemPressed: (CommNavigatorEntry navigatorEntry) {
onNavigatorItemPressed(navigatorEntry, model);
},
navigatorEntries: model.navigatorEntries,
),
ChatInputBar(
chatInputBarController: chatInputBarController,
moreOptionEntries: model.moreOptionEntries,
showPostEnterButton: checkShowPostAndStatistics(model),
),
],
),
);
3.4. リストスライド弾性効果
ScrollPhysicsを継承するChatScrollPhysicsをカスタマイズする必要がある
滑り荷重の弾性効果と上向き滑りをシールドする弾性効果を実現。(BouncingScrollPhysics には上下に弾性効果があります)
class ChatScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that bounce back from the edge.
const ChatScrollPhysics({
ScrollPhysics? parent}) : super(parent: parent);
ChatScrollPhysics applyTo(ScrollPhysics? ancestor) {
return ChatScrollPhysics(parent: buildParent(ancestor));
}
/// The multiple applied to overscroll to make it appear that scrolling past
/// the edge of the scrollable contents is harder than scrolling the list.
/// This is done by reducing the ratio of the scroll effect output vs the
/// scroll gesture input.
///
/// This factor starts at 0.52 and progressively becomes harder to overscroll
/// as more of the area past the edge is dragged in (represented by an increasing
/// `overscrollFraction` which starts at 0 when there is no overscroll).
double frictionFactor(double overscrollFraction) =>
0.52 * math.pow(1 - overscrollFraction, 2);
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
print("applyPhysicsToUserOffset position:${
position}, offset:${
offset}");
assert(offset != 0.0);
assert(position.minScrollExtent <= position.maxScrollExtent);
if (!position.outOfRange) return offset;
final double overscrollPastStart =
math.max(position.minScrollExtent - position.pixels, 0.0);
final double overscrollPastEnd =
math.max(position.pixels - position.maxScrollExtent, 0.0);
final double overscrollPast =
math.max(overscrollPastStart, overscrollPastEnd);
final bool easing = (overscrollPastStart > 0.0 && offset < 0.0) ||
(overscrollPastEnd > 0.0 && offset > 0.0);
final double friction = easing
// Apply less resistance when easing the overscroll vs tensioning.
? frictionFactor(
(overscrollPast - offset.abs()) / position.viewportDimension)
: frictionFactor(overscrollPast / position.viewportDimension);
final double direction = offset.sign;
double applyPhysicsToUserOffset =
direction * _applyFriction(overscrollPast, offset.abs(), friction);
print("applyPhysicsToUserOffset:${
applyPhysicsToUserOffset}");
return applyPhysicsToUserOffset;
}
static double _applyFriction(
double extentOutside, double absDelta, double gamma) {
assert(absDelta > 0);
double total = 0.0;
if (extentOutside > 0) {
final double deltaToLimit = extentOutside / gamma;
if (absDelta < deltaToLimit) return absDelta * gamma;
total += extentOutside;
absDelta -= deltaToLimit;
}
return total + absDelta;
}
double applyBoundaryConditions(ScrollMetrics position, double value) {
print("applyBoundaryConditions:${
position},value:${
value}");
return 0.0;
}
Simulation? createBallisticSimulation(
ScrollMetrics position, double velocity) {
final Tolerance tolerance = this.tolerance;
print(
"createBallisticSimulation:${
position},velocity:${
velocity},tolerance.velocity:${
tolerance.velocity}");
if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
return BouncingScrollSimulation(
spring: spring,
position: position.pixels,
velocity: velocity,
leadingExtent: position.minScrollExtent,
trailingExtent: position.maxScrollExtent,
tolerance: tolerance,
);
}
return null;
}
// The ballistic simulation here decelerates more slowly than the one for
// ClampingScrollPhysics so we require a more deliberate input gesture
// to trigger a fling.
double get minFlingVelocity {
double aMinFlingVelocity = kMinFlingVelocity * 2.0;
print("minFlingVelocity:${
aMinFlingVelocity}");
return aMinFlingVelocity;
}
// Methodology:
// 1- Use https://github.com/flutter/platform_tests/tree/master/scroll_overlay to test with
// Flutter and platform scroll views superimposed.
// 3- If the scrollables stopped overlapping at any moment, adjust the desired
// output value of this function at that input speed.
// 4- Feed new input/output set into a power curve fitter. Change function
// and repeat from 2.
// 5- Repeat from 2 with medium and slow flings.
/// Momentum build-up function that mimics iOS's scroll speed increase with repeated flings.
///
/// The velocity of the last fling is not an important factor. Existing speed
/// and (related) time since last fling are factors for the velocity transfer
/// calculations.
double carriedMomentum(double existingVelocity) {
double aCarriedMomentum = existingVelocity.sign *
math.min(0.000816 * math.pow(existingVelocity.abs(), 1.967).toDouble(),
40000.0);
print(
"carriedMomentum:${
aCarriedMomentum},existingVelocity:${
existingVelocity}");
return aCarriedMomentum;
}
// Eyeballed from observation to counter the effect of an unintended scroll
// from the natural motion of lifting the finger after a scroll.
double get dragStartDistanceMotionThreshold {
print("dragStartDistanceMotionThreshold");
return 3.5;
}
}
3.5. ListView のスライディングリップルの削除 - ScrollBehavior の定義
ScrollBehaviorを実装する
class ChatScrollBehavior extends ScrollBehavior {
final bool showLeading;
final bool showTrailing;
ChatScrollBehavior({
this.showLeading: false, //不显示头部水波纹
this.showTrailing: false, //不显示尾部水波纹
});
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
return child;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return GlowingOverscrollIndicator(
child: child,
showLeading: showLeading,
showTrailing: showTrailing,
axisDirection: axisDirection,
color: Theme.of(context).accentColor,
);
}
return null;
}
}
4. プルダウンしてさらにメッセージをロードし、メッセージをロードしないようにします
さらにメッセージを読み込むためにプルダウンするときにリストビューに ChatLoadingIndicator を追加します
リストの最後の項目で判断します。リスト
itemCount: messageList.length + 1,
if (index == messageList.length) {
if (historyMessageList != null && historyMessageList!.isEmpty) {
return const ChatNoMoreIndicator();
}
return const ChatLoadingIndicator();
}
さらにメッセージ インジケーター コードをロードする
// 刷新的动画
class ChatLoadingIndicator extends StatelessWidget {
const ChatLoadingIndicator({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Container(
height: 60.0,
width: double.infinity,
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CupertinoActivityIndicator(
color: ColorUtil.hexColor(0x333333),
),
const SizedBox(
width: 10,
),
buildIndicatorTitle(context),
],
),
);
}
Widget buildIndicatorTitle(BuildContext context) {
return Text(
"加载中",
textAlign: TextAlign.left,
maxLines: 1000,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
fontStyle: FontStyle.normal,
color: ColorUtil.hexColor(0x555555),
decoration: TextDecoration.none,
),
);
}
}
データがなくなった場合には、この時点ではメッセージを表示しないようにする必要がある。
// 没有更多消息时候
class ChatNoMoreIndicator extends StatelessWidget {
const ChatNoMoreIndicator({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Container(
height: 40.0,
width: double.infinity,
alignment: Alignment.center,
// 不显示提示文本
child: buildIndicatorTitle(context),
);
}
Widget buildIndicatorTitle(BuildContext context) {
return Text(
"没有更多消息",
textAlign: TextAlign.left,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
fontStyle: FontStyle.normal,
color: ColorUtil.hexColor(0x555555),
decoration: TextDecoration.none,
),
);
}
}
ScorllController をリッスンして読み込みやその他のメッセージを制御します
scrollController.position.pixelsとscrollController.position.maxScrollExtentで判断
// 滚动控制器Controller
void addScrollListener() {
scrollController.addListener(() {
LoggerManager()
.debug("addScrollListener pixels:${
scrollController.position.pixels},"
"maxScrollExtent:${
scrollController.position.maxScrollExtent}"
"isLoading:${
isLoading}");
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent) {
if (isLoading == false) {
loadHistoryMore();
}
}
});
}
これまでのところ、フラッター チャット インターフェイス - チャット リスト ドロップダウンでより多くの履歴メッセージを読み込むことは基本的に完了しており、ここには多くのカプセル化されたメッセージ クラスがあります。その後のメッセージ送信操作が整理されます。
V. まとめ
Flutter チャット インターフェイス - チャット リストは、より多くの履歴メッセージを読み込むためにプルダウンされ、主に Column での Expand ネストされた ListView レイアウトの使用を実現し、リバース、物理、および ScrollBehavior を設定します。reverse が true の場合に上部に大きな空白領域が発生する問題を解決し、ListView のスライドの波紋を除去します。最後のメッセージが追加メッセージ インジケーターをロードするように設定されると、メッセージ プロンプトは表示されなくなります。
学習記録、日々改善を続けてください。