Place Details コンポーネント

Places UI キットの Place Details コンポーネントを使用すると、アプリに場所の詳細を表示する個別の UI コンポーネントを追加できます。

場所の詳細のコンパクト コンポーネント

PlaceDetailsCompactFragment は、最小限のスペースを使用して、選択した場所の詳細をレンダリングします。これは、地図上の場所をハイライト表示する情報ウィンドウ、チャットでの場所の共有などのソーシャル メディア エクスペリエンス、現在地の選択候補として、またはメディア記事内で Google マップ上の場所を参照する場合に役立ちます。PlaceDetailsCompactFragment には、名前、住所、評価、タイプ、料金、バリアフリー アイコン、営業状況、1 枚の写真が表示されます。

プレイス詳細コンポーネントは、単独で使用することも、他の Google Maps Platform API やサービスと組み合わせて使用することもできます。このコンポーネントは、プレイス ID または緯度/経度の座標を受け取り、レンダリングされたプレイス情報を返します。

課金

Place Details UI キットを使用する場合、.loadWithPlaceId() メソッドまたは .loadWithResourceName() メソッドが呼び出されるたびに課金されます。同じ場所を複数回読み込む場合は、リクエストごとに課金されます。

複数回請求されないように、Android ライフサイクル メソッドに .loadWithPlaceId() または .loadWithResourceName() を直接追加しないでください。たとえば、onResume() メソッドで .loadWithPlaceId().loadWithResourceName() を直接呼び出さないでください。

アプリに場所の詳細を追加する

場所の詳細をアプリに追加するには、レイアウトにフラグメントを追加します。フラグメントをインスタンス化すると、ニーズに合わせて、またアプリの外観に合わせて、プレイス詳細情報の外観をカスタマイズできます。

Kotlin と Java の両方で使用できるメソッドは 2 つあります。1 つはプレイス ID を使用してフラグメントを読み込むメソッド(loadWithPlaceId())、もう 1 つはリソース名を使用してフラグメントを読み込むメソッド(loadWithResourceName())です。どちらか一方のメソッドを選択できます。プレイス ID とリソース名の両方を使用する場合は、両方のメソッドを選択できます。

向き(横向きまたは縦向き)、テーマのオーバーライド、コンテンツを指定できます。コンテンツ オプションは、メディア、住所、評価、料金、タイプ、車椅子対応の入り口、現在営業中ステータスです。カスタマイズの詳細をご覧ください。

Kotlin

              
        // Create a new instance of the fragment from the Places SDK.
        val fragment = PlaceDetailsCompactFragment.newInstance(
            PlaceDetailsCompactFragment.ALL_CONTENT,
            orientation,
            R.style.CustomizedPlaceDetailsTheme,
        ).apply {
            // Set a listener to be notified when the place data has been loaded.
            setPlaceLoadListener(object : PlaceLoadListener {
                override fun onSuccess(place: Place) {
                    Log.d(TAG, "Place loaded: ${place.id}")
                    // Hide loader, show the fragment container and the dismiss button
                    binding.loadingIndicator.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)
                    // Hide everything on failure
                    dismissPlaceDetails()
                    Toast.makeText(this@MainActivity, "Failed to load place details.", Toast.LENGTH_SHORT).show()
                }
            })
        }

        // Add the fragment to the container in the layout.
        supportFragmentManager
            .beginTransaction()
            .replace(binding.placeDetailsContainer.id, fragment)
            .commitNow() // Use commitNow to ensure the fragment is immediately available.

        // **This is the key step**: Tell the fragment to load data for the given Place ID.
        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);

Place Details コンポーネントを読み込むコードの全文

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.
 */
class MainViewModel : ViewModel() {
    var selectedPlaceId: String? = null
}

/**
 * Main Activity for the application. This class is responsible for:
 * 1. Displaying a Google Map.
 * 2. Handling location permissions to center the map on the user's location.
 * 3. Handling clicks on Points of Interest (POIs) on the map.
 * 4. Displaying a [PlaceDetailsCompactFragment] to show details of a selected POI.
 */
class MainActivity : AppCompatActivity(), OnMapReadyCallback, GoogleMap.OnPoiClickListener {
    // ViewBinding for safe and easy access to views.
    private lateinit var binding: ActivityMainBinding
    private var googleMap: GoogleMap? = null

    // Client for retrieving the device's last known location.
    private lateinit var fusedLocationClient: FusedLocationProviderClient

    // Modern approach for handling permission requests and their results.
    private lateinit var requestPermissionLauncher: ActivityResultLauncher<Array<String>>

