272 lines
7.3 KiB
Dart
272 lines
7.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:stream_chat_flutter/src/attachment/thumbnail/giphy_attachment_thumbnail.dart';
|
|
import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart';
|
|
import 'package:stream_chat_flutter/src/attachment/thumbnail/video_attachment_thumbnail.dart';
|
|
import 'package:stream_chat_flutter/src/misc/flex_grid.dart';
|
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
|
|
|
/// {@template streamGalleryAttachment}
|
|
/// Constructs a gallery of images, videos, and gifs from a list of attachments.
|
|
///
|
|
/// This widget uses a [FlexGrid] to display the attachments in a grid format.
|
|
/// The grid will automatically resize based on the size of the attachment.
|
|
/// {@endtemplate}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [StreamImageAttachmentThumbnail], which is used to display the image
|
|
/// thumbnails.
|
|
/// * [StreamVideoAttachmentThumbnail], which is used to display the video
|
|
/// thumbnails.
|
|
/// * [StreamGiphyAttachmentThumbnail], which is used to display the gif
|
|
/// thumbnails.
|
|
class StreamGalleryAttachment extends StatelessWidget {
|
|
/// {@macro streamGalleryAttachment}
|
|
const StreamGalleryAttachment({
|
|
super.key,
|
|
required this.attachments,
|
|
required this.message,
|
|
this.shape,
|
|
this.constraints = const BoxConstraints(),
|
|
this.spacing = 2.0,
|
|
this.runSpacing = 2.0,
|
|
required this.itemBuilder,
|
|
});
|
|
|
|
/// List of attachments to show
|
|
final List<Attachment> attachments;
|
|
|
|
/// The [Message] that the images are attached to
|
|
final Message message;
|
|
|
|
/// The shape of the attachment.
|
|
///
|
|
/// Defaults to [RoundedRectangleBorder] with a radius of 14.
|
|
final ShapeBorder? shape;
|
|
|
|
/// The constraints of the [attachments]
|
|
final BoxConstraints constraints;
|
|
|
|
/// How much space to place between children in a run in the main axis.
|
|
///
|
|
/// For example, if [spacing] is 10.0, the children will be spaced at least
|
|
/// 10.0 logical pixels apart in the main axis.
|
|
///
|
|
/// Defaults to 2.0.
|
|
final double spacing;
|
|
|
|
/// How much space to place between the runs themselves in the cross axis.
|
|
///
|
|
/// For example, if [runSpacing] is 10.0, the runs will be spaced at least
|
|
/// 10.0 logical pixels apart in the cross axis.
|
|
///
|
|
/// Defaults to 2.0.
|
|
final double runSpacing;
|
|
|
|
/// Item builder for the gallery.
|
|
final IndexedWidgetBuilder itemBuilder;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
assert(
|
|
attachments.length >= 2,
|
|
'Gallery should have at least 2 attachments, found ${attachments.length}',
|
|
);
|
|
|
|
final chatTheme = StreamChatTheme.of(context);
|
|
final colorTheme = chatTheme.colorTheme;
|
|
final shape = this.shape ??
|
|
RoundedRectangleBorder(
|
|
side: BorderSide(
|
|
color: colorTheme.borders,
|
|
strokeAlign: BorderSide.strokeAlignOutside,
|
|
),
|
|
borderRadius: BorderRadius.circular(14),
|
|
);
|
|
|
|
return Container(
|
|
constraints: constraints,
|
|
clipBehavior: Clip.hardEdge,
|
|
decoration: ShapeDecoration(shape: shape),
|
|
// Added a builder just for the sake of calculating the image count
|
|
// and building the appropriate layout based on the image count.
|
|
child: Builder(
|
|
builder: (context) {
|
|
final attachmentCount = attachments.length;
|
|
if (attachmentCount == 2) {
|
|
return _buildForTwo(context, attachments);
|
|
}
|
|
|
|
if (attachmentCount == 3) {
|
|
return _buildForThree(context, attachments);
|
|
}
|
|
|
|
return _buildForFourOrMore(context, attachments);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildForTwo(BuildContext context, List<Attachment> attachments) {
|
|
final aspectRatio1 = attachments[0].originalSize?.aspectRatio;
|
|
final aspectRatio2 = attachments[1].originalSize?.aspectRatio;
|
|
|
|
// check if one image is landscape and other is portrait or vice versa
|
|
final isLandscape1 = aspectRatio1 != null && aspectRatio1 > 1;
|
|
final isLandscape2 = aspectRatio2 != null && aspectRatio2 > 1;
|
|
|
|
// Both the images are landscape.
|
|
if (isLandscape1 && isLandscape2) {
|
|
// ----------
|
|
// | |
|
|
// ----------
|
|
// | |
|
|
// ----------
|
|
return FlexGrid(
|
|
pattern: const [
|
|
[1],
|
|
[1],
|
|
],
|
|
spacing: spacing,
|
|
runSpacing: runSpacing,
|
|
children: [
|
|
itemBuilder(context, 0),
|
|
itemBuilder(context, 1),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Both the images are portrait.
|
|
if (!isLandscape1 && !isLandscape2) {
|
|
// -----------
|
|
// | | |
|
|
// | | |
|
|
// | | |
|
|
// -----------
|
|
return FlexGrid(
|
|
pattern: const [
|
|
[1, 1],
|
|
],
|
|
spacing: spacing,
|
|
runSpacing: runSpacing,
|
|
children: [
|
|
itemBuilder(context, 0),
|
|
itemBuilder(context, 1),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Layout on the basis of isLandscape1.
|
|
// 1. True
|
|
// -----------
|
|
// | | |
|
|
// | | |
|
|
// | | |
|
|
// -----------
|
|
//
|
|
// 2. False
|
|
// -----------
|
|
// | | |
|
|
// | | |
|
|
// | | |
|
|
// -----------
|
|
return FlexGrid(
|
|
pattern: [
|
|
if (isLandscape1) [2, 1] else [1, 2],
|
|
],
|
|
spacing: spacing,
|
|
runSpacing: runSpacing,
|
|
children: [
|
|
itemBuilder(context, 0),
|
|
itemBuilder(context, 1),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildForThree(BuildContext context, List<Attachment> attachments) {
|
|
final aspectRatio1 = attachments[0].originalSize?.aspectRatio;
|
|
final isLandscape1 = aspectRatio1 != null && aspectRatio1 > 1;
|
|
|
|
// We layout on the basis of isLandscape1.
|
|
// 1. True
|
|
// -----------
|
|
// | |
|
|
// | |
|
|
// |---------|
|
|
// | | |
|
|
// | | |
|
|
// -----------
|
|
//
|
|
// 2. False
|
|
// -----------
|
|
// | | |
|
|
// | | |
|
|
// | |----|
|
|
// | | |
|
|
// | | |
|
|
// -----------
|
|
return FlexGrid(
|
|
pattern: const [
|
|
[1],
|
|
[1, 1],
|
|
],
|
|
spacing: spacing,
|
|
runSpacing: runSpacing,
|
|
reverse: !isLandscape1,
|
|
children: [
|
|
itemBuilder(context, 0),
|
|
itemBuilder(context, 1),
|
|
itemBuilder(context, 2),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildForFourOrMore(
|
|
BuildContext context, List<Attachment> attachments) {
|
|
final pattern = <List<int>>[];
|
|
final children = <Widget>[];
|
|
|
|
for (var i = 0; i < attachments.length; i++) {
|
|
if (i.isEven) {
|
|
pattern.add([1]);
|
|
} else {
|
|
pattern.last.add(1);
|
|
}
|
|
|
|
children.add(itemBuilder(context, i));
|
|
}
|
|
|
|
// -----------
|
|
// | | |
|
|
// | | |
|
|
// ------------
|
|
// | | |
|
|
// | | |
|
|
// ------------
|
|
return FlexGrid(
|
|
pattern: pattern,
|
|
maxChildren: 4,
|
|
spacing: spacing,
|
|
runSpacing: runSpacing,
|
|
children: children,
|
|
overlayBuilder: (context, remaining) {
|
|
return IgnorePointer(
|
|
child: ColoredBox(
|
|
color: Colors.black38,
|
|
child: Center(
|
|
child: Text(
|
|
'+$remaining',
|
|
style: const TextStyle(
|
|
fontSize: 26,
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|