Flattening Android’s TextInputLayout

Creating a TextInputLayout with a ConstraintLayout

TJ Dahunsi

Nov 25 2018 · 5 mins

Categories:

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:

  1. A vertical LinearLayout as the TextInputLayout’s root.

  2. An inner FrameLayout for housing the EditText 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:

  1. A ConstraintLayout to house all the components

  2. A TextView for the input hint

  3. An 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 Hint Animation for TextInputLayout

There are three transformations on the hint TextView:

  1. Its scale in both the X and Y axes are reduced by some factor, around 80% or thereabouts.

  2. It has a negative (upwards is negative in Android’s coordinate system) translation on the y-axis.

  3. 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.

  4. 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. 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. 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:

  1. My input forms are RecyclerView based. Whatever entity, be it a User, Team, Event, Game, or what have you, has a List representation that can be rendered in a RecyclerView and edited in place. This saves me the trouble of having a new xml layout for each separate entity, and lets me reuse the same DiffUtil based logic for all of them. Since they’re in a RecyclerView, each ItemView rendered should have a quick layout inflation time from xml, 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.

  2. 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 An implementation of the Material Design Components input field with a ConstraintLayout

,