贝塞尔曲线
,这个词你们都不陌生,特别是在前端里面,没用过相信也都听过。前端
我这里也继续啰嗦一下,贝塞尔曲线的知识:git
贝塞尔曲线就是这样的一条曲线,它是依据四个位置任意的点坐标绘制出的一条光滑曲线。在历史上,研究贝塞尔曲线的人最初是按照已知曲线参数方程来肯定四个点的思路设计出这种矢量曲线绘制法。贝塞尔曲线的有趣之处更在于它的“皮筋效应”,也就是说,随着点有规律地移动,曲线将产生皮筋伸引同样的变换,带来视觉上的冲击。1962年,法国数学家Pierre Bézier第一个研究了这种矢量绘制曲线的方法,并给出了详细的计算公式,所以按照这样的公式绘制出来的曲线就用他的姓氏来命名是为贝塞尔曲线。 - 百度百科github
他的出现为计算机矢量图形学奠基了基础,那么咱们能用它作什么?ide
有的人说了,网上一搜一大把,这是必须的,「波浪形状」、「抛物线效果」等等等等。post
这里我就不讲各阶贝塞尔曲线的区别了,直接把效果和公式贴上来。学习
剩下的还有高阶,就很少赘述了,可推断公式以下:动画
复习了一下贝塞尔曲线的原理以后,咱们来看一下今天要实现的效果:ui
在实现以前,咱们仍是先来理清一下思路,首先能确定的是咱们是要使用二阶贝塞尔曲线来实现「抛物线效果」。this
二阶贝塞尔曲线所须要的参数:spa
怎么获取坐标点先不提,接着看图,还有一个很重要的地方,就是根据抛物线一块儿坠落的「小红点」。
「小红点」该如何显示出来?咱们继续。
下面开始实现上图效果,从哪入手?
页面很简单,代码以下:
Column(
children: <Widget>[
Expanded(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Row(
// 隐藏无用代码
);
},
itemCount: 100,
),
),
Container(
height: 1,
color: Colors.grey.withOpacity(0.5),
),
Container(
height: 60,
color: Colors.white,
child: Row(
children: <Widget>[
Padding(
padding: EdgeInsets.only(left: 20),
child: Icon(
Icons.shop_two,
key: _key,
),
)
],
),
)
],
)
复制代码
一个Column
,上面是 ListView
,下面跟着一个 「购物车图标」。
起点是咱们 ListView
里面每个 item 的 + 号,终点就是左下角的「购物车图标」。
终点的坐标很好说,给定一个 GlobalKey
,而后在 第一帧回调 中获取位置便可:
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((c) {
// 获取「购物车」的位置
_endOffset = (_key.currentContext.findRenderObject() as RenderBox)
.localToGlobal(Offset.zero);
});
}
复制代码
那起点的呢?
由于起点是在 ListView
中,还会滚动,这时候可能不少小朋友就会说:“每个 icon 都给一个 GlobalKey 不就行了嘛!”
小朋友,你肯定不是在做死吗?
GlobalKey 使用了一个静态常量 Map 来保存它对应的 Element。 你能够经过 GlobalKey 找到持有该GlobalKey的 Widget,State 和 Element。 注意:GlobalKey 是很是昂贵的,须要谨慎使用。
若是这个时候有 10000 个商品列表,岂不是要爆炸?
咱们回看刚才获取「购物车」位置的代码,其实也就是用 GlobalKey
来获取 context
,用 context
来获取位置,那咱们何不直接用一个带 context
的组件?
代码以下:
Builder(
builder: (context) {
return IconButton(
icon: Icon(Icons.add_circle_outline),
onPressed: () {
// 经过 Builder 组件来获取 context
RenderBox box = context.findRenderObject();
var offset = box.localToGlobal(Offset.zero);
},
);
},
)
复制代码
直接使用 Builder
来获取该组件的位置便可。
这样咱们起点和终点的坐标都拿到了,那控制点呢?
这就比较简单了,咱们能够看一下这个图:
(手残画的,凑合看)
这个控制点咱们能够本身随意发挥,看你的效果如何再决定,我这里是这样的:
var x1 = widget.startPosition.dx - 250;
var y1 = widget.startPosition.dy - 100;
复制代码
二阶贝塞尔曲线所需的值都有了,下面就能够算出位置了。
仍是先把这个图和公式拿过来,其中 P0(起点),P1(控制点),P2(终点)值咱们都有了,那还有个 t,咱们使用 Flutter 的 Tween
来获取就行了,最后套入公式:
@override
void initState() {
super.initState();
_controller =
AnimationController(duration: Duration(milliseconds: 800), vsync: this);
_animation = Tween(begin: 0.0, end: 1.0).animate(_controller);
// 二阶贝塞尔曲线用值
var x0 = widget.startPosition.dx;
var y0 = widget.startPosition.dy;
var x1 = widget.startPosition.dx - 250;
var y1 = widget.startPosition.dy - 100;
var x2 = widget.endPosition.dx;
var y2 = widget.endPosition.dy;
_animation.addListener(() {
// t 动态变化的值
var t = _animation.value;
if (mounted)
setState(() {
left = pow(1 - t, 2) * x0 + 2 * t * (1 - t) * x1 + pow(t, 2) * x2;
top = pow(1 - t, 2) * y0 + 2 * t * (1 - t) * y1 + pow(t, 2) * y2;
});
});
// 初始化小圆点的位置
left = widget.startPosition.dx;
top = widget.startPosition.dy;
// 显示小圆点的时候动画就开始
_controller.forward();
}
复制代码
这样在动画开始之后,就能够获取每一帧小红点的位置了。
如何把小红点给显示出来?点击的时候须要怎么操做呢。
首先我想到的居然是 IndexStack
包裹住,点击的时候设置小红点的位置,而后把他显示出来。
后来忽然想到了 Overlay
😂。
ListView
中 Button
的代码以下:
IconButton(
icon: Icon(Icons.add_circle_outline),
onPressed: () {
// 点击的时候获取当前 widget 的位置,传入 overlayEntry
var _overlayEntry = OverlayEntry(builder: (_) {
RenderBox box = context.findRenderObject();
var offset = box.localToGlobal(Offset.zero);
return RedDotPage(
startPosition: offset,
endPosition: _endOffset,
);
});
// 显示Overlay
Overlay.of(context).insert(_overlayEntry);
// 等待动画结束
Future.delayed(Duration(milliseconds: 800), () {
_overlayEntry.remove();
_overlayEntry = null;
});
},
)
复制代码
其中 RedDotPage
就是咱们定义好的小红点页面,给他传入起始点,让 Overlay
显示出来,显示出来的同时就开始作贝塞尔曲线动画了,等到动画结束 remove 掉这个 OverlayEntry
就ok了。
这就是用 Flutter 实现添加购物车的全部内容,仍是有一些细节在里面的。
代码已经提交到了 Github - 添加购物车Demo。
若有缺陷,但愿你们提出,共同窗习!🤝