Flutter - 利用貝塞爾曲線實現添加購物車效果

🥦 前言 - 關於貝塞爾曲線

貝塞爾曲線,這個詞你們都不陌生,特別是在前端裏面,沒用過相信也都聽過。前端

我這裏也繼續囉嗦一下,貝塞爾曲線的知識:git

貝塞爾曲線就是這樣的一條曲線,它是依據四個位置任意的點座標繪製出的一條光滑曲線。在歷史上,研究貝塞爾曲線的人最初是按照已知曲線參數方程來肯定四個點的思路設計出這種矢量曲線繪製法。貝塞爾曲線的有趣之處更在於它的「皮筋效應」,也就是說,隨着點有規律地移動,曲線將產生皮筋伸引同樣的變換,帶來視覺上的衝擊。1962年,法國數學家Pierre Bézier第一個研究了這種矢量繪製曲線的方法,並給出了詳細的計算公式,所以按照這樣的公式繪製出來的曲線就用他的姓氏來命名是爲貝塞爾曲線。 - 百度百科github

他的出現爲計算機矢量圖形學奠基了基礎,那麼咱們能用它作什麼?ide

有的人說了,網上一搜一大把,這是必須的,「波浪形狀」、「拋物線效果」等等等等。post

🍋 貝塞爾曲線的效果和計算公式

這裏我就不講各階貝塞爾曲線的區別了,直接把效果和公式貼上來。學習

一階貝塞爾曲線

二階貝塞爾曲線

三階貝塞爾曲線

剩下的還有高階,就很少贅述了,可推斷公式以下:動畫

🥝 實現商品添加購物車效果

複習了一下貝塞爾曲線的原理以後,咱們來看一下今天要實現的效果:ui

實現以前

在實現以前,咱們仍是先來理清一下思路,首先能確定的是咱們是要使用二階貝塞爾曲線來實現「拋物線效果」。this

二階貝塞爾曲線所須要的參數:spa

  1. 起始點 p0
  2. 控制點 p1
  3. 終點 p2

怎麼獲取座標點先不提,接着看圖,還有一個很重要的地方,就是根據拋物線一塊兒墜落的「小紅點」。

「小紅點」該如何顯示出來?咱們繼續。

開始實現

下面開始實現上圖效果,從哪入手?

1. 先來搞定起點和終點

頁面很簡單,代碼以下:

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 是很是昂貴的,須要謹慎使用。

​ - Vadaski - Flutter | 深刻淺出Key

若是這個時候有 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 來獲取該組件的位置便可。

這樣咱們起點和終點的座標都拿到了,那控制點呢?

2. 設置二階貝塞爾曲線的控制點

這就比較簡單了,咱們能夠看一下這個圖:

(手殘畫的,湊合看)

這個控制點咱們能夠本身隨意發揮,看你的效果如何再決定,我這裏是這樣的:

var x1 = widget.startPosition.dx - 250;
var y1 = widget.startPosition.dy - 100;
複製代碼

二階貝塞爾曲線所需的值都有了,下面就能夠算出位置了。

3. 獲取每一幀小紅點的位置

仍是先把這個圖和公式拿過來,其中 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();
}
複製代碼

這樣在動畫開始之後,就能夠獲取每一幀小紅點的位置了。

4. 把小紅點顯示出來

如何把小紅點給顯示出來?點擊的時候須要怎麼操做呢。

首先我想到的居然是 IndexStack 包裹住,點擊的時候設置小紅點的位置,而後把他顯示出來。

後來忽然想到了 Overlay 😂。

ListViewButton 的代碼以下:

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。

若有缺陷,但願你們提出,共同窗習!🤝

相關文章
相關標籤/搜索