Flutter — кнопка отмены и повтора текста

привет, я искал в Интернете, как создать кнопки повтора и отмены и подключить их к флаттерному текстовому полю, но пока ничего не нашел. Я надеюсь, что кто-то знает, как это сделать, я надеюсь на вашу помощь.


person Daniel Salas DG    schedule 20.02.2021    source источник


Ответы (1)


Вы можете посмотреть отменить или replay_bloc.

Или вы можете просто попробовать реализовать эту функцию в своем собственном проекте и настроить ее под свои конкретные требования.

введите здесь описание изображения

Вот черновик реализации такой функции.

Он поддерживает отмену, повтор и сброс.

Я использовал следующие пакеты:

Полный исходный код вы найдете в конце этого поста. но вот несколько важных моментов:

Структура решения:

  1. Приложение

    MaterialApp в капсуле Riverpod ProviderScope

  2. HomePage

    HookWidget поддержание глобального состояния: uid выбранной котировки и editing независимо от того, отображаем мы форму или нет.

  3. QuoteView

    Очень простое отображение выбранной котировки.

  4. QuoteForm

    Эта форма используется для изменения выбранной котировки. Перед (повторным) построением формы мы проверяем, была ли изменена кавычка (это происходит после отмены/сброса/повторения), и если да, то сбрасываем значения (и положение курсора) изменившихся полей.

  5. UndoRedoResetWidget

    Этот виджет предоставляет три кнопки для запуска отмены/сброса и повтора в нашем `pendingQuoteProvider. Кнопки отмены и повтора также отображают количество доступных отмен и повторов.

  6. pendingQuoteProvider

    Это семейство StateNotifierProvider (см. здесь для получения дополнительной информации о семейных провайдерах). позволяет легко и просто отслеживать изменения в каждой цитате. Он даже сохраняет отслеживаемые изменения, даже когда вы переходите от одной цитаты к другой цитате и обратно. Вы также увидите, что внутри нашего PendingQuoteNotifier я отменяю изменения в течение 500 миллисекунд, чтобы уменьшить количество состояний в истории котировок.

  7. PendingQuoteModel

    Это Государственная Модель нашего pendingQuoteProvider. Он состоит из List<Quote> history, а также index для текущей позиции в истории.

  8. 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',
  ),
};
person Thierry    schedule 20.02.2021