preface

SideBar is one of the common functions in APP development. It is mainly used for index list, such as city selection and classification. When optimizing the OpenGit trend list, I tried to develop this control because I needed to use this control when selecting a language. The result is shown below

To prepare

To complete the SideBar, you need to provide the following parameters

  1. SideBar width and the height of each letter;
  2. Default background and text colors;
  3. Background color and text color when pressed;
  4. Callback to letter currently selected;
  5. Index list;

The callback function for selecting letter is shown below

typedef OnTouchingLetterChanged = void Function(String letter);
Copy the code

The index list data is shown in the code below

const List<String> A_Z_LIST = const [
  "A"."B"."C"."D"."E"."F"."G"."H"."I"."J"."K"."L"."M"."N"."O"."P"."Q"."R"."S"."T"."U"."V"."W"."X"."Y"."Z"."#"
];
Copy the code

The UI needs to be refreshed when SideBar is pressed, so SideBar needs to inherit the StatefulWidget, as shown in the constructor below

class SideBar extends StatefulWidget {
  SideBar({
    Key key,
    @required this.onTouch,
    this.width = 30.this.letterHeight = 16.this.color = Colors.transparent,
    this.textStyle = const TextStyle(
      fontSize: 12.0,
      color: Color(YZColors.subTextColor),
    ),
    this.touchDownColor = const Color(0x40E0E0E0),
    this.touchDownTextStyle = const TextStyle(
      fontSize: 12.0,
      color: Color(YZColors.mainTextColor),
    ),
  });

  final int width;

  final int letterHeight;

  final Color color;

  final Color touchDownColor;

  final TextStyle textStyle;

  final TextStyle touchDownTextStyle;

  final OnTouchingLetterChanged onTouch;
}
Copy the code

To encapsulate the SideBar

In _SideBarState, the display of the background color needs to be judged by the state of touch, as shown below

class _SideBarState extends State<SideBar> {
  bool _isTouchDown = false;

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      color: _isTouchDown ? widget.touchDownColor : widget.color,
      width: widget.width.toDouble(),
      child: _SlideItemBar(
        letterWidth: widget.width,
        letterHeight: widget.letterHeight,
        textStyle: _isTouchDown ? widget.touchDownTextStyle : widget.textStyle,
        onTouch: (letter) {
          if(widget.onTouch ! =null) { setState(() { _isTouchDown = ! TextUtil.isEmpty(letter); }); widget.onTouch(letter); }},),); }}Copy the code

The following code changes the color of the Container by changing the letter state of the _SlideItemBar

class _SlideItemBar extends StatefulWidget {
  final int letterWidth;

  final int letterHeight;

  final TextStyle textStyle;

  final OnTouchingLetterChanged onTouch;

  _SlideItemBar(
      {Key key,
      @required this.onTouch,
      this.letterWidth = 30.this.letterHeight = 16.this.textStyle})
      : assert(onTouch ! =null),
        super(key: key);

  @override
  _SlideItemBarState createState() {
    return_SlideItemBarState(); }}Copy the code

The above code does not do much, just define a few variables, detailed operations in _SlideItemBarState. In _SlideItemBarState, you need to know the vertical offset height of each letter as shown in the code below

void _init() {
    _letterPositionList.clear();
    _letterPositionList.add(0);
    int tempHeight = 0; A_Z_LIST? .forEach((value) { tempHeight = tempHeight + widget.letterHeight; _letterPositionList.add(tempHeight); }); }Copy the code

Fill each letter widget with a fixed width and height as shown below

List<Widget> children = List(a); A_Z_LIST.forEach((v) { children.add(SizedBox( width: widget.letterWidth.toDouble(), height: widget.letterHeight.toDouble(), child: Text(v, textAlign: TextAlign.center, style: _style), )); });Copy the code

In the SideBar swipe, you need to detect gesture events, as shown below

