Android Compose 通用地图组件封装

国内的地图 SDK 目前暂不支持 Compose,之前给高德地图封装过一次,可以达到想要的效果。

这次准备封装一个通用的地图组件,把不同地图 SDK 之间共同的组件以及功能抽象出来,开发者不再需要关心不同地图 SDK 对同一功能的不同实现,真正实现在功能相同的情况下在不同地图之间无缝切换。

这次选用的地图 SDK 是高德地图和百度地图,下面是必要依赖:

// 高德地图 (已聚合导航及定位服务)
implementation("com.amap.api:navi-3dmap-location-search:10.0.800_3dmap10.0.800_loc6.4.5_sea9.7.2")
// 百度地图基础
implementation("com.baidu.lbsyun:BaiduMapSDK_Map:7.6.3")
// 百度地图定位
implementation("com.baidu.lbsyun:BaiduMapSDK_Location:9.6.4")

结构设计

通用对象

不同的地图 SDK 虽然提供的功能实现方式不同,但是它们都需要用到一些与 SDK 无关的数据类,例如经纬度、地图相机参数、POI、地图标记等,我们可以先定义这些通用的类,以便接下来的封装。

实对象容器(ActualVariable)

有时候需要用到一个地址不变但其内部属性值可以改变的对象,先封装一个对象容器:

data class ActualVariable<T>(var value: T)

fun <T> T.toActualVariable() = ActualVariable(this)

模糊对象容器(AmbiguousVariable)

不同地图之间的一些对象虽然有共同属性,但仍然有一些不同。例如百度的 POI 类提供了高德没有的信息,为了保证框架对不同 SDK 通用的前提下,给开发者提供尽可能完整的信息,于是引入一个模糊对象容器,给开发者提供完整的数据对象类。

data class AmbiguousVariable(
    val clazz: KClass<*>,
    var value: Any? = null
) {
    override fun toString(): String {
        return "{\"@class\": \"${this::class.qualifiedName}\", \"clazz\": \"${clazz.qualifiedName}\", \"value\": \"$value\"}"
    }
}

经纬度(LatLng)

@Parcelize
data class LatLngPosition @JvmOverloads constructor(
    var lat: Double = 0.0,
    var lng: Double = 0.0
) : Parcelable {
    fun toDisplayString() = "$lat, $lng"

    fun isZero() = this.lat == 0.0 && this.lng == 0.0

    fun compareTo(position: LatLngPosition): Boolean {
        return position.lat == this.lat && position.lng == this.lng
    }

    override fun toString(): String {
        return "{\"@class\": \"${this::class.qualifiedName}\", \"lat\": $lat, \"lng\": $lng}"
    }
}

相机参数(CameraState)

data class GenericMapCameraState(
    var center: LatLngPosition,
    var zoom: Float,
    var routation: Float,
    var overLooking: Float
)

兴趣点(POI)

data class GenericPOI(
    val originalObject: AmbiguousVariable,
    val id: AmbiguousVariable,
    val name: String,
    val position: LatLngPosition
) {
    override fun toString(): String {
        return "{\"@class\": \"${this::class.qualifiedName}\", \"originalObject\": $originalObject, \"id\": $id, \"name\": \"$name\", \"position\": $position}"
    }
}

这里的 originalObject 用来保存不同 SDK 返回的最原始的 POI 对象,考虑到 POI 的 id 的数据类型可能不一致,所以也使用了 AmbiguousVariable 来保存,但高德和百度的 poiId实际上都是 String 类型的。

地图组件

地图组件指的是地图上的指南针、大小缩放按钮、定位按钮等可以分别设置是否显示的组件,对于不同的地图 SDK 来说,这些组件的显示/隐藏方式有些不同。、

组件枚举

首先定义一个 ComponentType 枚举来表示不同组件,这个枚举应该包含所有可能的地图组件:

enum class Type {
    // 指南针
    COMPASS,
    // 定位按钮
    LOCATION_BUTTON,
    // 比例尺组件
    SCALE_CONTROL_BUTTON,
    // 缩放按钮
    ZOOM_CONTROL_BUTTON,
    // 室内地图
    IN_DOOR_MAP
}

组件类

接着定义一个地图组件类来保存组件的启用状态:

data class GenericMapComponent(
    val type: Type,
    private var enabled: Boolean = false
) {
    val isEnabled: Boolean get() = enabled

    fun setEnabled(enabled: Boolean) {
        this.enabled = enabled
    }

    enum class Type {
        COMPASS,
        LOCATION_BUTTON,
        SCALE_CONTROL,
        ZOOM_CONTROL_BUTTON,
        IN_DOOR_MAP
    }
}

组件操作接口

现在还差一个接口以供开发者设置组件的启用状态,于是创建一个 GenericMapComponentSupport 接口

interface GenericMapComponentSupport {
    /**
     * This variable should be initialized by the reified MapView Class
     */
    val mapComponents: Set<GenericMapComponent>

    /**
     * Enable/Disable map component without checking
     *
     * @param componentType [GenericMapComponent.Type]
     * @throws Exception Unexpected exception thrown by the reified mapView class
     */
    fun forceSetComponentEnabled(componentType: GenericMapComponent.Type, enabled: Boolean)
}

这个接口很简单,有一个 mapComponents 常量,因为不同 SDK 支持的地图组件是确定的,所以这个常量一旦被确定之后就无法修改,同时 GenericMapComponent 也可以保存地图组件的启用状态。

提示
Kotlin 中的接口支持常量以及具体函数的定义,但实际上常量编译成 Java 后也是一个接口,而这个变量则作为实现类中的一个成员常量。而接口实函数的定义编译成 Java 后则是用一个名为 DefaultImpls的伴生类实现的。

现在最基本的接口已经定义好了,接下来用上面提到的特性,完善一下这个接口:

interface GenericMapComponentSupport {
    /**
     * This variable should be initialized by the reified MapView Class
     */
    val mapComponents: Set<GenericMapComponent>

    /**
     * Enable/Disable map component
     *
     * @param componentType [GenericMapComponent.Type]
     * @throws MapComponentNotSupportedException
     */
    fun setComponentEnabled(componentType: GenericMapComponent.Type, enabled: Boolean) {
        val actualComponent = mapComponents.find { it.type == componentType }
        if (actualComponent == null) {
            throw MapComponentNotSupportedException(componentType, mapComponents.map { it.type })
        }

        this.forceSetComponentEnabled(componentType, enabled)

        // Update states of components
        actualComponent.setEnabled(enabled)
    }

    /**
     * Enable/Disable map component without checking
     *
     * @param componentType [GenericMapComponent.Type]
     * @throws Exception Unexpected exception thrown by the reified mapView class
     */
    fun forceSetComponentEnabled(componentType: GenericMapComponent.Type, enabled: Boolean)

    /**
     * Enable map component
     *
     * @param componentType [GenericMapComponent.Type]
     * @throws MapComponentNotSupportedException
     */
    fun enableComponent(componentType: GenericMapComponent.Type) {
        this.setComponentEnabled(componentType, true)
    }

    /**
     * Disable map component
     *
     * @param componentType [GenericMapComponent.Type]
     * @throws MapComponentNotSupportedException
     */
    fun disableComponent(componentType: GenericMapComponent.Type) {
        this.setComponentEnabled(componentType, false)
    }
}

现在开发者可以通过 setComponentEnabled() 方法来安全地设置组件状态,若该地图 SDK 不支持某个组件,则抛出 MapComponentNotSupportedException 异常。

关于 MapComponentNotSupportedException 的定义如下:

class MapComponentNotSupportedException(
    componentType: GenericMapComponent.Type,
    supportedComponentTypes: Collection<GenericMapComponent.Type>
) : RuntimeException("This component [$componentType] is not supported by the map, allowing: $supportedComponentTypes")

