Flutter开发笔记 —— 图像缩略图功能实战

前言

这是在掘金发布的第一篇文章,大家在做图像浏览或部分关于图像的项目时,难免会遇到缩略图的相关功能,特地写了一个demo给大家进行分享,文笔一般,欢迎回复指正!。

插件应用列表

  • scrollable_positioned_list: ^0.3.8 (滑动处理)
  • flukit: ^3.0.1 (用来拿视图size)

效果图

功能分析

视图主要以到 底图 + 侧边栏 + 动画三个方面,难度不大,可以自己自定义

视图相关代码

dart 复制代码
import 'dart:math';

import 'package:flukit/flukit.dart';
import 'package:flutter/material.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
/**
 * @author Marinda
 * @date 2023/12/7 14:27
 * @description 缩略图实战
 */
class ThumbComponentWidget extends StatefulWidget{

  const ThumbComponentWidget({super.key});


  @override
  State<StatefulWidget> createState() {
    return ThumbState();
  }

}

extension on String{
  String get assets{
    return "assets/$this";
  }
}

/**
 * @author Marinda
 * @date 2023/12/7 14:28
 * @description 缩略图State
 */
class ThumbState extends State<ThumbComponentWidget> with TickerProviderStateMixin{
  //平移
  late Animation<double> translationAnimation;
  late AnimationController controller;
  //底图路径
  String src =  "logo.jpg".assets;
  //缩略图列表
  List<String> imgList = ["logo.jpg".assets,"img2.jpg".assets,"img3.jpg".assets,"img4.jpg".assets,"logo.jpg".assets,"img2.jpg".assets,"img4.jpg".assets];
  @override
  void initState() {

    controller = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 300),
      reverseDuration: Duration(milliseconds: 300)
    );
    translationAnimation = Tween<double>(begin: 0.0,end: 200).animate(controller);
    // TODO: implement initState
    super.initState();
  }

  void readerLayout(RenderAfterLayout ral){
    screenSize = ral.size;
  }

  @override
  void dispose() {
    controller.dispose();
    // TODO: implement dispose
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    var size = MediaQuery.of(context).size;
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blue,
        title: const Text(
            "缩略图应用实战",
          style: TextStyle(
            color: Colors.white,
            fontSize: 20
          ),
        ),
      ),
      body: Container(
        child: Stack(
          children: [
          //  底图显示
            Positioned(
              left: 0,
              right: 0,
              child: Container(
                width: size.width,
                height:size.height,
                decoration: BoxDecoration(
                  image: DecorationImage(
                    image: Image.asset(
                      "${src}",
                    ).image,
                    fit: BoxFit.cover,
                    filterQuality: FilterQuality.high
                  )
                ),
              ),
            ),
            //  遮罩层的缩略图
              Positioned(
                top: 0,
                left: 0,
                child: Visibility(
                    child: InkWell(
                      child: Container(
                        width: size.width,
                        height: size.height,
                        color: Colors.black.withOpacity(.5),
                      ),
                      onTap: (){
                        controller.reverse();
                      },
                    ),
                  visible: true,
                ),
              ),
            //缩略图按钮
            Positioned(
              left: 0,
              top: size.height / 3,
              child: Container(
                child: Column(
                  children: [
                    InkWell(
                      child: Container(
                        decoration: BoxDecoration(
                          color: Colors.grey,
                          borderRadius: BorderRadius.only(topRight:Radius.circular(5),bottomRight: Radius.circular(5))
                        ),
                        padding: EdgeInsets.only(left: 10,bottom: 5,top: 5,right: 10),
                        child: Column(
                          children: [
                            Container(
                              margin: EdgeInsets.only(bottom: 5),
                              child: SizedBox(
                                width: 30,
                                height: 30,
                                child: Image.asset(
                                  "assets/thumb.png",
                                  fit: BoxFit.fill,
                                  color: Colors.white,
                                ),
                              ),
                            ),
                            //文字
                            Container(
                              child: Text(
                                  "缩略图",
                                style: TextStyle(
                                  color: Colors.white,
                                  fontSize: 13
                                ),
                              ),
                            )
                          ],
                        ),
                      ),
                      onTap: (){
                        controller.forward();
                      },
                    ),
                  ],
                )
              ),
            ),

            //  遮罩层的缩略图
            Positioned(
              top: 0,
              left: 0,
              child: AnimatedBuilder(
                animation: controller,
                builder: (BuildContext context, Widget? child) {
                  return Container(
                    width: translationAnimation.value,
                    height: size.height,
                    color: Colors.white,
                    padding: EdgeInsets.all(20),
                    child: Stack(
                      children: [
                        //构建缩略图
                        Container(
                          color: Colors.white,
                          child: ScrollablePositionedList.builder(
                            itemCount: imgList.length,
                            physics: BouncingScrollPhysics(),
                            itemBuilder: (BuildContext context, int index) {
                              var element = imgList[index];
                              return AfterLayout(
                                callback: readerLayout,
                                child: InkWell(
                                  child: Container(
                                    // height: 100,
                                    padding: EdgeInsets.all(5),
                                    margin: EdgeInsets.only(bottom: 20),
                                    decoration: BoxDecoration(
                                      border: Border.all(color: Colors.grey.withOpacity(.5),width: 1),
                                    ),
                                    child: Image.asset(
                                      element,
                                      fit: BoxFit.fill,
                                    ),
                                  ),
                                  onTap: ()=>changeImage(index),
                                ),
                              );
                            },
                          ),
                        ),
                        Positioned(
                          right: 10,
                          top: size.height /2.5,
                          child: InkWell(
                            child: SizedBox(
                              width: 30,
                              height: 30,
                              child: Transform.rotate(
                                angle: pi * 1.5,
                                child: Image.asset(
                                    "assets/hide.png",
                                  fit: BoxFit.fill,
                                ),
                              ),
                            ),
                            onTap: (){
                              controller.reverse();
                            },
                          ),
                        )

                      ],
                    ),
                  );
                },
              ),
            )
          ],
        ),
      ),
    );
  }

}

