Flutter development practice-hero implements image preview function extend_image

Flutter development practice-hero implements image preview function extend_image

During development, we often encounter the need for image preview. When you click on an image in the feed, the preview will be opened. Multiple images can be switched left and right with swiper, and the zoom function can be performed by double-clicking the image and using gestures.
This main implementation uses the extend_image plug-in. Use hero animation to display when clicking on the picture.

Hero is easy to use and can be viewedhttps://brucegwo.blog.csdn.net/article/details/134005601

Hero implements image preview function renderings

Insert image description here
Insert image description here

1. Picture GridView

When displaying multiple pictures, use GridView to display them.

GridView can construct a two-dimensional grid list, and its default constructor is defined as follows:

  GridView({
    
    
    Key? key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController? controller,
    bool? primary,
    ScrollPhysics? physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry? padding,
    required this.gridDelegate,  //下面解释
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    double? cacheExtent, 
    List<Widget> children = const <Widget>[],
    ...
  })

SliverGridDelegate is an abstract class that defines GridView Layout related interfaces. Subclasses need to implement them to implement specific layout algorithms.

Implement GridView for displaying pictures

GridView.builder(
          gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
            maxCrossAxisExtent: 300,
            crossAxisSpacing: 10,
            mainAxisSpacing: 10,
          ),
          itemBuilder: (BuildContext context, int index) {
    
    

...

The complete code is as follows

class GridSimplePhotoViewDemo extends StatefulWidget {
    
    
  
  _GridSimplePhotoViewDemoState createState() =>
      _GridSimplePhotoViewDemoState();
}

class _GridSimplePhotoViewDemoState extends State<GridSimplePhotoViewDemo> {
    
    
  List<String> images = <String>[
    'https://photo.tuchong.com/14649482/f/601672690.jpg',
    'https://photo.tuchong.com/17325605/f/641585173.jpg',
    'https://photo.tuchong.com/3541468/f/256561232.jpg',
    'https://photo.tuchong.com/16709139/f/278778447.jpg',
    'This is an video',
    'https://photo.tuchong.com/5040418/f/43305517.jpg',
    'https://photo.tuchong.com/3019649/f/302699092.jpg'
  ];

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('SimplePhotoView'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(10.0),
        child: GridView.builder(
          gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
            maxCrossAxisExtent: 300,
            crossAxisSpacing: 10,
            mainAxisSpacing: 10,
          ),
          itemBuilder: (BuildContext context, int index) {
    
    
            final String url = images[index];
            return GestureDetector(
              child: AspectRatio(
                aspectRatio: 1.0,
                child: Hero(
                  tag: url,
                  child: url == 'This is an video'
                      ? Container(
                          alignment: Alignment.center,
                          child: const Text('This is an video'),
                        )
                      : ExtendedImage.network(
                          url,
                          fit: BoxFit.cover,
                        ),
                ),
              ),
              onTap: () {
    
    
                Navigator.of(context).push(TransparentPageRoute(pageBuilder:
                    (BuildContext context, Animation<double> animation,
                        Animation<double> secondaryAnimation) {
    
    
                  return PicSwiper(
                    index: index,
                    pics: images,
                  );
                }));
              },
            );
          },
          itemCount: images.length,
        ),
      ),
    );
  }
}

2. Jump to Swiper’s TransparentPageRoute

When you click to jump to a new page, you can use TransparentPageRoute. This class inherits from PageRouteBuilder to implement FadeTransition. When you click on the picture to display the preview image, you can jump to the next route by fading in and out.

Widget _defaultTransitionsBuilder(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
    ) {
    
    
  return FadeTransition(
    opacity: CurvedAnimation(
      parent: animation,
      curve: Curves.easeOut,
    ),
    child: child,
  );
}

The complete code is as follows

import 'package:flutter/material.dart';

/// Transparent Page Route
class TransparentPageRoute<T> extends PageRouteBuilder<T> {
    
    
  TransparentPageRoute({
    
    
    RouteSettings? settings,
    required RoutePageBuilder pageBuilder,
    RouteTransitionsBuilder transitionsBuilder = _defaultTransitionsBuilder,
    Duration transitionDuration = const Duration(milliseconds: 250),
    bool barrierDismissible = false,
    Color? barrierColor,
    String? barrierLabel,
    bool maintainState = true,
  }) : super(
    settings: settings,
    opaque: false,
    pageBuilder: pageBuilder,
    transitionsBuilder: transitionsBuilder,
    transitionDuration: transitionDuration,
    barrierDismissible: barrierDismissible,
    barrierColor: barrierColor,
    barrierLabel: barrierLabel,
    maintainState: maintainState,
  );
}

