Hi, Habr!
Recently, I had the interesting task of repainting an application on a JSON object pulled from a server. Google dictates the idea that all colors / themes are in xml. Because of what a slight movement of the hand will not work everywhere to replace any R.color.primary_button from blue to green.
If you are interested in a small retelling of the weekly adventure for Resources, then welcome to cat.
A little background
Our application has several variations, each of which is registered using productFlavors. Any change to any trifles (for example, text color) requires the intervention of the developer, so a number of measures were taken to separate the application and its resources. As part of this task, we also noticed that any change in the color scheme entails updating the application in PlayMarket / AppStore. Therefore, one of the developers came up with the idea: "And let's push the color scheme from the server and repaint the application at runtime."
')
So, what is the action field:
- 47 different screens;
- ~ 50 shapes and selectors;
- ~ 70 different colors (some elements may have a gradient and a frame, others may be specific for a particular screen).
By existing experience the following solutions were highlighted:
- In each Activity, write the code that will repaint the UI (the solution is in the forehead, all Views are assigned an id, and in each Activity the colors are programmed).
- Inheritance from all used UI elements (development of the first solution, excluding changes to the Activity, xml is rewritten for the place of this).
- A wrapper over Resources or over anything else that would allow the required task to be performed during the creation of a View or Shape.
Then I will go research on the third solution.
Experiment number one. Attempt to wrap Resources
Android has a monopolist for all resources - this is Resources. Any creation of a View or Shape receives an instance of this class from the passed in context constructor. And the only way to intervene in the work of the constructor is to change the Context.
Google has nothing against it and gives us access to ContextWrapper, which is a complete wrapper. Context substitution occurs via the attachBaseContext overload. There is nothing difficult.
Now about the Resources class
When studying this class, it was found that many of the methods that I would like to overload are package methods. No one bothers to overload, for example, getColor, but it is not used either in the construction of the View or in TypedArray (needed to retrieve a set of resource values corresponding to the transferred set of attributes). And what is used is hidden. Thus, the first naive idea failed.
But at the same time, the use of TypedValue and TypedArray was abundant. In general, Resources and work with it are built on active work through these two classes.
There are no problems with the first one; there is a getValue method in Resources. By overloading this method, you immediately get a properly working getColor (in the case of color) and getDrawable (in the case of ColoredDraawble).
And with TypedArray everything is much worse. This class is not wrapped because its constructors are private. His fields are closed and he does not have methods for changing them. Intervening in its filling also does not work, because it happens through the final class AssetManager. The only thing I had to do with it was to get access to the right field through reflection.
As a result, this method is operational. At least, the first screens were completely repainted. By intervening in the work of TypedValue and TypedArray, in terms of resources, you can change almost anything you want. But I did not begin to bring it to the end, since I consider reflexion risky and resort to it in extreme cases.
Already during the second experiment I encountered another problem with the wrapper Resources. It turned out that Android already exists android.support.v7.widget.ResourcesWrapper. Its implementations can wrap your class for some component and produce a completely different result. By the way, ResourcesWrapper is packaged and hidden for mere mortals.
Experiment number two
Due to the inability to do everything centrally, the task was divided into two parts:
- Replacing resources in View.
- Replacing resources in Shape and Selector.
O view. LayoutInflater spoofing
Probably many are familiar with
github.com/chrisjenx/Calligraphy . For the second experiment, the idea used in this library was chosen, namely the substitution of LayoutInflater. The substitution of LayoutInflater is also done through ContextWrapper. Inside LayoutInflater, the factories that process the View are redefined (one of them, unfortunately, through reflection). And inside the factory, the code is implemented, which, depending on the View and attributes, is engaged in the substitution of necessary resources.
About Shape
It's harder here. There are no factories for them. The creation takes place inside the Resources through the static method createFromXml, which parses the xml file transferred, and then uses TypedArray. Similarly with ColorStateList.
To intervene in the work of creation will not work (except for the method described in the first experiment). And the created object does not store an Id of the resource in itself, which is why it will not be possible to repaint it after creation. But you can go around. There is a getXml method in Resources. It allows you to get any xml and parse it yourself. Thus, having Id and Resources you can get any Drawable and make the required changes to it.
ColorStateList (Unlike any implementation of Drawble) does not allow to change its content. Here either use reflection, or create a new instance and implement caching on its side.
More about resource cache
Initially, it was hoped to use the Resources cache just by changing the necessary Drawable and ColorStateList in it. But this had to be abandoned for two reasons.
The first is described above and affects the ColorStateList. Without reflection, the properties of its instances cannot be changed, which means that the instances cached in Resources cannot be used.
The second is related to the caching of ColorDrawable and single ColorStateList (this is when ColorStateList is requested for a color, not a selector). Their caching is optimized and occurs not by the resource id, but by the color referenced by the resource.
Result
As a result, the application has:
- Its own LayoutInflater, which makes changes to the View.
- Great Singletone with a set of getDrawable (int resId, Resources baseResource) method methods that store color schemes, Drawables, and ColorStateLists.
- The base activity containing repainting bar status and context wrapping.
The task was solved with a slight change in the existing code (for example, where the text color changes programmatically depending on the result of the calculations). And on the further development should not particularly affect.
The fee for this: at least increased load when creating a View, in the case of Shapes and Selectors - double. As well as possible problems when switching to the next version of the API (we are currently using 24) and device specific bugs.
I believe that among you there are those who have encountered similar problems. And it would be interesting to see your thoughts on runtime repainting in the comments.
Thanks for attention!