- Change Android Themes Instantly Using the Circular Reveal Animation
- The Telegram Animation
- The Concepts
- Implementing the Feature in PDF Viewer
- Circular Reveal Animation
- Conclusion
- Change Theme Dynamically with Circular Reveal Animation on Android
- Introduction
- Basics
- Implementation
- Layout
- Change Theme
- ThemeManager
- Custom Views
- Dynamic theme changes
- Views with reactive theme changes support
- Custom LayoutInflater
- Finally
- What next
- Conclusion
Change Android Themes Instantly Using the Circular Reveal Animation
In its most recent update, the Telegram app for Android introduced an eye-catching animation that happens when users change the theme. Luckily for us, the source code is available on GitHub, so we can take a closer look at how the effect is achieved.
In this article, I’ll delve into the process of achieving this fancy transition, beginning with the concepts and ending with the actual implementation. Then I’ll try to apply it to PDF Viewer for Android.
The Telegram Animation
The introduction of this animation was warmly welcomed by the Android community. In fact, there are many subreddits on the topic asking for information about the technique used. The reason for all this interest is due to the complexity of the Android theme system.
The Concepts
The entire algorithm can be easily unraveled with the use of two key steps: taking a screenshot of the window, and using ViewAnimationUtils#createCircularReveal() .
Once the transition starts, here is the logic:
Take a screenshot of the window.
Set the screenshot as an image bitmap in an ImageView that covers the entire window.
Make the ImageView visible.
Apply the circular reveal effect to the drawer layout container.
Change the theme.
Make the ImageView invisible, setting its state as GONE .
ℹ️ Note: The moment when the theme changes (step number 4), the layout will have the ImageView on top, covering the new theme; the only change that can be visually perceived in this step will be that of the navigation bar, because it is positioned outside of the area covered by the ImageView .
Once you understand the key concepts, the code is rather simple.
Implementing the Feature in PDF Viewer
I began experimenting with implementing the feature in PDF Viewer for Android, and it became immediately clear that obtaining a screenshot by using a draw call on a canvas (i.e. drawerLayoutContainer.draw(canvas) ) was not a viable option with our codebase. Given that our theme chooser UI was a DialogFragment , the outcome would have been a bitmap with a transparent background instead of a full screenshot of the window.
One approach for solving this problem was to take a screenshot of the entire activity. There are many libraries on GitHub that can do this, but I’d recommend taking a look at Falcon.
Using Falcon to take a screenshot requires only one line of code: val bitmap = Falcon.takeScreenshotBitmap(activity) .
To show the screenshot on top of the other views, there are two options:
Add a new ImageView to the current layout that takes all the space matching its parent width and height.
Start a new activity with a minimal layout composed of a single ImageView .
Before going further, we should stop for a moment here and consider a few things. Starting a new activity just for showing an animation may sound like overkill for this particular case, however, adding an image view to the current layout might not produce the expected result.
In the specific case of PDF Viewer where there is a DialogFragment containing the theme chooser UI, the first option will not draw the screenshot bitmap on top of the dialog, but rather behind it. A workaround might be closing the dialog immediately after the theme has been selected and then drawing the image view immediately before starting the circular reveal transition.
Because this solution entails a radical modification of the current behavior of the app, I decided to discard it. I won’t go into more detail about the challenges I encountered while trying this, but below you can see an example of the outcome. Notice how clear the flickering is and how the content of the image view is slightly misaligned.
Due to the above, I decided to explore the second solution.
The next step is related to the configuration used in PDF Viewer for the system UI visibility, so there’s not a unique approach that fits every configuration.
In our settings activity, we are using the default SYSTEM_UI_FLAG_VISIBLE , which implies having a solid status bar.
Because of the SYSTEM_UI_FLAG_VISIBLE flag, the screenshot needs to be taken without including the status bar. Falcon takes the screenshot of the whole window, status bar included. We can still rely on the Falcon implementation, but the portion including the status bar needs to be cut out from the bitmap.
A simple solution can be achieved in this way:
The screenshot activity needs a transparent theme to allow the settings activity underneath it to be seen. In the AndroidManifest.xml file, set android:theme=»@style/AppTheme.Transparent» for the screenshot activity and use the following theme:
To make the transition from one activity to another quicker, it’s better to set the FLAG_ACTIVITY_NO_ANIMATION flag and override any pending transaction:
The ScreenshotActivity requires three parameters: the bitmap, and the x and y coordinates. Passing the coordinates can be easily achieved using the Intent :
The bitmap cannot be passed using the Intent , as it can easily go over the byte limit imposed for the Parcelable objects and may produce the following Java binder error:
JavaBinder﹕ . FAILED BINDER TRANSACTION .
It’s a well-known problem, and there are several solutions to overcome it. For the sake of brevity in this blog post, I’ve used a static field to hold the reference of the bitmap, even though it’s not advisable.
When the screenshot activity starts, the bitmap must be shown immediately:
Circular Reveal Animation
The reveal animation is the final thing that needs to be applied to complete the theme switching. The image view containing the screenshot is visible at this point. We don’t have control over the content behind it, so to keep this experiment simple (and short), I’m going to apply createCircularReveal to the image view itself. I’ll keep the tweaking out of scope, but to better give the gist, we are going to call the startCircularAnimation method in onCreate :
ℹ️ Note: CubicBezierInterpolator can be retrieved from the Telegram source code on GitHub.
If we had just started the circular reveal animation in onCreate , we would be greeted with an IllegalStateException with the message Cannot start this animator on a detached view! . We need to use an OnAttachStateChangeListener to make sure we are starting the animation on the attached view:
And finally, the entire animation is complete!
The transition from one activity to another can still be seen, even with the FLAG_ACTIVITY_NO_ANIMATION flag. This is probably due to some timing issue and would require more experimentation to tweak the delay on the activity start and recreation.
Conclusion
Studying the circular reveal animation and how the developers at Telegram implemented it was an exciting journey. Applying it to your codebase the exact same way they did may not always be possible, but it was interesting experimenting with some alternative approaches and checking the final result.
Depending on your app’s architecture, it may not always be possible to achieve complicated animations. For our specific case, dealing with Dialog s and DialogFragment s exponentially increased the complexity of the experiment. Most of the time, the strategy adopted by wise developers is to pinpoint a compromise between the time you have at your disposal and the complexity of the feature you want to achieve.
The folks working at Telegram have done a splendid job putting together a transition ahead of its time — one that brings something new to the tricky topic of switching themes. Telegram remains one of my favorite messaging apps for Android, and its UX is top-notch.
Источник
Change Theme Dynamically with Circular Reveal Animation on Android
Introduction
Dark theme on Android is on a hype at the moment. More and more apps add support for Dark theme over time allowing users to customize their phones, save battery and provide better accessibility. Though another trend which grows today is animated theme changes. First app I saw support of this was Telegram:
Source
After that in different modifications such feature started to appear in other apps, for example VK.
The effect is amazing. And in this article we’ll go into how to implement such functionality in your app.
Disclaimer: Android UI Toolkit currently is pretty bad, especially in dynamic styling. To implement such a feature one need to have custom views with custom theming support in the app. If your app is small, then there is no actual need for investing time into implementing your custom views. Hopefully with help of Jetpack Compose we’ll get more adequate dynamic theming and implementation of such a feature will be more straightforward. Though in this article we’ll try to get some simple solution for dynamic customization.
Basics
Thanks to developers of Telegram app for making code open sourced, so we can take a look at how developers implemented this feature in the repository (lines highlighted).
There is a lot of code, so let’s get the idea of the whole algorithm.
In Android 9 we have native Dark Theme support. The issue is that it is part of configuration and Android UI Toolkit doesn’t allow to change configuration dynamically and to change theme it is needed to restart all the activities in the app. That is clearly not the solution we are looking for.
Basic support for Dark Theme with app restart is fine and is better than only providing just light theme. But to provide a really good UX it seems without custom theming we’ll not be able to achieve what we want.
The main algorithm is the following:
- we have some hidden ImageView on the screen
- on request to change theme we draw all the content of the screen on the Canvas
- convert content of Canvas into Bitmap and put it into ImageView
- change theme on all views
- start circular reveal animation for our content of the screen (from 0 radius to full size)
This will create animation when new theme reveals with animation over old theme.
Sounds hacky, though pretty working solution.
Also with such a technique we can implement other different effects, for example we can implement old theme to disappear with revealing new theme underneath. For such we’ll have:
- hidden ImageView as in previous case with setting content of screen as Bitmap into it
- change theme on all views
- start circular reveal animation for our image (from full size to 0 radius)
Let’s dive into how this can be implemented
Implementation
Layout
We’ll start from describing our test layout:
Here we’ll have our hidden ImageView and container (LinearLayout containing out test TextView and Button which we’ll use in our test).
Change Theme
Here is our code which is responsible to change theme with animation:
Let’s look in details what we do here:
- first of all we make a defensive check to not start new animation when previous is still in progress
- then we get the dimensions of the container with content to create bitmap, draw all the content into that bitmap and set that bitmap into ImageView
- we have left TODO in the code, where we’ll need to change theme for all views. We’ll get back to that later
- finally we start reveal animation for container, which will actually reveal our new theme
Next we should somehow make our views to support dynamic theming.
ThemeManager
We’ll create single place for managing application theme — ThemeManager, and object which describes the Theme itself.
Theme will contain different sub-Themes for each design component — TextView, Button etc.
For example for TextView we can create the following Theme description:
Similarly for container (e.g. LinearLayout) we can create:
And the Theme itself will be enum containing different combinations mapped over some finite number of themes — for example LIGHT and DARK:
And our ThemeManager will just provide single instance of current theme:
Custom Views
Next we need to create our own wrappers for views to support changes of dynamic theme changing.
Let’s make our own custom TextView as example:
We just create our view with setTheme method, where we apply all the required fields to be styled.
Then we replace in xml our TextView with com.MyTextView and in our TODO access that view and set theme.
Such approach would work, but the issue is that it doesn’t scale and it requires layout files changes. But there is a way to fix all the issues, let’s see how.
Dynamic theme changes
Instead of explicitly setting themes on each view it would be better if views subscribed to theme changes and reactively updated by themselves.
For this we’ll add a bit more functionality into ThemeManager — we’ll add ability to add listeners. Yes, listeners in 2020 🙂 One can use RxJava or kotlin Flow, it actually doesn’t matter. Listeners will be enough, so, we’ll use them.
Nothing really interesting, just added listeners, ability to add and remove them and we update listeners on each change of theme.
Views with reactive theme changes support
Using these listeners we update our MyTextView to trigger update on theme changed:
Instead of setTheme method we now have onThemeChanged. And in different callbacks of View lifecycle we subscribe and unsubscribe from ThemeManager.
Custom LayoutInflater
In order to not change our layouts we can use custom layout inflater factories. The idea is that we can intercept inflating views based on the names (“TextView”, “Button”, “com.MyTextView” etc.). And we can provide our own implementations for base views. Same approach is done in AppCompat.
Basic implementation of the LayoutInflater.Factory2 looks like this:
We intercept in onCreateView some views which support our dynamic theme changes and other views we ask for AppCompatDelegate to create.
But there is one trick with setting this factory — it should be done before super.onCreate(. ):
Finally
And we’re good! Let’s look at the results (sorry, for artifacts in gif, unfortunately video recorder in Android Studio didn’t want to record without them 🙁 ):
Also if we change our setTheme method we can implement another version:
https://medium.com/media/2d2f34f59619ecdf5f27ee3bda92e91f/href
Awesome results, it works pretty smooth and provides a good UX with not that huge effort so far.
What next
You can find code for these in this gist.
Also if you’d like to play with this as an exercise you can change place where reveal animation starts. Currently we use center for simplicity, but you can use for example Switch (which changes theme) as a pivot.
Conclusion
I hope you found this article useful and maybe some ideas will help you to implement new cool features in your apps.
Don’t forget to try new things, read code written by others, add animations to your app for better UX.
Thanks for reading!
If you enjoyed this article you can like it by clicking on the👏 button (up to 50 times!), also you can share this article to help others.
Источник