地图手势

同样地,地图手势也能作为地图的一部分,单独设置是否启用。

手势类

和上面的地图组件一样,定义一个地图手势类:

data class GenericMapGesture(
    val type: Type,
    private var enabled: Boolean = false
) {
    val isEnabled: Boolean get() = enabled

    fun setEnabled(enabled: Boolean) {
        this.enabled = enabled
    }

    enum class Type {
        // 滑动
        SCROLL,
        // 缩放
        ZOOM,
        // 俯视 (倾斜)
        OVER_LOOKING,
        // 旋转
        ROTATE,
        // 双击地图是否按中心点放大
        ENLARGE_CENTER_WITH_DOUBLE_CLICK
    }
}

手势操作接口

这部分和上面的 地图组件操作接口 是类似的,只是接口的参数类型不同。

当开发者调用 setGestureEnabled 设置地图不支持的手势时抛出 MapGestureNotSupportedException 异常,该异常的具体定义在 GenericMapGestureSupport 接口代码的下方。

interface GenericMapGestureSupport {
    /**
     * This variable should be initialized by the reified MapView Class
     */
    val mapGestures: Set<GenericMapGesture>

    /**
     * Enable/Disable map gesture
     *
     * @param gestureType [GenericMapGesture.Type]
     * @throws MapGestureNotSupportedException
     */
    fun setGestureEnabled(gestureType: GenericMapGesture.Type, enabled: Boolean) {
        val actualGesture = mapGestures.find { it.type == gestureType }
        if (actualGesture == null) {
            throw MapGestureNotSupportedException(gestureType, mapGestures.map { it.type })
        }

        this.forceGestureEnabled(gestureType, enabled)

        // Update states of gestures
        actualGesture.setEnabled(enabled)
    }

    /**
     * Enable/Disable map gesture without checking
     *
     * @param gestureType [GenericMapGesture.Type]
     * @throws Exception Unexpected exception thrown by the reified mapView class
     */
    fun forceGestureEnabled(gestureType: GenericMapGesture.Type, enabled: Boolean)

    /**
     * Enable map gesture
     *
     * @param gestureType [GenericMapGesture.Type]
     * @throws MapGestureNotSupportedException
     */
    fun enableGesture(gestureType: GenericMapGesture.Type) {
        this.setGestureEnabled(gestureType, true)
    }

    /**
     * Disable map gesture
     *
     * @param gestureType [GenericMapGesture.Type]
     * @throws MapGestureNotSupportedException
     */
    fun disableGesture(gestureType: GenericMapGesture.Type) {
        this.setGestureEnabled(gestureType, false)
    }
}

MapGestureNotSupportedException 的定义如下:

class MapGestureNotSupportedException(
    gestureType: GenericMapGesture.Type,
    supportedGestureTypes: Collection<GenericMapGesture.Type>
) : RuntimeException("This gesture [$gestureType] is not supported by the map, allowing: $supportedGestureTypes")

地图图层

地图 SDK 一般会提供普通地图、卫星图等地图图层,作为通用的地图属性之一,这一部分也必须抽象出来。

先定义一个图层枚举:

/**
 * Type/Map    AMap  BaiduMap
 * Normal       Y       Y
 * Satellite    Y       Y
 * Night        Y       N
 */
enum class LayerType {
    // 普通地图
    NORMAL,
    // 卫星地图
    SATELLITE,
    // 夜景地图
    NIGHT
}

和上面的地图/手势组件接口一样,定义一个 GenericMapLayerSupport 接口:

interface GenericMapLayerSupport {
    val supportedMapLayers: Set<LayerType>
    val currentMapLayer: ActualVariable<LayerType>

    /**
     * Change displaying layer of the map
     *
     * @param newLayer LayerType
     * @throws MapLayerNotSupportedException
     */
    fun changeLayer(newLayer: LayerType) {
        if (!supportedMapLayers.contains(newLayer)) {
            throw MapLayerNotSupportedException(newLayer, supportedMapLayers)
        }

        this.forceChangeLayer(newLayer)

        // Update current layer
        currentMapLayer.value = newLayer
    }