关于列表渲染这一块没有使用SingleChildScroll 或者ListView

而是使用了ScrollablePositionedList作为渲染父组件

scrollable_positioned_list插件应用

这是一款很优秀的插件,本文以分享为主给大家简单解析。

插件地址:pub.dev/packages/sc...

我们来简单讲讲为什么使用这个插件

根据官方插件文献可以得知相较于传统滑动控制处理。

该插件中拥有可以根据索引页跳转相对位置偏移量跳转,传统方式还需要计算position点位信息,相比较为麻烦,感兴趣的可以自己去插件文献看看。

滑动控制器

我们接下来会使用到以下两个控制器做滑动跳转处理。

  • ItemScrollController (项目滑动控制器)
  • ScrollOffsetController (滑动偏移量控制器)

ItemScrollController 主要以索引号进行跳转控制
ScrollOffsetContainer 主要以相对位置偏移量做跳转控制

滑动监听器

接下来是配套的滑动监听器

  • ItemPositionsListener (监听滑动后的可视视图列表)
  • ScrollOffsetListener (监听滑动后的具体滑动值)

ItemPositionsListener 主要监听以滑动后当前可视范围内的所有项目点位信息列表 ScrollOffsetListener 主要监听当前滑动的滑动总值,用来方便做点位计算

应用

使用到的相关控制器和监听器讲完了,我们来看看具体实现方法

定义相关控制器和监听器以及变量

dart 复制代码
List<ItemPosition> visibleItemViewList = [];
Size viewScreenSize = Size.zero;
double scrollDetails = 0;
Size screenSize = Size.zero;
ItemScrollController itemScrollController = ItemScrollController();
final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();
ScrollOffsetListener scrollOffsetListener = ScrollOffsetListener.create();
ScrollOffsetController scrollOffsetController = ScrollOffsetController();

