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 实战

若是本文有帮助到你的地方,不胜荣幸,若有文章中有错误和疑问,欢迎你们提出!

相关文章
相关标签/搜索