Android: rendering a path with a Bitmap fill
Posted by Dimitri | May 26th, 2012 | Filed under Programming
This Android tutorial shows how to render a Path that is filled by a Bitmap and displays stroke in a different color. It also explains how to manipulate the texture coordinates to make it independent of the position of the path, just like a mask, but without using any of the PorterDuff rendering modes. The code featured in this code was created and tested on Android 2.1, both on a real and at an emulated device.
Here’s a video of the example application in action:
If you can’t play the video, don’t worry: there is a screenshot of the application at the bottom of this post.
The main advantage in using this approach to create this inverse masking effect is that it simplifies any draw method from classes that extend from the Path, since the rendering mode isn’t changed. This kind of effect is achieved by creating a BitmapShader and applying it to a Paint object. Also, an additional Paint object will be required for the stroke, since Android doesn’t offer a simple way use a Paint object with different fill and stroke colors. Here’s the example project’s View code:
package fortyonepost.com.pbmpfill; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Shader; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import fortyonepost.com.pbmpfill.R; public class PathRendererView extends View { //Create a paint for the fill private Paint fillPaint; //Create a paint for the stroke private Paint strokePaint; //A Bitmap object that is going to be passed to the BitmapShader private Bitmap fillBMP; //The shader that renders the Bitmap private BitmapShader fillBMPshader; //A matrix object private Matrix m = new Matrix(); //Two floats to store the touch position private float posX = 105; private float posY = 105; public PathRendererView(Context context, AttributeSet attrs) { super(context, attrs); //This View can receive focus, so it can react to touch events. this.setFocusable(true); //Initialize the strokePaint object and define some of its parameters strokePaint = new Paint(); strokePaint.setDither(true); strokePaint.setColor(0xFFFFFF00); strokePaint.setStyle(Paint.Style.STROKE); strokePaint.setAntiAlias(true); strokePaint.setStrokeWidth(3); //Initialize the bitmap object by loading an image from the resources folder fillBMP = BitmapFactory.decodeResource(context.getResources(), R.drawable.bricks); //Initialize the BitmapShader with the Bitmap object and set the texture tile mode fillBMPshader = new BitmapShader(fillBMP, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); //Initialize the fillPaint object fillPaint = new Paint(); fillPaint.setColor(0xFFFFFFFF); fillPaint.setStyle(Paint.Style.FILL); //Assign the 'fillBMPshader' to this paint fillPaint.setShader(fillBMPshader); } @Override public boolean onTouchEvent(MotionEvent event) { //Store the position of the touch event at 'posX' and 'posY' if(event.getAction() == MotionEvent.ACTION_MOVE) { posX = event.getX(); posY = event.getY(); invalidate(); } return true; } @Override protected void onDraw(Canvas canvas) { //Invert the current matrix, so that the Bitmap background stays at the same position. //Only necessary if the canvas matrix is being transformed in some way. canvas.getMatrix().invert(m); //Assign the matrix to the BitmapShader. Again, not required to make this example work. fillBMPshader.setLocalMatrix(m); //Draw the fill canvas.drawCircle(posX, posY, 100, fillPaint); //Afterwards, draw the circle again, using the stroke paint canvas.drawCircle(posX, posY, 100, strokePaint); } }
The first pair of member variables being instantiated are two Paint objects (line 19 and 21). As previously explained, one is responsible for rendering the fill and the other one renders the stroke. Then, a Bitmap and a BitmapShader are instantiated (lines 24 and 26). The Bitmap will load an image file into the memory and the BitmapShader is going to be associated with the fill paint.
After that, a Matrix object is instantiated and initialized (line 29). This matrix can be used to manipulate any texture positioning transformation such as scale, offset among others. Not only that, but the Matrix can be used to store the inverse of the current canvas transformation, making the texture remain at the same position, even if the shape is translated, rotated or scaled. The last couple of variables are just floats that store the position of the touch events (lines 32 and 33).
Inside the constructor, the PathRendererView is set to receive focus, in other words, it can receive keyboard and touch events (line 40). Also, inside the constructor, all declared variables are initialized, such as the strokePaint at line 43. The following lines after this Paint object’s initialization sets some properties of its properties, like enabling dithering for better color results, setting the Paint color to yellow (A: FF, R:FF, G:FF, B: 0), and most importantly, setting the Paint style to stroke (lines 44, 45 and 46). By doing so, only the borders of the Path are rendered when using this Paint. Additionally, the anti-aliasing flag is set to true, which makes the Path look better by smoothing it’s edges and the stroke width is set to 3 pixels (lines 47 and 48).
Next, the fillBMP is initialized by loading the bricks.png image file placed at the resources folder (line 51). With the Bitmap object it’s passed as the first parameter to the BitmapShader constructor. It also takes two other parameters which defines the X and Y tile modes for the textures. In this example, the textured is set to repeat on both axis (line 53). This is internally setting the texture wrap mode at OpenGL.
The last member variable to be initialized is the fill Paint (line 56). The lines after it initializes the Paint color to black and the the Paint style to fill (lines 57 and 58). This way, any path rendered with this Paint will only get it’s inner part rendered, and not the borders. To make the fillPaint render a Path with the fillBMP, the fillBMPshader is assigned as this Paint‘s shader by calling the setShader() method (line 60). That way, any Path that uses the fillPaint will render a tiled brick texture inside it.
Moving on, the onTouchEvent() is being overridden. There’s nothing much to explain here: it just stores the position of the move touch event at the posX and posY variables. They are going to be used to move the path. In this method, the View is also being invalidated, causing it to be rendered again (lines 63 through 75).
Finally, the last method being overridden is the onDraw() method which is responsible for drawing any element on the screen. Inside it, the first thing it accomplishes is storing the inverse of the current canvas matrix at the m member variable (line 82). Then, the Matrix object is set as the fillBMPshader matrix. It makes the fillBMP to be rendered at the same position, regardless of any transformations made to the current canvas matrix. As the reader can see on the code comments, this isn’t necessary for this example: removing lines 82 and 84 would make the application behave the same way. However, since transforming the canvas matrix is a common operation and for the sake of completion, these two lines were added.
Now, all that’s left to do is to draw a Path twice, using the strokePaint the first time and the fillPaint the second time (lines 87 and 89). In the above code, a circle is rendered using the two paints. Here’s a screenshot of the application for those who can’t open Youtube:
Very interesting posting. You should be commended for your work. I have downloaded the source and played with it..and I still cant understand why the bricks appear to stay in the same place. Why wouldnt the circle be drawn with the same pattern each time?
Thanks!
The bricks stay at the same pattern because of lines 82 and 84.
You see, the local transformation matrix for the texture coordinates is being set as the inverse of the canvas matrix. To undo almost all transformations applied to a set of points by a matrix, all that is required is to multiply the set of points by the inverse of the said matrix.
In the case of the above code, since there are no transformations being applied to the canvas (for example, no canvas.translate(), canvas.rotate() or canvas.scale() method calls), the canvas matrix is an identity 3×3 matrix. The inverse of an identity matrix is the same identity matrix. That’s why, in the example above, lines 82 and 84 aren’t necessary for that specific case only.
In other words, this means that the fillBMPshader uses (X:0, Y:0) as the origin for the texture coordinates, and not the position of the circle as the reference.
I want to erase front image and show background image onTouch
what will i do?
That is a really great post, thank you!