How to implement an awesome Login Screen?
Most applications today have an authorization activity which can be done with Facebook, Google, Twitter or you can do it the old fashioned way — with an email and password. It may not be the very first screen of your application (some applications have welcome screens, like a sneak peek of the app).
However, the main point here is that you don’t want to make the authorization process more painful than it should be, right? You want to leave the user with a good impression about your application, even more — it’s your responsibility to provide them with a feeling of delight and excitement.
Alright, where are you going with this?
Well, I’d had a struggle with designing a login/sign up screen for one of my android applications(it had to be perfect) before I stumbled upon this great design concept:
Imitation is the sincerest form of flattery, right?:)
So, I decided to implement the concept, and will provide a comprehensive guide to creating the above from scratch throughout the article. If you are this kind of person that likes to read code, you may want to skip to the end of the article:) If not, shall we begin?.
Structure of the application
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 the concept again and define the nuts and bolts. Here we go:
- 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’s not a big effort to interpret these components in terms of Android Views. For example, each authorization screen should be a 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
- Login Fragment
- Sign up Fragment
Wait a minute, what about the gap?
Well, that’s one of the reasons why I’ve chosen to use a ViewPager, its adapter has the PagerAdapter.getPageWidth(int position) method which returns the proportional width of a given page as a percentage of the ViewPager’s measured width from (0.f-1.f] So, you can adjust the width of some particular page to fit your needs, and the next/previous page will be shifted to the left/right to fill the gap which has been left by the current page.
Here’s the result:
But how do you 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 which is calculated in the constructor of PagerAdapter, and then will use the factor in the PagerAdapter.getPageWidth(int position) method.
Let’s break it down:
- TextSize is the size of text when it’s been folded (vertical text).
- It’s a simple padding from the left and right.
- We add the size and padding , divide that by the width of the screen, and subtract all that stuff 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.
Alright, it’s time to get to the meat of the matter! The meat is our Activity:)
First things first — the XML file:
Here’s what we get:
So, I’m using ConstraintLayout, as a parent view, which allows you to build complex and responsive layouts in a flat hierarchy of views. A common advice in Android is to avoid deep hierarchies of views inside your layouts, since it damages the performance and the time that your UI takes to be drawn on the screen.
I suppose you are familiar with the ConstraintLayout and how it works. If you are not, I highly recommend to read this.
Whoa there..What the heck is AnimatedViewPager?
Essentially, that’s a custom view which doesn’t 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 are shared elements of both screens(Login/Sign Up) that’s why it’s 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?
- Loads and sets up the background image.
- Creates an adapter for the ViewPager.
Tinting the shared elements
As you may have noticed, the shared elements 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.
However, you don’t really need to follow this, I’m just trying to stick to the original design:)
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 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’s pretty simple — just fetch the width of the screen, multiply by 2, and load the image using Glide, Picasso, Fresco or whatever library you like.
- After you’ve loaded the image, you need to shift the user focus to the very left edge of your image by using the ImageView.scrollTo() method.
- Afterwards you want to fire a scale animation, so you will get this beautiful scale effect.
- 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.
In fact, 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, so you can refer to both fragments without knowing who is who.
Just to make it clear, 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. Yep! When you click on that vertical TextView.
- clearFocus() is called when you release the inputs. For example, when you’ve entered your password or login, 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.
So far so good, but what about this Callback interface?
Well, we need to provide the ability to notify the adapter about any events which have recently occurred in a fragment. So, the adapter just implements this interface, and we can refer to the adapter from our fragments. Plain and simple.
Brace yourself! Here goes the code of the Adapter:
That’s undoubtedly the most important part here. Alright, let’s poke around!
You’ve seen the constructor before, right? We just simply calculate a factor for the gap, set the duration for the ViewPager. Also, we create an array of fragments, so it’s easier to fetch a fragment by its position.
- show(AuthFragment) 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(AuthFragment) is just a helper method which returns the width of the gap for each fragment.
- shiftSharedElements(float, float) 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(boolean) scales up/down the background image when a text input has lost/gained focus.
So, basically it’s all about the Adapter.
Login and Sign Up screens.
Okay, it’s time to set the main screens. It’s 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. So, it makes sense to have absolutely the same ids for each “duplicated” view.
With that being said, let’s take a look at the XML file of the Sign Up screen:
I’m using a simple TextInputEditText view for every single input field, which is wrapped into a TextInputLayout, so we get this floating label at the top.
In order to prevent duplication of attributes for every TextInputEditText view in the file, I created a style which contains all the common attributes. In fact, I did the same thing for the TextInputLayout views. Check out this here.
By the way, have you noticed something here? Yeah! 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:
Also, you can have a rectangular background of the inputs if you want. That’s a simple change in a line of code:
Attention! The inputs gain focus as soon as your application opens, which results in opening the keyboard. We don’t want this. That’s why I have a simple View at the top of the layout, which is set to capture focus:)
Just for the sake of completeness, here’s a snapshot of the Login fragment:
As you can see, I’m using a custom view called VerticalTextView. Yep! That’s the bold text which says Log In/Sign Up. You can check out the code here.
You can say “Why the heck do you need a custom TextView if you can just use the View.setRotation() method instead?”
Well, 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 view at all. This really messes up the layout:(
I took a snapshot on my phone, and if you look closer, you can see that the bounds of the view are still horizontal even though the text is vertical. (I enabled the Show layout bounds option before making the shot)
The point is that the VerticalTextView gets the job done. It has some problems with animation though (later in the article). Refer to this link for details.
It’s time to add some animations!
Basically, we have three types of animation here:
- fold animation.
- unfold animation.
- scale animation when the input has been touched.
Let’s define the lifecycle of animation:
click on Sign Up →SignUpFragment.unfold() →LogInFragment.fold()
click on Log In →LogIn.fold() →SignUpFragment.unfold()
Just a little recap:
Every time when the unfold() method gets called, the appropriate Callback.show() is called as well. Since the adapter implements the Callback interface, we can easily call the fold() method of another fragment, as a result we get two animations running at the same time.
- 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. Basically, 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 transition. Here’s a perfect font transition:
TextSizeTransition can’t draw the VerticalTextView (being set to vertical position) into a bitmap. It gets clipped for some reason.
However, we can slowly rotate the view by 90 degrees as it goes up. After the transition has stopped, we need set the rotation to 0, and call the VerticalTextView.setVerticalText(true) method, therefore we eliminate that extra space which has been left after rotating the view. Also, I translated the view to the left/right a little bit. I did this for two reasons:
- We need to have a little margin from the left/right (depends what fragment).
- We don’t have a jerky effect when setting it to vertical position.
Considering the fact that the TextView is set in the vertical position, we run into the same problem again — TextSizeTransition can’t have a vertical bitmap. So what we are going to do is completely opposite to what we did in the fold transition. 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.
In order to set the TextView in the horizontal position, we have to use the VerticalTextView.setVerticalText(false) 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 TextView.post() method.
You can see this process in the code above. Again, here’s what we get:
As you can see, the rotation from the horizontal position back to the vertical position happens super fast.
This is pretty straightforward. Every time when 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 stuff. When the keyboard is gone, we have to call the clearFocus() method which is used to clear focus (View.clearFocus() method) for the touched input.
If you’ve made this far, you should be able to reproduce this in your own app. In any case, you can check out the source code in my repository on GitHub.
Thanks for reading!:)