Flutter(十一)之封裝幾個小Widget

更新地點: 首發於公衆號,次日更新於掘金、思否、開發者頭條等地方;vue

更多交流: 能夠添加個人微信 372623326,關注個人微博:coderwhy算法

學習完列表渲染後,我打算作一個綜合一點的練習小項目:豆瓣Top電影排行列表;微信

image-20191002084721424

這個練習小項目主要是爲了鍛鍊佈局Widget,可是也涉及到一些其餘知識點:評分展現、分割線、bottomNavigationBar等。數據結構

這些內容,咱們放到後面進行補充,可是在進行豆瓣Top電影模仿時,有兩個東西實現起來比較複雜:app

一、評分展現: 咱們須要根據不一樣的評分顯示不一樣的星級展現,這裏我封裝了一個StarRating的小Widget來實現;less

二、分割線: 最初我考慮使用邊框虛線來完成分割線,後來發現Flutter並不支持虛線邊框,所以封裝了一個DashedLine的小Widget來實現。ide

固然,這個章節若是你以爲過於複雜,能夠直接把我封裝好的兩個東西拿過去使用;佈局

一. StarRating

1.1. 最終效果展現

目的:實現功能展現的同時,提供高度的定製效果學習

  • rating:必傳參數,告訴Widget當前的評分。
  • maxRating:可選參數,最高評分,根據它來計算一個比例,默認值爲10;
  • size:星星的大小,決定每個star的大小;
  • unselectedColor:未選中星星的顏色(該屬性是使用默認的star纔有效);
  • selectedColor:選中星星的顏色(該屬性也是使用默認的star纔有效);
  • unselectedImage:定製未選中的star;
  • selectedImage:定義選中時的star;
  • count:展現星星的個數;

暫時實現上面的定製,後續有新的需求繼續添加新的功能點~ui

image-20191002085908926

1.2. 實現思路分析

理清楚思路後,你會發現並非很是複雜,主要就是兩點的展現:

  • 未選中star的展現:根據個數和傳入的unselectedImage建立對應個數的Widget便可;
  • 選中star的展現:
    • 計算出滿star的個數,建立對應的Widget;
    • 計算剩餘比例的評分,對最後一個Widget進行裁剪;

問題一:選擇StatelessWidget仍是StatefulWidget?

考慮到後面可能會作用戶點擊進行評分或者用戶手指滑動評分的效果,因此這裏選擇StatefulWidget

  • 目前尚未講解事件監聽相關,因此暫時不添加這個功能

問題二:如何讓選中的star未選中的star重疊顯示?

  • 很是簡單,使用Stack便可;
child: Stack(
  children: <Widget>[
    Row(children: getUnSelectImage(), mainAxisSize: MainAxisSize.min,),
    Row(children: getSelectImage(), mainAxisSize: MainAxisSize.min,),
  ],
),
複製代碼

問題三:如何實現對選中的最後一個star進行裁剪?

  • 可使用ClipRect定製CustomClipper進行裁剪

定義CustomClipper裁剪規則:

class MyRectClipper extends CustomClipper<Rect>{
  final double width;

  MyRectClipper({
    this.width
  });

  @override
  Rect getClip(Size size) {
    return Rect.fromLTRB(0, 0, width, size.height);
  }

  @override
  bool shouldReclip(MyRectClipper oldClipper) {
    return width != oldClipper.width;
  }
}
複製代碼

使用MyRectClipper進行裁剪:

Widget leftStar = ClipRect(
  clipper: MyRectClipper(width: leftRatio * widget.size),
  child: widget.selectedImage,
);
複製代碼

1.3. 最終代碼實現

最終代碼並不複雜,並且我也有給出主要註釋:

import 'package:flutter/material.dart';

class HYStarRating extends StatefulWidget {
  final double rating;
  final double maxRating;
  final Widget unselectedImage;
  final Widget selectedImage;
  final int count;
  final double size;
  final Color unselectedColor;
  final Color selectedColor;

  HYStarRating({
    @required this.rating,
    this.maxRating = 10,
    this.size = 30,
    this.unselectedColor = const Color(0xffbbbbbb),
    this.selectedColor = const Color(0xffe0aa46),
    Widget unselectedImage,
    Widget selectedImage,
    this.count = 5,
  }): unselectedImage = unselectedImage ?? Icon(Icons.star, size: size, color: unselectedColor,),
        selectedImage = selectedImage ?? Icon(Icons.star, size: size, color: selectedColor);

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

class _HYStarRatingState extends State<HYStarRating> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Stack(
        children: <Widget>[
          Row(children: getUnSelectImage(), mainAxisSize: MainAxisSize.min),
          Row(children: getSelectImage(), mainAxisSize: MainAxisSize.min),
        ],
      ),
    );
  }

  // 獲取評星
  List<Widget> getUnSelectImage() {
    return List.generate(widget.count, (index) => widget.unselectedImage);
  }

  List<Widget> getSelectImage() {
    // 1.計算Star個數和剩餘比例等
    double oneValue = widget.maxRating / widget.count;
    int entireCount = (widget.rating / oneValue).floor();
    double leftValue = widget.rating - entireCount * oneValue;
    double leftRatio = leftValue / oneValue;

    // 2.獲取start
    List<Widget> selectedImages = [];
    for (int i = 0; i < entireCount; i++) {
      selectedImages.add(widget.selectedImage);
    }

    // 3.計算
    Widget leftStar = ClipRect(
      clipper: MyRectClipper(width: leftRatio * widget.size),
      child: widget.selectedImage,
    );
    selectedImages.add(leftStar);

    return selectedImages;
  }
}


class MyRectClipper extends CustomClipper<Rect>{
  final double width;

  MyRectClipper({
    this.width
  });

  @override
  Rect getClip(Size size) {
    return Rect.fromLTRB(0, 0, width, size.height);
  }

  @override
  bool shouldReclip(MyRectClipper oldClipper) {
    return width != oldClipper.width;
  }
}
複製代碼

二. DashedLine

2.1. 最終實現效果

目的:實現效果的同時,提供定製,而且能夠實現水平和垂直兩種虛線效果:

  • axis:肯定虛線的方向;
  • dashedWidth:根據虛線的方向肯定本身虛線的寬度;
  • dashedHeight:根據虛線的方向肯定本身虛線的高度;
  • count:內部會根據設置的個數和寬高肯定密度(虛線的空白間隔);
  • color:虛線的顏色,很少作解釋;

image-20191002105938669

暫時實現上面的定製,後續有新的需求繼續添加新的功能點~

2.2. 實現思路分析

實現比較簡單,主要是根據用戶傳入的方向肯定添加對應的SizedBox便可。

這裏有一個注意點:虛線究竟是設置多寬或者多高呢?

  • 我這裏是根據方向獲取父Widget的寬度和高度來決定的;
  • 經過LayoutBuilder能夠獲取到父Widget的寬度和高度;
return LayoutBuilder(
  builder: (BuildContext context, BoxConstraints constraints) {
    // 根據寬度計算個數
    return Flex(
      direction: this.axis,
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: List.generate(this.count, (int index) {
        return SizedBox(
          width: dashedWidth,
          height: dashedHeight,
          child: DecoratedBox(
            decoration: BoxDecoration(color: color),
          ),
        );
      }),
    );
  },
);
複製代碼

2.3. 最終代碼實現

比較簡單的封裝,直接給出最終代碼實現:

class HYDashedLine extends StatelessWidget {
  final Axis axis;
  final double dashedWidth;
  final double dashedHeight;
  final int count;
  final Color color;

  HYDashedLine({
    @required this.axis,
    this.dashedWidth = 1,
    this.dashedHeight = 1,
    this.count,
    this.color = const Color(0xffff0000)
  });

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        // 根據寬度計算個數
        return Flex(
          direction: this.axis,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: List.generate(this.count, (int index) {
            return SizedBox(
              width: dashedWidth,
              height: dashedHeight,
              child: DecoratedBox(
                decoration: BoxDecoration(color: color),
              ),
            );
          }),
        );
      },
    );
  }
}
複製代碼

