📜 ⬆️ ⬇️

Themes design. Blackjack and WeakReference

Once I faced the task of making theme support in an Android application. What I wanted to get:

  1. The ability to switch design - change some colors and graphics
  2. The change should take place “on the fly”, only the design should change for the user, everything else (the content of the input fields, the position of the elements in the list, etc.) should not change
  3. In the future, I would like the topic to be changed without the participation of the user, for example, by the time of day
  4. I wouldn’t like to greatly modify the existing code or markup. Ideally, I would just like to somehow mark the elements in the markup
  5. It would be great to be able to upload new themes without updating the application.


About what was achieved and how it was implemented - under the cut.
')


The most obvious way that Stack Overflow and Android’s documentation offer us is Context.setTheme. One snag is to install the theme before creating all our View. Immediately it is clear that “on the fly” it is impossible to switch the topic, the user will definitely notice the complete re-creation of the entire contents of the Activity. Yes, and with the code of each Activity one way or another will have to tinker. I did not find any other recommendations on the Internet (if someone has information, I will be grateful for the link).

Well, we will write the implementation. Blackjack and WeakReference.

Let's start from point 4. I consider myself to be developers who prefer not to write code. I do not like to write code so much that I am ready to write a lot of code, just not to write it in the future. I can’t do anything with myself: I don’t want to think about the logic of interaction when a new window appears in order to take into account the susceptibility to a dynamic design change. I just want to indicate in the markup next to the element that, for example, its color will be equal to the background color.

This will help us tag property. If somewhere else in the application tags are used, for example, as Holder'ov for optimizing adapters in ListView, it is not scary. In the code, you can use setTag / getTag with the id parameter. If there are many such places, the search and replacement will help us.

Now let's think of some simple format for tags. First, we separate the wheat from the chaff and make the simplest test that this tag is really an indication of the use of our themes: our tag will always begin with the “!” Symbol. Next comes the name of the resource, for example “background”. Then some separator, something like “|”, and the type of resource — text color, background picture or background color, etc. For example, the background for the chat window using tiling: “! Chat | tiled_bg”. Maybe not too aesthetic, but quickly parse. In order to make a minimum of mistakes when writing such tags, it is better to put them into string resources and reuse - in our application, the resource! Primary | text_fg is used 77 times.

The most difficult thing is over, it remains only to somehow process these tags ... Elements with such tags must be processed immediately upon “inflating” (inflate) View, and then each time the topic is changed. “Inflating” actually occurs in two ways - setContentView in the Activity and using the LayoutInflater. Let's start with setContentView.

In our application, all Activities are inherited from a small number of basic Activities. It is enough to override the setContentView method:
public void setContentView(int id) { super.setContentView(id); HotTheme.manage(mActivity.getWindow().getDecorView()); } 

The getDecorView method returns the “root” of the View hierarchy.
To do the same when creating a View using a LayoutInflater, wrap it:
 public class HotLayoutInflater { private LayoutInflater inflater; private HotLayoutInflater(LayoutInflater inflater) { this.inflater = inflater; } public View inflate(int resource, ViewGroup root, boolean attachToRoot) { View v = inflater.inflate(resource, root, attachToRoot); HotTheme.manage(v); return v; } public View inflate(int resource, ViewGroup root) { View v = inflater.inflate(resource, root); HotTheme.manage(v); return v; } public static HotLayoutInflater wrap(LayoutInflater layoutInflater) { return new HotLayoutInflater(layoutInflater); } public static HotLayoutInflater from(Context context) { return new HotLayoutInflater(LayoutInflater.from(context)); } } 

Now - analysis of hierarchy View:

HotTheme.java:
  public static void manage(View... views) { for (View v : views) { simpleManage(v); if (v instanceof ViewGroup) { ViewGroup vg = (ViewGroup) v; for (int i = 0; i < vg.getChildCount(); i++) { manage(vg.getChildAt(i)); } } } } public static void simpleManage(View view) { Object t = view.getTag(); if (t instanceof String) { String tag = (String) t; if (tag.startsWith("!")) { tag = tag.substring(1); String[] elements = tag.split("\\|"); String base = elements[0]; for (int i = elements.length - 1; i >= 1; i--) { ThemedView tv = createThemedView(view, base, elements[i]); tv.notifyChange(); HotTheme.sViews.add(tv); } } } } 

As you can see, the View that our tag contains is pulled out of the hierarchy. Just in case, we consider that there can be several “|” delimiters - then the resource will be applied to each type (this can be useful).

Further, these elements are wrapped in a certain ThemedView, which is responsible for all the magic. The notifyChange method will apply the current theme to this View. Well, save ThemedView for the future to notify about the change of topics - nothing complicated.
The ThemedView class itself is a simple wrapper around View that prevents context leakage:
 private static abstract class ThemedView { private WeakReference<View> view; ThemedView(View v) { view = new WeakReference<View>(v); } boolean notifyChange() { View v = view.get(); if (v == null) { return false; } onChange(v); return true; } abstract void onChange(View v); @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ThemedView view1 = (ThemedView) o; View v1 = view.get(); View v2 = view1.view.get(); return (v1 != null ? v1.equals(v2) : v2 == null); } @Override public int hashCode() { if (view == null) { return 0; } View v = view.get(); return v != null ? v.hashCode() : 0; } } 

