上篇博客我们学习了flutter中提供的ListView等常见的滚动类组件,本篇博客我们开始对常用的功能类组件和包裹容器类组件的学习
功能类组件
InkWell
在flutter开发中,我们可能会遇到一种场景下我们需要给没有默认提供手势点击事件的组件添加点击事件或者双击事件等操作,从而触发一系列操作,这个时候我们应该怎么做呢?在原生安卓开发中,我们知道,可以给任意view添加一个onclick即可完成点击事件,那么在flutter中如何去做?其实在flutter中给我们提供了一个功能组件用来包裹需要出发手势点击功能的组件--InkWell,翻看源码才发现,当前组件只有一个默认的构造,如下:
const InkWell({
Key key,
Widget child,//需要添加事件的子组件,在这里挂载
GestureTapCallback onTap,//给子组件提供的点击事件能力,回调中写需要触发的功能
GestureTapCallback onDoubleTap,//给子组件提供的双击触发事件的能力,回调中写需要实现的功能
GestureLongPressCallback onLongPress,//给子组件提供的长按触发事件的能力,回调中填写具体实现业务
GestureTapDownCallback onTapDown,//子组件提供的按下触发的事件的能力,回调中填写具体业务
GestureTapCancelCallback onTapCancel,//子组件提供的点击取消事件的能力,回调中填写具体实现业务
ValueChanged<bool> onHighlightChanged,//值改变的时候触发的事件
Color highlightColor,//值改变的时候的突出的颜色
Color splashColor,
InteractiveInkFeatureFactory splashFactory,//可以实现飞溅效果的工厂,类型为InteractiveInkFeatureFactory
double radius,
BorderRadius borderRadius,
ShapeBorder customBorder,
bool enableFeedback = true,//是否启用反馈操作,默认启用
bool excludeFromSemantics = false,//是否将当前组件从from中排除,不受管控,默认是false,不排除
}) : super(
key: key,
child: child,
onTap: onTap,
onDoubleTap: onDoubleTap,
onLongPress: onLongPress,
onTapDown: onTapDown,
onTapCancel: onTapCancel,
onHighlightChanged: onHighlightChanged,
containedInkWell: true,
highlightShape: BoxShape.rectangle,
highlightColor: highlightColor,
splashColor: splashColor,
splashFactory: splashFactory,
radius: radius,
borderRadius: borderRadius,
customBorder: customBorder,
enableFeedback: enableFeedback,
excludeFromSemantics: excludeFromSemantics,
);
可以从构造中看出来,只要添加一个子组件和一个具体触发的事件就可以了,接下来我们来简单实现一个案例:
import 'package:flutter/material.dart';
class MyWell extends StatefulWidget{
@override
State<StatefulWidget> createState() {
return new _MyWell();
}
}
class _MyWell extends State<StatefulWidget>{
@override
Widget build(BuildContext context) {
return new Scaffold(
body:new Column(
children: <Widget>[
new Center(
child:new Material(
child: new InkWell(//添加了InkWell,重写了onTap点击事件,提供点击事件的能力
onTap: (){
print('触发了点击事件');
},
child: new Container(//不要在这里设置背景色,会遮挡水波纹效果,如果设置的话在Material下面的color来实现
width: 300.0,
height: 100.0,
margin: EdgeInsets.all(0.0),//整体没有外边距
),
),
color: Colors.yellow,
),
),
],
),
);
}
}
这样本身没有默认点击事件的容器就可以指定触发点击 事件等操作了,其他的操作可以自行操作研究
WillPopScope
在app开发中,我们会有这样的需求,整个页面开发的时候我们需要拦截back键的操作,不可以回退某些页面,或者有点击多次退出app等这样的需求,同样在flutter上也有对应的功能性组件----WillPopScope,构造如下(前面组件介绍已经写过注释的属性我们后面不会在写注释):
const WillPopScope({
Key key,
@required this.child,//包裹的子组件,相当于我们在什么组件上触发回退事件的时候需要进行拦截back操作,原理和LinkWell差不多
@required this.onWillPop,//当前属性是一个回调函数,类型为WillPopCallback,当我们点击返回按钮的时候,改回调需要返回一个Future对象,返回的结果为false的话,说明当前的页面路由不从栈中移除(即不移除当前页面),如果为true,说明当前页面退出(出栈)
}) : assert(child != null),
super(key: key);
构造函数看起来很简单,那么我们通过当前组件实现一个app开发经常使用的功能,即页面中拦截back操作,当我们1s内点击两次后,退出当前页面(也可以是退出app,看你实现)的案例:
import 'package:flutter/material.dart';
class WillPopScopeTest extends StatefulWidget {
@override
WillPopScopeTestState createState() {
return new WillPopScopeTestState();
}
}
class WillPopScopeTestState extends State<WillPopScopeTest> {
DateTime _oldTime; //上次点击时间
@override
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: () async {
//如果上次点击的时间为null,并且上次的时间和当前的时间差距大于1s
//DateTime.now()方法获取当前的时间(按照当前语言国际化的时间),
//difference函数可以拿前一个时间与参数中的时间进行比较和计算,计算出时间差,单位为秒或者毫秒
if (_oldTime == null || DateTime.now().difference(_oldTime) > Duration(seconds: 1)) {
//两次点击间隔超过1秒则重新计时
_oldTime = DateTime.now();
return false;
}
return true;
},
child: Container(
alignment: Alignment.center,
child: Text("连续点击两次才可以退出当前页面"),
)
);
}
}
包裹容器
Padding
相信看到Padding 这个组件你一定不会陌生了,不错,上面的很多案例中我们都使用了当前组件,而当前组件的作用是给子组件进行留白(填充空白,内边距填充空白)功能,构造如下:
const Padding({
Key key,
@required this.padding,//当前参数必填,类型为EdgeInsetsGeometry,我们一般开发的时候使用EdgeInsets类来进行位置偏移和留白的操作
Widget child,//需要进行填充的子组件
}) : assert(padding != null),
super(key: key, child: child);
可以看出来,基本上都是padding属性实现的效果,我们来看下EdgeInsets类:
const EdgeInsets.fromLTRB(this.left, this.top, this.right, this.bottom);
默认构造只要我们提供一个上下左右坐标的偏移量,就可以了,除此之外还有些简易的快捷构造,如下:
函数 | 函数功能介绍 |
---|---|
fromLTRB(double left, double top, double right, double bottom) | 给当前子组件的四个方向分别设置偏移留白 |
all(double value) | 四个方向使用同样的偏移量进行留白 |
only({left, top, right ,bottom }) | 可选的参数列表,可以指定任意几个方向的偏移量进行留白 |
symmetric({ vertical, horizontal }) | 用来指定对称方向的留白操作, vertical指top和bottom,horizontal指left和right ,指定一个对称方向即这两个方向同时指定一样数值进行偏移留白 |
那么我们来根据这些简单的实现个案例:
import 'package:flutter/material.dart';
class PaddingTest extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
//上下左右各添加16像素补白
padding: EdgeInsets.all(16.0),
child: Column(
//指定对齐方式为左对齐(横轴对齐)
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(left: 8.0),//仅仅是左边偏移8px进行补白
child: Text("only left"),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),//垂直方向整体设置了8px补白(上下各8px)
child: Text("top and bottom"),
),
Padding(
padding: const EdgeInsets.fromLTRB(20.0,0,20.0,0),//四个方向都分别指定不一样的偏移和补白策略
child: Text("left, top, right ,bottom"),
)
],
),
);
}
}
RenderConstrainedBox
flutter中常用到一种约束组件,可以用来限制子组件的大小,或者强制改变内部组件的空间的一类组件,这类组件都是通过RenderConstrainedBox 来渲染,所以这里归为一类学习,而开发中我们常用的有ConstrainedBox和SizedBox
ConstrainedBox
ConstrainedBox可以用来给子组件设置额外的约束,比如内部的子widget的最小的高度我们需要指定为60px,就可以使用当前的组件进行限制,构造如下:
ConstrainedBox({
Key key,
@required this.constraints, //必传参数,类型为BoxConstraints,当前属性指定了对子组件的约束行为
Widget child //需要进行约束的子组件,可以强制改变子组件的行为
}) : assert(constraints != null),
assert(constraints.debugAssertIsValid()),
super(key: key, child: child);
可以看出来我们通过BoxConstraints进行约束组件,那么该类的默认构造我们看一下:
const BoxConstraints({
this.minWidth = 0.0,//可以限制最小的宽度
this.maxWidth = double.infinity,//可限制的最大宽度,默认为尽可能大,即宽度最大是多少就是多少
this.minHeight = 0.0,//可以限制的最小高度
this.maxHeight = double.infinity//可以限制的最大高度
});
从上面我们可以看出来,ConstrainedBox可以限制子组件的宽度和高度的值,接下来我们来实现一个限制子组件的案例:
//定义一个蓝色的盒子容器,但是我们不去指定宽度和高度
Widget blueBox=new DecoratedBox(
decoration: BoxDecoration(color: Colors.blue),
);
//在使用的组件中使用当前组件
new ConstrainedBox(
constraints: new BoxConstraints(
minWidth: double.infinity, //宽度最大
minHeight: 50.0 //最小高度为50px
),
child: new Container(
height: 5.0,
child: blueBox
),
)
写完以后我们会发现,我们虽然设置的高度是5px,但是效果却是50px,宽度撑满了屏幕,这是因为我们限制了最小宽度和最小高度,这个时候,如果子组件不足这个数,会强制限制按照这个数值来显示,同理设置了最大值也是一样的效果,如果子组件大于这个数值,也会按照这个数值来显示
SizedBox
SizedBox可以用来给每一个子的widget强制指定宽高,例如:
SizedBox(
width: 80.0,//强制指定宽度80
height: 80.0,//强制指定高度也是80
child: blueBox
)
然后我们就会看到子组件的大小和宽高变化成我们约束的样式了,这个时候我们就很奇怪,ConstrainedBox不也可以限制我们子组件的大小吗?这两者有什么区别吗?其实这两者没有太大区别,因为SizedBox就是基于ConstrainedBox实现的,我们可以看成一个简化版,接着我们看下SizedBox的构造:
const SizedBox({ Key key, this.width, this.height, Widget child })
: super(key: key, child: child);
从构造可以看出来,我们只要传递宽高和当前组件就可以了,比起ConstrainedBox要简单许多,而我们上面的案例等价于
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
child: blueBox,
)
而BoxConstraints.tightFor这个构造其实等同于
BoxConstraints(minHeight: 80.0,maxHeight: 80.0,minWidth: 80.0,maxWidth: 80.0)
所以正常情况下我们需要限制子组件的宽高的时候建议都使用SizedBox
UnconstrainedBox
我们上面介绍了一些可以限制宽高的组件,那么我们如果在复杂的布局中,限制了大量的组件,但是我们需要按照触发条件不对某些组件进行限制,让他们按照自己的宽高进行展示该怎么办呢?别担心,UnconstrainedBox 组件可以接触多重限制,让限制类组件对当前子组件无效
首先我们先看看构造:
const UnconstrainedBox({
Key key,
Widget child, //接触限制的组件
this.textDirection,//文字方向
this.alignment = Alignment.center,//对齐方式默认为居中对齐
this.constrainedAxis,//约束的轴方向
}) : assert(alignment != null),
super(key: key, child: child);
从构造可以看出来这一类约束组件需要的属性都比较少,所以我们来举一个例子:
AppBar(
title: Text(title),
actions: <Widget>[
SizedBox(
width: 20, //限制宽度20
height: 20,//限制高度20
child: CircularProgressIndicator(//加载进度条
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(Colors.white70),
),
)
],
)
我们在appBar上挂载一个加载的进度条,但是我们可以看到SizedBox包裹了当前的进度条,也就是说我们当前的组件的宽高已经被固定了,即使我们修改了进度条也是无济于事,那么我们加入解除约束试下
AppBar(
title: Text(title),
actions: <Widget>[
UnconstrainedBox(//当前组件包裹的所有的组件以及子组件都会解除宽高的约束
child: SizedBox(//当前的宽高约束为20,但是已经失效
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(Colors.white70),
),
),
)
],
)
我们再次运行,会看到约束的宽高已经失效了,那么我们加入了UnconstrainedBox组件以后是不是真的就是完全解除了约束了呢?还是说我们看到的只是显示出来的效果,并不是真的解除了约束?这点特性让我们不禁想起了安卓view的显示和隐藏,是不是也会出现视觉上的解除约束的情况呢?答案是肯定的,如果这个时候我们限制的宽高比较高,即使加入了UnconstrainedBox,我们会发现当前组件的确显示出了自己的宽高,但是SizedBox限制的宽度高度依然占了那么大的区域,所以我们可以看出来,UnconstrainedBox也只是解除了当前组件的约束,但是对于包裹当前组件的父容器无能为力。那么有木有什么办法可以强制解除所有的约束呢?答案是否定的,不存在一个组件可以解除所有的布局约束并且能无视约束组件自身的宽高,所以博主建议UnconstrainedBox组件慎用,最好的实现方式就是在开发的时候就确定了当前组件的布局以及大小
DecoratedBox(装饰器组件)
我们在开发的过程中经常遇到一种需求,比如给当前的组件头部或者尾部添加一个小尾巴,或者添加一个slogo/背景等,所以在flutter中也提供了一个可以用来对组件进行修饰的组件--DecoratedBox ,用来满足此类需求
首先,我们先来看构造:
const DecoratedBox({
Decoration decoration,//用来创建具体绘制的实现以及画笔等,类型为Decoration抽象类的实现类,我们最常用的实现类是BoxDecoration类
DecorationPosition position = DecorationPosition.background,//装饰生效的位置,默认是在背景生效,可选值为DecorationPosition.background(widget之后绘制,即背景装饰)或者DecorationPosition.foreground(在widget之前绘制,即前景装饰)
Widget child //需要装饰的组件
})
BoxDecoration 是DecoratedBox 最常见的子类,我们一般也使用当前类来实现一部分装饰的效果,我们先来看一下构造:
BoxDecoration({
Color color, //颜色
DecorationImage image,//装饰的图片
BoxBorder border, //装饰的边框效果
BorderRadiusGeometry borderRadius, //装饰的圆角效果
List<BoxShadow> boxShadow, //阴影样式的集合
Gradient gradient, //渐变的效果样式
BlendMode backgroundBlendMode, //背景混合模式,类型为BlendMode
BoxShape shape = BoxShape.rectangle, //装饰绘制的形状,默认值为长方形
})
例如我们需要给一个按钮添加一个渐变的效果,案例实现如下:
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(colors:[Colors.red,Colors.orange[700]]), //背景渐变,颜色由红色渐变为700色域的橙色
borderRadius: BorderRadius.circular(3.0), //快速构造一个3px的圆角
boxShadow: [ //装饰的阴影策略
BoxShadow(
color:Colors.black54,
offset: Offset(2.0,2.0),
blurRadius: 4.0
)
]
),
child: Padding(padding: EdgeInsets.symmetric(horizontal: 80.0, vertical: 18.0),
child: Text("渐变色按钮", style: TextStyle(color: Colors.white),),
)
)
这样就可以实现一个由红->橙色过度的一个渐变的按钮效果了
Container(组合容器)
我们在flutter开发的过程中,经常看到Container 这个组件出现,我们也经常使用这个组件设置了很多属性,现在我们再回头看看,是不是会感觉很熟悉?咦,不知不觉间这些属性似乎我们都见过了?没错,Container 组件拥有DecoratedBox、ConstrainedBox、Padding、Align 等我们常见的包裹类容器的功能,而内部的众多属性,也对应了这些组件的特定属性,那么同样的,我们可以使用Container 组件实现同时拥有装饰,动画以及复杂布局限制的高级效果,这个组件也会是我们flutter开发过程中很重要的一个组件,很多时候我们都选择使用当前组件,接下来,我们先看Container 的构造:
Container({
this.alignment,//对齐方式
this.padding, //容器内补白,属于decoration的装饰管控范围
Color color, // 背景颜色
Decoration decoration, // 装饰器装饰组件,一般都是BoxDecoration
Decoration foregroundDecoration, //前景装饰器组件,可以实现同时装饰前景和其他背景
double width,//容器的宽度(等同ConstrainedBox组件限制的宽度)
double height, //容器的高度(等同ConstrainedBox组件限制的高度)
BoxConstraints constraints, //容器大小的限制条件
this.margin,//容器外补白,不受decoration的装饰范围管控
this.transform, //变换
this.child,
})
我们从构造中可以看出来,这些属性都很熟悉,我们在上面的组件中都见到过,但是color和decoration 这两个其实是互斥的,如果指定了color,其内部也会自动创建一个decoration ,并且width/height与constraints其实都是会限制宽高,如果两个都设置了,那么会先按照width/height优先,constraints会按照宽高生成一个对应的实例出现
Container的Padding和Margin
在Container中有两个属性可以指定内边距和外边距,实现留白的效果,接下来我们看看使用的方式以及区别:
Container(
margin: EdgeInsets.all(20.0), //容器外补白(外边距)
color: Colors.orange,
child: Text("Margin补白"),
),
Container(
padding: EdgeInsets.all(20.0), //容器内补白(内边距)
color: Colors.orange,
child: Text("Padding补白"),
),
运行起来的效果会发现padding设置的范围内,组件变大了,分别多了对应20px的空间,这个和Padding组件的效果表现无二,同样的这两个代码的使用方式与下面的代码是一样的效果:
Padding(
padding: EdgeInsets.all(20.0),//通过padding组件限制内边距补白
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.orange),
child: Text("Padding补白"),
),
),
DecoratedBox(
decoration: BoxDecoration(color: Colors.orange),
child: Padding(
padding: const EdgeInsets.all(20.0),//限制子组件的padding所在的位置设置补白
child: Text("Margin补白"),
),
),
至此,我们常用的功能类组件以及包裹容器组件介绍完毕,下一篇我们将会开始学习布局组件