问题描述
嗨,我一直在互联网上搜索如何创建重做和撤消按钮并将它们连接到颤动的 TextField,但到目前为止我还没有找到任何东西。我希望有人知道如何做到这一点,希望得到您的帮助。
解决方法
您可以查看 undo 或 replay_bloc 包。
或者,您可以尝试在自己的项目中实现该功能并根据您的特定要求对其进行微调。
这是该功能的实现草案。
支持撤销、重做和重置。
我使用了以下软件包:
- Flutter Hooks,作为 StatefulWidgets 的替代方案
- Hooks Riverpod,用于状态管理
- Freezed,用于不变性
- Easy Debounce,谴责历史变化
您将在本文末尾找到完整的源代码。但是,这里有一些重要的亮点:
解决方案的结构:
-
应用
封装在 Riverpod
MaterialApp
中的ProviderScope
-
HomePage
维护全局状态的
HookWidget
:所选引用的uid
和editing
,无论我们是否显示表单。 -
QuoteView
所选报价的非常基本的显示。
-
QuoteForm
此表单用于修改所选报价。在(重新)构建表单之前,我们检查引用是否已更改(这发生在撤消/重置/重做之后),如果是,我们将重置已更改字段的值(和光标位置)。
-
UndoRedoResetWidget
这个小部件提供了三个按钮来触发我们的`pendingQuoteProvider 上的撤消/重置和重做。撤消和重做按钮还会显示可用的撤消和重做次数。
-
pendingQuoteProvider
这是一个家庭 StateNotifierProvider(有关家庭提供者的更多信息,请查看 here),它使跟踪每个报价的更改变得容易和简单。即使您从一个引用导航到另一个引用并返回,它甚至可以保留跟踪的更改。您还将看到,在我们的
PendingQuoteNotifier
中,我将更改去抖动 500 毫秒以减少报价历史记录中的状态数。 -
PendingQuoteModel
这是我们的
pendingQuoteProvider
的状态模型。它由List<Quote> history
和index
组成,表示历史上的当前位置。 -
Quote
我们行情的基础类,由
uid
、text
、author
和year
组成。
完整源代码
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:easy_debounce/easy_debounce.dart';
part '66288827.undo_redo.freezed.dart';
// APP
void main() {
runApp(
ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,title: 'Undo/Reset/Redo Demo',home: HomePage(),),);
}
// HOMEPAGE
class HomePage extends HookWidget {
@override
Widget build(BuildContext context) {
final selected = useState(quotes.keys.first);
final editing = useState(false);
return Scaffold(
body: SingleChildScrollView(
child: Container(
padding: EdgeInsets.all(16.0),alignment: Alignment.center,child: Column(
children: [
Wrap(
children: quotes.keys
.map((uid) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,vertical: 2.0,child: ChoiceChip(
label: Text(uid),selected: selected.value == uid,onSelected: (_) => selected.value = uid,))
.toList(),const Divider(),ConstrainedBox(
constraints: BoxConstraints(maxWidth: 250),child: QuoteView(uid: selected.value),if (editing.value)
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 250),child: QuoteForm(uid: selected.value),const SizedBox(height: 16.0),ElevatedButton(
onPressed: () => editing.value = !editing.value,child: Text(editing.value ? 'CLOSE' : 'EDIT'),)
],);
}
}
// VIEW
class QuoteView extends StatelessWidget {
final String uid;
const QuoteView({Key key,this.uid}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,children: [
Text('“${quotes[uid].text}”',textAlign: TextAlign.left),Text(quotes[uid].author,textAlign: TextAlign.right),Text(quotes[uid].year,],);
}
}
// FORM
class QuoteForm extends HookWidget {
final String uid;
const QuoteForm({Key key,this.uid}) : super(key: key);
@override
Widget build(BuildContext context) {
final quote = useProvider(
pendingQuoteProvider(uid).state.select((state) => state.current));
final quoteController = useTextEditingController();
final authorController = useTextEditingController();
final yearController = useTextEditingController();
useEffect(() {
if (quoteController.text != quote.text) {
quoteController.text = quote.text;
quoteController.selection =
TextSelection.collapsed(offset: quote.text.length);
}
if (authorController.text != quote.author) {
authorController.text = quote.author;
authorController.selection =
TextSelection.collapsed(offset: quote.author.length);
}
if (yearController.text != quote.year) {
yearController.text = quote.year;
yearController.selection =
TextSelection.collapsed(offset: quote.year.length);
}
return;
},[quote]);
return Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,children: [
UndoRedoResetWidget(uid: uid),TextFormField(
decoration: InputDecoration(
labelText: 'Quote',controller: quoteController,keyboardType: TextInputType.multiline,maxLines: null,onChanged: (value) =>
context.read(pendingQuoteProvider(uid)).updateText(value),TextFormField(
decoration: InputDecoration(
labelText: 'Author',controller: authorController,onChanged: (value) =>
context.read(pendingQuoteProvider(uid)).updateAuthor(value),TextFormField(
decoration: InputDecoration(
labelText: 'Year',controller: yearController,onChanged: (value) =>
context.read(pendingQuoteProvider(uid)).updateYear(value),);
}
}
// UNDO / RESET / REDO
class UndoRedoResetWidget extends HookWidget {
final String uid;
const UndoRedoResetWidget({Key key,this.uid}) : super(key: key);
@override
Widget build(BuildContext context) {
final pendingQuote = useProvider(pendingQuoteProvider(uid).state);
return Row(
mainAxisAlignment: MainAxisAlignment.end,children: [
_Button(
iconData: Icons.undo,info: pendingQuote.hasUndo ? pendingQuote.nbUndo.toString() : '',disabled: !pendingQuote.hasUndo,alignment: Alignment.bottomLeft,onPressed: () => context.read(pendingQuoteProvider(uid)).undo(),_Button(
iconData: Icons.refresh,onPressed: () => context.read(pendingQuoteProvider(uid)).reset(),_Button(
iconData: Icons.redo,info: pendingQuote.hasRedo ? pendingQuote.nbRedo.toString() : '',disabled: !pendingQuote.hasRedo,alignment: Alignment.bottomRight,onPressed: () => context.read(pendingQuoteProvider(uid)).redo(),);
}
}
class _Button extends StatelessWidget {
final IconData iconData;
final String info;
final Alignment alignment;
final bool disabled;
final VoidCallback onPressed;
const _Button({
Key key,this.iconData,this.info = '',this.alignment = Alignment.center,this.disabled = false,this.onPressed,}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,child: Stack(
children: [
Container(
width: 24 + alignment.x.abs() * 6,height: 24,decoration: BoxDecoration(
color: Colors.black12,border: Border.all(
color: Colors.black54,// red as border color
),borderRadius: BorderRadius.only(
topLeft: Radius.circular(alignment.x == -1 ? 10.0 : 0.0),topRight: Radius.circular(alignment.x == 1 ? 10.0 : 0.0),bottomRight: Radius.circular(alignment.x == 1 ? 10.0 : 0.0),bottomLeft: Radius.circular(alignment.x == -1 ? 10.0 : 0.0),Positioned.fill(
child: Align(
alignment: Alignment(alignment.x * -.5,0),child: Icon(
iconData,size: 12,color: disabled ? Colors.black38 : Colors.lightBlue,Positioned.fill(
child: Align(
alignment: Alignment(alignment.x * .4,.8),child: Text(
info,style: TextStyle(fontSize: 6,color: Colors.black87),).showCursorOnHover(
disabled ? SystemMouseCursors.basic : SystemMouseCursors.click);
}
}
// PROVIDERS
final pendingQuoteProvider =
StateNotifierProvider.family<PendingQuoteNotifier,String>(
(ref,uid) => PendingQuoteNotifier(quotes[uid]));
class PendingQuoteNotifier extends StateNotifier<PendingQuoteModel> {
PendingQuoteNotifier(Quote initialValue)
: super(PendingQuoteModel().afterUpdate(initialValue));
void updateText(String value) {
EasyDebounce.debounce('quote_${state.current.uid}_text',kDebounceDuration,() {
state = state.afterUpdate(state.current.copyWith(text: value));
});
}
void updateAuthor(String value) {
EasyDebounce.debounce(
'quote_${state.current.uid}_author',() {
state = state.afterUpdate(state.current.copyWith(author: value));
});
}
void updateYear(String value) {
EasyDebounce.debounce('quote_${state.current.uid}_year',() {
state = state.afterUpdate(state.current.copyWith(year: value));
});
}
void undo() => state = state.afterUndo();
void reset() => state = state.afterReset();
void redo() => state = state.afterRedo();
}
// MODELS
@freezed
abstract class Quote with _$Quote {
const factory Quote({String uid,String author,String text,String year}) =
_Quote;
}
@freezed
abstract class PendingQuoteModel implements _$PendingQuoteModel {
factory PendingQuoteModel({
@Default(-1) int index,@Default([]) List<Quote> history,}) = _PendingModel;
const PendingQuoteModel._();
Quote get current => index >= 0 ? history[index] : null;
bool get hasUndo => index > 0;
bool get hasRedo => index < history.length - 1;
int get nbUndo => index;
int get nbRedo => history.isEmpty ? 0 : history.length - index - 1;
PendingQuoteModel afterUndo() => hasUndo ? copyWith(index: index - 1) : this;
PendingQuoteModel afterReset() => hasUndo ? copyWith(index: 0) : this;
PendingQuoteModel afterRedo() => hasRedo ? copyWith(index: index + 1) : this;
PendingQuoteModel afterUpdate(Quote newValue) => newValue != current
? copyWith(
history: [...history.sublist(0,index + 1),newValue],index: index + 1)
: this;
}
// EXTENSIONS
extension HoverExtensions on Widget {
Widget showCursorOnHover(
[SystemMouseCursor cursor = SystemMouseCursors.click]) {
return MouseRegion(cursor: cursor,child: this);
}
}
// CONFIG
const kDebounceDuration = Duration(milliseconds: 500);
// DATA
final quotes = {
'q_5374': Quote(
uid: 'q_5374',text: 'Always pass on what you have learned.',author: 'Minch Yoda',year: '3 ABY','q_9534': Quote(
uid: 'q_9534',text: "It’s a trap!",author: 'Admiral Ackbar',year: "2 BBY",'q_9943': Quote(
uid: 'q_9943',text: "It’s not my fault.",author: 'Han Solo',year: '7 BBY',};