Flutter DropdownButton簡單使用及魔改源碼

咱們通常在寫業務的時候多會用到下拉菜單,git

前面講過 ExpansionPanelExpansionPanel大部分狀況用來實現展開列表等稍微複雜的業務邏輯。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);
複製代碼

挑幾個重要的參數解釋一下:

  • items:類型爲 List<DropdownMenuItem<T>>,沒必要多說,天然是咱們下拉出現的列表
  • value:當前選定的值,若是沒有選擇任何一個,則爲空。
  • disabledHint:禁用下拉列表的時候顯示的消息。
  • onChanged:當用戶選擇了其中一個值得時候調用
  • underline:用於繪製按鈕下劃線的 widget
  • isDense:是否下降按鈕的高度

剩下的看名字應該也能瞭解個大概了。

剛纔咱們看到的圖中是有下劃線的,若是想去除下劃線的話,簡單能夠這麼操做:underline: Container(),

也可使用 DropdownButtonHideUnderline 包裹住 DropdownButton

簡單魔改源碼

若是需求是以下樣式:

Kapture 2019-06-27 at 16.40.48

點擊彈出列表在下方,該如何寫?

剛纔在上面的圖也看到了,每次點擊更改後,下次展開就會以上次點擊的 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

點進 _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

_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/…

相關文章
相關標籤/搜索