Widget _defaultTransitionsBuilder(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
    ) {
    
    
  return FadeTransition(
    opacity: CurvedAnimation(
      parent: animation,
      curve: Curves.easeOut,
    ),
    child: child,
  );
}

3. Use extend_image

Introduce extend_image in pubspec.yaml

  # extended_image
  extended_image: ^7.0.2

When a picture is clicked, multiple pictures are passed in and the current index is positioned. Multiple pictures can be switched left and right with Swiper. ExtendedImageGesturePageView is used here. ExtendedImageGesturePageView is similar to PageView in that it is designed for displaying zoom/pan images.
If you have cached gestures, remember to call the clearGestureDetailsCache() method at the right time. (e.g. page view page is discarded)

ExtendedImageGesturePageView Property

  • cacheGesture saves the gesture state (for example, in ExtendedImageGesturePageView, the gesture state does not change when scrolling backward), remember clearGestureDetailsCache
  • inPageView is in ExtendedImageGesturePageView

Usage example

ExtendedImageGesturePageView.builder(
  itemBuilder: (BuildContext context, int index) {
    
    
    var item = widget.pics[index].picUrl;
    Widget image = ExtendedImage.network(
      item,
      fit: BoxFit.contain,
      mode: ExtendedImageMode.gesture,
      gestureConfig: GestureConfig(
        inPageView: true, initialScale: 1.0,
        //you can cache gesture state even though page view page change.
        //remember call clearGestureDetailsCache() method at the right time.(for example,this page dispose)
        cacheGesture: false
      ),
    );
    image = Container(
      child: image,
      padding: EdgeInsets.all(5.0),
    );
    if (index == currentIndex) {
    
    
      return Hero(
        tag: item + index.toString(),
        child: image,
      );
    } else {
    
    
      return image;
    }
  },
  itemCount: widget.pics.length,
  onPageChanged: (int index) {
    
    
    currentIndex = index;
    rebuild.add(index);
  },
  controller: PageController(
    initialPage: currentIndex,
  ),
  scrollDirection: Axis.horizontal,
)

4. Use hero_widget

When the picture is clicked, hero_widget is implemented to implement hero animation to realize picture preview.
Use Flutter’s Hero widget to create hero animations. Fly hero from one route to another. Converts the hero's shape from a circle to a rectangle, while animating it as it flies from one route to another.

The complete code of hero_widget used here is as follows

import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';

/// make hero better when slide out
class HeroWidget extends StatefulWidget {
    
    
  const HeroWidget({
    
    
    required this.child,
    required this.tag,
    required this.slidePagekey,
    this.slideType = SlideType.onlyImage,
  });
  final Widget child;
  final SlideType slideType;
  final Object tag;
  final GlobalKey<ExtendedImageSlidePageState> slidePagekey;
  
  _HeroWidgetState createState() => _HeroWidgetState();
}

class _HeroWidgetState extends State<HeroWidget> {
    
    
  RectTween? _rectTween;
  
  Widget build(BuildContext context) {
    
    
    return Hero(
      tag: widget.tag,
      createRectTween: (Rect? begin, Rect? end) {
    
    
        _rectTween = RectTween(begin: begin, end: end);
        return _rectTween!;
      },
      // make hero better when slide out
      flightShuttleBuilder: (BuildContext flightContext,
          Animation<double> animation,
          HeroFlightDirection flightDirection,
          BuildContext fromHeroContext,
          BuildContext toHeroContext) {
    
    
        // make hero more smoothly
        final Hero hero = (flightDirection == HeroFlightDirection.pop
            ? fromHeroContext.widget
            : toHeroContext.widget) as Hero;
        if (_rectTween == null) {
    
    
          return hero;
        }

        if (flightDirection == HeroFlightDirection.pop) {
    
    
          final bool fixTransform = widget.slideType == SlideType.onlyImage &&
              (widget.slidePagekey.currentState!.offset != Offset.zero ||
                  widget.slidePagekey.currentState!.scale != 1.0);

          final Widget toHeroWidget = (toHeroContext.widget as Hero).child;
          return AnimatedBuilder(
            animation: animation,
            builder: (BuildContext buildContext, Widget? child) {
    
    
              Widget animatedBuilderChild = hero.child;

              // make hero more smoothly
              animatedBuilderChild = Stack(
                clipBehavior: Clip.antiAlias,
                alignment: Alignment.center,
                children: <Widget>[
                  Opacity(
                    opacity: 1 - animation.value,
                    child: UnconstrainedBox(
                      child: SizedBox(
                        width: _rectTween!.begin!.width,
                        height: _rectTween!.begin!.height,
                        child: toHeroWidget,
                      ),
                    ),
                  ),
                  Opacity(
                    opacity: animation.value,
                    child: animatedBuilderChild,
                  )
                ],
              );

              // fix transform when slide out
              if (fixTransform) {
    
    
                final Tween<Offset> offsetTween = Tween<Offset>(
                    begin: Offset.zero,
                    end: widget.slidePagekey.currentState!.offset);

                final Tween<double> scaleTween = Tween<double>(
                    begin: 1.0, end: widget.slidePagekey.currentState!.scale);
                animatedBuilderChild = Transform.translate(
                  offset: offsetTween.evaluate(animation),
                  child: Transform.scale(
                    scale: scaleTween.evaluate(animation),
                    child: animatedBuilderChild,
                  ),
                );
              }

              return animatedBuilderChild;
            },
          );
        }
        return hero.child;
      },
      child: widget.child,
    );
  }
}

