前言
本篇依然是针对Flutter中UI界面的实操。我们通过实现一个类似于iOS下UITableView 右侧的索引条的一个小部件,来加深对之前内容的学习。
部件中主要涉及到的有:
Stack()
,
Positioned()
,
Column()
,
Expanded()
,
GestureDetector()
,
ListView.builder()
ScrollController()
,
Alignment()
.......
这里先说几个比较关键的东西:
-
Alignment(x,y)
:是控制容器内子部件的偏移量,取值范围是-1~1,当小于-1或者大于1时,偏移就超出了容器,这也是实现IndexBar 放大气泡能上下移动的关键 -
Expanded()
:多个被它包住的子部件会在容器内平分区域,如果只有一个被包裹的子部件,默认沾满整个容器 -
ScrollController()
:滚动控制器,它并非独立存在,而是需要传递给ListView.buidler()
的controller
属性,换句话说就是ListView.buidler()
自己无法完成滚动等操作,而是委托给了ScrollController()
。比如在ios开发中我们可以通过调用UITableView.scrollToRowAtIndexPath:atScrollPosition:animated:
来主动触发滚动,而这里需要委托ScrollController()
来间接完成。
GestureDetector()
:让一个部件具有事件响应能力,内部有很多的回调函数,包括单击,长按,拖拽等。
先上一张效果图:
效果图.gif
布局分析
首先当前ListView()
跟ZZIndexBar````需要一个
Stack()布局,使得
ZZIndexBar压在
ListView()上层,在
ZZIndexBar内部,先是一个
Row布局,左边是气泡框,右边是字母列表,字母列表内部又是一个Column布局。结构也比较简单;字母列表需要一个
GestureDetector()```包裹起来达到响应事件的能力。
- 已上课程页面
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('已上课程'),
actions: [
GestureDetector(
child: Container(
margin: EdgeInsets.only(right: 10),
child: Icon(Icons.add),
),
onTap: (){
},
),
],
),
body: Container(
child: Stack(
children: [
ListView.builder( //具备复用能力的ListView()
itemCount: _headerData.length+_listData.length,
itemBuilder: _cellForRowAtIndex,
controller: _scrollController, //给ListView设置一个委托控制器(ScrollController),间接操作滚动行为等
),
ZZIndexBar(
indexBarCallBack: (String str){
//这里响应后 我们通过Str换算出ListView需要偏移的位置
//并通过ScrollController来间接操作ListView的滚动行为
print('====='+str);
//_groupOffsetMap 维护了一个根据str找到具体的offset值的Map 就不赘述了
if (_groupOffsetMap[str] != null) {
//animateTo()来发生滚动偏移
_scrollController.animateTo(
_groupOffsetMap[str], //偏移的值
duration: Duration(milliseconds: 10), //动画持续时长
curve: Curves.easeIn, //动效执行曲线
);
}
},
)
],
)
),
);
}
}
- ZZIndexBar
@override
Widget build(BuildContext context) {
List<Widget> words = [];
for (int i = 0; i < INDEX_WORDS.length; i++) {
words.add(Expanded(
child: Container(
child: Text(
INDEX_WORDS[i],
style: TextStyle(color: Colors.white, fontSize: 10),
),
),
));
}
return Positioned(
right: 0,
top: ScreenHeight(context) / 8,
height: ScreenHeight(context) / 2,
width: 120,
child: Row(
children: [
Container(
alignment: Alignment(0.0,_offsetY), //上下移动的核心
width: 100,
// color: Colors.red,
child: _poHidden == true
? null
: Stack(
alignment: Alignment(-0.2, 0), //对齐方式
children: [
Image(
image: AssetImage('images/ppo.png'),
width: 60,
),
Text(
_indexText,
style: TextStyle(fontSize: 35, color: Colors.white),
)
],
),
),
GestureDetector(
onVerticalDragDown: (DragDownDetails details) {
//垂直方向 touchesBegin:
//details.globalPosition 获取当前拖动的屏幕坐标
//details.localPosition 获取当前context的坐标
int index = getIndex(context, details.localPosition);
if(index != _selectorIndex){
_selectorIndex = index;
setState(() {
//callback 回调
widget.indexBarCallBack(INDEX_WORDS[index]);
});
}
//UI显示
setState(() {
_indexText = INDEX_WORDS[index];
_offsetY = 2.2 / (INDEX_WORDS.length - 1) * index - 1.1;
_poHidden = false;
});
},
onVerticalDragUpdate: (DragUpdateDetails details) {
//垂直方向 touchesMove:
int index = getIndex(context, details.localPosition);
if(index != _selectorIndex){
_selectorIndex = index;
setState(() {
widget.indexBarCallBack(INDEX_WORDS[index]);
});
}
//UI显示
setState(() {
_indexText = INDEX_WORDS[index];
_offsetY = 2.2 / (INDEX_WORDS.length - 1) * index - 1.1;
_poHidden = false;
});
},
onVerticalDragEnd: (DragEndDetails details) {
//垂直方向 touchesEnd:
//UI显示
_poHidden = true;
_selectorIndex = -1;
setState(() {});
},
child: Container(
width: 20,
color: Color.fromRGBO(1, 1, 1, 0.3),
child: Column(
children: words,
),
),
)
],
));
}
Positioned()
通常用在Stack()
中来控制一个容器的显示位置。
onVerticalDragDown
,onVerticalDragUpdate
,onVerticalDragEnd
,GestureDetector()
的具体触摸回调函数,我们在这3个阶段做了一些逻辑处理来控制UI的显示效果。通过indexBarCallBack
将事件结果传递出去。
这里主要看一下
int getIndex(BuildContext context,Offset localPosition)
//根据屏幕坐标来换算获取字母索引值
int getIndex(BuildContext context,Offset localPosition){
double y = localPosition.dy;
//每一个Item的高度
var itemHeigth = ScreenHeight(context) /2 /INDEX_WORDS.length;
//'~/'相除取整 'clamp'取值范围
int index = (y ~/ itemHeigth).clamp(0, INDEX_WORDS.length);
return index;
}
外部通过scrollController 实现联动
ZZIndexBar(
indexBarCallBack: (String str){
//这里响应后 我们通过Str换算出ListView需要偏移的位置
//并通过ScrollController来间接操作ListView的滚动行为
print('====='+str);
//_groupOffsetMap 维护了一个根据str找到具体的offset值的Map 就不赘述了
if (_groupOffsetMap[str] != null) {
//animateTo()来发生滚动偏移
_scrollController.animateTo(
_groupOffsetMap[str], //偏移的值
duration: Duration(milliseconds: 10), //动画持续时长
curve: Curves.easeIn, //动效执行曲线
);
}
},
)
总结
依然不存在任何难点,没什么可说的。。。。。🙃
网友评论