How to change selected letter when scrolling alphabet scroll view?

Viewed 33

I have implemented https://pub.dev/packages/alphabet_scroll_view in my project, I edited it for my own customization but when I scroll list with items scroll with letters is not changing.

https://imgur.com/a/ResAVrE here is a video to better understand what I want to achieve. That changes on selected letter is just my tap on them. Here is other link if above is not working https://drive.google.com/file/d/1Oy6XWalXwXM-yqk7IZU0Av2_jEL3ZNk1/view?usp=sharing. I hope this will working.

I want to change selected letter dynamically when I am scrolling items.

Here is my code:

import 'package:alphabet_scroll_view/src/meta.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart';

enum LetterAlignment { left, right }

class AlphabetScrollView extends StatefulWidget {
  AlphabetScrollView(
      {Key? key,
      required this.list,
      this.alignment = LetterAlignment.right,
      this.isAlphabetsFiltered = true,
      this.overlayWidget,
      required this.selectedTextStyle,
      required this.unselectedTextStyle,
      this.itemExtent = 40,
      required this.itemBuilder})
      : super(key: key);

  /// List of Items should be non Empty
  /// and you must map your
  /// ```
  ///  List<T> to List<AlphaModel>
  ///  e.g
  ///  List<UserModel> _list;
  ///  _list.map((user)=>AlphaModel(user.name)).toList();
  /// ```
  /// where each item of this ```list``` will be mapped to
  /// each widget returned by ItemBuilder to uniquely identify
  /// that widget.
  final List<AlphaModel> list;

  /// ```itemExtent``` specifies the max height of the widget returned by
  /// itemBuilder if not specified defaults to 40.0
  final double itemExtent;

  /// Alignment for the Alphabet List
  /// can be aligned on either left/right side
  /// of the screen
  final LetterAlignment alignment;

  /// defaults to ```true```
  /// if specified as ```false```
  /// all alphabets will be shown regardless of
  /// whether the item in the [list] exists starting with
  /// that alphabet.

  final bool isAlphabetsFiltered;

  /// Widget to show beside the selected alphabet
  /// if not specified it will be hidden.
  /// ```
  /// overlayWidget:(value)=>
  ///    Container(
  ///       height: 50,
  ///       width: 50,
  ///       alignment: Alignment.center,
  ///       color: Theme.of(context).primaryColor,
  ///       child: Text(
  ///                 '$value'.toUpperCase(),
  ///                  style: TextStyle(fontSize: 20, color: Colors.white),
  ///              ),
  ///      )
  /// ```

  final Widget Function(String)? overlayWidget;

  /// Text styling for the selected alphabet by which
  /// we can customize the font color, weight, size etc.
  /// ```
  /// selectedTextStyle:
  ///   TextStyle(
  ///     fontWeight: FontWeight.bold,
  ///     color: Colors.black,
  ///     fontSize: 20
  ///   )
  /// ```

  final TextStyle selectedTextStyle;

  /// Text styling for the unselected alphabet by which
  /// we can customize the font color, weight, size etc.
  /// ```
  /// unselectedTextStyle:
  ///   TextStyle(
  ///     fontWeight: FontWeight.normal,
  ///     color: Colors.grey,
  ///     fontSize: 18
  ///   )
  /// ```

  final TextStyle unselectedTextStyle;

  /// The itemBuilder must return a non-null widget and the third paramter id specifies
  /// the string mapped to this widget from the ```[list]``` passed.

  Widget Function(BuildContext, int, String) itemBuilder;

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

class _AlphabetScrollViewState extends State<AlphabetScrollView> {
  void init() {
    widget.list
        .sort((x, y) => x.key.toLowerCase().compareTo(y.key.toLowerCase()));
    _list = widget.list;
    setState(() {});

    /// filter Out AlphabetList
    if (widget.isAlphabetsFiltered) {
      List<String> temp = [];
      alphabets.forEach((letter) {
        AlphaModel? firstAlphabetElement = _list.firstWhereOrNull(
            (item) => item.key.toLowerCase().startsWith(letter.toLowerCase()));
        if (firstAlphabetElement != null) {
          temp.add(letter);
        }
      });
      _filteredAlphabets = temp;
    } else {
      _filteredAlphabets = alphabets;
    }
    calculateFirstIndex();
    setState(() {});
  }

