css 颤动
颤振教程 (Flutter Tutorial)
In the following tutorial, we‘re going to build a simplified version of the text editor used in the ‘New Story’ section of the Medium mobile app. Afterward, your app should look like this:
在下面的教程中,我们将构建中型移动应用程序的“新故事”部分中使用的文本编辑器的简化版本。 之后,您的应用应如下所示:
![Image for post](https://miro.medium.com/freeze/max/9999/1*JBhYe-LjKF2052dtsbz7uQ.gif)
对中型应用程序中“新故事”部分的简短分析 (A short analysis of the ‘New Story’ Section in the Medium App)
If you navigate to the ‘New story’ section in the Medium app, you’ll notice that the keyboard toolbar is apparent at the bottom of the screen even if the keyboard is not visible. When the keyboard appears, the toolbar moves up to display itself above the keyboard. If you create two text blocks, you’ll see that trying to select text from multiple blocks will fail. You can only do so for one text block at a time. Therefore, we can see that Medium probably uses a ListView
of TextFields
with different stylings.
如果您导航到“中型”应用程序中的“新故事”部分,则您会注意到,即使键盘不可见,键盘工具栏也会显示在屏幕底部。 出现键盘时,工具栏将向上移动以在键盘上方显示自己。 如果创建两个文本块,您将看到尝试从多个块中选择文本将失败。 您一次只能对一个文本块执行此操作。 因此,我们可以看到Medium可能使用具有不同样式的TextFields
的ListView
。
开始之前 (Before Getting Started)
First, lets’s create a new Flutter project
首先,让我们创建一个新的Flutter项目
flutter create medium_text_editor
Now, we add the following packages to the pubspec.yaml
inside the dependencies:
现在,我们将以下包添加到dependencies:
pubspec.yaml
内的pubspec.yaml
dependencies:
provider: ^4.1.3
community_material_icon: ^5.3.45
keyboard_visibility: ^0.5.6
and install the packages with
并安装软件包
flutter packages get
第1部分:使用键盘工具栏设置文本格式 (Part 1: Text Formatting using a Keyboard Toolbar)
Keep it simple and focus on what matters.Don’t let yourself be overwhelmed.- Confucius -
保持简单,专注于重要事情。不要让自己不知所措。-Kong子-
In the first part, we will keep the implementation as simple as possible. We will focus on displaying a keyboard toolbar that changes the text formatting of a text field on button clicks.
在第一部分中,我们将使实现尽可能简单。 我们将重点显示键盘工具栏,该工具栏可在单击按钮时更改文本字段的文本格式。
文字格式 (Text Formatting)
We will only implement headings, plain text, bullet points, and quotes as text formatting options. Let’s specify the text style and padding for each option.
我们仅将标题,纯文本,项目符号和引号用作文本格式设置选项。 让我们为每个选项指定文本样式和填充。
Note: We used the Unicode character \u2002
as a prefix for the bullet points.
注意: 我们使用Unicode字符 \u2002
作为项目符号的前缀。
可自定义的键盘工具栏 (Customizable Keyboard Toolbar)
Next, we will build the keyboard toolbar that we will display above the keyboard. The Toolbar is just a Row
of IconButtons
that set the selected formatting type on a click.
接下来,我们将构建键盘工具栏,该工具栏将显示在键盘上方。 工具栏只是一Row
IconButtons
,可在单击时设置选定的格式类型。
Note: We won’t use the keyboard_actions package as it is not very flexible and leads to lots of weird implementations.
注意: 我们不会使用 keyboard_actions 包,因为它不是很灵活,并且会导致很多奇怪的实现。
连接键盘工具栏和文本格式 (Connecting the Keyboard Toolbar and Text Formatting)
For now, state management is quite easy and only requires to update the selected text type. Since it will become more complicated in the second part, we’ll use the Provider
framework for simplicity.
目前,状态管理非常简单,只需要更新所选的文本类型即可。 由于第二部分将变得更加复杂,因此我们将使用Provider
框架来简化操作。
Note: The EditorProvider
notifier will become more useful later on.
注 : EditorProvider
通知将变得更加有用以后。
Now that we defined our ChangeNotifier
, we need to initialize our state using theChangeNotifierProvider.
现在我们定义了ChangeNotifier
,我们需要使用ChangeNotifierProvider.
初始化状态ChangeNotifierProvider.
To display the toolbar above the keyboard, we simply use Stack
and position our SmartTextField
with bottom: 56
and our Toolbar
with bottom: 0.
The resizeToAvoidBottomInset
property of Scaffold
will automatically adjust the layout if a keyboard is visible so that the toolbar is displayed above.
要在键盘上方显示工具栏,我们只需使用Stack
并将SmartTextField
的bottom: 56
和Toolbar
的bottom: 0.
resizeToAvoidBottomInset
。如果可见键盘,则Scaffold
的resizeToAvoidBottomInset
属性将自动调整布局,以便工具栏显示在上方。
风格很重要 (Style Matters)
Since it’s always more pleasant to code if things don’t look like shit, we modify the theme in the main.dart
.
因为如果事情看起来不怎么样,编码总是更令人愉快,所以我们在main.dart
修改了主题。
Ta-da🎉
塔达🎉
![Image for post](https://miro.medium.com/freeze/max/9999/1*rdQcOFmo5yGKpd8ed9L7KQ.gif)
第2部分:从单行到全文编辑器 (Part 2: Going from Single-Line to a Full-Text Editor)
Now that we implemented the single TextField
case, it is time to build a fully functional text editor.
现在,我们实现了单个TextField
情况,是时候构建一个功能齐全的文本编辑器了。
丰富国家管理 (Enriching the State Management)
As mentioned before, we need a ListView
of TextFields
. Therefore, we will store three lists in the EditorProvider
:
如前所述,我们需要一个TextFields
的ListView
。 因此,我们将三个列表存储在EditorProvider
:
List<SmartTextType>
contains the formatting style for each blockList<SmartTextType>
包含每个块的格式样式List<TextEditingController>
contains the controllers for each block that we can use to listen to user inputList<TextEditingController>
包含每个块的控制器,我们可以使用它们来监听用户输入List<FocusNode>
contains aFocusNode
for each block that we will use to manage the focus of theTextFields
List<FocusNode>
包含每个块的FocusNode
,我们将使用它们管理TextFields
的焦点
For simplicity and readability, we create the following five getters that we will use at multiple parts in the code:
为了简单起见,我们创建了以下五个getter,将在代码的多个部分使用它们:
length: returns the number of text blocks
length:返回文本块的数量
focus: returns the index of the block that has the current focus
focus:返回具有当前焦点的块的索引
nodeAt: returns the
FocusNode
at a certain indexnodeAt:在特定索引处返回
FocusNode
textAt: returns the
TextEditingController
at a certain indextextAt:在特定索引处返回
TextEditingController
typeAt: returns the
SmartTextType
at a certain indextypeAt:在特定索引处返回
SmartTextType
Additionally, we will add a setFocus()
method to update the keyboard toolbar when the user changes the focus to another text block and an insert()
method to add a new text block.
另外,当用户将焦点更改到另一个文本块时,我们将添加setFocus()
方法来更新键盘工具栏,并添加insert()
方法来添加新的文本块。
Ok, let’s update our EditorProvider
using the following implementation:
好的,让我们使用以下实现更新我们的EditorProvider
:
Now that we changed the EditorProvider
to store a list of text blocks, we need to update our TextEditor
to display a list of SmartTextFields
. To do so, go to the implementation of the TextEditor
, and at line 31 replace the Consumer
with the following code.
现在,我们更改了EditorProvider
来存储文本块列表,我们需要更新TextEditor
以显示SmartTextFields
列表。 为此,转到TextEditor
的实现,并在第31行用以下代码替换Consumer
。
Consumer<EditorProvider>(
builder: (context, state, _) {
return ListView.builder(
itemCount: state.length,
itemBuilder: (context, index) {
return Focus(
onFocusChange: (hasFocus) {
if (hasFocus) state.setFocus(state.typeAt(index));
},
child: SmartTextField(
type: state.typeAt(index),
controller: state.textAt(index),
focusNode: state.nodeAt(index),
)
);
}
);
}
)
Note: We used the Focus
widget to easily listen to focus changes and update the selectedType
of the EditorProvider
注意: 我们使用的 Focus
窗口小部件轻松收听集中修改和更新 selectedType
中的 EditorProvider
创建和删除文本块 (Creating & Deleting Text Blocks)
Okay, now that we updated our state management and text editor to store and display a list of text blocks, we need to implement the logic of creating and deleting blocks. If the user presses enter, we need to create a new TextField
and switch the focus to it. If the user presses the backspace key to remove a text block, we need to erase the text or merge it with the text block above.
好的,现在我们更新了状态管理和文本编辑器以存储和显示文本块列表,我们需要实现创建和删除块的逻辑。 如果用户按下Enter键,我们需要创建一个新的TextField
并将焦点切换到它。 如果用户按退格键删除文本块,则需要擦除文本或将其与上方的文本块合并。
We will first go over the logic and code snippets on how to handle the two events. Afterward, I’ll show you the full code you need to copy into the insert
method of the EditorProvider
. So bear with me!
我们将首先介绍如何处理这两个事件的逻辑和代码片段。 之后,我将向您展示需要复制到EditorProvider
的insert
方法中的完整代码。 所以忍受我!
Handling the Remove Event
处理删除事件
To detect the remove event, we’ll use a small trick. We use the zero-width space Unicode character \u200B
as a reference for the start of a new line via
为了检测remove事件,我们将使用一个小技巧。 我们使用零宽度空格Unicode字符\u200B
作为参考,通过以下行开始新行
final TextEditingController controller = TextEditingController(
text: '\u200B' + (text?? '')
);
If the user then presses the backspace key and removes the starting character, i.e. \u200B
, we detect a remove event, delete the focused text block, and move the focus to the text block above.
如果用户然后按下Backspace键并删除开始字符,即\u200B
,则我们将检测到remove事件,删除已聚焦的文本块,然后将焦点移至上方的文本块。
if (!controller.text.startsWith('\u200B')) {
final int index = _text.indexOf(controller);
if (index > 0) {
textAt(index-1).text += controller.text;
nodeAt(index-1).requestFocus();
_text.removeAt(index);
_nodes.removeAt(index);
_types.removeAt(index);
}
}
Note: We concatenate the text of the two text blocks via += controller.text
so that the user can easily merge two separate blocks into one.
注意: 我们通过 += controller.text
连接两个文本块的文本, 以便用户可以轻松地将两个单独的块合并为一个。
Handling the Enter Event
处理Enter事件
Next, we need to detect when the user pressed the Enter key. Since our TextFields
use the multiline keyboard type (i.e. keyboardType: TextInputType.multiline
), we can check if the TextEditingController
contains the Unicode character \n
which represents a line break. If it does, we gonna split the text and move the part after the \n
character to the next text block that we create on-the-go. This approach is very important, as it allows the user to split an existing text block into two parts. Here you can see the implementation of the logic described above.
接下来,我们需要检测用户何时按下Enter键。 由于我们的TextFields
使用多行键盘类型(即keyboardType: TextInputType.multiline
),因此我们可以检查TextEditingController
包含表示换行符的Unicode字符\n
。 如果是这样,我们将分割文本并将\n
字符之后的部分移动到我们在移动中创建的下一个文本块中。 这种方法非常重要,因为它允许用户将现有的文本块分为两部分。 在这里,您可以看到上述逻辑的实现。
if(controller.text.contains('\n')) {
final int index = _text.indexOf(controller);
List<String> _split = controller.text.split('\n');
controller.text = _split.first;
insert(
index: index+1,
text: _split.last
);
nodeAt(index+1).requestFocus();
}
When pressing enter on a bullet point, it is very intuitive that another one appears below. So, we modify the call of the insert
function a bit, as you can see in the following snippet.
在项目符号点上按Enter键时,很直观地在下面显示另一个。 因此,我们对insert
函数的调用进行了一些修改,如下面的代码片段所示。
insert(
index: index+1,
text: _split.last,
type: typeAt(index) == SmartTextType.BULLET
? SmartTextType.BULLET
: SmartTextType.T
);
The Implementation
实施
Combining the above steps leads us to the following implementation of the TextEditingController
and Listener
that we can paste into the beginning of the insert
function of the EditorProvider.
结合上述步骤,我们可以实现TextEditingController
和Listener
的以下实现,可以将其粘贴到EditorProvider.
的insert
函数的EditorProvider.
隐藏键盘工具栏 (Hiding the Keyboard Toolbar)
While we only implemented a simplified version of the text editor from the Medium mobile app, there is one thing that we can easily improve. In the medium app, the toolbar is visible at the bottom even if the keyboard is not. This is certainly not a problem, but from a design point of view, not very appealing. But good news, it is easy to fix using the keyboard_visibility package.
虽然我们仅从Medium移动应用程序中实现了文本编辑器的简化版本,但我们可以轻松改进一件事。 在中型应用程序中,即使没有键盘,工具栏也位于底部。 这当然不是问题,但是从设计的角度来看并不是很吸引人。 但好消息是,使用keyboard_visibility包很容易修复。
First, import the keyboard_visibility package to text_editor.dart
via
首先,通过以下方式将keyboard_visibility包导入到text_editor.dart
import 'package:keyboard_visibility/keyboard_visibility.dart';
and then add the following to _TextEditorState
:
然后将以下内容添加到_TextEditorState
:
bool showToolbar = false;@override
void initState() {
super.initState();
KeyboardVisibilityNotification().addNewListener(
onChange: (isVisible) {
setState(() {
showToolbar = isVisible;
});
},
);
}@override
void dispose() {
KeyboardVisibilityNotification().dispose();
super.dispose();
}
Then, also in _TextEditorState
, wrap the second Positioned
widget with the following if-statement:
然后,也在_TextEditorState
,使用以下if语句包装第二个Positioned
小部件:
if (showToolbar) Positioned(
bottom: 0,
...
And, that’s it!
而且,就是这样!
翻译自: https://levelup.gitconnected.com/flutter-medium-like-text-editor-b41157f50f0e
css 颤动