Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/message_content #98

Merged
merged 3 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@
"giffy",
"giphy",
"ccon",
"nodoc"
"nodoc",
"ChildLayouter"
],
"ignorePaths": [
".github/workflows/**",
Expand Down
49 changes: 49 additions & 0 deletions lib/chats/chat/widgets/bubble/bubble.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:flutter_instagram_offline_first_clone/chats/chat/widgets/bubble/bubble_border_widget.dart';
import 'package:flutter_instagram_offline_first_clone/chats/chat/widgets/bubble/bubble_clipper.dart';
import 'package:flutter_instagram_offline_first_clone/chats/chat/widgets/bubble/bubble_render_box.dart';

class Bubble extends StatefulWidget {
const Bubble({
required this.child,
required this.radius,
required this.borderColor,
super.key,
});

final Widget child;
final double radius;
final Color borderColor;

@override
State<Bubble> createState() => _BubbleState();
}

class _BubbleState extends State<Bubble> implements BorderPathProvider {
late final BubbleClipper _clipper = BubbleClipper(radius: widget.radius);

@override
Widget build(BuildContext context) {
return BubbleBorderWidget(
borderPathProvider: this,
color: widget.borderColor,
child: ClipPath(
clipper: BubbleClipper(
radius: widget.radius,
),
child: widget.child,
),
);
}

@override
void didUpdateWidget(covariant Bubble oldWidget) {
if (oldWidget.radius != widget.radius) {
_clipper.radius = widget.radius;
}
super.didUpdateWidget(oldWidget);
}

@override
Path getPath(Size size) => _clipper.getClip(size);
}
30 changes: 30 additions & 0 deletions lib/chats/chat/widgets/bubble/bubble_border_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// ignore_for_file: cascade_invocations

import 'package:flutter/material.dart';
import 'package:flutter_instagram_offline_first_clone/chats/chat/widgets/bubble/bubble_render_box.dart';

class BubbleBorderWidget extends SingleChildRenderObjectWidget {
const BubbleBorderWidget({
required Widget super.child,
required this.color,
required this.borderPathProvider,
super.key,
});

final Color color;
final BorderPathProvider borderPathProvider;

@override
RenderObject createRenderObject(BuildContext context) {
return BubbleRenderBox(
color: color,
borderPathProvider: borderPathProvider,
);
}

@override
void updateRenderObject(BuildContext context, BubbleRenderBox renderObject) {
renderObject.color = color;
renderObject.borderPathProvider = borderPathProvider;
}
}
68 changes: 68 additions & 0 deletions lib/chats/chat/widgets/bubble/bubble_clipper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import 'package:flutter/rendering.dart';

class BubbleClipper extends CustomClipper<Path> {
BubbleClipper({required double radius}) : _radius = radius;

double get radius => _radius;
double _radius;

set radius(double value) {
if (value == _radius) {
return;
}
_radius = value;
}

@override
Path getClip(Size size) {
const rightPadding = 0.0;
const leftPadding = 0.0;

final radiusCircular = Radius.circular(radius);
final path = Path()
..moveTo(0, radius)

// start top left
..lineTo(leftPadding, radius)
..arcToPoint(
Offset(leftPadding + radius, 0),
radius: radiusCircular,
)
// end top left

// start top right
..lineTo(size.width - radius - rightPadding, 0)
..arcToPoint(
Offset(size.width - rightPadding, radius),
radius: radiusCircular,
)
// end top right

// start bottom right
..lineTo(size.width - rightPadding, size.height - radius)
..arcToPoint(
Offset(size.width - rightPadding - radius, size.height),
radius: radiusCircular,
)

// end bottom right

// start bottom left
..lineTo(radius + leftPadding, size.height)
..arcToPoint(
Offset(leftPadding, size.height - radius),
radius: radiusCircular,
)
// end bottom left

..lineTo(leftPadding, radius)
..close();

return path;
}

@override
bool shouldReclip(covariant BubbleClipper oldClipper) {
return oldClipper.radius != radius;
}
}
69 changes: 69 additions & 0 deletions lib/chats/chat/widgets/bubble/bubble_render_box.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// ignore_for_file: one_member_abstracts

import 'package:flutter/rendering.dart';

class BubbleRenderBox extends RenderProxyBoxWithHitTestBehavior {
BubbleRenderBox({
required Color color,
required BorderPathProvider borderPathProvider,
}) : _color = color,
_borderPathProvider = borderPathProvider,
super(behavior: HitTestBehavior.opaque) {
_createBorderPaint();
}

Paint? borderPaint;

BorderPathProvider get borderPathProvider => _borderPathProvider;
BorderPathProvider _borderPathProvider;

set borderPathProvider(BorderPathProvider value) {
if (value == _borderPathProvider) {
return;
}
_borderPathProvider = value;
_createBorderPaint();
markNeedsPaint();
}

Color get color => _color;
Color _color;

set color(Color value) {
if (value == _color) {
return;
}
_color = value;
_createBorderPaint();
markNeedsPaint();
}

void _createBorderPaint() {
borderPaint = Paint()
..isAntiAlias = true
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = _color;
}

@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.paintChild(child!, offset);
}

final paint = borderPaint;

if (paint != null) {
context.canvas
..save()
..translate(offset.dx, offset.dy)
..drawPath(_borderPathProvider.getPath(size), paint)
..restore();
}
}
}

abstract class BorderPathProvider {
Path getPath(Size size);
}
76 changes: 76 additions & 0 deletions lib/chats/chat/widgets/message/chat_context.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'package:flutter/widgets.dart';

class ChatContext extends StatelessWidget {
const ChatContext({required this.data, required this.child, super.key});

final ChatContextData data;

final Widget child;

static ChatContextData of(BuildContext context) {
final inheritedTheme =
context.dependOnInheritedWidgetOfExactType<_InheritedContext>();
final theme = inheritedTheme!.theme.data;
return theme;
}

@override
Widget build(BuildContext context) {
return _InheritedContext(
theme: this,
child: child,
);
}
}

enum MediaType { video, photo, animation }

class ChatContextData {
const ChatContextData.raw({
required this.maxWidth,
required this.width,
required this.horizontalPadding,
required this.verticalPadding,
required this.mediaConstraints,
});

factory ChatContextData.desktop({
required double width,
required double maxWidth,
}) =>
ChatContextData.raw(
width: width,
horizontalPadding: 8,
verticalPadding: 4,
maxWidth: maxWidth,
mediaConstraints: <MediaType, Size>{
MediaType.video: const Size(double.infinity, 450),
MediaType.photo: const Size(double.infinity, 450),
MediaType.animation: const Size(300, 300),
},
);

final double maxWidth;
final double width;
final double horizontalPadding;
final double verticalPadding;
final Map<MediaType, Size> mediaConstraints;
}

class _InheritedContext extends InheritedTheme {
const _InheritedContext({
required this.theme,
required super.child,
});

final ChatContext theme;

@override
Widget wrap(BuildContext context, Widget child) {
return ChatContext(data: theme.data, child: child);
}

@override
bool updateShouldNotify(_InheritedContext old) =>
theme.data != old.theme.data;
}
Loading
Loading