Flutter简单实现滑块验证
现在实现一个 Flutter 滑动验证组件,类似于许多网站和应用程序中常见的“滑动以验证”功能。它通过滑动一个滑块来完成验证操作,用户需要将滑块拖动到指定位置以完成验证。
前置知识点整理
StatefulWidget
在 Flutter 中,`StatefulWidget` 是一种可以拥有状态的组件,状态的变化会导致 UI 的重建。这与 `StatelessWidget` 不同,后者是无状态的,通常用于不需要维护状态的简单 UI 组件。
`StatefulWidget` 的基本结构
一个完整的 `StatefulWidget` 由两个类组成:
1.`StatefulWidget` 类:
- 这个类本身是不可变的。
- 它负责创建一个 `State` 对象,该对象持有所有与这个组件相关的状态。
2.`State` 类:
- 这是一个泛型类,通常与特定的 `StatefulWidget` 绑定。
- 负责存储与 `StatefulWidget` 相关的数据,并包含构建 UI 的逻辑。
- 当状态改变时,通过调用 `setState` 方法来触发 UI 的重建。
`StatefulWidget` 的生命周期
1.`createState`:
- 当 Flutter 框架准备构建 `StatefulWidget` 时调用。
- 返回一个 `State` 对象,持有组件的状态。
2.`initState`:
- 在 `State` 对象第一次被插入到树中时调用。
- 通常用于初始化数据或订阅服务。
3.`didChangeDependencies`:
- 当 `State` 对象的依赖关系发生变化时调用。
- 例如,`InheritedWidget` 中的数据改变。
4.`build`:
- 必须实现的方法,构建 UI。
- 当您调用 `setState` 时,`build` 方法会被重新调用。
5.setState`:
- 用来通知框架状态已经改变,并且需要重建 UI。
- 只更新最小范围内的 UI。
6.`deactivate`:
- 当 `State` 对象被从树中移除时调用。
7.`dispose`:
- 当 `State` 对象永久性被从树中移除时调用。
- 用于释放资源,比如取消订阅或者关闭动画控制器。
`StatefulWidget` 设计理念
- 不可变性:`StatefulWidget` 本身是不可变的,任何需要改变的数据都应该放在 `State` 对象中。这是因为 `StatefulWidget` 仅用于描述 UI 的布局和配置,而状态变化应该由 `State` 管理。
- 分离逻辑和状态:将状态管理逻辑放在 `State` 类中,可以更好地组织代码,使得 UI 和业务逻辑更易于维护和测试。
`State` 生命周期方法的使用
`initState()`:
- 在这里进行初始化操作,例如创建动画控制器、订阅服务、初始化变量等。
- 调用 `super.initState()` 是必须的。
`didChangeDependencies()`:
- 当 `State` 依赖的 `InheritedWidget` 发生变化时调用。
- 通常用于在依赖的环境改变时,重新计算一些需要的状态。
`setState()`:
- 调用此方法后,Flutter 框架会在下一个帧重新调用 `build()` 方法。
- 只应在 `State` 类中调用 `setState`,而不应在 `build()` 方法中调用,以避免无限的重建循环。
`dispose()`:
- 在这里释放资源,例如取消订阅、销毁动画控制器等。
- 确保调用 `super.dispose()` 以遵循框架的正确处理流程。
性能优化
最小化 `setState` 范围:
只更新需要改变的部分,不要在 `setState` 中更新整个 widget 树,以提升性能。
避免不必要的重建:
通过提取组件和使用 `const` 构造函数来减少无意义的重建。
使用 `Keys`:
在需要保持 widget 状态的一致性时,使用 `Keys` 来帮助 Flutter 底层算法识别 widget。
状态管理的扩展
Provider:
在更复杂的应用中,使用 `Provider` 或其他状态管理解决方案来管理跨多个 widget 的状态。
BLoC:
使用 BLoC 模式来分离业务逻辑和 UI,适合处理更复杂的交互和数据流。
Riverpod:
一个现代化的状态管理库,可以提供更灵活和简洁的方式来管理应用状态。
AnimationController
`AnimationController` 是 Flutter 中用于创建动画效果的核心类之一。它负责管理动画的时间线,包括动画的启动、停止、方向和速度控制。`AnimationController` 通常与其他动画类(如 `Tween` 和 `Animation`)结合使用,以创建复杂的动画效果。
基本用法
初始化
`AnimationController` 需要一个 `TickerProvider`,通常通过在 `State` 类中使用 `SingleTickerProviderStateMixin` 或 `TickerProviderStateMixin` 来实现。
代码示例
import 'package:flutter/material.dart';
class MyAnimatedWidget extends StatefulWidget {
const MyAnimatedWidget({super.key});
@override
_MyAnimatedWidgetState createState() {
return _MyAnimatedWidgetState();
}
}
class _MyAnimatedWidgetState extends State<MyAnimatedWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
Widget build(BuildContext context) {}
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
使用 `AnimationController`
启动动画:
- `forward()`: 正向播放动画。
- `reverse()`: 反向播放动画。
- `repeat()`: 循环播放动画,可以指定是否反向。
控制动画:
- `stop()`: 停止动画。
- `reset()`: 重置动画到初始状态。
- `animateTo()`: 将动画移动到特定的值。
监听动画:
- `addListener()`: 添加回调函数,每帧都会调用。
- `addStatusListener()`: 监听动画状态变化(如开始、结束、前进、反向)。
结合 `Tween` 和 `AnimatedBuilder`
`Tween` 用于定义动画值的范围和插值方式。`AnimatedBuilder` 则用于在每一帧重新构建 UI。
@overrided
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _controller.value,
child: child,
);
},
child: Container(
width: 100,
height: 200,
color: Colors.grey,
),
);
}
进阶用法
使用 `CurvedAnimation`
`CurvedAnimation` 可以在 `AnimationController` 基础上应用不同的插值曲线(如加速、减速、弹性等)。
final Animation<double> _animation = CurvedAnimation(d
parent: _controller,
curve: Curves.easeInOut,
);
多控制器与 `TickerProviderStateMixin`
当需要同时管理多个动画时,可以使用 `TickerProviderStateMixin`,但要注意资源管理,确保在 `dispose()` 中释放所有 `AnimationController`。
注意事项
- 资源释放:始终在 `dispose()` 方法中调用 `dispose()` 来释放 `AnimationController` 资源,以避免内存泄漏。
- 性能优化:动画应尽量简化,不要在动画中执行复杂的计算或 I/O 操作。
- 帧率:Flutter 尝试以每秒 60 帧的速率渲染动画,确保动画逻辑不会阻塞主线程以保持流畅。
通过灵活应用 `AnimationController`,可以在 Flutter 中创建丰富的动画效果,从简单的过渡到复杂的交互式动画。
GestureDetector
`GestureDetector` 是 Flutter 中用于检测用户手势的一个重要小部件。它提供了一种方式来捕获和响应屏幕上的各种触摸事件和手势,比如点击、双击、长按、拖动、缩放等。通过 `GestureDetector`,我们可以使应用对用户交互更具响应性和互动性。
基本功能
`GestureDetector` 通过一系列回调函数来处理不同类型的手势事件。
常用属性和回调
1.点击类手势:
- `onTap`: 用户轻触屏幕时触发。
- `onDoubleTap`: 用户双击屏幕时触发。
- `onLongPress`: 用户长按屏幕时触发。
2.拖动类手势:
- `onPanStart`: 用户开始拖动时触发。
- `onPanUpdate`: 用户拖动时持续触发,返回拖动的位移。
- `onPanEnd`: 用户结束拖动时触发。
3.缩放类手势:
- `onScaleStart`: 用户开始进行缩放操作时触发。
- `onScaleUpdate`: 用户缩放时持续触发,返回缩放比例。
- `onScaleEnd`: 用户结束缩放操作时触发。
4.特定方向拖动手势:
- `onVerticalDragStart` / `onVerticalDragUpdate` / `onVerticalDragEnd`: 检测垂直方向拖动。
- `onHorizontalDragStart` / `onHorizontalDragUpdate` / `onHorizontalDragEnd`: 检测水平方向拖动。
代码示例
import 'package:flutter/material.dart';
class DraggableBox extends StatefulWidget {
const DraggableBox({super.key});
@override
_DraggableBoxState createState() {
return _DraggableBoxState();
}
}
class _DraggableBoxState extends State<DraggableBox> {
double _xOffset = 0.0;
double _yOffset = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
_xOffset += details.delta.dx;
_yOffset += details.delta.dy;
});
},
child: Transform.translate(
offset: Offset(_xOffset, _yOffset),
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
),
),
);
}
}
高级用法和注意事项
事件冲突:
- 在复杂的 UI 中,多个手势检测器可能重叠,导致事件冲突。可以使用 `GestureDetector` 的 `behavior` 属性来控制手势事件的分发,例如 `HitTestBehavior.opaque`、`HitTestBehavior.translucent`、`HitTestBehavior.deferToChild`。
手势优先级:
- 当多个手势重叠时,Flutter 会根据手势识别机制来确定哪个手势有效。通过回调的返回值或 `GestureArena` 机制
手势优先级和冲突解决
手势识别冲突:
- 当多个手势检测器同时监听同一事件流时,可能会发生冲突。Flutter 使用 `GestureArena` 来管理这种冲突。
- `GestureDetector` 中的某些手势,如 `onTap` 和 `onDoubleTap`,可能会同时触发。在这种情况下,Flutter 会尝试根据手势的优先级和时间顺序来确定哪个手势应该生效。
使用 `behavior` 属性:
- `behavior` 属性控制 `GestureDetector` 如何处理点击测试:
- `HitTestBehavior.deferToChild`: 默认值,表示事件先传递给子组件。
- `HitTestBehavior.opaque`: 组件即使透明也能接受事件。
- `HitTestBehavior.translucent`: 透明区域可点击,但事件也会传递给下面的组件。
组合手势
组合多个手势:
- 可以在一个 `GestureDetector` 中组合多个手势检测回调,以实现复杂的交互。例如,同时处理拖动和缩放:
GestureDetector(
onPanUpdate: (details) {
// 处理拖动
},
onScaleUpdate: (details) {
// 处理缩放
},
);
性能优化
避免不必要的重建:
- 在手势回调中,尽量减少 `setState` 的调用范围,只更新需要改变的部分。
- 对于复杂的计算或动画,考虑使用 `AnimationController` 或 `AnimatedBuilder` 来分离手势逻辑和 UI 重建。
其他注意事项
响应区域:
- `GestureDetector` 默认的响应区域是其子组件的大小。如果想扩大响应区域,可以在 `GestureDetector` 外包裹一个较大的 `Container` 或使用 `Padding`。
与其他手势检测器的交互:
- 当 `GestureDetector` 与其他手势检测器(如 `InkWell` 或 `RawGestureDetector`)一起使用时,需注意手势处理的顺序和优先级。
案例场景
- 滑动删除:在列表项中使用 `GestureDetector` 实现滑动删除功能,结合 `Dismissible` 小部件。
- 图片缩放与拖动:在画廊应用中,结合拖动和缩放手势,允许用户放大和移动图片。
- 游戏中的手势控制:利用复杂的手势组合实现游戏中的角色移动、旋转和其他互动操作。
通过正确使用 `GestureDetector`,开发者可以为 Flutter 应用添加丰富的交互体验,使应用更加生动和用户友好。
Positioned
`Positioned` 是 Flutter 中用于在 `Stack` 小部件内精准定位子小部件的一个小部件。它允许你通过设置距离 `Stack` 边界的偏移量来定位子小部件。`Positioned` 只能作为 `Stack` 的子小部件使用。
基本用法
`Positioned` 提供了 `left`、`right`、`top` 和 `bottom` 属性,通过这些属性,你可以指定子部件相对于 `Stack` 容器的偏移量。这些属性可以组合使用,以便更精确地定位子部件。diam
代码示例
import 'package:flutter/material.dart';
class PositionedExamplePage extends StatelessWidget {
const PositionedExamplePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Positioned Example")),
body: Center(
child: Stack(
children: <Widget>[
Container(
width: 200,
height: 200,
color: Colors.blue,
),
Positioned(
top: 10,
left: 10,
child: Container(
width: 100,
height: 100,
color: Colors.red,
)),
const Positioned(
bottom: 10,
right: 10,
child: Text("Bottom Right"),
)
],
),
),
);
}
}
高级用法和注意事项
1. 使用多个属性
- 可以同时设置对立的边界(如 `left` 和 `right`),这会导致 `Positioned` 子部件的大小被拉伸以适应指定的边距。
- 例如,如果你同时指定了 `left` 和 `right`,就可以控制子部件的宽度。
2. 自动适应大小
- 如果没有指定宽度或高度,`Positioned` 将根据其子部件的大小进行调整。
3. 与 `Align` 的对比
- `Positioned` 是通过固定偏移量定位子部件,而 `Align` 则通过比例(0 到 1)定位。
- 如果需要相对位置(如居中、居左上角),可以考虑使用 `Align`。
4. 动态布局
- 在响应式布局中,可能需要结合 `MediaQuery` 或 `LayoutBuilder` 动态计算偏移量,以适应不同的屏幕尺寸和方向。
通过正确使用 `Positioned`,你可以在 `Stack` 中灵活地布局子部件,实现复杂的界面设计。它非常适合用于需要精确控制子部件位置的场景,比如覆盖、标注和自定义布局。
Row
`Row` 是 Flutter 中用于水平布局的一个小部件,允许你将多个子小部件沿水平轴排列。它是一个非常常用的布局小部件,适合用于创建水平排列的组件集合。
基本用法
`Row` 小部件的核心功能是水平排列其子小部件,并提供了一些属性来控制子小部件的布局方式。
代码示例
import 'package:flutter/material.dart';
class RowExamplePage extends StatelessWidget {
const RowExamplePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Row Example")),
body: const Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(
Icons.star,
color: Colors.red,
size: 50,
),
Icon(
Icons.star,
color: Colors.grey,
size: 50,
),
Icon(
Icons.star,
color: Colors.black,
size: 50,
)
],
),
),
);
}
}
代码解析
`mainAxisAlignment`
1.控制子小部件在主轴(水平轴)上的对齐方式。
2.常用选项包括:
- `MainAxisAlignment.start`: 子部件在行的起始处排列。
- `MainAxisAlignment.end`: 子部件在行的末尾处排列。
- `MainAxisAlignment.center`: 子部件在行的中心排列。
- `MainAxisAlignment.spaceBetween`: 子部件均匀分布,第一个和最后一个子部件贴边。
- `MainAxisAlignment.spaceAround`: 子部件均匀分布,每个子部件周围有相等的空间。
- `MainAxisAlignment.spaceEvenly`: 子部件均匀分布,且空隙相等。
`crossAxisAlignment`
1.控制子小部件在交叉轴(垂直轴)上的对齐方式。
2.常用选项包括:
- `CrossAxisAlignment.start`: 子部件在交叉轴起始处对齐。
- `CrossAxisAlignment.end`: 子部件在交叉轴末尾处对齐。
- `CrossAxisAlignment.center`: 子部件在交叉轴居中对齐。
- `CrossAxisAlignment.stretch`: 子部件在交叉轴上拉伸以填满父容器。
- `CrossAxisAlignment.baseline`: 子部件基于文本基线对齐(需要指定 `TextBaseline`)。
高级用法和注意事项
子小部件的尺寸
- Row` 会根据其父容器的约束来布局子小部件。子小部件可以是灵活的(如使用 `Flexible` 或 `Expanded`),也可以是固定宽度的。
- 如果子小部件的宽度超出了 `Row` 的可用空间,则可能会出现布局溢出。
灵活布局
使用 `Flexible` 和 `Expanded` 可以创建自适应的子小部件:
- `Flexible`: 允许子小部件在可用空间内灵活调整大小。
- `Expanded`: 强制子小部件填满 `Row` 的可用空间。
代码示例:Expanded`: 强制子小部件填满 `Row` 的可用空间
import 'package:flutter/material.dart';
class RowExamplePageDemo1 extends StatelessWidget {
const RowExamplePageDemo1({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Row Example")),
body: Center(
child: Row(
children: <Widget>[
Expanded(child: Container(color: Colors.red, height: 50)),
Expanded(child: Container(color: Colors.green, height: 50)),
Expanded(child: Container(color: Colors.blue, height: 50)),
],
),
),
);
}
}
灵活布局示例
使用 `Flexible` 和 `Expanded` 可以让 `Row` 的子小部件在水平空间上进行灵活的大小调整:
import 'package:flutter/material.dart';
class RowExamplePageDemo2 extends StatelessWidget {
const RowExamplePageDemo2({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Row Example")),
body: Center(
child: Row(
children: <Widget>[
Expanded(
flex: 1, // 占用1份可用空间
child: Container(color: Colors.red, height: 50),
),
Expanded(
flex: 2, // 占用2份可用空间
child: Container(color: Colors.green, height: 50),
),
Expanded(
flex: 1, // 占用1份可用空间
child: Container(color: Colors.blue, height: 50),
),
]
),
),
);
}
}
flex` 属性:`flex` 用于指定子小部件在 `Row` 的可用空间中占据的比例。上面的例子中,绿色的容器将占用红色和蓝色容器两倍的宽度。
使用 `Flexible` 的场景
`Flexible` 可以让子小部件在 `Row` 中占据一定比例的空间,而不强制其填满整个可用空间,这在某些需要固定或最小宽度的场景中特别有用。
import 'package:flutter/material.dart';
class RowExamplePageDemo3 extends StatelessWidget {
const RowExamplePageDemo3({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Row Example")),
body: Center(
child: Row(
children: <Widget>[
Flexible(
flex: 1,
fit: FlexFit.tight, // 强制子小部件填满可用空间
child: Container(
color: Colors.red,
height: 50,
),
),
Flexible(
flex: 1,
fit: FlexFit.loose,// 子小部件根据本身大小适应可用空间
child: Container(
color: Colors.green,
height: 50,
),
),
Flexible(
fit: FlexFit.tight,
flex: 1,
child: Container(
color: Colors.blue,
height: 50,
),
)
],
)));
}
}
注意事项
布局溢出
当 `Row` 中的子小部件总宽度超过 `Row` 的可用宽度时,会出现布局溢出错误(通常在调试模式下显示为红色的溢出警告)。这时可以考虑以下解决方案:
- 使用 `Expanded` 或 `Flexible` 使子小部件自适应。
- 通过 `SingleChildScrollView` 包裹 `Row`,提供水平滚动功能。
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: <Widget>[
Container(color: Colors.red, width: 400, height: 50),
Container(color: Colors.green, width: 400, height: 50),
Container(color: Colors.blue, width: 400, height: 50),
],
),
)
嵌套布局
- 如果需要在 `Row` 中嵌套其他布局(如 `Column` 或 `Stack`),确保对齐方式和布局约束被正确处理,以避免不期望的布局结果。
总结
- `Row` 是构建水平布局的基本工具,通过结合 `mainAxisAlignment` 和 `crossAxisAlignment` 可以实现丰富的布局对齐。
- 使用 `Flexible` 和 `Expanded` 可以实现灵活和自适应的布局。
- 注意处理可能的布局溢出,并根据需要结合其他布局小部件(如 `SingleChildScrollView`)来提供更好的用户体验。
实现滑块验证代码学习
import 'package:flutter/material.dart';
class SlideVerifyPage extends StatelessWidget {
const SlideVerifyPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("SlideVerifyPage"),
),
body: const Center(
child: SlideVerify(
sliderImage: "static/ic_demo.png",
successText: "验证成功",
initText: "滑动验证",
),
),
);
}
}
class SlideVerify extends StatefulWidget {
final double height;
final double width;
final Color borderColor;
final Color bgColor;
final Color moveColor;
final String? successText;
final String? sliderImage;
final String? initText;
final String? initImage;
final TextStyle successTextStyle;
final TextStyle initTextStyle;
final VoidCallback? successListener;
const SlideVerify(
{super.key,
this.height = 60,
this.width = 250,
this.successText,
this.initText,
this.sliderImage,
this.initImage,
this.successTextStyle =
const TextStyle(fontSize: 14, color: Colors.white),
this.initTextStyle = const TextStyle(fontSize: 14, color: Colors.black12),
this.bgColor = Colors.grey,
this.moveColor = Colors.blue,
this.borderColor = Colors.blueAccent,
this.successListener});
@override
State<StatefulWidget> createState() {
return SlideVerifyState();
}
}
class SlideVerifyState extends State<SlideVerify>
with TickerProviderStateMixin {
AnimationController? _animController;
Animation? _curve;
double initX = 0.0;
double height = 0;
double width = 0;
double moveDistance = 0;
double sliderWidth = 0;
bool verifySuccess = false;
bool enable = true;
void _init() {
sliderWidth = widget.height - 4;
_animController = AnimationController(
duration: const Duration(milliseconds: 400), vsync: this);
_curve = CurvedAnimation(parent: _animController!, curve: Curves.easeOut);
_curve?.addListener(() {
setState(() {
moveDistance = moveDistance - moveDistance * _curve!.value;
if (moveDistance <= 0) {
moveDistance = 0;
}
});
});
_animController?.addStatusListener((status) {
if (status == AnimationStatus.completed) {
enable = true;
_animController?.reset();
}
});
}
@override
void initState() {
super.initState();
width = widget.width;
height = widget.height;
_init();
}
@override
void dispose() {
_animController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onHorizontalDragStart: (DragStartDetails details) {
if (!enable) {
return;
}
initX = details.globalPosition.dx;
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
if (!enable) {
return;
}
moveDistance = details.globalPosition.dx - initX;
if (moveDistance < 0) {
moveDistance = 0;
}
if (moveDistance > width - sliderWidth) {
moveDistance = width - sliderWidth;
enable = false;
verifySuccess = true;
if (widget.successListener != null) {
widget.successListener?.call();
}
}
setState(() {});
},
onHorizontalDragEnd: (DragEndDetails details) {
if (enable) {
enable = false;
_animController?.forward();
}
},
child: Container(
height: height,
width: width,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: widget.bgColor,
border: Border.all(color: widget.borderColor),
borderRadius: BorderRadius.all(Radius.circular(height))),
child: Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
Positioned(
top: 0,
left: 0,
child: Container(
height: height - 2,
width: moveDistance < 1 ? 0 : moveDistance + sliderWidth / 2,
decoration: BoxDecoration(
color: widget.moveColor,
),
),
),
Center(
child: Text(
verifySuccess
? widget.successText ?? ""
: widget.initText ?? "",
style: verifySuccess
? widget.successTextStyle
: widget.initTextStyle,
),
),
Positioned(
top: 1,
left:
moveDistance > sliderWidth ? moveDistance - 2 : moveDistance,
child: Container(
width: sliderWidth,
height: sliderWidth,
alignment: Alignment.center,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(
Radius.circular(sliderWidth),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
if (widget.sliderImage != null)
Image.asset(
widget.sliderImage!,
height: sliderWidth,
width: sliderWidth,
fit: BoxFit.cover,
),
],
),
),
),
],
),
),
);
}
}
原文地址:https://blog.csdn.net/zhangying1994/article/details/144092086
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!