Flutter源碼解析-TextField (1)

說明

本文源碼基于flutter 1.7.8
之前我們分析完文本,現(xiàn)在來分析一下輸入框

使用

關于怎么使用,這里不做過多的介紹了
推薦看一下:Flutter TextField詳解
介紹的還是蠻詳細的

問題

  1. 值的監(jiān)聽和變化是怎么實現(xiàn)的
  2. 限制輸入字數(shù)的效果是怎么實現(xiàn)的
  3. 長按出現(xiàn)的復制、粘貼提示是怎么實現(xiàn)的
  4. 光標呼吸是如何實現(xiàn)的

分析

由簡單的開始分析,逐步深入


1. 問題一:值的監(jiān)聽和變化是怎么實現(xiàn)的

我們通常通過以下方式來獲取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();

  ...
}

到這里其實很明朗了,既然能監(jiān)聽肯定是一個觀察者模式了

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  ValueNotifier(this._value);

  @override
  T get value => _value;
  T _value;
  //一旦賦值,就通知所有的監(jiān)聽器,值已經(jīng)發(fā)生變化
  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>();

  ...
 //添加單個監(jiān)聽回調(diào)方法
  @override
  void addListener(VoidCallback listener) {
    assert(_debugAssertNotDisposed());
    _listeners.add(listener);
  }

  //移出單個監(jiān)聽回調(diào)方法
  @override
  void removeListener(VoidCallback listener) {
    assert(_debugAssertNotDisposed());
    _listeners.remove(listener);
  }

  //監(jiān)聽器數(shù)組置空
  @mustCallSuper
  void dispose() {
    assert(_debugAssertNotDisposed());
    _listeners = null;
  }

  @protected
  @visibleForTesting
  void notifyListeners() {
    assert(_debugAssertNotDisposed());
    if (_listeners != null) {
      final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners);
      //遍歷循環(huán),調(diào)用回調(diào)方法
      for (VoidCallback listener in localListeners) {
        try {
          if (_listeners.contains(listener))
            listener();
        } catch (exception, stack) {
          //這個就是常見的出現(xiàn)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,
              );
            },
          ));
        }
      }
    }
  }
}

總結來說值的變化就是通過觀察者模式來作用的
下面我們看下一個重要的類,這個類影響到后續(xù)的光標顯示

@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);
  //選擇區(qū)域的左邊開始點,可簡單理解為光標位置
  //初始為-1,有值的時候,位于光標起始點為0(如: 12,光標在1前面則為0,后面就是1)
  final int baseOffset;
 //選擇區(qū)域的結束點 (如: 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. 問題二:限制輸入字數(shù)的效果是怎么實現(xiàn)的

當我們給maxLength服了值后,右下角就有個字數(shù)限制顯示
來看下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;
        //創(chuàng)建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;
        //創(chuàng)建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),
          //選擇區(qū)域文本手勢
          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)就可以實現(xiàn)了,我們繼續(xù)追蹤下去,會發(fā)現(xiàn)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);
  }

調(diào)用LengthLimitingTextInputFormatter的formatEditUpdate進行文本裁剪

class LengthLimitingTextInputFormatter extends TextInputFormatter {

  ...
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue, // unused.
    TextEditingValue newValue,
  ) {
    if (maxLength != null && maxLength > 0 && newValue.text.runes.length > maxLength) {
      //修改選擇區(qū)域,簡而言之就是修改光標位置
      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;
      //對輸入的值進行字符串裁剪,經(jīng)過上面的循環(huán),rawIndex就是value的長度或限制輸入最大值中最小的一個
      final String truncated = newValue.text.substring(0, iterator.rawIndex);
      return TextEditingValue(
        text: truncated,
        selection: newSelection,
        composing: TextRange.empty,
      );
    }
    return newValue;
  }
}

那么限制字數(shù)的繪制是在哪實現(xiàn)的呢?

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,這個計數(shù)的控件自己實現(xiàn)
    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);
    }
    //沒設置就返回一個無字數(shù)統(tǒng)計的裝飾
    if (widget.maxLength == null)
      return effectiveDecoration; // No counter widget

    String counterText = '$currentLength';
    String semanticCounterText = '';

    // 最大長度大于0
    if (widget.maxLength > 0) {
      //顯示的統(tǒng)計數(shù) 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,
    );
  }

因此,其實對應的有三種方法來解決不顯示右下角的統(tǒng)計數(shù)字,但卻限制最大字數(shù)

