前言
这是在掘金发布的第一篇文章,大家在做图像浏览或部分关于图像的项目时,难免会遇到缩略图的相关功能,特地写了一个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插件应用
这是一款很优秀的插件,本文以分享为主给大家简单解析。
我们来简单讲讲为什么使用这个插件
根据官方插件文献可以得知相较于传统滑动控制处理。
该插件中拥有可以根据索引页跳转 和相对位置偏移量跳转,传统方式还需要计算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的一半或者少许
结束语
功能到这里就结束了,如果有不对的地方或者建议欢迎指正,感谢你的观看!