问题
在将 Flutter SDK 升级到 2.0 之后,如果设备开启了 resamplingEnabled,会导致部分 Android 设备点击异常。
相关 issues
背景
触控采样率
resamplingEnabled 是 Flutter 1.22 提供的一个新 API,用来重新采集触控事件。
触摸事件的采集频率叫触控采样率,触控采样率越高,你滑动的时候会感觉越顺手。如果触控采样率低,你就会觉得不跟手。
除了触控采样率,还有个屏幕刷新率,顾名思义,屏幕刷新率是指电子束对屏幕上的图像重复扫描的次数。
这两个刷新率单位都是 HZ,表示 1s 采集的次数,现在市面上的屏幕刷新率,大部分在 60HZ,部分高端机型能达到 90HZ,甚至 120HZ,一般来说,触控采样率会是屏幕刷新率的倍数,例如屏幕刷新率是 60HZ,那么触控采样率 120HZ 是比较好的,不然,触控事件的消费会不均匀,最终导致 UI 产生抖动。
手机屏幕的刷新率和触控采样率有什么关系?且如何影响用户体验?
重新采样
当 resamplingEnabled 设置为 true 时,Flutter 在处理触控事件时,不会立即处理,而是会添加到待处理的队列中。
// The sampling interval.
//
// Sampling interval is used to determine the approximate time for subsequent
// sampling. This is used to decide if early processing of up and removed events
// is appropriate. 16667 us for 60hz sampling interval.
const Duration _samplingInterval = Duration(microseconds: 16667);
void handlePointerEvent(PointerEvent event) {
assert(!locked);
if (resamplingEnabled) {
// 如果开启了重采样
_resampler.addOrDispatch(event);
_resampler.sample(samplingOffset, _samplingInterval);
return;
}
// Stop resampler if resampling is not enabled. This is a no-op if
// resampling was never enabled.
_resampler.stop();
_handlePointerEventImmediately(event);
}
复制代码
sample
方法会按 60HZ 的频率重新分发事件,这样虽然解决了UI 抖动问题,却会带来屏幕不跟手的感觉,Google 后面应该会重新优化这个方案。
在 2020.10.03 时,master 分支合并了 #67080 这个 PR,这个 PR 修复了 down,up 事件的重新采样处理。
// Add synthetics `move` or `hover` event if position has changed.
// Note: Devices without `hover` events are expected to always have
// `add` and `down` events with the same position and this logic will
// therefor never produce `hover` events.
if (position != _position) {
final Offset delta = position - _position;
callback(_toMoveOrHoverEvent(event, position, delta, _pointerIdentifier, sampleTime, wasDown));
_position = position;
}
复制代码
在重新采样时,会将 PointerDownEvent 和 PointerUpEvent 等事件,重新转换成 PointerMoveEvent 或 PointerHoverEvent。
这里我们主要看下 PointerMoveEvent 的转换处理。
PointerEvent _toMoveEvent(
PointerEvent event,
Offset position,
Offset delta,
int pointerIdentifier,
Duration timeStamp,
) {
return PointerMoveEvent(
timeStamp: timeStamp,
pointer: pointerIdentifier,
kind: event.kind,
device: event.device,
position: position,
delta: delta,
buttons: event.buttons,
obscured: event.obscured,
pressure: event.pressure,
pressureMin: event.pressureMin,
pressureMax: event.pressureMax,
distanceMax: event.distanceMax,
size: event.size,
radiusMajor: event.radiusMajor,
radiusMinor: event.radiusMinor,
radiusMin: event.radiusMin,
radiusMax: event.radiusMax,
orientation: event.orientation,
tilt: event.tilt,
platformData: event.platformData,
synthesized: event.synthesized,
embedderId: event.embedderId,
);
}
复制代码
转换逻辑很简单,就是将各个字段原封不动赋值给新的 PointerMoveEvent,问题就出在这个 “buttons” 字段。
“buttons” 字段是用来表示触控事件类型,它和 “kind” 字段搭配表示触控事件,例如,”kind” 为 touch,”buttons” 为 kPrimaryButton,这表示基于触摸产生的主事件。
PointerMoveEvent 和 PointerDownEvent 的 “buttons” 字段默认会是 kPrimaryButton,而 PointerUpEvent 默认是 0,所以在将 PointerUpEvent 转换成 PointerMoveEvent 后,会导致 PointerMoveEvent 的 “buttons” 默认值丢失。
问题根源
Flutter 单击事件的触发处理是在 BaseTapGestureRecognizer.handlePrimaryPointer
中。
@override
void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent) {
// 触发单击事件
_up = event;
_checkUp();
} else if (event is PointerCancelEvent) {
resolve(GestureDisposition.rejected);
if (_sentTapDown) {
_checkCancel(event, '');
}
_reset();
} else if (event.buttons != _down!.buttons) {
// 问题根源
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer!);
}
}
复制代码
当接收到 PointerMoveEvent 时,会判断 “buttons” 字段是否和 PointerDownEvent 事件的 “buttons” 是否一致,当有不同类型的触摸事件进来,这次的单击处理流程就会被取消。
而 PointerUpEvent 转换过来的 PointerMoveEvent 事件,”buttons” 会被设置为 0,而不是默认的 kPrimaryButton,所以最终会导致点击异常。
解决方案
临时解决
如果不能回退 SDK 版本的话,可以暂时关闭 resamplingEnabled。
最终解决
在转换 PointerUpEvent 时,增加默认的 “buttons” 值为 kPrimaryButton。
这个解决方案已提交 PR #81022 等待合并。