Flattening Android’s TextInputLayout
Creating a TextInputLayout with a ConstraintLayout

TJ Dahunsi
Nov 25 2018 · 5 mins
Google’s Material Design Components offer a spiffy ViewGroup for text input, the TextInputLayout. Its implementation details may leave ConstraintLayout purists a little green at the gills however, as the internal implementation of the TextInputLayout has quite a bit of nesting:
A vertical
LinearLayout
as theTextInputLayout
’s root.An inner
FrameLayout
for housing theEditText
used for input.
Now, this isn’t all that bad, and there is a good explanation for this nesting; the Android Material Components would need to add an explicit dependency on ConstraintLayout
as they are different artifacts in different repositories, managed by different teams.
Luckily for us, we don’t have that constraint (pun very much intended), so let’s recreate the TextInputLayout with a flat layout hierarchy.
Our flattened version will consist of the following:
A
ConstraintLayout
to house all the componentsA
TextView
for the input hintAn
EditText
for actual input
The most visually appealing part of the TextInputLayout is highlighting and animating the hint TextView
to the upper left corner when the input area gains focus, so naturally, is the most fun part of this exercise. Let’s break it down:
Hint Animation for TextInputLayout
There are three transformations on the hint TextView:
Its scale in both the X and Y axes are reduced by some factor, around 80% or thereabouts.
It has a negative (upwards is negative in Android’s coordinate system) translation on the y-axis.
It has a negative translation on the x-axis to align its left with the left of the input EditText as a result of the down scaling.
Its color changes to the accent color of the theme.
The first 3 sound like excellent candidates for Android’s ViewPropertyAnimator, and because of the fluency of that API, shrinking the hint text can be encompassed succinctly with the following methods:
private static final int HINT_ANIMATION_DURATION = 200; private static final float HINT_SHRINK_SCALE = 0.8F; private static final float HALF = 0.5F; private void scaleHint(boolean grow) { float scale = grow ? 1F : HINT_SHRINK_SCALE; float translationX = grow ? 0 : getHintLateralTranslation(); float translationY = grow ? 0 : getHintLongitudinalTranslation(); hint.animate() .scaleX(scale) .scaleY(scale) .translationX(translationX) .translationY(translationY) .setDuration(HINT_ANIMATION_DURATION) .start(); } protected float getHintLateralTranslation() { int width = hint.getWidth(); return -((width - (HINT_SHRINK_SCALE * width)) * HALF); } protected float getHintLongitudinalTranslation() { return -((itemView.getHeight() - hint.getHeight()) * HALF); }
Let’s talk about the math behind the translations for a bit.
The lateral translation is my favorite because it’s based on the algebra of similar shapes.
Figuring out how much to translate the hint value on the x-axis after shrinking.
When the hint text is shrunk to 80% of its original value, its original origin is displaced as well. The value of the displacement on the x-axis can be calculated based on the relationships derived above, yielding the getLateralTranslation
method defined above.
With regards to vertical translation, the math isn’t quite as fun. It’s half of the difference between the ConstraintLayout’s height, and the hint’s height.
An illustration of the vertical translation of the hint.
This leaves us with animating the highlight of the hint when it’s focused. This time, we’ll use a ValueAnimator
as text color isn’t a property supported by the ViewPropertyAnimator
. The implementation is as follows:
private void tintHint(boolean hasFocus) { int start = hint.getCurrentTextColor(); int end = ContextCompat.getColor(hint.getContext(), hasFocus ? R.color.colorAccent : R.color.dark_grey); ValueAnimator animator = ValueAnimator.ofObject(new ArgbEvaluator(), start, end); animator.setDuration(HINT_ANIMATION_DURATION); animator.addUpdateListener(animation -> hint.setTextColor((int) animation.getAnimatedValue())); animator.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { setTintAlpha(hasFocus); } }); animator.start(); } private void setTintAlpha(boolean hasFocus) { hint.setAlpha(!hasFocus ? 0.38F : 1F); }
The Material spec for textview hint color changes the alpha of the tint, not the color, hence the set tint alpha method.
Finally, here’s the xml of the layout being animated:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="@dimen/single_margin" android:paddingRight="@dimen/single_margin"> <EditText android:id="@+id/input" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/three_quarter_margin" android:layout_marginBottom="@dimen/three_quarter_margin" android:textSize="@dimen/small_text" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/hint" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/quarter_margin" android:textSize="@dimen/small_text" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="@+id/input" app:layout_constraintTop_toTopOf="parent" tools:text="Hint" /> </androidx.constraintlayout.widget.ConstraintLayout>
xml for a flattened input field
There we have it; a flattened, leaner, ConstraintLayout
based input field. Why go through all this though? After all, the TextInputLayout
that ships with the Android Material Design Components is more robust and fully featured including RTL layout support and theming, amongst a host of other niceties. It’s also more future proof and requires less maintenance on our part.
The answer for me was two fold:
My input forms are RecyclerView based. Whatever entity, be it a
User
,Team
,Event
,Game
, or what have you, has aList
representation that can be rendered in a RecyclerView and edited in place. This saves me the trouble of having a newxml
layout for each separate entity, and lets me reuse the sameDiffUtil
based logic for all of them. Since they’re in a RecyclerView, eachItemView
rendered should have a quick layout inflation time fromxml
, so the lighter the better. The difference is small, but just a little noticeable, especially when I’m animating a floating action button and doing shared element transitions between image heavy screens. This saves me two layers of ViewGroup nesting for each item in the list, and less object allocations; the TextInputLayout class has quite a lot of member variables.It was a fun challenge. I followed the Android Dev Summit livestream last month, and really enjoyed the talk about what animators to use and when by Doris Liu. This was a great application for it, since it’s a small, localized problem, and a quick win if successful.
The full source for the java code and xml layout for the project can be seen in the embedded links. You can also build the project yourself from the the source in the linked repo, the code is available in the develop branch.
An implementation of the Material Design Components input field with a ConstraintLayout