From 3ad6a25a828566b758c27d26eb9dff133cb722e6 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 12 Nov 2020 17:04:01 +0530 Subject: [PATCH] feat: Added new image detail screen --- lib/src/full_screen_image.dart | 110 +++++++++++++++++++++++---- lib/src/giphy_attachment.dart | 12 ++- lib/src/image_attachment.dart | 6 +- lib/src/image_footer.dart | 133 +++++++++++++++++++++++++++++++++ lib/src/image_group.dart | 32 ++------ lib/src/image_header.dart | 101 +++++++++++++++++++++++++ 6 files changed, 345 insertions(+), 49 deletions(-) create mode 100644 lib/src/image_footer.dart create mode 100644 lib/src/image_header.dart diff --git a/lib/src/full_screen_image.dart b/lib/src/full_screen_image.dart index 76953b1a..e0dd79a3 100644 --- a/lib/src/full_screen_image.dart +++ b/lib/src/full_screen_image.dart @@ -1,34 +1,112 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; import 'package:photo_view/photo_view.dart'; +import 'package:stream_chat_flutter/src/image_footer.dart'; +import 'package:stream_chat_flutter/src/image_header.dart'; + +import '../stream_chat_flutter.dart'; /// A full screen image widget -class FullScreenImage extends StatelessWidget { +class FullScreenImage extends StatefulWidget { /// The url of the image - final String url; + final List urls; + + final int startIndex; + final String userName; + final DateTime sentAt; /// Instantiate a new FullScreenImage const FullScreenImage({ Key key, - @required this.url, + @required this.urls, + this.startIndex = 0, + this.userName = '', + this.sentAt, }) : super(key: key); + @override + _FullScreenImageState createState() => _FullScreenImageState(); +} + +class _FullScreenImageState extends State + with SingleTickerProviderStateMixin { + bool _optionsShown = true; + + AnimationController _controller; + PageController _pageController; + + int _currentPage; + + @override + void initState() { + super.initState(); + _controller = + AnimationController(vsync: this, duration: Duration(milliseconds: 300)); + _pageController = PageController(initialPage: widget.startIndex); + _currentPage = widget.startIndex; + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - backgroundColor: Colors.black, - iconTheme: IconThemeData( - color: Colors.white, - ), - ), - body: PhotoView( - imageProvider: CachedNetworkImageProvider(url), - maxScale: PhotoViewComputedScale.covered, - minScale: PhotoViewComputedScale.contained, - heroAttributes: PhotoViewHeroAttributes( - tag: url, - ), + body: Stack( + children: [ + AnimatedBuilder( + animation: _controller, + builder: (context, snapshot) { + return PageView.builder( + controller: _pageController, + onPageChanged: (val) { + setState(() { + _currentPage = val; + }); + }, + itemBuilder: (context, position) { + return PhotoView( + imageProvider: CachedNetworkImageProvider(widget.urls[position]), + maxScale: PhotoViewComputedScale.covered, + minScale: PhotoViewComputedScale.contained, + heroAttributes: PhotoViewHeroAttributes( + tag: widget.urls, + ), + backgroundDecoration: BoxDecoration( + color: ColorTween( + begin: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .color, + end: Colors.black).lerp(_controller.value), + ), + onTapUp: (a, b, c) { + setState(() { + _optionsShown = !_optionsShown; + }); + if(_controller.isCompleted) { + _controller.reverse(); + } else { + _controller.forward(); + } + }, + ); + }, + itemCount: widget.urls.length, + ); + } + ), + AnimatedOpacity( + opacity: _optionsShown ? 1.0 : 0.0, + duration: Duration(milliseconds: 300), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ImageHeader(userName: widget.userName, sentAt: 'Sent at${Jiffy(widget.sentAt.toLocal()) + .format(' HH:mm')}',), + ImageFooter(currentPage: _currentPage, totalPages: widget.urls.length,), + ], + ), + ), + ], ), ); } diff --git a/lib/src/giphy_attachment.dart b/lib/src/giphy_attachment.dart index bfa21239..f99ccb27 100644 --- a/lib/src/giphy_attachment.dart +++ b/lib/src/giphy_attachment.dart @@ -67,9 +67,11 @@ class GiphyAttachment extends StatelessWidget { Navigator.push(context, MaterialPageRoute(builder: (_) { return FullScreenImage( - url: attachment.imageUrl ?? + urls: [attachment.imageUrl ?? attachment.assetUrl ?? - attachment.thumbUrl, + attachment.thumbUrl], + userName: message.user.name, + sentAt: message.createdAt, ); })); }, @@ -299,9 +301,11 @@ class GiphyAttachment extends StatelessWidget { onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) { return FullScreenImage( - url: attachment.imageUrl ?? + urls: [attachment.imageUrl ?? attachment.assetUrl ?? - attachment.thumbUrl, + attachment.thumbUrl], + userName: message.user.name, + sentAt: message.createdAt, ); })); }, diff --git a/lib/src/image_attachment.dart b/lib/src/image_attachment.dart index ba11252a..d493fa66 100644 --- a/lib/src/image_attachment.dart +++ b/lib/src/image_attachment.dart @@ -44,9 +44,11 @@ class ImageAttachment extends StatelessWidget { MaterialPageRoute( builder: (_) { return FullScreenImage( - url: attachment.imageUrl ?? + urls: [attachment.imageUrl ?? attachment.assetUrl ?? - attachment.thumbUrl, + attachment.thumbUrl], + userName: message.user.name, + sentAt: message.createdAt, ); }, ), diff --git a/lib/src/image_footer.dart b/lib/src/image_footer.dart new file mode 100644 index 00000000..fade96bf --- /dev/null +++ b/lib/src/image_footer.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/back_button.dart'; +import 'package:stream_chat_flutter/src/channel_info.dart'; +import 'package:stream_chat_flutter/src/channel_name.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/stream_icons.dart'; + +import './channel_name.dart'; +import 'channel_image.dart'; +import 'stream_channel.dart'; + +class ImageFooter extends StatelessWidget { + /// Callback to call when pressing the back button. + /// By default it calls [Navigator.pop] + final VoidCallback onBackPressed; + + /// Callback to call when the header is tapped. + final VoidCallback onTitleTap; + + /// Callback to call when the image is tapped. + final VoidCallback onImageTap; + + final int currentPage; + final int totalPages; + + /// Creates a channel header + ImageFooter({ + Key key, + this.onBackPressed, + this.onTitleTap, + this.onImageTap, + this.currentPage = 0, + this.totalPages = 0, + }) : preferredSize = Size.fromHeight(kToolbarHeight), + super(key: key); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Container( + color: StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon( + StreamIcons.share, + size: 24.0, + color: Colors.black, + ), + onPressed: onBackPressed, + ), + InkWell( + onTap: onTitleTap, + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${currentPage + 1} of $totalPages', + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + ], + ), + ), + ), + IconButton( + icon: Icon( + StreamIcons.grid, + color: Colors.black, + ), + onPressed: () {}, + ), + ], + ), + ), + ); + + return AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + leading: IconButton( + icon: Icon(StreamIcons.share), + onPressed: onBackPressed, + ), + backgroundColor: + StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, + actions: [ + IconButton( + icon: Icon(StreamIcons.grid), + onPressed: () {}, + ), + ], + centerTitle: true, + title: InkWell( + onTap: onTitleTap, + child: Container( + height: preferredSize.height, + width: preferredSize.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Demo name', + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .title, + ), + Text( + '1 of 1', + style: StreamChatTheme.of(context).channelPreviewTheme.subtitle, + ), + ], + ), + ), + ), + ); + } + + @override + final Size preferredSize; +} diff --git a/lib/src/image_group.dart b/lib/src/image_group.dart index 1a7144d8..4592d94d 100644 --- a/lib/src/image_group.dart +++ b/lib/src/image_group.dart @@ -110,34 +110,12 @@ class ImageGroup extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Colors.transparent, - iconTheme: IconThemeData( - color: Colors.white, - ), + builder: (context) => + FullScreenImage(urls: images.map((e) => e.imageUrl).toList(), + startIndex: index, + userName: message.user.name, + sentAt: message.createdAt, ), - backgroundColor: Colors.black, - body: SizedBox.expand( - child: CarouselSlider( - items: images - .map((image) => FullScreenImage( - url: image.imageUrl ?? - image.thumbUrl ?? - image.assetUrl, - )) - .toList(), - options: CarouselOptions( - initialPage: index ?? 0, - enableInfiniteScroll: false, - viewportFraction: 0.95, - height: MediaQuery.of(context).size.height, - ), - ), - ), - ); - }, ), ); } diff --git a/lib/src/image_header.dart b/lib/src/image_header.dart new file mode 100644 index 00000000..39f2ccb9 --- /dev/null +++ b/lib/src/image_header.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/back_button.dart'; +import 'package:stream_chat_flutter/src/channel_info.dart'; +import 'package:stream_chat_flutter/src/channel_name.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/stream_icons.dart'; + +import './channel_name.dart'; +import 'channel_image.dart'; +import 'stream_channel.dart'; + +class ImageHeader extends StatelessWidget implements PreferredSizeWidget { + /// True if this header shows the leading back button + final bool showBackButton; + + /// Callback to call when pressing the back button. + /// By default it calls [Navigator.pop] + final VoidCallback onBackPressed; + + /// Callback to call when the header is tapped. + final VoidCallback onTitleTap; + + /// Callback to call when the image is tapped. + final VoidCallback onImageTap; + + final String userName; + final String sentAt; + + /// Creates a channel header + ImageHeader({ + Key key, + this.showBackButton = true, + this.onBackPressed, + this.onTitleTap, + this.onImageTap, + this.userName = '', + this.sentAt = '', + }) : preferredSize = Size.fromHeight(kToolbarHeight), + super(key: key); + + @override + Widget build(BuildContext context) { + return AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + leading: showBackButton + ? IconButton( + icon: Icon( + StreamIcons.close, + color: Colors.black, + size: 24.0, + ), + onPressed: onBackPressed, + ) + : SizedBox(), + backgroundColor: + StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, + actions: [ + IconButton( + icon: Icon( + StreamIcons.menu_point_v, + color: Colors.black, + size: 24.0, + ), + onPressed: () {}, + ), + ], + centerTitle: true, + title: InkWell( + onTap: onTitleTap, + child: Container( + height: preferredSize.height, + width: preferredSize.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + userName, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + Text( + sentAt, + style: StreamChatTheme.of(context).channelPreviewTheme.subtitle, + ), + ], + ), + ), + ), + ); + } + + @override + final Size preferredSize; +}