GestureDetector(
      onVerticalDragDown: (DragDownDetails details) {
        // Calculate the distance from the top of the index list
        if (_widgetTop == - 1) {
          RenderBox box = context.findRenderObject();
          Offset topLeftPosition = box.localToGlobal(Offset.zero);
          _widgetTop = topLeftPosition.dy.toInt();
        }
        // Get the offset of the touch point in the index list
        int offset = details.globalPosition.dy.toInt() - _widgetTop;
        int index = _getIndex(offset);
        // Determine if the index is in the list, and if so, notify the upper layer to update the data
        if(index ! =- 1) {
          _lastIndex = index;
          _triggerTouchEvent(A_Z_LIST[index]);
        }
      },
      onVerticalDragUpdate: (DragUpdateDetails details) {
        // Get the offset of the touch point in the index list
        int offset = details.globalPosition.dy.toInt() - _widgetTop;
        int index = _getIndex(offset);
        // Check whether the data is consistent. If not, notify upper layer to update the data
        if(index ! =- 1&& _lastIndex ! = index) { _lastIndex = index; _triggerTouchEvent(A_Z_LIST[index]); } }, onVerticalDragEnd: (DragEndDetails details) { _lastIndex =- 1;
        _triggerTouchEvent(' ');
      },
      onTapUp: (TapUpDetails details) {
        _lastIndex = - 1;
        _triggerTouchEvent(' ');
      },
      / / fill the UI
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: children,
      ),
    )
Copy the code

In the above code, during the onVerticalDragDown event, the height of the index from the top is obtained for the first time, and the offset value of the touch point is obtained by the Y coordinate of the touch point, and the current index of the touch point is found by this value, and the state is recorded and the upper UI is notified. During the onVerticalDragUpdate event, fetching the index is the same as onVerticalDragDown, but with a heavier operation. OnVerticalDragEnd and onTapUp represent the end of the Touch event. At this point, you can get the letter data returned when you touch SideBar.

Show letter

SideBar is usually displayed at the top of the ListView, and we use Stack for the parent container, as shown in the code below

@override
Widget build(BuildContext context) {
    super.build(context);

    return Scaffold(
      body: Stack(
        children: <Widget>[
          _buildSideBar(context),
          _buildLetterTips(),
        ],
      ),
    );
}

Widget _buildSideBar(BuildContext context) {
    return Offstage(
      offstage: widget.offsetBuilder == null, child: Align( alignment: Alignment.centerRight, child: SideBar( onTouch: (letter) { setState(() { _letter = letter; }); },),),); } Widget _buildLetterTips() {return Offstage(
      offstage: TextUtil.isEmpty(_letter),
      child: Align(
        alignment: Alignment.center,
        child: Container(
          alignment: Alignment.center,
          width: 65.0,
          height: 65.0,
          color: Color(0x40000000),
          child: Text(
            TextUtil.isEmpty(_letter) ? ' ' : _letter,
            style: YZConstant.largeLargeTextWhite,
          ),
        ),
      ),
    );
}
Copy the code

When the received letter changes, the UI will be refreshed by setState, and when _letter is not empty, the prompt for the current letter will be displayed.

Scroll the ListView

Scrolling through the ListView so far only finds two methods, as shown in the code below

// Make the picture scroll
scrollController.animateTo(double offset);
// Do not scroll the painting
scrollController.jumpTo(double offset);
Copy the code

Since both methods need to know exactly where to scroll, you need to know the offset of each item in the ListView relative to the top of the screen, so the height must be fixed.

Gets the language list data

Call the interface github-trending-api.now.sh/languages and encapsulate the bean object, as shown in the code below

List<TrendingLanguageBean> getTrendingLanguageBeanList(List<dynamic> list) {
  List<TrendingLanguageBean> result = [];
  list.forEach((item) {
    result.add(TrendingLanguageBean.fromJson(item));
  });
  return result;
}

