Flutter | Key的原理和使用(下)

简介: Flutter | Key的原理和使用(下)

栗子:

在切换屏幕方向的时候改变布局排列方式,并且保证状态不会重置


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,原因就不需要多说了吧!


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


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 也可以。


由于是 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),
      ),
    );
  }
}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)),
    );
  }
}
相关文章
|
19天前
|
开发者 容器
Flutter&鸿蒙next 布局架构原理详解
本文详细介绍了 Flutter 中的主要布局方式,包括 Row、Column、Stack、Container、ListView 和 GridView 等布局组件的架构原理及使用场景。通过了解这些布局 Widget 的基本概念、关键属性和布局原理,开发者可以更高效地构建复杂的用户界面。此外,文章还提供了布局优化技巧,帮助提升应用性能。
78 4
|
19天前
|
存储 Dart 前端开发
flutter鸿蒙版本mvvm架构思想原理
在Flutter中实现MVVM架构,旨在将UI与业务逻辑分离,提升代码可维护性和可读性。本文介绍了MVVM的整体架构,包括Model、View和ViewModel的职责,以及各文件的详细实现。通过`main.dart`、`CounterViewModel.dart`、`MyHomePage.dart`和`Model.dart`的具体代码,展示了如何使用Provider进行状态管理,实现数据绑定和响应式设计。MVVM架构的分离关注点、数据绑定和可维护性特点,使得开发更加高效和整洁。
146 3
|
25天前
动画控制器在 Flutter 中的工作原理
【10月更文挑战第18天】总的来说,动画控制器 `AnimationController` 在 Flutter 中起着关键的作用,它通过控制动画的数值、速度、节奏和状态,实现了丰富多彩的动画效果。理解它的工作原理对于我们在 Flutter 中创建各种精彩的动画是非常重要的。
|
1月前
|
容器
Flutter&鸿蒙next 布局架构原理详解
Flutter&鸿蒙next 布局架构原理详解
|
5月前
|
存储 开发框架 JavaScript
深入探讨Flutter中动态UI构建的原理、方法以及数据驱动视图的实现技巧
【6月更文挑战第11天】Flutter是高效的跨平台移动开发框架,以其热重载、高性能渲染和丰富组件库著称。本文探讨了Flutter中动态UI构建原理与数据驱动视图的实现。动态UI基于Widget树模型,状态变化触发UI更新。状态管理是关键,Flutter提供StatefulWidget、Provider、Redux等方式。使用ListView等可滚动组件和StreamBuilder等流式组件实现数据驱动视图的自动更新。响应式布局确保UI在不同设备上的适应性。Flutter为开发者构建动态、用户友好的界面提供了强大支持。
100 2
|
1月前
flutter:定时器&通过key操作状态 (十二)
本文档介绍了Flutter中的定时器使用方法、通过key操作状态的几种方式,包括localKey和GlobalKey的使用场景与示例代码,以及如何处理屏幕旋转导致的组件状态丢失问题。通过配置全局key,可以有效地管理父子组件之间的状态交互,确保在屏幕旋转等情况下保持组件状态的一致性。
|
4月前
|
Dart JavaScript Java
flutter 架构、渲染原理、家族
flutter 架构、渲染原理、家族
86 3
|
6月前
|
Android开发
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
|
6月前
|
开发框架 Dart API
Flutter引擎工作原理:深入解析FlutterEngine
【4月更文挑战第26天】FlutterEngine是Flutter应用的关键,负责Dart代码转换为原生代码,管理应用生命周期、渲染和事件处理。它初始化Flutter运行时环境,加载并编译Dart代码,创建渲染树,处理事件并实现跨平台兼容。通过理解其工作原理,开发者能更好地掌握Flutter应用内部机制并优化开发。随着Flutter生态系统发展,FlutterEngine将持续提供强大支持。
|
6月前
|
Web App开发 前端开发 iOS开发
CSS3 转换,深入理解Flutter动画原理,前端基础图形
CSS3 转换,深入理解Flutter动画原理,前端基础图形