Home > database >  Flutter - How to create List of zoomable images?
Flutter - How to create List of zoomable images?

Time:08-30

I'm trying to create a list of zoomable images inside the list view but the problem is when the user tries to zoom in the layout becomes confusing and laggy because the device can't detect if the user wants to scroll the list or if he just wants to zoom in!

I want to do the same as the Instagram multi-picture in one post (pinch zooming).

here is the code, i'm writing this in Sliver to adapter because it's a child of custom scroll view.

             SliverToBoxAdapter(
            child: FutureBuilder<ProductDataModel>(
              future: Provider.of<ProductViewModel>(context, listen: false)
                  .getProductDetails(context, widget.productId),
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return const Center(
                    child: Text('Loading...'),
                  );
                } else if (snapshot.hasData) {
                  ProductDataModel? productData = snapshot.data;

                  /// just to filter something

                  List<String> imagesPath = [];
                  snapshot.data!.product!.files!
                      .map((e) => imagesPath.add(e.path!))
                      .toList();

                  snapshot.data!.product!.options!.map((e) {
                    e.name == 'Color'
                        ? e.values!
                            .map((s) => imagesPath.add(s.optionImage!))
                            .toList()
                        : null;
                  }).toList();

                  /// start of the list view

                  return SizedBox(
                    height: 350.h,
                    child: ListView.builder(
                      itemCount: imagesPath.length,
                      physics: const BouncingScrollPhysics(),
                      scrollDirection: Axis.horizontal,
                      itemBuilder: (context, index) {
                        return ZoomOverlay(
                          twoTouchOnly: true,
                          minScale: 0.8,
                          maxScale: 4,
                          child: CachedNetworkImage(
                            alignment: Alignment.center,
                            width: ScreenUtil.defaultSize.width,
                            imageUrl: imagesPath[index],
                            progressIndicatorBuilder:
                                (context, url, downloadProgress) =>
                                    const DaraghmehShimmer(),
                            errorWidget: (context, url, error) =>
                                const Icon(Icons.error),
                          ),
                        );
                      },
                    ),
                  );
                }

                return const SizedBox();
              },
            ),
          ),

CodePudding user response:

You can use InteractiveViewer.

@override
Widget build(BuildContext context) {
  return Center(
    child: InteractiveViewer(
      panEnabled: false, // Set it to false
      boundaryMargin: EdgeInsets.all(100),
      minScale: 0.5,
      maxScale: 2,
      child: Image.network('https://res.cloudinary.com/demo/image/upload/v1312461204/sample.jpg',
        width: 400,
        height: 400,
        fit: BoxFit.cover,
      ),
    ),
  );
}

CodePudding user response:

ok, so after a long time of trying, i found the solution...

PinchZooming

