千家信息网

Flutter用户侧问题怎么解决

发表于:2025-01-29 作者:千家信息网编辑
千家信息网最后更新 2025年01月29日,这篇"Flutter用户侧问题怎么解决"文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇"
千家信息网最后更新 2025年01月29日Flutter用户侧问题怎么解决

这篇"Flutter用户侧问题怎么解决"文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇"Flutter用户侧问题怎么解决"文章吧。

背景

现在的app基本都会提供用户反馈问题的入口,然而提供给用户反馈问题一般有两种方式:

  • 直接用文字输入表达,或者截图

  • 直接录制视频反馈

这两种反馈方式常常带来以下抱怨:

  • 用户:输入文字好费时费力

  • 开发1:看不懂用户反馈说的是什么意思?

  • 开发2:大概看懂用户说的是什么意思了,但是我线下没办法复现哈

  • 开发3:看了用户录制的视频,但是我线下没办法重现,也定位不到问题

所以:为了解决以上问题,我们用一套全新的思路来设计线上问题回放体系

Flutter 手势基础知识

如果要录制和回放flutter ui事件,那么我们首先必须了解flutter ui手势基本原理。

1. Flutter UI触摸原始数据Pointer

我们可以把Flutter中的手势系统分两层概念来理解。第一层概念为原始触摸数据(pointer),它描述了屏幕上指针(例如,触摸,鼠标和触控笔)的时间,类型,位置和移动。 第二层概念为手势,描述由一个或多个原始移动数据组成的语义动作。一般情况下单独的原始触摸数据没有任何意义。
原始触摸数据是由系统传给native,native再通过flutter view channel传给flutter。
flutter接收native传来的原始数据接口如下:

  void _handlePointerDataPacket(ui.PointerDataPacket packet) {    // We convert pointer data to logical pixels so that e.g. the touch slop can be    // defined in a device-independent manner.    _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, ui.window.devicePixelRatio));    if (!locked)      _flushPointerEventQueue();  }

2. Flutter UI碰撞测试

当屏幕接收到触摸时,dart Framework会对您的应用程序执行碰撞测试,以确定触摸与屏幕相接的位置存在哪些视图(renderobject)。 触摸事件然后被分发到最内部的renderobject上。 从最内部renderobject开始,这些事件在renderobject树中向上冒泡传递,通过冒泡传递最后把所有的renderobject遍历出来,从这个传递机制可想而知,遍历出来renderobject列表里的最后一个是WidgetsFlutterBinding(严格来讲WidgetsFlutterBinding不是renderobject),后面会介绍到WidgetsFlutterBinding。

 void _handlePointerEvent(PointerEvent event) {    assert(!locked);    HitTestResult result;    if (event is PointerDownEvent) {      assert(!_hitTests.containsKey(event.pointer));      result = HitTestResult();      hitTest(result, event.position);      _hitTests[event.pointer] = result;      assert(() {        if (debugPrintHitTestResults)          debugPrint('$event: $result');        return true;      }());    } else if (event is PointerUpEvent || event is PointerCancelEvent) {      result = _hitTests.remove(event.pointer);    } else if (event.down) {      result = _hitTests[event.pointer];    } else {      return; // We currently ignore add, remove, and hover move events.    }    if (result != null)      dispatchEvent(event, result);  }

上面代码以 histTest()检测当前触摸 pointer event 涉及到哪些视图。
最后通过dispatchEvent(event, result)来处理该事件。

void dispatchEvent(PointerEvent event, HitTestResult result) {    assert(!locked);     assert(result != null);    for (HitTestEntry entry in result.path) {      try {        entry.target.handleEvent(event, entry);      } catch (exception, stack) {      }    }  }

上面的代码就是用来分别调用每个视图(RenderObject)的手势识别器独自处理当前触摸事件(决定是否接收此事件)。
entry.target是每个widget对应的RenderObject,所有的RenderObject都需要实现(implements)HitTestTarget类的接口,HitTestTarget里面有就有handleEvent这个接口,所以每个RenderObject都需要实现handleEvent这个接口, 这个接口就是用来处理手势识别。

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget

除了最后一个WidgetsFlutterBinding外,其他视图RenderObject调用自己的handleEvent来识别手势,其作用就是判断当前手势是否要放弃,如果不放弃则丢到一个路由器里(这个路由器就是手势竞技场)最后由WidgetsFlutterBinding 调用handleEvent统一决议这些手势识别器最终谁胜出,所以这里WidgetsFlutterBinding.handleEvent其实就是统一处理接口,它的代码如下:

  void handleEvent(PointerEvent event, HitTestEntry entry) {    pointerRouter.route(event);    if (event is PointerDownEvent) {      gestureArena.close(event.pointer);    } else if (event is PointerUpEvent) {      gestureArena.sweep(event.pointer);    }  }

3. Flutter UI手势决议

从上面的介绍可以得出一次触摸事件可能触发多个手势识别器。框架通过让每个识别器加入一个"手势竞争场"来决议用户想要的手势。"手势竞争场"使用以下规则来决议哪个手势胜出,非常简单

  1. 在任何时候,任何识别器都可以自己宣布失败并主动离开"手势竞争场"。如果在当前"竞争场"中只剩下一个识别器,那么剩下来的就是赢家,赢家意味着独自接收此触摸事件并做出响应动作

  2. 在任何时候,任何识别器都可以自己宣布胜利,并且最终就是它胜利,所有剩下的其他识别器都会失败

4. Flutter UI手势例子

下面示例表示屏幕window由ABCDEFKG视图组成,其中A视图是根视图,即是最底下的视图。红圈表示触摸点位置,触摸落在G视图的中间位置。

根据碰撞测试,遍历出响应此触摸事件的视图路径:
WidgetsFlutterBinding <- A <- C <- K <- G (其中GKCA是renderObject)

遍历路径列表后,开始调用各自的视图(GKCA)entry.target.handleEvent来把自己识别器放到竞技场里参加决议,当然有些视图由于根据自己的逻辑判断主动放弃识别该触摸事件。这个处理过程如下图

Flutter UI录制

从上面的flutter手势处理可知,我们只需要在手势识别器回调上包装回调方法,即可拦截到手势回调方法,这样我们就可以在拦截过程读到WidgetsFlutterBinding <- A <- C <- K <- G链路的这棵视图树。我们只需要把这个棵树,树上的节点相关属性和手势类型记录下来,那回放时,通过这些信息去匹配到当前界面上的对应视图即可回放。下面是tap事件的录制代码,其他类型手势的录制代码原理一样,这里略过。

  static GestureTapCallback onTapWithRecord(GestureTapCallback orgOnTap,       BuildContext context)  {    if (null != orgOnTap && null != context)    {      final GestureTapCallback onTapWithRecord = () {        if(bStartRecord)        {          saveTapInfo(context, TouchEventUIType.OnTap,null);        }        if (null != orgOnTap)        {          orgOnTap();        }      };      return onTapWithRecord;    }    return orgOnTap;  }  static void saveTapInfo(BuildContext context, TouchEventUIType type, Offset point)  {    if(null == point && null != pointerPacketList && pointerPacketList.isNotEmpty)    {      final ui.PointerDataPacket last = pointerPacketList.last;      if(null != last && null != last.data && last.data.isNotEmpty)      {        final ui.Rect rect = QueReplayTool.getWindowRect(context);        point = new Offset(last.data.last.physicalX / ui.window.devicePixelRatio - rect.left,          last.data.last.physicalY /ui.window.devicePixelRatio - rect.top);      }    }    final RecordInfo record = createTapRecordInfo(context, type, point);    if(null != record)    {      FlutterQuestionReplayPlugin.saveRecordDataToNative(record);    }    clearPointerPacketList();  }

Flutter UI回放

ui回放分两部分,第一部分通过录制的相关信息match到当前界面相应视图,第二部分是在此视图上进行模拟相关手势动作,这部分是个难点,也是重点,其中涉及到怎样生成原始的触摸数据信息,里面有时间,类型,坐标,方向,如果这些信息设置不合理或者错误会导致crash,还有滚动距离不符需要补偿,怎么补偿等等。
下面是滚动事件回放流程图,其他类型手势的回放原理一样。

上面的预处理,识别消耗指的是在滚动开始时,手势识别器要判断是否符合滚动手势所需要滚动的距离。
所以我们为了让其控件滚动首先要生成一些触摸点数据,让手势识别器识别为滚动事件。这样才能进行后续的滚动动作。
下面是滚动处理逻辑代码,如下:

 void verticalScroll(double dstPoint, double moveDis) {    preReplayPacket = null;    if (0.0 != moveDis) {      //此处计算滚动方向,和滚动单元像素偏移,由于代码太长略过      int count =          ((ui.window.devicePixelRatio * moveDis) / (unit.abs())).round() * 2;      if (count < minCount) {        count = minCount; //保证最少偏移50/2=25 小于这个数 可能没反应,因为被其他控件检测滚动消耗掉了        //还有就是如果count太小,count被scroll view消耗完前并没有滚动,这是就触摸结束了(ui.PointerChange.up),那可能引起cell        //点击事件跳转事件      }      final double physicalX =          rect.center.dx * ui.window.devicePixelRatio; //376.0;      double physicalY;      final double needOffset = (count * unit).abs();      final double targetHeight = rect.size.height * ui.window.devicePixelRatio;      final int scrollPadding = rect.height ~/ 4;      if (needOffset <= targetHeight / 2) {        physicalY = rect.center.dy * ui.window.devicePixelRatio;      } else if (needOffset > targetHeight / 2 && needOffset < targetHeight) {        physicalY = (orgMoveDis > 0)            ? (rect.bottom - scrollPadding) * ui.window.devicePixelRatio            : (rect.top + scrollPadding) * ui.window.devicePixelRatio;      } else {        physicalY = (orgMoveDis > 0)            ? (rect.bottom - scrollPadding) * ui.window.devicePixelRatio            : (rect.top + scrollPadding) * ui.window.devicePixelRatio;        count = ((rect.height - 2 * scrollPadding) *                ui.window.devicePixelRatio /                unit.abs())            .round();      }      final List packetList =createTouchDataList(count, unit, physicalY, physicalX);      exeScroolTouch(packetList,dstPoint);    } else {      new Timer(const Duration(microseconds: fpsInterval), () {        replayScrollEvent();      });    }  }

上面代码大概处理逻辑:1.计算滚动方向,每个生成的触摸数据偏移单元 2.计算滚动的开始位置 3.生成滚动原始触摸数据列表 4.循环发射原始触摸数据,并计算是否滚动到指定的位置,如果还达不到指定的位置,则继续补给

生成滚动原始触摸数据列表代码如下:
第一数据是down触摸数据,其他都是move触摸数据。up数据在这里不需要生成,当滚动距离到目标位置后才另外生成up触摸数据。为什么这样设计?此处留给大家思考!

List  createTouchDataList(int count,double unit,double physicalY,double physicalX)  {      final List packetList =  [];      int uptime = 0;      for (int i = 0; i < count; i++) {      ui.PointerChange change;      if (0 == i) {      change = ui.PointerChange.down;      } else {      change = ui.PointerChange.move;      physicalY += unit;      if (i < 15) //前面几个点让在短时间内偏移的距离长点 这样避开单击和长按事件          {      physicalY += unit;      physicalY += unit;      }      }      uptime += replayOnePointDuration;      final ui.PointerData pointer = new ui.PointerData(      timeStamp: new Duration(microseconds: uptime),      change: change,      kind: ui.PointerDeviceKind.touch,      device: 1,      physicalX: physicalX,      physicalY: physicalY,      buttons: 0,      pressure: 0.0,      pressureMin: 0.0,      pressureMax: touchPressureMax,      distance: 0.0,      distanceMax: 0.0,      radiusMajor: downRadiusMajor,      radiusMinor: 0.0,      radiusMin: downRadiusMin,      radiusMax: downRadiusMax,      orientation: orientation,      tilt: 0.0);      final List pointerList = [];      pointerList.add(pointer);      final ui.PointerDataPacket packet =      new ui.PointerDataPacket(data: pointerList);      packetList.add(packet);      }      return packetList;  }

循环发射原始触摸数据,并判断是否继续补给代码如下:
我们以定时器不断的往系统发送触摸数据,每次发送数据前都需要判断是否已经达到目标位置。

void exeScroolTouch(List packetList,double dstPoint){  Timer.periodic(const Duration(microseconds: fpsInterval), (Timer timer) {  final ScrollableState state = element.state;  final double curPoint = state.position.pixels;//ui.window.physicalSize.height*state.position.pixels/RecordInfo.recordedWindowH;  final double offset = (dstPoint - curPoint).abs();  final bool existOffset = offset > 1 ? true : false;  if (packetList.isNotEmpty && existOffset) {    sendTouchData(packetList, offset);  } else if (packetList.isNotEmpty) {  record.succ = true;  timer.cancel();  packetList.clear();  if (null != preReplayPacket) {  final ui.PointerDataPacket packet =  createUpTouchPointPacket();  if (null != packet) {  ui._window.onPointerDataPacket(packet);  }  }  new Timer(const Duration(microseconds: fpsInterval), () {  replayScrollEvent();  });  } else if (existOffset) {  record.succ = true;  timer.cancel();  packetList.clear();  final ui.PointerDataPacket packet =  createUpTouchPointPacket();  if (null != packet) {  ui._window.onPointerDataPacket(packet);  }  verticalScroll(dstPoint, dstPoint - curPoint);  } else {    finishReplay();  }  });  }

以上就是关于"Flutter用户侧问题怎么解决"这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注行业资讯频道。

手势 数据 事件 视图 识别器 原始 用户 代码 就是 问题 位置 处理 生成 接口 内容 决议 类型 信息 动作 屏幕 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 fifa22服务器水平 常见的软件开发类型 能源互联网..金风科技 数据库查看所有数据 镇江市网络安全与信息化领导小组 计算机网络技术要学英语吗 义乌市网络安全宣传 altium离线数据库 百度网络技术有限公司有公寓没 网络安全管理责任体系 云服务器如何挂安卓游戏 商品销售管理系统数据库设计ER 102规约对于服务器的要求 宜昌极客网络技术公司 不能用or运算符的数据库 数据库简述创建视图的方法 物联网lwip并发服务器 成绩管理系统数据库的设计 华为腾讯阿里的服务器都在那里 工商银行成都软件开发中心 性价比高的重庆服务器托管 xsx设置代理服务器 2018国家网络安全周 台州椒江区正规软件开发 网络安全知识内容幼儿园 惠普服务器进入安全模式按键 如何找到肿瘤数据库 猫王北京互联网科技有限公司 重庆惠普服务器虚拟化价格 软件开发w7安装什么版
0