feat: Use dynamic gradient for chat bubbles
This commit is contained in:
parent
27a2361ba8
commit
480513c708
2 changed files with 191 additions and 103 deletions
|
|
@ -152,6 +152,7 @@ class ChatEventList extends StatelessWidget {
|
||||||
nextEvent: i + 1 < events.length ? events[i + 1] : null,
|
nextEvent: i + 1 < events.length ? events[i + 1] : null,
|
||||||
previousEvent: i > 0 ? events[i - 1] : null,
|
previousEvent: i > 0 ? events[i - 1] : null,
|
||||||
wallpaperMode: hasWallpaper,
|
wallpaperMode: hasWallpaper,
|
||||||
|
scrollController: controller.scrollController,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
|
@ -36,6 +38,7 @@ class Message extends StatelessWidget {
|
||||||
final bool animateIn;
|
final bool animateIn;
|
||||||
final void Function()? resetAnimateIn;
|
final void Function()? resetAnimateIn;
|
||||||
final bool wallpaperMode;
|
final bool wallpaperMode;
|
||||||
|
final ScrollController scrollController;
|
||||||
|
|
||||||
const Message(
|
const Message(
|
||||||
this.event, {
|
this.event, {
|
||||||
|
|
@ -54,6 +57,7 @@ class Message extends StatelessWidget {
|
||||||
this.animateIn = false,
|
this.animateIn = false,
|
||||||
this.resetAnimateIn,
|
this.resetAnimateIn,
|
||||||
this.wallpaperMode = false,
|
this.wallpaperMode = false,
|
||||||
|
required this.scrollController,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -323,118 +327,132 @@ class Message extends StatelessWidget {
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Container(
|
child: BubbleBackground(
|
||||||
decoration: BoxDecoration(
|
colors: [
|
||||||
borderRadius: BorderRadius.circular(
|
theme.brightness == Brightness.light
|
||||||
AppConfig.borderRadius,
|
? theme.colorScheme.tertiary
|
||||||
|
: theme.colorScheme
|
||||||
|
.tertiaryContainer,
|
||||||
|
color,
|
||||||
|
],
|
||||||
|
ignore: noBubble || !ownMessage,
|
||||||
|
scrollController: scrollController,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppConfig.borderRadius,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
padding: noBubble || noPadding
|
||||||
padding: noBubble || noPadding
|
? EdgeInsets.zero
|
||||||
? EdgeInsets.zero
|
: const EdgeInsets.symmetric(
|
||||||
: const EdgeInsets.symmetric(
|
horizontal: 16,
|
||||||
horizontal: 16,
|
vertical: 8,
|
||||||
vertical: 8,
|
),
|
||||||
),
|
constraints: const BoxConstraints(
|
||||||
constraints: const BoxConstraints(
|
maxWidth:
|
||||||
maxWidth:
|
FluffyThemes.columnWidth * 1.5,
|
||||||
FluffyThemes.columnWidth * 1.5,
|
),
|
||||||
),
|
child: Column(
|
||||||
child: Column(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
crossAxisAlignment:
|
||||||
crossAxisAlignment:
|
CrossAxisAlignment.start,
|
||||||
CrossAxisAlignment.start,
|
children: <Widget>[
|
||||||
children: <Widget>[
|
if (event.relationshipType ==
|
||||||
if (event.relationshipType ==
|
RelationshipTypes.reply)
|
||||||
RelationshipTypes.reply)
|
FutureBuilder<Event?>(
|
||||||
FutureBuilder<Event?>(
|
future: event
|
||||||
future: event
|
.getReplyEvent(timeline),
|
||||||
.getReplyEvent(timeline),
|
builder: (
|
||||||
builder: (
|
BuildContext context,
|
||||||
BuildContext context,
|
snapshot,
|
||||||
snapshot,
|
) {
|
||||||
) {
|
final replyEvent = snapshot
|
||||||
final replyEvent = snapshot
|
.hasData
|
||||||
.hasData
|
? snapshot.data!
|
||||||
? snapshot.data!
|
: Event(
|
||||||
: Event(
|
eventId: event
|
||||||
eventId: event
|
.relationshipEventId!,
|
||||||
.relationshipEventId!,
|
content: {
|
||||||
content: {
|
'msgtype':
|
||||||
'msgtype': 'm.text',
|
'm.text',
|
||||||
'body': '...',
|
'body': '...',
|
||||||
},
|
},
|
||||||
senderId:
|
senderId:
|
||||||
event.senderId,
|
event.senderId,
|
||||||
type:
|
type:
|
||||||
'm.room.message',
|
'm.room.message',
|
||||||
room: event.room,
|
room: event.room,
|
||||||
status:
|
status: EventStatus
|
||||||
EventStatus.sent,
|
.sent,
|
||||||
originServerTs:
|
originServerTs:
|
||||||
DateTime.now(),
|
DateTime.now(),
|
||||||
);
|
);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.only(
|
const EdgeInsets.only(
|
||||||
bottom: 4.0,
|
bottom: 4.0,
|
||||||
),
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: ReplyContent
|
|
||||||
.borderRadius,
|
|
||||||
onTap: () =>
|
|
||||||
scrollToEventId(
|
|
||||||
replyEvent.eventId,
|
|
||||||
),
|
),
|
||||||
child: AbsorbPointer(
|
child: InkWell(
|
||||||
child: ReplyContent(
|
borderRadius:
|
||||||
replyEvent,
|
ReplyContent
|
||||||
ownMessage:
|
.borderRadius,
|
||||||
ownMessage,
|
onTap: () =>
|
||||||
timeline: timeline,
|
scrollToEventId(
|
||||||
|
replyEvent.eventId,
|
||||||
|
),
|
||||||
|
child: AbsorbPointer(
|
||||||
|
child: ReplyContent(
|
||||||
|
replyEvent,
|
||||||
|
ownMessage:
|
||||||
|
ownMessage,
|
||||||
|
timeline: timeline,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
MessageContent(
|
|
||||||
displayEvent,
|
|
||||||
textColor: textColor,
|
|
||||||
linkColor: linkColor,
|
|
||||||
onInfoTab: onInfoTab,
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
timeline: timeline,
|
|
||||||
),
|
|
||||||
if (event.hasAggregatedEvents(
|
|
||||||
timeline,
|
|
||||||
RelationshipTypes.edit,
|
|
||||||
))
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 4.0,
|
|
||||||
),
|
),
|
||||||
child: Row(
|
MessageContent(
|
||||||
mainAxisSize:
|
displayEvent,
|
||||||
MainAxisSize.min,
|
textColor: textColor,
|
||||||
children: [
|
linkColor: linkColor,
|
||||||
Icon(
|
onInfoTab: onInfoTab,
|
||||||
Icons.edit_outlined,
|
borderRadius: borderRadius,
|
||||||
color: textColor
|
timeline: timeline,
|
||||||
.withAlpha(164),
|
),
|
||||||
size: 14,
|
if (event.hasAggregatedEvents(
|
||||||
),
|
timeline,
|
||||||
Text(
|
RelationshipTypes.edit,
|
||||||
' - ${displayEvent.originServerTs.localizedTimeShort(context)}',
|
))
|
||||||
style: TextStyle(
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(
|
||||||
|
top: 4.0,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize:
|
||||||
|
MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.edit_outlined,
|
||||||
color: textColor
|
color: textColor
|
||||||
.withAlpha(164),
|
.withAlpha(164),
|
||||||
fontSize: 12,
|
size: 14,
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
],
|
' - ${displayEvent.originServerTs.localizedTimeShort(context)}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: textColor
|
||||||
|
.withAlpha(164),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -574,3 +592,72 @@ class Message extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BubbleBackground extends StatelessWidget {
|
||||||
|
const BubbleBackground({
|
||||||
|
super.key,
|
||||||
|
required this.scrollController,
|
||||||
|
required this.colors,
|
||||||
|
required this.ignore,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ScrollController scrollController;
|
||||||
|
final List<Color> colors;
|
||||||
|
final bool ignore;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (ignore) return child;
|
||||||
|
return CustomPaint(
|
||||||
|
painter: BubblePainter(
|
||||||
|
repaint: scrollController,
|
||||||
|
colors: colors,
|
||||||
|
context: context,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BubblePainter extends CustomPainter {
|
||||||
|
BubblePainter({
|
||||||
|
required this.context,
|
||||||
|
required this.colors,
|
||||||
|
required super.repaint,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BuildContext context;
|
||||||
|
final List<Color> colors;
|
||||||
|
ScrollableState? _scrollable;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final scrollable = _scrollable ??= Scrollable.of(context);
|
||||||
|
final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
|
||||||
|
final scrollableRect = Offset.zero & scrollableBox.size;
|
||||||
|
final bubbleBox = context.findRenderObject() as RenderBox;
|
||||||
|
|
||||||
|
final origin =
|
||||||
|
bubbleBox.localToGlobal(Offset.zero, ancestor: scrollableBox);
|
||||||
|
final paint = Paint()
|
||||||
|
..shader = ui.Gradient.linear(
|
||||||
|
scrollableRect.topCenter,
|
||||||
|
scrollableRect.bottomCenter,
|
||||||
|
colors,
|
||||||
|
[0.0, 1.0],
|
||||||
|
TileMode.clamp,
|
||||||
|
Matrix4.translationValues(-origin.dx, -origin.dy, 0.0).storage,
|
||||||
|
);
|
||||||
|
canvas.drawRect(Offset.zero & size, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(BubblePainter oldDelegate) {
|
||||||
|
final scrollable = Scrollable.of(context);
|
||||||
|
final oldScrollable = _scrollable;
|
||||||
|
_scrollable = scrollable;
|
||||||
|
return scrollable.position != oldScrollable?.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue