#Flutter項(xiàng)目(3)之仿寫微信通訊錄界面

1 導(dǎo)航欄按鈕的添加

導(dǎo)航欄 appBar 使用AppBar()方法創(chuàng)建;主要用到的控件屬性如下:

  • title:導(dǎo)航欄標(biāo)題
/// 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視圖控件;

  • leading:導(dǎo)航欄標(biāo)題前按鈕;即左邊的按鈕欄,返回的是一個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:導(dǎo)航欄標(biāo)題后按鈕;即右邊的按鈕欄,返回的是一個List<Widget>集合;
/// 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;

示例實(shí)現(xiàn):


appBar: AppBar(
          backgroundColor: WechatThemeColor,
          title: Text('通訊錄'),//標(biāo)題
          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 通訊錄列表及分組實(shí)現(xiàn)

2.1 通訊錄數(shù)據(jù)的處理

對于每一個用戶模型,需要一個屬性值indexLetter來存儲首字母信息,通過對這個屬性值的排序來確定分組,這是按照微信分組的基本思路:

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

通過indexLetter屬性值,對數(shù)組內(nèi)的元素進(jìn)行排序:

_ListDatas.sort((Friends a, Friends b) {
      return a.indexLetter.compareTo(b.indexLetter);
    });
        
//簡寫:(當(dāng)花括號里面只有一句代碼的時候可以簡寫:)

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

    
//使用sort函數(shù)對數(shù)組進(jìn)行排序的用法,當(dāng)數(shù)組元素全部為int類型的時候直接使用sort函數(shù)即可:

/**
   * 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 通訊錄分組表頭的展示實(shí)現(xiàn)

通訊錄組頭的展示邏輯,在創(chuàng)建ListView返回cell的時候:

  • 如果當(dāng)前cell和上一個cell的indexLetter值相同,也就是同一個分組,則當(dāng)前cell不展示頭部;
  • 如果當(dāng)前cell和上一個cell的indexLetter值不相同,也就是新分組,則當(dāng)前cell展示頭部;

Widget _CellForRow(BuildContext context, int index) { 
    //前4個分組為微信固定的 新的朋友,群聊,標(biāo)簽,公眾號4個cell
    if (index < header_datas.length) {
      return _FriendsCell(
        assertImage: header_datas[index].assertImage,
        name: header_datas[index].name,
      );
    }

   // 當(dāng)indexLetter值相同的時候,創(chuàng)建cell,使用_FriendsCell方法不傳入groupTitle值,使得當(dāng)前cell不展示頭部;
    if (index > 4 &&
        _ListDatas[index - 4].indexLetter ==
            _ListDatas[index - 5].indexLetter) {
      return _FriendsCell(
        imageUrl: _ListDatas[index - 4].imageUrl,
        name: _ListDatas[index - 4].name,
      );
    }

   // 當(dāng)indexLetter值不相同的時候,創(chuàng)建cell,使用_FriendsCell方法傳入groupTitle值,使得當(dāng)前cell展示頭部;

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

3 左邊按鈕欄IndexBar實(shí)現(xiàn)

左邊indexBar示例

IndexBar需要實(shí)現(xiàn)的效果:點(diǎn)擊其中的一個字母,通訊錄跳轉(zhuǎn)到指定的分組

3.1 IndexBar的封裝

indexBar是一個單獨(dú)的控件,可以使用一個dart文件將其封裝起來:

  • 需要一個數(shù)組來將A-Z字母裝起來,這個控件其實(shí)一個個小widget上面放Text就可以實(shí)現(xiàn);
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'
];

  • 這些控件是可以點(diǎn)擊的,所以這些widget是有狀態(tài)的,stateful,可以刷新,改變點(diǎn)擊時的狀態(tài);
  • 封裝的這個IndexBar控件需要給外界一個回調(diào),回調(diào)到通訊錄頁面知道點(diǎn)擊的是哪個字母,通訊錄滾動到哪里;
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] );
          }//重復(fù)點(diǎn)擊添加容錯處理

        },

        //按下
        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數(shù)組中直接添加是Expanded包裝的控件,child是一個Text;
然后在IndexBar控件中,返回的是Positioned(自適應(yīng)控件);
然后使用的是Cloumn上下布局,它的children是一個List<Widget> children;所以可以返回的是一個數(shù)組的元素;

需要添加容錯處理的地方:

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

// index = 獲取當(dāng)前手勢的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: 按下刷新狀態(tài),可以添加容錯防止多次重復(fù)點(diǎn)擊;

  • onVerticalDragDown: 按下時,此時回調(diào)callBack到通訊錄中滑動到指定的位置;

  • onVerticalDragEnd: 按下狀態(tài)結(jié)束,改變IndexBar的背景顏色,狀態(tài)等;

4 IndexBar回調(diào),通訊錄滑動到指定分組的位置

在自定義封裝的IndexBar中,需要在選中某個字母的時候,回調(diào)給當(dāng)前頁面選中可某個字母,然后滑動到指定的某個分組的位置;

滑動當(dāng)前的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]等可以滑動的控件;

用法:

  • 創(chuàng)建一個ScrollController實(shí)例化對象;

  • 在創(chuàng)建ListView的時候,傳入一個控制器,傳入當(dāng)前創(chuàng)建的_scrollController;

  • 在滑動的首先添加容錯callBack返回的字符串是不是為空;然后使用_scrollController.animateTo實(shí)現(xiàn)滑動的動畫;

 ScrollController _scrollController = ScrollController();
body: Stack(
  children: <Widget>[
    Container(
      child: ListView.builder(
        controller: _scrollController, //傳入已經(jīng)創(chuàng)建好的ScrollController實(shí)例化對象
        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滑動到指定分組位置的算法實(shí)現(xiàn)

可以在初始化ListView的時候?qū)⒔M或者元素的位置保存通過數(shù)組保存起來;

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

INDEX_WORDS為存放IndexBar元素A-Z的數(shù)組,_groupMap存放的是字母為key,偏移量offset為value;
在使用的時候可以直接通過key也就是字母取出偏移量offset;

算法簡單實(shí)現(xiàn):

 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; 這個是微信原有的新的朋友,群聊,標(biāo)簽,公眾號4個cell的初始化高度,這個是死的;
  • i < 1 ,第一個肯定是有頭部的,存放對應(yīng)的key和value值
  • 如果兩個的IndexLetter相同,表示是同一個組的元素,不是組頭,這時候不需要往字典中存儲元素
  • 如果兩個的IndexLetter不相同,表示當(dāng)前的字母對應(yīng)的為組頭, 這時候需要往字典中存儲元素
  • 不是組頭元素的時候_groupOffset +=54;
  • 是組頭元素的時候_groupOffset +=84;(組頭的高度是30)。

5 總結(jié)

運(yùn)用Flutter構(gòu)建微信通訊錄界面,實(shí)現(xiàn)難度較之前所仿寫的微信發(fā)現(xiàn)和我的界面難度和復(fù)雜度有了較大的提升,通訊錄界面主要有一下幾大難點(diǎn):

  1. 通訊錄ListView的組頭如何實(shí)現(xiàn),需要如何巧妙的實(shí)現(xiàn)
  2. IndexBar的封裝,點(diǎn)擊和回調(diào)的實(shí)現(xiàn),
  3. ListView滑動到指定的分組的位置,需要實(shí)現(xiàn)算好對應(yīng)字母的偏移量,并存放到數(shù)組中;

頁面還有很多需要優(yōu)化的地方,比如在點(diǎn)擊對應(yīng)字母的分組沒有好友的,這時候不需要跳轉(zhuǎn),這種處理方式不太友好,但是基本的功能是實(shí)現(xiàn)了;

通過學(xué)習(xí)這個頁面,發(fā)現(xiàn)算法的思路在任何一門語言中都是必備的,有一個好的算法和思想,有助于提高自己的邏輯,讓自己的思路更清晰,需要多多積累,步步為營。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容