//textfield限制最大字數(shù),但無字數(shù)統(tǒng)計
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. 問題三: 長按出現(xiàn)的復制、粘貼提示是怎么實現(xiàn)的

工具欄

在問題二里我們看到了TextSelectionGestureDetector 手勢控件
先看onTapDown中調(diào)用的_handleTapDown,這個手勢是必然調(diào)用的(只要觸發(fā)了事件)

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事件已經(jīng)記錄了點,所以這里直接修改位置即可
          _renderEditable.selectPosition(cause: SelectionChangedCause.tap);
          break;
      }
    }
   //申請軟鍵盤
    _requestKeyboard();
    //動畫效果
    _confirmCurrentSplash();
    if (widget.onTap != null)
      widget.onTap();
  }

因為down事件已經(jīng)確認點擊位置,當up觸發(fā)后,確認這是一個單擊事件后,直接更新光標到點擊位置
之后我們看下長按選擇區(qū)域的,這里也會出現(xiàn)復制工具欄

  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:
         //設置一個區(qū)域,從坐標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();
    }
  }

當長按且左右移動時會出現(xiàn)一個選擇區(qū)域,松開手后就會顯示工具欄

void selectWordsInRange({ @required Offset from, Offset to, @required SelectionChangedCause cause }) {
    //擺放文本
    _layoutText(constraints.maxWidth);
    if (onSelectionChanged != null) {
      //絕對坐標轉(zhuǎn)相對坐標,然后計算當前坐標在文本的第幾個位置
      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) {
    //根據(jù)點返回這個字的TextRange,簡單來說就是一個轉(zhuǎn)換
    final TextRange word = _textPainter.getWordBoundary(position);
    if (position.offset >= word.end)
      return TextSelection.fromPosition(position);
    return TextSelection(baseOffset: word.start, extentOffset: word.end);
  }

確認文本選擇的區(qū)域其實靠的就是起點坐標和終點坐標
繼續(xù)看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);
  }

我們繼續(xù)剖析下去,_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個風格實現(xiàn)的原理都差不多,我們直接看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內(nèi)的build方法說白了就是一個Row布局內(nèi)放4個FlatButton控件,著重查看一下TextSelectionToolbar的四個回調(diào)方法都做了什么?

abstract class TextSelectionControls {
  //省略部分內(nèi)容
  ...

  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) {
    //舉個例子
    //文本是: "我是一個好人" , 現(xiàn)在要剪切"一個"
    final TextEditingValue value = delegate.textEditingValue;
    //往Clipboard添加剪切的數(shù)據(jù)
    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),
    ));
    //和上面一個,但是文本未發(fā)生變化,光標移動到末尾,即"一個"的后面,也就是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保存的數(shù)據(jù)
    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) {
    //文本不變,但是選擇區(qū)域變?yōu)?-文本的末尾,也就是全選
    delegate.textEditingValue = TextEditingValue(
      text: delegate.textEditingValue.text,
      selection: TextSelection(
        baseOffset: 0,
        extentOffset: delegate.textEditingValue.text.length,
      ),
    );
    delegate.bringIntoView(delegate.textEditingValue.selection.extent);
  }
}

輔助旋轉(zhuǎn)的小點是怎么繪制的:

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) {
      //左的的向右旋轉(zhuǎn)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;
      //中間的向右旋轉(zhuǎn)45°
      case TextSelectionHandleType.collapsed: // points up
        return Transform.rotate(
          angle: math.pi / 4.0,
          child: handle,
        );
    }
    assert(type != null);
    return null;
  }

至此,工具欄也分析完了。讀完了流程,其實我們也能模仿官方,然后像微信那么弄個自己的工具欄,比如說收藏或刪除什么的

4. 問題四: 光標呼吸是如何實現(xiàn)的

先說光標呼吸怎么實現(xiàn),之前我們反復說道了一個控件EditableText,再往其內(nèi)部的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這個是個重要的控件,繪制基本就在這發(fā)生了
            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,會發(fā)現(xiàn)其是一個LeafRenderObjectWidget,然后在createRenderObject我們可以找到RenderEditable
根據(jù)問題,我們從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);
    }

    //繪制選擇文本區(qū)域
    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(光標呼吸主要是通過這個顏色的透明度實現(xiàn))
    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循環(huán)調(diào)用動畫控制器

最后說明

你并不能依靠本文來手寫一個完整的TextField,因為還有很多細節(jié)部分并沒有介紹,這里主要是能明白一些功能的一個大致的流程。
關于聚焦問題就留到下一篇分析了

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373