Flutter&OpenHarmony商城App图片预览组件开发

前言

图片预览是商城应用中查看商品详情图、评价图等场景的重要功能。用户需要能够放大查看图片细节、左右滑动切换图片、双击缩放等操作。一个设计良好的图片预览组件需要提供流畅的手势交互和清晰的图片展示效果。本文将详细介绍如何在Flutter和OpenHarmony平台上开发图片预览组件。

图片预览的设计需要考虑手势的自然性和图片的加载性能。用户期望能够像操作实体照片一样自然地缩放和移动图片,同时大图的加载不应该造成明显的等待。通过合理的手势处理和图片缓存策略,我们可以为用户提供流畅的图片浏览体验。

Flutter图片预览基础结构

首先定义图片预览组件的基础结构:

dart 复制代码
class ImagePreview extends StatefulWidget {
  final List<String> images;
  final int initialIndex;
  final VoidCallback? onClose;

  const ImagePreview({
    Key? key,
    required this.images,
    this.initialIndex = 0,
    this.onClose,
  }) : super(key: key);

  @override
  State<ImagePreview> createState() => _ImagePreviewState();
}

class _ImagePreviewState extends State<ImagePreview> {
  late PageController _pageController;
  late int _currentIndex;

  @override
  void initState() {
    super.initState();
    _currentIndex = widget.initialIndex;
    _pageController = PageController(initialPage: _currentIndex);
  }

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

ImagePreview组件接收图片列表和初始显示索引。PageController控制图片的左右滑动切换,initialPage设置初始显示的图片。_currentIndex记录当前显示的图片索引,用于更新页码指示器。dispose方法中释放控制器资源。这种设计支持多图预览和指定初始图片的场景。

预览界面构建:

dart 复制代码
@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Colors.black,
    body: Stack(
      children: [
        _buildImageViewer(),
        _buildTopBar(),
        _buildBottomIndicator(),
      ],
    ),
  );
}

Widget _buildImageViewer() {
  return PageView.builder(
    controller: _pageController,
    itemCount: widget.images.length,
    onPageChanged: (index) {
      setState(() {
        _currentIndex = index;
      });
    },
    itemBuilder: (context, index) {
      return InteractiveViewer(
        minScale: 1.0,
        maxScale: 4.0,
        child: Center(
          child: Image.network(
            widget.images[index],
            fit: BoxFit.contain,
            loadingBuilder: (context, child, progress) {
              if (progress == null) return child;
              return Center(
                child: CircularProgressIndicator(
                  value: progress.expectedTotalBytes != null
                    ? progress.cumulativeBytesLoaded / 
                      progress.expectedTotalBytes!
                    : null,
                  color: Colors.white,
                ),
              );
            },
          ),
        ),
      );
    },
  );
}

Scaffold使用黑色背景,营造沉浸式的图片浏览环境。Stack层叠图片查看器、顶部栏和底部指示器。PageView.builder实现图片的左右滑动切换,onPageChanged更新当前索引。InteractiveViewer提供缩放和平移手势支持,minScale和maxScale限制缩放范围。loadingBuilder显示图片加载进度,提供清晰的加载反馈。

顶部操作栏

dart 复制代码
Widget _buildTopBar() {
  return Positioned(
    top: 0,
    left: 0,
    right: 0,
    child: Container(
      padding: EdgeInsets.only(
        top: MediaQuery.of(context).padding.top + 10,
        left: 16,
        right: 16,
        bottom: 10,
      ),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [
            Colors.black.withOpacity(0.5),
            Colors.transparent,
          ],
        ),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          GestureDetector(
            onTap: () {
              Navigator.of(context).pop();
              widget.onClose?.call();
            },
            child: const Icon(
              Icons.close,
              color: Colors.white,
              size: 24,
            ),
          ),
          Text(
            '${_currentIndex + 1}/${widget.images.length}',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 16,
            ),
          ),
          GestureDetector(
            onTap: _saveImage,
            child: const Icon(
              Icons.download,
              color: Colors.white,
              size: 24,
            ),
          ),
        ],
      ),
    ),
  );
}

顶部操作栏包含关闭按钮、页码指示和保存按钮。Container使用渐变背景,从半透明黑色渐变到透明,既不遮挡图片又保证按钮可见。padding考虑了状态栏高度,确保内容不被遮挡。页码显示当前图片位置和总数,帮助用户了解浏览进度。关闭按钮返回上一页,保存按钮将图片保存到相册。

底部指示器