Now, when changing the topic, to apply it to all interested View it is enough to call:
  for (Iterator<ThemedView> it = views.iterator(); it.hasNext(); ) { if (!it.next().notifyChange()) { it.remove(); } } 

Small lyrical digression
I love java. Yes, Java is a bit slower C. Yes, it is less flexible than Python. But she (yes, for me Java - “she”) can do amazing things:
  1. Do for me. Seriously, I'm just telling her what I want. If I want a beer, she goes for a bottle, opens it and kindly waits while I enjoy a wonderful drink, after which she throws the bottle away. Thank you, GC!
  2. Think for me. I do not need to keep in mind the data types, as in languages ​​with weak typing. I don't need to think about whether memory is allocated on the stack or on the heap. When you have Java, you rarely have to think at all - it is often enough to explain to her what you want.
  3. Write the code for me. Javaassist, CGLib, java.lang.reflect.Proxy, JSR-269, annotations, reflections ... Metaprogramming with Java is great!
  4. Cast for me. And safe! Almost. At a minimum, until you yell at it with @SuppressWarning (“unchecked, rawtypes”). Thanks, Generics!
  5. Java is not proud. She can do Unsafe, despite the fact that it is contrary to her nature.

Yes, she has flaws. She likes to chat - I haven’t met a much more verbose Java (Pascal doesn’t count for the language, of course). But usually IDE allows you to overcome this with the help of various auto-permutations and templates.

Android uses Java. Yes, that's just none of its merits in it left. He looks more like a drunken unshaven peasant than a beautiful and submissive woman. I tell him - I want a beer, and he told me - get a constant, create an Intent, serialize the data, open the Activity, get the result ... If everything is fine, deserialize it and bring it to the type of "Beer". And yes, bear in mind that at any moment your operation to get a beer can be interrupted. Even if you already paid for it. Especially happy when you are in the context of a single physical process.



I constantly have to keep in mind what type of Message.obj will be based on Message.what. And make a huge switch. Very comfortably.

Code generation on Android is something else. You can almost forget about Javaassit / CGLib (there are some realizations of something similar, but the speed of their work leaves much to be desired). With the rest (Proxy, JSR-269, annotations and reflections) I periodically sin, but I have to make a lot of gestures in order to make it work at a more or less acceptable speed.

Android is proud. He can Unsafe. And this is not contrary to its nature (including NDK, RenderScript, etc.). Yes, only it is available exclusively through reflections, which destroys most of the advantages of Unsafe.

So, what is it for me? Due to the obedience of Java, such a tool as WeakReference is used quite rarely, only in the wildest erotic fantasies (for example, data consistency support in various ORMs). With Android instead of romance, WeakReference has to be used for domination in the BDSM style. We have to put up with the fact that objects live their lives, obeying the unknown life-cycle. You have to “cling” to them with the help of WeakReference, so as not to cause context leaks (Context). Perhaps it would be worthwhile to “bend” under Android, and in each activation, upon exit, “unregister” the View hierarchy, but the trouble is, it can change, and some of the View will not be there (especially typical for ListView, whose elements can constantly appear / disappear) from the screen). That's why I use the WeakReference almost always when some of the application modules affect the visual part - all the View are stored only on the WeakReference, which, of course, greatly complicates the logic of work.

