What to expect from this tutorial?
This is a simple tutorial to add fading animation for a text field using flutter_hooks
usually, to add animation to any Flutter UI element, it has to be a state-full widget, the animation controller should be created in the state class, initState
should be overwritten to populate the animation instance, and we should also override dispose
to clean the memory from our animation controller when navigating to another screen!
It has always been a hurdle 😵, so I avoided using it as much as possible, but flutter_hooks
has finally simplified the operation a bit, let's start by cleaning the home_screen
code (you can start with your own screen of course).
The idea
Well, our diary is kinda similar to "Tom Riddle" diary (in "Harry Potter and the Chamber of Secrets"). And I liked how Harry's writing disappears, and Tom's answer appears in the diary. So, let's add fading animation to the text when sent to the AI and an appearing animation when received back from the AI. We will start by creating a simple fade-in and fade-out animation, then work on building app-specific behavior.
The behavior we wanna build is like this:
- The user clicks on the text field
- The app closes the keyboard (for the first time)
- The app sends a conversation request to the AI
- app fade in the server response
- The user clicks on the text field to start writing the response
- The app fades out the server response
- The user clicks the
send
button from the keyboard - The app sends the message to the AI
- the app fades out the user text and fades in the server response
Clean home_screen
code (optional)
If you are following this tutorial series, you will have some code in home_screen, so let's clean it a bit by using LayoutWidget
from the previous article, removing screen contents, changing the action button to a logout button, and showing only the listen
floating button
import 'package:alive_diary_app/config/dependencies.dart';
import 'package:alive_diary_app/config/router/app_router.dart';
import 'package:alive_diary_app/domain/repositories/preferences_repository.dart';
import 'package:alive_diary_app/presentation/widgets/layout_widget.dart';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'home_bloc.dart';
@RoutePage()
class HomeScreen extends HookWidget {
const HomeScreen({super.key, this.title});
final String? title;
@override
Widget build(BuildContext context) {
final bloc = BlocProvider.of<HomeBloc>(context);
final isLoading = useState(false);
final speechText = useState("");
return BlocListener<HomeBloc, HomeState>(
listener: (context, state) {
isLoading.value = state is HomeListeningState;
if (state is HomeSTTState) {
speechText.value = state.text ?? "";
}
},
child: LayoutWidget(
title: 'home'.tr(),
actions: [
IconButton(
icon: const Icon(Icons.logout, color: Colors.black,),
onPressed: (){
locator<PreferencesRepository>().logout();
appRouter.replaceAll([LoginRoute()]);
},
)
],
floatingActionButton: FloatingActionButton(
tooltip: 'Listen',
child: Icon(
Icons.keyboard_voice_outlined,
color: isLoading.value ? Colors.green : Colors.blue,
),
onPressed: () => bloc.add(HomeSTTEvent()),
),
child: Container(
alignment: Alignment.center,
child: const Text("Empty"),
),
),
);
}
}
lib/presentation/screens/home/home_screen.dart
The fun part 😁, the Animation!
To create animations with flutter_hooks
, we start by creating the animation controller hook
AnimationController animationController = useAnimationController(
duration: const Duration(seconds: 4),
initialValue: 1,
);
then we use it in the Transition
component
FadeTransition(
opacity: animationController,
child: Center(
child: Text("Hello World")
),
),
that is it! no initState
, no dispose
, no headache
For our app, we need to show and hide text, so let's add two buttons to the home screen, one for showing and another one for hiding, it would look somewhat like
import 'package:alive_diary_app/config/dependencies.dart';
import 'package:alive_diary_app/config/router/app_router.dart';
import 'package:alive_diary_app/domain/repositories/preferences_repository.dart';
import 'package:alive_diary_app/presentation/widgets/layout_widget.dart';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:google_fonts/google_fonts.dart';
import 'home_bloc.dart';
@RoutePage()
class HomeScreen extends HookWidget {
const HomeScreen({super.key, this.title});
final String? title;
@override
Widget build(BuildContext context) {
final bloc = BlocProvider.of<HomeBloc>(context);
final isLoading = useState(false);
final speechText = useState("");
AnimationController animationController = useAnimationController(
duration: const Duration(seconds: 4),
initialValue: 1,
);
return BlocListener<HomeBloc, HomeState>(
listener: (context, state) {
isLoading.value = state is HomeListeningState;
if (state is HomeSTTState) {
speechText.value = state.text ?? "";
}
},
child: LayoutWidget(
title: 'home'.tr(),
actions: [
IconButton(
icon: const Icon(Icons.logout, color: Colors.black,),
onPressed: (){
locator<PreferencesRepository>().logout();
appRouter.replaceAll([LoginRoute()]);
},
)
],
floatingActionButton: FloatingActionButton(
tooltip: 'Listen',
child: Icon(
Icons.keyboard_voice_outlined,
color: isLoading.value ? Colors.green : Colors.blue,
),
onPressed: () => bloc.add(HomeSTTEvent()),
),
child:Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FadeTransition(
opacity: animationController,
child: Center(
child: Text("Hello World",
style: GoogleFonts.caveat(
fontSize: 30,
color: Colors.black,
),
)
),
),
ElevatedButton(
child: Text("show".tr()),
onPressed: () async {
animationController.forward();
},
),
ElevatedButton(
child: Text("hide".tr()),
onPressed: (){
animationController.animateBack(0,
duration: const Duration(seconds: 1),
);
},
)
],
),
),
),
);
}
}
lib/presentation/screens/home/home_screen.dart
We are using google_fonts to make the font more human-like 🤓, with google fonts, you are free to choose from a wide range of great fonts, but feel free to ignore it since it doesn't have to do with the animation itself 😅
The home UI should look somewhat like
hidden | shown |
---|---|
The text should be visible in the initial state, clicking hide
should fade it away, and clicking show
should fade it in. We can change the initial text state to hidden by changing initialValue
to 0.
simple and great! no need to run from UI animation anymore 😁, now for the app behavior, let's start working on it
Diary app text field behavior
Let's start by creating a large text field, with a nice paper-like background, and address the onTab
and onSubmit
actions
...
Widget build(BuildContext context) {
final textController = useTextEditingController();
...
child: LayoutWidget(
title: 'home'.tr(),
...
child:Container(
height: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 15),
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/images/paper_bg.jpg"),
fit: BoxFit.cover,
),
),
child: TextField(
decoration: const InputDecoration(border: InputBorder.none),
cursorHeight: 35,
style: GoogleFonts.caveat(
fontSize: 30,
color: Colors.black
),
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.send,
controller: textController,
maxLines: null,
onTap: () async {
},
onSubmitted: (text) async {
},
),
),
),
...
lib/presentation/screens/home/home_screen.dart
We have added a text field, with a background, multiple lines support, and functions for clicking and submitting (the software keyboard submit button), let's start with how to dismiss the software keyboard?
Dismiss the software keyboard in flutter
How to dismiss the software keyboard? it is kinda tricky, in Android, we start by adding PROCESS_TEXT
query in the manifest at the same level as the application
section
...
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
<application
android:label="alive_diary_app"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
...
android/app/src/main/AndroidManifest.xml
Now we can test it in the app with the TextInput.hide
method as
import 'package:flutter/services.dart';
...
child: TextField(
decoration: const InputDecoration(border: InputBorder.none),
cursorHeight: 35,
style: GoogleFonts.caveat(
fontSize: 30,
color: Colors.black,
),
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.send,
controller: textController,
maxLines: null,
onTap: () async {
await Future.delayed(const Duration(seconds: 2));
SystemChannels.textInput.invokeMethod('TextInput.hide');
},
...
lib/presentation/screens/home/home_screen.dart
The keyboard will appear when clicking on the text field, then after two seconds, it will just disappear.
Text field animation
We want to apply the animation on the text field, so it must be wrapped with FadeTransition
, and we need two functions
-
showText
function, which hides the keyboard, sets the new value and fades it in -
clearText
function, which fades out field contents, and sets it to an empty text.
...
Widget build(BuildContext context) {
final isLoading = useState(false);
final speechText = useState("");
final canWrite = useState<bool?>(null);
final textController = useTextEditingController();
...
void showText(String? text) async {
await Future.delayed(const Duration(milliseconds: 100));
SystemChannels.textInput.invokeMethod('TextInput.hide');
animationController.animateBack(0, duration: const Duration(seconds: 1));
await Future.delayed(const Duration(seconds: 2));
textController.text = text ?? "";
animationController.forward();
}
void clearText() async {
animationController.animateBack(0, duration: const Duration(seconds: 1));
await Future.delayed(const Duration(seconds: 2));
textController.text = "";
animationController.forward();
}
...
child: TextField(
decoration: const InputDecoration(border: InputBorder.none),
cursorHeight: 35,
style: GoogleFonts.caveat(
fontSize: 30,
color: Colors.black,
),
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.send,
controller: textController,
maxLines: null,
onTap: () async {
if (canWrite.value == null) {
showText("How was your day?");
canWrite.value = true;
} else if (canWrite.value == true) {
clearText();
}
},
onSubmitted: (text) async {
showText("Tell me more");
},
),
...
lib/presentation/screens/home/home_screen.dart
That is it! well, almost, we are mimicking the server response with a plain text, but whatever, let's concentrate on the behavior for now
we have created the two functions showText
and clearText
, we have created a new helper state canWrite
in order to distinguish the first click,
if this is the first click, we show the server response (static text for now), but if this is not the first time, then we simply clear the text.
when clicking the keyboard submit button, we (should send the text to server and show its response) show the message "Tell me more"
This is the required behavior as described, let's continue building the app together and as always
Stay tuned 😎
Top comments (0)