dart 复制代码
Widget _buildBottomIndicator() {
  if (widget.images.length <= 1) {
    return const SizedBox.shrink();
  }
  
  return Positioned(
    bottom: MediaQuery.of(context).padding.bottom + 20,
    left: 0,
    right: 0,
    child: Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: List.generate(
        widget.images.length,
        (index) => Container(
          width: _currentIndex == index ? 16 : 6,
          height: 6,
          margin: const EdgeInsets.symmetric(horizontal: 3),
          decoration: BoxDecoration(
            color: _currentIndex == index
              ? Colors.white
              : Colors.white.withOpacity(0.5),
            borderRadius: BorderRadius.circular(3),
          ),
        ),
      ),
    ),
  );
}

底部指示器显示当前图片在列表中的位置。当只有一张图片时不显示指示器。当前图片的指示点宽度更大呈椭圆形,其他指示点呈圆形。选中状态使用纯白色,未选中使用半透明白色。Row居中排列所有指示点,margin设置点之间的间距。这种设计让用户能够直观地了解当前位置和总图片数量。

双击缩放功能

dart 复制代码
class ZoomableImage extends StatefulWidget {
  final String imageUrl;

  const ZoomableImage({
    Key? key,
    required this.imageUrl,
  }) : super(key: key);

  @override
  State<ZoomableImage> createState() => _ZoomableImageState();
}

class _ZoomableImageState extends State<ZoomableImage> 
    with SingleTickerProviderStateMixin {
  final TransformationController _controller = TransformationController();
  late AnimationController _animationController;
  Animation<Matrix4>? _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 200),
    );
  }

  void _handleDoubleTap() {
    final currentScale = _controller.value.getMaxScaleOnAxis();
    
    Matrix4 targetMatrix;
    if (currentScale > 1.5) {
      targetMatrix = Matrix4.identity();
    } else {
      targetMatrix = Matrix4.identity()..scale(2.5);
    }
    
    _animation = Matrix4Tween(
      begin: _controller.value,
      end: targetMatrix,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeOut,
    ));
    
    _animation!.addListener(() {
      _controller.value = _animation!.value;
    });
    
    _animationController.forward(from: 0);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onDoubleTap: _handleDoubleTap,
      child: InteractiveViewer(
        transformationController: _controller,
        minScale: 1.0,
        maxScale: 4.0,
        child: Image.network(
          widget.imageUrl,
          fit: BoxFit.contain,
        ),
      ),
    );
  }
}

ZoomableImage组件实现双击缩放功能。TransformationController控制图片的变换矩阵,AnimationController实现缩放动画。_handleDoubleTap方法判断当前缩放比例,如果已放大则恢复原始大小,否则放大到2.5倍。Matrix4Tween创建矩阵动画,CurvedAnimation添加缓动效果。这种设计让用户可以通过双击快速切换放大和原始状态。

OpenHarmony图片预览实现

typescript 复制代码
@Component
struct ImagePreview {
  @State currentIndex: number = 0
  @Prop images: string[] = []
  @Prop initialIndex: number = 0
  private onClose: () => void = () => {}
  private swiperController: SwiperController = new SwiperController()

  aboutToAppear() {
    this.currentIndex = this.initialIndex
  }

  build() {
    Stack() {
      Swiper(this.swiperController) {
        ForEach(this.images, (imageUrl: string) => {
          this.ZoomableImage(imageUrl)
        })
      }
      .index(this.initialIndex)
      .indicator(false)
      .loop(false)
      .onChange((index: number) => {
        this.currentIndex = index
      })
      
      this.TopBar()
      this.BottomIndicator()
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Black)
  }
}

OpenHarmony的图片预览使用Swiper组件实现图片切换。@State装饰的currentIndex管理当前索引,aboutToAppear生命周期方法初始化索引值。Swiper的index属性设置初始显示图片,indicator设为false隐藏默认指示器,loop设为false禁用循环滑动。Stack层叠图片、顶部栏和底部指示器。这种实现方式与Flutter版本结构一致。

可缩放图片ArkUI实现:

typescript 复制代码
@Builder
ZoomableImage(imageUrl: string) {
  Image(imageUrl)
    .width('100%')
    .height('100%')
    .objectFit(ImageFit.Contain)
    .gesture(
      PinchGesture()
        .onActionUpdate((event: GestureEvent) => {
          // 处理缩放手势
        })
    )
    .gesture(
      TapGesture({ count: 2 })
        .onAction(() => {
          // 处理双击缩放
        })
    )
}

@Builder装饰器定义了可缩放图片的构建方法。Image组件设置100%宽高和Contain适应模式。PinchGesture处理双指缩放手势,TapGesture配合count: 2处理双击手势。ArkUI的手势系统支持多种手势的组合使用,可以实现复杂的交互效果。

顶部栏ArkUI实现

typescript 复制代码
@Builder
TopBar() {
  Row() {
    Image($r('app.media.close'))
      .width(24)
      .height(24)
      .onClick(() => this.onClose())
    
    Text((this.currentIndex + 1) + '/' + this.images.length)
      .fontSize(16)
      .fontColor(Color.White)
    
    Image($r('app.media.download'))
      .width(24)
      .height(24)
      .onClick(() => this.saveImage())
  }
  .width('100%')
  .height(60)
  .padding({ left: 16, right: 16, top: 30 })
  .justifyContent(FlexAlign.SpaceBetween)
  .linearGradient({
    direction: GradientDirection.Bottom,
    colors: [['#80000000', 0], ['#00000000', 1]]
  })
  .position({ x: 0, y: 0 })
}

顶部栏使用Row水平排列关闭按钮、页码和保存按钮。justifyContent设为FlexAlign.SpaceBetween实现两端对齐。linearGradient设置渐变背景,从半透明黑色渐变到透明。position将顶部栏定位在Stack的顶部。onClick事件处理器分别处理关闭和保存操作。这种实现方式与Flutter版本的视觉效果一致。

图片保存功能

dart 复制代码
Future<void> _saveImage() async {
  try {
    final imageUrl = widget.images[_currentIndex];
    
    // 显示加载提示
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => const Center(
        child: CircularProgressIndicator(color: Colors.white),
      ),
    );
    
    // 下载图片
    final response = await http.get(Uri.parse(imageUrl));
    
    // 保存到相册
    final result = await ImageGallerySaver.saveImage(
      response.bodyBytes,
      quality: 100,
    );
    
    Navigator.of(context).pop();
    
    if (result['isSuccess']) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('图片已保存到相册')),
      );
    }
  } catch (e) {
    Navigator.of(context).pop();
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('保存失败,请重试')),
    );
  }
}

图片保存功能下载当前显示的图片并保存到设备相册。首先显示加载提示,然后使用http包下载图片数据,最后使用ImageGallerySaver保存到相册。保存成功或失败都会显示对应的提示信息。try-catch捕获可能的异常,确保用户得到明确的反馈。这种设计让用户可以方便地保存喜欢的商品图片。

图片预览入口

dart 复制代码
class ImagePreviewHelper {
  static void show(
    BuildContext context, {
    required List<String> images,
    int initialIndex = 0,
  }) {
    Navigator.of(context).push(
      PageRouteBuilder(
        opaque: false,
        pageBuilder: (context, animation, secondaryAnimation) {
          return FadeTransition(
            opacity: animation,
            child: ImagePreview(
              images: images,
              initialIndex: initialIndex,
            ),
          );
        },
      ),
    );
  }
}

ImagePreviewHelper工具类封装了图片预览的显示方法。PageRouteBuilder创建自定义路由,opaque设为false使背景透明。FadeTransition添加淡入动画效果,使预览界面的出现更加自然。调用者只需传入图片列表和初始索引即可显示预览界面。这种封装方式使图片预览的调用更加便捷。

总结

本文详细介绍了Flutter和OpenHarmony平台上图片预览组件的开发过程。图片预览作为商城应用的重要功能,其设计质量直接影响用户查看商品详情的体验。通过缩放手势、双击缩放、图片切换、保存功能等特性的合理设计,我们为用户提供了流畅的图片浏览体验。在实际项目中,还可以进一步添加图片分享、长按菜单、视频预览等功能。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
程序员Ctrl喵1 天前
异步编程:Event Loop 与 Isolate 的深层博弈
开发语言·flutter
前端不太难1 天前
Flutter 如何设计可长期维护的模块边界?
flutter
小蜜蜂嗡嗡1 天前
flutter列表中实现置顶动画
flutter
始持1 天前
第十二讲 风格与主题统一
前端·flutter
始持1 天前
第十一讲 界面导航与路由管理
flutter·vibecoding
始持1 天前
第十三讲 异步操作与异步构建
前端·flutter
新镜1 天前
【Flutter】 视频视频源横向、竖向问题
flutter
黄林晴1 天前
Compose Multiplatform 1.10 发布:统一 Preview、Navigation 3、Hot Reload 三箭齐发
android·flutter
Swift社区1 天前
Flutter 应该按功能拆,还是按技术层拆?
flutter
肠胃炎1 天前
树形选择器组件封装
前端·flutter