How to implement an impressive login screen?

Most applications today have an authorization activity which can be done with Facebook, Google, Twitter or you can do it in the old fashioned way — with an email and password. It may not be the very first screen of your application (many apps have welcome screens, like a sneak peek of the app).

The objective here is that you don’t want to make the authorization process more painful than it usually is, right? You want to leave the user with a good impression of your application, even more — it is your responsibility to provide them with a feeling of delight and excitement.

Where to start?

I’d had a struggle with designing a login/sign up screen for one of my Android apps before I stumbled upon this excellent design concept:

Created by Yaroslav Zubko.
Imitation is the sincerest form of flattery, right?

So, I decided to implement the idea and will provide a comprehensive guide to creating the above from scratch throughout the article. If you are this kind of person who likes to read the code, you may want to skip to the end of the article.

Structure of the app.

Apart from making this splendid interaction, I want to make the code reusable, and to separate the main concerns. That is, my login screen should know nothing about the sign up screen and vice versa.

Now, let’s take a look at the concept again and define the nuts and bolts.

  • Background image.
  • Sign up screen.
  • Login screen.
  • Gap at the edges of each screen.
  • Input fields.
  • Logo, bottom buttons and login /sign up labels.

I think it is not a significant effort to interpret these components in terms of Android Views. For instance, each screen should be an Android Fragment, and you can use a ViewPager to switch between them. The background, logo, and buttons could be simple ImageView objects.

So, the application will have the following structure:

  • Activity with a ViewPager
  • Adapter
  • Login Fragment
  • Sign Up Fragment

What about the gap?

That’s one of the reasons why I have chosen to use the ViewPager component; its adapter has the getPageWidth method which returns the proportional width of a given page as a percentage of the ViewPager’s measured width from (0.f-1.f]. You can adjust the width of a particular page to fit your needs, and the next/previous pages will be shifted to the left/right to fill the gap which has been left by the current page.

Here’s how it looks like:

Two different pages get clipped.

How do we know the width of the gap? And what about the text that should be positioned vertically in the middle of the gap?

Don’t worry about it; we’ll create a factor variable which is calculated in the constructor of our adapter, and then will use that factor in the getPageWidth method as shown below in the code.

Let’s break it down:

  1. textSize is the size of the text when it’s been folded (vertical text).
  2. textPadding is simple padding from the left and right sides.
  3. We add the size and padding, divide that by the width of the screen, and then subtract the whole thing from one. Now, if we multiply what we’ve got by the page’s width, we should be able to clip the current page. Refer to the result above.

Authorization Activity

It’s time to get to the meat of the matter! The meat is our Android Activity.

First things first — the XML file:

Here is what we get:

I’m using ConstraintLayout, as a parent view, which allows you to build complex and responsive layouts in a flat hierarchy of views. The well-known advice in Android is to avoid creating deep hierarchies of Views in your layouts because it hurts the performance and increases the time of drawing your UI on the screen.

I assume that you are familiar with the ConstraintLayout and how it works. If you are not, I highly recommend reading this.

What is AnimatedViewPager?

AnimatedViewPager is a custom view which does not respond to any touch events and changes the duration of a swipe. You can take a look at the code here.

Logo and buttons.

These elements will be shared across Login and Sign Up screens; therefore it is reasonable to keep them in one place. The logo, as well as the buttons, are simple ImageView objects which are smoothly translated from one fragment to another.

Second things second — the activity class.

Let’s jump straight into the code:

What does it do?

  • Tints
  • Loads and sets up the background image.
  • Creates an adapter for the ViewPager.

Tinting the shared elements

As you may have noticed, the shared components in the original design are not so opaque. The bottom elements should be close to transparency, let’s say #B3FFFCFC (just a white color with opacity 173). The logo changes as it goes to another page, whereas the bottom elements do not. You can see that it always has a lighter color than the current background.

That’s where the tinting occurs:

Note: keep in mind that I will change the tint of the logo in our adapter as well.

You don’t need to follow the above decisions; I’m just trying to stick to the original design.

Background image.

You need to stretch out the background image by its width, so it is as wide as your two pages together. It’s good to pick up a big image(approximately 1,900 x 1,200 JPEG, 24-bit color), so it could be drawn without losing any quality.

It is pretty simple — fetch the width of the screen, multiply by 2, and load the image using Glide, Picasso, Fresco or whatever image loading library you like.

  1. After you have loaded the image, you need to shift the user focus to the very left edge of your image by using the ImageView.scrollTo method.
  2. Afterward, you want to fire a scale animation; this way you will get this beautiful scale effect.

A GIF would have some jerks; a video is a more smooth option.

  • Last but certainly not least, initialize your adapter.

Adapter and its guts.

We need to have a controller that is aware of what’s going on between those fragments. The perfect candidate for this role is our adapter.

We will have a base class for the fragments, which serves as an interface that can be referred to by the adapter.

This is our abstract class:

As you can see we have three methods for each type of animation. This way you can refer to both fragments without knowing who is who.

To avoid any confusion, let’s run through the methods above:

  • fold is called when you are switching to the next/previous page for the current fragment.
  • unfold is called for the next/previous page before a switch. Please, keep in mind that the method isn’t called by the adapter. This method gets called when a click event occurs — when you click on that vertical TextView.
  • clearFocus is called when you release the inputs. For example, when you have entered your password or email, we need to clear the focus. At this moment a scale animation is fired as well.
  • authLayout provides a resource to inflate in the Fragment.onCreateView method.

What about that callback interface?

We need to provide the ability to notify the adapter about any events that occur in the fragments. The solution is simple — let the adapter to implement that AuthFragment.Callback.

Here is the code of our adapter:

That’s undoubtedly the most significant part here. Don’t worry! I will go over this.