    /**
     * Change displaying layer of the map without checking
     *
     * @param newLayer LayerType
     * @throws Exception Unexpected exception thrown by the reified mapView class
     */
    fun forceChangeLayer(newLayer: LayerType)

    /**
     * Type/Map    AMap  BaiduMap
     * Normal       Y       Y
     * Satellite    Y       Y
     * Night        Y       N
     */
    enum class LayerType {
        NORMAL,
        SATELLITE,
        NIGHT
    }
}

MapLayerNotSupportedException 异常的定义如下:

class MapLayerNotSupportedException(
    layerType: GenericMapLayerSupport.LayerType,
    supportedLayerTypes: Collection<GenericMapLayerSupport.LayerType>
) : RuntimeException("This layer [${layerType.name}] is not supported by the map, allowing: $supportedLayerTypes")

MapView 生命周期

地图组件一般会提供和 Activity 类似的生命周期函数,在官方给出的实例中一般是在 Activity 对应的生命周期中调用这些函数,但在 Compose 中已经把 UI 组件化,于是便与 Activity 失去了必然联系。

但是我们仍可以通过 Compose 提供的 LocalLifecycleOwner 来得到 Composable 组件的生命周期,由此来等效传统的 Activity 的生命周期。

这一部分的具体实现在下一小节的 GenericMap 中,现在先定义一个 IGenericMapLifeCycle 接口:

interface IGenericMapLifeCycle<V: View> {
    /**
     * On mapView component created
     *
     * @param mapView Reified MapView Instance
     * @param context Component Context
     * @param bundle Bundle
     */
    fun onCreate(mapView: V, context: Context, bundle: Bundle)

    /**
     * On mapView component resumed
     *
     * @param mapView Reified MapView Instance
     */
    fun onResume(mapView: V)

    /**
     * On mapView component paused
     *
     * @param mapView Reified MapView Instance
     */
    fun onPause(mapView: V)

    /**
     * On mapView component destroyed
     *
     * @param mapView Reified MapView Instance
     */
    fun onDestroy(mapView: V)

    /**
     * On mapView component in low memory
     *
     * @param mapView Reified MapView Instance
     */
    fun onLowMemory(mapView: V)
}

泛型 V 表示的是具体的 MapView 类,百度和高德 SDK 中都提供了他们各自的 MapView。

GenericMap

接下来是最重要的 GenericMap 抽象类,对于不同的地图,都应该有一个类来实现这个抽象类,继而框架可以适配其原生的 MapView。

基本实现

先定义一个 GenericMap:

abstract class GenericMap<V: View> : IGenericMapLifecycle<V> {
}

刚才已经定义过 IGenericMapLifecycle<V> 接口了,这个生命周期接口应该由具体的地图类来实现,在 GenericMap 中统一管理地图组件的生命周期即可。

下面定义两个拓展函数,给 View 创建 lifecycleObserver 和 componentCallbacks:

/**
 * Build LifecycleObserver for Composable View
 *
 * @param context Context
 * @return LifecycleObserver
 */
private fun V.lifecycleObserver(context: Context): LifecycleObserver {
    return LifecycleEventObserver { _, event ->
        when(event) {
            Lifecycle.Event.ON_CREATE -> {
                onCreate(this@lifecycleObserver, context, Bundle())
            }
            Lifecycle.Event.ON_RESUME -> {
                onResume(this@lifecycleObserver)
            }
            Lifecycle.Event.ON_PAUSE -> {
                onPause(this@lifecycleObserver)
            }
            Lifecycle.Event.ON_DESTROY -> {
                onDestroy(this@lifecycleObserver)
            }
            else -> {}
        }
    }
}
/**
 * Build component callbacks
 *
 * @return ComponentCallbacks
 */
private fun V.componentCallbacks(): ComponentCallbacks {
    return object : ComponentCallbacks {
        override fun onConfigurationChanged(newConfig: Configuration) {}
        override fun onLowMemory() {
            onLowMemory(this@componentCallbacks)
        }
    }
}

接着定义一个 Composable 组件来将自定义的生命周期观察者(LifecycleObserver)应用到地图组件:

@Composable
private fun MapViewLifecycle(mapView: V) {
    val context = LocalContext.current
    val lifecycle = LocalLifecycleOwner.current.lifecycle
    DisposableEffect(context, lifecycle, mapView) {
        val mapLifecycleObserver = mapView.lifecycleObserver(context)
        val callbacks = mapView.componentCallbacks()
        lifecycle.addObserver(mapLifecycleObserver)
        context.registerComponentCallbacks(callbacks)
        onDispose {
            lifecycle.removeObserver(mapLifecycleObserver)
            context.unregisterComponentCallbacks(callbacks)
        }
    }
}

由于不同的 SDK 创建地图组件(MapView)的方法有些不同,百度和高德都要求在使用之前调用一些方法才可以使用(如同意隐私协议、全局初始化),下面给 GenericMap 定义两个抽象方法:

abstract class GenericMap<V: View> : IGenericMapLifecycle<V> {
    /**
     * Run before the creation of mapView
     *
     * @param context Component context
     */
    protected abstract fun doBefore(context: Context)

    /**
     * Create the actual MapView instance
     *
     * @param context Component context
     * @return Actual MapView Instance
     */
    protected abstract fun createView(context: Context): V
}

于是再利用 Compose 提供的 AndroidView 就能实现 Compose 中使用原生的 View 组件了:

@Composable
fun View(
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    doBefore(context)
    val mapView = remember {
        createView(context)
    }
    this.mapViewInstance = mapView
   
    AndroidView(modifier = modifier, factory = { mapView })
    MapViewLifecycle(mapView = mapView)
}

现在完整的 GenericMap 如下所示:

abstract class GenericMap<V: View> : IGenericMapLifecycle<V> {
    /**
     * Actual mapView instance
     */
    private var mapViewInstance: V? = null

    /**
     * Get reified mapView instance
     *
     * @return V (Reified MapInstance)
     */
    fun getMapViewInstance(): V = this.mapViewInstance ?: throw IllegalStateException("mapViewInstance is not initialized, make sure you have called @Composable[GenericMap\$View()] before this method.")

    @Composable
    fun View(
        modifier: Modifier = Modifier
    ) {
        val context = LocalContext.current

        doBefore(context)

        val mapView = remember {
            createView(context)
        }

        this.mapViewInstance = mapView

        AndroidView(modifier = modifier, factory = { mapView })
        MapViewLifecycle(mapView = mapView)
    }

    /**
     * Run before the creation of mapView
     *
     * @param context Component context
     */
    protected abstract fun doBefore(context: Context)

    /**
     * Create the actual MapView instance
     *
     * @param context Component context
     * @return Actual MapView Instance
     */
    protected abstract fun createView(context: Context): V
}

完善实现

上面还定义了 GenericMapLayerSupport、GenericMapComponentSupport 以及 GenericMapGestureExtendedSupport 接口。

虽然 GenericMap 这个抽象类不需要实现这些接口,但上面定义的地图组件和地图手势类是有默认状态的,所以地图初始化的时候应该应用地图的默认设置,也就是读取 mapComponents、mapGestures 等常量读取地图组件的默认状态,在地图创建时应用这些状态。

修改后的 View 如下所示:

// 用一个地址不变的对象容器来保存当前显示的地图图层
private val _currentMapLayer = defaultMapLayer.toActualVariable()
override val currentMapLayer: ActualVariable<GenericMapLayerSupport.LayerType>
    get() = _currentMapLayer

@Composable
fun View(
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    doBefore(context)
    val mapView = remember {
        createView(context)
    }
    this.mapViewInstance = mapView
    // Initialize all components, gestures
    this.mapComponents.forEach {
        this.setComponentEnabled(it.type, it.isEnabled)
    }
    this.mapGestures.forEach {
        this.setGestureEnabled(it.type, it.isEnabled)
    }
    // Initialize default map layer
    if (this.currentMapLayer.value != GenericMapLayerSupport.LayerType.NORMAL) {
        this.changeLayer(this.currentMapLayer.value)
    }
    AndroidView(modifier = modifier, factory = { mapView })
    MapViewLifecycle(mapView = mapView)
}

高德地图

上面写了这么多,终于到地图组件实现的部分了,下面先完成最基本的实现,将地图展示出来看看。

AMapView

按照上面的思路,创建一个 AMapView 类继承 GenericMap,并且指定高德地图支持的地图组件、手势以及图层:

class AMapView(
    override val mapComponents: Set<GenericMapComponent> = setOf(
        GenericMapComponent(GenericMapComponent.Type.COMPASS, false),
        GenericMapComponent(GenericMapComponent.Type.LOCATION_BUTTON, true),
        GenericMapComponent(GenericMapComponent.Type.ZOOM_CONTROL_BUTTON, false),
        GenericMapComponent(GenericMapComponent.Type.SCALE_CONTROL, false)
    ),
    override val mapGestures: Set<GenericMapGesture> = setOf(
        GenericMapGesture(GenericMapGesture.Type.SCROLL, true),
        GenericMapGesture(GenericMapGesture.Type.ZOOM, true),
        GenericMapGesture(GenericMapGesture.Type.ROTATE),
        GenericMapGesture(GenericMapGesture.Type.OVER_LOOKING),
        GenericMapGesture(GenericMapGesture.Type.ENLARGE_CENTER_WITH_DOUBLE_CLICK),
    ),
    override val supportedMapLayers: Set<GenericMapLayerSupport.LayerType> = setOf(
        GenericMapLayerSupport.LayerType.NORMAL,
        GenericMapLayerSupport.LayerType.SATELLITE,
        GenericMapLayerSupport.LayerType.NIGHT
    ),
    defaultMapLayer: GenericMapLayerSupport.LayerType = GenericMapLayerSupport.LayerType.NORMAL
) : GenericMap<MapView>(defaultMapLayer) {}

生命周期

先实现 Lifecycle 接口的所有方法:

    override fun onLowMemory(mapView: MapView) {
        mapView.onLowMemory()
    }

    override fun onDestroy(mapView: MapView) {
        mapView.map.setOnCameraChangeListener(null)
        mapView.onDestroy()
    }

    override fun onPause(mapView: MapView) {
        mapView.onPause()
    }

    override fun onResume(mapView: MapView) {
        mapView.onResume()
    }

    override fun onCreate(mapView: MapView, context: Context, bundle: Bundle) {
        /**
         * Fix:
         * java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
         *
         * Reason:
         * If this method is called, the rendered map created in [createView] will be overwritten
         */
        // mapView.onCreate(bundle)
    }

高德地图重复调用 mapView 的 onCreate 方法会导致之前创建的被覆盖,因此这里不要再调用 mapView 的 onCreate,避免地图无法正常使用。

地图创建

override fun doBefore(context: Context) {
    MapsInitializer.updatePrivacyShow(context, true, true)
    MapsInitializer.updatePrivacyAgree(context, true)
}

override fun createView(context: Context): MapView {
    return MapView(context, AMapOptions()).apply {
        onCreate(null)
    }
}

根据高德地图 SDK 文档给出的教程,需要先调用 MapsInitializer 内的两个方法才可以正常使用地图。

组件状态设置

