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线上画线,此功能还在编写中。

相关文章
相关标签/搜索