Flutter | Key 的原理和使用

概述

在幾乎全部的 widget 中,都有一個參數 key ,那麼這個 key 的做用是什麼,在何時才須要使用到 key ?java

沒有 key 會出現什麼問題?

咱們直接看一個計數器的例子:編程

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

image-20210608151911182

能夠看到上圖中藍色的數字時三,而紅色的是 5,接着修改代碼,將藍色和紅色的位置互換,而後熱重載一下,以下:app

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Box(Colors.red),
    Box(Colors.blue),
  ],
),
複製代碼
image-20210608154106755

接着就會發現,顏色已經互換了,可是數字並無發生改變,less

這時,咱們在後面新添加一個紅色,以下:dom

image-20210608154249651

接着在刪除第一個帶有數字 3 的紅色,按道理來講應該就會剩下 5,0,結果以下:ide

image-20210608154407087

可是你會發現結果依舊是 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 的定義就是 對一個 Element 配置的描述,也就是說,widget 只是一個配置的描述,並非真正的渲染對象,就至關因而 Android 裏面的 xml,只是描述了一下屬性,但他並非真正的 View。而且經過查看源碼可知 widget 中有一個 createElement 方法,用來建立 Element。

而 Element 則就是 Widget 樹 中特定位置對應的實例,以下圖所示:

image-20210608183249759

上圖恰好對應上面的例子:

**在沒有 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 的三種類型

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

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),
     ],
   ),
 )
複製代碼

從新運行,而且點擊自增,運行效果以下:

image-20210610220722876

接着咱們來修改一下代碼:

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Counter(),
    Container(child: Counter(key: _globalKey)),
  ],
),
複製代碼

咱們將最外層的 Row 換成了 Column,而且給最後一個 Counter 包裹了一個 Container 組件,猜一下結果會如何??,咱們來看一下結果:

image-20210610221029925

結果就是 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)),
          ],
        ),
)
複製代碼

上面是最開始寫的代碼,咱們來看一下結果:

345

經過上面的動圖就會發現,第二個 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,緣由就不須要多說了吧!


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 用到的可能並不相同,這裏須要注意一點

實例

這個例子咱們寫一個小遊戲,一個列表中有不少不一樣顏色的小方塊,經過拖動這些方塊來進行顏色的重排序。效果以下:

345

經過點擊按鈕來打亂順序,而後長按方框拖動進行從新排序;

下面咱們來寫一下代碼:

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:可重排序的列表,支持拖動排序

  • onReorder:拖動後的回調,會給出新的 index 和 舊的 index,經過這兩個參數就能夠對位置就行修改,如上所示
  • scrollDirection:指定橫向或者豎向

還有一個須要注意的是 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。

體驗了幾回以後就發現了一些問題,

  • 好比拖動的時候只能是一維的,只能上下或者左右,
  • 拖動的時候是整個 item 拖動,而且會有一些陰影效果等,
  • 必須是長按才能拖動

由於 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 是一個可拖拽組件,經常使用的屬性以下:

  • feedback:跟隨拖拽的組件
  • childWhenDragging:拖拽時 chilid 子組件顯示的樣式
  • onDargStarted:第一次按下的回調

上面的代碼工做流程以下:

1,當手指按住 Box 以後,計算 Box 的 index 。

2,當手指開始移動時經過移動的位置和按下時的位置進行比較。

3,若是大於,則 index 和 index +1 進行互換,小於則 index 和 index-1互換。

4,進行判決處理,若是處於第一個或最後一個時直接 return。

須要注意的是上面並無使用 UniqueKey,由於 UniqueKey 是唯一的,在從新 build 的時候 由於 key 不相等,以前的狀態就會丟失,致使 AnimatedPositioned 的動畫沒法執行,因此這裏使用 ValueKey。這樣就能保證不會出現狀態丟失的問題。

固然也能夠給每個 Box 建立一個唯一的 UniqueKey 也能夠。

上面例子中執行效果以下:

345

因爲是 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減去這個位置便可。最終效果以下:

345

優化細節

通過上面的操做,基本的功能都實現了,最後咱們優化一下細節,如隨機顏色,固定第一個顏色,添加遊戲成功檢測等。

最終代碼以下:

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)),
    );
  }
}
複製代碼

最終效果以下:

345

參考文獻

B站王叔不禿視頻

Flutter 實戰

若是本文有幫助到你的地方,不勝榮幸,若有文章中有錯誤和疑問,歡迎你們提出!

相關文章
相關標籤/搜索