转载自:http://www.eoeandroid.com/thread-74871-1-1.html
高级指南针表盘的例子
已经创建了一个简单的指南针。而在上一章,你又回到了这个例子,对它进行了扩展从而使它够使用加速计硬件来显示横向和纵向方向。
那些例子中的UI都很简单,从而保证了那些章节中的代码都尽可能地清晰。
在下面的例子中,将对CompassView的onDraw方法做一些重要的改动,从而把它从一个简单的、平面的指南针,变成一个动态的航空地平仪(artificial horizon ),如图所示。
由于上面的图片是黑白的,所以需要实际动手创建这个控件来看到完全的效果。
java代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="text_color">#FFFF</color>
<color name="background_color">#F000</color>
<color name="marker_color">#FFFF</color>
<color name="shadow_color">#7AAA</color>
<color name="outer_border">#FF444444</color>
<color name="inner_border_one">#FF323232</color>
<color name="inner_border_two">#FF414141</color>
<color name="inner_border">#FFFFFFFF</color>
<color name="horizon_sky_from">#FFA52A2A</color>
<color name="horizon_sky_to">#FFFFC125</color>
<color name="horizon_ground_from">#FF5F9EA0</color>
<color name="horizon_ground_to">#FF00008B</color>
</resources>
(2) 用作航空地平仪的天空和地面的Paint和Shader对象是根据当前View的大小创建的,所以它们不能像你在创建的Paint对象那样,是静态的。因此,不再创建Paint对象,取而代之的是构造它们所使用的渐变数组和颜色。
java代码:
int[] borderGradientColors;
float[] borderGradientPositions;
int[] glassGradientColors;
float[] glassGradientPositions;
int skyHorizonColorFrom;
int skyHorizonColorTo;
int groundHorizonColorFrom;
int groundHorizonColorTo;
(3) 更新CompassView的initCompassView方法,来使用第(1)步中所创建的资源来初始化第(2)步中所创建的变量。现存的方法代码大部分可以保留,而只需要对textPaint、circlePaint和markerPaint变量做些许改动,如下所示:
java代码:
protected void initCompassView() {
setFocusable(true);
// 获得外部资源
Resources r = this.getResources();
circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
circlePaint.setColor(R.color.background_color);
circlePaint.setStrokeWidth(1);
circlePaint.setStyle(Paint.Style.STROKE);
northString = r.getString(R.string.cardinal_north);
eastString = r.getString(R.string.cardinal_east);
southString = r.getString(R.string.cardinal_south);
westString = r.getString(R.string.cardinal_west);
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(r.getColor(R.color.text_color));
textPaint.setFakeBoldText(true);
textPaint.setSubpixelText(true);
textPaint.setTextAlign(Align.LEFT);
textHeight = (int)textPaint.measureText("yY");
markerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
markerPaint.setColor(r.getColor(R.color.marker_color));
markerPaint.setAlpha(200);
markerPaint.setStrokeWidth(1);
markerPaint.setStyle(Paint.Style.STROKE);
markerPaint.setShadowLayer(2, 1, 1, r.getColor(R.color.shadow_color));
a. 创建径向Shader用来绘制外边界所使用的颜色和位置数组。
java代码:
borderGradientColors = new int[4];
borderGradientPositions = new float[4];
borderGradientColors[3] = r.getColor(R.color.outer_border);
borderGradientColors[2] = r.getColor(R.color.inner_border_one);
borderGradientColors[1] = r.getColor(R.color.inner_border_two);
borderGradientColors[0] = r.getColor(R.color.inner_border);
borderGradientPositions[3] = 0.0f;
borderGradientPositions[2] = 1-0.03f;
borderGradientPositions[1] = 1-0.06f;
b. 现在创建径向渐变的颜色和位置数组,它们将用来创建半透明的"glass dome"(玻璃圆顶),它放置在View的上面,从而使人产生深度的幻觉。
java代码:
glassGradientColors = new int[5];
glassGradientPositions = new float[5];
int glassColor = 245;
glassGradientColors[4]=Color.argb(65,glassColor,glassColor, glassColor);
glassGradientColors[3]=Color.argb(100,glassColor,glassColor,glassColor);
glassGradientColors[2]=Color.argb(50,glassColor,glassColor, glassColor);
glassGradientColors[1]=Color.argb(0,glassColor,glassColor, glassColor);
glassGradientColors[0]=Color.argb(0,glassColor,glassColor, glassColor);
glassGradientPositions[4] = 1-0.0f;
glassGradientPositions[3] = 1-0.06f;
glassGradientPositions[2] = 1-0.10f;
glassGradientPositions[1] = 1-0.20f;
glassGradientPositions[0] = 1-1.0f;
c. 最后,获得创建线性颜色渐变所使用的颜色,它们将用来表示航空地平仪中的天空和地面。
java代码:
skyHorizonColorFrom = r.getColor(R.color.horizon_sky_from);
skyHorizonColorTo = r.getColor(R.color.horizon_sky_to);
groundHorizonColorFrom = r.getColor(R.color.horizon_ground_from);
groundHorizonColorTo = r.getColor(R.color.horizon_ground_to);
}
(4) 在开始绘制表盘之前,先创建一个新的enum来存储基本的方向。
java代码:
private enum CompassDirection { N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW }
现在需要完全代替已有的onDraw方法。首先,需要计算出某些与大小有关的值,包括View的中心、圆形控件的半径、包围外盘元素(方向)和内盘元素(倾斜和转动)的矩形。
java代码:
@Override
protected void onDraw(Canvas canvas) {
(1) 根据用来绘制方向值的字体的大小,计算出外圆(方向)的宽度。
float ringWidth = textHeight + 4;
(2) 然后计算出View的高度和宽度,并使用那些值来计算外刻度盘和内刻度盘的半径,同时创建每一个盘面的边界box。
java代码:
int height = getMeasuredHeight();
int width =getMeasuredWidth();
int px = width/2;
int py = height/2;
Point center = new Point(px, py);
int radius = Math.min(px, py)-2;
RectF boundingBox = new RectF(center.x - radius, center.y - radius, center.x + radius, center.y + radius);
RectF innerBoundingBox = new RectF(center.x - radius + ringWidth, center.y - radius + ringWidth, center.x + radius - ringWidth, center.y + radius - ringWidth);
float innerRadius = innerBoundingBox.height()/2;
(3) 在确定了View的外形尺寸之后,现在就要开始绘制盘面了。
从外围的底层开始,向里面和上面发展,首先是外盘面(方向)。使用之前的那个例子中所定义的颜色和位置,创建一个新的RadialGradient Shader,把那个Shader分配给一个新的Paint,然后使用它画一个圆。
java代码:
RadialGradient borderGradient = new RadialGradient(px, py, radius, borderGradientColors, borderGradientPositions, TileMode.CLAMP);
Paint pgb = new Paint();
pgb.setShader(borderGradient);
Path outerRingPath = new Path();
outerRingPath.addOval(boundingBox, Direction.CW);
canvas.drawPath(outerRingPath, pgb);
(4) 接下来,需要绘制航空地平仪。通过把圆形表面分成两个部分来创建地平仪,其中一部分代表天空,另一部分代表地面。每一部分所占的比例与当前的高度(pitch)有关。
首先创建用来绘制天空和地面的Shader和Paint对象。
java代码:
LinearGradient skyShader = new LinearGradient(center.x, innerBoundingBox.top, center.x, innerBoundingBox.bottom, skyHorizonColorFrom, skyHorizonColorTo, TileMode.CLAMP);
Paint skyPaint = new Paint();
skyPaint.setShader(skyShader);
LinearGradient groundShader = new LinearGradient(center.x, innerBoundingBox.top, center.x, innerBoundingBox.bottom, groundHorizonColorFrom, groundHorizonColorTo, TileMode.CLAMP); Paint groundPaint = new Paint();
groundPaint.setShader(groundShader);
(5) 现在通过形式化俯仰角和翻转角的值来让它们分别处于±90度和±180度范围内。
java代码:
float tiltDegree = pitch;
while (tiltDegree > 90 || tiltDegree < -90) {
if (tiltDegree > 90) tiltDegree = -90 + (tiltDegree - 90);
if (tiltDegree < -90) tiltDegree = 90 - (tiltDegree + 90);
}
float rollrollDegree = roll;
while (rollDegree > 180 || rollDegree < -180) {
if (rollDegree > 180) rollDegree = -180 + (rollDegree - 180);
if (rollDegree < -180) rollDegree = 180 - (rollDegree + 180);
}
(6) 创建用来填充圆的每个部分(地面和天空)的路径。每一部分的比例应该与形式化之后的俯仰值有关。
java代码:
Path skyPath = new Path();
skyPath.addArc(innerBoundingBox, -tiltDegree, (180 + (2 * tiltDegree)));
(7) 将Canvas围绕圆心,按照与当前翻转角相反的方向进行旋转,并且使用在第(4)步中所创建的Paint来绘制天空和地面路径。
java代码:
canvas.rotate(-rollDegree, px, py);
canvas.drawOval(innerBoundingBox, groundPaint);
canvas.drawPath(skyPath, skyPaint);
canvas.drawPath(skyPath, markerPaint);
(8) 接下来是盘面标记,首先计算水平的水平标记的起止点。
java代码:
int markWidth = radius / 3; int startX = center.x - markWidth; int endX = center.x + markWidth;
(9) 要让水平值更易于读取,应该保证俯仰角刻度总是从当前值开始。下面的代码计算了天空和地面的接口在水平面上的位置:
java代码:
double h = innerRadius*Math.cos(Math.toRadians(90-tiltDegree)); double justTiltY = center.y - h;
(10) 找到表示每一个倾斜角的像素的数目。
java代码:
float pxPerDegree = (innerBoundingBox.height()/2)/45f;
(11) 现在遍历180度,以当前的倾斜值为中心,给出一个可能的俯仰角的滑动刻度。
java代码:
for (int i = 90; i >= -90; i -= 10) {
double ypos = justTiltY + i*pxPerDegree;
// 只显示内表盘的刻度
if ((ypos < (innerBoundingBox.top + textHeight)) || (ypos > innerBoundingBox.bottom - textHeight)) continue;
// 为每一个刻度增加画一个直线和一个倾斜角
canvas.drawLine(startX, (float)ypos, endX, (float)ypos, markerPaint);
t displayPos = (int)(tiltDegree - i);
String displayString = String.valueOf(displayPos);
float stringSizeWidth = textPaint.measureText(displayString);
canvas.drawText(displayString, (int)(center.x-stringSizeWidth/2), (int)(ypos)+1, textPaint);
}
12) 现在,在大地/天空接口处绘制一条更粗的线。在画线之前,改变markerPaint对象的线条粗度(然后把它设置回以前的值)。
java代码:
markerPaint.setStrokeWidth(2);
canvas.drawLine(center.x - radius / 2, (float)justTiltY, center.x + radius / 2, (float)justTiltY, markerPaint);
markerPaint.setStrokeWidth(1);
(13) 要让用户能够更容易地读取精确的翻转值,应该画一个箭头,并显示一个文本字符串来表示精确值。
创建一个新的Path,并使用moveTo/lineTo方法构建一个开放的箭头,它指向直线的前方。然后绘制路径和一个文本字符串来展示当前的翻转。
java代码:
// 绘制箭头
Path rollArrow = new Path();
rollArrow.moveTo(center.x - 3, (int)innerBoundingBox.top + 14);
rollArrow.lineTo(center.x, (int)innerBoundingBox.top + 10);
rollArrow.moveTo(center.x + 3, innerBoundingBox.top + 14);
rollArrow.lineTo(center.x, innerBoundingBox.top + 10);
canvas.drawPath(rollArrow, markerPaint);
// 绘制字符串
String rollText = String.valueOf(rollDegree);
double rollTextWidth = textPaint.measureText(rollText);
canvas.drawText(rollText, (float)(center.x - rollTextWidth / 2), innerBoundingBox.top + textHeight + 2, textPaint);
(14) 将Canvas旋转到正上方,这样就可以绘制其他的盘面标记了。
java代码:
canvas.restore();
(15) 每次将Canvas旋转10度,然后画一个标记或者一个值,直到画完翻转值表盘为止。当完成表盘之后,把Canvas恢复为正上方的方向。
java代码:
canvas.save();
canvas.rotate(180, center.x, center.y);
for (int i = -180; i < 180; i += 10) {
// 每30度显示一个数字值
if (i % 30 == 0) {
String rollString = String.valueOf(i*-1);
float rollStringWidth = textPaint.measureText(rollString);
PointF rollStringCenter = new PointF(center.x-rollStringWidth / 2, innerBoundingBox.top+1+textHeight);
canvas.drawText(rollString, rollStringCenter.x, rollStringCenter.y, textPaint);
}
// 否则,绘制一个标记直线
else { canvas.drawLine(center.x, (int)innerBoundingBox.top, center.x, (int)innerBoundingBox.top + 5, markerPaint);
}
canvas.rotate(10, center.x, center.y);
}
canvas.restore();
创建表盘的最后一步是在外边界绘制方向标记。
java代码:
canvas.save();
canvas.rotate(-1*(bearing), px, py);
double increment = 22.5;
for (double i = 0; i < 360; i += increment) {
CompassDirection cd = CompassDirection.values()[(int)(i / 22.5)];
String headString = cd.toString();
float headStringWidth = textPaint.measureText(headString);
PointF headStringCenter = new PointF(center.x - headStringWidth / 2, boundingBox.top + 1 + textHeight);
if (i % increment == 0) canvas.drawText(headString, headStringCenter.x, headStringCenter.y, textPaint);
else
canvas.drawLine(center.x, (int)boundingBox.top, center.x, (int)boundingBox.top + 3, markerPaint);
canvas.rotate((int)increment, center.x, center.y);
}
canvas.restore();
添加某些收尾的工作。
首先在最上面添加一个"玻璃顶"来给人一种表盘的感觉。使用你前面创建的radial渐变数组,创建一个新的Shader和Paint对象。使用它们在内盘面画一个圆,这样可以更像是覆盖在玻璃下。
java代码:
RadialGradient glassShader = new RadialGradient(px, py, (int)innerRadius, glassGradientColors, glassGradientPositions, TileMode.CLAMP);
Paint glassPaint = new Paint();
glassPaint.setShader(glassShader);
canvas.drawOval(innerBoundingBox, glassPaint);
剩下的所有工作就是再画两个圆分别作为内表盘和外表盘范围的规则边界。然后把Canvas恢复为正上方向,并完成onDraw方法。
java代码:
//绘制外环
canvas.drawOval(boundingBox, circlePaint);
// 绘制内环
circlePaint.setStrokeWidth(2);
canvas.drawOval(innerBoundingBox, circlePaint);
canvas.restore();
}
7. 增强地图覆盖
在以前我们已经学习了如何使用覆盖在MapView上添加一个注释层,用来注释MapView覆盖的Canvas和用来绘制新的View控件的Canvas是相同的类,所以,到目前为止,本章描述的所有高级功能都可以用来增强地图覆盖。
也就是说,可以使用任何draw方法、透明度、Shader、Color Mask和Filter效果并使用Android图形框架来创建丰富的覆盖。
与覆盖交互
MapView中的触摸屏交互是由MapView的每一个覆盖单独进行处理的。要处理覆盖上的地图单击事件,需要重写onTap方法。
下面的代码段展示了一个onTap实现,它可以接收单击的地图坐标,以及发生单击的MapView:
java代码:
@Override public boolean onTap(GeoPoint point, MapView map) {
// 获得和屏幕坐标相互转换的投影
Projection projection = map.getProjection();
// 如果我们处理了这个onTap(),就返回true return [ … hit test passed … ];
}
MapView可以用来获取地图被单击时的投影。把它和GeoPoint参数一块使用,可以确定与屏幕位置相对应的真实世界的经纬度。
一个Overlay派生类的onTap方法应该在处理了单击之后返回true(否则返回false)。如果分配给一个MapView的所有Overlay都没有返回true,那么单击事件将会由MapView本身处理,否则,活动会取消这个事件。