привет, я искал в Интернете, как создать кнопки повтора и отмены и подключить их к флаттерному текстовому полю, но пока ничего не нашел. Я надеюсь, что кто-то знает, как это сделать, я надеюсь на вашу помощь.
Flutter — кнопка отмены и повтора текста
Ответы (1)
Вы можете посмотреть отменить или replay_bloc.
Или вы можете просто попробовать реализовать эту функцию в своем собственном проекте и настроить ее под свои конкретные требования.
Вот черновик реализации такой функции.
Он поддерживает отмену, повтор и сброс.
Я использовал следующие пакеты:
- Flutter Hooks в качестве альтернативы StatefulWidgets
- Hooks Riverpod, для управления состоянием
- Заблокировано для неизменности
- Easy Debounce, чтобы осудить историю изменений
Полный исходный код вы найдете в конце этого поста. но вот несколько важных моментов:
Структура решения:
Приложение
MaterialApp
в капсуле RiverpodProviderScope
HomePage
HookWidget
поддержание глобального состояния:uid
выбранной котировки иediting
независимо от того, отображаем мы форму или нет.QuoteView
Очень простое отображение выбранной котировки.
QuoteForm
Эта форма используется для изменения выбранной котировки. Перед (повторным) построением формы мы проверяем, была ли изменена кавычка (это происходит после отмены/сброса/повторения), и если да, то сбрасываем значения (и положение курсора) изменившихся полей.
UndoRedoResetWidget
Этот виджет предоставляет три кнопки для запуска отмены/сброса и повтора в нашем `pendingQuoteProvider. Кнопки отмены и повтора также отображают количество доступных отмен и повторов.
pendingQuoteProvider
Это семейство StateNotifierProvider (см. здесь для получения дополнительной информации о семейных провайдерах). позволяет легко и просто отслеживать изменения в каждой цитате. Он даже сохраняет отслеживаемые изменения, даже когда вы переходите от одной цитаты к другой цитате и обратно. Вы также увидите, что внутри нашего
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),
),
const Divider(),
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, textAlign: TextAlign.right),
],
);
}
}
// 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,
disabled: !pendingQuote.hasUndo,
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', kDebounceDuration, () {
state = state.afterUpdate(state.current.copyWith(author: value));
});
}
void updateYear(String value) {
EasyDebounce.debounce('quote_${state.current.uid}_year', kDebounceDuration,
() {
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',
),
};