您的位置:首页 > 其它

实现一个定制的3DListview——第二部分

2014-03-15 13:07 253 查看
原文在这里->Making your own 3D list – Part 2

Add some padding

首先我们要做的是增加一些padding,就是在列表项之间添加一些间隙。这个List本身可以实现但我们这里不用它。

左右的padding很容易通过减小项的宽度来产生,就是在我们measure子view的时候,然后在onLayout的时候让他居中。测量的部分像这样:

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


现在我们这样替换

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


ITEM_WIDTH定义了项的宽占总宽的比值

private static final float ITEM_WIDTH = 0.85f;


这样项的宽就会限制在列表宽的85%,这会在onLayout的时候产生一个很漂亮的左右间距,但项之间却没有。

为项之间添加间隙的最直接的方式是在layout的时候加一个偏移,这在大多数情况下可以,但是不是我想要的,我想做的是间距可以随着项的高度变化,所以我们这样定义padding:

private static final float ITEM_VERTICAL_SPACE = 1.45f;


这样较大的view就会有更宽的间隙,由于每个列表项会占用更大的空间,所以我们需要修改使用getTop(),getBottom(),getMeasureHeight()来得到子view数据的方法,例如,fillListDown()要依赖getMeasuredHeight()返回的值。在这样的定义下,一个列表项占的空间将是ITEM_VERTICAL_SPACE倍,首先我们来实现这些工具方法

private int getChildMargin(View view) {
		int margin = (int) (view.getMeasuredHeight() * (ITEM_VERTICAL_SPACE - 1) / 2);
		return margin;
	}
	
	private int getChildTop(View view) {
		return view.getTop() - getChildMargin(view);
	}
	
	private int getChildBottom(View view) {
		return view.getBottom() + getChildMargin(view); 
	}
	
	private int getChildHeight(View view) {
		return view.getMeasuredHeight() + getChildMargin(view) * 2;
	}
你可能想知道为什么getChildHeight()这样实现,为什么不直接返回child.getMeasuredHeight() +2*ITEM_VERTICAL_SPACE。原因是有时候我们只需要计算一边的padding,有时候两边都需要,如果我们不使用相同的规则计算,最后会发现getChildHeight()和getChildBottom() - getChildTop()的结果不一致而产生错误。

现在替换所有child.getTop()、child.getBottom()和child.getMeasuredHeight(),使用上面的工具方法。这里着重强调一下positionItems()方法,

private void positionItems() {
		int top = mListTop + mListTopOffset;
		for (int index = 0; index < getChildCount(); ++index) {
			View childView = getChildAt(index);
			int width = childView.getMeasuredWidth();
			int height = childView.getMeasuredHeight();
			int left = (getWidth() - width) / 2;
			int margin = getChildMargin(childView);
			int childTop = margin + top;
			childView.layout(left, childTop, left + width, childTop + height);
			top += height + 2 * margin;
		}
	}


之后再运行它看起来是这样的



当你觉得一个view会有不同的缩放大小时,通过关联来定义变量,像view的宽和高,是一种很好的做法。这样可以方便地支持不同的屏幕大小。

Changeing appearance

当在canvas上作图时,图形会受到canvas的变换矩阵的影响,这个矩阵可以缩放、移动、旋转或者改变内容,canvas类提供了简便的方法来做这些变换,像scale()、rotate()和translate()。
然而在使用这些之前,我们需要覆写draw()方法来获取关联的canvas。通常你需要覆写onDraw(),然后在上面绘制内容,但是列表控件本身是空的,最终绘制的其实是列表项的内容。要覆写子view相关的绘制我们可以用dispatchDraw()或者drawChild(),这里我们使用drawChild()。
我们从使用常用的canvas操作开始来改变控件,下面的代码会将距离中心渐远的项进行缩放和旋转。
@Override
	protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
		int left = child.getLeft();
		int top = child.getTop();
		
		int centerX = child.getWidth() / 2;
		int centerY = child.getHeight() / 2;
		
		float pivotX = left + centerX;
		float pivotY = top + centerY;
		
		float centerScreen = getHeight() / 2;
		float distFromCenter = (pivotY - centerScreen) / centerScreen;
		
		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;
	}
有趣的部分是从canvas调用save方法开始的,这样可以让我们在restore时恢复到现在的状态,以免在按步骤绘制时造成混乱。然后我们旋转并缩放子view。你可以看到下面的效果,但是有严重的锯齿。



通常你可以使用Paint对象来绘制图形,paint可以设置anti-aliasing和filtering,这两个都会降低性能,但是在大多数情况下需要这样做。为了修复锯齿我们可以获取子view的绘制缓存(确保在调用之前已使能该功能),然后用带有filtering和anti-aliasing的Paint的Canvas来绘制这个图像,将super的调用替换为下面的部分:

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


这样结果看上去好多了



我们现在只是改变了子view绘制的地方而没有改变layout,子view实际的区域还没有变,也就是说显示出来的view不在它实际会响应的区域。结果就是我们点在一个view上,发生响应的却是另一个view。当然这取决于你移动view的距离,如果产生问题了,你就需要修改代码来找到它实际的位置,我们这里跳过这部分,只是将我们的listview设计的尽量避免这个问题。

另一个问题是现在我们的list有点丑,像我们这样旋转没什么意思,为了更有趣点,我们给它加点3D效果

Getting to know the Camera

