一、问题描述
最近项目接到了一个遮罩层需求,效果图如下
凭感觉会觉得不难,遮罩层+遮罩层之上的元素,通过高德自己的api进行图层排序就可以。
假如用Polygon来绘制遮罩层,那么测试代码如下:
val bottomMarkerPosition = LatLng(29.0, 114.0)
val topMarkerPosition = LatLng(29.05, 114.05)
val maskLayerPath = listOf(
LatLng(28.9, 113.9), LatLng(28.9, 114.1), LatLng(29.1, 114.1),
LatLng(29.1, 113.9)
)
val bounds =
LatLngBounds.builder().include(maskLayerPath[0]).include(maskLayerPath[2]).build()
map.map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100))
val maskLayer = map.map.addPolygon(PolygonOptions().also {
it.points = maskLayerPath
it.fillColor(Color.parseColor("#A0000000"))
it.zIndex(50f)
})
val bottomMarker = map.map.addMarker(MarkerOptions().position(bottomMarkerPosition))
.also {
it.zIndex = 1f
}
val topMarker = map.map.addMarker(MarkerOptions().position(topMarkerPosition))
.also {
it.zIndex = 100f
}
上述测试代码,通过zIndex对遮罩层之上,之下的Marker都进行了排序,但实际的运行效果:
遮罩层并没有如预期一般将下层Marker挡住。
产生这个问题的原因也很简单,下面是官方工作人员对我的工单回复:
所以简单的说,通过Marker、Polygon或者自定义TileProvider等方式来添加可以明确区分上下层的遮罩层都是不可行的,或者说无法达到比较完美的效果。
- 如果用Polygon实现遮罩,则遮罩层无法遮挡住Marker,即使通过混合遮罩层颜色的方法让下层Marker看起来像被遮挡了,只要这个Marker和某个上层非Marker元素存在重叠,则这个看起来应该在下层的Marker还是会挡住上层元素。
- 如果用Marker实现遮罩,只要遮罩层上有非Marker元素,那么显然该元素会被遮罩层盖住,不符合需求。
- 通过自定义TileProvider等方式实现遮罩层的问题同Polygon。
综上,在常规api无法实现的情况下,最终我采用OpenGLES+AMap.setCustomRenderer方式来实现,以此绕过了该问题。
二、CustomRenderer接口解决方案
com.amap.api.maps.CustomRenderer接口是高德提供的一个自己的接口,该接口继承于android.opengl.GLSurfaceView.Renderer。
当有一个自己的CustomRenderer对象时,可以通过aMap.setCustomRenderer()方法,将该对象传递给高德。从而实现在高德地图上的自定义的OpenGLES渲染逻辑。
通过该接口绘制的图形始终位于高德地图组件中的最上层,也就是这一点构成了我们解决前面提到问题的基础。
另一方面,通过aMap对象,我们可以获得高德地图的projectionMatrix和viewMatrix(在官方文档中并没有提到,但实际上是可以调用到的,当然,因此也要注意SDK版本号,这两个属性理论上应该各版本都是存在的,因为高德一个官方demo中直接有这两个属性的调用,如果这两个属性不允许访问的话,官方demo也不应该把它们放进去)。借助于这两个矩阵,我们就可以实现地理信息数据到OpenGLES坐标系的位置转换。
所以综上,只要自己用OpenGLES实现遮罩层和遮罩层之上数据的绘制,就可以实现效果较好的遮罩层。但显然该方法也是存在缺点的,如果遮罩层之上的元素比较复杂,那么无疑会写的很麻烦,其次交互事件也需要自己处理。
我的项目中由于只需要在遮罩层上绘制路径和两个Marker,所以大概的代码如下:
遮罩层绘制:
private class GLMask {
var color = GLColor(Color.parseColor("#A0000000"))
private val matrix = FloatArray(16).also {
Matrix.setIdentityM(it, 0)
}
private val indicesBuffer = shortArrayOf(0, 3, 1, 1, 3, 2).toBuffer()
private val indicesCount = 6
private val vertexBuffer = floatArrayOf(
-1f, -1f, 0f,
1f, -1f, 0f,
1f, 1f, 0f,
-1f, 1f, 0f
).toBuffer()
fun drawMask(vertexLoc: Int, colorLoc: Int, matrixLoc: Int) {
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glUniformMatrix4fv(matrixLoc, 1, false, matrix, 0)
glUniform4f(colorLoc, color.r, color.g, color.b, color.a)
glEnableVertexAttribArray(vertexLoc)
glVertexAttribPointer(
vertexLoc, 3, GL_FLOAT, false, 0, vertexBuffer
)
glDrawElements(GL_TRIANGLES, indicesCount, GL_UNSIGNED_SHORT, indicesBuffer)
glDisable(GL_BLEND)
glDisableVertexAttribArray(vertexLoc)
}
}
遮罩层绘制时,它的顶点数组不用动态计算,固定为代码中vertexBuffer对应的顶点数组就好,这样刚好对应整个Map。简单来说就是在Map上画一个刚好和Map一样大小的矩形就完事。
路径绘制的代码略过,反正就是画线就行。
起点和终点的两个Marker,可以直接利用高德自己的BitmapDescriptor对象。通过BitmapDescriptorFactory的静态方法获得一个该对象,然后通过该对象可以取得一个Bitmap,所以直接把这个Bitmap画到合适的位置就好。
private class GLBitmap(private val aMap: AMap) {
var icon: BitmapDescriptor? = null
var position: LatLng? = null
var anchor = PointF(0.5f, 1f)
private var width = 0
private var height = 0
private var textureId: Int? = null
private var scaledWidth = 0f
private var scaledHeight = 0f
private var bitmapWidth = 0
private var bitmapHeight = 0
private lateinit var vertexBuffer: FloatBuffer
private val indicesBuffer = shortArrayOf(0, 3, 1, 1, 3, 2).toBuffer()
private val uvBuffer = floatArrayOf(
0f, 1f,
1f, 1f,
1f, 0f,
0f, 0f
).toBuffer()
private fun update() {
if (icon == null) {
if (textureId != null) {
glDeleteTextures(1, intArrayOf(textureId!!), 0)
textureId = null
}
return
}
if (position == null) return
genTexture()
scaledWidth = aMap.projection.toOpenGLWidth(bitmapWidth)
scaledHeight = aMap.projection.toOpenGLWidth(bitmapHeight)
if (width == 0 || height == 0) return
val basePoint = aMap.projection.toScreenLocation(position)
val glX = (2f * basePoint.x - width) / width
val glY = (2f * (height - basePoint.y) - height) / height
val glW = bitmapWidth * 2f / width
val glH = bitmapHeight * 2f / height
val left = glX - glW * anchor.x
val right = left + glW
val bottom = glY + glH * anchor.y - glH
val top = bottom + glH
val vertices = floatArrayOf(
left, bottom, 0f,
right, bottom, 0f,
right, top, 0f,
left, top, 0f
)
vertexBuffer = vertices.toBuffer()
}
private fun genTexture() {
if (textureId == null) {
bitmapWidth = icon!!.width
bitmapHeight = icon!!.height
val intArray = IntArray(1)
glGenTextures(1, intArray, 0)
textureId = intArray[0]
glBindTexture(GL_TEXTURE_2D, textureId!!)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
GLUtils.texImage2D(GL_TEXTURE_2D, 0, icon!!.bitmap, 0)
}
}
// index用于确定texture所绑定的位置
fun drawBitmap(
vertexLoc: Int, textureLoc: Int, uvLoc: Int,
index: Int, width: Int, height: Int
) {
this.width = width
this.height = height
update()
if (textureId == null) return
glEnableVertexAttribArray(vertexLoc)
glVertexAttribPointer(
vertexLoc, 3, GL_FLOAT, false, 0, vertexBuffer
)
glEnableVertexAttribArray(uvLoc)
glVertexAttribPointer(
uvLoc, 2, GL_FLOAT, false, 0, uvBuffer
)
glActiveTexture(GL_TEXTURE0 + index)
glBindTexture(GL_TEXTURE_2D, textureId!!)
glUniform1i(textureLoc, index)
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indicesBuffer)
glDisableVertexAttribArray(vertexLoc)
glDisableVertexAttribArray(uvLoc)
}
fun assignSize(width: Int, height: Int) {
this.width = width
this.height = height
}
fun remove() {
glDeleteTextures(1, intArrayOf(textureId!!), 0)
icon = null
textureId = null
}
}
高德的Marker具有一个特性,它始终是面向屏幕的,不会随着地图旋转而旋转,所以自己画Marker的时候也要注意这一点。
三、总结
最终的实现效果:
综上,对于高德Android SDK,Marker的zIndex导致的遮罩层问题,在上层元素不复杂的情况下,完全可以通过OpenGLES+CustomRenderer接口来解决。