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的一半或者少许

结束语

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

相关推荐
ALLIN12 小时前
Flutter 三种方式实现页面切换后保持原页面状态
flutter
Dabei12 小时前
Flutter 国际化
flutter
Dabei13 小时前
Flutter MQTT 通信文档
flutter
Dabei16 小时前
Flutter 中实现 TCP 通信
flutter
孤鸿玉16 小时前
ios flutter_echarts 不在当前屏幕 白屏修复
flutter
前端 贾公子18 小时前
《Vuejs设计与实现》第 16 章(解析器) 上
vue.js·flutter·ios
tangweiguo030519871 天前
Flutter 数据存储的四种核心方式 · 从 SharedPreferences 到 SQLite:Flutter 数据持久化终极整理
flutter
0wioiw01 天前
Flutter基础(②④事件回调与交互处理)
flutter
肥肥呀呀呀1 天前
flutter配置Android gradle kts 8.0 的打包名称
android·flutter
吴Wu涛涛涛涛涛Tao1 天前
Flutter 实现「可拖拽评论面板 + 回复输入框 + @高亮」的完整方案
android·flutter·ios