@JsonSerializable(a)class TrendingLanguageBean extends Object {
  @JsonKey(name: 'id')
  String id;

  @JsonKey(name: 'name')
  String name;

  String letter;

  bool isShowLetter;

  TrendingLanguageBean(this.id, this.name, {this.letter});

  factory TrendingLanguageBean.fromJson(Map<String.dynamic> srcJson) =>
      _$TrendingLanguageBeanFromJson(srcJson);

  Map<String.dynamic> toJson() => _$TrendingLanguageBeanToJson(this);
}
Copy the code

Sort the retrieved data as shown in the code below

void _sortListByLetter(List<TrendingLanguageBean> list) {
    if (list == null || list.isEmpty) return;
    list.sort(
      (a, b) {
        if (a.letter == "@" || b.letter == "#") {
          return - 1;
        } else if (a.letter == "#" || b.letter == "@") {
          return 1;
        } else {
          returna.letter.compareTo(b.letter); }}); }Copy the code

Set its presentation state by the corresponding initial letter of the language, as shown in the code below

void _setShowLetter(List<TrendingLanguageBean> list) {
    if(list ! =null && list.isNotEmpty) {
      String tempLetter;
      for (int i = 0, length = list.length; i < length; i++) {
        TrendingLanguageBean bean = list[i];
        String letter = bean.letter;
        if(tempLetter ! = letter) { tempLetter = letter; bean.isShowLetter =true;
        } else {
          bean.isShowLetter = false; }}}}Copy the code

With the list data ready, initialize the height of a single item, as shown in the code below

double getLetterHeight() => 48.0;

double getItemHeight() => 56.0;
Copy the code

Then calculate the height of each letter in the ListView, as shown in the code below

void _initListOffset(List<TrendingLanguageBean> list) {
    _letterOffsetMap.clear();
    double offset = 0;
    Stringletter; list? .forEach((v) {if(letter ! = v.letter) { letter = v.letter; _letterOffsetMap.putIfAbsent(letter, () => offset); offset = offset + getLetterHeight() + getItemHeight(); }else{ offset = offset + getItemHeight(); }}); }Copy the code

Get the specified scrolling height by letter, as shown in the code below

double getOffset(String letter) => _letterOffsetMap[letter];
Copy the code

When the height is obtained, complete the ListView scrolling as shown in the code below

if(offset ! =null) {
    _scrollController.jumpTo(offset.clamp(
            . 0, _scrollController.position.maxScrollExtent));
}
Copy the code

Encapsulate the SideBar with CustomPainter

Wrap the SideBar above, enclose each letter with SizeBox, store it in the List

List, and fill the Column, as shown in the code below

List<Widget> children = List(a); A_Z_LIST.forEach((v) { children.add(SizedBox( width: widget.letterWidth.toDouble(), height: widget.letterHeight.toDouble(), child: Text(v, textAlign: TextAlign.center, style: _style), )); }); child: Column( mainAxisSize: MainAxisSize.min, children: children, ),Copy the code

Now implement the SideBar with CustomPainter, as shown in the code below

class _SideBarPainter extends CustomPainter {
  final TextStyle textStyle;
  final int width;
  final int height;

  TextPainter _textPainter;

  _SideBarPainter(this.textStyle, this.width, this.height) {
    _textPainter = new TextPainter(
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
  }

  @override
  void paint(Canvas canvas, Size size) {
    int length = A_Z_LIST.length;

    for (int i = 0; i < length; i++) {
      _textPainter.text = new TextSpan(
        text: A_Z_LIST[i],
        style: textStyle,
      );

      _textPainter.layout();
      _textPainter.paint(
          canvas, Offset(width.toDouble() / 2, i * height.toDouble())); }}@override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true; }}Copy the code

Use _SideBarPainter as shown in the code below

child: CustomPaint(
    painter: _SideBarPainter(
        widget.textStyle, widget.width, widget.letterHeight),
    size: Size(widget.width.toDouble(), _height),
),
Copy the code

Program source code

OpenGit_Fultter