【问题标题】:Flutter - Text undo and redo buttonFlutter - 文本撤消和重做按钮
【发布时间】:2021-05-23 02:37:06
【问题描述】:

嗨,我一直在互联网上搜索如何创建重做和撤消按钮并将它们连接到颤动的 TextField,但到目前为止我还没有找到任何东西。我希望有人知道如何做到这一点,希望对您有所帮助。

【问题讨论】:

    标签: flutter text undo


    【解决方案1】:

    您可以查看undoreplay_bloc 包。

    或者,您可以尝试在自己的项目中实现该功能并根据您的特定要求对其进行微调。

    以下是此类功能的实施草案。

    它支持撤消、重做和重置。

    我使用了以下软件包:

    您将在本文末尾找到完整的源代码。但是,这里有一些重要的亮点:

    解决方案的结构:

    1. 应用程序

      MaterialApp 封装在 Riverpod ProviderScope

    2. HomePage

      维护全局状态的HookWidget:所选报价的uidediting,无论我们是否显示表单。

    3. QuoteView

      所选报价的非常基本的显示。

    4. QuoteForm

      此表单用于修改选定的报价。在(重新)构建表单之前,我们检查引用是否已更改(这发生在撤消/重置/重做之后),如果是,我们重置已更改字段的值(和光标位置)。

    5. UndoRedoResetWidget

      这个小部件提供了三个按钮来触发我们的`pendingQuoteProvider 上的撤消/重置和重做。撤消和重做按钮还显示可用的撤消和重做次数。

    6. pendingQuoteProvider

      这是一个家庭 StateNotifierProvider(请查看 here 了解有关家庭提供者的更多信息),它使跟踪每个报价的更改变得容易和简单。即使您从一个引用导航到另一个引用并返回,它甚至会保留跟踪的更改。您还将看到,在我们的 PendingQuoteNotifier 中,我将更改去抖动 500 毫秒以减少报价历史记录中的状态数。

    7. PendingQuoteModel

      这是我们pendingQuoteProvider 的状态模型。它由List<Quote> historyindex 组成,表示当前历史位置。

    8. Quote

      我们的报价基础类,由uidtextauthoryear 组成。

    完整源代码

    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',
      ),
    };
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-03-22
      • 1970-01-01
      • 2013-01-17
      • 2016-10-28
      • 2021-04-12
      • 2019-09-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多