Let's return to our ThemedView, in which in its onChange method we define what happens to the View:
  private static ThemedView createThemedView(View v, final String base, String element) { ThemeType type = types.get(element); switch (type) { case TILED_BG: return new ThemedView(v) { @Override public void onChange(View v) { Bitmap bmp = decodeBitmap(base + "_bg"); BitmapDrawable bd = new BitmapDrawable(app().getResources(), bmp); bd.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); v.setBackgroundDrawable(bd); } }; case VIEW_COLOR_BG: return new ThemedView(v) { @Override public void onChange(View v) { int color = getColor(base + "_bg"); v.setBackgroundColor(color); if (v instanceof ListView) { // There is an android bug in setCacheColorHint // That caused the IndexOutOfBoundsException // look here: // http://code.google.com/p/android/issues/detail?id=12840 // // Moreover, that bug doesn't allow us to setDrawableCacheColor // for recycled views. That's why we need to perform cleaning up // via reflections // // Fixed in android 4.1.1_r1 try { ((ListView) v).setCacheColorHint(color); } catch (IndexOutOfBoundsException ex) { try { Field mRecycler = AbsListView.class.getDeclaredField("mRecycler"); mRecycler.setAccessible(true); Object recycler = mRecycler.get(v); Method m = recycler.getClass().getDeclaredMethod("clear"); m.setAccessible(true); m.invoke(recycler); } catch (Throwable t) { // No need to report this } } } } }; case VIEW_IMAGE_BG: return new ThemedView(v) { @Override public void onChange(View v) { v.setBackgroundDrawable(decodeDrawable(base + "_bg")); } }; case IMAGE_FG: return new ThemedView(v) { @Override public void onChange(View v) { ((ImageView) v).setImageDrawable(decodeDrawable(base + "_bg")); } }; case TEXT_COLOR: return new ThemedView(v) { @Override public void onChange(View v) { final int color = getColor(base + "_fg"); if (v instanceof TextView) { ((TextView) v).setTextColor(color); } } }; case TEXT_HINT: return new ThemedView(v) { @Override public void onChange(View v) { final int color = getColor(base + "_hint_fg"); if (v instanceof TextView) { ((TextView) v).setHintTextColor(color); } } }; case PAGER: return new ThemedView(v) { @Override public void onChange(View v) { int active = getColor(base + "_active_fg"); int inactive = getColor(base + "_inactive_fg"); int footer = getColor(base + "_footer_bg"); TitlePageIndicator pager = (TitlePageIndicator) v; pager.setSelectedColor(active); pager.setTextColor(inactive); pager.setFooterColor(footer); } }; case DIVIDER: return new ThemedView(v) { @Override public void onChange(View v) { int color = getColor(base + "_divider"); ListView lv = (ListView) v; int h = lv.getDividerHeight(); lv.setDivider(new ColorDrawable(color)); lv.setDividerHeight(h); } }; case TABBUTTON_BG: return new ThemedView(v) { @Override void onChange(View v) { StateListDrawable stateDrawable = new StateListDrawable(); Drawable selectedBd = decodeDrawable(base + "_selected"); stateDrawable.addState(new int[]{android.R.attr.state_selected}, selectedBd); stateDrawable.addState(new int[]{android.R.attr.state_pressed}, selectedBd); stateDrawable.addState(new int[]{}, decodeDrawable(base + "_unselected")); v.setBackgroundDrawable(stateDrawable); } }; case EDITTEXT_COLOR: return new ThemedView(v) { @Override void onChange(View v) { int color = getColor(base + "_fg"); EditText edit = (EditText) v; edit.setTextColor(color); int hintColor = getColor(base + "_disabled_fg"); edit.setHintTextColor(hintColor); } }; case GROUP_TINT: return new ThemedView(v) { @Override void onChange(View v) { int tintColor = getColor(base + "_fg"); ImageView imageView = (ImageView) v; imageView.setColorFilter(tintColor, PorterDuff.Mode.SRC_ATOP); } }; default: throw new IllegalArgumentException("Error in layout: no such type \"" + element + "\" (" + base + ")"); } } 

The types.get (element) code simply returns enum on a lowercase string.

Of the interesting are the methods decodeBitmap, decodeDrawable and getColor:

  private static ResourceInfo findResource(String base, ResourceType type) { return sCurrentProvider.findResource(base, type); } public static Drawable decodeDrawable(String base) { ResourceInfo info = findResource(base, ResourceType.Drawable); return info.getResources().getDrawable(info.getResId()); } public static Bitmap decodeBitmap(String base) { ResourceInfo info = findResource(base, ResourceType.Drawable); return BitmapFactory.decodeResource(info.getResources(), info.getResId(), Util.newPurgeableBitmapOptions()); } public static int getColor(String base) { ResourceInfo info = findResource(base, ResourceType.Color); return info.getResources().getColor(info.getResId()); } 

The sCurrentProvider is an object of the ThemeProvider class, whose only task is to obtain resource information by its name and type.
The simplest implementation will add some theme ID as a prefix to the resource name:
  @Override public ResourceInfo findResource(String name, ResourceType type) { int id = IdUtils.getResId(app().getResources(), mPrefix + "_" + name, type.getType(), PACKAGE_NAME); if (id == 0 && mNext != null) { return mNext.findResource(name, type); } return new ResourceInfo(app().getResources(), id); } 

The getResId method is a small wrapper around the Resources.getIdentifier method.
The mNext field is also a ThemeProvider object. It is needed in order to search the chain, if the resource was not found (eventually the default will be taken).

As a result, in order to make another topic, you just need to add a set of necessary resources, adding some prefix. For example, resource names for the chat window background:
def_chat_bg
night_chat_bg
pink_chat_bg
wood_chat_bg

Total

As it was said at the beginning, it would be great to be able to load resources not only from the application itself, but also from the outside. The source may be, for example, another application. In this case, everything will work the same, except for the package name and the Resources object. This, in turn, can be obtained through PackageManager.getResourcesForApplication.

Remember again what we wanted to achieve:
  1. Ability to switch design - done
  2. The change should take place “on the fly” - ready
  3. In the future, I would like the topic to change without the participation of the user - the prospect is peeped out, there are no obstacles
  4. I would not like to substantially change the already existing code or markup - the code has actually changed quite a lot, but mostly with the help of search and replace, so here you can also put a plus
  5. Upload new themes without updating the application - ready, resources can be loaded from any apk


Looks like it worked out. Thanks to everyone who mastered the article to the end. I hope someone will use the described technique in order to give your application the ability to adapt to the mood of the users - believe me, they will thank you!

PS: well, and, as expected, a minute of advertising - Agent's application with the topics here .

Source: https://habr.com/ru/post/178061/


All Articles