三. 實現底部TabBar

3.1. TabBar實現說明

image-20191002121426017

在即將完成的小練習中,咱們有實現一個底部的TabBar,如何實現呢?

在Flutter中,咱們會使用Scaffold來搭建頁面的基本結構,實際上它裏面有一個屬性就能夠實現底部TabBar功能:bottomNavigationBar。

bottomNavigationBar對應的類型是BottomNavigationBar,咱們來看一下它有什麼屬性:

  • 屬性很是多,可是都是設置底部TabBar相關的,咱們介紹幾個:
  • currentIndex:當前選中哪個item;
  • selectedFontSize:選中時的文本大小;
  • unselectedFontSize:未選中時的文本大小;
  • type:當item的數量超過2個時,須要設置爲fixed;
  • items:放入多個BottomNavigationBarItem類型;
  • onTap:監聽哪個item被選中;
class BottomNavigationBar extends StatefulWidget {
  BottomNavigationBar({
    Key key,
    @required this.items,
    this.onTap,
    this.currentIndex = 0,
    this.elevation = 8.0,
    BottomNavigationBarType type,
    Color fixedColor,
    this.backgroundColor,
    this.iconSize = 24.0,
    Color selectedItemColor,
    this.unselectedItemColor,
    this.selectedIconTheme = const IconThemeData(),
    this.unselectedIconTheme = const IconThemeData(),
    this.selectedFontSize = 14.0,
    this.unselectedFontSize = 12.0,
    this.selectedLabelStyle,
    this.unselectedLabelStyle,
    this.showSelectedLabels = true,
    bool showUnselectedLabels,
  }) 
}
複製代碼

當實現了底部TabBar展現後,咱們須要監聽它的點擊來切換顯示不一樣的頁面,這個時候咱們可使用IndexedStack來管理多個頁面的切換:

body: IndexedStack(
  index: _currentIndex,
  children: <Widget>[
    Home(),
    Subject(),
    Group(),
    Mall(),
    Profile()
  ],
複製代碼

3.2. TabBar代碼實現

注意事項:

  • 一、咱們須要在其餘地方建立對應要切換的頁面;
  • 二、須要引入對應的資源,而且在pubspec.yaml中引入;
import 'package:flutter/material.dart';
import 'views/home/home.dart';
import 'views/subject/subject.dart';
import 'views/group/group.dart';
import 'views/mall/mall.dart';
import 'views/profile/profile.dart';


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "豆瓣",
      theme: ThemeData(
        primaryColor: Colors.green,
        highlightColor: Colors.transparent,
        splashColor: Colors.transparent
      ),
      home: MyStackPage(),
    );
  }
}

class MyStackPage extends StatefulWidget {
  @override
  _MyStackPageState createState() => _MyStackPageState();
}

class _MyStackPageState extends State<MyStackPage> {

  var _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        selectedFontSize: 14,
        unselectedFontSize: 14,
        type: BottomNavigationBarType.fixed,
        items: [
          createItem("home", "首頁"),
          createItem("subject", "書影音"),
          createItem("group", "小組"),
          createItem("mall", "市集"),
          createItem("profile", "個人"),
        ],
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
      ),
      body: IndexedStack(
        index: _currentIndex,
        children: <Widget>[
          Home(),
          Subject(),
          Group(),
          Mall(),
          Profile()
        ],
      ),
    );
  }
}

BottomNavigationBarItem createItem(String iconName, String title) {
  return BottomNavigationBarItem(
      icon: Image.asset("assets/images/tabbar/$iconName.png", width: 30,),
      activeIcon: Image.asset("assets/images/tabbar/${iconName}_active.png", width: 30,),
      title: Text(title)
  );
}
複製代碼

備註:全部內容首發於公衆號,以後除了Flutter也會更新其餘技術文章,TypeScript、React、Node、uniapp、mpvue、數據結構與算法等等,也會更新一些本身的學習心得等,歡迎你們關注

公衆號
相關文章
相關標籤/搜索