class PinchZooming extends StatefulWidget {
final Widget child;
final double maxScale, minScale;
final Duration resetDuration;
final bool zoomEnabled;
final Function? onZoomStart, onZoomEnd;



const PinchZooming(
  {Key? key,
  required this.child,
  this.resetDuration = const Duration(milliseconds: 100),
  this.maxScale = 4.0,
  this.minScale = 1.0,
  this.zoomEnabled = true,
  this.onZoomStart,
  this.onZoomEnd})
  : assert(maxScale != 0 && minScale != 0 && maxScale > minScale,
        'Either min or max scale value equal zero or max scale is less 
 than min scale'),
    super(key: key);

@override
_PinchZoomingState createState() => _PinchZoomingState();
}

class _PinchZoomingState extends State<PinchZooming>
with SingleTickerProviderStateMixin {
final TransformationController _transformationController =
  TransformationController();

late Animation<Matrix4> _animation;

late AnimationController _controller;

OverlayEntry? _entry;

  @override
     void initState() {
   super.initState();
   _controller = AnimationController(
    duration: widget.resetDuration,
    vsync: this,
   );
    _animation = Matrix4Tween().animate(_controller);
    _controller
    .addListener(() => _transformationController.value = 
   _animation.value);
   _controller.addStatusListener((status) {
     if (status == AnimationStatus.completed) {
    removeOverlay();
   }
  });
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

void showOverlay(BuildContext context) {
  final RenderBox _renderBox = context.findRenderObject()! as RenderBox;
  final Offset _offset = _renderBox.localToGlobal(Offset.zero);
  removeOverlay();
  _entry = OverlayEntry(
    builder: (c) => Stack(
    children: [
      Positioned.fill(
          child:
              Opacity(opacity: 0.5, child: Container(color: 
      Colors.black))),
      Positioned(
        left: _offset.dx,
        top: _offset.dy,
        child: InteractiveViewer(
          minScale: widget.minScale,
          clipBehavior: Clip.none,
          scaleEnabled: widget.zoomEnabled,
          maxScale: widget.maxScale,
          panEnabled: false,
          onInteractionStart: (ScaleStartDetails details) {
            if (details.pointerCount < 2) return;
            if (_entry == null) {
              showOverlay(context);
            }
          },
          onInteractionEnd: (_) => restAnimation(),
          transformationController: _transformationController,
          child: widget.child,
        ),
      ),
    ],
  ),
);
     final OverlayState? _overlay = Overlay.of(context);
    _overlay!.insert(_entry!);
  }

   void removeOverlay() {
     _entry?.remove();
    _entry = null;
    }

    void restAnimation() {
     _animation = Matrix4Tween(
        begin: _transformationController.value, end: Matrix4.identity())
         .animate(
        CurvedAnimation(parent: _controller, curve: Curves.easeInBack));
     _controller.forward(from: 0);
    }

   @override
   Widget build(BuildContext context) {
    return InteractiveViewer(
    child: widget.child,
     clipBehavior: Clip.none,
     minScale: widget.minScale,
     scaleEnabled: widget.zoomEnabled,
      maxScale: widget.maxScale,
     panEnabled: false,
     onInteractionStart: (ScaleStartDetails details) {
    if (details.pointerCount < 2) return;
    if (_entry == null) {
      showOverlay(context);
    }
    if (widget.onZoomStart != null) {
      widget.onZoomStart!();
    }
  },
  onInteractionUpdate: (details) {
    if (_entry == null) return;
    _entry!.markNeedsBuild();
  },
  onInteractionEnd: (details) {
    if (details.pointerCount != 1) return;
    restAnimation();
    if (widget.onZoomEnd != null) {
      widget.onZoomEnd!();
      }
  },
     transformationController: _transformationController,
   );
  }
 }

TouchCountRecognizer

   class TouchCountRecognizer extends OneSequenceGestureRecognizer {
   TouchCountRecognizer(this.onMultiTouchUpdated);

   Function(bool) onMultiTouchUpdated;
   int touchcount = 0;

   @override
   void addPointer(PointerDownEvent event) {
   startTrackingPointer(event.pointer);
    if (touchcount < 1) {
    //resolve(GestureDisposition.rejected);
    //_p = event.pointer;

    onMultiTouchUpdated(false);
     } else {
     onMultiTouchUpdated(true);
     //resolve(GestureDisposition.accepted);
     }
     touchcount  ;
    }

    @override
    String get debugDescription => 'touch count recognizer';

    @override
    void didStopTrackingLastPointer(int pointer) {}

   @override
   void handleEvent(PointerEvent event) {
    if (!event.down) {
     touchcount--;
      if (touchcount < 1) {
       onMultiTouchUpdated(false);
     }
    }  
   }
 }

then i companied these two classes together like this...

return SizedBox(
                    height: 350.h,
                    child: Consumer<ProductViewModel>(
                      builder: (_, state, child) => RawGestureDetector(
                        gestures: <Type, GestureRecognizerFactory>{
                          TouchCountRecognizer:
                              GestureRecognizerFactoryWithHandlers<
                                  TouchCountRecognizer>(
                            () => TouchCountRecognizer(
                                state.onMultiTouchUpdated),
                            (TouchCountRecognizer instance) {},
                          ),
                        },
                        child: NotificationListener(
                          onNotification: (notification) {
                            if (notification is ScrollNotification) {
                              WidgetsBinding.instance
                                  .addPostFrameCallback((timeStamp) {
                                state.scrolling = true;

                                if (state.scrolling == true &&
                                    state.multiTouch == false) {
                                  state.stopZoom();
                                } else if (state.scrolling == false &&
                                    state.multiTouch == true) {
                                  state.stopZoom();
                                } else if (state.scrolling == true &&
                                    state.multiTouch == true) {
                                  state.startZoom();
                                }
                              });
                            }
                            if (notification is ScrollUpdateNotification) {}
                            if (notification is ScrollEndNotification) {
                              WidgetsBinding.instance
                                  .addPostFrameCallback((timeStamp) {
                                state.scrolling = false;
                                if (state.scrolling == true &&
                                    state.multiTouch == false) {
                                  state.stopZoom();
                                } else if (state.scrolling == false &&
                                    state.multiTouch == true) {
                                  state.stopZoom();
                                } else if (state.scrolling == true &&
                                    state.multiTouch == true) {
                                  state.startZoom();
                                }
                              });
                            }

                            return state.scrolling;
                          },
                          child: ListView.builder(
                            shrinkWrap: false,
                            physics: state.imagePagerScrollPhysics,
                            itemCount: imagesPath.length,
                            scrollDirection: Axis.horizontal,
                            itemBuilder: (context, index) {
                              return PinchZooming(
                                zoomEnabled: state.isZoomEnabled,
                                onZoomStart: () => state.startZoom(),
                                onZoomEnd: () => state.stopZoom(),
                                child: CachedNetworkImage(
                                  alignment: Alignment.center,
                                  width: ScreenUtil.defaultSize.width,
                                  imageUrl: imagesPath[index],
                                  progressIndicatorBuilder:
                                      (context, url, downloadProgress) =>
                                          const DaraghmehShimmer(),
                                  errorWidget: (context, url, error) =>
                                      const Icon(Icons.error),
                                ),
                              );
                            },
                          ),
                        ),
                      ),
                    ),
                  );
  • Related