override fun forceSetComponentEnabled(componentType: GenericMapComponent.Type, enabled: Boolean) {
    with(super.getMapViewInstance().map.uiSettings) {
        when (componentType) {
            GenericMapComponent.Type.COMPASS -> {
                this.isCompassEnabled = enabled
            }
            GenericMapComponent.Type.LOCATION_BUTTON -> {
                this.isMyLocationButtonEnabled = enabled
            }
            GenericMapComponent.Type.SCALE_CONTROL -> {
                this.isScaleControlsEnabled = enabled
            }
            GenericMapComponent.Type.ZOOM_CONTROL_BUTTON -> {
                this.isZoomControlsEnabled = enabled
            }
            GenericMapComponent.Type.IN_DOOR_MAP -> {
                super.getMapViewInstance().map.showIndoorMap(true)
            }
        }
    }
}

手势状态设置

override fun forceGestureEnabled(gestureType: GenericMapGesture.Type, enabled: Boolean) {
    with(super.getMapViewInstance().map.uiSettings) {
        when (gestureType) {
            GenericMapGesture.Type.SCROLL -> {
                this.isScrollGesturesEnabled = enabled
            }
            GenericMapGesture.Type.ZOOM -> {
                this.isZoomGesturesEnabled = enabled
            }
            GenericMapGesture.Type.OVER_LOOKING -> {
                this.isTiltGesturesEnabled = enabled
            }
            GenericMapGesture.Type.ROTATE -> {
                this.isRotateGesturesEnabled = enabled
            }
            GenericMapGesture.Type.ENLARGE_CENTER_WITH_DOUBLE_CLICK -> {
                this.setZoomInByScreenCenter(enabled)
            }
        }
    }
}

切换图层

override fun forceChangeLayer(newLayer: GenericMapLayerSupport.LayerType) {
    super.getMapViewInstance().map.mapType = when (newLayer) {
        GenericMapLayerSupport.LayerType.NORMAL -> {
            AMap.MAP_TYPE_NORMAL
        }
        GenericMapLayerSupport.LayerType.SATELLITE -> {
            AMap.MAP_TYPE_SATELLITE
        }
        GenericMapLayerSupport.LayerType.NIGHT -> {
            AMap.MAP_TYPE_NIGHT
        }
    }
}

测试

到这里就完成对高德地图的适配了,接下来测试一下吧:

class GenericMapTestActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MetaTripTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Column(modifier = Modifier.padding(innerPadding)) {
                        val amap = AMapView(defaultMapLayer = GenericMapLayerSupport.LayerType.SATELLITE)
                        amap.View(modifier = Modifier.weight(1f))

                        amap.changeLayer(GenericMapLayerSupport.LayerType.NIGHT)
                        amap.setRotateGestureEnabled(true)
                        amap.setEnlargeCenterWithDoubleClickGestureEnabled(true)
                        }
                    }
                }
            }
        }
    }
}

直接使用 AMapView 以及里面由 GenericMap 提供的 View 创建地图即可,效果如下:

百度地图

和高德地图是一样的实现方法,根据百度地图 SDK 官方文档很容易实现:

class BaiduMapView(
    override val mapComponents: Set<GenericMapComponent> = setOf(
        GenericMapComponent(GenericMapComponent.Type.COMPASS, false),
        GenericMapComponent(GenericMapComponent.Type.LOCATION_BUTTON, true),
        GenericMapComponent(GenericMapComponent.Type.ZOOM_CONTROL_BUTTON, false),
        GenericMapComponent(GenericMapComponent.Type.SCALE_CONTROL, false)
    ),
    override val mapGestures: Set<GenericMapGesture> = setOf(
        GenericMapGesture(GenericMapGesture.Type.SCROLL, true),
        GenericMapGesture(GenericMapGesture.Type.ZOOM, true),
        GenericMapGesture(GenericMapGesture.Type.ROTATE),
        GenericMapGesture(GenericMapGesture.Type.OVER_LOOKING),
        GenericMapGesture(GenericMapGesture.Type.ENLARGE_CENTER_WITH_DOUBLE_CLICK),
    ),
    override val supportedMapLayers: Set<GenericMapLayerSupport.LayerType> = setOf(
        GenericMapLayerSupport.LayerType.NORMAL,
        GenericMapLayerSupport.LayerType.SATELLITE
    ),
    defaultMapLayer: GenericMapLayerSupport.LayerType = GenericMapLayerSupport.LayerType.NORMAL
) : GenericMap<MapView>(defaultMapLayer) {
    override fun doBefore(context: Context) {
        SDKInitializer.setAgreePrivacy(context.applicationContext, true)
        SDKInitializer.initialize(context.applicationContext)
        SDKInitializer.setCoordType(CoordType.GCJ02)
    }

    override fun createView(context: Context): MapView {
        return MapView(context, BaiduMapOptions()).apply {
            onCreate(context, null)
        }
    }

    override fun onLowMemory(mapView: MapView) {
        // Nothing to do
    }

    override fun forceSetComponentEnabled(componentType: GenericMapComponent.Type, enabled: Boolean) {
        with(super.getMapViewInstance()) {
            val map = this.map
            val uiSettings = map.uiSettings
            when (componentType) {
                GenericMapComponent.Type.COMPASS -> {
                    uiSettings.isCompassEnabled = enabled
                }
                GenericMapComponent.Type.LOCATION_BUTTON -> {
                    map.isMyLocationEnabled = enabled
                }
                GenericMapComponent.Type.SCALE_CONTROL -> {
                    this.showScaleControl(enabled)
                }
                GenericMapComponent.Type.ZOOM_CONTROL_BUTTON -> {
                    this.showZoomControls(enabled)
                }
                GenericMapComponent.Type.IN_DOOR_MAP -> {
                    map.setIndoorEnable(enabled)
                }
            }
        }
    }

    override fun forceGestureEnabled(gestureType: GenericMapGesture.Type, enabled: Boolean) {
        with(super.getMapViewInstance().map.uiSettings) {
            when (gestureType) {
                GenericMapGesture.Type.SCROLL -> {
                    this.isScrollGesturesEnabled = enabled
                }
                GenericMapGesture.Type.ZOOM -> {
                    this.isZoomGesturesEnabled = enabled
                }
                GenericMapGesture.Type.OVER_LOOKING -> {
                    this.isOverlookingGesturesEnabled = enabled
                }
                GenericMapGesture.Type.ROTATE -> {
                    this.isRotateGesturesEnabled = enabled
                }
                GenericMapGesture.Type.ENLARGE_CENTER_WITH_DOUBLE_CLICK -> {
                    this.setEnlargeCenterWithDoubleClickEnable(enabled)
                }
            }
        }
    }

    override fun forceChangeLayer(newLayer: GenericMapLayerSupport.LayerType) {
        super.getMapViewInstance().map.mapType = when (newLayer) {
            GenericMapLayerSupport.LayerType.NORMAL -> {
                BaiduMap.MAP_TYPE_NORMAL
            }
            GenericMapLayerSupport.LayerType.SATELLITE -> {
                BaiduMap.MAP_TYPE_SATELLITE
            }
            else -> throw RuntimeException()
        }
    }

    override fun onDestroy(mapView: MapView) {
        mapView.onDestroy()
    }

    override fun onPause(mapView: MapView) {
        mapView.onPause()
    }

    override fun onResume(mapView: MapView) {
        mapView.onResume()
    }

    override fun onCreate(mapView: MapView, context: Context, bundle: Bundle) {
        mapView.onCreate(context, bundle)
    }
}

截止这篇文章发布的时间,百度地图官方文档中并没有支持夜景地图,其 MapView 也没有提供 onLowMemory() 方法。

结束语

目前仅支持了地图组件、地图手势以及图层切换,按照上面的思路,你可以继续完善这套框架。

未经允许禁止转载本站内容,经允许转载后请严格遵守CC-BY-NC-ND知识共享协议4.0,代码部分则采用GPL v3.0协议
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