美文网首页Flutter跨平台
Flutter源码解析-TextField (1)

Flutter源码解析-TextField (1)

作者: 叶落清秋 | 来源:发表于2019-08-02 14:57 被阅读0次

    说明

    本文源码基于flutter 1.7.8
    之前我们分析完文本,现在来分析一下输入框

    使用

    关于怎么使用,这里不做过多的介绍了
    推荐看一下:Flutter TextField详解
    介绍的还是蛮详细的

    问题

    1. 值的监听和变化是怎么实现的
    2. 限制输入字数的效果是怎么实现的
    3. 长按出现的复制、粘贴提示是怎么实现的
    4. 光标呼吸是如何实现的

    分析

    由简单的开始分析,逐步深入


    1. 问题一:值的监听和变化是怎么实现的

    我们通常通过以下方式来获取textField里输入的值

      //将这个controller传递给textField
      final controller = TextEditingController();
      controller.addListener(() {
       //获取输入的值
        print('input ${controller.text}');
      });
    

    那么TextEditingController到底是什么?

    class TextEditingController extends ValueNotifier<TextEditingValue> {
      //将TextEditingValue传给父类
      TextEditingController({ String text })
        : super(text == null ? TextEditingValue.empty : TextEditingValue(text: text));
    
      TextEditingController.fromValue(TextEditingValue value)
        : super(value ?? TextEditingValue.empty);
    
      String get text => value.text;
    
      set text(String newText) {
        value = value.copyWith(
          text: newText,
          selection: const TextSelection.collapsed(offset: -1),
          composing: TextRange.empty,
        );
      }
    
      TextSelection get selection => value.selection;
    
      set selection(TextSelection newSelection) {
        if (newSelection.start > text.length || newSelection.end > text.length)
          throw FlutterError('invalid text selection: $newSelection');
        value = value.copyWith(selection: newSelection, composing: TextRange.empty);
      }
    
      void clear() {
        value = TextEditingValue.empty;
      }
    
      void clearComposing() {
        value = value.copyWith(composing: TextRange.empty);
      }
    }
    
    @immutable
    class TextEditingValue {
      const TextEditingValue({
        this.text = '',
        this.selection = const TextSelection.collapsed(offset: -1),
        this.composing = TextRange.empty,
      }) : assert(text != null),
           assert(selection != null),
           assert(composing != null);
    
      ...
      //当前输入的文本
      final String text;
      //选择文本组,用于确认光标的位置
      final TextSelection selection;
     //这个值实际根本没用上
      final TextRange composing;
    
      static const TextEditingValue empty = TextEditingValue();
    
      ...
    }
    

    到这里其实很明朗了,既然能监听肯定是一个观察者模式了

    class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
      ValueNotifier(this._value);
    
      @override
      T get value => _value;
      T _value;
      //一旦赋值,就通知所有的监听器,值已经发生变化
      set value(T newValue) {
        if (_value == newValue)
          return;
        _value = newValue;
        notifyListeners();
      }
    
      @override
      String toString() => '${describeIdentity(this)}($value)';
    }
    
    class ChangeNotifier implements Listenable {
      ObserverList<VoidCallback> _listeners = ObserverList<VoidCallback>();
    
      ...
     //添加单个监听回调方法
      @override
      void addListener(VoidCallback listener) {
        assert(_debugAssertNotDisposed());
        _listeners.add(listener);
      }
    
      //移出单个监听回调方法
      @override
      void removeListener(VoidCallback listener) {
        assert(_debugAssertNotDisposed());
        _listeners.remove(listener);
      }
    
      //监听器数组置空
      @mustCallSuper
      void dispose() {
        assert(_debugAssertNotDisposed());
        _listeners = null;
      }
    
      @protected
      @visibleForTesting
      void notifyListeners() {
        assert(_debugAssertNotDisposed());
        if (_listeners != null) {
          final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners);
          //遍历循环,调用回调方法
          for (VoidCallback listener in localListeners) {
            try {
              if (_listeners.contains(listener))
                listener();
            } catch (exception, stack) {
              //这个就是常见的出现bug的红色框
              FlutterError.reportError(FlutterErrorDetails(
                exception: exception,
                stack: stack,
                library: 'foundation library',
                context: ErrorDescription('while dispatching notifications for $runtimeType'),
                informationCollector: () sync* {
                  yield DiagnosticsProperty<ChangeNotifier>(
                    'The $runtimeType sending notification was',
                    this,
                    style: DiagnosticsTreeStyle.errorProperty,
                  );
                },
              ));
            }
          }
        }
      }
    }
    

    总结来说值的变化就是通过观察者模式来作用的
    下面我们看下一个重要的类,这个类影响到后续的光标显示

    @immutable
    class TextSelection extends TextRange {
      const TextSelection({
        @required this.baseOffset,
        @required this.extentOffset,
        this.affinity = TextAffinity.downstream,
        this.isDirectional = false,
      }) : super(
             start: baseOffset < extentOffset ? baseOffset : extentOffset,
             end: baseOffset < extentOffset ? extentOffset : baseOffset
           );
    
      const TextSelection.collapsed({
        @required int offset,
        this.affinity = TextAffinity.downstream,
      }) : baseOffset = offset,
           extentOffset = offset,
           isDirectional = false,
           super.collapsed(offset);
    
      TextSelection.fromPosition(TextPosition position)
        : baseOffset = position.offset,
          extentOffset = position.offset,
          affinity = position.affinity,
          isDirectional = false,
          super.collapsed(position.offset);
      //选择区域的左边开始点,可简单理解为光标位置
      //初始为-1,有值的时候,位于光标起始点为0(如: 12,光标在1前面则为0,后面就是1)
      final int baseOffset;
     //选择区域的结束点 (如: 12345,假如选择234,那么这个值就为4)
      final int extentOffset;
    
      final TextAffinity affinity;
    
      final bool isDirectional;
    
      TextPosition get base => TextPosition(offset: baseOffset, affinity: affinity);
    
      TextPosition get extent => TextPosition(offset: extentOffset, affinity: affinity);
    
      ...
    }
    

    2. 问题二:限制输入字数的效果是怎么实现的

    当我们给maxLength服了值后,右下角就有个字数限制显示
    来看下textfield的build方法:

    @override
      Widget build(BuildContext context) {
        super.build(context);
        final ThemeData themeData = Theme.of(context);
        final TextStyle style = themeData.textTheme.subhead.merge(widget.style);
        final Brightness keyboardAppearance = widget.keyboardAppearance ?? themeData.primaryColorBrightness;
        final TextEditingController controller = _effectiveController;
        final FocusNode focusNode = _effectiveFocusNode;
        final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[];
        //maxLength设置类值后, maxLengthEnforced默认为true
        if (widget.maxLength != null && widget.maxLengthEnforced)
          //限制长度为设置的值
          formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
    
        bool forcePressEnabled;
        TextSelectionControls textSelectionControls;
        bool paintCursorAboveText;
        bool cursorOpacityAnimates;
        Offset cursorOffset;
        Color cursorColor = widget.cursorColor;
        Radius cursorRadius = widget.cursorRadius;
    
        switch (themeData.platform) {
          case TargetPlatform.iOS:
            forcePressEnabled = true;
            //创建ios风格的复制粘贴工具栏
            textSelectionControls = cupertinoTextSelectionControls;
            paintCursorAboveText = true;
            cursorOpacityAnimates = true;
            cursorColor ??= CupertinoTheme.of(context).primaryColor;
            cursorRadius ??= const Radius.circular(2.0);
       
            const int _iOSHorizontalOffset = -2;
            cursorOffset = Offset(_iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
            break;
    
          case TargetPlatform.android:
          case TargetPlatform.fuchsia:
            forcePressEnabled = false;
            //创建md风格的复制粘贴工具栏
            textSelectionControls = materialTextSelectionControls;
            paintCursorAboveText = false;
            cursorOpacityAnimates = false;
            cursorColor ??= themeData.cursorColor;
            break;
        }
        //使用了EditableText,这个后面会重点介绍
        Widget child = RepaintBoundary(
          child: EditableText(
            key: _editableTextKey,
            readOnly: widget.readOnly,
            showCursor: widget.showCursor,
            showSelectionHandles: _showSelectionHandles,
            controller: controller,
            focusNode: focusNode,
            keyboardType: widget.keyboardType,
            textInputAction: widget.textInputAction,
            textCapitalization: widget.textCapitalization,
            style: style,
            strutStyle: widget.strutStyle,
            textAlign: widget.textAlign,
            textDirection: widget.textDirection,
            autofocus: widget.autofocus,
            obscureText: widget.obscureText,
            autocorrect: widget.autocorrect,
            maxLines: widget.maxLines,
            minLines: widget.minLines,
            expands: widget.expands,
            selectionColor: themeData.textSelectionColor,
            selectionControls: widget.selectionEnabled ? textSelectionControls : null,
            onChanged: widget.onChanged,
            onSelectionChanged: _handleSelectionChanged,
            onEditingComplete: widget.onEditingComplete,
            onSubmitted: widget.onSubmitted,
            onSelectionHandleTapped: _handleSelectionHandleTapped,
            inputFormatters: formatters,
            rendererIgnoresPointer: true,
            cursorWidth: widget.cursorWidth,
            cursorRadius: cursorRadius,
            cursorColor: cursorColor,
            cursorOpacityAnimates: cursorOpacityAnimates,
            cursorOffset: cursorOffset,
            paintCursorAboveText: paintCursorAboveText,
            backgroundCursorColor: CupertinoColors.inactiveGray,
            scrollPadding: widget.scrollPadding,
            keyboardAppearance: keyboardAppearance,
            enableInteractiveSelection: widget.enableInteractiveSelection,
            dragStartBehavior: widget.dragStartBehavior,
            scrollController: widget.scrollController,
            scrollPhysics: widget.scrollPhysics,
          ),
        );
    
        if (widget.decoration != null) {
          //装饰框聚焦动画
          child = AnimatedBuilder(
            animation: Listenable.merge(<Listenable>[ focusNode, controller ]),
            builder: (BuildContext context, Widget child) {
              return InputDecorator(
                //添加装饰,这个方法下面就说明
                decoration: _getEffectiveDecoration(),
                baseStyle: widget.style,
                textAlign: widget.textAlign,
                textAlignVertical: widget.textAlignVertical,
                isHovering: _isHovering,
                isFocused: focusNode.hasFocus,
                isEmpty: controller.value.text.isEmpty,
                expands: widget.expands,
                child: child,
              );
            },
            child: child,
          );
        }
    
        return Semantics(
          onTap: () {
            if (!_effectiveController.selection.isValid)
              _effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length);
            _requestKeyboard();
          },
          child: Listener(
            onPointerEnter: _handlePointerEnter,
            onPointerExit: _handlePointerExit,
            child: IgnorePointer(
              ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
              //选择区域文本手势
              child: TextSelectionGestureDetector(
                onTapDown: _handleTapDown,
                onForcePressStart: forcePressEnabled ? _handleForcePressStarted : null,
                onSingleTapUp: _handleSingleTapUp,
                onSingleTapCancel: _handleSingleTapCancel,
                onSingleLongTapStart: _handleSingleLongTapStart,
                onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
                onSingleLongTapEnd: _handleSingleLongTapEnd,
                onDoubleTapDown: _handleDoubleTapDown,
                onDragSelectionStart: _handleMouseDragSelectionStart,
                onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
                behavior: HitTestBehavior.translucent,
                child: child,
              ),
            ),
          ),
        );
      }
    

    也就是说限制输入长度只要LengthLimitingTextInputFormatter(widget.maxLength)就可以实现了,我们继续追踪下去,会发现formatters传递给了EditableText,然后在下面使用到了formatters
    当android的输入法输入字到控件上时,InputConnection就会通过MethodChannel传递到修改后的新值这个方法里

      void _formatAndSetValue(TextEditingValue value) {
        final bool textChanged = _value?.text != value?.text;
        if (textChanged && widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) {
          for (TextInputFormatter formatter in widget.inputFormatters)
            value = formatter.formatEditUpdate(_value, value);
          _value = value;
          _updateRemoteEditingValueIfNeeded();
        } else {
          _value = value;
        }
        if (textChanged && widget.onChanged != null)
          widget.onChanged(value.text);
      }
    

    调用LengthLimitingTextInputFormatter的formatEditUpdate进行文本裁剪

    class LengthLimitingTextInputFormatter extends TextInputFormatter {
    
      ...
      @override
      TextEditingValue formatEditUpdate(
        TextEditingValue oldValue, // unused.
        TextEditingValue newValue,
      ) {
        if (maxLength != null && maxLength > 0 && newValue.text.runes.length > maxLength) {
          //修改选择区域,简而言之就是修改光标位置
          final TextSelection newSelection = newValue.selection.copyWith(
              baseOffset: math.min(newValue.selection.start, maxLength),
              extentOffset: math.min(newValue.selection.end, maxLength),
          );
          final RuneIterator iterator = RuneIterator(newValue.text);
          if (iterator.moveNext())
            for (int count = 0; count < maxLength; ++count)
              if (!iterator.moveNext())
                break;
          //对输入的值进行字符串裁剪,经过上面的循环,rawIndex就是value的长度或限制输入最大值中最小的一个
          final String truncated = newValue.text.substring(0, iterator.rawIndex);
          return TextEditingValue(
            text: truncated,
            selection: newSelection,
            composing: TextRange.empty,
          );
        }
        return newValue;
      }
    }
    

    那么限制字数的绘制是在哪实现的呢?

    InputDecoration _getEffectiveDecoration() {
        final MaterialLocalizations localizations = MaterialLocalizations.of(context);
        final ThemeData themeData = Theme.of(context);
        final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
          .applyDefaults(themeData.inputDecorationTheme)
          .copyWith(
            enabled: widget.enabled,
            hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
          );
    
       //如果你传入了decoration,并且他的counter和counterText都不为空
        if (effectiveDecoration.counter != null || effectiveDecoration.counterText != null)
          return effectiveDecoration;
    
        //如果你传入了buildCounter,这个计数的控件自己实现
        final int currentLength = _effectiveController.value.text.runes.length;
        if (effectiveDecoration.counter == null
            && effectiveDecoration.counterText == null
            && widget.buildCounter != null) {
          final bool isFocused = _effectiveFocusNode.hasFocus;
          counter = Semantics(
            container: true,
            liveRegion: isFocused,
            child: widget.buildCounter(
              context,
              currentLength: currentLength,
              maxLength: widget.maxLength,
              isFocused: isFocused,
            ),
          );
          return effectiveDecoration.copyWith(counter: counter);
        }
        //没设置就返回一个无字数统计的装饰
        if (widget.maxLength == null)
          return effectiveDecoration; // No counter widget
    
        String counterText = '$currentLength';
        String semanticCounterText = '';
    
        // 最大长度大于0
        if (widget.maxLength > 0) {
          //显示的统计数 1/5
          counterText += '/${widget.maxLength}';
          final int remaining = (widget.maxLength - currentLength).clamp(0, widget.maxLength);
          semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);
    
          if (_effectiveController.value.text.runes.length > widget.maxLength) {
            return effectiveDecoration.copyWith(
              errorText: effectiveDecoration.errorText ?? '',
              counterStyle: effectiveDecoration.errorStyle
                ?? themeData.textTheme.caption.copyWith(color: themeData.errorColor),
              counterText: counterText,
              semanticCounterText: semanticCounterText,
            );
          }
        }
    
        return effectiveDecoration.copyWith(
          counterText: counterText,
          semanticCounterText: semanticCounterText,
        );
      }
    

    因此,其实对应的有三种方法来解决不显示右下角的统计数字,但却限制最大字数

    //textfield限制最大字数,但无字数统计
    class TextFieldWithNoCount extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Column(
          children: <Widget>[
            TextField(
              maxLength: 5,
              decoration: InputDecoration(
                counterText: '',
                counter: Container(),
              ),
            ),
            TextField(
              maxLength: 5,
              buildCounter: (context,{currentLength,maxLength, isFocused,}){
                return Container();
              },
            ),
            TextField(
              inputFormatters: [LengthLimitingTextInputFormatter(5)],
            ),
          ],
        );
      }
    }
    

    图片效果就不展示了(展示了也说明不了什么),感兴趣的自己去试试

    3. 问题三: 长按出现的复制、粘贴提示是怎么实现的

    工具栏

    在问题二里我们看到了TextSelectionGestureDetector 手势控件
    先看onTapDown中调用的_handleTapDown,这个手势是必然调用的(只要触发了事件)

    void _handleTapDown(TapDownDetails details) {
        //记录当前点击点
        _renderEditable.handleTapDown(details);
        _startSplash(details.globalPosition);
    
        final PointerDeviceKind kind = details.kind;
        //判断是否显示工具栏,仅限触摸和手写设备
        _shouldShowSelectionToolbar =
            kind == null ||
            kind == PointerDeviceKind.touch ||
            kind == PointerDeviceKind.stylus;
      }
    
    enum PointerDeviceKind {
      /// 触摸
      touch,
    
      /// 鼠标点击
      mouse,
    
      /// 手写
      stylus,
    
      /// 反向手写
      invertedStylus,
    
      /// 不识别事件设备
      unknown
    }
    

    接下来我们看单击事件,也就是移动光标

    void _handleSingleTapUp(TapUpDetails details) {
        if (widget.selectionEnabled) {
          switch (Theme.of(context).platform) {
            case TargetPlatform.iOS:
              _renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
              break;
            case TargetPlatform.android:
            case TargetPlatform.fuchsia:
             //因为down事件已经记录了点,所以这里直接修改位置即可
              _renderEditable.selectPosition(cause: SelectionChangedCause.tap);
              break;
          }
        }
       //申请软键盘
        _requestKeyboard();
        //动画效果
        _confirmCurrentSplash();
        if (widget.onTap != null)
          widget.onTap();
      }
    

    因为down事件已经确认点击位置,当up触发后,确认这是一个单击事件后,直接更新光标到点击位置
    之后我们看下长按选择区域的,这里也会出现复制工具栏

      void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
        if (widget.selectionEnabled) {
          switch (Theme.of(context).platform) {
            case TargetPlatform.iOS:
              _renderEditable.selectPositionAt(
                from: details.globalPosition,
                cause: SelectionChangedCause.longPress,
              );
              break;
            case TargetPlatform.android:
            case TargetPlatform.fuchsia:
             //设置一个区域,从坐标from到坐标to
              _renderEditable.selectWordsInRange(
                from: details.globalPosition - details.offsetFromOrigin,
                to: details.globalPosition,
                cause: SelectionChangedCause.longPress,
              );
              break;
          }
        }
      }
    
    void _handleSingleLongTapEnd(LongPressEndDetails details) {
        if (widget.selectionEnabled) {
          //显示工具栏
          if (_shouldShowSelectionToolbar)
            _editableTextKey.currentState.showToolbar();
        }
      }
    

    当长按且左右移动时会出现一个选择区域,松开手后就会显示工具栏

    void selectWordsInRange({ @required Offset from, Offset to, @required SelectionChangedCause cause }) {
        //摆放文本
        _layoutText(constraints.maxWidth);
        if (onSelectionChanged != null) {
          //绝对坐标转相对坐标,然后计算当前坐标在文本的第几个位置
          final TextPosition firstPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
          final TextSelection firstWord = _selectWordAtOffset(firstPosition);
          final TextSelection lastWord = to == null ?
            firstWord : _selectWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset)));
    
          _handlePotentialSelectionChange(
            //baseOffset和 extentOffset的意思很明显
            TextSelection(
              baseOffset: firstWord.base.offset,
              extentOffset: lastWord.extent.offset,
              affinity: firstWord.affinity,
            ),
            cause,
          );
        }
      }
    
    TextSelection _selectWordAtOffset(TextPosition position) {
        //根据点返回这个字的TextRange,简单来说就是一个转换
        final TextRange word = _textPainter.getWordBoundary(position);
        if (position.offset >= word.end)
          return TextSelection.fromPosition(position);
        return TextSelection(baseOffset: word.start, extentOffset: word.end);
      }
    

    确认文本选择的区域其实靠的就是起点坐标和终点坐标
    继续看showToolbar显示工具栏的步骤

      bool showToolbar() {
        if (_selectionOverlay == null || _selectionOverlay.toolbarIsVisible) {
          return false;
        }
        _selectionOverlay.showToolbar();
        return true;
      }
    
    void showToolbar() {
        //_buildToolbar就是整个工具栏的绘制了,比较简单
        _toolbar = OverlayEntry(builder: _buildToolbar);
        //Overlay是一个层,与stack有关,这里就是讲工具栏加载在输入框之上
        Overlay.of(context, debugRequiredFor: debugRequiredFor).insert(_toolbar);
        _toolbarController.forward(from: 0.0);
      }
    

    我们继续剖析下去,_selectionOverlay.showToolbar()里_selectionOverlay这个是什么?在_buildToolbar的最后可以找到这个:

    return FadeTransition(
          opacity: _toolbarOpacity,
          child: CompositedTransformFollower(
            link: layerLink,
            showWhenUnlinked: false,
            offset: -editingRegion.topLeft,
            child: selectionControls.buildToolbar(
              context,
              editingRegion,
              renderObject.preferredLineHeight,
              midpoint,
              endpoints,
              selectionDelegate,
            ),
          ),
        );
    

    简而言之就是将绘制交给了selectionControls,那么selectionControls的值是怎么来的呢?
    可以在第二个问题的textfield的build方法中找到答案,遗憾的是textfield不能自己选择风格,如果android想用ios风格的工具栏的话就需要重改代码或者直接用EditableText
    2个风格实现的原理都差不多,我们直接看md的吧

    class _MaterialTextSelectionControls extends TextSelectionControls {
      @override
      Widget buildToolbar(
        BuildContext context,
        Rect globalEditableRegion,
        double textLineHeight,
        Offset position,
        List<TextSelectionPoint> endpoints,
        TextSelectionDelegate delegate,
      ) {
        //省略了计算高度部分,工具栏默认显示在输入框的上方,当上方显示的位置不够时就显示在其的下方
        ...
    
        return ConstrainedBox(
          constraints: BoxConstraints.tight(globalEditableRegion.size),
          child: CustomSingleChildLayout(
            delegate: _TextSelectionToolbarLayout(
              MediaQuery.of(context).size,
              globalEditableRegion,
              preciseMidpoint,
            ),
            //4个方法
            child: _TextSelectionToolbar(
              handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
              handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null,
              handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
              handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
            ),
          ),
        );
      }
      ...
    }
    

    _TextSelectionToolbar内的build方法说白了就是一个Row布局内放4个FlatButton控件,着重查看一下TextSelectionToolbar的四个回调方法都做了什么?

    abstract class TextSelectionControls {
      //省略部分内容
      ...
    
      bool canCut(TextSelectionDelegate delegate) {
        //cutEnabled: 当自己设为readonly为true是,这个值为false,默认为true
        //isCollapsed:当selection的start和end相等时返回true
        return delegate.cutEnabled && !delegate.textEditingValue.selection.isCollapsed;
      }
    
      bool canCopy(TextSelectionDelegate delegate) {
        //copyEnabled: 当自己设为readonly为true是,这个值为false,默认为true
        return delegate.copyEnabled && !delegate.textEditingValue.selection.isCollapsed;
      }
    
      bool canPaste(TextSelectionDelegate delegate) {
        // TODO(goderbauer): return false when clipboard is empty, https://github.com/flutter/flutter/issues/11254
        return delegate.pasteEnabled;
      }
    
      bool canSelectAll(TextSelectionDelegate delegate) {
        return delegate.selectAllEnabled && delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed;
      }
    
      void handleCut(TextSelectionDelegate delegate) {
        //举个例子
        //文本是: "我是一个好人" , 现在要剪切"一个"
        final TextEditingValue value = delegate.textEditingValue;
        //往Clipboard添加剪切的数据
        Clipboard.setData(ClipboardData(
          text: value.selection.textInside(value.text),
        ));
        //text的文本就为:"我是好人",光标则移动到"一个"的前面,即2的位置
        delegate.textEditingValue = TextEditingValue(
          text: value.selection.textBefore(value.text)
              + value.selection.textAfter(value.text),
          selection: TextSelection.collapsed(
            offset: value.selection.start
          ),
        );
        //这个其实就是更新界面,滚动到光标的位置,_scrollController.jumpTo
        delegate.bringIntoView(delegate.textEditingValue.selection.extent);
        delegate.hideToolbar();
      }
    
      void handleCopy(TextSelectionDelegate delegate) {
        final TextEditingValue value = delegate.textEditingValue;
        Clipboard.setData(ClipboardData(
          text: value.selection.textInside(value.text),
        ));
        //和上面一个,但是文本未发生变化,光标移动到末尾,即"一个"的后面,也就是4的位置
        delegate.textEditingValue = TextEditingValue(
          text: value.text,
          selection: TextSelection.collapsed(offset: value.selection.end),
        );
        delegate.bringIntoView(delegate.textEditingValue.selection.extent);
        delegate.hideToolbar();
      }
    
      Future<void> handlePaste(TextSelectionDelegate delegate) async {
        final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`.
        //获取Clipboard保存的数据
        final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
        if (data != null) {
          //在光标位置插入这个保存的文本,同时光标移动到保存文本的尾部
          delegate.textEditingValue = TextEditingValue(
            text: value.selection.textBefore(value.text)
                + data.text
                + value.selection.textAfter(value.text),
            selection: TextSelection.collapsed(
              offset: value.selection.start + data.text.length
            ),
          );
        }
        delegate.bringIntoView(delegate.textEditingValue.selection.extent);
        delegate.hideToolbar();
      }
    
      void handleSelectAll(TextSelectionDelegate delegate) {
        //文本不变,但是选择区域变为0-文本的末尾,也就是全选
        delegate.textEditingValue = TextEditingValue(
          text: delegate.textEditingValue.text,
          selection: TextSelection(
            baseOffset: 0,
            extentOffset: delegate.textEditingValue.text.length,
          ),
        );
        delegate.bringIntoView(delegate.textEditingValue.selection.extent);
      }
    }
    

    辅助旋转的小点是怎么绘制的:

    Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight) {
        final Widget handle = SizedBox(
          width: _kHandleSize,
          height: _kHandleSize,
          child: CustomPaint(
            painter: _TextSelectionHandlePainter(
              color: Theme.of(context).textSelectionHandleColor
            ),
          ),
        );
    
        switch (type) {
          //左的的向右旋转90°
          case TextSelectionHandleType.left: // points up-right
            return Transform.rotate(
              angle: math.pi / 2.0,
              child: handle,
            );
          case TextSelectionHandleType.right: // points up-left
            return handle;
          //中间的向右旋转45°
          case TextSelectionHandleType.collapsed: // points up
            return Transform.rotate(
              angle: math.pi / 4.0,
              child: handle,
            );
        }
        assert(type != null);
        return null;
      }
    

    至此,工具栏也分析完了。读完了流程,其实我们也能模仿官方,然后像微信那么弄个自己的工具栏,比如说收藏或删除什么的

    4. 问题四: 光标呼吸是如何实现的

    先说光标呼吸怎么实现,之前我们反复说道了一个控件EditableText,再往其内部的build方法

      @override
      Widget build(BuildContext context) {
        assert(debugCheckHasMediaQuery(context));
        _focusAttachment.reparent();
        super.build(context); // See AutomaticKeepAliveClientMixin.
    
        final TextSelectionControls controls = widget.selectionControls;
        //这是一个可以滚动的控件
        return Scrollable(
          excludeFromSemantics: true,
          axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
          controller: _scrollController,
          physics: widget.scrollPhysics,
          dragStartBehavior: widget.dragStartBehavior,
          viewportBuilder: (BuildContext context, ViewportOffset offset) {
            return CompositedTransformTarget(
              link: _layerLink,
              //语义辅助类,导盲语音播报用的
              child: Semantics(
                onCopy: _semanticsOnCopy(controls),
                onCut: _semanticsOnCut(controls),
                onPaste: _semanticsOnPaste(controls),
                //_Editable这个是个重要的控件,绘制基本就在这发生了
                child: _Editable(
                  key: _editableKey,
                  textSpan: buildTextSpan(),
                  value: _value,
                  cursorColor: _cursorColor,
                  backgroundCursorColor: widget.backgroundCursorColor,
                  showCursor: EditableText.debugDeterministicCursor
                      ? ValueNotifier<bool>(widget.showCursor)
                      : _cursorVisibilityNotifier,
                  hasFocus: _hasFocus,
                  maxLines: widget.maxLines,
                  minLines: widget.minLines,
                  expands: widget.expands,
                  strutStyle: widget.strutStyle,
                  selectionColor: widget.selectionColor,
                  textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
                  textAlign: widget.textAlign,
                  textDirection: _textDirection,
                  locale: widget.locale,
                  obscureText: widget.obscureText,
                  autocorrect: widget.autocorrect,
                  offset: offset,
                  onSelectionChanged: _handleSelectionChanged,
                  onCaretChanged: _handleCaretChanged,
                  rendererIgnoresPointer: widget.rendererIgnoresPointer,
                  cursorWidth: widget.cursorWidth,
                  cursorRadius: widget.cursorRadius,
                  cursorOffset: widget.cursorOffset,
                  paintCursorAboveText: widget.paintCursorAboveText,
                  enableInteractiveSelection: widget.enableInteractiveSelection,
                  textSelectionDelegate: this,
                  devicePixelRatio: _devicePixelRatio,
                ),
              ),
            );
          },
        );
      }
    

    深入到_Editable,会发现其是一个LeafRenderObjectWidget,然后在createRenderObject我们可以找到RenderEditable
    根据问题,我们从paint方面来分析RenderEditable

    class RenderEditable extends RenderBox {
      @override
      void paint(PaintingContext context, Offset offset) {
        _layoutText(constraints.maxWidth);
        //超出边界就裁剪
        if (_hasVisualOverflow)
          context.pushClipRect(needsCompositing, offset, Offset.zero & size, _paintContents);
        else
          _paintContents(context, offset);
      }
    
    void _paintContents(PaintingContext context, Offset offset) {
        final Offset effectiveOffset = offset + _paintOffset;
    
        bool showSelection = false;
        bool showCaret = false;
    
        if (_selection != null && !_floatingCursorOn) {
          if (_selection.isCollapsed && _showCursor.value && cursorColor != null)
            showCaret = true;
          else if (!_selection.isCollapsed && _selectionColor != null)
            showSelection = true;
          _updateSelectionExtentsVisibility(effectiveOffset);
        }
    
        //绘制选择文本区域
        if (showSelection) {
          _selectionRects ??= _textPainter.getBoxesForSelection(_selection);
          _paintSelection(context.canvas, effectiveOffset);
        }
    
        // 光标在文本之上,paintCursorAboveText:ios为true
        if (paintCursorAboveText)
          _textPainter.paint(context.canvas, effectiveOffset);
    
        //绘制光标
        if (showCaret)
          _paintCaret(context.canvas, effectiveOffset, _selection.extent);
    
        //文本覆盖光标,paintCursorAboveText: android为false
        if (!paintCursorAboveText)
          _textPainter.paint(context.canvas, effectiveOffset);
    
        if (_floatingCursorOn) {
          if (_resetFloatingCursorAnimationValue == null)
            _paintCaret(context.canvas, effectiveOffset, _floatingCursorTextPosition);
          _paintFloatingCaret(context.canvas, _floatingCursorOffset);
        }
      }
    }
    
      void _paintCaret(Canvas canvas, Offset effectiveOffset, TextPosition textPosition) {
        //改变画笔颜色_cursorColor(光标呼吸主要是通过这个颜色的透明度实现)
        final Paint paint = Paint()
          ..color = _floatingCursorOn ? backgroundCursorColor : _cursorColor;
       //省略部分代码 
       ...
    
        caretRect = caretRect.shift(_getPixelPerfectCursorOffset(caretRect));
        if (cursorRadius == null) {
         //默认矩形光标
          canvas.drawRect(caretRect, paint);
        } else {
          //可以使用自定义的圆角光标
          final RRect caretRRect = RRect.fromRectAndRadius(caretRect, cursorRadius);
          canvas.drawRRect(caretRRect, paint);
        }
    
        if (caretRect != _lastCaretRect) {
          _lastCaretRect = caretRect;
          if (onCaretChanged != null)
            onCaretChanged(caretRect);
        }
      }
    

    光标呼吸闪烁主要是通过动画,控制_cursorColor改变透明度,通过Timer.periodic循环调用动画控制器

    最后说明

    你并不能依靠本文来手写一个完整的TextField,因为还有很多细节部分并没有介绍,这里主要是能明白一些功能的一个大致的流程。
    关于聚焦问题就留到下一篇分析了

    相关文章

      网友评论

        本文标题:Flutter源码解析-TextField (1)

        本文链接:https://www.haomeiwen.com/subject/uapilctx.html