溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Flutter如何支持放大鏡的輸入框功能

發(fā)布時(shí)間:2022-02-28 13:44:49 來源:億速云 閱讀:238 作者:小新 欄目:開發(fā)技術(shù)

這篇文章將為大家詳細(xì)講解有關(guān)Flutter如何支持放大鏡的輸入框功能,小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。

功能需求

最近需求開發(fā)中遇到一個(gè)Flutter開發(fā)問題,為了優(yōu)化用戶輸入體驗(yàn)。產(chǎn)品同學(xué)希望能夠在輸入框支持在移動(dòng)光標(biāo)過程中可以出現(xiàn)放大鏡功能。原先以為是一個(gè)小需求,因?yàn)樵到y(tǒng)上iOS和安卓印象中是自帶這個(gè)功能的。在實(shí)施開發(fā)時(shí)才發(fā)現(xiàn)原來并不是這樣的,Flutter好像并沒有去支持原有的功能。

Flutter如何支持放大鏡的輸入框功能

需求調(diào)研

為了確認(rèn)官方是否支持了輸入框放大鏡功能,去github項(xiàng)目上搜索issue后發(fā)現(xiàn)這個(gè)問題在18年就有人提到過,但官方卻一直沒有去支持實(shí)現(xiàn)。

Flutter如何支持放大鏡的輸入框功能

既然官方?jīng)]有支持,秉承有輪子我就用的思想繼續(xù)通過github搜索是否有開發(fā)者自定義實(shí)現(xiàn)了這個(gè)功能。

搜索Magnifier找到了一篇文章是對(duì)放大鏡的實(shí)現(xiàn),但他并不是在輸入框上的實(shí)現(xiàn),只對(duì)屏幕手勢(shì)觸摸的地方進(jìn)行放大。

Flutter如何支持放大鏡的輸入框功能

因?yàn)檎也坏酵耆珜?shí)現(xiàn)輸入框放大鏡功能,那么只能自行去實(shí)現(xiàn)該功能了??梢愿鶕?jù)Magnifier來為輸入框?qū)崿F(xiàn)放大鏡功能。

需求實(shí)現(xiàn)

通過對(duì)TextField的使用會(huì)發(fā)現(xiàn),當(dāng)使用光標(biāo)雙擊或是長(zhǎng)按會(huì)出現(xiàn)TextToolBar功能欄,隨著光標(biāo)的移動(dòng),上方的編輯欄也會(huì)跟著光標(biāo)進(jìn)行移動(dòng)。這個(gè)發(fā)現(xiàn)正好能夠在放大鏡功能上運(yùn)用:跟隨光標(biāo)移動(dòng)+放大就能夠?qū)崿F(xiàn)最終期望的效果了。

Flutter如何支持放大鏡的輸入框功能

源碼解讀

那么在功能實(shí)現(xiàn)之前就需要閱讀TextField源碼了解光標(biāo)上方的編輯欄是如何實(shí)現(xiàn)并且能夠跟隨光標(biāo)的。

PS:源碼解析使用的是extended_text_field,主因是項(xiàng)目中使用了富文本輸入和顯示。

ExtendedTextField輸入框組件源碼找到ExtendedEditableText中視圖build方法可以看到CompositedTransformTarget_toolbarLayerLink。而這兩個(gè)已經(jīng)是實(shí)現(xiàn)放大鏡功能的關(guān)鍵信息了。

關(guān)于CompositedTransformTarget的使用可以在網(wǎng)上搜到很多,作用是來綁定兩個(gè)View視圖。除了CompositedTransformTarget之外還有CompositedTransformFollower。簡(jiǎn)單理解就是CompositedTransformFollower是綁定者,CompositedTransformTarget是被綁定者,前者跟隨后者。_toolbarLayerLink就是跟隨光標(biāo)操作欄的綁定媒介。

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

通過源碼查詢找到_toolbarLayerLink另一個(gè)使用者ExtendedTextSelectionOverlay