    // ViewModel to store state across configuration changes (like screen rotation).
    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Initialize the permissions launcher. This defines what to do after the user
        // responds to the permission request dialog.
        requestPermissionLauncher =
            registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
                if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true || permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true) {
                    // Permission was granted. Fetch the user's location.
                    Log.d(TAG, "Location permission granted by user.")
                    fetchLastLocation()
                } else {
                    // Permission was denied. Show a message and default to a fallback location.
                    Log.d(TAG, "Location permission denied by user.")
                    Toast.makeText(
                        this,
                        "Location permission denied. Showing default location.",
                        Toast.LENGTH_LONG
                    ).show()
                    moveToSydney()
                }
            }

        // Standard setup for ViewBinding and enabling edge-to-edge display.
        enableEdgeToEdge()
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Set up the dismiss button listener
        binding.dismissButton.setOnClickListener {
            dismissPlaceDetails()
        }

        // --- Crucial: Initialize Places SDK ---
        val apiKey = BuildConfig.PLACES_API_KEY
        if (apiKey.isEmpty() || apiKey == "YOUR_API_KEY") {
            Log.e(TAG, "No api key")
            Toast.makeText(
                this,
                "Add your own API_KEY in local.properties",
                Toast.LENGTH_LONG
            ).show()
            finish()
            return
        }

        // Initialize the SDK with the application context and API key.
        Places.initializeWithNewPlacesApiEnabled(applicationContext, apiKey)

        // Initialize the location client.
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
        // ------------------------------------

        // Obtain the SupportMapFragment and request the map asynchronously.
        val mapFragment =
            supportFragmentManager.findFragmentById(R.id.map_fragment) as SupportMapFragment?
        mapFragment?.getMapAsync(this)

        // After rotation, check if a place was selected. If so, restore the fragment.
        if (viewModel.selectedPlaceId != null) {
            viewModel.selectedPlaceId?.let { placeId ->
                Log.d(TAG, "Restoring PlaceDetailsFragment for place ID: $placeId")
                showPlaceDetailsFragment(placeId)
            }
        }
    }

    /**
     * Callback triggered when the map is ready to be used.
     */
    override fun onMapReady(map: GoogleMap) {
        Log.d(TAG, "Map is ready")
        googleMap = map
        // Set a listener for clicks on Points of Interest.
        googleMap?.setOnPoiClickListener(this)

        // Check for location permissions to determine the initial map position.
        if (isLocationPermissionGranted()) {
            fetchLastLocation()
        } else {
            requestLocationPermissions()
        }
    }

    /**
     * Checks if either fine or coarse location permission has been granted.
     */
    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
    }

    /**
     * Launches 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 and moves the map camera to it.
     * This function should only be called after verifying permissions.
     */
    @SuppressLint("MissingPermission")
    private fun fetchLastLocation() {
        if (isLocationPermissionGranted()) {
            fusedLocationClient.lastLocation
                .addOnSuccessListener { location: Location? ->
                    if (location != null) {
                        // Move camera to user's location if available.
                        val userLocation = LatLng(location.latitude, location.longitude)
                        googleMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(userLocation, 13f))
                        Log.d(TAG, "Moved to user's last known location.")
                    } else {
                        // Fallback to a default location if the last location is null.
                        Log.d(TAG, "Last known location is null. Falling back to Sydney.")
                        moveToSydney()
                    }
                }
                .addOnFailureListener {
                    // Handle errors in fetching location.
                    Log.e(TAG, "Failed to get location.", it)
                    moveToSydney()
                }
        }
    }

    /**
     * A default fallback location for the map camera.
     */
    private fun moveToSydney() {
        val sydney = LatLng(-33.8688, 151.2093)
        googleMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(sydney, 13f))
        Log.d(TAG, "Moved to Sydney")
    }

    /**
     * Callback for when a Point of Interest on the map is clicked.
     */
    override fun onPoiClick(poi: PointOfInterest) {
        val placeId = poi.placeId
        Log.d(TAG, "Place ID: $placeId")

        // Save the selected place ID to the ViewModel to survive rotation.
        viewModel.selectedPlaceId = placeId
        showPlaceDetailsFragment(placeId)
    }

    /**
     * Instantiates 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")

        // Show the wrapper, hide the dismiss button, and show the loading indicator.
        binding.placeDetailsWrapper.visibility = View.VISIBLE
        binding.dismissButton.visibility = View.GONE
        binding.placeDetailsContainer.visibility = View.GONE
        binding.loadingIndicator.visibility = View.VISIBLE

        // Determine the orientation based on the device's current configuration.
        val orientation =
            if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
                Orientation.HORIZONTAL
            } else {
                Orientation.VERTICAL
            }

        
        // Create a new instance of the fragment from the Places SDK.
        val fragment = PlaceDetailsCompactFragment.newInstance(
            PlaceDetailsCompactFragment.ALL_CONTENT,
            orientation,
            R.style.CustomizedPlaceDetailsTheme,
        ).apply {
            // Set a listener to be notified when the place data has been loaded.
            setPlaceLoadListener(object : PlaceLoadListener {
                override fun onSuccess(place: Place) {
                    Log.d(TAG, "Place loaded: ${place.id}")
                    // Hide loader, show the fragment container and the dismiss button
                    binding.loadingIndicator.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)
                    // Hide everything on failure
                    dismissPlaceDetails()
                    Toast.makeText(this@MainActivity, "Failed to load place details.", Toast.LENGTH_SHORT).show()
                }
            })
        }

        // Add the fragment to the container in the layout.
        supportFragmentManager
            .beginTransaction()
            .replace(binding.placeDetailsContainer.id, fragment)
            .commitNow() // Use commitNow to ensure the fragment is immediately available.

        // **This is the key step**: Tell the fragment to load data for the given Place ID.
        binding.root.post {
            fragment.loadWithPlaceId(placeId)
        }
    }


    private fun dismissPlaceDetails() {
        binding.placeDetailsWrapper.visibility = View.GONE
        viewModel.selectedPlaceId = null
    }

    override fun onDestroy() {
        super.onDestroy()
        // Clear references to avoid memory leaks.
        googleMap = null
    }
}
        
ヒント: GitHub で完全なコードサンプルにアクセスできます。

場所の詳細をカスタマイズする

Places UI キットは、マテリアル デザインをベースにしたビジュアル カスタマイズにデザインシステム アプローチを提供します(Google マップ固有の変更がいくつかあります)。タイポグラフィについては、マテリアル デザインのリファレンスをご覧ください。デフォルトでは、このスタイルは Google マップのビジュアル デザイン言語に準拠しています。

店舗情報のカスタマイズ オプション

フラグメントをインスタンス化するときに、デフォルトのスタイル属性をオーバーライドするテーマを指定できます。オーバーライドされていないテーマ属性は、デフォルト スタイルを使用します。ダークモードをサポートする場合は、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 コンテナとダイアログの背景
placesColorOnSurface 見出し、ダイアログのコンテンツ
placesColorOnSurfaceVariant お店/スポット情報
placesColorPrimary リンク
placesColorOutlineDecorative コンテナの境界線
placesColorSecondaryContainer ボタンの背景
placesColorOnSecondaryContainer ボタンのテキストとアイコン
placesColorPositive 「営業中」ラベルを配置
placesColorNegative 場所に「閉鎖済み」のラベルを追加
placesColorInfo 入口がバリアフリーのアイコン
   
タイポグラフィ
placesTextAppearanceHeadlineMedium ダイアログの見出し
placesTextAppearanceTitleSmall 場所の名前
placesTextAppearanceBodyMedium ダイアログのコンテンツ
placesTextAppearanceBodySmall お店/スポット情報
placesTextAppearanceLabelLarge ボタンのラベル
   
placesCornerRadius コンテナの角
   
Google マップのブランド アトリビューション
placesColorAttributionLight 明るいテーマの Google マップのアトリビューションと開示ボタン(白、グレー、黒の列挙型)
placesColorAttributionDark ダークモードの Google マップのアトリビューションと開示ボタン(白、グレー、黒の列挙型)

幅と高さ

縦向きの場合は、180 dp ~ 300 dp の幅が推奨されます。横向きの場合、推奨される幅は 180 dp ~ 500 dp です。160 dp 未満のビューは正しく表示されないことがあります。

高さを設定しないことをおすすめします。これにより、ウィンドウ内のコンテンツの高さを設定して、すべての情報を表示できるようになります。

アトリビューションの色

Google マップの利用規約では、Google マップの帰属表示に 3 つのブランドカラーのいずれかを使用することが義務付けられています。この帰属情報は、カスタマイズが変更されたときに表示され、アクセス可能である必要があります。

明るいテーマと暗いテーマで個別に設定できる 3 つのブランドカラーから選択できます。

  • ライトモード: placesColorAttributionLight(白、グレー、黒の列挙型を使用)。
  • ダークモード: placesColorAttributionDark(白、グレー、黒の列挙型)。

カスタマイズの例

このサンプルでは、標準コンテンツをカスタマイズします。

  val fragmentStandardContent = PlaceDetailsCompactFragment.newInstance(
    PlaceDetailsCompactFragment.STANDARD_CONTENT,
    orientation,
    R.style.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
  )