skip to Main Content

I want to show image as part of TextFormField in flutter. Like how it is shown in ChatGPT. I should be able to delete that image by pressing backspace. I don’t want to add the image to the prefix or suffix as suggested in many cases. I want it to be a part of the chat.

I came across rich text editors like flutter quill but I am not sure how good it will be. Is there any other way to achieve the same?

UPDATE:
I am looking for something like the below image . But instead of close icon using backspace to clear the image would be better.

enter image description here

2

Answers


  1. You can display an image in the TextField like this.

    1. Create a class extending TextEditingController:
    class ImageTextEditingController extends TextEditingController {}
    
    1. Override the method buildTextSpan.
    @override
      TextSpan buildTextSpan({
        required BuildContext context,
        TextStyle? style,
        required bool withComposing,
      }) {
        final children = <InlineSpan>[];
        // ...Display image logic goes here
        return TextSpan(children: children, style: TextStyle(color: Colors.black));
      }
    
    1. Add placeholder logic. When you override buildTextSpan, you must ensure the text value in TextEditingController matches the TextSpan children. Since you want to add images, a solution is to add a placeholder character in the text string where the image is. Eg your text might be "check out this photo:" and display an image, so we add a character such as "ʉ" : "check out this photo:ʉ".
    class ImageTextEditingController extends TextEditingController {
    
      void insertImage() {
        if(!selection.isValid) return;
        final cursorPos = selection.baseOffset;
        text = text.substring(0, cursorPos) + 'ʉ' + text.substring(cursorPos);
        selection = TextSelection.collapsed(offset: cursorPos + 1);
      }
    
      @override
      TextSpan buildTextSpan({
        required BuildContext context,
        TextStyle? style,
        required bool withComposing,
      }) {
        final children = <InlineSpan>[];
        final chars = text.split('');
        int currImageIndex = 0;
        for (int i = 0; i < chars.length; i++) {
          final char = chars[i];
          if (char == 'ʉ') {
    
            const url = 'https://picsum.photos/250?image=9';
    
            currImageIndex++;
            children.add(TextSpan(children: [
              WidgetSpan(child: SizedBox(width: 30, child: Image.network(url)))
            ]));
    
          } else {
            children.add(TextSpan(text: char));
          }
        }
    
        return TextSpan(children: children, style: TextStyle(color: Colors.black));
      }
    }
    

    Now whenever the method insertImage is called, the character ‘ʉ’ will be inserted at the cursor position. But it will be displayed as an image instead. Now use this controller in your TextField controller.
    Delete using the backspace will now work as well.

    Login or Signup to reply.
  2. You can break down the design into basic Flutter widgets. Here’s a breakdown:

    UI breakdown

    Also, to remove the images one by one on backspace events, we can use the Focus widget to capture key events, specifically listening for the backspace key.

    Here is my implementation of this custom widget:

    class CustomTextFormField extends StatefulWidget {
      final TextFormField textFormField;
      final List<Widget> images;
      final Widget? button;
    
      CustomTextFormField({
        super.key,
        required this.textFormField,
        this.button,
        this.images = const [],
      }) : assert(
              textFormField.controller != null,
              'textFormField must have a controller',
            );
    
      @override
      State<CustomTextFormField> createState() => _CustomTextFormFieldState();
    }
    
    class _CustomTextFormFieldState extends State<CustomTextFormField> {
      @override
      Widget build(BuildContext context) {
        return Stack(
          alignment: Alignment.bottomRight,
          children: [
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              decoration: BoxDecoration(
                color: Colors.grey.shade300,
                borderRadius: BorderRadius.circular(16),
              ),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // Display images if available
                  if (widget.images.isNotEmpty) ...[
                    SizedBox(
                      height: 80,
                      child: ListView.separated(
                        scrollDirection: Axis.horizontal,
                        itemCount: widget.images.length,
                        separatorBuilder: (_, __) => const SizedBox(width: 8),
                        itemBuilder: (_, index) => ClipRRect(
                          borderRadius: BorderRadius.circular(12),
                          child: widget.images[index],
                        ),
                      ),
                    ),
                    const SizedBox(height: 8),
                  ],
                  Theme(
                    data: Theme.of(context).copyWith(
                      inputDecorationTheme: InputDecorationTheme(
                        border: InputBorder.none,
                        contentPadding: widget.button != null
                            ? const EdgeInsets.only(right: 42)
                            : EdgeInsets.zero,
                      ),
                    ),
                    // The TextFormField wrapped with Focus to listen for backspace
                    child: Focus(
                      onKeyEvent: (_, event) {
                        if (event is KeyDownEvent &&
                            event.logicalKey == LogicalKeyboardKey.backspace) {
                          final text = widget.textFormField.controller?.text.trim();
    
                          // Remove the last image when backspace is pressed and input is empty
                          if ((text?.isEmpty ?? false) & widget.images.isNotEmpty) {
                            setState(() => widget.images.removeLast());
                          }
                        }
    
                        return KeyEventResult.ignored;
                      },
                      child: widget.textFormField,
                    ),
                  ),
                ],
              ),
            ),
            // Optional button (e.g., mic icon) if provided
            if (widget.button != null)
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                child: widget.button,
              ),
          ],
        );
      }
    }
    

    Then our CustomTextFormField can be used like this:

    CustomTextFormField(
      textFormField: TextFormField(
        minLines: 1,
        maxLines: 6,
        controller: TextEditingController(),
        decoration: const InputDecoration(
          hintText: 'Message',
        ),
      ),
      images: List.generate(
        3,
        (index) => Image.network(
          'https://picsum.photos/100?random=$index',
          width: 80,
          height: 80,
          fit: BoxFit.cover,
        ),
      ),
      button: IconButton(
        icon: const Icon(Icons.mic),
        onPressed: () {},
      ),
    ),
    
    

    Sample Output

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search