Flutter

#Flutter项目(3)之仿写微信通讯录界面

2020-02-26  本文已影响0人  YYFast

1 导航栏按钮的添加

导航栏 appBar 使用AppBar()方法创建;主要用到的控件属性如下:

/// The primary widget displayed in the app bar.
///
/// Typically a [Text] widget containing a description of the current contents
/// of the app.
final Widget title;

注意:title需要返回的是一个widget, Typically a [Text],一般情况下是一个文本,也可以是一个图片,也可以是一个自定义的widget视图控件;

/// A widget to display before the [title].
   AppBar(
    leading: Builder(
       builder: (BuildContext context) {
         return IconButton(
           icon: const Icon(Icons.menu),
           onPressed: () { Scaffold.of(context).openDrawer(); },
           tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
         );
       },
     ),
   )
  /// {@end-tool}
  ///
  /// The [Builder] is used in this example to ensure that the `context` refers
  /// to that part of the subtree. That way this code snippet can be used even
  /// inside the very code that is creating the [Scaffold] (in which case,
  /// without the [Builder], the `context` wouldn't be able to see the
  /// [Scaffold], since it would refer to an ancestor of that widget).
  ///
  /// See also:
  ///
  ///  * [Scaffold.appBar], in which an [AppBar] is usually placed.
  ///  * [Scaffold.drawer], in which the [Drawer] is usually placed.
  final Widget leading;
/// Widgets to display after the [title] widget.
  ///
  /// Typically these widgets are [IconButton]s representing common operations.
  /// For less common operations, consider using a [PopupMenuButton] as the
  /// last action.
  final List<Widget> actions;

示例实现:


appBar: AppBar(
          backgroundColor: WechatThemeColor,
          title: Text('通讯录'),//标题
          leading://左按钮
          GestureDetector(
            child: Container(
              margin: EdgeInsets.only(left: 15,top: 15),
              child:Text(
                '更多',
                style: TextStyle(
                fontSize: 20,

              ),
                textAlign: TextAlign.center,
              )
            ),
            onTap: () {
              Navigator.of(context)
                  .push(MaterialPageRoute(builder: (BuildContext context) {
                return SubDiscover_Page(
                  title: '添加好友',
                );
              }));
            },
          ),
          actions: <Widget>[ //右按钮
            GestureDetector(
              child: Container(
                margin: EdgeInsets.only(right: 15),
                child: Image(
                  image: AssetImage('images/icon_friends_add.png'),
                  width: 25,
                ),
              ),
              onTap: () {
                Navigator.of(context)
                    .push(MaterialPageRoute(builder: (BuildContext context) {
                  return SubDiscover_Page(
                    title: '添加好友',
                  );
                }));
              },
            ),
         ],
        ),

2 通讯录列表及分组实现

2.1 通讯录数据的处理

对于每一个用户模型,需要一个属性值indexLetter来存储首字母信息,通过对这个属性值的排序来确定分组,这是按照微信分组的基本思路:

Friends(
    imageUrl: 'https://randomuser.me/api/portraits/women/57.jpg',
    name: 'Lina',
    indexLetter: 'L', //用于好友分组
    message: 'hello YYFast !',
    time: '下午 3:45',
  ),

通过indexLetter属性值,对数组内的元素进行排序:

_ListDatas.sort((Friends a, Friends b) {
      return a.indexLetter.compareTo(b.indexLetter);
    });
        
//简写:(当花括号里面只有一句代码的时候可以简写:)

_ListDatas.sort((Friends a, Friends b) =>
      a.indexLetter.compareTo(b.indexLetter);
    );

    
//使用sort函数对数组进行排序的用法,当数组元素全部为int类型的时候直接使用sort函数即可:

/**
   * Sorts this list according to the order specified by the [compare] function.
   *
   * The [compare] function must act as a [Comparator].
   *
   *     List<String> numbers = ['two', 'three', 'four'];
   *     // Sort from shortest to longest.
   *     numbers.sort((a, b) => a.length.compareTo(b.length));
   *     print(numbers);  // [two, four, three]
   *
   * The default List implementations use [Comparable.compare] if
   * [compare] is omitted.
   *
   *     List<int> nums = [13, 2, -11];
   *     nums.sort();
   *     print(nums);  // [-11, 2, 13]
   *
   * A [Comparator] may compare objects as equal (return zero), even if they
   * are distinct objects.
   * The sort function is not guaranteed to be stable, so distinct objects
   * that compare as equal may occur in any order in the result:
   *
   *     List<String> numbers = ['one', 'two', 'three', 'four'];
   *     numbers.sort((a, b) => a.length.compareTo(b.length));
   *     print(numbers);  // [one, two, four, three] OR [two, one, four, three]
   */


