Akbar Pulatov
Akbar Pulatov

Reputation: 3359

How to know the end of Hero animation in flutter?

I want to achieve this:

enter image description here

But when Hero animation starts keyboard is forced to dismiss:

enter image description here

I tried to use widgets callback which is triggered after layout. But this callback is fired whenever hero animation starts. I also tried to use Future.delayed(Duration(seconds: 2), but it does not help. Everything is working as expected if I only remove Hero widget from the widget tree.

Here is my first Screen:

import 'package:flutter/material.dart';
import 'package:move_me_delivery/components/rounded_app_bar.dart';
import 'package:move_me_delivery/components/search_field.dart';
import '../screens.dart';

class HomeTab extends StatelessWidget {
  const HomeTab({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: RoundedAppBar(title: ""),
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
        child: Column(
          children: [
            SearchTextField(
              onFocusChange: (val) async {
                if(val){
                  await Navigator.push(context, PageRouteBuilder(
                    transitionDuration: Duration(milliseconds: 400),
                    pageBuilder: (_, __, ___) => SearchScreen()));
                }
              },

            )
          ],
        ),
      )
    );
  }
}

Here is my second screen:

import 'package:flutter/material.dart';
import 'package:line_awesome_flutter/line_awesome_flutter.dart';
import 'package:move_me_delivery/components/search_field.dart';

class SearchScreen extends StatefulWidget {
  const SearchScreen({Key? key}) : super(key: key);

  @override
  _SearchScreenState createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  final _focusNode = FocusNode();

  @override
  void initState() {
    super.initState();
    _focusNode.requestFocus();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      child: SafeArea(
        child: Scaffold(
          body: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),

            child: Column(
              children: [
                SearchTextField(
                  focus: _focusNode,
                  onCancel: (){
                    FocusScope.of(context).unfocus();
                    Navigator.pop(context);
                  },
                  inputDecoration: InputDecoration(
                      prefixIcon: Icon(LineAwesomeIcons.search, color: Colors.black,),
                      filled: true,
                      fillColor: Colors.white,
                      border: OutlineInputBorder(borderRadius: BorderRadius.circular(8),
                          borderSide: const BorderSide(color: Colors.blue, width: 1))
                  ),
                ),

              ],
            ),
          ),
        ),
      ),
    );
  }
}

And finally here is my SearchField screen with Hero animation:

import 'package:flutter/material.dart';
import 'package:line_awesome_flutter/line_awesome_flutter.dart';
import 'package:move_me_delivery/data/styles.dart';

class SearchTextField extends StatefulWidget {
  const SearchTextField({Key? key,
    this.onFocusChange,
    this.focus,
    this.onCancel,
    this.inputDecoration
  }) : super(key: key);

  final void Function(bool hasFocus)? onFocusChange;
  final FocusNode? focus;
  final VoidCallback? onCancel;
  final InputDecoration? inputDecoration;




  @override
  _SearchTextFieldState createState() => _SearchTextFieldState();
}

class _SearchTextFieldState extends State<SearchTextField>{
  late FocusNode _focus;

  @override
  void initState() {
    super.initState();
    _focus = widget.focus ?? new FocusNode();
    _focus.addListener(
      (){
        if(widget.onFocusChange != null){
          widget.onFocusChange!(_focus.hasFocus);
        }
      }
    );
  }

  @override
  Widget build(BuildContext context) {
    return Hero(
      tag: "search",
      child: Material(
        type: MaterialType.card,
        child: Row(
          children: [

            Expanded(
              child: TextField(style: AppTextStyles.body2,
                  focusNode: _focus,
                  decoration: InputDecoration(
                    prefixIcon: Icon(LineAwesomeIcons.search, color: Colors.black,),
                    // suffixIcon: Text("Cancel"),
                    filled: true,
                    fillColor: Colors.white,
                    border: OutlineInputBorder(borderRadius: BorderRadius.circular(8),
                        borderSide: const BorderSide(color: Colors.blue, width: 1))
                  ))),

            if(widget.onCancel != null)
            GestureDetector(
              onTap: widget.onCancel,
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Text("Cancel"),
              ),
            )
          ],
        ),
      ),
    );
  }
}

Upvotes: 6

Views: 2310

Answers (3)

RodXander
RodXander

Reputation: 843

I solved this exact same issue by creating a FocusNode that'll requestFocus at the end of the hero animation. However, it is also imperative that as a return for the flightShuttleBuilder function you return a widget similar to the one on the destination, except for the fact that it won't include this FocusNode.

Here's how it looks (on the destination page):

child: Hero(
  tag: 'your_tag',
  flightShuttleBuilder: (_, animation, __, ___, ____) {
    animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _focusNode.requestFocus();
      }
    });
    return TextField();
  },
  child: TextField(focusNode: _focusNode),
),

Upvotes: 1

wjploop
wjploop

Reputation: 259

The reason of keyboard dimiss is when hero animation start flight TextField unmounted so its focus loss and then keyboard dismiss.

And why TextFiled become to be unmounted, you need to understand how Hero Animaiton work, refer this https://docs.flutter.dev/development/ui/animations/hero-animations.

do something at the end of hero, you can do like below:

         child: Hero(
                tag: "hero_tag",
                flightShuttleBuilder: ((flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
                  animation.addStatusListener((status) {
                    if (status == AnimationStatus.completed) {
                      // the end of hero animation end
                      _focusNode.requestFocus();
                    }
                  });

Upvotes: 3

PatrickMahomes
PatrickMahomes

Reputation: 944

The interface you want to achieve doesn't necessarily use Hero widgets. It can be done with other animations. But, if you wan't to use Hero, you can try a rather hacky solution:

On your Screen 1, set these two properties in your Hero's TextField:

Hero(
  tag: 'search',
  child: Material(
    type: MaterialType.transparency,
    child: TextField(
      readOnly: true,
      showCursor: true,
      onTap: () {
        Navigator.push() //to SearchScreen()
      }
    ),
  ),
),

Then, on Screen 2:

Hero(
  tag: 'search',
  child: Material(
    type: MaterialType.transparency,
    child: TextField(
      autofocus: true,
    ),
  ),
),

You'll have to avoid using the same SearchTextField on both screens; they each need their own as I showed. Also, you can probably remove all of that FocusNode code if you use this method.

Disclaimer: I haven't tested this code. It's just something to try

Upvotes: 0

Related Questions