绑定控制器和监听器

dart 复制代码
ScrollablePositionedList.builder(
  itemCount: imgList.length,
  itemPositionsListener: itemPositionsListener,
  scrollOffsetListener: scrollOffsetListener,
  scrollOffsetController: scrollOffsetController,
  physics: BouncingScrollPhysics(),
  itemScrollController: itemScrollController,
  itemBuilder: (BuildContext context, int index) {
    var element = imgList[index];
    return AfterLayout(
      callback: readerLayout,
      child: InkWell(
        child: Container(
          // height: 100,
          padding: EdgeInsets.all(5),
          margin: EdgeInsets.only(bottom: 20),
          decoration: BoxDecoration(
            border: Border.all(color: Colors.grey.withOpacity(.5),width: 1),
          ),
          child: Image.asset(
            element,
            fit: BoxFit.fill,
          ),
        ),
        onTap: ()=>changeImage(index),
      ),
    );
  },
)

initState中进行初始化控制

dart 复制代码
//做滑动视图监听处理
itemPositionsListener.itemPositions.addListener(() {
   //储存可视范围内的点位信息列表
   var list = itemPositionsListener.itemPositions.value.toList();
   visibleItemViewList = list;
});
//滑动点位值
scrollOffsetListener.changes.listen((event) {
    scrollDetails += event;
});

目前我们已经拿到了可视范围内的点位列表&滑动总值,接下来处理切换图像

changeImage方法

dart 复制代码
/*
 * @author Marinda
 * @date 2023/12/7 15:42
 * @description 修改图像
 */
changeImage(int index){
  var element =imgList[index];
  var target = visibleItemViewList.firstWhere((element) => element.index == index);
  //不可见底部内容
  if(target.itemTrailingEdge >=1.0){
    //边距
    double step = 30;
    double value = screenSize.height - step;
    scrollOffsetController.animateScroll(offset: value, duration: Duration(milliseconds: 300));
  }
  //边界处理
  if(target.itemLeadingEdge <=0.0){
    itemScrollController.scrollTo(index: index, duration: Duration(milliseconds: 300));
  }
  src = element;
  setState(() {

  });
}

ItemPosition(项目点位)下的两个参数值简单讲讲

  • itemTrailingEdge (在可视区域范围内尾部可视的比例)
  • itemLeadingEdge (在可视区域范围内首部可视的比例)

感兴趣的可以自己看看注释,这是我所理解下来的意思,不对欢迎指正!

上文判断意思:

  • 如果 itemTrailingEdge值大于等于1了,则当前只能看到尾部Item的一半或者少许
  • 如果 itemLeadingEdge的值小于等于0了,则当前只能看到首部Item的一半或者少许

结束语

功能到这里就结束了,如果有不对的地方或者建议欢迎指正,感谢你的观看!

相关推荐
君蓦11 小时前
Flutter 本地存储与数据库的使用和优化
flutter
problc21 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
lqj_本人1 天前
鸿蒙next选择 Flutter 开发跨平台应用的原因
flutter·华为·harmonyos
lqj_本人1 天前
Flutter&鸿蒙next 状态管理框架对比分析
flutter·华为·harmonyos
起司锅仔2 天前
Flutter启动流程(2)
flutter
hello world smile2 天前
最全的Flutter中pubspec.yaml及其yaml 语法的使用说明
android·前端·javascript·flutter·dart·yaml·pubspec.yaml
lqj_本人2 天前
Flutter 的 Widget 概述与常用 Widgets 与鸿蒙 Next 的对比
flutter·harmonyos
iFlyCai2 天前
极简实现酷炫动效:Flutter隐式动画指南第二篇之一些酷炫的隐式动画效果
flutter
lqj_本人2 天前
Flutter&鸿蒙next 中使用 MobX 进行状态管理
flutter·华为·harmonyos