Flutter小小實踐——KLine 繪製篇(三)

前言

上篇已經完成了K線的基礎繪製工做。可是還有不少的工做須要完善
bash

今天來聊聊手勢的處理。

k線的手勢還真是一個K線開發的一個難點之一,筆者也是用了很多精力才處理完成。app

主要有如下手3種勢須要處理less

  • 水平滑動
  • 縮放
  • 長按

有的同窗可能對手勢還不是很清楚,所以在這裏仍是對上面的三種手勢分別來聊一聊。ide

不得不說,目前不少的金融軟件裏的K線體驗是很糟糕的,滑動,縮放抖動的的很厲害,一點也不平滑,體驗真的是還須要提高啊。
函數

這裏就奔着達到天然絲滑滑動,縮放的目標處理手勢

先看看效果




效果解析

效果圖是用在線的視頻轉的gif圖,掉幀比較嚴重,實際效果是很流暢的,感興趣的能夠在文章末尾下載體驗優化

k線首次展現時是滑動到最後的,異常屏幕上的endIndex就是數據的最後一條數據的索引, startIndex是endIndex減去動畫

手勢Widget

flutter 已經提供了一些很方便的手勢處理類,筆者使用的就是flutter提供的GestureDetector類來處理回調的,簡單看看,能夠發現GestureDetector中包裝了RawGestureDetector,RawGestureDetector中使用的是Listener來監聽觸摸事件的。
若是須要自定義手勢的話可使用Listener來處理個性化的識別
筆者使用這裏須要的是滑動識別,縮放識別,長按識別,因此使用GestureDetector就能夠了,GestureDetector中這些手勢都已是現成的了,很是的方便
Flutter的基礎模塊仍是挺齊全的.
ui

手勢的回調函數監聽代碼以下。this

Widget _wrapperGesture(Widget widget) {
    return GestureDetector(
        onTapDown: _controllerModel.onTapDownGesture,
        onTapUp: _controllerModel.onTapUpGesture,
        onTapCancel: _controllerModel.onTapCancelGesture,
        onHorizontalDragStart: _controllerModel.onHorizontalDragStartGesture,
        onHorizontalDragDown: _controllerModel.onHorizontalDragDownGesture,
        onHorizontalDragUpdate: _controllerModel.onHorizontalDragUpdateGesture,
        onHorizontalDragEnd: _controllerModel.onHorizontalDragEndGesture,
        onLongPress: _controllerModel.onLongPressGesture,
        onLongPressStart: _controllerModel.onLongPressStartGesture,
        onLongPressMoveUpdate: _controllerModel.onLongPressMoveUpdateGesture,
        onLongPressUp: _controllerModel.onLongPressUpGesture,
        onLongPressEnd: _controllerModel.onLongPressEndGesture,
        onScaleStart: _controllerModel.onScaleStartGesture,
        onScaleUpdate: _controllerModel.onScaleUpdateGesture,
        onScaleEnd: _controllerModel.onScaleEndGesture,
        child: widget);
  }
複製代碼

水平滑動

K線的水平滑動和scrollview是相似的
水平滑動,咱們能夠想象成是電影底片,很長的膠片,不停的轉動,而後在鏡頭處發光,把圖像射到幕布上,電影的每一張圖像就是這裏的每一幀畫面,膠片的長度就是K線裏可滑動的長度。spa



有個這些基本的想法,就能夠開始着手滑動事件的處理了。
/// 水平滾動執行流程
/// 一、_onHorizontalDragStart
/// 二、_onHorizontalDragDown
/// 三、_onHorizontalDragUpdate
/// 四、_onHorizontalDragEnd
複製代碼

具體的處理方法以下

/// 設置當前的k線是滑動操做
 void onHorizontalDragStartGesture(DragStartDetails details) {
    klineOp = KlineOp.Scroll;
  }

  /// 暫時沒處理
  void onHorizontalDragDownGesture(DragDownDetails details) {}
  /// 手勢的更新,有人就是move事件觸發就用執行這個函數
  void onHorizontalDragUpdateGesture(DragUpdateDetails details) {
    _addHorizontalOffset(details.delta);
  }
  /// 當水平滑動結束時
  void onHorizontalDragEndGesture(DragEndDetails details) {
    klineOp = KlineOp.None;
    ...
  }
  /// 滑動時更新k線的scrollOffset,默認是0也就是在最右側時
  /// 這個須要注意的是咱們經常使用的ScrollView的在最左邊時offset是0,
  /// 這是K線和ScrollView的一個小小區別吧,固然K線也能夠把最scrollOffset爲0是是最左邊,不過再轉換一次就行了
  void _addHorizontalOffset(Offset offset) {
    /// 當前的偏移量加上兩個touchEvent的offset值,
    scrollOffset += offset;
    /// 計算新的k線開始index和結束index
    /// 也就是定義電影膠片播放的位置
    /// 若是發生了變化就會通知更新,同步個UI的render去繪製新的畫面
    var change = _computeIndex();
    if (change) {
      notifyListeners();
    }
  }
複製代碼

初次以外,咱們在使用ScrollView的時候,ScrollView在迅速滑動時(fling),scrollview在手指離開了屏幕任然會向前滑動一段距離。

咱們來想先如何來實現這個功能呢?
在初中物理裏面學過經典的牛頓力學,還記得公式嗎?
好比:

  • F=ma 好比重力G=mg, 質量*重力加速度9.8
  • S=V0t+1/2at^2 在t時間類運動的距離

在onHorizontalDragEndGesture時,系統會給咱們一個結束時的速度. 模擬正是環境的話,速度應該越來也小。直到停下來,多是天然停下來的,也多是撞牆停下來。

使用Tween動畫來過分Fling中Offset的變化,從而達到一個平滑的拋出效果

var velocity = details.velocity;

   // 加速度 t=(v1-v0)/a,這個值是嘗試屢次後,感受效果還能夠。
   var a = 200;
   // 須要滑動的時間, 先轉成dp的速度
   var t = velocity.pixelsPerSecond.dx / devicePixelRatio / a;

   var normalVelocity = velocity.pixelsPerSecond.dx / screenWidth;
   bool flingToLeft = normalVelocity < 0;

   double maxOffsetX = getMaxOffsetX();
   double offsetX = getScrollOffsetX();

   // S = VoT+0.5*a*t^2
   double predictDelta = 0.5 * a * t * t;
   double begin = offsetX;
   double end = flingToLeft ? max(0, offsetX - predictDelta) : min(maxOffsetX, offsetX + predictDelta);

   _scrollAnimation = scrollController.drive(Tween<double>(begin: begin, end: end));
   _scrollAnimation.removeListener(_handleMoveListener);
   _scrollAnimation.addListener(_handleMoveListener);
   scrollController.reset();
   scrollController.duration = Duration(milliseconds: (t * 1000).toInt());
   scrollController.fling(velocity: normalVelocity.abs());

複製代碼

縮放動畫

縮放時,其實就是縮放每一條蠟燭的寬度,處理好一條蠟燭圖的寬度就相對也所有都處理好了。

這個須要主力幾點:

  • 縮放是平滑的
  • 縮放的中心點對應的k線,應該一直都是這一條
  • 縮放有縮放的最大值和最小值,不能無限縮放
/// 開始縮放時標記當前操做類型
  /// 保存縮放前的蠟燭寬度
  /// _orgCandleWidthScaleGap 待會載說
  void onScaleStartGesture(ScaleStartDetails details) {
    klineOp = KlineOp.Scale;
    _orgCandleWidthBeforeScale = candleWidth;
    _orgCandleW
    idthScaleGap = null;
  }
  /// 縮放結束
  void onScaleEndGesture(ScaleEndDetails details) {
    klineOp = KlineOp.None;
    _orgCandleWidthBeforeScale = null;
    _orgCandleWidthScaleGap = null;
  }
  /// 縮放更新
  void onScaleUpdateGesture(ScaleUpdateDetails details) {
    /// 第一次更新是給_orgCandleWidthScaleGap賦值
    /// 爲啥要在第一次賦值?
    /// details.horizontalScale的值相對於兩指開始距離的倍數。
    /// 因爲識別縮放是有一個閾值了,必須兩個手指move的距離超過閾值才能觸發縮放
    /// 因此在第一次觸發時,horizontalScale的值會離1比較遠,
    /// 這時若是原始的horizontalScale就是忽然抖動一下
    /// 並且這個閾值可能致使縮放的識別比較慢,而誤識別成別的手勢。
    _orgCandleWidthScaleGap ??= 1 - details.horizontalScale;
    details.localFocalPoint.dx;
    double horizontalScale = details.horizontalScale + _orgCandleWidthScaleGap;
    double expectCandleWidth = _orgCandleWidthBeforeScale * horizontalScale;
    double expectDisplayCount = boxWidth / expectCandleWidth;
    if (expectDisplayCount < configModel.kLineConfig.minDisplayCount) {
      return;
    }
    if (expectDisplayCount > configModel.kLineConfig.maxDisplayCount) {
      return;
    }

    if (expectDisplayCount > configModel.kLineConfig.maxDisplayCount) {
      // 修正縮小的邊界
      expectCandleWidth = boxWidth / configModel.kLineConfig.maxDisplayCount;
    } else if (expectDisplayCount < configModel.kLineConfig.minDisplayCount) {
      // 修正放大的邊界
      expectCandleWidth = boxWidth / configModel.kLineConfig.minDisplayCount;
    }
    // 縮放的中心點
    double focalX = details.focalPoint.dx;
    _reCalcScaleAxis(focalX, expectCandleWidth);
  }

複製代碼

scale閾值較大可能致使縮放的識別比較慢,而誤識別成別的手勢的問題?

方案一:修改源碼

閱讀ScaleGestureRecognizer源碼

void _advanceStateMachine(bool shouldStartIfAccepted) {
    if (_state == _ScaleState.ready)
      _state = _ScaleState.possible;

    if (_state == _ScaleState.possible) {
      final double spanDelta = (_currentSpan - _initialSpan).abs();
      final double focalPointDelta = (_currentFocalPoint - _initialFocalPoint).distance;
      /// 這裏就是縮放觸發的閾值kScaleSlop,kPanSlop
      /// 經過源碼查看kScaleSlop是個常量, 修改這個常量值。
      if (spanDelta > kScaleSlop || focalPointDelta > kPanSlop)
        resolve(GestureDisposition.accepted);
    } else if (_state.index >= _ScaleState.accepted.index) {
      resolve(GestureDisposition.accepted);
    }

    if (_state == _ScaleState.accepted && shouldStartIfAccepted) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
    }

    if (_state == _ScaleState.started && onUpdate != null)
      invokeCallback<void>('onUpdate', () {
        onUpdate(ScaleUpdateDetails(
          scale: _scaleFactor,
          horizontalScale: _horizontalScaleFactor,
          verticalScale: _verticalScaleFactor,
          focalPoint: _currentFocalPoint,
          localFocalPoint: PointerEvent.transformPosition(_lastTransform, _currentFocalPoint),
          rotation: _computeRotationFactor(),
        ));
      });
  }

複製代碼
/// The distance a touch has to travel for the framework to be confident that
/// the gesture is a scroll gesture, or, inversely, the maximum distance that a
/// touch can travel before the framework becomes confident that it is not a
/// tap.
///
/// A total delta less than or equal to [kTouchSlop] is not considered to be a
/// drag, whereas if the delta is greater than [kTouchSlop] it is considered to
/// be a drag.
// This value was empirically derived. We started at 8.0 and increased it to
// 18.0 after getting complaints that it was too difficult to hit targets.
const double kTouchSlop = 18.0; // Logical pixels

/// The distance a touch has to travel for the framework to be confident that
/// the gesture is a paging gesture. (Currently not used, because paging uses a
/// regular drag gesture, which uses kTouchSlop.)
// TODO(ianh): Create variants of HorizontalDragGestureRecognizer et al for
// paging, which use this constant.
const double kPagingTouchSlop = kTouchSlop * 2.0; // Logical pixels

/// The distance a touch has to travel for the framework to be confident that
/// the gesture is a panning gesture.
const double kPanSlop = kTouchSlop * 2.0; // Logical pixels

/// The distance a touch has to travel for the framework to be confident that
/// the gesture is a scale gesture.
const double kScaleSlop = kTouchSlop;
複製代碼

方案二:

因爲方案一須要修源碼,別的f同窗的Flutter SDK也得修改纔可,因此,想到的最快方法就是複製一份手勢識別的代碼到項目中而後修改相應的常量便可。

方案三:

直接是Listener來自定義手勢,這樣對縮放特殊處理,好比當有兩個手指時,就觸犯scale,而不是須要滑動一段距離才觸發,這樣就可針對K線的場景特殊優化縮放了。

長按的手勢處理

長按相比滑動和縮放要簡單不少了,

長按時計算好對應的長按K線數據的pressedIndex就行了,而後通知能夠處理長按更新的render去處理就行了。

且看代碼

void onLongPressStartGesture(LongPressStartDetails details) {
    klineOp = KlineOp.LongPress;
    _handleLongPress(details.localPosition.dx);
  }

  void onLongPressMoveUpdateGesture(LongPressMoveUpdateDetails details) {
    _handleLongPress(details.localPosition.dx);
  }

  void _handleLongPress(double x) {
    int pressedIndex = getIndexByX(x).toInt();
    if (this.pressedIndex != pressedIndex) {
      this.pressedIndex = pressedIndex;
    }
    hightLightModel.notify();
  }

  void onLongPressUpGesture() {}

  void onLongPressEndGesture(LongPressEndDetails details) {
    klineOp = KlineOp.None;
    pressedIndex = INVALID;
    hightLightModel.notify();
  }

複製代碼

總結

優勢:手勢處理這塊筆者仍是比較滿意的,相對同類產品還算就很流暢的了。 不足之處: 縮放的識別還得重構一下,由於系統的縮放識別閾值太大了,有時不能觸發縮放。

想下載體驗的歡迎下載

連接: https://pan.baidu.com/s/15JZToKwuELN2RoemNEws1A 
提取碼: trej 複製這段內容後打開百度網盤手機App,操做更方便哦複製代碼

下節聊聊在K線上畫線,此功能還在編寫中。

相關文章
相關標籤/搜索