Flutter 源码系列:DropdownButton 源码浅析
source link: https://www.tuicool.com/articles/jeiYZjV
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
作为源码浅析系列的文章,我想说一下:
我发现很多人对于各种 widget 的使用不是很理解,经常会在群里问一些比较简单的问题,例如 TextField 如何监听确认按钮。
而关于Flutter 中控件的使用及实现方式,其实只要耐下心来好好的看一下它的构造函数和源码,都能看得懂。
而且我打算这个系列也不会讲的很深,也就是围绕这两点:1、构造函数 2、实现方式。
DropdownButton 构造函数及简单使用
其实关于 DropdownButton
的构造函数和简单使用我在上一篇文章中已经有过讲解,
如有不懂怎么用的,可以看这篇文章: Flutter DropdownButton简单使用及魔改源码 。
下面重点说一下 DropdownButton
是如何实现的。
DropdownButton 的实现
我们需要带着如下几个问题去看源码:
1. DropdownButton 是用什么来实现的? 2. 在点击 DropdownButton 的时候发生了什么? 3. 为什么每次弹出的位置都是我上次选择item的位置?
带着如上问题,我们开始。
DropdownButton 是用什么实现的?
我们在上一篇文章中已经了解到,DropdownButton 是一个 statefulWidget,那我们想要了解他是如何实现的,就直接跳转到他的 _DropdownButtonState 类中。
二话不说,直接找到 build(BuildContext context)
方法。
Return 了什么
先看看 return 了个什么:
return Semantics(
button: true,
child: GestureDetector(
onTap: _enabled ? _handleTap : null,
behavior: HitTestBehavior.opaque,
child: result,
),
);
可以看到返回了一个 Semantics
,这个控件简单来说就是用于视障人士的,对于我们正常APP来说可用可不用,如果是特殊的APP,那么建议使用。
然后下面 child 返回了一个手势:
1. onTap:判断是否可用,如果可用则走 handleTap
方法,如果不可用就算了。 2. behavior:设置在命中的时候如何工作: HitTestBehavior.opaque
为不透明的可以被选中 3. child:返回了 result
Result 是什么
不看点击方法,先来找到 result:
Widget result = DefaultTextStyle(
style: _textStyle,
child: Container(
padding: padding.resolve(Directionality.of(context)),
height: widget.isDense ? _denseButtonHeight : null,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
widget.isExpanded ? Expanded(child: innerItemsWidget) : innerItemsWidget,
IconTheme(
data: IconThemeData(
color: _iconColor,
size: widget.iconSize,
),
child: widget.icon ?? defaultIcon,
),
],
),
),
);
我们可以看到,其实result 最终是一个 Row,里面一共有两个 widget:
1. innerItemsWidget 2. Icon
样子如下:
其中 One 就是 innerItemsWidget
,箭头就是 Icon。
而且 innerItemsWidget
判断了是否是展开状态,如果是展开状态则套一个 Expanded 来水平填充父级。
innerItemsWidget 是什么
接着往上面找:
// 如果值为空(则_selectedindex为空),或者如果禁用,则显示提示或完全不显示。
final int index = _enabled ? (_selectedIndex ?? hintIndex) : hintIndex;
Widget innerItemsWidget;
if (items.isEmpty) {
innerItemsWidget = Container();
} else {
innerItemsWidget = IndexedStack(
index: index,
alignment: AlignmentDirectional.centerStart,
children: items,
);
}
从这我们可以看得出来, innerItemsWidget
是一个 IndexedStack
,
它把所有的 item 都罗列到了一起,用 index 来控制展示哪一个。
那看到这我们也就明白了,其实 DropdownButton
就是一个 IndexedStack
。
那这样来说,主要的逻辑应该在点击事件里。
在点击 DropdownButton 的时候发生了什么?
上面我们在 return 的时候看到了,在 onTap 的时候调用的是 _handleTap()
方法。
那我们直接来看一下:
void _handleTap() {
final RenderBox itemBox = context.findRenderObject();
final Rect itemRect = itemBox.localToGlobal(Offset.zero) & itemBox.size;
final TextDirection textDirection = Directionality.of(context);
final EdgeInsetsGeometry menuMargin = ButtonTheme.of(context).alignedDropdown
?_kAlignedMenuMargin
: _kUnalignedMenuMargin;
assert(_dropdownRoute == null);
_dropdownRoute = _DropdownRoute<T>(
items: widget.items,
buttonRect: menuMargin.resolve(textDirection).inflateRect(itemRect),
padding: _kMenuItemPadding.resolve(textDirection),
selectedIndex: 0,
elevation: widget.elevation,
theme: Theme.of(context, shadowThemeOnly: true),
style: _textStyle,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
);
Navigator.push(context, _dropdownRoute).then<void>((_DropdownRouteResult<T> newValue) {
_dropdownRoute = null;
if (!mounted || newValue == null)
return;
if (widget.onChanged != null)
widget.onChanged(newValue.result);
});
}
首先上面定义了几个 final 的变量,这些变量就是一些参数,见名知意。
后面重点来了:
1. 首先定义了一个 _DropdownRoute
2. 然后跳转该 route,并且在返回的时候把该 route 置空。
_DropdownRoute
首先我们来看一下 _DropdownRoute
,上篇文章魔改代码的时候也已经说过,
_DropdownRoute
继承自 PopupRoute
,是一个浮在当前页面上的 route。
然后我们找到他 buildPage
方法:
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return _DropdownRoutePage<T>(
route: this,
constraints: constraints,
items: items,
padding: padding,
buttonRect: buttonRect,
selectedIndex: selectedIndex,
elevation: elevation,
theme: theme,
style: style,
);
}
);
}
可以看到这里是返回了一个 LayoutBuilder
。
LayoutBuilder
最有用的是他可以知道该父级的大小和约束,通过该约束我们就可以做一些操作。
并且我们也看到确实是给 _DropdownRoutePage
传入了 constraints .
_DropdownRoutePage
如上, _DropdownRoute
返回了 _DropdownRoutePage
,那下面就来看一下它,
_DropdownRoutePage
是一个无状态的小部件,我们也是直接来看一下 build 方法的 return:
return MediaQuery.removePadding(
context: context,
removeTop: true,
removeBottom: true,
removeLeft: true,
removeRight: true,
child: Builder(
builder: (BuildContext context) {
return CustomSingleChildLayout(
delegate: _DropdownMenuRouteLayout<T>(
buttonRect: buttonRect,
menuTop: menuTop,
menuHeight: menuHeight,
textDirection: textDirection,
),
child: menu,
);
},
),
);
首先 MediaQuery.removePadding
是创建一个给定的 context 的 MediaQuery,但是删除了 padding。最后通过 CustomSingleChildLayout
返回了 menu
。
其中 delegate 为自定义的 _DropdownMenuRouteLayout
,这里主要是给定一些约束和控制了位置,这里不在本节内容当中,所以不过多的讲解。
到这里点击的逻辑就结束了,主要就是弹出了一个 PopupRoute
。
为什么每次弹出的位置都是我上次选择item的位置?
上面可以看到在点击的时候跳转到了 _DropdownRoute
,而 _DropdownRoute
最终返回了一个 _DropdownMenu
。
_DropdownMenu
_DropdownMenu
是一个有状态的小部件,那我们直接看它的 _State.
还是找到 build 方法,看一下都返回了什么:
return FadeTransition(
opacity: _fadeOpacity,
child: CustomPaint(
painter: _DropdownMenuPainter(
color: Theme.of(context).canvasColor,
elevation: route.elevation,
selectedIndex: route.selectedIndex,
resize: _resize,
),
child: Semantics(
scopesRoute: true,
namesRoute: true,
explicitChildNodes: true,
label: localizations.popupMenuLabel,
child: Material(
type: MaterialType.transparency,
textStyle: route.style,
child: ScrollConfiguration(
behavior: const _DropdownScrollBehavior(),
child: Scrollbar(
child: ListView(
controller: widget.route.scrollController,
padding: kMaterialListPadding,
itemExtent: _kMenuItemHeight,
shrinkWrap: true,
children: children,
),
),
),
),
),
),
);
首先是返回了一个自定义组件,自定义组件里的逻辑是: 根据当前选中的 index 来画展开的方框 :
就是外面带阴影的那个框。
代码如下:
@override
void paint(Canvas canvas, Size size) {
final double selectedItemOffset = selectedIndex * _kMenuItemHeight + kMaterialListPadding.top;
final Tween<double> top = Tween<double>(
begin: selectedItemOffset.clamp(0.0, size.height - _kMenuItemHeight),
end: 0.0,
);
final Tween<double> bottom = Tween<double>(
begin: (top.begin + _kMenuItemHeight).clamp(_kMenuItemHeight, size.height),
end: size.height,
);
final Rect rect = Rect.fromLTRB(0.0, top.evaluate(resize), size.width, bottom.evaluate(resize));
_painter.paint(canvas, rect.topLeft, ImageConfiguration(size: rect.size));
}
这里就不多说,有兴趣的可以自行看一下。
然后最终返回了一个 ListView,我们可以去看一下这个 children:
final List<Widget> children = <Widget>[];
for (int itemIndex = 0; itemIndex < route.items.length; ++itemIndex) {
CurvedAnimation opacity;
if (itemIndex == route.selectedIndex) {
opacity = CurvedAnimation(parent: route.animation, curve: const Threshold(0.0));
} else {
final double start = (0.5 + (itemIndex + 1) * unit).clamp(0.0, 1.0);
final double end = (start + 1.5 * unit).clamp(0.0, 1.0);
opacity = CurvedAnimation(parent: route.animation, curve: Interval(start, end));
}
children.add(FadeTransition(
opacity: opacity,
child: InkWell(
child: Container(
padding: widget.padding,
child: route.items[itemIndex],
),
onTap: () => Navigator.pop(
context,
_DropdownRouteResult<T>(route.items[itemIndex].value),
),
),
));
}
children
当中最主要的逻辑有三个:
1. 如果是已经选中的index,则不显示透明动画 2. 如果不是选中的 index,则根据 index 来控制透明动画延时时间,来达到效果 3. 点击时用 Navigator.pop
来返回选中的值
到这里我们就把 material/dropdown.dart
中所有的代码看了一遍。
总结
把源码看完,我们可以来进行总结一下:
1. 未展开的 DropdownButton 是一个 IndexStack 2. 展开的 DropdownButton 是通过 PopupRoute 浮在当前页上面的 ListView 3. 展开时通过计算当前选中的 index 来进行绘制背景,以达到效果
通过查看源码,我们是不是可以进行举一反三:
1. 是否可以使用 PopupRoute 来实现一些功能? 2. 是否可以使用 IndexStack 来实现一些功能? 3. 是否学会了一点自定义 widget 的知识?
其实个人认为,查看源码,不仅仅可以学到当前组件是如何实现的,
而且在查看源码的过程中,会遇到非常多的问题,这些问题都会促使我们去查文档,查资料,
这难道不也是一个学习的过程么。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK