> For the complete documentation index, see [llms.txt](https://ryukiedev.gitbook.io/wiki/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ryukiedev.gitbook.io/wiki/flutter/12.-suo-yin-tiao-shou-shi-ji-clamp-han-shu.md).

# 12.索引条：手势及clamp函数

### 前言

在进行类似联系人页面的开发过程中，我们经常会遇到侧边栏索引条。在iOS中我们只需要简单设置即可使用系统提供的控件。

这里我们使用 Flutter 自定义一个索引条，来熟悉一些常用的知识点。

### 一、 一个索引条包含什么？

* 数据源
  * 索引数组
* 点击手势
* 滑动手势
* 事件回调

### 二、 开始布局

索引条展示的数据是可变的，所以这里我们使用`有状态的Widget`进行布局。这里以 `Container` + `Column` 进行基础封装，看看效果。

```
import 'package:flutter/material.dart';

class IndexBar extends StatefulWidget {
  IndexBar({
    required this.dataSource,
  });

  final List<String> dataSource;
  final double _indexItemHeight = 22;

  @override
  _IndexBarState createState() => _IndexBarState();
}

class _IndexBarState extends State<IndexBar> {
  List<Widget> _indexsWidgets = [];

  @override
  void initState() {
    super.initState();

    for (int i = 0; i < widget.dataSource.length; i++) {
      _indexsWidgets.add(Container(
        height: widget._indexItemHeight,
        child: Text(
          widget.dataSource[i],
          style: const TextStyle(color: Colors.grey),
        ),
      ));
    }
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapUp: (details) {
        RenderBox box = context.findRenderObject() as RenderBox;
        Offset point = box.globalToLocal(details.globalPosition);
        double y = point.dy;
        // 在当前 Widget 内的 Offset
        print(y);
      },
      child: Container(
        width: 22,
        color: const Color.fromRGBO(1, 1, 1, 0.2),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: _indexsWidgets,
        ),
      ),
    );
  }
}
```

基础使用，传入索引数组：

```
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Stack(
        children: [
          Container(...),
          Align(
            alignment: Alignment.centerRight,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                IndexBar(dataSource: _indexs)
              ],
            ),
          ),
        ],
      )
    );
  }
```

![1](/files/gk4BYsgSnjwulE0i334h)

这里我们完成了基础布局，和数据源的传入，下面我们继续完善。

### 三、 点击事件

![2](/files/vnYHqqD5ZDSJdFy7tDrx)

这里我们需要定位到具体点击了哪个 index 所以 onTap 不能够满足，这里通过注释我们可以发现 `onTapUp` 回调带有更多信息。我们使用这个进行点击事件的处理

> `typedef GestureTapUpCallback = void Function(TapUpDetails details);`

![3](/files/9GvuuOakAdkNiEKiLUfL)

现在我们能够获取点击坐标了，接下来进行计算获得具体 index。

![4](/files/IjWA2h3bz9A6ckCITWzY)

```
flutter: L
flutter: N
flutter: E
flutter: B
flutter: J
flutter: L
flutter: J
flutter: K
flutter: N
flutter: J
flutter: F
```

### 四、 滑动事件

和点击事件类似，这里我们添加一下滑动选择事件：

```
onVerticalDragUpdate: (details) {
  RenderBox box = context.findRenderObject() as RenderBox;
  Offset point = box.globalToLocal(details.globalPosition);
  print('onVerticalDragUpdate${widget.dataSource[_currentIndex(point)]}');
},
onTapUp: (details) {
  RenderBox box = context.findRenderObject() as RenderBox;
  Offset point = box.globalToLocal(details.globalPosition);
  print('onTapUp${widget.dataSource[_currentIndex(point)]}');
},
```

测试有效：

```
flutter: onVerticalDragUpdateF
flutter: onVerticalDragUpdateF
flutter: onVerticalDragUpdateH
flutter: onVerticalDragUpdateH
flutter: onVerticalDragUpdateH
```

### 五、 事件回调

内部的事件处理完成了，我们来来添加一下外部的回调：

#### 定义回调类型

```
typedef IndexBarSelectCallBack = void Function(int index,  String title);

class IndexBar extends StatefulWidget {
  IndexBar({
    required this.dataSource,
    required this.callBack
  });

  final List<String> dataSource;
  final double _indexItemHeight = 22;
  final IndexBarSelectCallBack callBack;

  @override
  _IndexBarState createState() => _IndexBarState();
}
```

#### 调整事件处理及回调

```
onVerticalDragUpdate: (details) {
  RenderBox box = context.findRenderObject() as RenderBox;
  Offset point = box.globalToLocal(details.globalPosition);
  int index = _currentIndex(point);
  String title = widget.dataSource[index];
  widget.callBack(index, title);
},
onTapUp: (details) {
  RenderBox box = context.findRenderObject() as RenderBox;
  Offset point = box.globalToLocal(details.globalPosition);
  int index = _currentIndex(point);
  String title = widget.dataSource[index];
  widget.callBack(index, title);
},
```

#### 外部传入回调

![5](/files/qOb27MuoelKmR2UXs4Fy)

### 六、 Clamp 函数

在滑动偏移过大超出范围的时候会发生越界，出现下面的警告

> RangeError (index): Invalid value: Not in inclusive range 0..10: 14

这里安利一个很好用的函数 clamp 来对方法 `_currentIndex(Offset point)` 进行一下优化：

```
int _currentIndex(Offset point) {
  return (point.dy ~/ widget._indexItemHeight).clamp(0, widget.dataSource.length - 1);
}
```

这样就对返回的值的范围作出了限制。

### 七、 最终代码

<https://github.com/RyukieSama/FlutterStudy/blob/master/fake\\_wechat/lib/widgets/contacts/index\\_bar.dart>
