我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Flutter如何支持放大镜的输入框功能

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Flutter如何支持放大镜的输入框功能

这篇文章将为大家详细讲解有关Flutter如何支持放大镜的输入框功能,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。

功能需求

最近需求开发中遇到一个Flutter开发问题,为了优化用户输入体验。产品同学希望能够在输入框支持在移动光标过程中可以出现放大镜功能。原先以为是一个小需求,因为原生系统上iOS和安卓印象中是自带这个功能的。在实施开发时才发现原来并不是这样的,Flutter好像并没有去支持原有的功能。

Flutter如何支持放大镜的输入框功能

需求调研

为了确认官方是否支持了输入框放大镜功能,去github项目上搜索issue后发现这个问题在18年就有人提到过,但官方却一直没有去支持实现。

Flutter如何支持放大镜的输入框功能

既然官方没有支持,秉承有轮子我就用的思想继续通过github搜索是否有开发者自定义实现了这个功能。

搜索Magnifier找到了一篇文章是对放大镜的实现,但他并不是在输入框上的实现,只对屏幕手势触摸的地方进行放大。

Flutter如何支持放大镜的输入框功能

因为找不到完全实现输入框放大镜功能,那么只能自行去实现该功能了。可以根据Magnifier来为输入框实现放大镜功能。

需求实现

通过对TextField的使用会发现,当使用光标双击或是长按会出现TextToolBar功能栏,随着光标的移动,上方的编辑栏也会跟着光标进行移动。这个发现正好能够在放大镜功能上运用:跟随光标移动+放大就能够实现最终期望的效果了。

Flutter如何支持放大镜的输入框功能

源码解读

那么在功能实现之前就需要阅读TextField源码了解光标上方的编辑栏是如何实现并且能够跟随光标的。

PS:源码解析使用的是extended_text_field,主因是项目中使用了富文本输入和显示。

ExtendedTextField输入框组件源码找到ExtendedEditableText中视图build方法可以看到CompositedTransformTarget_toolbarLayerLink。而这两个已经是实现放大镜功能的关键信息了。

关于CompositedTransformTarget的使用可以在网上搜到很多,作用是来绑定两个View视图。除了CompositedTransformTarget之外还有CompositedTransformFollower。简单理解就是CompositedTransformFollower是绑定者,CompositedTransformTarget是被绑定者,前者跟随后者。_toolbarLayerLink就是跟随光标操作栏的绑定媒介。

