咱們通常在寫業務的時候多會用到下拉菜單,git
前面講過 ExpansionPanel
, ExpansionPanel
大部分狀況用來實現展開列表等稍微複雜的業務邏輯。github
而 DropdownButton
則是用來實現稍微簡單一點的 點擊選擇 業務場景。markdown
按照慣例咱們查看一下官方文檔上的說明:less
A material design button for selecting from a list of items.ide
用於從 item 列表中進行選擇的 material 按鈕。函數
說明的下方就是一大段的 demo,咱們先來看一下效果:源碼分析
沒錯,不要懷疑,One, Two, Free, Four,這就是官方 demo 上寫的。ui
代碼以下:this
String dropdownValue = 'One';
// ...
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: DropdownButton<String>(
value: dropdownValue,
onChanged: (String newValue) {
setState(() {
dropdownValue = newValue;
});
},
items: <String>['One', 'Two', 'Free', 'Four']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
})
.toList(),
),
),
);
}
複製代碼
這樣就簡單的實現了上圖的效果,如今先來看一下他的構造函數。spa
構造函數代碼以下:
DropdownButton({
Key key,
@required this.items,
this.value,
this.hint,
this.disabledHint,
@required this.onChanged,
this.elevation = 8,
this.style,
this.underline,
this.icon,
this.iconDisabledColor,
this.iconEnabledColor,
this.iconSize = 24.0,
this.isDense = false,
this.isExpanded = false,
}) : assert(items == null || items.isEmpty || value == null || items.where((DropdownMenuItem<T> item) => item.value == value).length == 1),
assert(elevation != null),
assert(iconSize != null),
assert(isDense != null),
assert(isExpanded != null),
super(key: key);
複製代碼
挑幾個重要的參數解釋一下:
List<DropdownMenuItem<T>>
,沒必要多說,天然是咱們下拉出現的列表剩下的看名字應該也能瞭解個大概了。
剛纔咱們看到的圖中是有下劃線的,若是想去除下劃線的話,簡單能夠這麼操做:underline: Container(),
也可使用 DropdownButtonHideUnderline
包裹住 DropdownButton
。
若是需求是以下樣式:
點擊彈出列表在下方,該如何寫?
剛纔在上面的圖也看到了,每次點擊更改後,下次展開就會以上次點擊的 index 做爲關鍵點來展開。
那對於這種需求,咱們只能 魔改源碼。
俗話說得好:
魔改一時爽,一直魔改一直爽。
那咱們首先找到 _DropdownButtonState
裏的點擊方法,看看他是如何寫的:
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: _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);
});
}
複製代碼
前面定義了一大堆變量,不重要,咱們只關心咱們想要的,
能夠看到在 _dropdownRoute
中傳入了一個 selectedIndex
,那咱們就能夠想象的到,這確定就是問題的根源。
先把它改爲 0 試試:
能夠看得出來,效果已經實現了大半,可仍是遮擋住了最開始的 button,
這個時候就要深刻到 _DropdownRoute
當中。
點進 _DropdownRoute
的源碼,能夠看到,他是繼承自 PopupRoute
,
class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
_DropdownRoute({
this.items,
this.padding,
this.buttonRect,
this.selectedIndex,
this.elevation = 8,
this.theme,
@required this.style,
this.barrierLabel,
}) : assert(style != null);
}
複製代碼
PopupRoute
是能夠覆蓋在當前 route 上的小部件模式的 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,
);
}
);
}
複製代碼
該方法返回了一個 _DropdownRoutePage
, 那咱們繼續深刻。
_DropdownRoutePage
是一個 StatelessWidget
,那咱們直接找到 build
方法,
@override
Widget build(BuildContext context) {
/// ...
final double topLimit = math.min(_kMenuItemHeight, buttonTop);
final double bottomLimit = math.max(availableHeight - _kMenuItemHeight, buttonBottom);
final double selectedItemOffset = selectedIndex * _kMenuItemHeight + kMaterialListPadding.top;
/// ...
return MediaQuery.removePadding(
/// ...
);
}
複製代碼
省略一些無用代碼,來關注咱們所要關注的點,
上面代碼中有一個變量 selectedItemOffset
,該變量就是咱們選中的 item 的偏移量,咱們只要改掉這個值,就能夠完成咱們的需求。
那這個值應該設爲多少?
很快咱們就能想到應該是點擊 button 的高度再加上一點間距,
若是獲取這個高度?
上面構建 _DropdownRoutePage
的時候已經給咱們傳入了一個參數:buttonRect
,根據這個咱們就能夠獲得點擊 button 的高度了。
那該變量改成:
final double selectedItemOffset = (buttonRect.height + 10) * -1;
複製代碼
最後必定要乘 -1,這樣就完成了咱們上圖的效果。
咱們在想要定製需求的時候,能夠先判斷一下原生的控件是否大部分知足咱們的需求,
若是大部分已經知足,那麼就能夠直接魔改源碼。
Flutter 的源碼真的是給與咱們極大的方便,每一種控件都在一個文件內,咱們直接複製出來就能夠改。
最後再說一句:魔改一時爽,一直魔改一直爽。
後續我會推出一系列的源碼分析文章,下一篇就是分析 DropdownButton
,敬請關注。
完整代碼已經傳至GitHub:github.com/wanglu1209/…