“地点详情”组件
Places 界面套件的地点详情组件可让您添加一个用于在应用中显示地点详情的界面组件。此组件可自定义。

地点详情组件可以单独使用,也可以与其他 Google Maps Platform API 和服务搭配使用。该组件接受地点 ID、资源名称或纬度/经度坐标,并返回呈现的地点详情信息。
地点详情组件完全可设置主题,让您可以自定义字体、颜色和圆角半径,以符合您的使用情形和视觉品牌推广指南。您可以创建扩展 PlacesMaterialTheme
的主题并提供主题属性的替换项,从而自定义地点详情的外观。您还可以通过指定 Content 条目的列表来自定义要包含的地点详情字段,每个条目都对应于显示的地点信息。
布局变体
Place Details 组件支持两种主要布局变体:
- 紧凑:用于预览关键信息的布局。
- 完整:一种全面的布局,可显示所有可用的地点详细信息。
紧凑型布局可以采用垂直或水平方向显示。这样一来,您就可以将该组件集成到各种设计布局和屏幕尺寸中。完整布局只能以竖屏模式显示。

借助“地点详情”组件,您可以精细控制组件中显示的内容。每个元素(例如照片、评价和联系信息)都可以单独显示或隐藏,从而实现对组件外观和信息密度的精细自定义。

地点详情精简视图
地点详情紧凑型 fragment (PlaceDetailsCompactFragment
) 使用最少的空间呈现所选地点的详细信息。这在以下情况下可能很有用:在突出显示地图上某个地点的信息窗口中;在社交媒体体验中(例如在聊天中分享位置信息);作为选择当前位置的建议;或在媒体文章中引用 Google 地图上的某个地点。
“地点详情”完整视图
地点详情全视图 (PlaceDetailsFragment
) 可提供更大的界面来显示地点详情信息,并让您显示更多类型的信息。
内容显示选项
您可以使用 PlaceDetailsCompactFragment.Content
或 PlaceDetailsFragment.Content
中的枚举指定要显示的内容。
精简视图 | 全本阅读 |
---|---|
|
|
结算
使用地点详情界面套件时,每次调用 .loadWithPlaceId()
、.loadWithResourceName()
或 loadWithCoordinates()
方法时,系统都会向您收取费用。如果您多次加载同一地点,则需要为每次请求付费。
为避免多次收费,请勿直接在 Android 生命周期方法中添加 .loadWithPlaceId()
或 .loadWithResourceName()
。例如,请勿在 onResume()
方法中直接调用 .loadWithPlaceId()
或 .loadWithResourceName()
。
向应用添加地点详情
您可以通过向布局添加 fragment,向应用添加地点详情。实例化 fragment 时,您可以自定义地点详情信息的外观和风格,以满足您的需求并与应用的外观相符。详细了解自定义。
在 Kotlin 和 Java 中,您可以使用三种方法:一种是使用地点 ID (loadWithPlaceId()
) 加载 fragment,一种是使用资源名称 (loadWithResourceName()
) 加载 fragment,还有一种是使用纬度/经度坐标 (loadWithCoordinates()
) 加载 fragment。您可以选择任意一种或多种方法。
紧凑视图的默认位置为竖屏。如果您想要横向布局,请指定 Orientation.HORIZONTAL
。您还可以选择指定 Orientation.VERTICAL
以提高清晰度。全屏视图只能以竖屏模式显示。
请参阅地点详情组件示例部分中的示例。
自定义视觉效果

Places 界面套件提供了一种设计系统方法,用于进行视觉自定义,大致基于 Material Design(进行了一些 Google 地图特有的修改)。请参阅 Material Design 的颜色和排版参考。默认情况下,样式遵循 Google 地图视觉设计语言。

实例化 fragment 时,您可以指定一个主题来替换任何默认样式属性。未被覆盖的任何主题背景属性都将使用默认样式。如果您想支持深色主题,可以在 values-night/colors.xml
中为颜色添加条目。
<style name="CustomizedPlaceDetailsTheme" parent="PlacesMaterialTheme"> <item name="placesColorPrimary">@color/app_primary_color</item> <item name="placesColorOnSurface">@color/app_color_on_surface</item> <item name="placesColorOnSurfaceVariant">@color/app_color_on_surface</item> <item name="placesTextAppearanceBodySmall">@style/app_text_appearence_small</item> <item name="placesCornerRadius">20dp</item> </style>
您可以自定义以下样式:

主题属性 | 用法 |
---|---|
颜色 | |
placesColorSurface |
容器和对话框背景 |
placesColorOutlineDecorative |
容器边框 |
placesColorPrimary |
链接、加载指示器、概览图标 |
placesColorOnSurface |
标题、对话框内容 |
placesColorOnSurfaceVariant |
地点信息 |
placesColorSecondaryContainer |
按钮背景 |
placesColorOnSecondaryContainer |
按钮文字和图标 |
placesColorNeutralContainer |
查看日期标记、加载占位符形状 |
placesColorOnNeutralContainer |
查看日期,加载时出错 |
placesColorPositiveContainer |
“有电动汽车充电桩”徽章 |
placesColorOnPositiveContainer |
“有电动汽车充电桩”徽章内容 |
placesColorPositive |
放置“营业中”标签 |
placesColorNegative |
“已关闭”地点标签 |
placesColorInfo |
“无障碍入口”图标 |
placesColorButtonBorder |
“在地图中打开”按钮和“确定”按钮 |
排版 | |
placesTextAppearanceBodySmall |
地点信息 |
placesTextAppearanceBodyMedium |
地点信息,对话框内容 |
placesTextAppearanceLabelMedium |
徽章内容 |
placesTextAppearanceLabelLarge |
按钮内容 |
placesTextAppearanceHeadlineMedium |
对话框标题 |
placesTextAppearanceDisplaySmall |
地点名称 |
placesTextAppearanceTitleSmall |
地点名称 |
间距 | |
placesSpacingExtraSmall |
|
placesSpacingSmall |
|
placesSpacingMedium |
|
placesSpacingLarge |
|
placesSpacingExtraLarge |
|
placesSpacingTwoExtraLarge |
|
效果衡量 | |
placesBorderWidth |
Container |
placesBorderWidthButton |
|
形状 | |
placesCornerRadius |
Container |
placesCornerRadiusButton |
“在 Google 地图中打开”按钮和“确定”按钮(圆形图标按钮除外) |
placesCornerRadiusThumbnail |
地点缩略图 |
placesCornerRadiusCollageOuter |
媒体拼图 |
placesCornerRadiusCard |
地点卡片、用户评价卡片 |
placesCornerRadiusDialog |
Google 地图披露声明对话框 |
Google 地图品牌提供方信息 | |
placesColorAttributionLightTheme |
浅色主题 Google 地图署名和披露信息按钮(白色、灰色和黑色枚举) |
placesColorAttributionDarkTheme |
深色主题 Google 地图署名和披露信息按钮(白色、灰色和黑色枚举) |
请参阅地点详情组件示例部分中的示例。
宽度和高度自定义
紧凑视图
建议的宽度:
- 竖屏方向:介于 180dp 和 300dp 之间。
- 横向屏幕方向:介于 180dp 和 500dp 之间。
宽度小于 160dp 的屏幕可能无法正确显示。
最佳实践是不为紧凑型视图设置高度。这样一来,窗口中的内容就可以设置高度,从而显示所有信息。
完整观看次数
对于完整视图,建议的宽度介于 250dp 和 450dp 之间。宽度小于 250dp 的屏幕可能无法正确显示。
您可以设置组件的高度:垂直地点详情视图将在分配的空间内垂直滚动。
最佳做法是为全屏视图设置高度。这样,窗口中的内容就可以正常滚动了。
归因颜色

