天天看點

Flutter 113: 圖解自定義 ACEPieWidget 餅狀圖 (二)

    小菜上一節嘗試繪制了一個簡單的餅狀圖,今天嘗試添加一點手勢操作,可以随手指旋轉餅狀圖;

ACEPieWidget

Gesture

    小菜在之前繪制好的餅狀圖基礎上添加一個簡單的旋轉手勢操作;

Flutter 113: 圖解自定義 ACEPieWidget 餅狀圖 (二)

1. 手勢範圍

    小菜習慣重寫 PanGestureRecognizer 來對手勢操作進行監聽,也可以直接通過 Gesture 來直接處理;

return Container(
    width: double.infinity,
    height: double.infinity,
    child: RawGestureDetector(
        child: CustomPaint(
            key: _key,
            painter: PiePainter(widget.listData, this.rotateAngle)),
        gestures: <Type, GestureRecognizerFactory>{
          ACEPieGestureRecognizer:
              GestureRecognizerFactoryWithHandlers<ACEPieGestureRecognizer>(
                  () => ACEPieGestureRecognizer(), (ACEPieGestureRecognizer gesture) {
            gesture.onDown = (detail) {
              
            };
            gesture.onUpdate = (detail) {
              
            };
            gesture.onEnd = (detail) {
             
            };
          })
        }));           

2. 計算旋轉角度

    小菜預計的想法是,通過 gesture.onUpdate 更新手勢坐标,與初始坐标差來定位旋轉角度;其中餅狀圖繪制是采用的笛卡爾坐标系,以左上角為坐标系原點;而居中的餅狀圖圓心是在整個元件所在的螢幕尺寸中心;

RenderBox box = _key.currentContext.findRenderObject();
Offset offset = box.localToGlobal(Offset.zero);
Offset _centerOffset = Offset(offset.dx + box.size.width * 0.5, offset.dy + box.size.height * 0.5);           

    小菜采用通用 RenderBox 的方式擷取自定義 ACEPieWidget 所占螢幕尺寸并擷取餅狀圖圓心坐标;

Flutter 113: 圖解自定義 ACEPieWidget 餅狀圖 (二)

    其中需要注意的是手勢監聽的 Offset details 擷取坐标方式略有不同:detail.localPosition 擷取的是目前組建内相對于左上角坐标原點的相對位置,而 detail.globalPosition 擷取的是整個裝置螢幕左上角坐标的實際位置,小菜剛開始通過 localPosition 方式擷取,計算得出的角度受 Widget 所占位置及尺寸影響,差别較大,建議使用 globalPosition 方式;

Flutter 113: 圖解自定義 ACEPieWidget 餅狀圖 (二)

    通過 gesture.onUpdate 更新後的坐标點與更新前的坐标點,再結合餅狀圖圓心坐标,三點确定一個三角形,通過餘弦定律擷取手勢操作的夾角,進而重新繪制餅狀圖;

_rotateAngle() {
  var _onDownLen = sqrt(pow(_startOffset.dx - _centerOffset.dx, 2) +
      pow(_startOffset.dy - _centerOffset.dy, 2));
  var _onUpdateLen = sqrt(pow(_updateOffset.dx - _centerOffset.dx, 2) +
      pow(_updateOffset.dy - _centerOffset.dy, 2));
  var _downToUpdateLen = sqrt(pow((_startOffset.dx - _updateOffset.dx), 2) +
      pow((_startOffset.dy - _updateOffset.dy), 2));
  var _cosAngle = (_onDownLen * _onDownLen + _onUpdateLen * _onUpdateLen -
          _downToUpdateLen * _downToUpdateLen) / (2 * _onDownLen * _onUpdateLen);
  rotateAngle += acos(_cosAngle);
  setState(() {});
}           
Flutter 113: 圖解自定義 ACEPieWidget 餅狀圖 (二)

3. 旋轉方向

    小菜通過上述方式擷取三角形角度後發現旋轉的方向隻能是順時針旋轉,反向的逆時針手勢缺未生效;其原因是通過餘弦定律轉換的角度都為正數,需要通過向量方式進行方向正負的判斷;于是小菜更換了另一種方式,以餅狀圖圓心為坐标軸原點,水準向右設定一個機關向量,再通過前後手勢變更的坐标進行計算兩個角度,相差即是夾角;