5. Use Pic_Swiper

Use ExtendedImageGesturePageView to switch the left and right functions of the swiper, and double-click the image and gesture to zoom.

The complete code is as follows

typedef DoubleClickAnimationListener = void Function();

class PicSwiper extends StatefulWidget {
    
    
  const PicSwiper({
    
    
    super.key,
    this.index,
    this.pics,
  });

  final int? index;
  final List<String>? pics;

  
  _PicSwiperState createState() => _PicSwiperState();
}

class _PicSwiperState extends State<PicSwiper> with TickerProviderStateMixin {
    
    
  final StreamController<int> rebuildIndex = StreamController<int>.broadcast();
  final StreamController<bool> rebuildSwiper =
      StreamController<bool>.broadcast();
  final StreamController<double> rebuildDetail =
      StreamController<double>.broadcast();
  late AnimationController _doubleClickAnimationController;
  late AnimationController _slideEndAnimationController;
  late Animation<double> _slideEndAnimation;
  Animation<double>? _doubleClickAnimation;
  late DoubleClickAnimationListener _doubleClickAnimationListener;
  List<double> doubleTapScales = <double>[1.0, 2.0];
  GlobalKey<ExtendedImageSlidePageState> slidePagekey =
      GlobalKey<ExtendedImageSlidePageState>();
  int? _currentIndex = 0;
  bool _showSwiper = true;
  double _imageDetailY = 0;
  Rect? imageDRect;

  
  Widget build(BuildContext context) {
    
    
    final Size size = MediaQuery.of(context).size;
    double statusBarHeight = MediaQuery.of(context).padding.top;

    imageDRect = Offset.zero & size;
    Widget result = Material(
      color: Colors.transparent,
      shadowColor: Colors.transparent,
      child: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          ExtendedImageGesturePageView.builder(
            controller: ExtendedPageController(
              initialPage: widget.index!,
              pageSpacing: 50,
              shouldIgnorePointerWhenScrolling: false,
            ),
            scrollDirection: Axis.horizontal,
            physics: const BouncingScrollPhysics(),
            canScrollPage: (GestureDetails? gestureDetails) {
    
    
              return _imageDetailY >= 0;
            },
            itemBuilder: (BuildContext context, int index) {
    
    
              final String item = widget.pics![index];

              Widget image = ExtendedImage.network(
                item,
                fit: BoxFit.contain,
                enableSlideOutPage: true,
                mode: ExtendedImageMode.gesture,
                imageCacheName: 'CropImage',
                //layoutInsets: EdgeInsets.all(20),
                initGestureConfigHandler: (ExtendedImageState state) {
    
    
                  double? initialScale = 1.0;

                  if (state.extendedImageInfo != null) {
    
    
                    initialScale = initScale(
                        size: size,
                        initialScale: initialScale,
                        imageSize: Size(
                            state.extendedImageInfo!.image.width.toDouble(),
                            state.extendedImageInfo!.image.height.toDouble()));
                  }
                  return GestureConfig(
                    inPageView: true,
                    initialScale: initialScale!,
                    maxScale: max(initialScale, 5.0),
                    animationMaxScale: max(initialScale, 5.0),
                    initialAlignment: InitialAlignment.center,
                    //you can cache gesture state even though page view page change.
                    //remember call clearGestureDetailsCache() method at the right time.(for example,this page dispose)
                    cacheGesture: false,
                  );
                },
                onDoubleTap: (ExtendedImageGestureState state) {
    
    
                  ///you can use define pointerDownPosition as you can,
                  ///default value is double tap pointer down postion.
                  final Offset? pointerDownPosition = state.pointerDownPosition;
                  final double? begin = state.gestureDetails!.totalScale;
                  double end;

                  //remove old
                  _doubleClickAnimation
                      ?.removeListener(_doubleClickAnimationListener);

                  //stop pre
                  _doubleClickAnimationController.stop();

                  //reset to use
                  _doubleClickAnimationController.reset();

                  if (begin == doubleTapScales[0]) {
    
    
                    end = doubleTapScales[1];
                  } else {
    
    
                    end = doubleTapScales[0];
                  }

                  _doubleClickAnimationListener = () {
    
    
                    //print(_animation.value);
                    state.handleDoubleTap(
                        scale: _doubleClickAnimation!.value,
                        doubleTapPosition: pointerDownPosition);
                  };
                  _doubleClickAnimation = _doubleClickAnimationController
                      .drive(Tween<double>(begin: begin, end: end));

                  _doubleClickAnimation!
                      .addListener(_doubleClickAnimationListener);

                  _doubleClickAnimationController.forward();
                },
                loadStateChanged: (ExtendedImageState state) {
    
    
                  if (state.extendedImageLoadState == LoadState.completed) {
    
    
                    return StreamBuilder<double>(
                      builder:
                          (BuildContext context, AsyncSnapshot<double> data) {
    
    
                        return ExtendedImageGesture(
                          state,
                          imageBuilder: (Widget image) {
    
    
                            return Stack(
                              children: <Widget>[
                                Positioned.fill(
                                  child: image,
                                ),
                              ],
                            );
                          },
                        );
                      },
                      initialData: _imageDetailY,
                      stream: rebuildDetail.stream,
                    );
                  }
                  return null;
                },
              );

              image = HeroWidget(
                tag: item,
                slideType: SlideType.onlyImage,
                slidePagekey: slidePagekey,
                child: image,
              );

              image = GestureDetector(
                child: image,
                onTap: () {
    
    
                  slidePagekey.currentState!.popPage();
                  Navigator.pop(context);
                },
              );

              return image;
            },
            itemCount: widget.pics!.length,
            onPageChanged: (int index) {
    
    
              _currentIndex = index;
              rebuildIndex.add(index);
              if (_imageDetailY != 0) {
    
    
                _imageDetailY = 0;
                rebuildDetail.sink.add(_imageDetailY);
              }
              _showSwiper = true;
              rebuildSwiper.add(_showSwiper);
            },
          ),
          StreamBuilder<bool>(
            builder: (BuildContext c, AsyncSnapshot<bool> d) {
    
    
              if (d.data == null || !d.data!) {
    
    
                return Container();
              }

              return Positioned(
                top: statusBarHeight,
                left: 0.0,
                right: 0.0,
                child: MySwiperPlugin(widget.pics, _currentIndex, rebuildIndex),
              );
            },
            initialData: true,
            stream: rebuildSwiper.stream,
          )
        ],
      ),
    );

    result = ExtendedImageSlidePage(
      key: slidePagekey,
      child: result,
      slideAxis: SlideAxis.vertical,
      slideType: SlideType.onlyImage,
      slideScaleHandler: (
        Offset offset, {
    
    
        ExtendedImageSlidePageState? state,
      }) {
    
    
        return null;
      },
      slideOffsetHandler: (
        Offset offset, {
    
    
        ExtendedImageSlidePageState? state,
      }) {
    
    
        return null;
      },
      slideEndHandler: (
        Offset offset, {
    
    
        ExtendedImageSlidePageState? state,
        ScaleEndDetails? details,
      }) {
    
    
        return null;
      },
      onSlidingPage: (ExtendedImageSlidePageState state) {
    
    
        ///you can change other widgets' state on page as you want
        ///base on offset/isSliding etc
        //var offset= state.offset;
        final bool showSwiper = !state.isSliding;
        if (showSwiper != _showSwiper) {
    
    
          // do not setState directly here, the image state will change,
          // you should only notify the widgets which are needed to change
          // setState(() {
    
    
          // _showSwiper = showSwiper;
          // });

          _showSwiper = showSwiper;
          rebuildSwiper.add(_showSwiper);
        }
      },
    );

    return result;
  }

  
  void dispose() {
    
    
    rebuildIndex.close();
    rebuildSwiper.close();
    rebuildDetail.close();
    _doubleClickAnimationController.dispose();
    _slideEndAnimationController.dispose();
    clearGestureDetailsCache();
    //cancelToken?.cancel();
    super.dispose();
  }

  
  void initState() {
    
    
    super.initState();
    _currentIndex = widget.index;
    _doubleClickAnimationController = AnimationController(
        duration: const Duration(milliseconds: 150), vsync: this);

    _slideEndAnimationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 150),
    );
    _slideEndAnimationController.addListener(() {
    
    
      _imageDetailY = _slideEndAnimation.value;
      if (_imageDetailY == 0) {
    
    
        _showSwiper = true;
        rebuildSwiper.add(_showSwiper);
      }
      rebuildDetail.sink.add(_imageDetailY);
    });
  }
}

