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

1 导航栏按钮的添加

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

  • title:导航栏标题
/// 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;
![4071582710911_.pic_hd.jpg](https://upload-images.jianshu.io/upload_images/6266734-a8b63f8f1956d325.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

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

  • leading:导航栏标题前按钮;即左边的按钮栏,返回的是一个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;
  • actions:导航栏标题后按钮;即右边的按钮栏,返回的是一个List集合;
/// 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的时候:

  • 如果当前cell和上一个cell的indexLetter值相同,也就是同一个分组,则当前cell不展示头部;
  • 如果当前cell和上一个cell的indexLetter值不相同,也就是新分组,则当前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文件将其封装起来:

  • 需要一个数组来将A-Z字母装起来,这个控件其实一个个小widget上面放Text就可以实现;
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'
];

  • 这些控件是可以点击的,所以这些widget是有状态的,stateful,可以刷新,改变点击时的状态;
  • 封装的这个IndexBar控件需要给外界一个回调,回调到通讯录页面知道点击的是哪个字母,通讯录滚动到哪里;
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 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);
  • onVerticalDragUpdate: 按下刷新状态,可以添加容错防止多次重复点击;

  • onVerticalDragDown: 按下时,此时回调callBack到通讯录中滑动到指定的位置;

  • onVerticalDragEnd: 按下状态结束,改变IndexBar的背景颜色,状态等;

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实例化对象;

  • 在创建ListView的时候,传入一个控制器,传入当前创建的_scrollController;

  • 在滑动的首先添加容错callBack返回的字符串是不是为空;然后使用_scrollController.animateTo实现滑动的动画;

 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');
  • _groupOffset的初始值为54.0 * 4; 这个是微信原有的新的朋友,群聊,标签,公众号4个cell的初始化高度,这个是死的;
  • i < 1 ,第一个肯定是有头部的,存放对应的key和value值
  • 如果两个的IndexLetter相同,表示是同一个组的元素,不是组头,这时候不需要往字典中存储元素
  • 如果两个的IndexLetter不相同,表示当前的字母对应的为组头, 这时候需要往字典中存储元素
  • 不是组头元素的时候_groupOffset +=54;
  • 是组头元素的时候_groupOffset +=84;(组头的高度是30)。

5 总结

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

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

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

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

原文链接

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
通过flutter的webview_flutter插件,我们可以在应用中嵌入支付宝和微信支付,并实现支付功能。使用webview_flutter的步骤如下: 1. 首先,在`pubspec.yaml`文件中引入webview_flutter插件。可以在dependencies部分添加`webview_flutter: ^2.0.0`,然后运行`flutter pub get`命令进行依赖安装。 2. 在需要嵌入支付功能的页面中,导入webview_flutter插件。在页面的顶部引入`import 'package:webview_flutter/webview_flutter.dart';`。 3. 在页面的主体中,创建一个WebView组件,并指定需要加载的URL。例如,在Container中使用WebView组件,可以使用如下代码: ```dart Container( child: WebView( initialUrl: 'https://www.alipay.com/', javascriptMode: JavascriptMode.unrestricted, ), ) ``` 4. 在支付宝和微信支付的URL中,传递相关的支付参数,例如订单号、支付金额等。 5. 在WebView组件中,可以通过注册一个JavaScript channel来监听网页中的支付回调信息。例如,在页面初始加载完成后,可以通过使用`onPageFinished`回调方法来执行一段JavaScript代码,监听支付结果。在该代码中,可以通过调用与原生平台交互的方法,将支付结果返回到Flutter中进行处理。 6. 在Flutter中,可以根据支付结果展示相关的提示信息,例如支付成功、支付失败等。 需要注意的是,支付宝和微信支付的具体接口和参数可能会根据版本的更新而有所变化,所以在代码实现中需要根据最新的文档进行调整。同时,为了确保支付过程的安全性,建议在应用中对支付接口进行适当的安全设置,例如使用HTTPS协议,并对支付参数进行加密处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值