本文仅做分析。项目作者见 github
-
GitHub 源地址: direct-select-flutter
先上预览图:

三个文件:
- direct_select_list.dart
- direct_select_item.dart
- direct_select_container.dart
先让我们看一下用法
点击查看代码
//...
return Scaffold(
key: scaffoldKey,
body: DirectSelectContainer(
child: Column(
children: [
SizedBox(height: 200.0),
MealSelector(data: _meals, label: "To which meal?"),
],
),
),
);
//...
class MealSelector extends StatelessWidget {
final buttonPadding = const EdgeInsets.fromLTRB(0, 8, 0, 0);
final List data;
final String label;
MealSelector({@required this.data, @required this.label});
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
alignment: AlignmentDirectional.centerStart,
margin: EdgeInsets.only(left: 4),
child: Text(label)),
Padding(
padding: buttonPadding,
child: Container(
decoration: BoxDecoration(
boxShadow: [
new BoxShadow(
color: Colors.black.withOpacity(0.06),
spreadRadius: 4,
offset: new Offset(0.0, 0.0),
blurRadius: 15.0,
),
],
),
child: Card(
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Padding(
child: DirectSelectList(
values: data,
defaultItemIndex: 0,
itemBuilder: (String value) {
return DirectSelectItem(
itemHeight: 56,
value: value,
itemBuilder: (context, value) {
return Text(value);
});
},
focusedItemDecoration: BoxDecoration(
border: BorderDirectional(
bottom: BorderSide(width: 1, color: Colors.black12),
top: BorderSide(width: 1, color: Colors.black12),
),
),
onItemSelectedListener:
(value, selectedIndex, context) {},
onUserTappedListener: () {},
),
padding: EdgeInsets.only(left: 12),
),
),
Padding(
padding: EdgeInsets.only(right: 8),
child: Icon(
Icons.unfold_more,
color: Colors.blueAccent,
),
)
],
),
),
),
),
],
);
}
}
MealSelector
是对 DirectSelectList<T>
的一个外观上的包装。它大概长这样:

MealSelector
在一开始,我们先忽略 DirectSelectContainer
, 该 widget 用于包裹当前已选择菜单和菜单列表,同时对外提供方法以控制菜单列表,稍后解释。
DirectSelectList<T>
的 values
接受 List<T>
类型,用于构建菜单内容。defaultItemIndex
表示默认选中的菜单索引,itemBuilder
用于构建单个“菜单项目”,如下图:

focusedItemDecoration
,用于着重标识当前选中的item,如下图:

onItemSelectedListener
和onUserTappedListener
都是回调,onItemSelectedListener
是指选item后的回调,onUserTappedListener
是仅仅用户点击了这个控件而产生的回调。
我们可以看到在 itemBuilder 下,我们传入了 :
DirectSelectItem<String>(
itemHeight: 56,
value: value,
itemBuilder: (context, value) {
return Text(value);
})
下面来分析一下 DirectSelectItem<T>。
direct_select_item.dart # DirectSelectItem<T>
点击查看代码
class DirectSelectItem extends StatefulWidget {
//Value of item
final T value;
//定义此项目是否被选中
final bool isSelected;
//列表中项目的高度
final double itemHeight;
//初始项目大小
final scale = ValueNotifier(1.0);
//未选中的项目不透明度
final opacity = ValueNotifier(0.5);
//值越大,最大比例减少
final scaleFactor;
final Widget Function(BuildContext context, T value) itemBuilder;
DirectSelectItem({
Key key,
this.scaleFactor = 4.0,
@required this.value,
@required this.itemBuilder,
this.itemHeight = 48.0,
this.isSelected = false,
}) : super(key: key);
@override
State createState() {
return DirectSelectItemState(isSelected: isSelected);
}
void updateScale(double scale) {
this.scale.value = scale;
}
void updateOpacity(double opacity) {
this.opacity.value = opacity;
}
Widget getSelectedItem(GlobalKey animatedStateKey,
GlobalKey paddingGlobalKey) {
return RectGetter(
key: paddingGlobalKey,
child: DirectSelectItem(
value: value,
key: animatedStateKey,
itemHeight: itemHeight,
itemBuilder: itemBuilder,
isSelected: true,
),
);
}
}
class DirectSelectItemState extends State>
with SingleTickerProviderStateMixin {
final bool isSelected;
AnimationController animationController;
Animation _animation;
Tween _tween;
DirectSelectItemState({this.isSelected = false});
bool isScaled = false;
Future runScaleTransition({@required bool reverse}) {
if (reverse) {
return animationController.reverse();
} else {
return animationController.forward(from: 0.0);
}
}
@override
void initState() {
super.initState();
animationController =
AnimationController(duration: Duration(milliseconds: 150), vsync: this);
_tween = Tween(begin: 1.0, end: 1 + 1 / widget.scaleFactor);
_animation = _tween.animate(animationController)
..addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
if (isSelected) {
return Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Container(
color: Colors.transparent,
height: widget.itemHeight,
alignment: AlignmentDirectional.centerStart,
child: Transform.scale(
scale: _animation.value,
alignment: Alignment.topLeft,
child: widget.itemBuilder(context, widget.value)),
),
),
],
);
} else {
return Material(
color: Colors.transparent,
child: Container(
child: ValueListenableBuilder(
valueListenable: widget.scale,
builder: (context, value, child) {
return Opacity(
opacity: widget.opacity.value,
child: Container(
height: widget.itemHeight,
child: Transform.scale(
scale: value,
alignment: Alignment.topLeft,
child: widget.itemBuilder(context, widget.value)),
alignment: AlignmentDirectional.centerStart),
);
},
),
),
);
}
}
@override
void dispose() {
super.dispose();
animationController.dispose();
}
}
具体看一下 DirectSelectItem<T>
后,可以发现这是菜单单个item widget,
在我们滑动菜单的时候,改变了单个item的大小和不透明度。所有在 DirectSelectItem<T>
中, scale
和 opacity
都使用了 ValueNotifier<double>
。
那么什么是 ValueNotifier<T>
呢?它是一个可以监听widget 变量,当变量值发生变化时,可以自动更新 widget。下面是一个示例:
点击查看代码
class BtnWrap extends StatefulWidget {
final text = ValueNotifier('default');
void setText(String text) {
this.text.value = text;
}
@override
_BtnWrapState createState() => _BtnWrapState();
}
class _BtnWrapState extends State {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: widget.text,
builder: (context, value, child) {
return MaterialButton(
onPressed: () {
widget.text.value = 'NEW';
},
child: Text(value),
);
},
);
}
}
解释完 ValueNotifier<T>
后,我们回过头看看:在 DirectSelectItemState<T>
中,我们通过 runScaleTransition
来控制item的缩放动画 (Transform.scale
)。
在 DirectSelectItemState<T>
build 方法中通过 isSelected 来构建已选中的item 和 未选中的item,如图:


