在幾乎全部的 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 實戰
若是本文有幫助到你的地方,不勝榮幸,若有文章中有錯誤和疑問,歡迎你們提出!