void createSelectionOverlay({ //創(chuàng)建操作欄
  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就是編輯欄的實(shí)現(xiàn)。

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是如何實(shí)現(xiàn)的。在_ExtendedTextFieldStatebuild方法中可以找到textSelectionControls默認(rèn)創(chuàng)建。由于安卓和iOS平臺(tái)存在差異性,因此有cupertinoTextSelectionControlsmaterialTextSelectionControls兩個(gè)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源碼實(shí)現(xiàn)。布局實(shí)現(xiàn)在_TextSelectionControlsToolbar中。_TextSelectionHandlePainter是繪制光標(biāo)樣式的方法。

 @override
  Widget build(BuildContext context) {
      // 左右光標(biāo)的定位位置
    final TextSelectionPoint startTextSelectionPoint = widget.endpoints[0];
    // 這里做了判斷是否是兩個(gè)光標(biāo)
    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, // 左邊光標(biāo)
      anchorBelow: anchorBelow,// 右邊光標(biāo)
      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(), // 每個(gè)編輯操作的按鈕功能
    );
  }
}
/// 安卓選中樣式繪制(默認(rèn)是圓點(diǎn)加上一個(gè)箭頭)
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;
  }
}

功能復(fù)刻

了解源碼功能之后就能拷貝MaterialTextSelectionControls實(shí)現(xiàn)來完成放大鏡功能了。同樣是繼承TextSelectionControls,實(shí)現(xiàn)MaterialMagnifierControls功能。

主要修改點(diǎn)在_MagnifierControlsToolbar的實(shí)現(xiàn)以及MaterialMagnifier功能

MagnifierControlsToolbar

其中的build方法返回了widget.endpoints光標(biāo)的定位信息,定位信息去計(jì)算出偏移量。最后將兩個(gè)光標(biāo)信息入?yún)⒌?code>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放大鏡的實(shí)現(xiàn)。這里是引入了安卓的一些布局參數(shù)來實(shí)現(xiàn),iOS是另外定制了布局參數(shù)可以參考Flutter官方源碼定制iOS布局。

放大鏡實(shí)現(xiàn)方法主要是BackdropFilterImageFilter來實(shí)現(xiàn)的,根據(jù)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,
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

交互優(yōu)化

實(shí)現(xiàn)放大鏡功能之外還需要控制顯示,由于在拖動(dòng)狀態(tài)下才顯示放大鏡,隱藏操作欄功能,因此需要去監(jiān)聽手勢(shì)狀態(tài)信息。

手勢(shì)監(jiān)聽是在_TextSelectionHandleOverlayState中,需要去監(jiān)聽onPanStart、onPanUpdate、onPanEnd、onPanCancel這幾個(gè)狀態(tài)。

狀態(tài)行動(dòng)
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,
          () {},
    ),
  ),
);

在開始拓展手勢(shì)時(shí)展示放大鏡,隱藏操作。_builderMagnifier嵌套在OverlayEntry組件在Overlay上插入,實(shí)現(xiàn)方式是和操作欄完全一樣的。

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

同理在拖拽結(jié)束時(shí)去隱藏放大鏡,重新創(chuàng)建操作欄恢復(fù)顯示。

void _handleDragEnd(DragEndDetails details) {
  widget.hideMagnifierBarFunc();
  if (toolBarRecover) {
    widget.showToolbarFunc();
    toolBarRecover = false;
  }
}

void hideMagnifierBar() {
  if (_magnifier != null) {
    _magnifier!.remove();
    _magnifier = null;
  }
}

最終效果

最后實(shí)現(xiàn)效果如下,通過移動(dòng)光標(biāo)可顯示放大鏡功能,松開手勢(shì)就是操作欄顯示恢復(fù)。

Flutter如何支持放大鏡的輸入框功能

關(guān)于“Flutter如何支持放大鏡的輸入框功能”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI