resamplingEnabled导致的血案

2,191 阅读3分钟

问题

在将 Flutter SDK 升级到 2.0 之后,如果设备开启了 resamplingEnabled,会导致部分 Android 设备点击异常。

相关 issues

github.com/flutter/flu…

背景

触控采样率

resamplingEnabled 是 Flutter 1.22 提供的一个新 API,用来重新采集触控事件。

触摸事件的采集频率叫触控采样率,触控采样率越高,你滑动的时候会感觉越顺手。如果触控采样率低,你就会觉得不跟手。

除了触控采样率,还有个屏幕刷新率,顾名思义,屏幕刷新率是指电子束对屏幕上的图像重复扫描的次数。

这两个刷新率单位都是 HZ,表示 1s 采集的次数,现在市面上的屏幕刷新率,大部分在 60HZ,部分高端机型能达到 90HZ,甚至 120HZ,一般来说,触控采样率会是屏幕刷新率的倍数,例如屏幕刷新率是 60HZ,那么触控采样率 120HZ 是比较好的,不然,触控事件的消费会不均匀,最终导致 UI 产生抖动。

手机屏幕的刷新率和触控采样率有什么关系?且如何影响用户体验?

www.zhihu.com/question/32…

重新采样

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

最终解决

PR #81022 已合并🎉