前言
上篇已经完成了K线的基础绘制工作。但是还有很多的工作需要完善
今天来聊聊手势的处理。
k线的手势还真是一个K线开发的一个难点之一,笔者也是用了不少精力才处理完成。
主要有以下手3种势需要处理
- 水平滑动
- 缩放
- 长按
有的同学可能对手势还不是很清楚,因此在这里还是对上面的三种手势分别来聊一聊。
不得不说,目前很多的金融软件里的K线体验是很糟糕的,滑动,缩放抖动的的很厉害,一点也不平滑,体验真的是还需要提升啊。
这里就奔着达到自然丝滑滑动,缩放的目标处理手势
先看看效果
效果解析
效果图是用在线的视频转的gif图,掉帧比较严重,实际效果是很流畅的,感兴趣的可以在文章末尾下载体验
k线首次展示时是滑动到最后的,屏幕上的endIndex就是数据的最后一条数据的索引, startIndex是endIndex减去显示条数
手势Widget
flutter 已经提供了一些很方便的手势处理类,笔者使用的就是flutter提供的GestureDetector类来处理回调的,简单看看,可以发现GestureDetector中包装了RawGestureDetector,RawGestureDetector中使用的是Listener来监听触摸事件的。
如果需要自定义手势的话可以使用Listener来处理个性化的识别
笔者使用这里需要的是滑动识别,缩放识别,长按识别,所以使用GestureDetector就可以了,GestureDetector中这些手势都已经是现成的了,非常的方便
Flutter的基础模块还是挺齐全的.
手势的回调函数监听代码如下。
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线里可滑动的长度。
有个这些基本的想法,就可以开始着手滑动事件的处理了。
/// 水平滚动执行流程
/// 1、_onHorizontalDragStart
/// 2、_onHorizontalDragDown
/// 3、_onHorizontalDragUpdate
/// 4、_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线上画线,此功能还在编写中。