如何创建精彩照片库(Flutter)

问题描述 投票:0回答:2

gallery photo

如何制作这个ScrollView?

如何实现对角滚动?

我尝试使用diagnostic_scrollview 但失败了。

flutter
2个回答
1
投票

Wonderous 是开源的,您只需查看原始实现并尝试了解它是如何实现的。在代码中,所有方向的滑动都是由八路滑动检测器(EightWaySwipeDetector)处理的。

https://github.com/gskinnerTeam/flutter-wonderous-app/blob/main/lib/ui/screens/photo_gallery/photo_gallery.dart


0
投票

试试这个

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:sized_context/sized_context.dart';

// part 'widgets/_animated_cutout_overlay.dart';

class PhotoGallery extends StatefulWidget {
  const PhotoGallery({super.key, this.imageSize, required this.collectionId});
  final Size? imageSize;
  final String collectionId;
  // final WonderType wonderType;

  @override
  State<PhotoGallery> createState() => _PhotoGalleryState();
}

class _PhotoGalleryState extends State<PhotoGallery> {
  static const int _gridSize = 5;
  // Index starts in the middle of the grid (eg, 25 items, index will start at 13)
  int _index = ((_gridSize * _gridSize) / 2).round();
  Offset _lastSwipeDir = Offset.zero;
  final double _scale = 1;
  bool _skipNextOffsetTween = false;
  late Duration swipeDuration = const Duration(milliseconds: 200);
  final _photoIds = ValueNotifier<List<String>>([]);
  int get _imgCount => pow(_gridSize, 2).round();

  late final List<FocusNode> _focusNodes =
      List.generate(_imgCount, (index) => FocusNode());

  @override
  void initState() {
    super.initState();
    _initPhotoIds();
    _focusNodes[_index].requestFocus();
  }

  Future<void> _initPhotoIds() async {
    setState(() {
      _photoIds.value = [
        'https://picsum.photos/200?random=0',
        'https://picsum.photos/200?random=1',
        'https://picsum.photos/200?random=2',
        'https://picsum.photos/200?random=3',
        'https://picsum.photos/200?random=4',
        'https://picsum.photos/200?random=5',
        'https://picsum.photos/200?random=6',
        'https://picsum.photos/200?random=7',
        'https://picsum.photos/200?random=8',
        'https://picsum.photos/200?random=9',
        'https://picsum.photos/200?random=10',
        'https://picsum.photos/200?random=11',
        'https://picsum.photos/200?random=12',
        'https://picsum.photos/200?random=13',
        'https://picsum.photos/200?random=14',
        'https://picsum.photos/200?random=15',
        'https://picsum.photos/200?random=16',
        'https://picsum.photos/200?random=17',
        'https://picsum.photos/200?random=18',
        'https://picsum.photos/200?random=19',
        'https://picsum.photos/200?random=20',
        'https://picsum.photos/200?random=21',
        'https://picsum.photos/200?random=22',
        'https://picsum.photos/200?random=23',
        'https://picsum.photos/200?random=24',
        'https://picsum.photos/200?random=25',
        
      ];
    });
  }

  void _setIndex(int value, {bool skipAnimation = false}) {
    if (value < 0 || value >= _imgCount) return;
    _skipNextOffsetTween = skipAnimation;
    setState(() => _index = value);
    _focusNodes[value].requestFocus();
  }

  /// Determine the required offset to show the current selected index.
  /// index=0 is top-left, and the index=max is bottom-right.
  Offset _calculateCurrentOffset(double padding, Size size) {
    double halfCount = (_gridSize / 2).floorToDouble();
    Size paddedImageSize = Size(size.width + padding, size.height + padding);
    // Get the starting offset that would show the top-left image (index 0)
    final originOffset = Offset(
        halfCount * paddedImageSize.width, halfCount * paddedImageSize.height);
    // Add the offset for the row/col
    int col = _index % _gridSize;
    int row = (_index / _gridSize).floor();
    final indexedOffset =
        Offset(-paddedImageSize.width * col, -paddedImageSize.height * row);
    return originOffset + indexedOffset;
  }