class MySwiperPlugin extends StatelessWidget {
    
    
  const MySwiperPlugin(this.pics, this.index, this.reBuild);

  final List<String>? pics;
  final int? index;
  final StreamController<int> reBuild;

  
  Widget build(BuildContext context) {
    
    
    return StreamBuilder<int>(
      builder: (BuildContext context, AsyncSnapshot<int> data) {
    
    
        return DefaultTextStyle(
          style: const TextStyle(color: Colors.blue),
          child: Container(
            height: 50.0,
            width: double.infinity,
            // color: Colors.grey.withOpacity(0.2),
            child: Row(
              children: <Widget>[
                Container(
                  width: 10.0,
                ),
                Text(
                  '${
      
      data.data! + 1}',
                ),
                Text(
                  ' / ${
      
      pics!.length}',
                ),
                const SizedBox(
                  width: 10.0,
                ),
                const SizedBox(
                  width: 10.0,
                ),
                if (!kIsWeb)
                  GestureDetector(
                    child: Container(
                      padding: const EdgeInsets.only(right: 10.0),
                      alignment: Alignment.center,
                      child: const Text(
                        'Save',
                        style: TextStyle(fontSize: 16.0, color: Colors.blue),
                      ),
                    ),
                    onTap: () {
    
    
                      // saveNetworkImageToPhoto(pics![index!].picUrl)
                      //     .then((bool done) {
    
    
                      //   showToast(done ? 'save succeed' : 'save failed',
                      //       position: const ToastPosition(
                      //           align: Alignment.topCenter));
                      // });
                    },
                  ),
              ],
            ),
          ),
        );
      },
      initialData: index,
      stream: reBuild.stream,
    );
  }
}

