上篇已经完成了K线的基础绘制工做。可是还有不少的工做须要完善
bash
k线的手势还真是一个K线开发的一个难点之一,笔者也是用了很多精力才处理完成。app
主要有如下手3种势须要处理less
有的同窗可能对手势还不是很清楚,所以在这里仍是对上面的三种手势分别来聊一聊。ide
不得不说,目前不少的金融软件里的K线体验是很糟糕的,滑动,缩放抖动的的很厉害,一点也不平滑,体验真的是还须要提高啊。
函数
效果图是用在线的视频转的gif图,掉帧比较严重,实际效果是很流畅的,感兴趣的能够在文章末尾下载体验优化
k线首次展现时是滑动到最后的,异常屏幕上的endIndex就是数据的最后一条数据的索引, startIndex是endIndex减去动画
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在手指离开了屏幕任然会向前滑动一段距离。
咱们来想先如何来实现这个功能呢?
在初中物理里面学过经典的牛顿力学,还记得公式吗?
好比:
使用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());
复制代码
缩放时,其实就是缩放每一条蜡烛的宽度,处理好一条蜡烛图的宽度就相对也所有都处理好了。
这个须要主力几点:
/// 开始缩放时标记当前操做类型
/// 保存缩放前的蜡烛宽度
/// _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);
}
复制代码
阅读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线上画线,此功能还在编写中。