Sunday, May 19, 2013

Android UI - Modifying Android views

In the previous part we discussed essential concepts of Views in Android, so now it's time to try something on practice. Today we'll create something like this:



As you can see, it's an image with a counter (of something) in the bottom-right corner.

From this arcticle and farther: by "widget" I mean some implementations of android.view.View that located in android.widget package (such as TextView, ImageView, EditText, etc.)

Where to start?

We have four different options to complete our task (arranged by increasing of complexity):
  1. Search for an existing one - indeed, maybe someone already acomplished it (or even maybe it is a default Android widget). However, sometimes it's faster (and may be more reliable, depending on your experience) to create your own implementation than dealing with someone's code (or bugs).
  2. Compose view from default Views - it's pretty simple to just create your own layout, fill it with views and arrange them in a required order.
  3. Extend some existing View - when all you need is slightly modify the appearance (or behaviour) of default widgets it's common to just override some key methods of base class (widget), use default implementation (by calling super) and then provide your own details. This approach also provides you power of polymorphism, so you can continue to use base-class in your code.
  4. Create your own View (or ViewGroup, in case you're working with layouts) - this is the most flexible (as well as most complicated) way to create your own unique behaviout and appearance of View.
Let's skip first option and continue with the rest.

Development


Composing custom View from existing one may sound great, but there is two problems. First of all - we can't fully use power of canvas-drawing. In our case - we can't draw such "transparent" text (in other words - hole) over the circle. Ok, let's forget about effects (imagine that we actually need just black or white text), we can compose our simplified View from FrameLayout with ImageView and TextView inside. Here is approximate XML:



But second problem arises. Adding additional layout and views requires additional measurement and layout processes, which causes additional overhead. Here how it may look like:
  1. Parent of FrameLayout will try to measure it.
  2. To find out it's size FrameLayout will need to find out the size of both ImageView and TextView (which is not that simple even for TextView). + 2 calls of onMeasure
  3. Now, when childs of FrameLayout was measured, it can finally calculate it's own size. + 1 call of onMeasure (well, actually it was called in first step, but it was completed just now).
  4. Now, when FrameLayout is measured, it's time to layout it's childs (to achieve required appearance). Additional computations.
  5. Finally, FrameLayout will draw itself and it's childs.
On practice, however, you won't notice significant difference. But, when there will be a lot of such Views, it may affect overall performance.

On the other hand, it's not so hard to create your own fast and efficient View for this purpose. So, since we're working with image, we'll most likely need all that behaviour and usefull methods provided by ImageView class. Then let's extend it!



Looks like it's time for explanation :)

First things first - we want to keep all standart look and behaviour (except our custom appearance), so we're keeping all constructors from the base class.

What we are actually doing in this View is creating Bitmap with our white circle and text, and then drawing it on canvas right above drawings of the base class (ImageView). Here is step-by-step process:

  • We creating new Bitmap inside onMeasure method if there was no bitmap before (it was null) or if size of our view has changed. Also, we're creating Canvas that represents our Bitmap, so we can draw directly into it.
  • Inside onSizeChanged we're calculating radius of our white circle.
  • Then onDraw method is invoked, which requires additional explanation. First, we're calling super version to draw default ImageView appearance.
  • onDraw: we're drawing our white circle into our Bitmap (using Canvas) using mCirclePaint (instance of Paint class, which describes parameters of our drawings, such as color).
  • onDraw: we're drawing out text into Bitmap. Key moment here is that we're using Paint (mTextPaint) with Porter-Duff Xfer mode. I'll provide additional explanation in next articles, but for now I'll just say that in our case it "erases" area on our white circle, so it will look like hole (shaped like text).
  • onDraw: now, when we finished drawing into our Bitmap, we draw Bitmap itself into Canvas provided as a method argument.
And here is the result:


In the next part we'll cover Xfer modes in more details.

No comments:

Post a Comment