I want to share some codes of a small effect that I implemented in my Android app called Arithmetic Puzzles. This is also a chance for me to listen to other people and make improvements. At the end of this post there is a link to the app so that you can see the code in action.
It is a pseudo-3D effect of the playing board items which are looking like rotating slightly based on your viewing angle when you move the device. The effect is not something of very visible but this small non-disturbing animations usually please the user and make the app look cooler.
Why I call it "pseudo"? Because there is no 3D animation behind, and not even a View animation like rotation around some axis or so. The solution is really simple - I just change the border of the item on gyroscope events which somehow fakes the viewing angle change.
The board item is a simple round rectangle and its border is drawn with a gradient of white color going to transparent. This creates the "fake viewing angle" effect.
So here is our BoardItemView class which is just a simple View:
As you see, there is nothing special about its initialization. I skipped the other member variables so that it doesn't have any info that is not yet needed for understanding, I will add them later.
Let's go to the onDraw() function:
In the first lines of onDraw() I am taking the clip bounds with getClipBounds() function - I need it to understand where I should do my drawing. My experience showed that it is a good idea to get clip bounds, usually you draw between (0, 0) and (width, height), but I have seen some ugly cases (when I was dealing with Android's Launcher codes) where this is not true.
Then I calculate the round rect parameters, like size and corner radius. As you noticed, no "new" calls in onDraw(), all the needed variables are kept as data members and created when this View is instantiated.
Next comes the drawing of the board itself, nothing special:
As you can see, I am just drawing a board item as a round rectangle. Before drawing I am setting up the Paint: setting shader to null, dither to false, style to FILL and color to mBoardBack (blue). I will explain the shader and dither in the next step where they are being set. Here I just need to reset (disable) them back. Style is set to FILL so that any shape I paint is also filled with the color of the Paint.
Let's go to the border part, which is interesting:
First of all, I am setting the paint style to STROKE. This means that any shape I draw will not be filled with the color, only the border will be drawn. There is also FILL_AND_STROKE style which both draws the border of the shape and fills it with the paint (remember that in previous step we just filled the round rectangle). Since I am not setting the width of the border stroking it will be just one pixel wide. This is enough to see the effect and not big enough for eyes to see the "pseudo"-ness of the 3D effect.
After that I am setting the color of the Paint and then calling a createGradient() function. We will come to that function in a few minutes. Then I am enabling the dither mode on the Paint and setting a shader on it to be the gradient that I just created with that createGradient() function call.
What does all that mean and what is a Shader in Android's Paint system? Its basically quite simple - a Shader is an object from where the Paint gets color information during drawing any shape (except drawing bitmaps). When the shader is null then the Paint object uses the color it was set, otherwise it asks the Shader object what color to use when painting a pixel at some coordinate. As an example you can see a picture acting as a shader and what will happen if a Paint will draw letter 'R' using that shader.
Seems like there are fixed number of shaders in Android, although BitmapShader is covering almost all of the possibilities. In my case I use LinearGradient class which extends Shader class.
TO BE CONTINUED in the thread, seems there is limit on post size...
It is a pseudo-3D effect of the playing board items which are looking like rotating slightly based on your viewing angle when you move the device. The effect is not something of very visible but this small non-disturbing animations usually please the user and make the app look cooler.
Why I call it "pseudo"? Because there is no 3D animation behind, and not even a View animation like rotation around some axis or so. The solution is really simple - I just change the border of the item on gyroscope events which somehow fakes the viewing angle change.
The board item is a simple round rectangle and its border is drawn with a gradient of white color going to transparent. This creates the "fake viewing angle" effect.
So here is our BoardItemView class which is just a simple View:
Code:
public class BoardItemView extends View {
// the color of the board itself (blue)
private int mBoardBack;
// the color of the border (white)
private int mBorderColor;
// the color of the gradient end (transparent)
private int mGradientEndColor;
// constructors
public ToolboxItemView(Context context) {
super(context);
init(context);
}
public ToolboxItemView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
// initialize the colors
Resources r = context.getResources();
mBorderColor = r.getColor(R.color.item_border);
mBoardBack = r.getColor(R.color.item_background);
int transparent = r.getColor(android.R.color.transparent);
setBackgroundColor(transparent);
mGradientEndColor = transparent;
}
As you see, there is nothing special about its initialization. I skipped the other member variables so that it doesn't have any info that is not yet needed for understanding, I will add them later.
Let's go to the onDraw() function:
Code:
...
private static final float RECT_PADDING_PERCENTAGE = 0.05f;
private static final float RECT_RADIUS_PERCENTAGE = 0.1f;
private RectF mRoundRect = new RectF();
private Rect mBounds = new Rect();
private float mRadius = 0.0f;
...
@Override
protected void onDraw(Canvas canvas) {
// step 1: collect information needed for drawing
canvas.getClipBounds(mBounds);
float padding = mBounds.height() * RECT_PADDING_PERCENTAGE;
mRoundRect.set(mBounds);
mRoundRect.inset(padding, padding);
mRadius = RECT_RADIUS_PERCENTAGE * mBounds.height();
...
In the first lines of onDraw() I am taking the clip bounds with getClipBounds() function - I need it to understand where I should do my drawing. My experience showed that it is a good idea to get clip bounds, usually you draw between (0, 0) and (width, height), but I have seen some ugly cases (when I was dealing with Android's Launcher codes) where this is not true.
Then I calculate the round rect parameters, like size and corner radius. As you noticed, no "new" calls in onDraw(), all the needed variables are kept as data members and created when this View is instantiated.
Next comes the drawing of the board itself, nothing special:
Code:
...
private Paint mRectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
...
@Override
protected void onDraw(Canvas canvas) {
...
// step 2: draw the background fill
mRectPaint.setShader(null);
mRectPaint.setDither(false);
mRectPaint.setStyle(Paint.Style.FILL);
mRectPaint.setColor(mBoardBack);
canvas.drawRoundRect(mRoundRect, mRadius, mRadius, mRectPaint);
...
As you can see, I am just drawing a board item as a round rectangle. Before drawing I am setting up the Paint: setting shader to null, dither to false, style to FILL and color to mBoardBack (blue). I will explain the shader and dither in the next step where they are being set. Here I just need to reset (disable) them back. Style is set to FILL so that any shape I paint is also filled with the color of the Paint.
Let's go to the border part, which is interesting:
Code:
...
private LinearGradient mBorderGradient;
...
@Override
protected void onDraw(Canvas canvas) {
...
// step 3: draw the background border
mRectPaint.setStyle(Paint.Style.STROKE);
mRectPaint.setColor(mBorderColor);
createGradient();
mRectPaint.setDither(true);
mRectPaint.setShader(mBorderGradient);
canvas.drawRoundRect(mRoundRect, mRadius, mRadius, mRectPaint);
// step 4: draw the content of a board item here, like text, image, etc...
}
First of all, I am setting the paint style to STROKE. This means that any shape I draw will not be filled with the color, only the border will be drawn. There is also FILL_AND_STROKE style which both draws the border of the shape and fills it with the paint (remember that in previous step we just filled the round rectangle). Since I am not setting the width of the border stroking it will be just one pixel wide. This is enough to see the effect and not big enough for eyes to see the "pseudo"-ness of the 3D effect.
After that I am setting the color of the Paint and then calling a createGradient() function. We will come to that function in a few minutes. Then I am enabling the dither mode on the Paint and setting a shader on it to be the gradient that I just created with that createGradient() function call.
What does all that mean and what is a Shader in Android's Paint system? Its basically quite simple - a Shader is an object from where the Paint gets color information during drawing any shape (except drawing bitmaps). When the shader is null then the Paint object uses the color it was set, otherwise it asks the Shader object what color to use when painting a pixel at some coordinate. As an example you can see a picture acting as a shader and what will happen if a Paint will draw letter 'R' using that shader.
Seems like there are fixed number of shaders in Android, although BitmapShader is covering almost all of the possibilities. In my case I use LinearGradient class which extends Shader class.
TO BE CONTINUED in the thread, seems there is limit on post size...