  @override
  void initState() {
    init();
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      listController.addListener(() {
        print('scrolling ${listController.position.pixels}');
        if (listController.position.pixels >=
            listController.position.maxScrollExtent) {
          print('achieved end');
        } else if (listController.position.pixels <=
            listController.position.minScrollExtent) {
          print('achieved start');
        }
      });
    });
    if (listController.hasClients) {
      maxScroll = listController.position.maxScrollExtent;
    }
    super.initState();
  }

  ScrollController listController = ScrollController();
  final _selectedIndexNotifier = ValueNotifier<int>(0);
  final positionNotifer = ValueNotifier<Offset>(Offset(0, 0));
  final Map<String, int> firstIndexPosition = {};
  List<String> _filteredAlphabets = [];
  final letterKey = GlobalKey();
  List<AlphaModel> _list = [];
  bool isLoading = false;
  bool isFocused = false;
  final key = GlobalKey();

  @override
  void didUpdateWidget(covariant AlphabetScrollView oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.list != widget.list ||
        oldWidget.isAlphabetsFiltered != widget.isAlphabetsFiltered) {
      _list.clear();
      firstIndexPosition.clear();
      init();
    }
  }

  int getCurrentIndex(double vPosition) {
    double kAlphabetHeight = letterKey.currentContext!.size!.height;
    return (vPosition ~/ kAlphabetHeight);
  }

  /// calculates and Maintains a map of
  /// [letter:index] of the position of the first Item in list
  /// starting with that letter.
  /// This helps to avoid recomputing the position to scroll to
  /// on each Scroll.
  void calculateFirstIndex() {
    _filteredAlphabets.forEach((letter) {
      AlphaModel? firstElement = _list.firstWhereOrNull(
          (item) => item.key.toLowerCase().startsWith(letter));
      if (firstElement != null) {
        int index = _list.indexOf(firstElement);
        firstIndexPosition[letter] = index;
      }
    });
  }

  void scrolltoIndex(int x, Offset offset) {
    int index = firstIndexPosition[_filteredAlphabets[x].toLowerCase()]!;
    final scrollToPostion = widget.itemExtent * index;
    if (index != null) {
      listController.animateTo((scrollToPostion),
          duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
    }
    positionNotifer.value = offset;
  }

  void onVerticalDrag(Offset offset) {
    int index = getCurrentIndex(offset.dy);
    if (index < 0 || index >= _filteredAlphabets.length) return;
    _selectedIndexNotifier.value = index;
    setState(() {
      isFocused = true;
    });
    scrolltoIndex(index, offset);
  }

  double? maxScroll;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        ListView.builder(
            controller: listController,
            scrollDirection: Axis.vertical,
            itemCount: _list.length,
            physics: ClampingScrollPhysics(),
            itemBuilder: (_, x) {
              return ConstrainedBox(
                  constraints: BoxConstraints(maxHeight: widget.itemExtent),
                  child: widget.itemBuilder(_, x, _list[x].key));
            }),
        Align(
          alignment: widget.alignment == LetterAlignment.left
              ? Alignment.centerLeft
              : Alignment.centerRight,
          child: Container(
            key: key,
            padding: const EdgeInsets.symmetric(horizontal: 2),
            child: SingleChildScrollView(
              child: GestureDetector(
                onVerticalDragStart: (z) => onVerticalDrag(z.localPosition),
                onVerticalDragUpdate: (z) => onVerticalDrag(z.localPosition),
                onVerticalDragEnd: (z) {
                  setState(() {
                    isFocused = false;
                  });
                },
                child: ValueListenableBuilder<int>(
                    valueListenable: _selectedIndexNotifier,
                    builder: (context, int selected, Widget? child) {
                      return Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: List.generate(
                            _filteredAlphabets.length,
                            (x) => GestureDetector(
                              key: x == selected ? letterKey : null,
                              onTap: () {
                                _selectedIndexNotifier.value = x;
                                scrolltoIndex(x, positionNotifer.value);
                              },
                              child: Container(
                                decoration: BoxDecoration(
                                  borderRadius: BorderRadius.only(
                                    topRight: widget.alignment == LetterAlignment.left ? Radius.circular(20) : Radius.circular(0),
                                    bottomRight: widget.alignment == LetterAlignment.left ? Radius.circular(20) : Radius.circular(0),
                                    topLeft: widget.alignment == LetterAlignment.right ? Radius.circular(20) : Radius.circular(0),
                                    bottomLeft: widget.alignment == LetterAlignment.right ? Radius.circular(20) : Radius.circular(0)
                                  ),
                                  color: selected == x ? Color(0xFFFA3B71) : Colors.transparent
                                ),
                                padding: const EdgeInsets.symmetric(
                                    horizontal: 12, vertical: 2),
                                child: Text(
                                  _filteredAlphabets[x].toUpperCase(),
                                  style: selected == x
                                      ? widget.selectedTextStyle
                                      : widget.unselectedTextStyle,
                                  // style: TextStyle(
                                  //     fontSize: 12,
                                  //     fontWeight: selected == x
                                  //         ? FontWeight.bold
                                  //         : FontWeight.normal),
                                ),
                              ),
                            ),
                          ));
                    }),
              ),
            ),
          ),
        ),
        !isFocused
            ? Container()
            : ValueListenableBuilder<Offset>(
                valueListenable: positionNotifer,
                builder:
                    (BuildContext context, Offset position, Widget? child) {
                  return Positioned(
                      right:
                          widget.alignment == LetterAlignment.right ? 40 : null,
                      left:
                          widget.alignment == LetterAlignment.left ? 40 : null,
                      top: position.dy,
                      child: widget.overlayWidget == null
                          ? Container()
                          : widget.overlayWidget!(_filteredAlphabets[
                              _selectedIndexNotifier.value]));
                })
      ],
    );
  }
}

