By mobilekid
3rd June 2010.
10:27
Great tutorial, looking forward to the final part!
0
0
Welcome to the second tutorial out of three in the series of how to make your own 3D list view implementation for an Android application. In this tutorial we continue to develop the quite basic list created in part one of the tutorial into a list with 3D look and feel. At the end of this article we will have something that looks a bit more interesting than the standard list.
To see what list will look like, download the ’Sony Ericsson Tutorials‘ application from Android Market. In this app you will also see what the list will look like after the third part of this tutorial. Below is a link to the source code of part 2, prepared for you to set up your own project in e.g. Eclipse.
[Download] 3D List sample project – Part 2 (37kb)
The first thing we are going to do is to add some padding. And by this I mean padding between the items. The list itself can have padding but we are currently ignoring that.
Left and right padding can easily be handled by decreasing the width of the item when we measure it and then center it during the layouting. The measure part looked like this:
[java]
int itemWidth = getWidth();
child.measure(MeasureSpec.EXACTLY | itemWidth, MeasureSpec.UNSPECIFIED);
[/java]
If we replace that with
[java]
int itemWidth = (int)(getWidth() * ITEM_WIDTH);
child.measure(MeasureSpec.EXACTLY | itemWidth, MeasureSpec.UNSPECIFIED);
[/java]
where ITEM_WIDTH is defined as this
[java]
/** Width of the items compared to the width of the list */
private static final float ITEM_WIDTH = 0.85f;
[/java]
the list items will be just 85% of the width of the list itself. This, together with our code in onLayout(), gives us some nice padding on the left and right, but not between list items. To get padding between list items we need to make some changes here and there but mainly in the methods that handle layout.
The most straight forward way to get padding between list items is simply to layout them a fixed number pixels apart. This works fine in more or less all circumstances, but actually not for what I have in mind. What I would like to do instead is to let the padding be dependent on the height of the item. Let’s define the padding as follows:
[java]
/** Space occupied by the item relative to the height of the item */
private static final float ITEM_VERTICAL_SPACE = 1.45f;
[/java]
This way bigger items will get more padding than smaller items. Since each item will take more vertical space, we need to modify all the methods that use getTop(), getBottom() or getMessuredHight() on the child views and rely on those values for layouting. For example, the method fillListDown() which we implemented last time relies on the fact that a view takes as much space as getMessuredHight() returns. With this padding definition a view take up that space times ITEM_VERTICAL_SPACE. First we implement some nice-to-have utility methods.
[java]
private int getChildMargin(View child) {
return (int)(child.getMeasuredHeight() * (ITEM_VERTICAL_SPACE – 1) / 2);
}
private int getChildTop(View child) {
return child.getTop() – getChildMargin(child);
}
private int getChildBottom(View child) {
return child.getBottom() + getChildMargin(child);
}
private int getChildHeight(View child) {
return child.getMeasuredHeight() + 2 * getChildMargin(child);
}
[/java]
You might wonder why getChildHeight() is implemented the way it is. Why not just return child.getMeasuredHeight() * ITEM_VERTICAL_SPACE. The thing is that we sometimes need to calculate the padding on just one side and sometimes on both, and if we don’t use the same way of calculating it we might end up in a situation where getChildHeight() does not return the same thing as getChildBottom() – getChildTop() due to rounding errors.
Now we replace all occurrences of child.getTop() with getChildTop(child) and the same for getBottom() and getMeasuredHeight(). An exception is the part of the code that actually calls layout on the children, in this case the positionItems() method, which will look like this.
[java]
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
int left = (getWidth() – width) / 2;
int margin = getChildMargin(child);
int childTop = top + margin;
child.layout(left, childTop, left + width, childTop + height);
top += height + 2 * margin;
[/java]
Now our items are nicely padded and not side by side.
Defining things relative to something else, mostly the width and height of the view, is a good practice if you want a view that easily scales to different sizes. There are a lot of different android devices out there and it’s certainly a good thing to be able to support as many as possible. I’ve tested the view we do here on both the QVGA display of the X10 mini and the WVGA display of the X10 and, since almost all things are defined relative to the width and height of the view, it scales quite nicely.
When drawing graphics on a canvas, the graphics are always affected by the transformation matrix that the canvas has. This matrix can scale, translate, rotate or otherwise transform the content. The canvas class has some handy utility methods to do these normal transformations, for example scale(), rotate() and translate().
However, before we get into using them, we need to override a draw method to get hold of the canvas. On a normal view you would override onDraw() and draw the content there. However, the list itself is empty and the content of the list is completely made up of what its child views draw. To override the drawing of the child views we could override dispatchDraw() or drawChild(). In this example we’re going to use drawChild().
Let’s start by using the normal canvas operations to change the list. The code below will scale items further from the center down and rotate them.
[java]
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
// get top left coordinates
int left = child.getLeft();
int top = child.getTop();
// get offset to center
int centerX = child.getWidth() / 2;
int centerY = child.getHeight() / 2;
// get absolute center of child
float pivotX = left + centerX;
float pivotY = top + centerY;
// calculate distance from center
float centerScreen = getHeight() / 2;
float distFromCenter = (pivotY – centerScreen) / centerScreen;
// calculate scale and rotation
float scale = (float)(1 – SCALE_DOWN_FACTOR * (1 – Math.cos(distFromCenter)));
float rotation = 30 * distFromCenter;
canvas.save();
canvas.rotate(rotation, pivotX, pivotY);
canvas.scale(scale, scale, pivotX, pivotY);
super.drawChild(canvas, child, drawingTime);
canvas.restore();
return false;
}
[/java]
The interesting part begins with the save call to the canvas. This lets us restore the canvas later to this state, something that we have to do in order to not mess things up. Then we rotate the canvas around the center of the child view and then scale it down, all depending on the distance from the middle. Then we call the super method and restore the canvas. You can see the effect of this in the screen shot below (taken from the X10 mini). Note that while the views are scaled and rotated, they suffer from a horrible aliasing problem.
Normally you use a Paint object to draw with and on the Paint object you can enable anti-aliasing and filtering. Filtering and anti-aliasing drags down performance but is, in most cases, a must have nonetheless. To be able to fix the aliasing problem we can ask the view for its drawing cache (making sure that we have enabled this before) and then drawing that bitmap to the canvas with a Paint object that has filtering and anti-aliasing enabled. Replacing the super call with
[java]
Bitmap bitmap = child.getDrawingCache();
canvas.drawBitmap(bitmap, left, top, mPaint);
[/java]
gives a much better result
Remember that we are now changing where the views are drawn but we are not changing the layout. The actual positions of the views are still the same. In other words, the image of the view on the display will not correspond to the place where it actually is. A consequence of this is that when we click on a view on the display it might not be where that view really is, it might be another view or no view at all.
This of course depends on how much you have transformed or moved the view. If it becomes a problem you need to modify the code that finds what view the user clicked on, you can no longer rely on the hit rect. You also might need to override dispatchTouchEvent() and transform the coordinates of the motion event before you pass it on to the child view. For this tutorial though, we are going to skip that part and design our list to minimize problems like this.
Another noticeable thing about the list right now is that it looks quite lame. Rotating the views as we do here is not very interesting. To make it interesting, let’s make it a bit more 3D.
The transformation matrix on the canvas is capable of 3D transformations, but we can’t modify it using the regular utility methods on the canvas. We need to make matrix ourselves and draw our views using that matrix.
Android has a Camera class that’s handy to use when creating 3D transformation matrices. With this class you can rotate and translate in X, Y and Z axes. A downside with this class is that it is completely undocumented. There is some sample code in the SDK that uses this class to do 3D transformations and I recommend looking at the sample code.
The below code rotates the views around the Y-axis using the camera class.
[java]
if (mCamera == null) {
mCamera = new Camera();
}
mCamera.save();
mCamera.rotateY(rotation);
if (mMatrix == null) {
mMatrix = new Matrix();
}
mCamera.getMatrix(mMatrix);
mCamera.restore();
mMatrix.preTranslate(-centerX, -centerY);
mMatrix.postScale(scale, scale);
mMatrix.postTranslate(pivotX, pivotY);
Bitmap bitmap = child.getDrawingCache();
canvas.drawBitmap(bitmap, mMatrix, mPaint);
[/java]
Let’s go through the code. First, we create a camera if we don’t have one. Then we save the camera state so that we can restore it to the default state later. Then we rotate the camera around the Y-axis. Now we need to get the matrix from the camera and then we are finished using the camera so we restore it. The matrix we have now has the rotation we did with the camera, but it’s a “raw” rotation matrix in the sense that it includes no translation so it will rotate everything around (0, 0). To rotate around the center of the child view we pre-translate the matrix so that (0, 0) will be at the center of the screen. Then we add some scaling and then we translate the matrix so it’s drawn at the correct position. Finally, we draw the bitmap using the matrix. The result can be seen below, though a static image does not convey the way it looks when scrolling the list.
There’s quite a lot of potential transformations you can play with here. I really suggest that you try out a few transformations to see the effect. Also, this code has some issues with the perspective. It’s quite easily corrected by translating the items using the camera instead of the matrix. I’ll touch upon this point later on. However, I will leave the task of correcting the translation as an exercise for the interested readers.
The X10 mini has quite amazing per-pixel performance for graphical stuff, so to show a bit of what it can do and at the same time show a bit of what you can do with canvas transformations and the camera class, we are going to do some more 3D effects. If you’ve seen the video you’ll know how it looks like. Each item will be a block (and not just a plane as before) that will rotate around its X-axis and look like it is rolling on the ground when the list stars to scroll. Each block will be as wide as the item normally is and the depth will be the same as the height. We’ll use the same bitmap for all the sides.
So what do we need to do to achieve this effect? In order to draw the blocks we need to draw the bitmap two times (since we will almost always see two sides of the block). We also need to have some kind of rotation variable to keep track of the main rotation. Since the blocks should rotate when the user scrolls the list and the blocks should have the same rotation (so that they all face up at the same time) we can no longer use the distance from the center to calculate the rotation for the block as we did before. A convenient way to calculate the rotation is to use the list top position. Let’s add the following line to scrollList().
[java]
mListRotation = -(DEGREES_PER_SCREEN * mListTop) / getHeight();
[/java]
Doing like this will make the blocks rotate DEGREES_PER_SCREEN degrees when the user scrolls the list an entire screen no matter the pixel-height of the screen.
That will take care of the rotation, now let’s think about the actual drawing. This is how the drawChild() method looks like right now.
[java]
@Override
protected boolean drawChild(final Canvas canvas, final View child, final long drawingTime) {
// get the bitmap
final Bitmap bitmap = child.getDrawingCache();
if (bitmap == null) {
// if the is null for some reason, default to the standard
// drawChild implementation
return super.drawChild(canvas, child, drawingTime);
}
// get top left coordinates
final int top = child.getTop();
final int left = child.getLeft();
// get centerX and centerY
final int childWidth = child.getWidth();
final int childHeight = child.getHeight();
final int centerX = childWidth / 2;
final int centerY = childHeight / 2;
// get scale
final float halfHeight = getHeight() / 2;
final float distFromCenter = (top + centerY – halfHeight) / halfHeight;
final float scale = (float)(1 – SCALE_DOWN_FACTOR * (1 – Math.cos(distFromCenter)));
// get rotation
float childRotation = mListRotation – 20 * distFromCenter;
childRotation %= 90;
if (childRotation < 0) {
childRotation += 90;
}
// draw the item
if (childRotation < 45) {
drawFace(canvas, bitmap, top, left, centerX, centerY, scale, childRotation – 90);
drawFace(canvas, bitmap, top, left, centerX, centerY, scale, childRotation);
} else {
drawFace(canvas, bitmap, top, left, centerX, centerY, scale, childRotation);
drawFace(canvas, bitmap, top, left, centerX, centerY, scale, childRotation – 90);
}
return false;
}
[/java]
Most of this is quite similar to the code we did before to rotate and scale the items so I want go into details. Worth noting is that the code that will draw one face of the block is the same, it just depends on the rotation, so it’s extracted to a method. To draw a complete block we then simply draw two faces 90 degrees apart at the same place.
To draw a face we first translate the camera so that the face will be drawn closer to us. Then we rotate it and after that we translate it back so we don’t scale it. Keep in mind that the calls to the camera, just like the rotate, translate and scale methods on Canvas, needs to be written in reversed order, so to speak. In the code below, it is the last line that translates the face towards us, then we rotate it, and finally, with the first line, we translate it back.
[java]
mCamera.translate(0, 0, centerY);
mCamera.rotateX(rotation);
mCamera.translate(0, 0, -centerY);
[/java]
The rest of drawFace is more or less the same as before. It gets the matrix from the camera, pre and post translates the matrix and then draws the bitmap with the matrix.
This code will draw each item as if placed in the origin in 3D space and then we move the items to the correct place on the screen using pre and post translate on the matrix. This moves what we draw in 2D space without changing the perspective. We could apply the translation in X and Y on the camera instead, then the translation would be in 3D space and it would affect the perspective. We’re not doing that here because I want the appearance of a larger field of view than the fixed field of view of the camera. Instead, we fake it by slightly rotating and scaling the items depending on the distance from center of the screen. For this view it works quite well.
Anyway, this is how the list looks blockified.
For the list to have a realistic 3D feel we need to add lighting. Without it, it will look flat. One way to do that is by modifying the alpha. It has the advantage of being very simple but the disadvantage is that it only works nicely on a black background. Also, it’s impossible to do highlights (specular lighting) in this way. We want our view to work on other backgrounds than black and we would like to have a highlight. Therefore, we will solve the lighting problem in another way.
On a Paint object we can set color filters which will affect the color values of what we draw with that Paint object. setColorFilter() takes a ColorFilter and one of the subclasses of ColorFilter is LightingColorFilter which is almost exactly what we need. A LightingColorFilter takes two colors that are used to modify the colors that we are drawing. The first color will be multiplied with the colors we draw, while the second one will be added to the colors we draw. The multiplication will darken the color and adding will make it brighter so we can use this class to model both shadows and highlights. It would have been even better if instead of adding it would have implemented the screen blend mode, but add works OK.
To actually calculate the light we’ll use a simplified version of Phong lighting. So let’s define some light constants.
[java]
/** Ambient light intensity */
private static final int AMBIENT_LIGHT = 55;
/** Diffuse light intensity */
private static final int DIFFUSE_LIGHT = 200;
/** Specular light intensity */
private static final float SPECULAR_LIGHT = 70;
/** Shininess constant */
private static final float SHININESS = 200;
/** The max intensity of the light */
private static final int MAX_INTENSITY = 0xFF;
[/java]
Now we implement a method that will calculate the light and create a LightingColorFilter that we can set to our Paint object.
[java]
private LightingColorFilter calculateLight(final float rotation) {
final double cosRotation = Math.cos(Math.PI * rotation / 180);
int intensity = AMBIENT_LIGHT + (int)(DIFFUSE_LIGHT * cosRotation);
int highlightIntensity = (int)(SPECULAR_LIGHT * Math.pow(cosRotation, SHININESS));
if (intensity > MAX_INTENSITY) {
intensity = MAX_INTENSITY;
}
if (highlightIntensity > MAX_INTENSITY) {
highlightIntensity = MAX_INTENSITY;
}
final int light = Color.rgb(intensity, intensity, intensity);
final int highlight = Color.rgb(highlightIntensity, highlightIntensity, highlightIntensity);
return new LightingColorFilter(light, highlight);
}
[/java]
The only input to this method is the rotation of the face. We use this rotation to compute two intensities. The first, “intensity”, will be our main light level. It’s used to create the color we multiply with so it will darken what we draw. The second intensity, “highlightIntensity”, is the specular light that we use to create the color we add so it will brighten what we draw. After we have calculated the intensities we make sure they are at most 255, and then we create the colors. In this case we have a white light source so our colors will be gray, but you can also use colored light. Finally we create a LightingColorFilter and return it. The result of drawing the list with lighting can be seen below.
This article talked about changing the appearance of the list. We haven’t added any real features, only changed the way the list is drawn so now it looks nice, but it’s still not very usable. The last part that’s missing is the behavior of the list and that is the subject for the next article where we will look into how to realize flinging and snapping of the list. That will be a large step towards a usable list.
By mobilekid
3rd June 2010.
10:27
Great tutorial, looking forward to the final part!
0
0
By Nacho
9th June 2010.
12:34
Nice.. but the design…
(I know, this is for test.. hehe)
0
0
By Manish
9th June 2010.
13:37
I have a question!
Can we not make the List appear in a Circular Stack, i mean after the last number, we can display the first number.
I think that can be achieved using some geometrical functions, but still failing to implement it.
Any Suggestions?
0
0
By Anders Ericson
10th June 2010.
10:44
@Manish: I don’t think it would be a problem doing the list circular. I haven’t looked into this myself, but I would probably do it using modulus (%) on the item positions.
0
0
By KC
11th June 2010.
14:19
Great tutorial so far and now I really interested to see Part 3. Any idea when you will be publishing the 3rd installment? Thanks again!
0
0
By Manish
12th June 2010.
14:58
@ Anders Ericson
Thanks for the reply! ![]()
I guess i didn’t make myself clear with the question above! I don’t want the views to extend! I mean if there are 20 contact list and m displaying 20 views, i just don’t want to make it 40 or 60 ( or 20x ), with those 20 views. I want to get the circular queue effect on the views! Scrolling the last view shouldn’t bring me to empty space, but the first view again! ( and not by increasing the size! )
Any suggestion would be highly appreciated!
0
0
By Gulfam Hassan
23rd June 2010.
11:28
Great Tutorial, Awesome, Waiting for next part:)
0
0
By Anders Ericson
23rd June 2010.
15:43
@Manish: I think you made yourself clear, however I probably did not make myself clear
.
To make the list circular (that is, when you scroll past the last item, the first item is displayed) you need to do changes in the list view. These changes would probably involve doing modulus on the item position.
Something like this: Lets say the adapter contains 20 items. Instead of the list displaying nothing after item 19 the list would display the item that has the position 20%20 = 0, the first item. After that 21%20 = 1 and so on. This will have the effect that you’re after.
0
0
By Android
4th July 2010.
23:15
I would like to see tutorials about making my own lock-screen.
0
0
By zorro
23rd August 2010.
10:35
Nice,nice, i like it but just a Tuturials?
0
0
Sort by