Flutter 113: 圖解自定義 ACEPieWidget 餅狀圖 (二)
_rotateAngle() {
  if (_startOffset.dy < _centerOffset.dy) {
    gestureDirection = -1;
  } else {
    gestureDirection = 1;
  }
  var _updateAngle = gestureDirection *
      _angle(_updateOffset, Offset(_centerOffset.dx + 100, _centerOffset.dy), _centerOffset);
  if (_updateOffset.dy < _centerOffset.dy) {
    gestureDirection = -1;
  } else {
    gestureDirection = 1;
  }
  var _startAngle = gestureDirection *
      _angle(_startOffset, Offset(_centerOffset.dx + 100, _centerOffset.dy), _centerOffset);
  return (_updateAngle - _startAngle);
}

_angle(_aPoint, _bPoint, _oPoint) {
  var _oALen = sqrt(pow(_aPoint.dx - _oPoint.dx, 2) + pow(_aPoint.dy - _oPoint.dy, 2));
  var _oBLen = sqrt(pow(_bPoint.dx - _oPoint.dx, 2) + pow(_bPoint.dy - _oPoint.dy, 2));
  var _aBLen = sqrt(pow(_aPoint.dx - _bPoint.dx, 2) + pow(_aPoint.dy - _bPoint.dy, 2));
  var _cosAngle = (pow(_oALen, 2) + pow(_oBLen, 2) - pow(_aBLen, 2)) /
      (2 * _oALen * _oBLen);
  return acos(_cosAngle);
}           

    其中在計算的時候用到一些基本的數學函數公式,之後小菜會簡單介紹一下 dart:math 函數庫;計算所得的角度加在餅狀圖周遊繪制的扇形圖角度中即可;其中注意在文字繪制時也要注意旋轉坐标系角度;

if (_listData != null) {
  for (int i = 0; i < _listData.length; i++) {
    startAngle += sweepAngle;
    sweepAngle = _listData[i].values.first * 2 * pi / _sum;
    canvas.drawArc(_circle, startAngle + _rotateAngle, sweepAngle, true,
        _paint..color = _subPaint(_listData[i].keys.first));
    if (sweepAngle >= pi / 6) {
      canvas.translate(size.width * 0.5, size.height * 0.5);
      canvas.rotate(startAngle + sweepAngle * 0.5 + _rotateAngle);
      Paragraph paragraph = (_pb..addText(_subName)).build()..layout(_paragraph);
      canvas.drawParagraph(paragraph, Offset(50.0, 0.0 - paragraph.height * 0.5));
      canvas.rotate(-startAngle - sweepAngle * 0.5 - _rotateAngle);
      canvas.translate(-size.width * 0.5, -size.height * 0.5);
    }
  }
}           
Flutter 113: 圖解自定義 ACEPieWidget 餅狀圖 (二)

dart:math

    小菜在繪制餅狀圖過程中需要使用三角函數等進行偏移量繪制,此時需要一些基礎的數學計算;而 Dart 也有簡單的 dart:math 庫,主要用來數學常數和函數使用,以及随機數生成器等;

1. 常量資料

    dart:math 提供了我們日常用的自然數底數 e、對數 ln 以及圓周率 pi 等,精确了很多位,避免我們自己定義;

// 自然對數的底數 e
'e -> $e';
// 以 e 為底 10 的對數
'ln10 -> $ln10';
// 以 e 為底 2 的對數
'ln2 -> $ln2';
// 以 2 為底 e 的對數
'log2e -> $log2e';
// 以 10 為底 e 的對數
'log10e -> $log10e';
// 圓周率
'pi -> $pi';
// 2 的平方根
'sqrt2 -> $sqrt2';
// 1/2 的平方根
'sqrt2 -> $sqrt2';           

2. 倍數/指數函數

    dart:math 提供了平方根,求幂,指數函數等便利的函數方法;

// 平方根
double sqrt(num x);
// 自然指數 e 的 x 次幂
double exp(num x);
// 自然數 x 的對數
double log(num x);
// 最小值比較
T min<T extends num>(T a, T b);
// 最大值比較
T max<T extends num>(T a, T b);
// x 的 y 次幂
num pow(num x, num exponent);           

3. 三角函數

    對于三角函數,提供了弧度轉為角度的正弦/餘弦/正切函數,同樣提供了由角度值轉為弧度值轉換方法,需要注意例如負數、0、無窮數、無理數等特殊場景;

// 正弦函數
double sin(num radians);
// 餘弦函數
double cos(num radians);
// 正切函數
double tan(num radians);
// 弧度轉為正弦值
double asin(num x);
// 弧度轉為餘弦值
double acos(num x);
// 弧度轉為正切值
double atan(num x);           
Flutter 113: 圖解自定義 ACEPieWidget 餅狀圖 (二)
ACEPieWidget 案例源碼 dart:math 案例源碼
來源: 阿策小和尚