return CompositedTransformTarget(  link: _toolbarLayerLink, // 操作工具  child: Semantics(    ...    child: _Editable(      key: _editableKey,      startHandleLayerLink: _startHandleLayerLink, //左边光标位置      endHandleLayerLink: _endHandleLayerLink, //右边光标位置      textSpan: _buildTextSpan(context),      value: _value,      cursorColor: _cursorColor,      ......    ),  ),);

通过源码查询找到_toolbarLayerLink另一个使用者ExtendedTextSelectionOverlay

void createSelectionOverlay({ //创建操作栏  ExtendedRenderEditable? renderObject,  bool showHandles = true,}) {  _selectionOverlay = ExtendedTextSelectionOverlay(     clipboardStatus: _clipboardStatus,    context: context,    value: _value,    debugRequiredFor: widget,    toolbarLayerLink: _toolbarLayerLink,    startHandleLayerLink: _startHandleLayerLink,    endHandleLayerLink: _endHandleLayerLink,    renderObject: renderObject ?? renderEditable,    selectionControls: widget.selectionControls,   .....  );    ...

通过源码查询可以找到CompositedTransformFollower组件使用,可以通过代码看到selectionControls!.buildToolbar就是编辑栏的实现。

return Directionality(  textDirection: Directionality.of(this.context),  child: FadeTransition(    opacity: _toolbarOpacity,    child: CompositedTransformFollower( // 操作栏的跟踪组件      link: toolbarLayerLink,      showWhenUnlinked: false,      offset: -editingRegion.topLeft,      child: Builder(        builder: (BuildContext context) {          return selectionControls!.buildToolbar(             context,            editingRegion,            renderObject.preferredLineHeight,            midpoint,            endpoints,            selectionDelegate!,            clipboardStatus!,            renderObject.lastSecondaryTapDownPosition,          );        },      ),    ),  ),);

然后返回去找selectionControls是如何实现的。在_ExtendedTextFieldStatebuild方法中可以找到textSelectionControls默认创建。由于安卓和iOS平台存在差异性,因此有cupertinoTextSelectionControlsmaterialTextSelectionControls两个selectionControls。

switch (theme.platform) {  case TargetPlatform.iOS:    final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);    forcePressEnabled = true;    textSelectionControls ??= cupertinoTextSelectionControls;    ......    break;     ......  case TargetPlatform.android:  case TargetPlatform.fuchsia:    forcePressEnabled = false;    textSelectionControls ??= materialTextSelectionControls;   .....    break;    ....}

这里就只看MaterialTextSelectionControls源码实现。布局实现在_TextSelectionControlsToolbar中。_TextSelectionHandlePainter是绘制光标样式的方法。

 @override  Widget build(BuildContext context) {      // 左右光标的定位位置    final TextSelectionPoint startTextSelectionPoint = widget.endpoints[0];    // 这里做了判断是否是两个光标    final TextSelectionPoint endTextSelectionPoint = widget.endpoints.length > 1      ? widget.endpoints[1]      : widget.endpoints[0];    final Offset anchorAbove = Offset(      widget.globalEditableRegion.left + widget.selectionMidpoint.dx,      widget.globalEditableRegion.top + startTextSelectionPoint.point.dy - widget.textLineHeight - _kToolbarContentDistance,    );    final Offset anchorBelow = Offset(      widget.globalEditableRegion.left + widget.selectionMidpoint.dx,      widget.globalEditableRegion.top + endTextSelectionPoint.point.dy + _kToolbarContentDistanceBelow,    );   ....    return TextSelectionToolbar(      anchorAbove: anchorAbove, // 左边光标      anchorBelow: anchorBelow,// 右边光标      children: itemDatas.asMap().entries.map((MapEntry<int, _TextSelectionToolbarItemData> entry) {        return TextSelectionToolbarTextButton(          padding: TextSelectionToolbarTextButton.getPadding(entry.key, itemDatas.length),          onPressed: entry.value.onPressed,          child: Text(entry.value.label),         );      }).toList(), // 每个编辑操作的按钮功能    );  }}/// 安卓选中样式绘制(默认是圆点加上一个箭头)class _TextSelectionHandlePainter extends CustomPainter {  _TextSelectionHandlePainter({ required this.color });  final Color color;  @override  void paint(Canvas canvas, Size size) {    final Paint paint = Paint()..color = color;    final double radius = size.width/2.0;    final Rect circle = Rect.fromCircle(center: Offset(radius, radius), radius: radius);    final Rect point = Rect.fromLTWH(0.0, 0.0, radius, radius);    final Path path = Path()..addOval(circle)..addRect(point);    canvas.drawPath(path, paint);  }  @override  bool shouldRepaint(_TextSelectionHandlePainter oldPainter) {    return color != oldPainter.color;  }}

功能复刻

了解源码功能之后就能拷贝MaterialTextSelectionControls实现来完成放大镜功能了。同样是继承TextSelectionControls,实现MaterialMagnifierControls功能。

主要修改点在_MagnifierControlsToolbar的实现以及MaterialMagnifier功能

MagnifierControlsToolbar

其中的build方法返回了widget.endpoints光标的定位信息,定位信息去计算出偏移量。最后将两个光标信息入参到MaterialMagnifier组件。

const double _kHandleSize = 22.0;const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0;const double _kToolbarContentDistance = 8.0;class MaterialMagnifierControls extends TextSelectionControls {  @override  Size getHandleSize(double textLineHeight) =>      const Size(_kHandleSize, _kHandleSize);  @override  Widget buildToolbar(    BuildContext context,    Rect globalEditableRegion,    double textLineHeight,    Offset selectionMidpoint,    List<TextSelectionPoint> endpoints,    TextSelectionDelegate delegate,    ClipboardStatusNotifier clipboardStatus,    Offset? lastSecondaryTapDownPosition,  ) {    return _MagnifierControlsToolbar(      globalEditableRegion: globalEditableRegion,      textLineHeight: textLineHeight,      selectionMidpoint: selectionMidpoint,      endpoints: endpoints,      delegate: delegate,      clipboardStatus: clipboardStatus,    );  }  @override  Widget buildHandle(      BuildContext context, TextSelectionHandleType type, double textHeight,      [VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {    return const SizedBox();  }  @override  Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight,      [double? startGlyphHeight, double? endGlyphHeight]) {    switch (type) {      case TextSelectionHandleType.left:        return const Offset(_kHandleSize, 0);      case TextSelectionHandleType.right:        return Offset.zero;      default:        return const Offset(_kHandleSize / 2, -4);    }  }}class _MagnifierControlsToolbar extends StatefulWidget {  const _MagnifierControlsToolbar({    Key? key,    required this.clipboardStatus,    required this.delegate,    required this.endpoints,    required this.globalEditableRegion,    required this.selectionMidpoint,    required this.textLineHeight,  }) : super(key: key);  final ClipboardStatusNotifier clipboardStatus;  final TextSelectionDelegate delegate;  final List<TextSelectionPoint> endpoints;  final Rect globalEditableRegion;  final Offset selectionMidpoint;  final double textLineHeight;  @override  _MagnifierControlsToolbarState createState() =>      _MagnifierControlsToolbarState();}class _MagnifierControlsToolbarState extends State<_MagnifierControlsToolbar>    with TickerProviderStateMixin {  Offset offset1 = Offset.zero;  Offset offset2 = Offset.zero;  void _onChangedClipboardStatus() {    setState(() {    });  }  @override  void initState() {    super.initState();    widget.clipboardStatus.addListener(_onChangedClipboardStatus);    widget.clipboardStatus.update();  }  @override  void didUpdateWidget(_MagnifierControlsToolbar oldWidget) {    super.didUpdateWidget(oldWidget);    if (widget.clipboardStatus != oldWidget.clipboardStatus) {      widget.clipboardStatus.addListener(_onChangedClipboardStatus);      oldWidget.clipboardStatus.removeListener(_onChangedClipboardStatus);    }    widget.clipboardStatus.update();  }  @override  void dispose() {    super.dispose();    if (!widget.clipboardStatus.disposed) {      widget.clipboardStatus.removeListener(_onChangedClipboardStatus);    }  }  @override  Widget build(BuildContext context) {    TextSelectionPoint point = widget.endpoints[0];    if(widget.endpoints.length > 1){      if(offset1 != widget.endpoints[0].point){        point =  widget.endpoints[0];        offset1 = point.point;      }      if(offset2 != widget.endpoints[1].point){        point =  widget.endpoints[1];        offset2 = point.point;      }    }    final TextSelectionPoint startTextSelectionPoint = point;    final Offset anchorAbove = Offset(      widget.globalEditableRegion.left + startTextSelectionPoint.point.dx,      widget.globalEditableRegion.top +          startTextSelectionPoint.point.dy -          widget.textLineHeight -          _kToolbarContentDistance,    );    final Offset anchorBelow = Offset(      widget.globalEditableRegion.left + startTextSelectionPoint.point.dx,      widget.globalEditableRegion.top +          startTextSelectionPoint.point.dy +          _kToolbarContentDistanceBelow,    );    return  MaterialMagnifier(        anchorAbove: anchorAbove,        anchorBelow: anchorBelow,        textLineHeight: widget.textLineHeight,    );  }}final TextSelectionControls materialMagnifierControls =    MaterialMagnifierControls();

MaterialMagnifier

MaterialMagnifier是参考Widget Magnifier放大镜的实现。这里是引入了安卓的一些布局参数来实现,iOS是另外定制了布局参数可以参考Flutter官方源码定制iOS布局。

放大镜实现方法主要是BackdropFilterImageFilter来实现的,根据Matrix4scaletranslate操作完成放大功能。

const double _kToolbarScreenPadding = 8.0;const double _kToolbarHeight = 44.0;class MaterialMagnifier extends StatelessWidget {  const MaterialMagnifier({    Key? key,    required this.anchorAbove,    required this.anchorBelow,    required this.textLineHeight,    this.size = const Size(90, 50),    this.scale = 1.7,  }) : super(key: key);  final Offset anchorAbove;  final Offset anchorBelow;  final Size size;  final double scale;  final double textLineHeight;  @override  Widget build(BuildContext context) {    final double paddingAbove =        MediaQuery.of(context).padding.top + _kToolbarScreenPadding;    final double availableHeight = anchorAbove.dy - paddingAbove;    final bool fitsAbove = _kToolbarHeight <= availableHeight;    final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);    final Matrix4 updatedMatrix = Matrix4.identity()      ..scale(1.1,1.1)      ..translate(0.0,-50.0);    Matrix4 _matrix = updatedMatrix;    return Container(      child: Padding(        padding: EdgeInsets.fromLTRB(          _kToolbarScreenPadding,          paddingAbove,          _kToolbarScreenPadding,          _kToolbarScreenPadding,        ),        child: Stack(          children: <Widget>[            CustomSingleChildLayout(              delegate: TextSelectionToolbarLayoutDelegate(                anchorAbove: anchorAbove - localAdjustment,                anchorBelow: anchorBelow - localAdjustment,                fitsAbove: fitsAbove,              ),              child: ClipRRect(                borderRadius: BorderRadius.circular(10),                child: BackdropFilter(                  filter: ImageFilter.matrix(_matrix.storage),                  child: CustomPaint(                    painter: const MagnifierPainter(color: Color(0xFFdfdfdf)),                    size: size,                  ),                ),              ),            ),          ],        ),      ),    );  }}

交互优化

实现放大镜功能之外还需要控制显示,由于在拖动状态下才显示放大镜,隐藏操作栏功能,因此需要去监听手势状态信息。

手势监听是在_TextSelectionHandleOverlayState中,需要去监听onPanStartonPanUpdateonPanEndonPanCancel这几个状态。

状态行动
onPanStart隐藏操作栏、显示放大镜
onPanUpdate显示放大镜,获取到偏移信息
onPanEnd显示操作栏、隐藏放大镜
onPanCancel显示操作栏、隐藏放大镜
final Widget child = GestureDetector(  behavior: HitTestBehavior.translucent,  dragStartBehavior: widget.dragStartBehavior,  onPanStart: _handleDragStart,  onPanUpdate: _handleDragUpdate,  onPanEnd: _handleDragEnd,  onPanCancel: _handleDragCancel,  onTap: _handleTap,  child: Padding(    padding: EdgeInsets.only(      left: padding.left,      top: padding.top,      right: padding.right,      bottom: padding.bottom,    ),    child: widget.selectionControls!.buildHandle(      context,      type,      widget.renderObject.preferredLineHeight,          () {},    ),  ),);

在开始拓展手势时展示放大镜,隐藏操作。_builderMagnifier嵌套在OverlayEntry组件在Overlay上插入,实现方式是和操作栏完全一样的。

void _handleDragStart(DragStartDetails details) {  final Size handleSize = widget.selectionControls!.getHandleSize(    widget.renderObject.preferredLineHeight,  );  _dragPosition = details.globalPosition + Offset(0.0, -handleSize.height);  widget.showMagnifierBarFunc(); // 回调展示放大镜功能  toolBarRecover = widget.hideToolbarFunc();}void showMagnifierBar() {  assert(_magnifier == null);  _magnifier = OverlayEntry(builder: _builderMagnifier);  Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!      .insert(_magnifier!);}

同理在拖拽结束时去隐藏放大镜,重新创建操作栏恢复显示。

void _handleDragEnd(DragEndDetails details) {  widget.hideMagnifierBarFunc();  if (toolBarRecover) {    widget.showToolbarFunc();    toolBarRecover = false;  }}void hideMagnifierBar() {  if (_magnifier != null) {    _magnifier!.remove();    _magnifier = null;  }}

最终效果

最后实现效果如下,通过移动光标可显示放大镜功能,松开手势就是操作栏显示恢复。

Flutter如何支持放大镜的输入框功能

关于“Flutter如何支持放大镜的输入框功能”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Flutter如何支持放大镜的输入框功能

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Flutter如何支持放大镜的输入框功能

这篇文章将为大家详细讲解有关Flutter如何支持放大镜的输入框功能,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。功能需求最近需求开发中遇到一个Flutter开发问题,为了优化用户输入体验。产品同学希望能
2023-06-29

flutter微信聊天输入框功能如何实现

这篇文章主要讲解了“flutter微信聊天输入框功能如何实现”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“flutter微信聊天输入框功能如何实现”吧!高仿微信聊天输入框,效果图如下(目前都
2023-07-05

jquery如何实现输入框数字的增加和减少功能

本篇内容主要讲解“jquery如何实现输入框数字的增加和减少功能”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“jquery如何实现输入框数字的增加和减少功能”吧!代码实现:首先,需要引入jque
2023-07-05

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录