說明
本文源碼基于flutter 1.7.8
之前我們分析完文本,現(xiàn)在來分析一下輸入框
使用
關于怎么使用,這里不做過多的介紹了
推薦看一下:Flutter TextField詳解
介紹的還是蠻詳細的
問題
- 值的監(jiān)聽和變化是怎么實現(xiàn)的
- 限制輸入字數(shù)的效果是怎么實現(xiàn)的
- 長按出現(xiàn)的復制、粘貼提示是怎么實現(xiàn)的
- 光標呼吸是如何實現(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é)部分并沒有介紹,這里主要是能明白一些功能的一個大致的流程。
關于聚焦問題就留到下一篇分析了