2.2 通讯录分组表头的展示实现

通讯录组头的展示逻辑,在创建ListView返回cell的时候:


Widget _CellForRow(BuildContext context, int index) { 
    //前4个分组为微信固定的 新的朋友,群聊,标签,公众号4个cell
    if (index < header_datas.length) {
      return _FriendsCell(
        assertImage: header_datas[index].assertImage,
        name: header_datas[index].name,
      );
    }

   // 当indexLetter值相同的时候,创建cell,使用_FriendsCell方法不传入groupTitle值,使得当前cell不展示头部;
    if (index > 4 &&
        _ListDatas[index - 4].indexLetter ==
            _ListDatas[index - 5].indexLetter) {
      return _FriendsCell(
        imageUrl: _ListDatas[index - 4].imageUrl,
        name: _ListDatas[index - 4].name,
      );
    }

   // 当indexLetter值不相同的时候,创建cell,使用_FriendsCell方法传入groupTitle值,使得当前cell展示头部;

    return _FriendsCell(
      imageUrl: _ListDatas[index - 4].imageUrl,
      name: _ListDatas[index - 4].name,
      groupTitle: _ListDatas[index - 4].indexLetter,
    );
  }

3 左边按钮栏IndexBar实现

左边indexBar示例

IndexBar需要实现的效果:点击其中的一个字母,通讯录跳转到指定的分组

3.1 IndexBar的封装

indexBar是一个单独的控件,可以使用一个dart文件将其封装起来:

const INDEX_WORDS = [
  '🔍',
  '☆',
  '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'
];

class IndexBar extends StatefulWidget {
  final void Function (String string) indexBarCallBack;

  const IndexBar({Key key, this.indexBarCallBack}) : super(key: key);
  @override
  _IndexBarState createState() => _IndexBarState();
}


int GetIndex(BuildContext context,Offset globalPosition){

  RenderBox box = context.findRenderObject();
  double y =  box.globalToLocal(globalPosition).dy;
  //每一个Item的高度
  var ItemHeight = ScreenHeignt(context)/2/INDEX_WORDS.length;

  //clamp 防止越界
  int index = (y ~/ItemHeight).clamp(0, INDEX_WORDS.length - 1);

  return  index;
  print(' index = $index  ,${INDEX_WORDS[index]}');

}

class _IndexBarState extends State<IndexBar> {
  var _selectedIndex = -1;

  Color _IndexBarBackColor = Color.fromRGBO(1, 1, 1, 0.0);
  Color _TextColor = Colors.black;

  @override

  Widget build(BuildContext context) {
    List <Widget> _WordsWidget = [];

    for(int i = 0; i < INDEX_WORDS.length;i ++){
      _WordsWidget.add(Expanded(child: Text(INDEX_WORDS[i],style: TextStyle(color:_TextColor),),));
    }
    return Positioned(
      right: 0.0,
      width: 30,
      top: ScreenHeignt(context)/8,
      height: ScreenHeignt(context)/2,
      child: GestureDetector(
        child: Container(
          color:_IndexBarBackColor,
          child: Column(
            children: _WordsWidget,
          ),
        ),


        onVerticalDragUpdate: (DragUpdateDetails details){
          if(_selectedIndex != GetIndex(context, details.globalPosition)){
            _selectedIndex = GetIndex(context, details.globalPosition);
            widget.indexBarCallBack(INDEX_WORDS[_selectedIndex] );
          }//重复点击添加容错处理

        },

        //按下
        onVerticalDragDown: (DragDownDetails details){
          setState(() {
            _IndexBarBackColor = Color.fromRGBO(1, 1, 1, 0.3);
            _TextColor = WechatThemeColor;
          });
          widget.indexBarCallBack(INDEX_WORDS[GetIndex(context, details.globalPosition)] );
        },
        
        onVerticalDragEnd: (DragEndDetails details){
          setState(() {
            _IndexBarBackColor = Color.fromRGBO(1, 1, 1, 0.0);
            _TextColor = Colors.black;
          });
        },
      ),
    );
  }
}

_WordsWidget数组中直接添加是Expanded包装的控件,child是一个Text;
然后在IndexBar控件中,返回的是Positioned(自适应控件);
然后使用的是Cloumn上下布局,它的children是一个List<Widget> children;所以可以返回的是一个数组的元素;

需要添加容错处理的地方:

//clamp 防止越界
int index = (y ~/ItemHeight).clamp(0, INDEX_WORDS.length - 1);

// index = 获取当前手势的y方向的偏移量/每个item的高度.clamp(最小值,最大值)

//在dart中相除取整可以使用: ~/
/** 
 * Returns this [num] clamped to be in the range [lowerLimit]-[upperLimit].
 *
 * The comparison is done using [compareTo] and therefore takes `-0.0` into
 * account. This also implies that [double.nan] is treated as the maximal
 * double value.
 *
 * The arguments [lowerLimit] and [upperLimit] must form a valid range where
 * `lowerLimit.compareTo(upperLimit) <= 0`.
 */
num clamp(num lowerLimit, num upperLimit);

4 IndexBar回调,通讯录滑动到指定分组的位置

在自定义封装的IndexBar中,需要在选中某个字母的时候,回调给当前页面选中可某个字母,然后滑动到指定的某个分组的位置;

滑动当前的ListView,需要的一个控制器ScrollController:

/// Controls a scrollable widget.
///
/// Scroll controllers are typically stored as member variables in [State]
/// objects and are reused in each [State.build]. A single scroll controller can
/// be used to control multiple scrollable widgets, but some operations, such
/// as reading the scroll [offset], require the controller to be used with a
/// single scrollable widget.
///
/// A scroll controller creates a [ScrollPosition] to manage the state specific
/// to an individual [Scrollable] widget. To use a custom [ScrollPosition],
/// subclass [ScrollController] and override [createScrollPosition].
///
/// A [ScrollController] is a [Listenable]. It notifies its listeners whenever
/// any of the attached [ScrollPosition]s notify _their_ listeners (i.e.
/// whenever any of them scroll). It does not notify its listeners when the list
/// of attached [ScrollPosition]s changes.
///
/// Typically used with [ListView], [GridView], [CustomScrollView].
///
/// See also:
///
///  * [ListView], [GridView], [CustomScrollView], which can be controlled by a
///    [ScrollController].
///  * [Scrollable], which is the lower-level widget that creates and associates
///    [ScrollPosition] objects with [ScrollController] objects.
///  * [PageController], which is an analogous object for controlling a
///    [PageView].
///  * [ScrollPosition], which manages the scroll offset for an individual
///    scrolling widget.
///  * [ScrollNotification] and [NotificationListener], which can be used to watch
///    the scroll position without using a [ScrollController].
class ScrollController extends ChangeNotifier 

ScrollController可以控制[ListView], [GridView], [CustomScrollView]等可以滑动的控件;

用法:

 ScrollController _scrollController = ScrollController();
body: Stack(
  children: <Widget>[
    Container(
      child: ListView.builder(
        controller: _scrollController, //传入已经创建好的ScrollController实例化对象
        itemCount: _ListDatas.length + header_datas.length,
        itemBuilder: _CellForRow,
      ),
    ), //通讯录列表

    IndexBar(
      indexBarCallBack: (String string) {
        print(_groupMap[string]);
        if(_groupMap[string]!=null){
          _scrollController.animateTo(_groupMap[string],
              duration: Duration(milliseconds: 100),
              curve: Curves.easeIn);
        }
      },
    ),
  ],
));

4.1 ListView滑动到指定分组位置的算法实现

可以在初始化ListView的时候将组或者元素的位置保存通过数组保存起来;

final Map _groupMap = {
    INDEX_WORDS[0]: 0.0,
    INDEX_WORDS[1]: 0.0,
  };

INDEX_WORDS为存放IndexBar元素A-Z的数组,_groupMap存放的是字母为key,偏移量offset为value;
在使用的时候可以直接通过key也就是字母取出偏移量offset;

算法简单实现:

 var _groupOffset = 54.0 * 4;

for (int i = 0; i < _ListDatas.length; i++) {
  if(i <1){
    //第一个一定是头部
    _groupMap.addAll({_ListDatas[i].indexLetter:_groupOffset});
    _groupOffset +=84 ;
  }else if(_ListDatas[i].indexLetter == _ListDatas[i -1].indexLetter){
    //如果没有头
    _groupOffset +=54;
  }else{
    _groupMap.addAll({_ListDatas[i].indexLetter:_groupOffset});
    _groupOffset +=84 ;
  }
}
print('-----$_groupMap');

5 总结

运用Flutter构建微信通讯录界面,实现难度较之前所仿写的微信发现和我的界面难度和复杂度有了较大的提升,通讯录界面主要有一下几大难点:

  1. 通讯录ListView的组头如何实现,需要如何巧妙的实现
  2. IndexBar的封装,点击和回调的实现,
  3. ListView滑动到指定的分组的位置,需要实现算好对应字母的偏移量,并存放到数组中;

页面还有很多需要优化的地方,比如在点击对应字母的分组没有好友的,这时候不需要跳转,这种处理方式不太友好,但是基本的功能是实现了;

通过学习这个页面,发现算法的思路在任何一门语言中都是必备的,有一个好的算法和思想,有助于提高自己的逻辑,让自己的思路更清晰,需要多多积累,步步为营。

上一篇下一篇

猜你喜欢

热点阅读