# 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>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ryukiedev.gitbook.io/wiki/flutter/12.-suo-yin-tiao-shou-shi-ji-clamp-han-shu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
