# 前言：

• 本文翻译于 sonymobile 的一系列教程，指导 Android 开发人员如何制作3D风格的ListView，共三部分。这是第二部分。接上篇, 在上一节的基础上，这一节主要实现的是一些视觉和感觉上的特效，代码有好多数学相关的运算和图像处理的细节，比较难懂，需要有相应的专业知识。不过通过阅读，可以让我们认识一些最基本的图像处理知识，或许可以部分地采用到我们的项目中。还是采用一段一段的翻译方式，文中多次提到效果图片，我现在已经找不到图片了，不过可以在文章结尾找到代码下载链接，下载代码运行就能看到完整的效果。

# 正文：

## 原文：

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.

## 原文：

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:

## 译文：

int itemWidth = getWidth();
child.measure(MeasureSpec.EXACTLY | itemWidth, MeasureSpec.UNSPECIFIED);

## 原文：

If we replace that with

## 译文：

int itemWidth = (int)(getWidth() * ITEM_WIDTH);
child.measure(MeasureSpec.EXACTLY | itemWidth, MeasureSpec.UNSPECIFIED);

## 原文：

where ITEM_WIDTH is defined as this

## 译文：

ITEM_WIDTH 是这样定义的

/** Width of the items compared to the width of the list */
private static final float ITEM_WIDTH = 0.85f;

## 原文：

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.

## 译文：

ListView的Item只会有它宽度的85%，这些代码和我们在onLayout()中的代码一起，给我们一些非常好的左边距和右边距，但不会再item之间有间距。为了在item之间产生边距，我需要在其他地方做一些改动，但主要是在处理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:

## 译文：

/** Space occupied by the item relative to the height of the item */
private static final float ITEM_VERTICAL_SPACE = 1.45f;

## 原文：

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.

## 译文：

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);
}

## 原文：

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.

## 译文：

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;

## 原文：

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.

# Changing appearance

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.

## 译文：

@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;
}

## 原文：

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

## 译文：

Bitmap bitmap = child.getDrawingCache();
canvas.drawBitmap(bitmap, left, top, mPaint);

## 原文：

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.

# Getting to know the Camera

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.

# 开始认识Camera（译注：这个不是照相的相机）

Canvas上的矩阵变换是能胜任3D变换的。但我们不能用canvas上的普通方法，我们需要自己制作矩阵并且用那个矩阵画我们的view。

## 原文：

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.

## 译文：

Android有一个Camera类，用它来创建3D变换矩阵是很便利的。用这个类你可以沿着X，Y，Z轴旋转和平移。这个类的一个缺点是完全没有文档。SDK里面有一些示例代码用这个类做3D变换，我推荐查看这些示例代码。

## 原文：

The below code rotates the views around the Y-axis using the camera class.

## 译文：

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);

## 原文：

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.

## 译文：

（译注：这里主要是matrix和camera的变换，需要相应的专业知识，我并不了解这两个类的数学细节）

## 原文：

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.

# Blockifying the list

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.

# 块状化列表

X10 mini 手机有令人惊讶的逐像素绘制性能，因此可以在显示一些它可以做的同时显示一些你可以用canvas 变换和camera做的。我们继续做更加3D的效果。如果你已经看过这个视频，你就知道它是什么样子。每一个item会是一个块状（而不仅仅是之前那样的平面），当列表滑动的时候，它将会沿着它的X轴翻转而看起来像是在滚动。每一个item将会和它正常时一样宽，它的深度和它的高度相等。我们对所有的面使用相同的bitmap。

## 原文：

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().

## 译文：

mListRotation = -(DEGREES_PER_SCREEN * mListTop) / getHeight();

## 原文：

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.

## 译文：

@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;
}

## 原文：

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.

## 译文：

mCamera.translate(0, 0, centerY);
mCamera.rotateX(rotation);
mCamera.translate(0, 0, -centerY);

## 原文：

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.

## 译文：

drawFace剩下的和前面的代码或多或少一样，从相机中取得matrix，先后平移matrix，然后用bitmat绘制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.

## 原文：

Let there be light
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.

## 译文：

/** 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;

## 原文：

Now we implement a method that will calculate the light and create a LightingColorFilter that we can set to our Paint object.

## 译文：

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);
}

## 原文：

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.

## 原文：

To be continued…
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.

## 译文：

• 评注：
这是第二部分，主要是实现了ListView中item的视觉特效，例如旋转，3D，光照等。但文章中展示的代码还不足以完成所述功能，可以在这里下载这一节的代码。
https://pan.baidu.com/s/1CORB1bpQo61UEYE5_OVtfw

### 推荐阅读更多精彩内容

• rljs by sennchi Timeline of History Part One The Cognitiv...
sennchi阅读 6,400评论 0 10
• 成长的过程需要理解和尊重。 2017年9月18日 星期一 阴转雨 进入了九月似乎就进入了雨季，今年的雨...
冰月月阅读 216评论 12 11
• 班展-在城南书院代表着大多数班级对城南两年生活的一个提前告别。来晒晒我们的班展啦～ 这次班展花了大概1800的样子...
萌萌哒蓉阅读 191评论 0 0
• 窈窕淑女，君子好逑 苏静个子高挑，文静内向，自第一天来公司面试，便入了上司苏然的法眼，在此之前，他已经面试了2个月...
少年长志阅读 466评论 3 3
• 时如白驹过隙匆 如梦醒时发斑白 唯持童心情未泯 笑忆攀树逐花时
凌峰峰行阅读 215评论 1 4