// // Tencent is pleased to support the open source community by making // Hippy available. // // Copyright (C) 2022 THL A29 Limited, a Tencent company. // All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import 'package:flutter/material.dart'; import '../common.dart'; import '../controller.dart'; import '../render.dart'; import '../style.dart'; import '../util.dart'; import '../viewmodel.dart'; import '../widget.dart'; class TextInputController extends BaseViewController { static const String kClassName = "TextInput"; static const String kTag = "TextInputController"; static const String kEventOnFocus = "focus"; static const String kEventOnBlur = "blur"; static const String kEventOnChangeText = "changetext"; static const String kEventOnKeyboardWillShow = "keyboardwillshow"; static const String kEventOnKeyboardWillHide = "keyboardwillhide"; static const String kEventOnEndEditing = "endediting"; static const String kEventOnSelectionChange = "selectionchange"; /// func static const String kFuncClear = "clear"; static const String kFuncFocus = "focusTextInput"; static const String kFuncBlur = "blurTextInput"; static const String kFuncGetValue = "getValue"; static const String kFuncSetValue = "setValue"; // 注意这里由于js侧使用了错误拼写,这里暂时不能修改成正确的拼写,否则会导致无法成功调用 static const String kFuncDismiss = "dissmiss"; @override TextInputRenderViewModel createRenderViewModel(RenderNode node, RenderContext context) { return TextInputRenderViewModel(node.id, node.rootId, name, context); } @override Widget createWidget(BuildContext context, TextInputRenderViewModel viewModel) { return TextInputWidget(viewModel); } @override Map get extendRegisteredMethodProp => { /// props NodeProps.kFontSize: ControllerMethodProp(setFontSize, NodeProps.kDefaultFontSizeSp), NodeProps.kDefaultValue: ControllerMethodProp(setDefaultValue, ""), NodeProps.kValidator: ControllerMethodProp(setValidator, ""), NodeProps.kEditable: ControllerMethodProp(setEditable, true), NodeProps.kCaretColorReact: ControllerMethodProp(setCaretColor, Colors.transparent.value), NodeProps.kCaretColorVue: ControllerMethodProp(setCaretColorAlias, Colors.transparent.value), NodeProps.kMultiline: ControllerMethodProp(multiLine, true), NodeProps.kReturnKeyType: ControllerMethodProp(setReturnKeyType, ""), NodeProps.kKeyboardType: ControllerMethodProp(setKeyboardType, ""), NodeProps.kFontStyle: ControllerMethodProp(setFontStyle, "normal"), NodeProps.kFontFamily: ControllerMethodProp(setFontFamily, "normal"), NodeProps.kFontWeight: ControllerMethodProp(setFontWeight, "normal"), NodeProps.kLineHeight: ControllerMethodProp(setLineHeight, 0.0), NodeProps.kMaxLengthProp: ControllerMethodProp(maxLength, -1), NodeProps.kOnSelectionChange: ControllerMethodProp(setOnSelectionChange, false), NodeProps.kLetterSpacing: ControllerMethodProp(letterSpacing, -1.0), NodeProps.kValueProp: ControllerMethodProp(value, ""), NodeProps.kPlaceholder: ControllerMethodProp(placeHolder, ""), NodeProps.kPlaceholderTextColorVue: ControllerMethodProp(setTextHitColorVue, 0xFF888888), NodeProps.kPlaceholderTextColorReact: ControllerMethodProp(setTextHitColorReact, 0xFF888888), NodeProps.kNumberOfLines: ControllerMethodProp(setMaxLines, kMaxLineCount), NodeProps.kUnderlineColor: ControllerMethodProp(setUnderlineColor, Colors.transparent.value), NodeProps.kColor: ControllerMethodProp(setColor, Colors.transparent.value), NodeProps.kTextAlign: ControllerMethodProp(setTextAlign, "auto"), NodeProps.kTextAlignVertical: ControllerMethodProp(setTextAlignVertical, "auto"), NodeProps.kPaddingTop: ControllerMethodProp(setPaddingTop, 0.0), NodeProps.kPaddingRight: ControllerMethodProp(setPaddingRight, 0.0), NodeProps.kPaddingBottom: ControllerMethodProp(setPaddingBottom, 0.0), NodeProps.kPaddingLeft: ControllerMethodProp(setPaddingLeft, 0.0), // listen NodeProps.kOnChangeText: ControllerMethodProp(setOnChangeText, true), NodeProps.kOnEndEditing: ControllerMethodProp(setOnEndEditing, true), NodeProps.kOnFocus: ControllerMethodProp(setOnFocus, true), NodeProps.kOnBlur: ControllerMethodProp(setOnBlur, true), NodeProps.kOnContentSizeChange: ControllerMethodProp(setOnContentSizeChange, true), NodeProps.kOnKeyboardWillShow: ControllerMethodProp(setOnKeyboardWillShow, true), NodeProps.kOnKeyboardWillHide: ControllerMethodProp(setOnKeyboardWillHide, true), }; @override String get name => kClassName; @ControllerProps(NodeProps.kFontSize) void setFontSize(TextInputRenderViewModel renderViewModel, double fontSize) { renderViewModel.fontSize = fontSize; } @ControllerProps(NodeProps.kDefaultValue) void setDefaultValue(TextInputRenderViewModel renderViewModel, String defaultValue) { renderViewModel.setValue(defaultValue); } @ControllerProps(NodeProps.kValidator) void setValidator(TextInputRenderViewModel renderViewModel, String strValidator) { renderViewModel.dispatcher.validator = strValidator; } @ControllerProps(NodeProps.kEditable) void setEditable(TextInputRenderViewModel renderViewModel, bool editable) { renderViewModel.editable = editable; } // 设置输入光标颜色 RN 语法 caret-color @ControllerProps(NodeProps.kCaretColorReact) void setCaretColor(TextInputRenderViewModel renderViewModel, int cursorColor) { renderViewModel.cursorColor = cursorColor; } // 设置输入光标颜色 //For Vue.vue的前端语法,会把caret-color转化成caretColor @ControllerProps(NodeProps.kCaretColorVue) void setCaretColorAlias(TextInputRenderViewModel renderViewModel, int cursorColor) { renderViewModel.cursorColor = cursorColor; } @ControllerProps(NodeProps.kMultiline) void multiLine(TextInputRenderViewModel renderViewModel, bool multiline) { renderViewModel.setMultiLine(multiline); } @ControllerProps(NodeProps.kReturnKeyType) void setReturnKeyType(TextInputRenderViewModel renderViewModel, String returnKeyType) { renderViewModel.setTextInputType(returnKeyType); } @ControllerProps(NodeProps.kKeyboardType) void setKeyboardType(TextInputRenderViewModel renderViewModel, String keyboardType) { renderViewModel.setKeyboardType(keyboardType); } @ControllerProps(NodeProps.kFontStyle) void setFontStyle(TextInputRenderViewModel renderViewModel, String fontStyleString) { renderViewModel.setFontStyle(fontStyleString); } @ControllerProps(NodeProps.kFontWeight) void setFontWeight(TextInputRenderViewModel renderViewModel, String fontWeightString) { renderViewModel.setFontWeight(fontWeightString); } @ControllerProps(NodeProps.kLineHeight) void setLineHeight(TextInputRenderViewModel renderViewModel, double lineHeight) { renderViewModel.lineHeight = lineHeight; } @ControllerProps(NodeProps.kFontFamily) void setFontFamily(TextInputRenderViewModel renderViewModel, String fontFamily) { renderViewModel.fontFamily = fontFamily; } @ControllerProps(NodeProps.kMaxLengthProp) void maxLength(TextInputRenderViewModel renderViewModel, int maxLength) { renderViewModel.maxLength = maxLength; } @ControllerProps(NodeProps.kOnSelectionChange) void setOnSelectionChange(TextInputRenderViewModel renderViewModel, bool change) { renderViewModel.dispatcher.selectionChangeEventEnable = change; } @ControllerProps(NodeProps.kLetterSpacing) void letterSpacing(TextInputRenderViewModel renderViewModel, double letterSpacing) { renderViewModel.letterSpacing = letterSpacing; } @ControllerProps(NodeProps.kValueProp) void value(TextInputRenderViewModel renderViewModel, String value) { renderViewModel.setValue(value); } @ControllerProps(NodeProps.kPlaceholder) void placeHolder(TextInputRenderViewModel renderViewModel, String placeholder) { renderViewModel.hint = placeholder; } @ControllerProps(NodeProps.kPlaceholderTextColorVue) void setTextHitColorVue(TextInputRenderViewModel renderViewModel, int color) { renderViewModel.hintTextColor = color; } @ControllerProps(NodeProps.kPlaceholderTextColorReact) void setTextHitColorReact(TextInputRenderViewModel renderViewModel, int color) { renderViewModel.hintTextColor = color; } @ControllerProps(NodeProps.kNumberOfLines) void setMaxLines(TextInputRenderViewModel renderViewModel, int numberOfLine) { renderViewModel.numberOfLine = numberOfLine; } @ControllerProps(NodeProps.kUnderlineColor) void setUnderlineColor(TextInputRenderViewModel renderViewModel, int underlineColor) { renderViewModel.underLineColor = underlineColor; } @ControllerProps(NodeProps.kOnFocus) void setOnFocus(TextInputRenderViewModel renderViewModel, bool change) { renderViewModel.dispatcher.focusEventEnable = change; } @ControllerProps(NodeProps.kOnBlur) void setOnBlur(TextInputRenderViewModel renderViewModel, bool change) { renderViewModel.dispatcher.blurEventEnable = change; } @ControllerProps(NodeProps.kOnChangeText) void setOnChangeText(TextInputRenderViewModel renderViewModel, bool change) { renderViewModel.dispatcher.changeTextEventEnable = change; } @ControllerProps(NodeProps.kOnContentSizeChange) void setOnContentSizeChange(TextInputRenderViewModel renderViewModel, bool contentSizeChange) { renderViewModel.dispatcher.contentSizeChangeEventEnable = contentSizeChange; } @ControllerProps(NodeProps.kOnKeyboardWillShow) void setOnKeyboardWillShow(TextInputRenderViewModel renderViewModel, bool change) { renderViewModel.dispatcher.keyboardWillShowEventEnable = change; } @ControllerProps(NodeProps.kOnKeyboardWillShow) void setOnKeyboardWillHide(TextInputRenderViewModel renderViewModel, bool change) { renderViewModel.dispatcher.keyboardWillHideEventEnable = change; } @ControllerProps(NodeProps.kOnEndEditing) void setOnEndEditing(TextInputRenderViewModel renderViewModel, bool change) { renderViewModel.dispatcher.endEditingEventEnable = change; } @ControllerProps(NodeProps.kColor) void setColor(TextInputRenderViewModel renderViewModel, int change) { renderViewModel.textColor = change; } @ControllerProps(NodeProps.kTextAlign) void setTextAlign(TextInputRenderViewModel renderViewModel, String textAlign) { renderViewModel.setTextAlign(textAlign); } @ControllerProps(NodeProps.kTextAlignVertical) void setTextAlignVertical(TextInputRenderViewModel renderViewModel, String? textAlignVertical) { if (textAlignVertical == null || "auto" == textAlignVertical) { renderViewModel.textAlignVertical = null; } else if ("top" == textAlignVertical) { renderViewModel.textAlignVertical = TextAlignVertical.top; } else if ("bottom" == textAlignVertical) { renderViewModel.textAlignVertical = TextAlignVertical.bottom; } else if ("center" == textAlignVertical) { renderViewModel.textAlignVertical = TextAlignVertical.center; } } @ControllerProps(NodeProps.kPaddingTop) void setPaddingTop(TextInputRenderViewModel renderViewModel, Object? paddingTop) { if (paddingTop is int) { renderViewModel.paddingTop = paddingTop.toDouble(); } else if (paddingTop is double) { renderViewModel.paddingTop = paddingTop; } else { renderViewModel.paddingTop = 0.0; } } @ControllerProps(NodeProps.kPaddingRight) void setPaddingRight(TextInputRenderViewModel renderViewModel, Object? paddingRight) { if (paddingRight is int) { renderViewModel.paddingRight = paddingRight.toDouble(); } else if (paddingRight is double) { renderViewModel.paddingRight = paddingRight; } else { renderViewModel.paddingRight = 0.0; } } @ControllerProps(NodeProps.kPaddingBottom) void setPaddingBottom(TextInputRenderViewModel renderViewModel, Object? paddingBottom) { if (paddingBottom is int) { renderViewModel.paddingBottom = paddingBottom.toDouble(); } else if (paddingBottom is double) { renderViewModel.paddingBottom = paddingBottom; } else { renderViewModel.paddingBottom = 0.0; } } @ControllerProps(NodeProps.kPaddingLeft) void setPaddingLeft(TextInputRenderViewModel renderViewModel, Object? paddingLeft) { if (paddingLeft is int) { renderViewModel.paddingLeft = paddingLeft.toDouble(); } else if (paddingLeft is double) { renderViewModel.paddingLeft = paddingLeft; } else { renderViewModel.paddingLeft = 0.0; } } @override void updateEvents( TextInputRenderViewModel renderViewModel, Set holders, ) { super.updateEvents(renderViewModel, holders); if (holders.isNotEmpty) { for (var holder in holders) { switch (holder.eventName) { case kEventOnFocus: setOnFocus(renderViewModel, holder.isAdd); break; case kEventOnBlur: setOnBlur(renderViewModel, holder.isAdd); break; case kEventOnChangeText: setOnChangeText(renderViewModel, holder.isAdd); break; case kEventOnKeyboardWillShow: setOnKeyboardWillShow(renderViewModel, holder.isAdd); break; case kEventOnKeyboardWillHide: setOnKeyboardWillHide(renderViewModel, holder.isAdd); break; case kEventOnEndEditing: setOnEndEditing(renderViewModel, holder.isAdd); break; case kEventOnSelectionChange: setOnSelectionChange(renderViewModel, holder.isAdd); break; } } } } @override void dispatchFunction(TextInputRenderViewModel viewModel, String functionName, VoltronArray array, {Promise? promise}) { switch (functionName) { case kFuncSetValue: var value = array.getString(0); if (value != null) { int? pos = array.get(1) ?? value.length; LogUtils.i("text_input", "js set value: $value, $pos"); viewModel.dispatcher.jsSetValue(array.getString(0), pos); } break; case kFuncClear: viewModel.dispatcher.jsSetValue("", 0); break; case kFuncFocus: viewModel.focus(); break; case kFuncDismiss: viewModel.dismiss(); break; case kFuncBlur: viewModel.blur(); break; case kFuncGetValue: if (promise != null) { var resultMap = viewModel.dispatcher.jsGetValue(); promise.resolve(resultMap); } break; } } } class FocusNodeProxy { FocusNode? node; bool _cacheIsFocus = false; VoidCallback? _focusChangeListener; FocusNode get realNode { return node ??= FocusNode(); } void init() { node = FocusNode(); _ensureListener(); _ensureFocus(); } void addListener(VoidCallback callback) { _focusChangeListener = callback; _ensureListener(); } void _ensureListener() { var curListener = _focusChangeListener; if (curListener != null) { node?.addListener(curListener); } } void _ensureFocus() { if (_cacheIsFocus) { focus(); } } void dispose() { node = null; } void unfocus() { _cacheIsFocus = false; node?.unfocus(); } bool get hasFocus => node?.hasFocus ?? _cacheIsFocus; void focus() { _cacheIsFocus = true; node?.requestFocus(); } } class TextEditingControllerProxy { TextEditingController? controller; TextEditingValue? _cacheTextEditingValue; VoidCallback? _textEditListener; void init() { controller = _newController; _ensureListener(); } void addListener(VoidCallback? callback) { _textEditListener = callback; _ensureListener(); } void _ensureListener() { var curListener = _textEditListener; if (curListener != null) { controller?.addListener(curListener); } } TextEditingController get _newController => _cacheTextEditingValue != null ? TextEditingController.fromValue(_cacheTextEditingValue) : TextEditingController.fromValue( TextEditingValue( text: "", selection: TextSelection.fromPosition( const TextPosition(offset: 0, affinity: TextAffinity.downstream), ), ), ); TextEditingController get realController { return controller ??= _newController; } set value(TextEditingValue value) { _cacheTextEditingValue = value; _ensureTextEditing(); } TextEditingValue get value { return _cacheTextEditingValue ??= TextEditingValue( text: '', selection: TextSelection.fromPosition( const TextPosition(offset: 0, affinity: TextAffinity.downstream), ), ); } TextSelection get selection => controller?.selection ?? (_cacheTextEditingValue?.selection ?? TextSelection.fromPosition( const TextPosition(offset: 0, affinity: TextAffinity.downstream), )); String get text { var v = controller?.value; // 这里要考虑iOS原生中文输入法中存在候选词的情况,候选词不算做输入文本 if (v != null) { if (v.isComposingRangeValid) { return '${v.composing.textBefore(v.text)}${v.composing.textAfter(v.text)}'; } else { return v.text; } } return _cacheTextEditingValue?.text ?? ''; } set text(String? text) { value = value.copyWith( text: text, selection: const TextSelection.collapsed(offset: -1), composing: TextRange.empty, ); } void _ensureTextEditing() { var curVal = _cacheTextEditingValue; if (curVal != null) { controller?.value = curVal; } if (controller == null) { _textEditListener?.call(); } } void dispose() { controller = null; } void destroy() { _cacheTextEditingValue = null; } } class TextInputDispatcher { TextEditingControllerProxy controller; // 过滤输入文本的正则表达式 String validator = ""; // 事件相关 bool focusEventEnable = false; bool blurEventEnable = false; bool selectionChangeEventEnable = false; bool changeTextEventEnable = false; bool endEditingEventEnable = false; bool contentSizeChangeEventEnable = false; bool keyboardWillShowEventEnable = false; bool keyboardWillHideEventEnable = false; RenderContext context; int rootId; int id; String _oldText = ""; bool _hasTextInput = false; String _jsSetValue = ""; String _regexValidRepeat = ""; bool hasFocus = false; TextInputDispatcher(this.context, this.rootId, this.id, this.controller); void onSelectionChanged(int selStart, int selEnd) { if (selectionChangeEventEnable) { var selection = VoltronMap(); selection.push("start", selStart); selection.push("end", selEnd); var paramsMap = VoltronMap(); paramsMap.push("selection", selection); context.renderBridgeManager.sendComponentEvent( rootId, id, TextInputController.kEventOnSelectionChange, paramsMap, ); } } void onFocusChange(bool focus) { if (hasFocus != focus) { hasFocus = focus; if (focusEventEnable || blurEventEnable) { var paramsMap = VoltronMap(); paramsMap.push("text", text); if (hasFocus) { context.renderBridgeManager.sendComponentEvent( rootId, id, TextInputController.kEventOnFocus, paramsMap, ); } else { context.renderBridgeManager.sendComponentEvent( rootId, id, TextInputController.kEventOnBlur, paramsMap, ); } } } } void onEndEdit() { if (endEditingEventEnable) { var paramsMap = VoltronMap(); paramsMap.push("text", text); context.renderBridgeManager.sendComponentEvent( rootId, id, TextInputController.kEventOnEndEditing, paramsMap, ); } } void onChangeText(String changeText) { var isJsSetValue = _isJsSetValue(changeText); if (validator.isEmpty) { //如果没有正则匹配 //如果文本输入过,判断是否两次相同 if (_hasTextInput && _oldText == changeText) { return; } _oldText = changeText; _jsSetValue = ""; _hasTextInput = true; if (!isJsSetValue) { //如果是前端设置下来的值,不再需要回调给前端. var paramsMap = VoltronMap(); paramsMap.push("text", changeText); context.renderBridgeManager.sendComponentEvent( rootId, id, TextInputController.kEventOnChangeText, paramsMap, ); LogUtils.d( TextInputController.kTag, "afterTextChanged 1 通知前端文本变化=$changeText", ); } } else { //如果设置了正则表达式 try { //如果当前的内容不匹配正则表达式 if (!RegExp(validator).hasMatch(changeText) && changeText != "") { LogUtils.d( TextInputController.kTag, "afterTextChanged 不符合正则表达式,需要设置回去=$changeText", ); //丢弃当前的内容,回退到上一次的值.上一次的值检查过,肯定是符合正则表达式的. controller.value = TextEditingValue( text: _oldText, selection: TextSelection.collapsed( offset: _oldText.length, ), ); //上一步的setText,将触发新一轮的beforeTextChanged,onTextChanged,afterTextChanged //为了避免前端收到两次内容同样的通知.记录一下正则匹配设置回去的值. _regexValidRepeat = _oldText; _hasTextInput = true; } else { //如果文本输入过,判断是否两次相同 if (_hasTextInput && changeText == _oldText) { return; } _hasTextInput = true; _oldText = changeText; _jsSetValue = ""; if (!isJsSetValue //如果是前端设置的一定不通知 && (isEmpty(_regexValidRepeat) //如果没有,输入过无效的内容 || _oldText != _regexValidRepeat)) { //如果本次输入的内容是上一次重复的蓉蓉 var paramsMap = VoltronMap(); paramsMap.push("text", changeText); context.renderBridgeManager.sendComponentEvent( rootId, id, TextInputController.kEventOnChangeText, paramsMap, ); LogUtils.d( TextInputController.kTag, "afterTextChanged 2 通知前端文本变化= $changeText", ); _regexValidRepeat = ""; } } } catch (error) { // 不知道外部的正则表达式,最好保护住 LogUtils.e(TextInputController.kTag, "change text error:$error"); } } } bool _isJsSetValue(String value) { return !isEmpty(_jsSetValue) && value == _jsSetValue; } void jsSetValue(String? value, int pos) { if (value == null) { _jsSetValue = value ?? ''; controller.text = value; } else { _jsSetValue = value; var offset = (pos < 0 || pos >= value.length) ? value.length : pos; controller.value = TextEditingValue( text: _jsSetValue, selection: TextSelection.fromPosition( TextPosition( offset: offset, affinity: TextAffinity.downstream, ), ), ); // 这里设置时候也要通知到前端 var paramsMap = VoltronMap(); paramsMap.push("text", value); context.renderBridgeManager.sendComponentEvent( rootId, id, TextInputController.kEventOnChangeText, paramsMap, ); } } VoltronMap jsGetValue() { var paramsMap = VoltronMap(); paramsMap.push("text", text); return paramsMap; } void onKeyboardWillShow(int height) { if (keyboardWillShowEventEnable) { var paramsMap = VoltronMap(); paramsMap.push("keyboardHeight", height); context.renderBridgeManager.sendComponentEvent( rootId, id, TextInputController.kEventOnKeyboardWillShow, paramsMap, ); } } void onKeyboardWillHide() { if (keyboardWillShowEventEnable) { var paramsMap = VoltronMap(); context.renderBridgeManager.sendComponentEvent( rootId, id, TextInputController.kEventOnKeyboardWillHide, paramsMap, ); } } String get text => controller.text; }