七、安卓权限和谷歌地图

概观

本章将为您提供如何在 Android 中请求和获取应用权限的知识。您将深入了解如何在应用中包含本地和全球交互式地图,以及如何通过使用谷歌地图应用编程接口请求使用提供更丰富功能的设备功能的权限。

本章结束时,您将能够为您的应用创建权限请求并处理缺失的权限。

简介

在前一章中,我们学习了如何使用RecyclerView在列表中呈现数据。我们利用这些知识向用户展示了一份秘密卡特彼勒代理商的名单。在本章中,我们将学习如何在地图上找到用户的位置,以及如何通过选择地图上的位置将卡特彼勒代理商部署到现场。

首先,我们将研究安卓权限系统。很多安卓功能并不是马上就能提供给我们的。为了保护用户,这些功能由权限系统控制。为了访问这些功能,我们必须请求用户允许我们这样做。一些这样的特征包括但不限于,获得用户的位置、访问用户的联系人、访问他们的相机以及建立蓝牙连接。不同的安卓版本执行不同的权限规则。例如,当安卓 6(棉花糖)在 2015 年推出时,许多权限被认为是不安全的(那些你可以在安装时悄悄获得的权限),并成为运行时权限。

然后我们将看看谷歌地图应用编程接口。该应用编程接口允许我们向用户呈现任何所需位置的地图,向该地图添加数据,并让用户与地图交互。它还可以让您显示感兴趣的点,并呈现受支持位置的街景,尽管我们不会在本书中讨论这些功能。

向用户请求权限

我们的应用可能希望实现谷歌认为危险的某些功能。这通常意味着访问这些功能可能会危及用户的隐私。例如,这些权限可能允许您读取用户的消息或确定他们的当前位置。

根据特定的权限和我们正在开发的目标安卓应用编程接口级别,我们可能需要向用户请求该权限。如果设备运行在 Android 6(Marshallow,或 API level 23)上,并且我们的应用的目标 API 是 23 或更高,这几乎肯定会是,因为到目前为止大多数设备都将运行较新版本的 Android,所以在安装时不会有用户通知提醒用户应用请求的任何权限。相反,我们的应用必须要求用户在运行时授予它这些权限。

当我们请求权限时,用户会看到一个对话框,类似于下面截图中显示的对话框:

Figure 7.1 Permission dialog for device location access

图 7.1 设备位置访问权限对话框

注意

有关权限及其保护级别的完整列表,请参见此处:https://developer . Android . com/reference/Android/manifest . permission

当我们打算使用权限时,我们必须在清单文件中包含该权限。具有SEND_SMS权限的清单看起来像下面的代码片段:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.snazzyapp">
 <uses-permission android:name="android.permission.SEND_SMS"/>
    <application ...>
...
    </application>
</manifest>

安全权限(或谷歌称之为普通权限)将自动授予用户。然而,危险的许可只有在用户明确批准的情况下才会被授予。如果我们未能向用户请求权限,并尝试执行需要该权限的操作,结果将是操作在最好的情况下不运行,在最坏的情况下我们的应用崩溃。

要向用户请求权限,我们应该首先检查用户是否已经授予我们该权限。

如果用户尚未授予我们权限,我们可能需要检查是否应该在权限请求之前显示基本原理对话框。这取决于请求的理由对用户来说有多明显。例如,如果一个相机应用请求访问相机的许可,我们可以放心地假设用户会清楚原因。然而,有些情况对用户来说可能不太清楚,尤其是如果用户不熟悉技术的话。在这些情况下,我们可能必须向用户证明请求的合理性。为此,谷歌为我们提供了一个名为shouldShowRequestPermissionRationale(Activity, String)的功能。在引擎盖下,该功能检查用户之前是否拒绝了权限,还检查用户是否在权限请求对话框中选择了Don't ask again。这个想法是给我们一个机会,让我们在请求许可之前向用户证明我们的请求是合理的,从而增加他们批准的可能性。

一旦我们确定是否应该向用户呈现许可理由,或者用户是否应该接受我们的理由或者不需要理由,我们就可以继续请求许可。

让我们看看如何申请许可。

我们向其请求权限的Activity类必须实现OnRequestPermissionsResultCallback接口。这是因为一旦用户被授予(或拒绝)权限,就会调用onRequestPermissionsResult(Int, Array<String>, IntArray)函数。AppCompatActivity扩展的FragmentActivity类已经实现了这个接口,所以我们只需要覆盖onRequestPermissionsResult函数来处理用户对权限请求的响应。下面是一个请求Location许可的Activity类的例子:

private const val PERMISSION_CODE_REQUEST_LOCATION = 1
class MainActivity : AppCompatActivity() {
    override fun onResume() {
        ...
        val hasLocationPermissions = getHasLocationPermission()
    }

当我们的Activity课恢复时,我们通过调用getHasLocationPermissions()来检查我们是否有位置权限(ACCESS_FINE_LOCATION):

    private fun getHasLocationPermission() = if (
        ContextCompat.checkSelfPermission(
            this, Manifest.permission.ACCESS_FINE_LOCATION
        ) == PackageManager.PERMISSION_GRANTED
    ) {
        true
    } else {
        if (ActivityCompat.shouldShowRequestPermissionRationale(
                this, Manifest.permission.ACCESS_FINE_LOCATION
            )
        ) {
            showPermissionRationale { requestLocationPermission() }
        } else {
            requestLocationPermission()
        }
        false
    }

该功能首先通过调用请求权限的checkSelfPermission(Context, String)来检查用户是否已经授予我们请求的权限。如果用户没有,我们就调用前面提到的shouldShowRequestPermissionRationale(Activity, String),检查是否应该向用户呈现一个基本原理对话框。

如果需要显示我们的基本原理,我们调用showPermissionRationale(() -> Unit),在用户关闭我们的基本原理对话框后,传入一个调用requestLocationPermission()的 lambda。如果不需要理由,我们直接称requestLocationPermission():

    private fun showPermissionRationale(positiveAction: () -> Unit) {
        AlertDialog.Builder(this)
            .setTitle("Location permission")
            .setMessage("We need your permission to find               your current position")
            .setPositiveButton(
                "OK"
            ) { _, _ -> positiveAction() }
            .create()
            .show()
    }

我们的showPermissionRationale功能只是向用户呈现一个对话框,简要说明我们为什么需要他们的许可。确认按钮将执行提供的积极措施:

Figure 7.2 Rationale dialog

图 7.2 基本原理对话框

    private fun requestLocationPermission() {
        ActivityCompat.requestPermissions(
            this,
            arrayOf(
                Manifest.permission.ACCESS_FINE_LOCATION
            ),
            PERMISSION_CODE_REQUEST_LOCATION
        )
    }

最后,我们的requestLocationPermission()函数调用requestPermissions(Activity, Array<out String>, Int),向我们的活动传递一个数组,该数组包含请求的权限和我们唯一的请求代码。我们稍后将使用该代码来识别属于该请求的响应。

如果我们已经向用户请求了位置许可,我们现在需要处理响应。这是通过覆盖onRequestPermissionsResult(Int, Array<out String>, IntArray)函数来完成的,如以下代码所示:

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, 
      grantResults)
    when (requestCode) {
        PERMISSION_CODE_REQUEST_LOCATION -> getLastLocation()
    }
}

onRequestPermissionsResult被调用时,传入三个值。首先是请求代码,这将是我们在调用requestPermissions时提供的相同请求代码。第二个是请求权限的数组。第三个是我们请求的一系列结果。对于每个请求的权限,该数组将包含PackageManager.PERMISSION_GRANTEDPackageManager.PERMISSION_DENIED

本章将带我们完成一个应用的开发,该应用在地图上显示我们的当前位置,并允许我们在想要部署我们的秘密猫特工的地方放置一个标记。让我们从第一个练习开始。

练习 7.01:请求位置许可

在本练习中,我们将请求用户提供位置权限。我们将首先创建一个谷歌地图活动项目。我们将在清单文件中定义所需的权限。首先,让我们实现向用户请求访问其位置的权限所需的代码:

  1. Start by creating a new Google Maps Activity project (File | New | New Project | Google Maps Activity). We're not using Google Maps in this exercise. However, the Google Maps Activity is still a good choice in this case. It will save you a lot of boilerplate coding in the next exercise (Exercise 7.02). Don't worry; it will have no impact on your current exercise. Click Next, as shown in the following screenshot:

    Figure 7.3: Choose your project

    图 7.3:选择你的项目

  2. 说出你的申请Cat Agent Deployer

  3. 确保您的套餐名称为com.example.catagentdeployer
  4. 将保存位置设置为要保存项目的位置。
  5. 将其他所有内容保留为默认值,然后单击Finish
  6. Make sure you are on the Android view in your Project pane:

    Figure 7.4: Android view

    图 7.4:安卓视图

  7. Open your AndroidManifest.xml file. Make sure the location permission was already added to your app:

    kt <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.catagentdeployer"> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <application ...> ... </application> </manifest>

    ACCESS_FINE_LOCATION是除了使用ACCESS_COARSE_LOCATION权限可以获得的不太准确的 Wi-Fi 和基于移动数据的位置信息之外,您还需要获得基于 GPS 的用户位置的权限。

  8. Open your MapsActivity.kt file. At the bottom of the MapsActivity class block, add an empty getLastLocation() function:

    kt class MapsActivity : AppCompatActivity(), OnMapReadyCallback { ... private fun getLastLocation() { Log.d("MapsActivity", "getLastLocation() called.") } }

    当您确定用户已经授予您定位权限时,您将调用这个函数。

  9. Next, add the request code constant to the top of the file, between the imports and the class definition:

    kt ... import com.google.android.gms.maps.model.MarkerOptions private const val PERMISSION_CODE_REQUEST_LOCATION = 1 class MapsActivity : AppCompatActivity(), OnMapReadyCallback {

    这将是我们请求位置许可时传递的代码。当用户通过授予或拒绝权限完成与请求对话框的交互时,我们在此定义的任何值都将返回给我们。

  10. Now add the requestLocationPermission() function right before the getLastLocation() function:

    kt private fun requestLocationPermission() { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), PERMISSION_CODE_REQUEST_LOCATION ) } private fun getLastLocation() { ... }

    该功能将向用户显示一个标准的权限请求对话框(如下图所示),要求他们允许应用访问他们的位置。我们传递活动,该活动将接收回调(this)、您希望用户授予您的应用的请求权限数组(Manifest.permission.ACCESS_FINE_LOCATION)以及您刚才定义的PERMISSION_CODE_REQUEST_LOCATION常量,以将其与权限请求相关联:

    Figure 7.5: Permission dialog

    图 7.5:权限对话框

  11. Override the onRequestPermissionsResult(Int, Array<String>, IntArray) function of your MapsActivity class:

    kt override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions,grantResults) when (requestCode) { PERMISSION_CODE_REQUEST_LOCATION -> if ( grantResults[0] == PackageManager.PERMISSION_GRANTED ) { getLastLocation() } } }

    您应该首先调用超级实现(一旦您重写了函数,这应该已经为您完成了)。这将处理对相关子片段的权限响应处理的委托。

    然后,您可以检查requestCode参数,看看它是否与您传递给requestPermissions(Activity, Array<out String>, Int)功能(PERMISSION_CODE_REQUEST_LOCATION)的requestCode参数相匹配。如果是的话,因为你知道你只请求了一个许可,你可以检查第一个grantResults值。如果等于PackageManager.PERMISSION_GRANTED,则用户已授予您的应用权限,您可以通过调用getLastLocation()继续获取他们的最后位置。

  12. If the user denied your app the requested permission, you can present them with the rationale for the request. Implement the showPermissionRationale(() -> Unit) function right before the requestLocationPermission() function:

    kt private fun showPermissionRationale(positiveAction: () -> Unit) { AlertDialog.Builder(this) .setTitle("Location permission") .setMessage("This app will not work without knowing your current location") .setPositiveButton( "OK" ) { _, _ -> positiveAction() } .create() .show() }

    该功能将向用户显示一个简单的警告对话框,解释在不知道用户当前位置的情况下,应用将无法运行,如下图所示。点击OK将执行提供的positiveActionλ:

    Figure 7.6: Rationale dialog

    图 7.6:基本原理对话框

  13. Add the logic required to determine whether to show the permission request dialog or the rationale one. Create the requestPermissionWithRationaleIfNeeded() function right before the showPermissionRationale(() -> Unit) function:

    kt private fun requestPermissionWithRationaleIfNeeded() = if ( ActivityCompat.shouldShowRequestPermissionRationale( this, Manifest.permission.ACCESS_FINE_LOCATION ) ) { showPermissionRationale { requestLocationPermission() } } else { requestLocationPermission() }

    此功能检查您的应用是否应该显示基本原理对话框。如果应该,它调用showPermissionRationale(() -> Unit),传入一个 lambda,通过调用requestLocationPermission()请求位置许可。否则直接调用requestLocationPermission()函数请求位置许可。

  14. 要确定您的应用是否已经拥有位置权限,请在requestPermissionWithRationaleIfNeeded()功能前引入此处显示的hasLocationPermission()功能:

    kt private fun hasLocationPermission() = ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED

  15. 最后,更新你的MapsActivity类的onMapReady()功能,一旦地图准备好就请求许可或获取用户的当前位置:

    kt override fun onMapReady(googleMap: GoogleMap) { mMap = googleMap if (hasLocationPermission()) { getLastLocation() } else { requestPermissionWithRationaleIfNeeded() } }

  16. 为了确保在用户拒绝许可时你给出理由,用一个else条件更新onRequestPermissionsResult(Int, Array<String>, IntArray):T2

  17. 运行您的应用。您现在应该会看到一个系统权限对话框,要求您允许应用访问设备的位置:

Figure 7.7: App requesting the location permission

图 7.7:申请位置许可的应用

如果您拒绝该权限,则会出现基本原理对话框,随后会出现另一个请求权限的系统权限对话框,如下图所示。这一次,用户可以选择不让应用再次请求许可。每次用户选择拒绝权限时,基本原理对话框将再次呈现给他们,直到他们选择允许权限或勾选Don't ask again选项:

Figure 7.8: Don’t ask again

图 7.8:不要再问了

一旦用户允许或永久拒绝该权限,该对话框将不再显示。要重置您的应用权限状态,您必须通过App Info界面手动授予其权限。

现在,我们可以获得位置许可,我们现在将研究获取用户的当前位置。

显示用户位置的地图

成功获得用户访问其位置的许可后,我们现在可以要求用户的设备向我们提供其最后已知的位置,这通常也是用户的当前位置。然后,我们将使用该位置向用户呈现他们当前位置的地图。

为了获得用户最后的已知位置,谷歌为我们提供了谷歌定位服务,更具体地说,是FusedLocationProviderClient类。FusedLocationProviderClient类帮助我们与谷歌的 Fused Location Provider API 进行交互,这是一个位置 API,可以智能地组合来自多个设备传感器的不同信号,为我们提供设备位置信息。

要访问FusedLocationProviderClient类,我们必须首先在我们的项目中包含 Google Play 位置服务库。这仅仅意味着将以下代码片段添加到我们的应用build.gradledependencies块中:

implementation "com.google.android.gms:play-services-location:17.1.0"

导入位置服务后,我们现在可以通过调用LocationServices.getFusedLocationProviderClient(this@MainActivity)获得FusedLocationProviderClient类的实例。

一旦我们有了融合位置客户端,假设我们已经从用户那里获得了位置许可,我们可以通过调用fusedLocationClient.lastLocation来获得用户的最后位置。由于这是一个异步调用,我们还应该至少提供一个成功的侦听器。如果我们愿意,我们还可以为取消、失败和请求的完成添加监听器。getLastLocation()呼叫(lastLocation简称 Kotlin)返回Task<Location>。任务是一个谷歌应用编程接口抽象类,其实现执行异步操作。在这种情况下,该操作返回一个位置。所以添加监听器只是一个链接的问题。我们将在调用中添加以下代码片段:

.addOnSuccessListener { location: Location? ->
}

请注意,如果客户端未能获取用户的当前位置,则location参数可以是null。这种情况并不常见,但是如果例如用户在呼叫期间禁用了他们的位置服务,这种情况就可能发生。

一旦成功监听器块中的代码被执行并且location不为空,我们就以Location实例的形式获得了用户的当前位置。

一个Location实例拥有一个地球上的单一坐标,用经度和纬度表示。对于我们的目的来说,只要知道地球表面的每个点都映射到一对经度(缩写:Lng)和纬度(缩写:Lat)值就足够了。

这才是真正令人兴奋的地方。谷歌允许我们通过使用SupportMapFragment类在交互式地图上呈现任何位置。只需要注册一个免费的应用编程接口密钥。当你用谷歌地图活动创建应用时,谷歌会为我们生成一个名为google_maps_api.xml的额外文件,可以在res/values下找到。该文件是我们的SupportMapFragment类工作所必需的,因为它包含我们的应用编程接口密钥。它还包含如何获取新的应用编程接口密钥的明确说明。方便的是,它还包含一个链接,可以为我们预先填充所需的注册数据。链接看起来有点像https://console.developers.google.com/flows/enableapi?apiid=...。从google_maps_api.xml文件复制到你的浏览器(或者 CMD + 点击链接上的,页面加载后按照页面上的指示操作,点击Create。一旦你有了一个密钥,用你新获得的密钥替换文件底部的YOUR_KEY_HERE字符串。

此时,如果您运行应用,您将在屏幕上看到一个交互式地图:

Figure 7.9: Interactive map

图 7.9:交互式地图

为了基于我们的当前位置定位地图,我们用来自我们的Location实例的坐标创建一个LatLng实例,并在GoogleMap实例上调用moveCamera(CameraUpdate)。为了满足CameraUpdate的要求,我们调用CameraUpdateFactory.newLatLng(LatLng),传入前面创建的LatLng参数。这个电话应该是这样的:

mMap.moveCamera(CameraUpdateFactory.newLatLng(latLng))

我们也可以调用newLatLngZoom(LatLng, Float)来修改地图的放大和缩小功能。

注意

有效的缩放值范围在2.0(最远)和21.0(最近)之间。超出该范围的值将被限制。

某些区域可能没有图块来渲染最接近的缩放值。要了解其他可用的CameraUpdateFactory选项,请访问

为了在用户的坐标上添加一个大头针(在谷歌地图应用编程接口中称为标记),我们在GoogleMap实例上调用addMarker(MarkerOptions)MarkerOptions参数通过链接调用MarkerOptions()实例来配置。对于我们期望位置的简单标记,我们可以称之为position(LatLng)title(String)。该调用看起来类似于以下内容:

mMap.addMarker(MarkerOptions().position(latLng).title("Pin Label"))

我们连接呼叫的顺序并不重要。

让我们在下面的练习中练习这个。

练习 7.02:获取用户的当前位置

现在,您的应用可以被授予位置权限,您可以继续使用位置权限来获取用户的当前位置。然后,您将显示地图并对其进行更新,以放大用户的当前位置,并在该位置显示 pin。请执行以下步骤:

  1. 首先,将谷歌 Play 定位服务添加到您的build.gradle文件中。您应该将其添加到dependencies区块内:

    kt dependencies { implementation "com.google.android.gms:play-services- location:17.1.0" implementation "org.jetbrains.kotlin:kotlin- stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.2.1' implementation 'com.google.android.gms:play-services-maps:17.0.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test .espresso:espresso-core:3.3.0' }

  2. 在 AndroidStudio 中点击Sync Project with Gradle Files按钮,Gradle 获取新添加的依赖项。

  3. 获取 API 密钥:首先打开生成的google_maps_api.xml文件(app/src/debug/res/values/google_maps_api.xml)和 CMD + 点击https://console.developers.google.com/flows/enableapi?apiid=开头的链接。
  4. 按照网站上的说明进行操作,直到生成新的应用编程接口密钥。
  5. 更新你的google_maps_api.xml文件,用你的新 API 密钥替换YOUR_KEY_HERE,如下行:

    kt <string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">YOUR_KEY_HERE</string>

  6. Open your MapsActivity.kt file. At the top of your MapsActivity class, define a lazily initialized fused location provider client:

    kt class MapsActivity : AppCompatActivity(), OnMapReadyCallback { private val fusedLocationProviderClient by lazy { LocationServices.getFusedLocationProviderClient(this) } override fun onCreate(savedInstanceState: Bundle?) { ... } ... }

    通过使fusedLocationProviderClient延迟初始化,您可以确保它仅在需要时初始化,这实质上保证了Activity类将在初始化之前已经被创建。

  7. getLastLocation()功能之后立即引入updateMapLocation(LatLng)功能和addMarkerAtLocation(LatLng, String)功能,分别在给定位置缩放地图和在该位置添加标记:

    kt private fun updateMapLocation(location: LatLng) { mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(location, 7f)) } private fun addMarkerAtLocation(location: LatLng, title: String) { mMap.addMarker(MarkerOptions().title(title).position(location)) }

  8. Now update your getLastLocation() function to retrieve the user's location:

    kt private fun getLastLocation() { fusedLocationProviderClient.lastLocation .addOnSuccessListener { location: Location? -> location?.let { val userLocation = LatLng(location.latitude, location.longitude) updateMapLocation(userLocation) addMarkerAtLocation(userLocation, "You") } } }

    您的代码通过调用lastLocation以 Kotlin 简洁的方式请求最后一个位置,然后附加一个lambda函数作为OnSuccessListener接口。一旦获得位置,执行lambda功能,更新地图位置,如果返回非空位置,则在该位置添加标题为You的标记。

  9. 运行您的应用:

Figure 7.10: Interactive map with a marker at the current location

图 7.10:在当前位置带有标记的交互式地图

一旦该应用获得许可,它就可以通过融合的位置提供商客户端从 Google Play 位置服务请求用户的最后位置。这为您提供了一种简单明了的获取用户当前位置的方法。请记住打开设备上的位置,以便应用工作。

有了用户的位置,你的应用可以告诉地图在哪里缩放,在哪里放置大头针。如果用户点击 pin,他们将看到您分配给它的标题(练习中的You)。

在下一节中,我们将学习如何响应地图上的点击以及如何移动标记。

地图点击和自定义标记

通过放大正确的位置并在那里放置大头针,地图显示了用户的当前位置,我们对如何渲染所需的地图有了初步的了解,也知道了如何获得所需的权限和用户的当前位置。

在本节中,我们将学习如何响应与地图交互的用户,以及如何更广泛地使用标记。我们将学习如何在地图上移动标记,以及如何用自定义图标替换默认大头针。当我们知道如何让用户在地图上的任何地方放置标记时,我们可以让他们选择在哪里部署秘密猫代理。

为了监听地图上的点击,我们需要向GoogleMap实例添加一个监听器。看看我们的MapsActivity.kt文件,最好的地方是在onMapReady(GoogleMap)。一个天真的实现应该是这样的:

override fun onMapReady(googleMap: GoogleMap) {
    mMap = googleMap.apply {
        setOnMapClickListener { latLng ->
            addMarkerAtLocation(latLng, "Deploy here")
        }
    }
    ...
}

然而,如果我们运行这段代码,我们会发现每次点击地图,都会添加一个新的标记。这不是我们想要的行为。

为了控制地图上的标记,我们需要保持对该标记的引用。通过保持对GoogleMap.addMarker(MarkerOptions)输出的引用,这很容易实现。addMarker函数返回一个Marker实例。要在地图上移动标记,我们只需通过调用其position设置器来分配一个新值。

要用自定义图标替换默认大头针图标,我们需要为标记或MarkerOptions()实例提供BitmapDescriptorBitmapDescriptor包装器围绕GoogleMap用来渲染标记(和地面覆盖)的位图工作,但我们不会在本书中介绍。我们通过使用BitmapDescriptorFactory获得BitmapDescriptor。工厂需要一项素材,可以通过多种方式提供。您可以为其提供assets目录中的位图名称、Bitmap、内部存储器中的文件文件名或资源标识。工厂还可以创建不同颜色的默认标记。我们对Bitmap选项感兴趣,因为我们打算使用可绘制的矢量,而工厂不直接支持这些选项。此外,当将可绘制转换为Bitmap时,我们可以根据需要对其进行操作(例如,我们可以更改其颜色)。

AndroidStudio 为我们提供了相当广泛的开箱即用的自由矢量Drawables。对于这个例子,我们希望paw可以绘制。为此,右键单击左安卓窗格中的任意位置,并选择New | Vector Asset

现在,点击图标列表中Clip Art标签旁边的安卓图标:

Figure 7.11: Asset Studio

图 7.11:素材工作室

我们现在将进入一个窗口,从中可以从提供的剪贴画池中进行选择:

Figure 7.12: Selecting an icon

图 7.12:选择图标

一旦我们选择了一个图标,我们就可以给它命名,它将被创建为一个矢量可绘制的 XML 文件。我们将其命名为target_icon

要使用创建的素材,我们必须首先将其作为Drawable实例获取。这是通过调用ContextCompat.getDrawable(Context, Int),传入活动和R.drawable.target_icon作为我们素材的参考来完成的。接下来,我们需要为要绘制的Drawable实例定义边界。用(00drawable.intrinsicWidthdrawable.intrinsicHeight)调用Drawable.setBound(Int, Int, Int, Int)会告诉它在固有大小内进行绘制。

要改变图标的颜色,我们必须给它着色。要以运行早于21的 API 的设备支持的方式着色Drawable实例,我们必须首先通过调用DrawableCompat.wrap(Drawable)DrawableCompat包装我们的Drawable实例。然后可以使用DrawableCompat.setTint(Drawable, Int)对返回的Drawable进行着色。

接下来,我们需要创建一个Bitmap来保存我们的图标。它的尺寸可以匹配Drawable边界的尺寸,我们希望它的ConfigBitmap.Config.ARGB_8888,这意味着全红色、绿色、蓝色和阿尔法通道。然后我们为Bitmap创建一个Canvas,允许我们通过调用Drawable实例…你猜对了,Drawable.draw(Canvas):

private fun getBitmapDescriptorFromVector(@DrawableRes   vectorDrawableResourceId: Int): BitmapDescriptor? {
    val bitmap =
        ContextCompat.getDrawable(this, vectorDrawableResourceId)?.let {           vectorDrawable ->
            vectorDrawable
                .setBounds(0, 0, vectorDrawable.intrinsicWidth,                   vectorDrawable.intrinsicHeight)
            val drawableWithTint = DrawableCompat.wrap(vectorDrawable)
            DrawableCompat.setTint(drawableWithTint, Color.RED)
            val bitmap = Bitmap.createBitmap(
                vectorDrawable.intrinsicWidth,
                vectorDrawable.intrinsicHeight,
                Bitmap.Config.ARGB_8888
            )
            val canvas = Canvas(bitmap)
            drawableWithTint.draw(canvas)
            bitmap
        }
    return BitmapDescriptorFactory.fromBitmap(bitmap)      .also {
          bitmap?.recycle()
    }
}

随着Bitmap包含我们的图标,我们现在准备从BitmapDescriptorFactory获得一个BitmapDescriptor实例。之后别忘了回收你的Bitmap。这将避免内存泄漏。

您学习了如何向用户呈现有意义的地图,方法是将地图放在用户当前位置的中心,并使用大头针标记显示他们的当前位置。

练习 7.03:在单击地图的位置添加自定义标记

在本练习中,您将通过在用户单击的地图位置放置红色爪形标记来响应用户的地图单击:

  1. MapsActivity.kt(位于app/src/main/java/com/example/catagentdeployer下)mMap变量定义的正下方,定义一个可空的Marker变量来保存对地图上爪子标记的引用:

    kt private lateinit var mMap: GoogleMap private var marker: Marker? = null

  2. Update addMarkerAtLocation(LatLng, String) to also accept a nullable BitmapDescriptor with a default value of null:

    kt private fun addMarkerAtLocation( location: LatLng, title: String, markerIcon: BitmapDescriptor? = null ) = mMap.addMarker( MarkerOptions() .title(title) .position(location) .apply { markerIcon?.let { icon(markerIcon) } } )

    如果提供的markerIcon不为空,则应用将其设置为MarkerOptions。该函数现在返回它添加到地图中的标记。

  3. Create a getBitmapDescriptorFromVector(Int): BitmapDescriptor? function below your addMarkerAtLocation(LatLng, String, BitmapDescriptor?): Marker function to provide BitmapDescriptor given a Drawable resource ID:

    kt private fun getBitmapDescriptorFromVector(@DrawableRes vectorDrawableResourceId: Int): BitmapDescriptor? { val bitmap = ContextCompat.getDrawable(this, vectorDrawableResourceId)?.let { vectorDrawable -> vectorDrawable .setBounds(0, 0, vectorDrawable.intrinsicWidth, vectorDrawable.intrinsicHeight) val drawableWithTint = DrawableCompat .wrap(vectorDrawable) DrawableCompat.setTint(drawableWithTint, Color.RED) val bitmap = Bitmap.createBitmap( vectorDrawable.intrinsicWidth, vectorDrawable.intrinsicHeight, Bitmap.Config.ARGB_8888 ) val canvas = Canvas(bitmap) drawableWithTint.draw(canvas) bitmap } return BitmapDescriptorFactory.fromBitmap(bitmap).also { bitmap?.recycle() } }

    该函数首先通过传入提供的资源标识,使用ContextCompat获取一个可绘制的。然后,它为可绘制对象设置绘制边界,将其包裹在DrawableCompat中,并将其色调设置为红色。

    然后,它为那个Bitmap创建了一个Bitmap和一个Canvas,并在上面绘制了着色的可拉伸图。该位图随后被返回给BitmapDescriptorFactory用于构建BitmapDescriptor。最后,Bitmap被回收以避免内存泄漏。

  4. 在使用Drawable实例之前,必须先创建它。右键点击安卓面板,然后选择New | Vector Asset

  5. In the window that opens, click on the Android icon next to the Clip Art label to select a different icon:

    Figure 7.13: Asset Studio

    图 7.13:素材工作室

  6. From the list of icons, select the pets icon. You can type pets into the search field if you can't find the icon. Once you select the pets icon, click OK:

    Figure 7.14: Selecting an icon

    图 7.14:选择图标

  7. 命名你的图标target_icon。点击NextFinish

  8. 定义一个addOrMoveSelectedPositionMarker(LatLng)函数来创建一个新的标记,或者如果已经创建了标记,将其移动到提供的位置。在getBitmapDescriptorFromVector(Int)功能后添加:

    kt private fun addOrMoveSelectedPositionMarker(latLng: LatLng) { if (marker == null) { marker = addMarkerAtLocation( latLng, "Deploy here", getBitmapDescriptorFromVector(R.drawable.target_icon) ) } else { marker?.apply { position = latLng } } }

  9. 更新您的onMapReady(GoogleMap)功能,在mMap上设置一个OnMapClickListener事件,该事件将向被点击的位置添加一个标记或将现有标记移动到被点击的位置:

    kt override fun onMapReady(googleMap: GoogleMap) { mMap = googleMap.apply { setOnMapClickListener { latLng -> addOrMoveSelectedPositionMarker(latLng) } } if (hasLocationPermission()) { getLastLocation() } else { requestPermissionWithRationaleIfNeeded() } }

  10. Run your app:

    Figure 7.15: The complete app

图 7.15:完整的应用

点击地图上的任何地方,现在将爪子图标移动到那个位置。点击爪子图标将显示Deploy here标签。注意爪子的位置是地理位置,不是屏幕位置。这意味着,如果你拖动地图或放大,爪子会随着地图移动,并保持在同一地理位置。您现在知道如何响应用户在地图上的点击,以及如何添加和移动标记。您还知道如何自定义标记的外观。

活动 7.01:创建一个应用来查找停放汽车的位置

有些人经常忘记他们把车停在哪里。假设你想通过开发一个应用来帮助这些人,让用户存储他们停车的最后一个地方。当用户启动应用时,它会在用户告诉应用汽车位置的最后一个地方显示一个 pin。用户可以点击I'm parked here按钮,在下次停车时将 pin 位置更新到当前位置。

您在本活动中的目标是开发一个应用,向用户显示当前位置的地图。它首先必须向用户请求访问其位置的许可。根据软件开发工具包,确保在需要时也提供一个基本原理对话框。该应用将显示一个汽车图标,用户最后一次告诉它汽车在哪里。用户可以点击标记为I'm parked here的按钮,将汽车图标移动到当前位置。当用户重新启动应用时,它会显示用户的当前位置和汽车上次停放的汽车图标。

作为应用的额外功能,您可以选择添加存储汽车位置的功能,以便在用户杀死后恢复汽车位置,然后重新打开应用。该奖励功能依赖于使用SharedPreferences;这一概念将在第 11 章持续数据中介绍。因此,下面的步骤 9 和 10 将为您提供所需的实现。

以下步骤将帮助您完成活动:

  1. 创建一个谷歌地图活动应用。
  2. 获取应用的应用编程接口密钥,并使用该密钥更新您的google_maps_api.xml文件。
  3. 在底部显示一个带有I'm parked here标签的按钮。
  4. 在您的应用中包含位置服务。
  5. 请求用户允许访问他们的位置。
  6. 获取用户的位置,并在地图上该位置放置一个大头针。
  7. 将汽车图标添加到项目中。
  8. 添加将汽车图标移动到用户当前位置的功能。
  9. 将所选位置存储在SharedPreferences中。该功能放在您的活动中,将有助于:

    kt private fun saveLocation(latLng: LatLng) = getPreferences(MODE_PRIVATE)?.edit()?.apply { putString("latitude", latLng.latitude.toString()) putString("longitude", latLng.longitude.toString()) apply() }

  10. Restore any saved location from SharedPreferences. You can use the following function:

    kt val latitude = sharedPreferences.getString("latitude", null) ?.toDoubleOrNull() ?: return null val longitude = sharedPreferences.getString("longitude", null)?.toDoubleOrNull() ?: return null

    注意

    这个活动的解决方案可以在:http://packt.live/3sKj1cp找到

总结

在这一章中,我们已经了解了安卓权限。我们谈到了拥有它们的原因,并了解了如何请求用户的许可来执行某些任务。我们还学习了如何使用谷歌地图应用编程接口,以及如何向用户呈现交互式地图。最后,我们利用我们在呈现地图和请求权限方面的知识来找出用户的当前位置并将其呈现在地图上。使用谷歌地图应用编程接口可以做的事情还有很多,你可以在一定的权限下探索更多的可能性。你现在应该对两者的基础有足够的了解,以便进一步探索。要阅读更多关于权限的信息,请访问 https://developer . Android . com/reference/Android/manifest . permission .要阅读更多关于地图 API 的信息,请访问https://developers . Google . com/Maps/documentation/Android-SDK/intro

在下一章中,我们将学习如何使用ServicesWorkManager执行后台任务。我们还将学习如何向用户呈现通知,即使应用没有运行。作为一名移动开发人员,这些都是强大的工具。