Source code
You can find the latest source code over at github: the source at github.
Introduction
In my previous post I tried to provide a kind of one stop shop for things about multi touch on the Android platform. My initial intention was to do the same for drawing in 2D.
However, while documenting myself I quickly came to the conclusion that this subject is big. So I have reset my target. My main concern was with how to define shapes in XML resources, draw shapes and then to apply some transformations on these objects.
And that is what I will cover in this post: define drawing objects in XML resources and then apply transformations, like roating, scaling, etc… on them.
So, without aby further ado:
Background
Drawing from XML resources
Drawing from XML resources requires 3 steps:
- Define the resource in XML
- Load the resource
- Draw the resource on the screen
Step 1: Defining the shape as a resource
You define the resource in the resource directory of your project. You will notice you have several resource folders:
- drawable
- drawable-ldpi
- drawable-mdpi
- drawable-hdpi
This allows you to specify specific resources according to the screen resolution of the targetted Android device. The important word in previous sentence is “resolution”. Mind that this is not the same as screensize! Stackoverflow has a very nice postabout this.
Defining the shape in the xml goes like this:
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<stroke android:width="2px" android:color="#ffffff" />
<solid android:color="#ffffd300"/>
</shape>
Step 2: Loading a resource as a shape
Resources res = context.getResources();
try {
rectangleDrawable = (Drawable) res.getDrawable(R.drawable.rectangle_drawable);
} catch (Exception ex) {
Log.e("Error", "Exception loading drawable: " + ex.getMessage());
}
In the above code, the context
variable is the context as given in the constructor of your view class, or if you are loading the resources inside your activity, then you can use the this
variable
rectangle_drawable is the name of the xml file created in Step 1
Step 3: Drawing the shapes
protected void onDraw(Canvas canvas) {
rectangleDrawable.setBounds(x, y, x + width, y + height);
rectangleDrawable.draw(canvas);
}
Drawing shapes is simple: you set the size of the shape and call the draw
method of the Drawable class providing it the canvas on wich to draw.
Draw shapes created in code
Drawing shapes in code requires 2 steps:
- Create the shape in code
- Draw the shape
Step 1: Create the shape in code
rectangleDrawable = new ShapeDrawable(new RectShape());
ovalDrawable = new ShapeDrawable(new OvalShape());
arcDrawable = new ShapeDrawable(new ArcShape(45, 300));
Creating shapes in code is not at all difficult, but if you’re used to creating shapes in XML you might be in for a surprise: allthough you may think that providing a shape specific class to the constructor of the ShapeDrawable is similar as defining the shape of the drawable, this is NOT the same as choosing the shape to draw in the XML file. The class of the shapes created by defining them in XML is GradientDrawable and NOT ShapeDrawable.
Step 2: Drawing the shape
arcDrawable.getPaint().setColor(0xff74AC23);
arcDrawable.setBounds(x, y, x + width, y + height);
arcDrawable.draw(canvas);
Drawing the shapes is the same is when drawing shapes created from resources.
Applying transformations: using the Matrix class
I will not explain the basics of transformations and how matrix algebra fits in. There are enough references on the internet explaining this.
Using the matrix class,you can chain several types of tranformations together. The four supported types which you can apply are:
- Translation
- Rotation
- Scaling
- Skew
Each of these transformations has 3 methods in the Matrix class:
- A method to apply the transformation in front of all current transformations of the Matrix. This method starts with
pre
followed by the name of the tranformationtype - A method to apply the transformation in the end of all current transformations of the Matrix. This method starts with
post
followed by the name of the tranformationtype - A method to apply the tranformation instead of all current transformations of the Matrix. This method starts with
set
followed by the name of the tranformationtype
An important remark: a transformation is always made with repect to a point (except for a translation). This point is the origin of the drawing canvas, and in Android this point is at the upper left corner of the screen.
Translation
A translation in the xy-plane is defined by movement along the x-axis and along the y-axis.
A translation is done by any of the methods preTranslate
, postTranslate
and setTranslate
Rotation
A rotation in the xy-plane is defined by an angle expressed in degrees. The rotation is around the origin of the screen which is, as allready mentioned above, at the upper left corner of the screen.
Scaling
A scaling in the xy-plane, which in the case of Android is anisotropic, is defined by a scaling along the x-axis and a scaling along the y-axis. The scaling is again with respect to the origin of the screen, thus the upper left corner. As a result also the distance of the object you draw on the screen is multiplied by the scaling factor.
Skew
A skew, or in mathematics known as shear in the xy plane is also defined by two variables: kx and ky.
Applying transformations
Applying transformations in code is done as follows:
Matrix matrix;
matrix = new Matrix();
matrix.postTranslate(dx, dy);
matrix.preScale(sx, sy);
canvas.concat(matrix);
rectangleDrawable.draw(canvas);
Applying transformations: using the Camera class
While the Matrix class allows you to define simple tranformations, the Camera class allows you to define transformations as they would happen when looking at the drawing plane from a certain point of view in space.
As such, it defines methods allowing you to position the camera in space:
- rotationX: rotation around the X-axis
- rotationY: rotation around the Y-axis
- rotationZ: rotation around the Z-axis
- translate: movement along the X, Y and Z-axis.
Rotation around the main axises
Rotations around the X-axis and Y-axis result in a type of deformation of the object you are transforming which can not be reproduced by a transformation defined by using a Matrix transformation. A rotation around the Z-axis is comparable with a regular rotation as defined by a Matrix tranformation.
Translation along the main axises
Translation along the X and Y axis corresponds to a regular translation using a Matrix transformation. A translation along the Z-axis can be seen as a scaling using a regular Matrix tranformation.
Applying transformations
Applying transformations in code is done as follows:
Camera camera;
camera = new Camera();
camera.rotateX(rotationX);
Matrix cameraMatrix = new Matrix();
camera.getMatrix(cameraMatrix);
canvas.concat(matrix);
rectangleDrawable.draw(canvas);
Do try this at home: the code
The code has four views allowing you to experiment with what has been explained above. When you start the application you will see the following screen:
Each entry corresponds with a view allowing you to experiment with that feature. Following is an explanation of what entry corresponds with what view/java file and the configurations possible in that view:
Drawable in XML: CustomDrawableFromXMLView
public CustomDrawableFromXMLView(Context context) {
super(context);
Resources res = context.getResources();
try {
rectangleDrawable = (Drawable) res.getDrawable(R.drawable.rectangle_drawable);
ovalDrawable = (Drawable) res.getDrawable(R.drawable.oval_drawable);
lineDrawable = (Drawable) res.getDrawable(R.drawable.line_drawable);
ringDrawable = (Drawable) res.getDrawable(R.drawable.ring_drawable);
} catch (Exception ex) {
Log.e("Error", "Exception loading drawable: " + ex.getMessage());
}
}
protected void onDraw(Canvas canvas) {
if(fSetBounds)
{
rectangleDrawable.setBounds(x, y, x + width, y + height);
ovalDrawable.setBounds(x, y, x + width, y + height);
lineDrawable.setBounds(x, y, x + width, y + height);
ringDrawable.setBounds(x, y, x + width, y + height);
}
if(fDrawRectangle)
if(fCastToShapeDrawable)
((ShapeDrawable)rectangleDrawable).draw(canvas);
else if(fCastToGradientDrawable)
((GradientDrawable)rectangleDrawable).draw(canvas);
else
rectangleDrawable.draw(canvas);
if(fDrawOval)
ovalDrawable.draw(canvas);
if(fDrawLine)
lineDrawable.draw(canvas);
if(fDrawRing)
ringDrawable.draw(canvas);
}
As is the intention of these posts, some variables allow you to experiment with a few things hich have been stated in the theory above. The config menu of the screen allows you to configure these variables.
Following table maps the configuration with a variable in the code and what it allows you to experiment with:
Configuration | Variable | What it does |
Draw rectangle | fDrawRectangle | Allows to choose if you want to draw a rectangle. The variables fDrawLine , fDrawOval and fDrawRing allow you to do the same for an line, oval and ring |
Set bounds | fSetBounds | If set to true, the bounds of the drawables will be set in the code. |
As ShapeDrawable | fCastToShapeDrawable | If set to true, the shape loaded from the resource will first be casted to an object of type ShapeDrawable before its draw -method is called. This will result in an exception being thrown. This variable only has effect when a rectangle is choosen to be drawn. |
As GradientDrawable | fCastToGradientDrawable | If set to true, the shape loaded from the resource will first be casted to an object of type GradientDrawable before its draw -method is called. This variable only has effect when a rectangle is choosen to be drawn. |
Drawables in code: CustomDrawableView
public CustomDrawableView(Context context) {
super(context);
rectangleDrawable = new ShapeDrawable(new RectShape());
ovalDrawable = new ShapeDrawable(new OvalShape());
arcDrawable = new ShapeDrawable(new ArcShape(45, 300));
}
protected void onDraw(Canvas canvas) {
arcDrawable.getPaint().setColor(0xff74AC23);
if(fSetBounds)
{
rectangleDrawable.setBounds(x, y, x + width, y + height);
ovalDrawable.setBounds(x, y, x + width, y + height);
arcDrawable.setBounds(x, y, x + width, y + height);
}
if(fDrawRectangle)
rectangleDrawable.draw(canvas);
if(fDrawOval)
ovalDrawable.draw(canvas);
if(fDrawArc)
arcDrawable.draw(canvas);
}
Again, the config menu allows to experiment with a few settings:
Following variables allow you to experiment with this view.
Configuration | Variable | What it does |
Draw rectangle | fDrawRectangle | Allows to choose if you want to draw a rectangle. The variables fDrawOval and fDrawArc allow you to do the same for an oval and arc |
Set bounds | fSetBounds | If set to true, the bounds of the drawables will be set in the code. |
Transformations using the Matrix
class: CustomDrawableMatrixTransformationView
protected void onDraw(Canvas canvas) {
int rectX = this.getWidth()/2 - (width/2);
int rectY = this.getHeight()/2 - (height/2);
rectangleDrawable.setBounds(rectX, rectY, rectX + width, rectY + height);
Matrix matrix;
matrix = new Matrix();
if(pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER) && pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER))
matrix.postTranslate(-1 * this.getWidth() / 2, -1 * this.getHeight() / 2);
int sequence = 0;
while(pipeline.containsKey("MTX_" + Integer.toString(sequence)))
{
Bundle currTransform = (Bundle)pipeline.getParcelable("MTX_" + Integer.toString(sequence));
String transformationType = currTransform.getString(CustomDrawableMatrixTransformationConfigActivity.TRANSFORMATION_TYPE);
if(transformationType.equals(MatrixTransformationRotateConfigActivity.TRANSFORMATION_ROTATE))
{
float rotation = currTransform.getFloat(MatrixTransformationRotateConfigActivity.ROTATE_ANGLE);
String transformationOrderType = currTransform.getString(CustomDrawableMatrixTransformationConfigActivity.TRANSFORMATIONORDER_TYPE);
if(transformationOrderType.equals(CustomDrawableMatrixTransformationConfigActivity.TRANSFORMATIONORDER_SET))
{
matrix.setRotate(rotation);
}
if(transformationOrderType.equals(CustomDrawableMatrixTransformationConfigActivity.TRANSFORMATIONORDER_PRE))
{
matrix.preRotate(rotation);
}
if(transformationOrderType.equals(CustomDrawableMatrixTransformationConfigActivity.TRANSFORMATIONORDER_POST))
{
matrix.postRotate(rotation);
}
}
sequence++;
}
if(pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER) && pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER))
matrix.postTranslate(this.getWidth() / 2, this.getHeight() / 2);
final Matrix currentMatrix = canvas.getMatrix();
canvas.concat(matrix);
if(pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM) && !pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM))
ovalDrawable.draw(canvas);
rectangleDrawable.draw(canvas);
canvas.setMatrix(currentMatrix);
if(!pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM) || pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM))
ovalDrawable.draw(canvas);
}
In the above code extract, I only show the code for a rotation. I left out the code for the translation, scaling and skew because it is very similar to that of rotation. You can see the complete code in the attached files.
In the configuration menu you can configure the following:
Configuration | Variable | What it does |
Add New Transform | none | Allows you concatenate various transformations. If you click this button you will get the following screen allowing you to select the type transformation:
|
Translate to center | TRANSLATETOCENTER | Allows you to set the center of the tranformations to the center of the screen, instead of the upper-left corner. This is done by prepending all transformations with a translation to the center of the screen. Afterward, the same translation in the opposite direction is appended. |
Translate to center | TRANSLATETOCENTER | Allows you to set the center of the tranformations to the center of the screen, instead of the upper-left corner. This is done by prepending all transformations with a translation to the center of the screen. Afterward, the same translation in the opposite direction is appended. |
Oval out of transform | OVALOUTTRANSFORM | If set, the drawing of the oval is done outside the transformation. Therefore, the oriiginal transformation matrix of the canvas is saved and restored before drawing the oval. |
When selecting a type of transformation you will get the chance to configure other variables:
Configuration | Screen | What it does |
Rotation | | Rotation has two variables to play with:
- order: if the transformation is prepended (
pre -method), appended (post -method) or replaces (set -method). - rotation: the angle of rotation around the origin of the screen.
|
Translation | | Translation has three variables to play with:
- order: if the transformation is prepended (
pre -method), appended (post -method) or replaces (set -method). - dX: the translation along the X-axis
- dY: the translation along the Y-axis
|
Scaling | | Scaling has three variables to play with:
- order: if the transformation is prepended (
pre -method), appended (post -method) or replaces (set -method). - sX: the scaling along the X-axis
- sY: the scaling along the Y-axis
|
Skew | | Skew has three variables to play with:
- order: if the transformation is prepended (
pre -method), appended (post -method) or replaces (set -method). - kX: the skew along the X-axis
- kY: the skew along the Y-axis
|
By using the button it is possible to build a list of transformations to apply, and by selecting the appropriate order of each transformation you can experiment with the order in which the transformations are applied.
Transformations using the Camera
class: CustomDrawableCameraTransformationView
protected void onDraw(Canvas canvas) {
int rectX = this.getWidth()/2 - (width/2);
int rectY = this.getHeight()/2 - (height/2);
rectangleDrawable.setBounds(rectX, rectY, rectX + width, rectY + height);
Matrix matrix;
matrix = new Matrix();
Camera camera;
camera = new Camera();
if(pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER) && pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER))
matrix.postTranslate(-1 * this.getWidth() / 2, -1 * this.getHeight() / 2);
int sequence = 0;
while(pipeline.containsKey("MTX_" + Integer.toString(sequence)))
{
Bundle currTransform = (Bundle)pipeline.getParcelable("MTX_" + Integer.toString(sequence));
String transformationType = currTransform.getString(CustomDrawableCameraTransformationConfigActivity.TRANSFORMATION_TYPE);
if(transformationType.equals(CameraTransformationRotateXConfigActivity.TRANSFORMATION_ROTATEX))
{
float rotationX = currTransform.getFloat(CameraTransformationRotateXConfigActivity.ROTATEX_ANGLE);
camera.rotateX(rotationX);
}
sequence++;
}
Matrix cameraMatrix = new Matrix();
camera.getMatrix(cameraMatrix);
matrix.postConcat(cameraMatrix);
if(pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER) && pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.TRANSLATETOCENTER))
matrix.postTranslate(this.getWidth() / 2, this.getHeight() / 2);
final Matrix currentMatrix = canvas.getMatrix();
canvas.concat(matrix);
if(pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM) && !pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM))
ovalDrawable.draw(canvas);
rectangleDrawable.draw(canvas);
canvas.setMatrix(currentMatrix);
if(!pipeline.containsKey(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM) || pipeline.getBoolean(CustomDrawableMatrixTransformationConfigActivity.OVALOUTTRANSFORM))
ovalDrawable.draw(canvas);
}
Again, the above code is a stripped down version of the code you can find in the attached files.
The configuration menu allows to experiment with following:
Configuration | Variable | What it does |
Add New Transform | none | Allows you concatenate various transformations. If you click this button you will get the following screen allowing you to select the type transformation:
|
Translate to center | TRANSLATETOCENTER | Allows you to set the center of the tranformations to the center of the screen, instead of the upper-left corner. This is done by prepending all transformations with a translation to the center of the screen. Afterward, the same translation in the opposite direction is appended. |
Oval out of transform | OVALOUTTRANSFORM | If set, the drawing of the oval is done outside the transformation. Therefore, the oriiginal transformation matrix of the canvas is saved and restored before drawing the oval. |
When selecting a type of transformation you will get the chance to configure other variables:
Configuration | Screen | What it does |
Rotation around the X-axis | | Rotation around the X-axis has one variable to play with:
- rotation: the angle of the rotation.
|
Rotation around the Y-axis | | Rotation around the Y-axis has one variable to play with:
- rotation: the angle of the rotation.
|
Rotation around the Z-axis | | Rotation around the Z-axis has one variable to play with:
- rotation: the angle of the rotation.
|
Translation | | Translation has three variables to play with:
- dX: the translation along the X-axis
- dY: the translation along the Y-axis
- dZ: the translation along the Z-axis
|
Conclusion
As stated in the beginning, the subject of drawing in Android is big. Although the article does not aim at providing all possible information, it does show some basic scenarios and the application in the accompanying source code gives the user the possibility to experiment with these different scenario?s and see how Android response.
Filed under: Android, CodeProject