Merge branch 'braid/chat-list-animation' into 'main'

fix: chat list view animation

See merge request famedly/fluffychat!852
This commit is contained in:
Krille Fear 2022-05-03 11:51:37 +00:00
commit 1d7cd26671
2 changed files with 221 additions and 212 deletions

View File

@ -0,0 +1,219 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/spaces_bottom_bar.dart';
import 'package:fluffychat/pages/chat_list/stories_header.dart';
import '../../utils/stream_extension.dart';
import '../../widgets/matrix.dart';
class ChatListViewBody extends StatefulWidget {
final ChatListController controller;
const ChatListViewBody(this.controller, {Key? key}) : super(key: key);
@override
State<ChatListViewBody> createState() => _ChatListViewBodyState();
}
class _ChatListViewBodyState extends State<ChatListViewBody> {
// the matrix sync stream
late StreamSubscription _subscription;
late StreamSubscription _clientSubscription;
// used to check the animation direction
String? _lastUserId;
String? _lastSpaceId;
@override
void initState() {
_subscription = Matrix.of(context)
.client
.onSync
.stream
.where((s) => s.hasRoomUpdate)
.rateLimit(const Duration(seconds: 1))
.listen((d) => setState(() {}));
_clientSubscription =
widget.controller.clientStream.listen((d) => setState(() {}));
super.initState();
}
@override
Widget build(BuildContext context) {
final reversed = !_animationReversed();
Widget child;
if (widget.controller.waitForFirstSync &&
Matrix.of(context).client.prevBatch != null) {
final rooms = widget.controller.activeSpacesEntry.getRooms(context);
if (rooms.isEmpty) {
child = Column(
key: const ValueKey(null),
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Image.asset(
'assets/private_chat_wallpaper.png',
width: 160,
height: 160,
),
Center(
child: Text(
L10n.of(context)!.startYourFirstChat,
textAlign: TextAlign.start,
style: const TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
),
],
);
} else {
final displayStoriesHeader = widget.controller.activeSpacesEntry
.shouldShowStoriesHeader(context);
child = ListView.builder(
key: ValueKey(Matrix.of(context).client.userID.toString() +
widget.controller.activeSpaceId.toString()),
controller: widget.controller.scrollController,
itemCount: rooms.length + (displayStoriesHeader ? 1 : 0),
itemBuilder: (BuildContext context, int i) {
if (displayStoriesHeader) {
if (i == 0) {
return const StoriesHeader();
}
i--;
}
return ChatListItem(
rooms[i],
selected: widget.controller.selectedRoomIds.contains(rooms[i].id),
onTap: widget.controller.selectMode == SelectMode.select
? () => widget.controller.toggleSelection(rooms[i].id)
: null,
onLongPress: () => widget.controller.toggleSelection(rooms[i].id),
activeChat: widget.controller.activeChat == rooms[i].id,
);
},
);
}
} else {
const dummyChatCount = 5;
final titleColor =
Theme.of(context).textTheme.bodyText1!.color!.withAlpha(100);
final subtitleColor =
Theme.of(context).textTheme.bodyText1!.color!.withAlpha(50);
child = ListView.builder(
itemCount: dummyChatCount,
itemBuilder: (context, i) => Opacity(
opacity: (dummyChatCount - i) / dummyChatCount,
child: ListTile(
leading: CircleAvatar(
backgroundColor: titleColor,
child: CircularProgressIndicator(
strokeWidth: 1,
color: Theme.of(context).textTheme.bodyText1!.color,
),
),
title: Row(
children: [
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.circular(3),
),
),
),
const SizedBox(width: 36),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
const SizedBox(width: 12),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
],
),
subtitle: Container(
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(3),
),
height: 12,
margin: const EdgeInsets.only(right: 22),
),
),
),
);
}
return PageTransitionSwitcher(
reverse: reversed,
transitionBuilder: (
Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
) {
return SharedAxisTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
transitionType:
widget.controller.snappingSheetController.currentPosition ==
kSpacesBottomBarHeight
? SharedAxisTransitionType.horizontal
: SharedAxisTransitionType.vertical,
fillColor: Theme.of(context).scaffoldBackgroundColor,
child: child,
);
},
child: child,
);
}
@override
void dispose() {
_subscription.cancel();
_clientSubscription.cancel();
super.dispose();
}
bool _animationReversed() {
bool reversed;
// in case the matrix id changes, check the indexOf the matrix id
final newClient = Matrix.of(context).client;
if (_lastUserId != newClient.userID) {
reversed = Matrix.of(context)
.currentBundle!
.indexWhere((element) => element!.userID == _lastUserId) <
Matrix.of(context)
.currentBundle!
.indexWhere((element) => element!.userID == newClient.userID);
}
// otherwise, the space changed...
else {
reversed = widget.controller.spaces
.indexWhere((element) => element.id == _lastSpaceId) <
widget.controller.spaces.indexWhere(
(element) => element.id == widget.controller.activeSpaceId);
}
_lastUserId = newClient.userID;
_lastSpaceId = widget.controller.activeSpaceId;
return reversed;
}
}

View File