First of all, you’ve seen that constructor early in the article. We simply calculate a factor for the gap and then set the duration of swipes for the ViewPager. Also, we create an array of fragments; it’s easier to fetch a fragment by its position.

  • show method is responsible for folding the current fragment, shifting the shared elements, and switching to another fragment using the ViewPager.setCurrentItem method. As I said before, this method gets called from one of the fragments.
  • getPageOffsetX is just a helper method which returns the width of the gap for each fragment.
  • shiftSharedElements since we’re clipping the fragments (in order to get the gap), we need to adjust the shared elements, so they don’t look crooked relatively to the fragment layout. Just create a simple translation animation which runs along with other animations.
  • scale scales up/down the background image when a text input has lost/gained focus.

That is all.

Login and Sign Up screens.

Okay, it’s time to set the main screens. It is obvious that we will have similar XML files for each fragment. In fact, we will have only one more set of views(2 views) in the Sign Up fragment, and a simple TextView for the Login fragment. Therefore it makes sense to have the same ids for each “duplicated” view.

Having said that, let’s take a look at the XML file of the Sign-Up screen:

That’s how it looks like! You can have rectangular input fields.

Note: the inputs gain focus as soon as your app enters that screen; this results in bringing up the keyboard, and we don’t want this. That is why I have a simple View at the top of the layout that captures focus before the inputs do.

I’m using a simple TextInputEditText view for every single input field, which is wrapped into a TextInputLayout (that’s how we get the floating label at the top).

To prevent duplication of attributes for every TextInputEditText view in the file, I created a style which contains all the common attributes. I did the same thing for the TextInputLayout views as well. You will find this here.

By the way, have you noticed something here? The password text is thinner than the email text. Also, there are some minor issues with the floating label. In order to fix this, you have to write some code in the onCreate method:

Note: this code uses the forEach method which doesn’t work with the API lower than 24. You will have to use simple iterations.

Also, you can have a rectangular background of the inputs if you want. Change the radius to 0dp:

For the sake of completeness, here’s a snapshot of the Login fragment:

The login screen with an additional TextView (Forgot Password?)

As you can see, I’m using a custom view called VerticalTextView. That’s the bold text that says Log In/Sign Up. You can check out the code here.

Why do we need a custom TextView if we can just utilize the View.setRotation method instead?

I wish I could do this. The problem here is that it doesn’t really rotate the view. It rotates only the text inside, but it doesn’t change the width or height of the object at all. This really messes up the layout.

I took a snapshot on my phone, and if you look a bit closer, you will see that the bounds of the view are still horizontal even though the text is vertical. (I’d enabled the Show layout bounds option before making the shot)

The issue that occurs if rotating with the View.setRotation() method.

The point is that the VerticalTextView gets the job done. It has some problems with the animation though (later in the article). Refer to this link for more detail.

It’s time to add some animations!

We have three kinds of animation here:

  • folding animation.
  • unfolding animation.
  • scale animation when the inputs have been touched.

Folding animation

We will be using the Transition Framework by Andrey Kulikov. This just a backport of Android Transitions API. They are compatible with Android 2.2+.

  • ChangeBounds is a simple transition that captures the layout bounds of a view before and after the scene change and animates those changes during the transition. Here it’s used to animate view’s position on the screen.
  • Rotation is a transition that captures the angle of a view before and after and creates a rotate animation between the start and final angle.
  • TextSizeTransition class is responsible for size and color transitions. It makes the TextView transparent and captures two bitmaps (one for the start font size and another for the final font size). So it animates a little bit from the start (the first bitmap), then it swaps out the bitmaps, and animates the rest of the way (the second bitmap). If you want to do this with one bitmap, you will get a blown image at the end of the transition. Here’s a perfect font transition:
TextSizeTransition in action!


TextSizeTransition can’t draw the VerticalTextView (when it’s in the vertical position) into a bitmap; it gets clipped.

Clipped text issue.

However, we can slowly rotate the view by 90 degrees as it goes up. After the transition has stopped, we need set the rotation attribute to 0 and call the VerticalTextView.setVerticalText(true) method; this way we will eliminate that extra space which has been left after rotating the view. This is how we do it:

Also, I translated the view object to the left/right a little bit. I did this for two reasons:

  1. We need to have a small margin from the left/right (depends what fragment).
  2. We don’t have a jerky effect when setting it to the vertical position.
That’s how it should be done!

Unfolding animation

Considering the fact that the TextView is getting set vertically, we run into the same problem again — TextSizeTransition can’t have a vertical bitmap. What we are going to do is opposite to what we did in the folding animation. We have to set the view in the horizontal position, then quickly rotate the view by 90 degrees (View.setRotation method), and slowly rotate to 0 degrees as it goes to down to the center.

To set the TextView in the horizontal position, we have to use the VerticalTextView.setVerticalText method, request layout and set up our transitions. Please note, that you need to call the TransitionManager.beginDelayedTransition method after the TextView has been redrawn. That’s why I do all the setup in the method.

Here is what we get from the code above:

This is our unfold transition when the view goes back to the center.

As you can see, the rotation from the horizontal position back to the vertical position happens super fast.

Scale animation

This is pretty straightforward. Whenever the keyboard appears on the screen — which means that a text input has been touched — we need to scale down the logo and background. To make my life easier, I used this library, which provides a callback every time the keyboard appears/disappears on the screen.

This is a method from our abstract class (AuthFragment.class). So, you just send a notification to the adapter via callback, and it handles the rest of the process. When the keyboard is gone, we need to clear focus by calling the clearFocus method.



If you’ve made this far, you should be able to reproduce this in your app. In any case, you can check out the source code in my repository on GitHub.

More smooth is here.

Thanks for reading!