Today we will make TextView render its text as transparent so that we can see underneath the TextView, for example the activity background. This technique uses the Porter’s and Duff’s transfer modes. A ready-to-use library along with its source code can be found here.
How to use the library
First, set up the dependency in gradle (from jcenter):
compile 'it.gilvegliach.android:transparent-text-textview:1.0.3'
Then, just use it in xml:
<it.gilvegliach.android.transparenttexttextview.TransparentTextTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/view_bg" android:text="Hello World" />
Or programmatically:
TransparentTextTextView tv = new TransparentTextTextView(getContext()); tv.setBackgroundResource(R.drawable.view_bg); tv.setText("Hello World");
The code is available on GitHub.
The technique
The high-level idea is quite simple: use a mask and carve out this mask from the TextView background. We can break this down in steps:
1. Draw the mask in a buffer
2. Draw the TextView’s background in another buffer
3. Carve out the mask from the background buffer
4. Draw the result on the final canvas
It is easy to translate this into code. Let’s start with the high-level
`onDraw()` method:
@Override protected void onDraw(final Canvas canvas) { drawMask(); drawBackground(); canvas.drawBitmap(mBackgroundBitmap, 0.f, 0.f, null); }
The method `drawMask()` implements step 1. The buffer is simply a bitmap, `mMaskBitmap`. In order to draw into this bitmap, we need a canvas linked to it, so that we have an api to issue draw calls: we call it `mMaskCanvas`. Then the method becomes:
// draw() calls onDraw() leading to stack overflow @SuppressLint("WrongCall") private void drawMask() { mMaskCanvas.drawColor(Color.BLACK, Mode.CLEAR); super.onDraw(mMaskCanvas); }
Note how we leverage the rendering code of TextView calling `super.onDraw()`. We cannot call simply `draw()` because it calls internally `onDraw()` and so we would enter an infinite cycle that will throw a stack overflow exception. Next we draw the background, steps 2 and 3:
private void drawBackground() { mBackground.draw(mBackgroundCanvas); mBackgroundCanvas.drawBitmap(mMaskBitmap, 0.f, 0.f, mPaint); }
First we draw the drawable `mBackground` on a second buffer `mBackgroundCanvas` and then we carve the mask out of it. The magic of the carving is in the paint object:
mPaint = new Paint(); mPaint.setXfermode(new PorterDuffXfermode(Mode.DST_OUT));
The second line sets what is called a Porter and Duff transfer mode (Xferm is for transfer or transformation). The two computer scientists wanted to define an algebra for composing digital images. This means giving mathematical functions that, given two images input, return as output a third image, the result. In concrete terms, these functions represent operations such as overlapping, superimposing, blending, carving out, etc. If you want, you can read on for a brief explanation or zero in directly in their paper.
Let’s dive into more detail: each pixel can be represented by four values or channels: alpha, red, green and blue. A function takes two pixels, i.e. 8 values, and outputs a new pixel. Looping through corresponding pixels in two images, we can create a whole new third image. Of course, the images should have the same size but this is generally trivial to fix.
Let’s see now how to define the `carving` function. We consider two pixels, one from the first image, or ‘source’, and one from the second image, or ‘destination’. We split the first pixel in alpha `Sa` (source alpha) and color `Sc` (source color), a combination of the red, green and blue channels. Similarly, the destination pixel is split in `Da` and `Dc`. The carving function, or in Android DST_OUT, is:
carving(Sa, Sc, Da, Dc) = [Da * (1 - Sa), Dc * (1 - Sa)] alpha color
As you can see, the value of the destination pixel is in the result if and only if the alpha of the source pixel is 0, which means completely transparent. In high-level terms, if we set the TextView background to the destination and the mask to the source, a pixel in the background will be drawn if and only if it matches a transparent pixel in the mask. Since the text is drawn in black on a transparent background in the mask, the final effect will be exactly what we want.