更新地点: 首发于公众号,次日更新于掘金、思否、开发者头条等地方;vue
更多交流: 能够添加个人微信 372623326,关注个人微博:coderwhy算法
学习完列表渲染后,我打算作一个综合一点的练习小项目:豆瓣Top电影排行列表;微信
这个练习小项目主要是为了锻炼布局Widget,可是也涉及到一些其余知识点:评分展现、分割线、bottomNavigationBar等。数据结构
这些内容,咱们放到后面进行补充,可是在进行豆瓣Top电影模仿时,有两个东西实现起来比较复杂:app
一、评分展现: 咱们须要根据不一样的评分显示不一样的星级展现,这里我封装了一个StarRating的小Widget来实现;less
二、分割线: 最初我考虑使用边框虚线
来完成分割线,后来发现Flutter并不支持虚线边框
,所以封装了一个DashedLine的小Widget来实现。ide
固然,这个章节若是你以为过于复杂,能够直接把我封装好的两个东西拿过去使用;布局
目的:实现功能展现的同时,提供高度的定制效果学习
rating
:必传参数,告诉Widget当前的评分。maxRating
:可选参数,最高评分,根据它来计算一个比例,默认值为10;size
:星星的大小,决定每个star的大小;unselectedColor
:未选中星星的颜色(该属性是使用默认的star才有效);selectedColor
:选中星星的颜色(该属性也是使用默认的star才有效);unselectedImage
:定制未选中的star;selectedImage
:定义选中时的star;count
:展现星星的个数;暂时实现上面的定制,后续有新的需求继续添加新的功能点~ui
理清楚思路后,你会发现并非很是复杂,主要就是两点的展现:
问题一:选择StatelessWidget仍是StatefulWidget?
考虑到后面可能会作用户点击进行评分或者用户手指滑动评分的效果,因此这里选择StatefulWidget
问题二:如何让选中的star
和未选中的star
重叠显示?
child: Stack(
children: <Widget>[
Row(children: getUnSelectImage(), mainAxisSize: MainAxisSize.min,),
Row(children: getSelectImage(), mainAxisSize: MainAxisSize.min,),
],
),
复制代码
问题三:如何实现对选中的最后一个star进行裁剪?
定义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,
);
复制代码
最终代码并不复杂,并且我也有给出主要注释:
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;
}
}
复制代码
目的:实现效果的同时,提供定制,而且能够实现水平和垂直两种虚线效果:
axis
:肯定虚线的方向;dashedWidth
:根据虚线的方向肯定本身虚线的宽度;dashedHeight
:根据虚线的方向肯定本身虚线的高度;count
:内部会根据设置的个数和宽高肯定密度(虚线的空白间隔);color
:虚线的颜色,很少作解释;暂时实现上面的定制,后续有新的需求继续添加新的功能点~
实现比较简单,主要是根据用户传入的方向肯定添加对应的SizedBox便可。
这里有一个注意点:虚线究竟是设置多宽或者多高呢?
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),
),
);
}),
);
},
);
复制代码
比较简单的封装,直接给出最终代码实现:
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,如何实现呢?
在Flutter中,咱们会使用Scaffold来搭建页面的基本结构,实际上它里面有一个属性就能够实现底部TabBar功能:bottomNavigationBar。
bottomNavigationBar对应的类型是BottomNavigationBar,咱们来看一下它有什么属性:
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()
],
复制代码
注意事项:
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、数据结构与算法等等,也会更新一些本身的学习心得等,欢迎你们关注