class ImageDetailInfo {
    
    
  ImageDetailInfo({
    
    
    required this.imageDRect,
    required this.pageSize,
    required this.imageInfo,
  });

  final GlobalKey<State<StatefulWidget>> key = GlobalKey<State>();

  final Rect imageDRect;

  final Size pageSize;

  final ImageInfo imageInfo;

  double? _maxImageDetailY;

  double get imageBottom => imageDRect.bottom - 20;

  double get maxImageDetailY {
    
    
    try {
    
    
      //
      return _maxImageDetailY ??= max(
          key.currentContext!.size!.height - (pageSize.height - imageBottom),
          0.1);
    } catch (e) {
    
    
      //currentContext is not ready
      return 100.0;
    }
  }
}

util during use

import 'package:extended_image/extended_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';

///
///  create by zmtzawqlp on 2020/1/31
///
double? initScale({
    
    
  required Size imageSize,
  required Size size,
  double? initialScale,
}) {
    
    
  final double n1 = imageSize.height / imageSize.width;
  final double n2 = size.height / size.width;
  if (n1 > n2) {
    
    
    final FittedSizes fittedSizes =
        applyBoxFit(BoxFit.contain, imageSize, size);
    //final Size sourceSize = fittedSizes.source;
    final Size destinationSize = fittedSizes.destination;
    return size.width / destinationSize.width;
  } else if (n1 / n2 < 1 / 4) {
    
    
    final FittedSizes fittedSizes =
        applyBoxFit(BoxFit.contain, imageSize, size);
    //final Size sourceSize = fittedSizes.source;
    final Size destinationSize = fittedSizes.destination;
    return size.height / destinationSize.height;
  }

  return initialScale;
}

Effect video

6. Summary

Flutter development practice-hero implements the image preview function extend_image. The description may not be accurate, please forgive me.

Study and record, keep improving every day.

Guess you like

Origin blog.csdn.net/gloryFlow/article/details/134011464