@ -1,25 +1,19 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:animations/animations.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
import 'package:matrix/matrix.dart';
import 'package:snapping_sheet/snapping_sheet.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/client_chooser_button.dart';
import 'package:fluffychat/pages/chat_list/spaces_bottom_bar.dart';
import 'package:fluffychat/pages/chat_list/stories_header.dart';
import 'package:fluffychat/widgets/connection_status_header.dart';
import '../../utils/stream_extension.dart';
import '../../widgets/matrix.dart';
import 'chat_list_body.dart';
class ChatListView extends StatelessWidget {
final ChatListController controller;
@ -231,7 +225,7 @@ class ChatListView extends StatelessWidget {
),
),
),
Expanded(child: _ChatListViewBody(controller)),
Expanded(child: ChatListViewBody(controller)),
],
),
initialSnappingPosition: showSpaces
@ -288,210 +282,6 @@ class ChatListView extends StatelessWidget {
}
}
class _ChatListViewBody extends StatefulWidget {
final ChatListController controller;
const _ChatListViewBody(this.controller, {Key? key}) : super(key: key);
@override
State<_ChatListViewBody> createState() => _ChatListViewBodyState();
}
class _ChatListViewBodyState extends State<_ChatListViewBody> {
// the matrix sync stream
late StreamSubscription _subscription;
late StreamSubscription _clientSubscription;
// used to check the animation direction
String? _lastUserId;
String? _lastSpaceId;
@override
void initState() {
_subscription = Matrix.of(context)
.client
.onSync
.stream
.where((s) => s.hasRoomUpdate)
.rateLimit(const Duration(seconds: 1))
.listen((d) => setState(() {}));
_clientSubscription =
widget.controller.clientStream.listen((d) => setState(() {}));
super.initState();
}
@override
Widget build(BuildContext context) {
final reversed = _animationReversed();
Widget child;
if (widget.controller.waitForFirstSync &&
Matrix.of(context).client.prevBatch != null) {
final rooms = widget.controller.activeSpacesEntry.getRooms(context);
if (rooms.isEmpty) {
child = Column(
key: const ValueKey(null),
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Image.asset(
'assets/private_chat_wallpaper.png',
width: 160,
height: 160,
),
Center(
child: Text(
L10n.of(context)!.startYourFirstChat,
textAlign: TextAlign.start,
style: const TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
),
],
);
} else {
final displayStoriesHeader = widget.controller.activeSpacesEntry
.shouldShowStoriesHeader(context);
child = ListView.builder(
key: ValueKey(Matrix.of(context).client.userID.toString() +
widget.controller.activeSpaceId.toString()),
controller: widget.controller.scrollController,
itemCount: rooms.length + (displayStoriesHeader ? 1 : 0),
itemBuilder: (BuildContext context, int i) {
if (displayStoriesHeader) {
if (i == 0) {
return const StoriesHeader();
}
i--;
}
return ChatListItem(
rooms[i],
selected: widget.controller.selectedRoomIds.contains(rooms[i].id),
onTap: widget.controller.selectMode == SelectMode.select
? () => widget.controller.toggleSelection(rooms[i].id)
: null,
onLongPress: () => widget.controller.toggleSelection(rooms[i].id),
activeChat: widget.controller.activeChat == rooms[i].id,
);
},
);
}
} else {
const dummyChatCount = 5;
final titleColor =
Theme.of(context).textTheme.bodyText1!.color!.withAlpha(100);
final subtitleColor =
Theme.of(context).textTheme.bodyText1!.color!.withAlpha(50);
child = ListView.builder(
itemCount: dummyChatCount,
itemBuilder: (context, i) => Opacity(
opacity: (dummyChatCount - i) / dummyChatCount,
child: ListTile(
leading: CircleAvatar(
backgroundColor: titleColor,
child: CircularProgressIndicator(
strokeWidth: 1,
color: Theme.of(context).textTheme.bodyText1!.color,
),
),
title: Row(
children: [
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.circular(3),
),
),
),
const SizedBox(width: 36),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
const SizedBox(width: 12),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
],
),
subtitle: Container(
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(3),
),
height: 12,
margin: const EdgeInsets.only(right: 22),
),
),
),
);
}
child = Material(
color: Theme.of(context).scaffoldBackgroundColor,
child: child,
);
return PageTransitionSwitcher(
reverse: reversed,
transitionBuilder: (
Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
) {
return SharedAxisTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.horizontal,
child: child,
);
},
child: child,
);
}
@override
void dispose() {
_subscription.cancel();
_clientSubscription.cancel();
super.dispose();
}
bool _animationReversed() {
bool reversed;
// in case the matrix id changes, check the indexOf the matrix id
final newClient = Matrix.of(context).client;
if (_lastUserId != newClient.userID) {
reversed = Matrix.of(context)
.currentBundle!
.indexWhere((element) => element!.userID == _lastUserId) <
Matrix.of(context)
.currentBundle!
.indexWhere((element) => element!.userID == newClient.userID);
}
// otherwise, the space changed...
else {
reversed = widget.controller.spaces
.indexWhere((element) => element.id == _lastSpaceId) <
widget.controller.spaces.indexWhere(
(element) => element.id == widget.controller.activeSpaceId);
}
_lastUserId = newClient.userID;
_lastSpaceId = widget.controller.activeSpaceId;
return reversed;
}
}
enum ChatListPopupMenuItemActions {
createGroup,
createSpace,