Canvas的变换矩阵可以实现3D变换。但是canvas提供的通用方法不能达到我们的效果,我们需要创建自己的矩阵并在绘制的时候使用它。

Android的Camera类可以用来创建3D变换矩阵,有了它你可以绕x、y、z轴做旋转和平移。它的缺点是这个类的说明文档太简单了,在SDK里有一个关于它如何使用的例子。

下面的代码会将view绕Y轴旋转:

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


我们来看下这段代码,首先,创建camera,然后做Y-轴旋转,现在我们需要从camera中将旋转后的矩阵取出来,目前这个矩阵只包含了y轴的旋转动作,使用它会让view绕着(0,0)坐标点旋转,也就是view的左上角,要让它以view的中心为原点,我们需要先将原点平移到这个中心位置,然后我们加一些缩放和平移让它绘制在正确的位置上。



Blockifying the list

我们还要将列表项做成一个立方体,滚动的时候会看到它发生翻转。为了绘制立方体我们需要画两次表面(一般正面看一个盒子就是两个面),我们还需要几个旋转的变量来追踪the main rotation。我们不再使用到列表中心的距离来计算旋转角度,而是采用当前list top的位置:

这样每当用户滚动一个屏幕的距离后,列表项就会旋转DEGREES_PER_SCREEN的角度。

下面是drawChild()现在的样子

@Override
	protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
		final Bitmap bitmap = child.getDrawingCache();
		if (bitmap == null) {
			return super.drawChild(canvas, child, drawingTime);
		}

		int left = child.getLeft();
		int top = child.getTop();

		int centerX = child.getWidth() / 2;
		int centerY = child.getHeight() / 2;

		float pivotX = left + centerX;
		float pivotY = top + centerY;

		float centerScreen = getHeight() / 2;
		float distFromCenter = (pivotY - centerScreen) / centerScreen;

		float scale = (float) (1 - SCALE_DOWN_FACTOR
				* (1 - Math.cos(distFromCenter)));
		float childRotation = mListRotation - 20 * distFromCenter;
		childRotation %= 90;
		if (childRotation < 0) {
			childRotation += 90;
		}

		if (childRotation < 45) {
			drawFace(canvas, bitmap, left, top, centerX, centerY, scale,
					childRotation - 90);
			drawFace(canvas, bitmap, left, top, centerX, centerY, scale,
					childRotation);
		} else {
			drawFace(canvas, bitmap, left, top, centerX, centerY, scale,
					childRotation);
			drawFace(canvas, bitmap, left, top, centerX, centerY, scale,
					childRotation - 90);
		}

		canvas.drawBitmap(bitmap, mMatrix, mPaint);
		return false;
	}
大部分代码和之前的一样,主要修改在立方体的绘制上,所以把它提到一个单独的方法中drawFace();

为了画一个面我们要先移动camera来将绘制的图像向我们拉近,然后绕x轴旋转后再移回去,就像这样

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


drawFace其余的代码和之前的类似,从camera获取matrix,移动然后绘制

private void drawFace(Canvas canvas, Bitmap bitmap, int left, int top,
int centerX, int centerY, float scale, float rotateDegree) {
if (mCamera == null) {
mCamera = new Camera();
}
mCamera.save();
mCamera.translate(0, 0, centerY); mCamera.rotateX(rotateDegree); mCamera.translate(0, 0, -centerY);

if (mMatrix == null) {
mMatrix = new Matrix();
}
mCamera.getMatrix(mMatrix);
mCamera.restore();

mMatrix.preTranslate(-centerX, -centerY);
mMatrix.postScale(scale, scale);
mMatrix.postTranslate(left + centerX, top + centerY);

canvas.drawBitmap(bitmap, mMatrix, mPaint);
}
现在看起来已经有3d的效果了



Let there be light

为了让List看着更有3D的效果,我们需要加一些光照,没有光照的话看起来还是平面的。一种方法是调整alpha,优点是相当简单,缺点是只在黑色的背景上比较明显,而且这种方式也不支持高光(反射光)。为了效果更加逼真,我们需要想其他办法。
在Paint对象上我们可以设置color filter来影响绘制时候的色值,setColorFilter()方法需要传入一个ColorFilter,ColortFilter有一个子类LightColorFilter正是我们想要的。LightColorFilter使用两个色值,第一个与要绘制的颜色相乘,另一个会相加,乘法运算会使颜色变暗而加法则会使颜色更明亮一些,所以我们可以使用这个类来作阴影和高光效果。
实际计算光线时我们使用冯氏着色法的一个简化版本,先来定义一些光照常量,
private static final int AMBIENT_LIGHT = 55;
	private static final int DIFFUSE_LIGHT = 200;
	private static final float SPECULAR_LIGHT = 70;
	private static final float SHININESS = 200;
	private static final int MAX_INTENSITY = 0xff;


然后实现一个计算光照的方法,以此创建LightColorFilter对象
private LightingColorFilter calculateColorFilter(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);
	}
这个方法的输入是面的旋转角度,我们使用这个角度来计算两种亮度,第一个是我们的主要的光亮,他用来计算我们相乘时候的值,第二个是反射光,计算完之后我们要确保最大色值为255。在这种情况下我们的光源是白色的,但是你也可以使用其他的有色光源。最后创建LightColorFilter并返回。

最后就会看到这样的结果



To be contimued

这个部分主要是改变外观,最后一部分将会实现滑动和回弹的物理效果。

代码在这里->http://download.csdn.net/detail/xu_fu/7046515
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