class AlphaModel {
  final String key;
  final String? secondaryKey;

  AlphaModel(this.key, {this.secondaryKey});
}

If you want to test my code you can install package linked above, I just changed customization for background of letter scrollview.

1 Answers

You can do Like this :

Check this Example:

 Expanded(
              child: AlphabetScrollView(
                list:
                    _filterList.map((e) => AlphaModel(e.employeeName)).toList(),
                // isAlphabetsFiltered: false,
                alignment: LetterAlignment.right,
                itemExtent: 90,

                unselectedTextStyle: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.normal,
                    color: _filterList.length > 5
                        ? Colors.black
                        : Colors.transparent),
                selectedTextStyle: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: _filterList.length > 5
                        ? Colors.red
                        : Colors.transparent),
                overlayWidget: (value) => Stack(
                  alignment: Alignment.center,
                  children: [
                    Icon(
                      Icons.star,
                      size: 50,
                      color: Colors.red,
                    ),
                    Container(
                      height: 50,
                      width: 50,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        // color: Theme.of(context).primaryColor,
                      ),
                      alignment: Alignment.center,
                      child: Text(
                        '$value'.toUpperCase(),
                        style: TextStyle(fontSize: 18, color: Colors.white),
                      ),
                    ),
                  ],
                ),

                itemBuilder: (_, index, id) {
                  return InkWell(
                    onTap: () => Get.toNamed(EmployeeInfo.route,
                        arguments: _filterList[index]),
                    child: Container(
                      margin: EdgeInsets.only(right: 20),
                      child: Column(
                        children: [
                          Padding(
                            padding: const EdgeInsets.only(bottom: 12.0),
                            child: Row(
                              children: [
                                SizedBox(
                                  width: 10,
                                ),
                                Stack(
                                  children: [
                                    CircularProfileAvatar(
                                      SystemConfiguration.baseUrl +
                                          SystemConfiguration.getEmployePhoto +
                                          _filterList[index]
                                              .id, //sets image path, it should be a URL string. default value is empty string, if path is empty it will display only initials
                                      radius: 30, // sets radius, default 50.0
                                      backgroundColor: Colors
                                          .transparent, // sets background color, default Colors.white
                                      borderWidth:
                                          2, // sets border, default 0.0

                                      borderColor: Colors
                                          .blue, // sets border color, default Colors.white
                                      cacheImage:
                                          true, // allow widget to cache image against provided url
                                      imageFit: BoxFit.cover,
                                      // sets on tap
                                      showInitialTextAbovePicture: true,
                                      errorWidget: (BuildContext context,
                                          String data, dynamic v) {
                                        return Icon(
                                          FeatherIcons.user,
                                          size: 40,
                                          color: Colors.white,
                                        );
                                      }, // setting it true will show initials text above profile picture, default false
                                    ),
                                    Positioned(
                                        bottom: 5,
                                        right: 0,
                                        child: Padding(
                                          padding:
                                              const EdgeInsets.only(left: 3.0),
                                          child: Container(
                                            width: 17,
                                            height: 17,
                                            decoration: BoxDecoration(
                                                shape: BoxShape.circle,
                                                color: Color(0xFFe0f2f1)),
                                            child: Icon(
                                              Icons.circle,

                                              size: 16.0,
                                              // ignore: unnecessary_null_comparison
                                              color: (_filterList[index]
                                                              .attendanceStatus
                                                              .length >
                                                          0 &&
                                                      _filterList[index]
                                                          .InOffice
                                                          .contains("IN"))
                                                  ? Colors.green
                                                  : Colors.red[500],
                                            ),
                                          ),
                                        ))
                                  ],
                                ),
                                Expanded(
                                  flex: 3,
                                  child: Padding(
                                    padding: const EdgeInsets.all(8.0),
                                    child: Column(
                                      mainAxisAlignment:
                                          MainAxisAlignment.start,
                                      crossAxisAlignment:
                                          CrossAxisAlignment.start,
                                      children: [
                                        Text(
                                          _filterList[index].employeeName,
                                          maxLines: 1,
                                          overflow: TextOverflow.ellipsis,
                                          style: GoogleFonts.roboto(
                                              fontSize: 13.0,
                                              fontWeight: FontWeight.w500),
                                        ),
                                      ],
                                    ),
                                  ),
                                ),
                                Expanded(
                                  flex: 2,
                                  child: Stack(
                                    children: [
                                      Positioned(
                                        left: 44,
                                        child: ElevatedButton(
                                          onPressed: () => _launchURL(
                                              _filterList[index].mobile1),
                                          child: Icon(
                                            Icons.call,
                                            size: 20.0,
                                            color: Colors.white,
                                          ),
                                          style: ElevatedButton.styleFrom(
                                            shape: CircleBorder(),
                                            padding: EdgeInsets.all(2),
                                            primary:
                                                Colors.blue, // <-- Button color
                                            onPrimary:
                                                Colors.red, // <-- Splash color
                                          ),
                                        ),
                                      ),
                                      ElevatedButton(
                                        onPressed: () {
                                          // _textMe(_filterList[index].mobile1); mobile text

                                          Get.toNamed(ChatViwer.route,
                                              arguments: {
                                                "employe_name":
                                                    _filterList[index]
                                                        .employeeName,
                                                "employe_id":
                                                    _filterList[index].id
                                              });
                                        },
                                        child: Icon(
                                          Icons.message,
                                          size: 20.0,
                                          color: Colors.white,
                                        ),
                                        style: ElevatedButton.styleFrom(
                                          shape: CircleBorder(),
                                          padding: EdgeInsets.all(2),
                                          primary:
                                              Colors.blue, // <-- Button color
                                          onPrimary:
                                              Colors.red, // <-- Splash color
                                        ),
                                      ),
                                    ],
                                  ),
                                )
Related