在几乎全部的 widget
中,都有一个参数 key
,那么这个 key 的做用是什么,在何时才须要使用到 key ?java
咱们直接看一个计数器的例子:编程
class Box extends StatefulWidget {
final Color color;
Box(this.color);
@override
_BoxState createState() => _BoxState();
}
class _BoxState extends State<Box> {
int _count = 0;
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Container(
width: 100,
height: 100,
color: widget.color,
alignment: Alignment.center,
child: Text(_count.toString(), style: TextStyle(fontSize: 30))),
onTap: () => setState(() => ++_count),
);
}
}
复制代码
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Box(Colors.blue),
Box(Colors.red),
],
)
复制代码
运行效果以下:markdown
能够看到上图中蓝色的数字时三,而红色的是 5,接着修改代码,将蓝色和红色的位置互换,而后热重载一下,以下:app
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Box(Colors.red),
Box(Colors.blue),
],
),
复制代码
接着就会发现,颜色已经互换了,可是数字并无发生改变,less
这时,咱们在后面新添加一个红色,以下:dom
接着在删除第一个带有数字 3 的红色,按道理来讲应该就会剩下 5,0,结果以下:ide
可是你会发现结果依旧是 3,5。布局
在这个示例中 flutter 不能经过 Container 的颜色来设置标识,因此就没办法肯定那个究竟是哪一个,因此咱们须要一个相似于 id 的东西,给每一个 widget 一个标识,而 key 就是这个标识。优化
接着咱们修改一下上面的示例:动画
class Box extends StatefulWidget {
final Color color;
Box(this.color, {Key key}) : super(key: key);
@override
_BoxState createState() => _BoxState();
}
复制代码
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Box(Colors.blue, key: ValueKey(1)),
Box(Colors.red, key: ValueKey(2)),
],
)
复制代码
在代码中添加了 key,而后就会发现已经没有上面的问题了。可是若是咱们给 Box 在包裹一层 Container,而后在次热重载的时候,数字都变成了 0,在去掉 Container 后数字也会变成 0,具体的缘由咱们在后面说;
widget 的定义就是 对一个 Element 配置的描述
,也就是说,widget 只是一个配置的描述,并非真正的渲染对象,就至关因而 Android 里面的 xml,只是描述了一下属性,但他并非真正的 View。而且经过查看源码可知 widget 中有一个 createElement 方法,用来建立 Element。
而 Element 则就是 Widget 树 中特定位置对应的实例,以下图所示:
上图恰好对应上面的例子:
**在没有 key 的状况下,**若是替换掉 第一个和第二个 box 置换,那么第二个就会使用第一个 box 的 Element,因此他的状态不会发生改变,可是由于颜色信息是在 widget 上的,因此颜色就会改变。最终置换后结果就是颜色改变了,可是里面的值没有发生变化。
又或者删除了第一个 box,第二个box 就会使用第一个 boxElement 的状态,因此说也会有上面的问题。
加上 key 的状况:
加上 key 以后,widget 和 element 会有对应关系,若是 key 没有对应就会从新在同层级下寻找,若是没有最终这个 widget 或者 Element 就会被删除
解释一下上面遗留的问题
在 Box 外部嵌套 Container 以后状态就没有了。这是由于 判断 key 以前首先会判断类型是否一致,而后在判断 key 是否相同。
正由于类型不一致,因此以前的 State 状态都没法使用,因此就会从新建立一个新的。
须要注意的是,继承自 StatelessWidget 的 Widget 是不须要使用 Key 的,由于它自己没有状态,不须要用到 Key。
键在具备相同父级的 [Element] 中必须是惟一的。相比之下,[GlobalKey] 在整个应用程序中必须是惟一的。另请参阅:[Widget.key],其中讨论了小部件如何使用键。
LocalKey
继承自 Key, 翻译过来就是局部键,LocalKey
在具备相同父级的 Element
中必须是唯一的。也就是说,LocalKey 在同一层级中必需要有惟一性。
LocalKey
有三种子类型,下面咱们来看一下:
ValueKey
class ValueKey<T> extends LocalKey {
final T value;
const ValueKey(this.value);
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is ValueKey<T>
&& other.value == value;
}
}
复制代码
使用特定类型的值来标识自身的键,ValueKey 在最上面的例子中已经使用过了,他能够接收任何类型的一个对象来最为 key。
经过源码咱们能够看到它重写了 == 运算符,在判断是否相等的时候首先判断了类型是否相等,而后再去判断 value 是否相等;
ObjectKey
class ObjectKey extends LocalKey {
const ObjectKey(this.value);
final Object? value;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is ObjectKey
&& identical(other.value, value);
}
@override
int get hashCode => hashValues(runtimeType, identityHashCode(value));
}
复制代码
ObjectKey 和 ValueKey 最大的区别就是比较的算不同,其中首先也是比较的类型,而后就调用 indentical 方法进行比较,其比较的就是内存地址,至关于 java 中直接使用 == 进行比较。而 LocalKey 则至关于 java 中的 equals 方法用来比较值的。
须要注意的是使用 ValueKey 中使用 == 比较的时候,若是没有重写 hashCode 和 == ,那样即便 对象的值是相等的,但比较出来也是不相等的。因此说尽可能重写吧!
UniqueKey
class UniqueKey extends LocalKey {
UniqueKey();
}
复制代码
很明显,从名字中能够看出来,这是一个独一无二的 key。
每次从新 build 的时候,UniqueKey 都是独一无二的,因此就会致使没法找到对应的 Element,状态就会丢失。那么在何时须要用到这个 UniqueKey呢?咱们能够自行思考一下。
还有一种作法就是把 UniqueKey 定义在 build 的外面,这样就不会出现状态丢失的问题了。
GlobalKey
继承自 Key,相比与 LocalKey,他的做用域是全局的,而 LocalKey 只做用于当前层级。
在以前咱们遇到一个问题,就是若是给一个 Widget 外面嵌套了一层,那么这个 Widget 的状态就会丢失,以下:
children: <Widget>[
Box(Colors.red),
Box(Colors.blue),
],
///修改成以下,而后从新 build
children: <Widget>[
Box(Colors.red),
Container(child:Box(Colors.blue)),
],
复制代码
缘由在以前咱们也讲过,就是由于类型不一样。只有在类型和 key 相同的时候才会保留状态 ,显然上面的类型是不相同的;
那么遇到这种问题要怎么办呢,这个时候就可使用 GlobalKey 了。咱们看下面的栗子:
class Counter extends StatefulWidget {
Counter({Key key}) : super(key: key);
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () => setState(() => _count++),
child: Text("$_count", style: TextStyle(fontSize: 70)),
);
}
}
复制代码
final _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(),
Counter(),
],
),
),
);
}
复制代码
上面代码中,咱们定义了一个 Counter 组件,点击后 count 自增,和一个 GlobakKey 的对象。
接着咱们点击 Counter 组件,自增以后,给 Counter 包裹一层 Container 以后进行热重载,就会发现以前自增的数字已经不见了。这个时候咱们尚未使用 GlobalKey。
接着咱们使用 GlobalKey,以下
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(),
Counter(key: _globalKey),
],
),
)
复制代码
从新运行,而且点击自增,运行效果以下:
接着咱们来修改一下代码:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(),
Container(child: Counter(key: _globalKey)),
],
),
复制代码
咱们将最外层的 Row 换成了 Column,而且给最后一个 Counter 包裹了一个 Container 组件,猜一下结果会如何??,咱们来看一下结果:
结果就是 Column 已经生效了,使用了 GlobalKey 的 Counter 状态没有被清除,而上面这个没有使用的则没有了状态。
咱们简单的分析一下,热重载的时候回从新
build
一下,执行到 Column 位置的时候发现以前的类型是 Row,而后以前 Row 的 Element 就会被扔掉,从新建立 Element。Row 的 Element 扔掉以后,其内部的全部状态也都会消失,可是到了最里面的 Counter 的时候,就会根据 Counter 的 globalkey 从新查找对应的状态,找到以后就会继续使用。
在切换屏幕方向的时候改变布局排列方式,而且保证状态不会重置
Center(
child: MediaQuery.of(context).orientation == Orientation.portrait
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(),
Container(child: Counter(key: _globalKey)),
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(),
Container(child: Counter(key: _globalKey)),
],
),
)
复制代码
上面是最开始写的代码,咱们来看一下结果:
经过上面的动图就会发现,第二个 Container 的状态是正确的,第一个则不对,由于第一个没有使用 GlobalKey,因此须要给第一个也加上 GlobalKey,以下:
Center(
child: MediaQuery.of(context).orientation == Orientation.portrait
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(key: _globalKey1),
Counter(key: _globalKey2)
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(key: _globalKey1),
Container(child: Counter(key: _globalKey2))
],
),
)
复制代码
可是这样的写法确实有些 low,而且这种需求咱们其实不须要 GlobalKey 也能够实现,代码以下:
Center(
child: Flex(
direction: MediaQuery.of(context).orientation == Orientation.portrait
? Axis.vertical
: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Counter(), Counter()],
),
)
复制代码
使用了 Flex 以后,在 build 的时候 Flex 没有发生改变,因此就会从新找到 Element,因此状态也就不会丢失了。
可是若是内部的 Container 在屏幕切换的过程当中会从新嵌套,那仍是须要使用 GlobalKey,缘由就不须要多说了吧!
Flutter 属于声明式编程,若是页面中某个组件的须要更新,则会将更新的值提取到全局,在更新的时候修改全局的值,并进行 setState。这就是最推荐的作法。若是这个状态须要在两个 widget 中共同使用,就把状态向上提高,毫无疑问这也是正确的作法。
可是经过 GlobalKey
咱们能够直接在别的地方进行更新,获取状态,widget中数据等操做。前提是咱们须要拿到 GlobalKey 对象,其实就相似于 Android 中的 findViewById 拿到对应的控件,可是相比 GlobalKey,GlobalKey 能够获取到 State,Widget,RenderObject 等。
下面咱们看一下栗子:
final _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Flex(
direction: MediaQuery.of(context).orientation == Orientation.portrait
? Axis.vertical
: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Counter(key: _globalKey),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
复制代码
和以前的例子差很少,如今只剩了一个 Counter 了。如今咱们须要作的就是在点击 FloatingActionButton 按钮的时候,使这个 Counter 中的计数自动增长,而且获取到他的一些属性,代码以下:
floatingActionButton: FloatingActionButton(
onPressed: () {
final state = (_globalKey.currentState as _CounterState);
state.setState(() => state._count++);
final widget = (_globalKey.currentWidget as Counter);
final context = _globalKey.currentContext;
final render =
(_globalKey.currentContext.findRenderObject() as RenderBox);
///宽高度
print(render.size);
///距离左上角的像素
print(render.localToGlobal(Offset.zero));
},
child: Icon(Icons.add),
),
);
复制代码
I/flutter (29222): Size(88.0, 82.0)
I/flutter (29222): Offset(152.4, 378.6)
复制代码
能够看到上面代码中经过 _globakKey 获取到了 三个属性,分别是 state,widget 和 context。
其中使用了 state 对 _count 进行了自增。
而 widget 则就是 Counter 了。
可是 context 又是什么呢,咱们点进去源码看一下:
Element? get _currentElement => _registry[this];
BuildContext? get currentContext => _currentElement;
复制代码
经过上面两句代码就能够看出来 context 其实就是 Element 对象,经过查看继承关系可知道,Element 是继承自 BuildContext 的。
经过这个 context 的 findRenderObject
方法能够获取到 RenderObject
,这个 RenderObject
就是最终显示到屏幕上的东西,经过 RenderObject
咱们能够获取到一一些数据,例如 widget 的宽高度,距离屏幕左上角的位置等等。
RenderObject
有不少种类型,例如 RenderBox 等,不一样的 Widget 用到的可能并不相同,这里须要注意一点
这个例子咱们写一个小游戏,一个列表中有不少不一样颜色的小方块,经过拖动这些方块来进行颜色的重排序。效果以下:
经过点击按钮来打乱顺序,而后长按方框拖动进行从新排序;
下面咱们来写一下代码:
final boxes = [
Box(Colors.red[100], key: UniqueKey()),
Box(Colors.red[300], key: UniqueKey()),
Box(Colors.red[500], key: UniqueKey()),
Box(Colors.red[700], key: UniqueKey()),
Box(Colors.red[900], key: UniqueKey()),
];
_shuffle() {
setState(() => boxes.shuffle());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
///可重排序的列表
child: Container(
child: ReorderableListView(
onReorder: (int oldIndex, newIndex) {
if (newIndex > oldIndex) newIndex--;
final box = boxes.removeAt(oldIndex);
boxes.insert(newIndex, box);
},
children: boxes),
width: 60,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _shuffle(),
child: Icon(Icons.refresh),
),
);
}
复制代码
ReorderableListView:可重排序的列表,支持拖动排序
还有一个须要注意的是 ReorderableListView 的 Item 必须须要一个 key,不然就会报错。
class Box extends StatelessWidget {
final Color color;
Box(this.color, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return UnconstrainedBox(
child: Container(
margin: EdgeInsets.all(5),
width: 50,
height: 50,
decoration: BoxDecoration(
color: color, borderRadius: BorderRadius.circular(10)),
),
);
}
}
复制代码
上面是列表中 item 的 widget,须要注意的是里面使用到了 UnconstrainedBox,由于在 ReorderableListView 中可能使用到了尺寸限制,致使在 item 中设置的宽高没法生效,因此使用了 UnconstrainedBox。
体验了几回以后就发现了一些问题,
由于 ReorderableListView 没有提供属性去修改上面的这些问题,因此咱们能够本身实现一个相似的效果。以下:
class _MyHomePageState extends State<MyHomePage> {
final colors = [
Colors.red[100],
Colors.red[300],
Colors.red[500],
Colors.red[700],
Colors.red[900],
];
_shuffle() {
setState(() => colors.shuffle());
}
int _slot;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Listener(
onPointerMove: (event) {
//获取移动的位置
final x = event.position.dx;
//若是大于抬起位置的下一个,则互换
if (x > (_slot + 1) * Box.width) {
if (_slot == colors.length - 1) return;
setState(() {
final temp = colors[_slot];
colors[_slot] = colors[_slot + 1];
colors[_slot + 1] = temp;
_slot++;
});
} else if (x < _slot * Box.width) {
if (_slot == 0) return;
setState(() {
final temp = colors[_slot];
colors[_slot] = colors[_slot - 1];
colors[_slot - 1] = temp;
_slot--;
});
}
},
child: Stack(
children: List.generate(colors.length, (i) {
return Box(
colors[i],
x: i * Box.width,
y: 300,
onDrag: (Color color) => _slot = colors.indexOf(color),
key: ValueKey(colors[i]),
);
}),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _shuffle(),
child: Icon(Icons.refresh),
),
);
}
}
class Box extends StatelessWidget {
final Color color;
final double x, y;
static final width = 50.0;
static final height = 50.0;
static final margin = 2;
final Function(Color) onDrag;
Box(this.color, {this.x, this.y, this.onDrag, Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedPositioned(
child: Draggable(
child: box(color),
feedback: box(color),
onDragStarted: () => onDrag(color),
childWhenDragging: box(Colors.transparent),
),
duration: Duration(milliseconds: 100),
top: y,
left: x,
);
}
box(Color color) {
return Container(
width: width - margin * 2,
height: height - margin * 2,
decoration:
BoxDecoration(color: color, borderRadius: BorderRadius.circular(10)),
);
}
}
复制代码
能够看到上面咱们将 ReorderableListView
直接改为了 Stack
, 这是由于在 Stack 中咱们能够再 子元素中经过 Positioned
来自由的控制其位置。而且在 Stack 外面套了一层 Listener,这是用来监听移动的事件。
接着咱们看 Box,Box 就是能够移动的小方块。在最外层使用了 带动画的 Positioned
,在 Positioned
的位置发生变化以后就会产平生移的动画效果。
接着看一下 Draggable
组件,Draggable
是一个可拖拽组件,经常使用的属性以下:
上面的代码工做流程以下:
1,当手指按住 Box
以后,计算 Box
的 index 。
2,当手指开始移动时经过移动的位置和按下时的位置进行比较。
3,若是大于,则 index 和 index +1 进行互换,小于则 index 和 index-1互换。
4,进行判决处理,若是处于第一个或最后一个时直接 return。
须要注意的是上面并无使用 UniqueKey,由于 UniqueKey 是唯一的,在从新 build 的时候 由于 key 不相等,以前的状态就会丢失,致使 AnimatedPositioned 的动画没法执行,因此这里使用 ValueKey。这样就能保证不会出现状态丢失的问题。
固然也能够给每个 Box 建立一个唯一的 UniqueKey 也能够。
上面例子中执行效果以下:
因为是 gif 图,因此就会显得比较卡顿。
其实在上面最终完成的例子中,仍是有一些问题,例如只能是横向的,若是是竖着的,就须要从新修改代码。
而且 x 的坐标是从 0 开始计算的,若是在前面还有一些内容就会出现问题了。例如若是是竖着的,在最上面有一个 appbar,则就会出现问题。
修改代码以下所示:
class _MyHomePageState extends State<MyHomePage> {
///...
int _slot;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Listener(
onPointerMove: (event) {
//获取移动的位置
final y = event.position.dy;
//若是大于抬起位置的下一个,则互换
if (y > (_slot + 1) * Box.height) {
if (_slot == colors.length - 1) return;
setState(() {
final temp = colors[_slot];
colors[_slot] = colors[_slot + 1];
colors[_slot + 1] = temp;
_slot++;
});
} else if (y < _slot * Box.height) {
if (_slot == 0) return;
setState(() {
final temp = colors[_slot];
colors[_slot] = colors[_slot - 1];
colors[_slot - 1] = temp;
_slot--;
});
}
},
child: Stack(
children: List.generate(colors.length, (i) {
return Box(
colors[i],
x: 300,
y: i * Box.height,
onDrag: (Color color) => _slot = colors.indexOf(color),
key: ValueKey(colors[i]),
);
}),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _shuffle(),
child: Icon(Icons.refresh),
),
);
}
}
复制代码
在上面代码中将本来横着的组件变成了竖着的,而后在拖动就会发现问题,如向上拖动的时候须要拖动两格才能移动,这就是由于y轴不是从0开始的,在最上面会有一个 appbar,咱们没有将他的高度计算进去,因此就出现了这个问题。
这个时候咱们就可使用 GlobalKey 来解决这个问题:
final _globalKey = GlobalKey();
double _offset;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
SizedBox(height: 30),
Text("WelCome", style: TextStyle(fontSize: 28, color: Colors.black)),
SizedBox(height: 30),
Expanded(
child: Listener(
onPointerMove: (event) {
//获取移动的位置
final y = event.position.dy - _offset;
//若是大于抬起位置的下一个,则互换
if (y > (_slot + 1) * Box.height) {
if (_slot == colors.length - 1) return;
setState(() {
final temp = colors[_slot];
colors[_slot] = colors[_slot + 1];
colors[_slot + 1] = temp;
_slot++;
});
} else if (y < _slot * Box.height) {
if (_slot == 0) return;
setState(() {
final temp = colors[_slot];
colors[_slot] = colors[_slot - 1];
colors[_slot - 1] = temp;
_slot--;
});
}
},
child: Stack(
key: _globalKey,
children: List.generate(colors.length, (i) {
return Box(
colors[i],
x: 180,
y: i * Box.height,
onDrag: (Color color) {
_slot = colors.indexOf(color);
final renderBox = (_globalKey.currentContext
.findRenderObject() as RenderBox);
//获取距离顶部的距离
_offset = renderBox.localToGlobal(Offset.zero).dy;
},
key: ValueKey(colors[i]),
);
}),
),
))
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _shuffle(),
child: Icon(Icons.refresh),
),
);
}
复制代码
解决的思路很是简单,
经过 GlobalKey 获取到当前 Stack 距离顶部的位置,而后用dy减去这个位置便可。最终效果以下:
通过上面的操做,基本的功能都实现了,最后咱们优化一下细节,如随机颜色,固定第一个颜色,添加游戏成功检测等。
最终代码以下:
class _MyHomePageState extends State<MyHomePage> {
MaterialColor _color;
List<Color> _colors;
initState() {
super.initState();
_shuffle();
}
_shuffle() {
_color = Colors.primaries[Random().nextInt(Colors.primaries.length)];
_colors = List.generate(8, (index) => _color[(index + 1) * 100]);
setState(() => _colors.shuffle());
}
int _slot;
final _globalKey = GlobalKey();
double _offset;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title), actions: [
IconButton(
onPressed: () => _shuffle(),
icon: Icon(Icons.refresh, color: Colors.white),
)
]),
body: Column(
children: [
SizedBox(height: 30),
Text("WelCome", style: TextStyle(fontSize: 28, color: Colors.black)),
SizedBox(height: 30),
Container(
width: Box.width - Box.margin * 2,
height: Box.height - Box.margin * 2,
decoration: BoxDecoration(
color: _color[900], borderRadius: BorderRadius.circular(10)),
child: Icon(Icons.lock, color: Colors.white),
),
SizedBox(height: Box.margin * 2.0),
Expanded(
child: Center(
child: Listener(
onPointerMove: event,
child: SizedBox(
width: Box.width,
child: Stack(
key: _globalKey,
children: List.generate(_colors.length, (i) {
return Box(
_colors[i],
y: i * Box.height,
onDrag: (Color color) {
_slot = _colors.indexOf(color);
final renderBox = (_globalKey.currentContext
.findRenderObject() as RenderBox);
//获取距离顶部的距离
_offset = renderBox.localToGlobal(Offset.zero).dy;
},
onEnd: _checkWinCondition,
);
}),
),
),
),
))
],
),
);
}
_checkWinCondition() {
List<double> lum = _colors.map((e) => e.computeLuminance()).toList();
bool success = true;
for (int i = 0; i < lum.length - 1; i++) {
if (lum[i] > lum[i + 1]) {
success = false;
break;
}
}
print(success ? "成功" : "");
}
event(event) {
//获取移动的位置
final y = event.position.dy - _offset;
//若是大于抬起位置的下一个,则互换
if (y > (_slot + 1) * Box.height) {
if (_slot == _colors.length - 1) return;
setState(() {
final temp = _colors[_slot];
_colors[_slot] = _colors[_slot + 1];
_colors[_slot + 1] = temp;
_slot++;
});
} else if (y < _slot * Box.height) {
if (_slot == 0) return;
setState(() {
final temp = _colors[_slot];
_colors[_slot] = _colors[_slot - 1];
_colors[_slot - 1] = temp;
_slot--;
});
}
}
}
class Box extends StatelessWidget {
final double x, y;
final Color color;
static final width = 200.0;
static final height = 50.0;
static final margin = 2;
final Function(Color) onDrag;
final Function onEnd;
Box(this.color, {this.x, this.y, this.onDrag, this.onEnd})
: super(key: ValueKey(color));
@override
Widget build(BuildContext context) {
return AnimatedPositioned(
child: Draggable(
child: box(color),
feedback: box(color),
onDragStarted: () => onDrag(color),
onDragEnd: (drag) => onEnd(),
childWhenDragging: box(Colors.transparent),
),
duration: Duration(milliseconds: 100),
top: y,
left: x,
);
}
box(Color color) {
return Container(
width: width - margin * 2,
height: height - margin * 2,
decoration:
BoxDecoration(color: color, borderRadius: BorderRadius.circular(10)),
);
}
}
复制代码
最终效果以下:
B站王叔不秃视频
Flutter 实战
若是本文有帮助到你的地方,不胜荣幸,若有文章中有错误和疑问,欢迎你们提出!