  /// Converts a swipe direction into a new index
  void _handleSwipe(Offset dir) {
    // Calculate new index, y swipes move by an entire row, x swipes move one index at a time
    int newIndex = _index;
    if (dir.dy != 0) newIndex += _gridSize * (dir.dy > 0 ? -1 : 1);
    if (dir.dx != 0) newIndex += (dir.dx > 0 ? -1 : 1);
    // After calculating new index, exit early if we don't like it...
    if (newIndex < 0 || newIndex > _imgCount - 1) {
      return; // keep the index in range
    }
    if (dir.dx < 0 && newIndex % _gridSize == 0) {
      return; // prevent right-swipe when at right side
    }
    if (dir.dx > 0 && newIndex % _gridSize == _gridSize - 1) {
      return; // prevent left-swipe when at left side
    }
    _lastSwipeDir = dir;
    // AppHaptics.lightImpact();
    _setIndex(newIndex);
  }






  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<List<String>>(
        valueListenable: _photoIds,
        builder: (_, value, __) {
          if (value.isEmpty) {
            return const Center(child: CircularProgressIndicator());
          }
          Size imgSize = context.isLandscape
              ? Size(context.widthPx * .5, context.heightPx * .66)
              : Size(context.widthPx * .66, context.heightPx * .5);
          imgSize = (widget.imageSize ?? imgSize) * _scale;
          // Get transform offset for the current _index
          const padding = 16.0;
          var gridOffset = _calculateCurrentOffset(padding, imgSize);
          gridOffset += Offset(0, -context.mq.padding.top / 2);
          final offsetTweenDuration =
              _skipNextOffsetTween ? Duration.zero : swipeDuration;
          final cutoutTweenDuration =
              _skipNextOffsetTween ? Duration.zero : swipeDuration * .5;
          return _AnimatedCutoutOverlay(
            animationKey: ValueKey(_index),
            cutoutSize: imgSize,
            swipeDir: _lastSwipeDir,
            duration: cutoutTweenDuration,
            opacity: _scale == 1 ? .7 : .5,
            enabled: true,
            child: SafeArea(
              bottom: false,
              // Place content in overflow box, to allow it to flow outside the parent
              child: OverflowBox(
                maxWidth: _gridSize * imgSize.width + padding * (_gridSize - 1),
                maxHeight:
                    _gridSize * imgSize.height + padding * (_gridSize - 1),
                alignment: Alignment.center,
                // Detect swipes in order to change index
                child: EightWaySwipeDetector(
                  onSwipe: _handleSwipe,
                  threshold: 30,
                  // A tween animation builder moves from image to image based on current offset
                  child: TweenAnimationBuilder<Offset>(
                    tween: Tween(begin: gridOffset, end: gridOffset),
                    duration: offsetTweenDuration,
                    curve: Curves.easeOut,
                    builder: (_, value, child) =>
                        Transform.translate(offset: value, child: child),
                    child: FocusTraversalGroup(
                      //policy: OrderedTraversalPolicy(),
                      child: GridView.count(
                        physics: const NeverScrollableScrollPhysics(),
                        crossAxisCount: _gridSize,
                        childAspectRatio: imgSize.aspectRatio,
                        mainAxisSpacing: padding,
                        crossAxisSpacing: padding,
                        children: List.generate(_imgCount,
                            (i) => _buildImage(i, swipeDuration, imgSize)),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          );
        });
  }

  Widget _buildImage(int index, Duration swipeDuration, Size imgSize) {
    /// Bind to collectibles.statesById because we might need to rebuild if a collectible is found.
    return FocusTraversalOrder(
      order: NumericFocusOrder(index.toDouble()),
      child: ValueListenableBuilder(
          valueListenable: _photoIds,
          builder: (_, __, ___) {
            bool isSelected = index == _index;
            final imgUrl = _photoIds.value[index];
            final photoWidget = TweenAnimationBuilder<double>(
              duration: swipeDuration,
              curve: Curves.easeOut,
              tween: Tween(begin: 1, end: 1),
              builder: (_, value, child) =>
                  Transform.scale(scale: value, child: child),
              child: Image.network(
                imgUrl,
                fit: BoxFit.cover,
                width: imgSize.width,
                height: imgSize.height,
              ).animate().fade(),
            );
            return isSelected
                ? Center(
                    child: Container(
                        decoration: BoxDecoration(
                          border: Border.all(color: Colors.white, width: 2),
                          borderRadius: BorderRadius.circular(10),
                        ),
                        clipBehavior: Clip.hardEdge,
                        child: photoWidget),
                  )
                : Container(
                    // Suggested code may be subject to a license. Learn more: ~LicenseLog:1894125391.
                    color: Colors.grey,
                    child: photoWidget,
                  );
          }),
    );
    // return Container(
    //   color: Colors.red,
    // );
  }
}

class _AnimatedCutoutOverlay extends StatelessWidget {
  const _AnimatedCutoutOverlay({
    required this.child,
    required this.cutoutSize,
    required this.animationKey,
    this.duration,
    required this.swipeDir,
    required this.opacity,
    required this.enabled,
  });
  final Widget child;
  final Size cutoutSize;
  final Key animationKey;
  final Offset swipeDir;
  final Duration? duration;
  final double opacity;
  final bool enabled;
  @override
  Widget build(BuildContext context) {
    if (!enabled) return child;
    return Stack(
      children: [
        child,
        Animate(
          effects: [
            CustomEffect(
                builder: _buildAnimatedCutout,
                curve: Curves.easeOut,
                duration: duration)
          ],
          key: animationKey,
          onComplete: (c) => c.reverse(),
          child: IgnorePointer(
              child: Container(color: Colors.black.withOpacity(opacity))),
        ),
      ],
    );
  }

  /// Scales from 1 --> (1 - scaleAmt) --> 1
  Widget _buildAnimatedCutout(BuildContext context, double anim, Widget child) {
    // controls how much the center cutout will shrink when changing images
    const scaleAmt = .25;
    final size = Size(
      cutoutSize.width * (1 - scaleAmt * anim * swipeDir.dx.abs()),
      cutoutSize.height * (1 - scaleAmt * anim * swipeDir.dy.abs()),
    );
    return ClipPath(clipper: _CutoutClipper(size), child: child);
  }
}

/// Creates an overlay with a hole in the middle of a certain size.
class _CutoutClipper extends CustomClipper<Path> {
  _CutoutClipper(this.cutoutSize);
  final Size cutoutSize;

  @override
  Path getClip(Size size) {
    double padX = (size.width - cutoutSize.width) / 2;
    double padY = (size.height - cutoutSize.height) / 2;

    return Path.combine(
      PathOperation.difference,
      Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)),
      Path()
        ..addRRect(
          RRect.fromLTRBR(
            padX,
            padY,
            size.width - padX,
            size.height - padY,
            const Radius.circular(6),
          ),
        )
        ..close(),
    );
  }

  @override
  bool shouldReclip(_CutoutClipper oldClipper) =>
      oldClipper.cutoutSize != cutoutSize;
}

class EightWaySwipeDetector extends StatefulWidget {
  const EightWaySwipeDetector(
      {super.key,
      required this.child,
      this.threshold = 50,
      required this.onSwipe});
  final Widget child;
  final double threshold;
  final void Function(Offset dir)? onSwipe;

  @override
  State<EightWaySwipeDetector> createState() => _EightWaySwipeDetectorState();
}

class _EightWaySwipeDetectorState extends State<EightWaySwipeDetector> {
  Offset _startPos = Offset.zero;
  Offset _endPos = Offset.zero;
  bool _isSwiping = false;

  void _resetSwipe() {
    _startPos = _endPos = Offset.zero;
    _isSwiping = false;
  }

  void _maybeTriggerSwipe() {
    // Exit early if we're not currently swiping
    if (_isSwiping == false) return;
    // Get the distance of the swipe
    Offset moveDelta = _endPos - _startPos;
    final distance = moveDelta.distance;
    // Trigger swipe if threshold has been exceeded, if threshold is < 1, use 1 as a minimum value.
    if (distance >= max(widget.threshold, 1)) {
      // Normalize the dx/dy values between -1 and 1
      moveDelta /= distance;
      // Round the dx/dy values to snap them to -1, 0 or 1, creating an 8-way directional vector.
      Offset dir = Offset(
        moveDelta.dx.roundToDouble(),
        moveDelta.dy.roundToDouble(),
      );
      widget.onSwipe?.call(dir);
      _resetSwipe();
    }
  }

  void _handleSwipeStart(d) {
    _isSwiping = true;
    _startPos = _endPos = d.localPosition;
  }

  void _handleSwipeUpdate(d) {
    _endPos = d.localPosition;
    _maybeTriggerSwipe();
  }

  void _handleSwipeEnd(d) {
    _maybeTriggerSwipe();
    _resetSwipe();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        behavior: HitTestBehavior.translucent,
        onPanStart: _handleSwipeStart,
        onPanUpdate: _handleSwipeUpdate,
        onPanCancel: _resetSwipe,
        onPanEnd: _handleSwipeEnd,
        child: widget.child);
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.