《Google 地图服务条款》要求您使用三种品牌颜色之一来注明 Google 地图的版权归属信息。在进行自定义更改后,此提供方信息必须可见且可访问。
我们提供 3 种品牌颜色供您选择,这些颜色可以针对浅色主题和深色主题分别设置:
- 浅色主题:
placesColorAttributionLight
,包含白色、灰色和黑色的枚举值。 - 深色主题:
placesColorAttributionDark
,具有白色、灰色和黑色的枚举值。
“地点详情”组件示例
创建紧凑视图或完整视图
Kotlin
// We create a new instance of the fragment using its factory method. // We can specify which content to show, the orientation, and a custom theme. val fragment = PlaceDetailsCompactFragment.newInstance( PlaceDetailsCompactFragment.ALL_CONTENT, // Show all available content. orientation, R.style.CustomizedPlaceDetailsTheme, ).apply { // The PlaceLoadListener provides callbacks for when the place data is successfully // loaded or when an error occurs. This is where we update our UI state. setPlaceLoadListener(object : PlaceLoadListener { override fun onSuccess(place: Place) { Log.d(TAG, "Place loaded: ${place.id}") // Once the data is loaded, we hide the loading indicator and show the fragment. binding.loadingIndicatorMain.visibility = View.GONE binding.placeDetailsContainer.visibility = View.VISIBLE binding.dismissButton.visibility = View.VISIBLE } override fun onFailure(e: Exception) { Log.e(TAG, "Place failed to load", e) // On failure, we hide the UI and notify the user. dismissPlaceDetails() Toast.makeText(this@MainActivity, "Failed to load place details.", Toast.LENGTH_SHORT).show() } }) } // We add the fragment to our layout's container view. // `commitNow()` is used to ensure the fragment is immediately added and available, // which is important because we need to call a method on it right after. supportFragmentManager .beginTransaction() .replace(binding.placeDetailsContainer.id, fragment) .commitNow() // **This is the key step**: After adding the fragment, we call `loadWithPlaceId` // to trigger the data loading process for the selected place. // We use `post` to ensure this runs after the layout has been measured, // which can prevent potential timing issues. binding.root.post { fragment.loadWithPlaceId(placeId) } }
Java
PlaceDetailsCompactFragment fragment = PlaceDetailsCompactFragment.newInstance( Orientation.HORIZONTAL, Arrays.asList(Content.ADDRESS, Content.TYPE, Content.RATING, Content.ACCESSIBLE_ENTRANCE_ICON), R.style.CustomizedPlaceDetailsTheme); fragment.setPlaceLoadListener( new PlaceLoadListener() { @Override public void onSuccess(Place place) { ... } @Override public void onFailure(Exception e) { ... } }); getSupportFragmentManager() .beginTransaction() .add(R.id.fragment_container, fragment) .commitNow(); // Load the fragment with a Place ID. fragment.loadWithPlaceId(placeId); // Load the fragment with a resource name. fragment.loadWithResourceName(resourceName);
此完整代码示例会根据用户设备的配置,以编程方式确定紧凑视图的方向。
Kotlin
package com.example.placedetailsuikit import android.Manifest import android.annotation.SuppressLint import android.content.pm.PackageManager import android.content.res.Configuration import android.location.Location import android.os.Bundle import android.util.Log import android.view.View import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.lifecycle.ViewModel import com.example.placedetailsuikit.databinding.ActivityMainBinding import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.OnMapReadyCallback import com.google.android.gms.maps.SupportMapFragment import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.PointOfInterest import com.google.android.libraries.places.api.Places import com.google.android.libraries.places.api.model.Place import com.google.android.libraries.places.widget.PlaceDetailsCompactFragment import com.google.android.libraries.places.widget.PlaceLoadListener import com.google.android.libraries.places.widget.model.Orientation private const val TAG = "PlacesUiKit" /** * A simple ViewModel to store UI state that needs to survive configuration changes. * In this case, it holds the ID of the selected place. Using a ViewModel is good practice * as it prevents data loss during events like screen rotation, ensuring a * seamless user experience. */ class MainViewModel : ViewModel() { var selectedPlaceId: String? = null } /** * This activity serves as a basic example of integrating the Place Details UI Kit. * It demonstrates the fundamental steps required: * 1. Setting up a Google Map. * 2. Requesting location permissions to center the map. * 3. Handling clicks on Points of Interest (POIs) to get a Place ID. * 4. Using the Place ID to load and display place details in a [PlaceDetailsCompactFragment]. */ class MainActivity : AppCompatActivity(), OnMapReadyCallback, GoogleMap.OnPoiClickListener { // ViewBinding provides type-safe access to views defined in the XML layout, // eliminating the need for `findViewById` and preventing null pointer exceptions. private lateinit var binding: ActivityMainBinding private var googleMap: GoogleMap? = null // The FusedLocationProviderClient is the main entry point for interacting with the // fused location provider, which intelligently manages the underlying location technologies. private lateinit var fusedLocationClient: FusedLocationProviderClient // Using registerForActivityResult is the modern, recommended approach for handling // permission requests. It decouples the request from the handling logic, making the // code cleaner and easier to manage compared to the older `onRequestPermissionsResult` callback. private lateinit var requestPermissionLauncher: ActivityResultLauncher<Array<String>> // The `by viewModels()` delegate provides a lazy-initialized ViewModel scoped to this Activity. // This ensures that we get the same ViewModel instance across configuration changes. private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // The ActivityResultLauncher is initialized here. The lambda defines the callback // that will be executed once the user responds to the permission dialog. requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> // We check if either fine or coarse location permission was granted. if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true || permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true) { Log.d(TAG, "Location permission granted by user.") fetchLastLocation() } else { // If permission is denied, we inform the user and default to a known location. // This ensures the app remains functional even without location access. Log.d(TAG, "Location permission denied by user.") Toast.makeText( this, "Location permission denied. Showing default location.", Toast.LENGTH_LONG ).show() moveToSydney() } } // enableEdgeToEdge() allows the app to draw behind the system bars for a more immersive experience. enableEdgeToEdge() binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.dismissButton.setOnClickListener { dismissPlaceDetails() } // --- Crucial: Initialize Places SDK --- // It's essential to initialize the Places SDK before making any other Places API calls. // This should ideally be done once, for example, in the Application's `onCreate`. val apiKey = BuildConfig.PLACES_API_KEY if (apiKey.isEmpty() || apiKey == "YOUR_API_KEY") { // A valid API key is required for the Places SDK to function. Log.e(TAG, "No api key") Toast.makeText( this, "Add your own API_KEY in local.properties", Toast.LENGTH_LONG ).show() finish() return } // `initializeWithNewPlacesApiEnabled` is used to opt-in to the new SDK version. Places.initializeWithNewPlacesApiEnabled(applicationContext, apiKey) fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) // ------------------------------------ // The SupportMapFragment is the container for the map. `getMapAsync` allows us to // work with the GoogleMap object via a callback once it's fully initialized. val mapFragment = supportFragmentManager.findFragmentById(R.id.map_fragment) as SupportMapFragment? mapFragment?.getMapAsync(this) // This block handles restoration after a configuration change (e.g., screen rotation). // If a place was selected before the rotation, its ID is stored in the ViewModel. // We use this ID to immediately show the details fragment again. if (viewModel.selectedPlaceId != null) { viewModel.selectedPlaceId?.let { placeId -> Log.d(TAG, "Restoring PlaceDetailsFragment for place ID: $placeId") showPlaceDetailsFragment(placeId) } } } /** * This callback is triggered when the GoogleMap object is ready to be used. * All map setup logic should be placed here. */ override fun onMapReady(map: GoogleMap) { Log.d(TAG, "Map is ready") googleMap = map // Setting the OnPoiClickListener allows us to capture user taps on points of interest. googleMap?.setOnPoiClickListener(this) // After the map is ready, we determine the initial camera position based on location permissions. if (isLocationPermissionGranted()) { fetchLastLocation() } else { requestLocationPermissions() } } /** * A helper function to centralize the check for location permissions. */ private fun isLocationPermissionGranted(): Boolean { return ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_COARSE_LOCATION ) == PackageManager.PERMISSION_GRANTED } /** * This function triggers the permission request flow. The result is handled by the * ActivityResultLauncher defined in `onCreate`. */ private fun requestLocationPermissions() { Log.d(TAG, "Requesting location permissions.") requestPermissionLauncher.launch( arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION ) ) } /** * Fetches the device's last known location. This is a fast and battery-efficient way * to get a location fix. It should only be called after verifying permissions. */ @SuppressLint("MissingPermission") private fun fetchLastLocation() { // Double-checking permissions here is a good practice, although the call sites are already guarded. if (isLocationPermissionGranted()) { fusedLocationClient.lastLocation .addOnSuccessListener { location: Location? -> if (location != null) { val userLocation = LatLng(location.latitude, location.longitude) googleMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(userLocation, 13f)) Log.d(TAG, "Moved to user's last known location.") } else { // `lastLocation` can be null if the location has never been recorded. // In this case, we fall back to a default location. Log.d(TAG, "Last known location is null. Falling back to Sydney.") moveToSydney() } } .addOnFailureListener { // This listener handles errors in the location fetching process. Log.e(TAG, "Failed to get location.", it) moveToSydney() } } } /** * Moves the map camera to a default, hardcoded location (Sydney). * This serves as a reliable fallback. */ private fun moveToSydney() { val sydney = LatLng(-33.8688, 151.2093) googleMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(sydney, 13f)) Log.d(TAG, "Moved to Sydney") } /** * This is the callback for the `OnPoiClickListener`. It's triggered when a user * taps a POI on the map. */ override fun onPoiClick(poi: PointOfInterest) { val placeId = poi.placeId Log.d(TAG, "Place ID: $placeId") // We save the selected place ID to the ViewModel. This is critical for surviving // configuration changes. If the user rotates the screen now, the `onCreate` // method will be able to restore the place details view. viewModel.selectedPlaceId = placeId showPlaceDetailsFragment(placeId) } /** * This function is the core of the integration. It creates, configures, and displays * the [PlaceDetailsCompactFragment]. * @param placeId The unique identifier for the place to be displayed. */ private fun showPlaceDetailsFragment(placeId: String) { Log.d(TAG, "Showing PlaceDetailsFragment for place ID: $placeId") // We manage the visibility of UI elements to provide feedback to the user. // The wrapper is shown, and a loading indicator is displayed while the data is fetched. binding.placeDetailsWrapper.visibility = View.VISIBLE binding.dismissButton.visibility = View.GONE binding.placeDetailsContainer.visibility = View.GONE binding.loadingIndicatorMain.visibility = View.VISIBLE // The Place Details widget can be displayed vertically or horizontally. // We dynamically choose the orientation based on the device's current configuration. val orientation = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { Orientation.HORIZONTAL } else { Orientation.VERTICAL } // We create a new instance of the fragment using its factory method. // We can specify which content to show, the orientation, and a custom theme. val fragment = PlaceDetailsCompactFragment.newInstance( PlaceDetailsCompactFragment.ALL_CONTENT, // Show all available content. orientation, R.style.CustomizedPlaceDetailsTheme, ).apply { // The PlaceLoadListener provides callbacks for when the place data is successfully // loaded or when an error occurs. This is where we update our UI state. setPlaceLoadListener(object : PlaceLoadListener { override fun onSuccess(place: Place) { Log.d(TAG, "Place loaded: ${place.id}") // Once the data is loaded, we hide the loading indicator and show the fragment. binding.loadingIndicatorMain.visibility = View.GONE binding.placeDetailsContainer.visibility = View.VISIBLE binding.dismissButton.visibility = View.VISIBLE } override fun onFailure(e: Exception) { Log.e(TAG, "Place failed to load", e) // On failure, we hide the UI and notify the user. dismissPlaceDetails() Toast.makeText(this@MainActivity, "Failed to load place details.", Toast.LENGTH_SHORT).show() } }) } // We add the fragment to our layout's container view. // `commitNow()` is used to ensure the fragment is immediately added and available, // which is important because we need to call a method on it right after. supportFragmentManager .beginTransaction() .replace(binding.placeDetailsContainer.id, fragment) .commitNow() // **This is the key step**: After adding the fragment, we call `loadWithPlaceId` // to trigger the data loading process for the selected place. // We use `post` to ensure this runs after the layout has been measured, // which can prevent potential timing issues. binding.root.post { fragment.loadWithPlaceId(placeId) } } /** * Hides the place details view and clears the selected place ID from the ViewModel. */ private fun dismissPlaceDetails() { binding.placeDetailsWrapper.visibility = View.GONE // Clearing the ID in the ViewModel is important so that if the user rotates the // screen after dismissing, the details view doesn't reappear. viewModel.selectedPlaceId = null } override fun onDestroy() { super.onDestroy() // It's a good practice to nullify references to objects that have a lifecycle // tied to the activity, like the GoogleMap object, to prevent potential memory leaks. googleMap = null } }
创建主题
实例化 fragment 时,您可以指定一个主题来替换任何默认样式属性。未被覆盖的任何主题背景属性都将使用默认样式。如果您想支持深色主题,可以在 values-night/colors.xml
中为颜色添加条目。
<style name="CustomizedPlaceDetailsTheme" parent="PlacesMaterialTheme"> <item name="placesColorPrimary">@color/app_primary_color</item> <item name="placesColorOnSurface">@color/app_color_on_surface</item> <item name="placesColorOnSurfaceVariant">@color/app_color_on_surface</item> <item name="placesTextAppearanceBodySmall">@style/app_text_appearence_small</item> <item name="placesCornerRadius">20dp</item> </style>
使用标准内容
此示例使用标准内容。
val fragmentStandardContent = PlaceDetailsCompactFragment.newInstance( PlaceDetailsCompactFragment.STANDARD_CONTENT, orientation, R.style.CustomizedPlaceDetailsTheme )
自定义特定内容
此示例仅选择地址、无障碍入口和媒体 Content
选项以实现紧凑视图,并使用 CustomizedPlaceDetailsTheme
呈现这些选项。
val placeDetailsFragment = PlaceDetailsCompactFragment.newInstance( orientation, listOf( Content.ADDRESS, Content.ACCESSIBLE_ENTRANCE, Content.MEDIA ), R.style.CustomizedPlaceDetailsTheme )
使用所有内容
此示例使用了紧凑视图的所有 Content
选项。
val fragmentAllContent = PlaceDetailsCompactFragment.newInstance( orientation, PlaceDetailsCompactFragment.ALL_CONTENT, R.style.CustomizedPlaceDetailsTheme )