direct_select_list.dart # DirectSelectList<T>
DirectSelectItem<T>
大致介绍完成,下面我们看看 DirectSelectList<T>
:
点击查看代码
class PaddingItemController {
GlobalKey paddingGlobalKey = RectGetter.createGlobalKey();
}
typedef DirectSelectItemsBuilder = DirectSelectItem Function(T value);
//typedef ItemSelected = Future Function(
// DirectSelectList owner, double location);
/// Widget that contains items and responds to user's interaction
/// Usage Example
///
/// final dsl2 = DirectSelectList(
/// values: _numbers,
/// itemBuilder: (String value) => getDropDownMenuItem(value),
/// focusedItemDecoration: _getDslDecoration());
class DirectSelectList extends StatefulWidget {
//Item widgets
final List> items;
//Current focused item overlay
final Decoration focusedItemDecoration;
//Default selected item index
//默认所选项目索引
final int defaultItemIndex;
//Notifies state about new item selected
//通知状态有关选定的新项目
final ValueNotifier selectedItem;
//Function to execute when item selected
// //选择项目时执行的功能
final void Function(T value, int selectedIndex, BuildContext context)
onItemSelectedListener;
//Callback for action when user just tapped instead of hold and scroll
//用户仅点击而不是按住并滚动时的回调操作
final VoidCallback onUserTappedListener;
final PaddingItemController paddingItemController = PaddingItemController();
DirectSelectList({
Key key,
@required List values,
@required DirectSelectItemsBuilder itemBuilder,
this.onItemSelectedListener,
this.focusedItemDecoration,
this.defaultItemIndex = 0,
this.onUserTappedListener,
}) : items = values.map((val) => itemBuilder(val)).toList(),
selectedItem = ValueNotifier(defaultItemIndex),
assert(defaultItemIndex + 1 <= values.length + 1),
super(key: key);
@override
State createState() {
return _DirectSelectState();
}
//todo pass item height in this class and build items with that height
//todo 在此类中传递项目高度并使用该高度构建项目
double itemHeight() {
if (items != null && items.isNotEmpty) {
return items.first.itemHeight;
}
return 0.0;
}
int getSelectedItemIndex() {
if (selectedItem != null) {
return selectedItem.value;
} else {
return 0;
}
}
void setSelectedItemIndex(int index) {
if (selectedItem != null && index != selectedItem.value) {
selectedItem.value = index;
}
}
T getSelectedItem() {
return items[selectedItem.value].value;
}
}
class _DirectSelectState extends State> {
final GlobalKey animatedStateKey =
GlobalKey();
Future Function(DirectSelectList, double) onTapEventListener;
void Function(double) onDragEventListener;
/// 标识list是否可见
bool isOverlayVisible = false;
int lastSelectedItem;
bool _isShowUpAnimationRunning = false;
Map selectedItemWidgets = Map();
@override
void initState() {
super.initState();
lastSelectedItem = widget.defaultItemIndex;
_updateSelectItemWidget();
}
@override
void didUpdateWidget(DirectSelectList oldWidget) {
widget.paddingItemController.paddingGlobalKey =
oldWidget.paddingItemController.paddingGlobalKey;
_updateSelectItemWidget();
super.didUpdateWidget(widget);
}
void _updateSelectItemWidget() {
selectedItemWidgets.clear();
for (int index = 0; index < widget.items.length; index++) {
selectedItemWidgets.putIfAbsent(
index,
() => widget.items[index].getSelectedItem(
animatedStateKey,
widget.paddingItemController.paddingGlobalKey,
),
);
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final dsListener = DirectSelectContainer.of(context);
assert(dsListener != null,
"A DirectSelectList must inherit a DirectSelectContainer!");
this.onTapEventListener = dsListener.toggleListOverlayVisibility;
this.onDragEventListener = dsListener.performListDrag;
}
@override
Widget build(BuildContext context) {
widget.selectedItem.addListener(() {
if (widget.onItemSelectedListener != null) {
widget.onItemSelectedListener(
widget.items[widget.selectedItem.value].value,
widget.selectedItem.value,
this.context);
}
});
bool transitionEnded = false;
// 使用 ValueListenableBuilder 数值同步
return ValueListenableBuilder(
valueListenable: widget.selectedItem,
builder: (context, value, child) {
final selectedItem = selectedItemWidgets[value];
return GestureDetector(
child: selectedItem,
// 点击
onTap: () {
// 回调点击事件
if (widget.onUserTappedListener != null) {
widget.onUserTappedListener();
}
},
// 按下
onTapDown: (tapDownDetails) async {
if (!isOverlayVisible) {
transitionEnded = false;
_isShowUpAnimationRunning = true;
await animatedStateKey.currentState
.runScaleTransition(reverse: false);
if (!transitionEnded) {
await _showListOverlay(_getItemTopPosition(context));
_isShowUpAnimationRunning = false;
lastSelectedItem = value;
}
}
},
// 按下后抬起
onTapUp: (tapUpDetails) async {
await _hideListOverlay(_getItemTopPosition(context));
animatedStateKey.currentState.runScaleTransition(reverse: true);
},
// 通过主按钮与屏幕接触并垂直移动的指针已沿垂直方向移动。
onVerticalDragUpdate: (dragInfo) {
if (!_isShowUpAnimationRunning) {
_showListOverlay(dragInfo.primaryDelta);
}
},
onVerticalDragEnd: (dragDetails) async {
transitionEnded = true;
_dragEnd();
},
onHorizontalDragEnd: (horizontalDetails) async {
transitionEnded = true;
_dragEnd();
},
);
});
}
void _dragEnd() async {
await _hideListOverlay(_getItemTopPosition(context));
animatedStateKey.currentState.runScaleTransition(reverse: true);
}
double _getItemTopPosition(BuildContext context) {
final RenderBox itemBox = context.findRenderObject();
final Rect itemRect = itemBox.localToGlobal(Offset.zero) & itemBox.size;
return itemRect.top;
}
_hideListOverlay(double dy) async {
if (isOverlayVisible) {
isOverlayVisible = false;
//fix to prevent stuck scale if selected item is the same as previous
await onTapEventListener(widget, dy);
if (lastSelectedItem == widget.selectedItem.value) {
animatedStateKey.currentState.runScaleTransition(reverse: true);
}
}
}
_showListOverlay(double dy) {
if (!isOverlayVisible) {
isOverlayVisible = true;
onTapEventListener(widget, _getItemTopPosition(context));
} else {
onDragEventListener(dy);
}
}
}
可以看到 selectedItem
也用 ValueNotifier<int>
来定义,selectedItem
存储当前选择的 item index 。
onItemSelectedListener
, onUserTappedListener
均为用户操作回调。事件回调具体用 GestureDetector
来实现:
GestureDetector
触发点击事件顺序为 onTapDown
-> onTapUp
-> onTap
// 按下
onTapDown: (tapDownDetails) async {
if (!isOverlayVisible) {
transitionEnded = false;
_isShowUpAnimationRunning = true;
await animatedStateKey.currentState
.runScaleTransition(reverse: false);
if (!transitionEnded) {
await _showListOverlay(_getItemTopPosition(context));
_isShowUpAnimationRunning = false;
lastSelectedItem = value;
}
}
},
onTapDown
把 flag 设置完成,且通过 animatedStateKey 调用 DirectSelectItem<T>
的 runScaleTransition
方法放大菜单,该方法上面介绍过,改变菜单的大小。同时调用 _showListOverlay
展开菜单列表。
onTapUp: (tapUpDetails) async {
await _hideListOverlay(_getItemTopPosition(context));
animatedStateKey.currentState.runScaleTransition(reverse: true);
},
onTapUp
当用户抬起手指,调用 _hideListOverlay
隐藏列表,同时调用 runScaleTransition 缩小菜单。
PS: _hideListOverlay
和 _showListOverlay
最终会调用 onTapEventListener
或 onDragEventListener
。这两个监听器的实现,后面会详解。
onTap: () {
// 回调点击事件
if (widget.onUserTappedListener != null) {
widget.onUserTappedListener();
}
},
onTap
调用用户传递的点击事件。
onTapDown
-> onTapUp
-> onTap
最终的动画效果:

到这,长按动画就分析完了。至于 滑动 (onVerticalDragUpdate
,onVerticalDragEnd
, onHorizontalDragEnd
),会放在后面进行分析。
direct_select_container.dart # DirectSelectContainer
& DirectSelectContainerState
点击查看代码
class DirectSelectContainer extends StatefulWidget {
//Actually content of screen
//实际画面内容
final Widget child;
//How fast list is scrolled
//列表滚动速度
final int dragSpeedMultiplier;
final Decoration decoration;
const DirectSelectContainer({
Key key,
this.child,
this.dragSpeedMultiplier = 2,
this.decoration,
}) : super(key: key);
@override
State createState() {
return DirectSelectContainerState();
}
static DirectSelectGestureEventListeners of(BuildContext context) {
if (context.dependOnInheritedWidgetOfExactType<
_InheritedContainerListeners>() ==
null) {
throw Exception(
"A DirectSelectList must inherit a DirectSelectContainer!");
}
return context
.dependOnInheritedWidgetOfExactType<_InheritedContainerListeners>()
.listeners;
}
}
class DirectSelectContainerState extends State
with SingleTickerProviderStateMixin
implements DirectSelectGestureEventListeners {
bool isOverlayVisible = false;
ScrollController _scrollController;
DirectSelectList _currentList =
DirectSelectList(itemBuilder: (val) => null, values: []);
double _currentScrollLocation = 0;
double _adjustedTopOffset = 0.0;
AnimationController fadeAnimationController;
int lastSelectedItem = 0;
double listPadding = 0.0;
final scrollToListElementAnimationDuration = Duration(milliseconds: 200);
final fadeAnimationDuration = Duration(milliseconds: 200);
@override
void initState() {
super.initState();
fadeAnimationController = AnimationController(
duration: fadeAnimationDuration,
vsync: this,
);
}
@override
Widget build(BuildContext context) {
double topOffset = 0.0;
RenderObject object = context.findRenderObject();
if (object?.parentData is ContainerBoxParentData) {
topOffset = (object.parentData as ContainerBoxParentData).offset.dy;
}
listPadding = MediaQuery.of(context).size.height;
_adjustedTopOffset = _currentScrollLocation - topOffset;
_scrollController = ScrollController(
initialScrollOffset: listPadding -
_currentScrollLocation +
topOffset +
_currentList.getSelectedItemIndex() * _currentList.itemHeight());
return Stack(
children: [
_InheritedContainerListeners(
listeners: this,
child: widget.child,
),
Visibility(
visible: isOverlayVisible,
child: FadeTransition(
opacity: fadeAnimationController
.drive(CurveTween(curve: Curves.easeOut)),
child: Column(
children: [
Expanded(
child: Stack(
children: [
_getListWidget(),
_getSelectionOverlayWidget(),
],
),
),
],
),
),
)
],
);
}
Widget _getListWidget() {
var paddingLeft = 0.0;
if (_currentList.items.isNotEmpty) {
Rect rect = RectGetter.getRectFromKey(
_currentList.paddingItemController.paddingGlobalKey);
if (rect != null) {
paddingLeft = rect.left;
}
}
Decoration dslContainerDecoration;
if (widget.decoration == null) {
final theme = Theme.of(context);
dslContainerDecoration =
BoxDecoration(color: theme.scaffoldBackgroundColor);
} else {
dslContainerDecoration = widget.decoration;
}
return Container(
decoration: dslContainerDecoration,
child: ListView.builder(
padding: EdgeInsets.only(left: paddingLeft),
controller: _scrollController,
itemCount: _currentList.items.length + 2,
itemBuilder: (BuildContext context, int index) {
if (index == 0 || index == _currentList.items.length + 1) {
return Container(height: listPadding);
}
final item = _currentList.items[index - 1];
final normalScale = 1.0;
if (lastSelectedItem == index - 1) {
item.updateScale(_calculateNewScale(normalScale));
} else {
item.updateScale(normalScale);
}
return item;
},
));
}
Widget _getSelectionOverlayWidget() {
return Positioned(
top: _adjustedTopOffset,
left: 0,
right: 0,
height: _currentList.itemHeight(),
child: Container(
height: _currentList.itemHeight(),
decoration: _currentList.focusedItemDecoration != null
? _currentList.focusedItemDecoration
: BoxDecoration()));
}
void performListDrag(double dragDy) {
try {
if (_scrollController != null && _scrollController.position != null) {
final currentScrollOffset = _scrollController.offset;
double allowedOffset = _allowedDragDistance(
currentScrollOffset + _adjustedTopOffset,
dragDy * widget.dragSpeedMultiplier);
if (allowedOffset != 0.0) {
_scrollController.jumpTo(currentScrollOffset + allowedOffset);
final scrollPixels =
_scrollController.offset - listPadding + _adjustedTopOffset;
final selectedItemIndex = _getCurrentListElementIndex(scrollPixels);
lastSelectedItem = selectedItemIndex;
_performScaleTransformation(scrollPixels, selectedItemIndex);
}
}
} catch (e) {
print(e);
}
}
double _allowedDragDistance(double currentScrollOffset, double position) {
double newPosition = currentScrollOffset + position;
double endOfListPosition =
(_currentList.items.length - 1) * _currentList.itemHeight() +
listPadding;
if (newPosition < listPadding) {
return listPadding - currentScrollOffset;
} else if (newPosition > endOfListPosition) {
return endOfListPosition - currentScrollOffset;
} else {
return position;
}
}
void _performScaleTransformation(double scrollPixels, int selectedItemIndex) {
final neighbourDistance = _getNeighbourListElementDistance(scrollPixels);
int neighbourIncrementDirection =
neighbourScrollDirection(neighbourDistance);
int neighbourIndex = lastSelectedItem + neighbourIncrementDirection;
double neighbourDistanceToCurrentItem =
_getNeighbourListElementDistanceToCurrentItem(neighbourDistance);
if (neighbourIndex < 0 || neighbourIndex > _currentList.items.length - 1) {
//incorrect neighbour index quit
return;
}
_currentList.items[selectedItemIndex].updateOpacity(1.0);
_currentList.items[neighbourIndex].updateOpacity(0.5);
_currentList.items[selectedItemIndex]
.updateScale(_calculateNewScale(neighbourDistanceToCurrentItem));
_currentList.items[neighbourIndex]
.updateScale(_calculateNewScale(neighbourDistance.abs()));
}
double _calculateNewScale(double distance) =>
1.0 + distance / _currentList.items[lastSelectedItem].scaleFactor;
int neighbourScrollDirection(double neighbourDistance) {
int neighbourScrollDirection = 0;
if (neighbourDistance > 0) {
neighbourScrollDirection = 1;
} else {
neighbourScrollDirection = -1;
}
return neighbourScrollDirection;
}
double _getNeighbourListElementDistanceToCurrentItem(
double neighbourDistance) {
double neighbourDistanceToCurrentItem = (1 - neighbourDistance.abs());
if (neighbourDistanceToCurrentItem > 1 ||
neighbourDistanceToCurrentItem < 0) {
neighbourDistanceToCurrentItem = 1.0;
}
return neighbourDistanceToCurrentItem;
}
int _getCurrentListElementIndex(double scrollPixels) {
int selectedElement = (scrollPixels / _currentList.itemHeight()).round();
final maxElementIndex = _currentList.items.length;
if (selectedElement < 0) {
selectedElement = 0;
}
if (selectedElement >= maxElementIndex) {
selectedElement = maxElementIndex - 1;
}
return selectedElement;
}
double _getNeighbourListElementDistance(double scrollPixels) {
double selectedElementDeviation =
(scrollPixels / _currentList.itemHeight());
int selectedElement = _getCurrentListElementIndex(scrollPixels);
return selectedElementDeviation - selectedElement;
}
Future toggleListOverlayVisibility(
final DirectSelectList visibleList, final double location) async {
if (isOverlayVisible) {
try {
await _scrollController.animateTo(
listPadding -
_adjustedTopOffset +
lastSelectedItem * _currentList.itemHeight(),
duration: scrollToListElementAnimationDuration,
curve: Curves.ease,
);
} catch (e) {} finally {
_currentList.setSelectedItemIndex(lastSelectedItem);
sleep(const Duration(milliseconds: 200));
await fadeAnimationController.reverse();
setState(() {
_hideListOverlay();
});
}
} else {
setState(() {
_showListOverlay(visibleList, location);
});
}
}
_showListOverlay(DirectSelectList visibleList, double location) async {
_currentList = visibleList;
_currentScrollLocation = location;
lastSelectedItem = _currentList.getSelectedItemIndex();
_currentList.items[lastSelectedItem].updateOpacity(1.0);
isOverlayVisible = true;
await fadeAnimationController.forward(from: 0.0);
}
void _hideListOverlay() {
_scrollController.dispose();
_scrollController = null;
_currentList.items[lastSelectedItem].updateScale(1.0);
_currentScrollLocation = 0;
_adjustedTopOffset = 0;
isOverlayVisible = false;
}
}
上面动画看起来似乎是菜单放大,然后出现待选菜单列表。但当我们区看 DirectSelectContainer
& DirectSelectContainerState
源码的时候会发现DirectSelectContainerState
它用了 Stack 包裹了已选择菜单(DirectSelectItem<T>
)和菜单列表(DirectSelectContainerState#_getListWidget()
),下面依旧图解:

上图对应的代码位置为 DirectSelectContainer#build
:
//.......
return Stack(
children: <Widget>[
_InheritedContainerListeners(
listeners: this,
child: widget.child,
),
Visibility(
visible: isOverlayVisible,
child: FadeTransition(
opacity: fadeAnimationController
.drive(CurveTween(curve: Curves.easeOut)),
child: Column(
children: <Widget>[
Expanded(
child: Stack(
children: <Widget>[
_getListWidget(),
_getSelectionOverlayWidget(),
],
),
),
],
),
),
)
],
);
//.......
_InheritedContainerListeners(
listeners: this,
child: widget.child,
),
在 DirectSelectContainer#build
的 Stack
中,_InheritedContainerListeners
它继承了 InheritedWidget
,InheritedWidget
是一个用于数据共享的 widget, 具体可以百度搜索。
direct_select_container.dart # DirectSelectGestureEventListeners
& _InheritedContainerListeners
要理清 DirectSelectContainerState
就必须要先看两个类(DirectSelectGestureEventListeners
, _InheritedContainerListeners
):
/// 接口以供 [_DirectSelectState] 调用。
class DirectSelectGestureEventListeners {
/// 控制菜单列表的显示
/// [list] 菜单列表
/// [location] 显示位置
toggleListOverlayVisibility(DirectSelectList list, double location) =>
throw 'Not implemented.';
/// 控制菜单滑动
/// [dragDy] 滑动值
performListDrag(double dragDy) => throw 'Not implemented';
}
/// Allows Direct Select List implementations to
/// 允许直接选择列表实现
class _InheritedContainerListeners extends InheritedWidget {
/// See [DirectSelectGestureEventListeners]
final DirectSelectGestureEventListeners listeners;
_InheritedContainerListeners({
Key key,
@required this.listeners,
@required Widget child,
}) : super(key: key, child: child);
@override
bool updateShouldNotify(_InheritedContainerListeners old) =>
old.listeners != listeners;
}
注释已经写的很明确了。_InheritedContainerListeners
内部存储着 DirectSelectGestureEventListeners
监听器,而 DirectSelectContainerState
又实现了这个接口。
DirectSelectContainer
对外提供一个方法,用于获取 DirectSelectGestureEventListeners
。
static DirectSelectGestureEventListeners of(BuildContext context) {
if (context.dependOnInheritedWidgetOfExactType<
_InheritedContainerListeners>() ==
null) {
throw Exception(
"A DirectSelectList must inherit a DirectSelectContainer!");
}
return context
.dependOnInheritedWidgetOfExactType<_InheritedContainerListeners>()
.listeners;
}
回过头看看:
_InheritedContainerListeners(
listeners: this,
child: widget.child,
),
listeners: this
直接指向 DirectSelectContainerState
本身。
child: widget.child
通用指向了 DirectSelectContainer#child
,而 DirectSelectContainer#child
又是通过构造传入的,回过头看看 DirectSelectContainer
用在哪:
DirectSelectContainer(
child: Column(
children: <Widget>[
SizedBox(height: 200.0),
MealSelector(data: _meals, label: "To which meal?"),
],
),
)
至于 MealSelector
开头有讲,它是一个 DirectSelectList<T>
的包装类 。
_InheritedContainerListeners
分析完成。
Visibility(
visible: isOverlayVisible,
child: FadeTransition(
opacity: fadeAnimationController
.drive(CurveTween(curve: Curves.easeOut)),
child: Column(
children: <Widget>[
Expanded(
child: Stack(
children: <Widget>[
_getListWidget(),
_getSelectionOverlayWidget(),
],
),
),
],
),
),
)
到目前为止
DirectSelectList<T>
DirectSelectItem<T>
DirectSelectContainer
这个三个主要类都大概的讲了一下。下面来简要概括一下:
DirectSelectList<T>
: 说白了就是一个 controller,它具备捕捉用户操作(点击、滑动)和控制 List (菜单列表),虽然它叫做 DirectSelectList 但是内部并没有任何列表控件。
DirectSelectItem<T>
: 单个菜单项目,其中主要靠 itemBuilder
来构建。在本案例 itemBuilder
返回 Text
:
itemBuilder: (context, value) {
return Text(value);
}
DirectSelectContainer
:内部包含了一个 ListView ,对外提供接口以便于控制 ListView。
到现在 DirectSelectContainer 分析的差不多了,可以回到 DirectSelectList<T>
内部,接着分析滑动操作:
onVerticalDragUpdate: (dragInfo) {
if (!_isShowUpAnimationRunning) {
_showListOverlay(dragInfo.primaryDelta);
}
},
onVerticalDragEnd: (dragDetails) async {
transitionEnded = true;
_dragEnd();
},
onHorizontalDragEnd: (horizontalDetails) async {
transitionEnded = true;
_dragEnd();
},
onVerticalDragUpdate
,也就是垂直滑动回调,内部先判断动画状态(flag),然后调用 _showListOverla
:
_showListOverlay(double dy) {
// 如果不可见,那么仅让他显示; 如果可见,那么调用拖拽
if (!isOverlayVisible) {
isOverlayVisible = true;
onTapEventListener(widget, _getItemTopPosition(context));
} else {
onDragEventListener(dy);
}
}
onTapEventListener,onDragEventListener定义如下:
Future Function(DirectSelectList, double) onTapEventListener;
void Function(double) onDragEventListener;
这两个 Listener 在 _DirectSelectState<T>#didChangeDependencies 内赋值。
@override
void didChangeDependencies() {
super.didChangeDependencies();
final dsListener = DirectSelectContainer.of(context);
assert(dsListener != null,
"A DirectSelectList must inherit a DirectSelectContainer!");
this.onTapEventListener = dsListener.toggleListOverlayVisibility;
this.onDragEventListener = dsListener.performListDrag;
}
首先通过 DirectSelectContainer.of(context)
来获取DirectSelectGestureEventListeners
。然后赋值到 onTapEventListener
,onDragEventListener
。
刚才分析过 DirectSelectGestureEventListeners
的实现类是 DirectSelectContainerState
。
分析一下这两个方法,我们显然能知道它的作用:
点击查看代码
Future toggleListOverlayVisibility(
final DirectSelectList visibleList, final double location) async {
if (isOverlayVisible) {
try {
await _scrollController.animateTo(
listPadding -
_adjustedTopOffset +
lastSelectedItem * _currentList.itemHeight(),
duration: scrollToListElementAnimationDuration,
curve: Curves.ease,
);
} catch (e) {} finally {
_currentList.setSelectedItemIndex(lastSelectedItem);
sleep(const Duration(milliseconds: 200));
await fadeAnimationController.reverse();
setState(() {
_hideListOverlay();
});
}
} else {
setState(() {
_showListOverlay(visibleList, location);
});
}
}
void performListDrag(double dragDy) {
try {
if (_scrollController != null && _scrollController.position != null) {
final currentScrollOffset = _scrollController.offset;
double allowedOffset = _allowedDragDistance(
currentScrollOffset + _adjustedTopOffset,
dragDy * widget.dragSpeedMultiplier);
if (allowedOffset != 0.0) {
_scrollController.jumpTo(currentScrollOffset + allowedOffset);
final scrollPixels =
_scrollController.offset - listPadding + _adjustedTopOffset;
final selectedItemIndex = _getCurrentListElementIndex(scrollPixels);
lastSelectedItem = selectedItemIndex;
_performScaleTransformation(scrollPixels, selectedItemIndex);
}
}
} catch (e) {
print(e);
}
}
toggleListOverlayVisibility
,用于控制列表是否显示。performListDrag
用于控制列表的滑动。
既然是控制滑动,那么它是使用 _scrollController
来控制的,_scrollController
的定义如下:
_scrollController = ScrollController(
initialScrollOffset: listPadding -
_currentScrollLocation +
topOffset +
_currentList.getSelectedItemIndex() * _currentList.itemHeight(),
);
_scrollController 是作用于ListView:
return Container(
decoration: dslContainerDecoration,
child: ListView.builder(
padding: EdgeInsets.only(left: paddingLeft),
controller: _scrollController,
itemCount: _currentList.items.length + 2,
itemBuilder: (BuildContext context, int index) {
if (index == 0 || index == _currentList.items.length + 1) {
return Container(height: listPadding);
}
final item = _currentList.items[index - 1];
final normalScale = 1.0;
if (lastSelectedItem == index - 1) {
item.updateScale(_calculateNewScale(normalScale));
} else {
item.updateScale(normalScale);
}
return item;
},
),
);
在具体分析滑动逻辑之前,必须搞清 listview 于 scrollcontrol :
先从 listview 中的 itemBuilder 可以看出来,在列表的顶部和底部(index: 0
和 _currentList.items.length + 1
)都返回一个高度为 listPadding
的 Container
。为什么要加两个 listpadding?是为了让菜单列表能在屏幕上任意位置显示,并可以滑动。为了方便理解,可以看看下面这个动图:
不可滑动 可滑动
在第二张图listview 的开头也加入了类似listpadding的机制。
这里面用的了几个控制位置的变量,我用图表示:

把这个listview
背景改为其它颜色(绿)、把listpadding改为黑色我们就可以直观的看出来:

回过头,在 toggleListOverlayVisibility
内,先判断list是否可见。
如果不可见调用 _showListOverlay
来显示 list。逻辑如下,没有什么复杂的地方:
点击查看代码
/// 显示 list
_showListOverlay(DirectSelectList visibleList, double location) async {
_currentList = visibleList;
_currentScrollLocation = location;
lastSelectedItem = _currentList.getSelectedItemIndex();
_currentList.items[lastSelectedItem].updateOpacity(1.0);
isOverlayVisible = true;
await fadeAnimationController.forward(from: 0.0);
}
如果可见,那么通过 ScrollController 滑动到指定位置:
listPadding - _adjustedTopOffset + lastSelectedItem * _currentList.itemHeight()
下面来看一下滑动 (performListDrag
) :
void performListDrag(double dragDy) {
try {
if (_scrollController != null && _scrollController.position != null) {
final currentScrollOffset = _scrollController.offset;
double allowedOffset = _allowedDragDistance(
currentScrollOffset + _adjustedTopOffset,
dragDy * widget.dragSpeedMultiplier);
if (allowedOffset != 0.0) {
_scrollController.jumpTo(currentScrollOffset + allowedOffset);
final scrollPixels =
_scrollController.offset - listPadding + _adjustedTopOffset;
final selectedItemIndex = _getCurrentListElementIndex(scrollPixels);
lastSelectedItem = selectedItemIndex;
_performScaleTransformation(scrollPixels, selectedItemIndex);
}
}
} catch (e) {
print(e);
}
}
其中调用了 _allowedDragDistance
:
double _allowedDragDistance(double currentScrollOffset, double position) {
double newPosition = currentScrollOffset + position;
double endOfListPosition =
(_currentList.items.length - 1) * _currentList.itemHeight() +
listPadding;
if (newPosition < listPadding) {
return listPadding - currentScrollOffset;
} else if (newPosition > endOfListPosition) {
return endOfListPosition - currentScrollOffset;
} else {
return position;
}
}
newPosition
:
计算新位置(newPosition
)与最大位置(endOfListPosition
),这里我们拆解一下 newPosition:
_scrollController.offset + _adjustedTopOffset + dragDy * widget.dragSpeedMultiplier
_scrollController.offset 、_adjustedTopOffset 在之前有解释。dragDy * widget.dragSpeedMultiplier
是指滑动偏移量×滑动速度 ,用一句通俗易懂得话来说它就是滑动距离。
其实,在默认情况、index为0、不滑动的状态下 newPosition = listpadding:

因为不滑动,所以 dragDy * widget.dragSpeedMultiplier
为 0,当我们向上滑动(item会向下滚动,逆着手指方向),dragDy * widget.dragSpeedMultiplier
会是负数,且 _scrollController.offset
减少所以nowPosition
会减少,变得小于 listPadding
。
double endOfListPosition =(_currentList.items.length - 1) * _currentList.itemHeight() + listPadding;

endOfListPosition
当我们向下滑动(item向上滚动),newPosition会增加,当滚动到最后一个item(_currentList.items.length - 1
)时newPosition的图解如下:

这时newPostion
= endOfListPosition
,如果继续向下滑动(item向上滚动),那么 dragDy * widget.dragSpeedMultiplier
会是正数, newPosition
会大于 endOfListPosition
。
最后回到 _allowedDragDistance
可以看到它用 if 将 newPosition
限制,listPadding
< newPosition
< endOfListPosition
:
double _allowedDragDistance(double currentScrollOffset, double position) {
double newPosition = currentScrollOffset + position;
double endOfListPosition =
(_currentList.items.length - 1) * _currentList.itemHeight() +
listPadding;
if (newPosition < listPadding) {
return listPadding - currentScrollOffset;
} else if (newPosition > endOfListPosition) {
return endOfListPosition - currentScrollOffset;
} else {
return position;
}
}
我们已经分析过在什么情况下会触发 newPosition < listPadding
和 newPosition > endOfListPosition
。
当 newPosition < listPadding
返回 return listPadding - currentScrollOffset
。会将list限制在第一个item,不允许list向下滚动。
当 newPosition > endOfListPosition
返回 return endOfListPosition - currentScrollOffset;
会将list限制在最后一个item,不允许list向上滚动。
除此之外 return position;
,直接返回 dragDy * widget.dragSpeedMultiplier
这种情况不会限制listview滚动。

好了,到此为止 _allowedDragDistance
分析完成。回到 performListDrag
,
//......
double allowedOffset = _allowedDragDistance(
currentScrollOffset + _adjustedTopOffset,
dragDy * widget.dragSpeedMultiplier,
);
if (allowedOffset != 0.0) {
_scrollController.jumpTo(currentScrollOffset + allowedOffset);
//...
}
//......
调用_scrollController.jumpTo
使得listview滚动到相应位置。
Comments | NOTHING