ValuePicker

For one of my projects I was looking for a NumberPicker to give the user the opportunity to select a specific duration.  I found the great numberpicker 1.0.0 package from Marcin Szalek . NumberPicker is a custom widget designed for choosing an integer or decimal number by scrolling spinners.  While this widged worked like intended, I was in need for a more general approach, a picker, wherer the user can choose from a predefined set of values. So I decided to sepearate values from it’s visual appearance. You have to inititalize the widget with a List of text/value pairs  List<ValuePickerItem>.

For example a list of durations would look like this:

text value in seconds (in this case int)
“00:30” 30
“01:00” 60
“02:00” 120
“05:00” 300
“10:00” 600

ValuePicker is heavily inspered by NumberPicker and shares some code, so please check the LICENSE AGREEMENTS of NumberPicker 1.0.0 of the source before you use it.


import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';


class ValuePickerItem {
  String text;
  dynamic value; 

  ValuePickerItem(this.text, this.value);
}


class ValuePicker extends StatefulWidget {

  final List<ValuePickerItem> itemList;
  final ValueChanged<num> onChanged;
  final int initialIndex;
  final double itemHeight;
  final double width;
  final int extraLines; 

  ValuePicker({
    Key key,
    @required this.itemList,
    @required this.initialIndex,
    @required this.onChanged,
    this.itemHeight = 50.0,
    this.width = 100.0,
    this.extraLines = 1,
  }) : 
    assert (itemList != null),
    assert (initialIndex != null),
    assert (onChanged != null),
    super(key: key);


  @override
  ValuePickerState createState() {
    return new ValuePickerState();
  }
}

class ValuePickerState extends State<ValuePicker> {

  int _currentIndex; 
  int _itemCount;
  int _listItemCount;
  double _listViewHeight;

  ScrollController _scrollController;
  TextStyle _defaultStyle;
  TextStyle _selectedStyle;


  animateToIndex(int index) {
    _animate(_scrollController, index * widget.itemHeight);
  }


  _animate(ScrollController scrollController, double value) {
    scrollController.animateTo(value, duration: Duration(seconds: 1), curve: ElasticOutCurve());
  }


  bool _userStoppedScrolling(Notification notification, ScrollController scrollController) {
    return notification is UserScrollNotification &&
        notification.direction == ScrollDirection.idle &&
        scrollController.position.activity is! HoldScrollActivity;
  }


  bool _onIndexNotification(Notification notification) {
    if (notification is ScrollNotification) {
      int newIndex = (notification.metrics.pixels / widget.itemHeight).round() + widget.extraLines;
      if (_userStoppedScrolling(notification, _scrollController)) animateToIndex(newIndex - widget.extraLines);
      if (newIndex != _currentIndex) {
        setState(() {
          _currentIndex = newIndex;
        });
        widget.onChanged(_currentIndex - widget.extraLines);
      }
    }
    return true;
  }


  @override
  void initState() {
    _currentIndex = widget.initialIndex + widget.extraLines;
    _listViewHeight = widget.itemHeight * (widget.extraLines * 2 + 1);
    _itemCount = widget.itemList.length;
    _listItemCount = _itemCount + widget.extraLines * 2;
    _scrollController = ScrollController( initialScrollOffset: ((_currentIndex - widget.extraLines) * widget.itemHeight).toDouble());
    super.initState();
  }
  
  ///main widget
  @override
  Widget build(BuildContext context) {
    _defaultStyle = Theme.of(context).textTheme.body1;
    _selectedStyle = Theme.of(context).textTheme.headline.copyWith(color: Theme.of(context).accentColor);
    return _buildListView();
  }


  Widget _buildItem(BuildContext context, int index) {
    if ( index < widget.extraLines || index >= (_listItemCount - widget.extraLines)) {
      return  Container();
    } else {
      String value = widget.itemList[index - widget.extraLines].text;
      final TextStyle itemStyle = (index == _currentIndex ? _selectedStyle : _defaultStyle);
      return Center( child: Text(value, style: itemStyle));
    }    
  }


  Widget _buildListView() {
    return NotificationListener(
      child: Container(
        height: _listViewHeight,
        width: widget.width,
        child: ListView.builder(
          controller: _scrollController,
          itemExtent: widget.itemHeight,
          itemCount: _listItemCount,
          itemBuilder: _buildItem,
        ),
      ),
      onNotification: _onIndexNotification,
    );
  }
}

 

Usage

...

List<ValuePickerItem> _durationList = List<ValuePickerItem>();
_durationList.add(ValuePickerItem("00:30", 30));
_durationList.add(ValuePickerItem("01:00", 60));
_durationList.add(ValuePickerItem("02:00", 120));
_durationList.add(ValuePickerItem("05:00", 300));
_durationList.add(ValuePickerItem("10:00", 600));

int _pickerIndex = 2;
...

ValuePicker( 
   itemList: _durationList  , 
   initialIndex: _pickerIndex ,
   onChanged: (newIndex) {
     _pickerIndex = _durationList[newIndex].value;
   },
   extraLines: 2,
   width: 60,
 ),

This widget works fine for me but is not testet on different devices.

Leave a Comment

*