คอมโพเนนต์รายละเอียดสถานที่

คอมโพเนนต์รายละเอียดสถานที่ของ UI Kit ของสถานที่ช่วยให้คุณเพิ่มคอมโพเนนต์ UI แต่ละรายการที่แสดงรายละเอียดสถานที่ในแอปได้

คอมโพเนนต์รายละเอียดสถานที่แบบกะทัดรัด

PlaceDetailsCompactFragment จะแสดงรายละเอียดของสถานที่ที่เลือกโดยใช้พื้นที่น้อยที่สุด ซึ่งอาจมีประโยชน์ในหน้าต่างข้อมูลไฮไลต์สถานที่บนแผนที่ ในประสบการณ์การใช้งานโซเชียลมีเดีย เช่น การแชร์ตำแหน่งในแชท คำแนะนำในการเลือกตำแหน่งปัจจุบัน หรือภายในบทความสื่อเพื่ออ้างอิงสถานที่ใน Google Maps PlaceDetailsCompactFragment สามารถแสดงชื่อ ที่อยู่ คะแนน ประเภท ราคา ไอคอนการช่วยเหลือพิเศษ สถานะ "เปิด" และรูปภาพ 1 รูป

คอมโพเนนต์รายละเอียดสถานที่สามารถใช้แยกต่างหากหรือร่วมกับ API และบริการอื่นๆ ของ Google Maps Platform ก็ได้ คอมโพเนนต์จะใช้รหัสสถานที่หรือพิกัดละติจูด/ลองจิจูด แล้วแสดงผลข้อมูลรายละเอียดสถานที่ที่ผ่านการจัดการแสดงผล

การเรียกเก็บเงิน

เมื่อใช้ชุดเครื่องมือ UI ของรายละเอียดสถานที่ ระบบจะเรียกเก็บเงินจากคุณทุกครั้งที่มีการเรียกใช้เมธอด .loadWithPlaceId() หรือ .loadWithResourceName() หากคุณโหลดสถานที่เดียวกันหลายครั้ง ระบบจะเรียกเก็บเงินสำหรับคำขอแต่ละรายการ

อย่าเพิ่ม .loadWithPlaceId() หรือ .loadWithResourceName() ในเมธอดวงจรชีวิตของ Android โดยตรงเพื่อหลีกเลี่ยงการเรียกเก็บเงินหลายครั้ง เช่น อย่าเรียก .loadWithPlaceId() หรือ .loadWithResourceName() ในเมธอด onResume() โดยตรง

เพิ่มรายละเอียดสถานที่ลงในแอป

คุณสามารถเพิ่มรายละเอียดสถานที่ลงในแอปได้โดยเพิ่มข้อมูลโค้ดไปยังเลย์เอาต์ เมื่อสร้างอินสแตนซ์ของข้อมูลโค้ดที่ฝัง คุณจะปรับแต่งรูปลักษณ์ของข้อมูลรายละเอียดสถานที่ให้เหมาะกับความต้องการและเข้ากับลักษณะที่ปรากฏของแอปได้

คุณมี 2 วิธีที่ใช้ได้ทั้งใน Kotlin และ Java วิธีหนึ่งจะโหลดข้อมูลโค้ดที่ตัดตอนมาโดยใช้รหัสสถานที่ (loadWithPlaceId()) และอีกวิธีจะโหลดข้อมูลโค้ดที่ตัดตอนมาโดยใช้ชื่อทรัพยากร (loadWithResourceName()) คุณสามารถเลือกวิธีใดวิธีหนึ่งหรือทั้ง 2 วิธีก็ได้หากใช้ทั้งรหัสสถานที่และชื่อทรัพยากร

คุณสามารถระบุการวางแนว (แนวนอนหรือแนวตั้ง) การลบล้างธีม และเนื้อหา ตัวเลือกเนื้อหา ได้แก่ สื่อ ที่อยู่ คะแนน ราคา ประเภท ทางเข้าที่เข้าถึงได้ และสถานะ "เปิดอยู่" ดูข้อมูลเพิ่มเติมเกี่ยวกับการปรับแต่ง

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);

ดูโค้ดที่สมบูรณ์เพื่อโหลดคอมโพเนนต์รายละเอียดสถานที่

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

ปรับแต่งรายละเอียดสถานที่

ชุด UI ของ Places นำเสนอแนวทางระบบการออกแบบสำหรับการปรับแต่งภาพโดยอิงตาม Material Design โดยคร่าวๆ (มีการแก้ไขบางอย่างสำหรับ Google Maps โดยเฉพาะ) ดูข้อมูลอ้างอิงเกี่ยวกับสีและการพิมพ์ของ Material Design โดยค่าเริ่มต้น สไตล์จะเป็นไปตามภาษาการออกแบบภาพของ Google Maps

ตัวเลือกการปรับแต่งรายละเอียดสถานที่

เมื่อสร้างอินสแตนซ์ของข้อมูลโค้ด คุณสามารถระบุธีมที่ลบล้างแอตทริบิวต์สไตล์เริ่มต้นได้ แอตทริบิวต์ธีมที่ไม่ได้ลบล้างจะใช้รูปแบบเริ่มต้น หากต้องการรองรับธีมมืด คุณสามารถเพิ่มรายการสำหรับสีใน 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 Maps
placesColorAttributionLight ปุ่มการระบุแหล่งที่มาและการเปิดเผยข้อมูลของ Google Maps ในธีมสว่าง (ชุดค่าผสมสำหรับสีขาว เทา และสีดํา)
placesColorAttributionDark ปุ่มการระบุแหล่งที่มาและการเปิดเผยข้อมูลของ Google Maps ในธีมมืด (ลิสต์สำหรับสีขาว สีเทา และสีดํา)

ความกว้างและความสูง

สำหรับมุมมองแนวตั้ง ความกว้างที่แนะนำคือระหว่าง 180dp ถึง 300dp สำหรับมุมมองแนวนอน ความกว้างที่แนะนำคือระหว่าง 180dp ถึง 500dp มุมมองที่เล็กกว่า 160dp อาจแสดงผลไม่ถูกต้อง

แนวทางปฏิบัติแนะนำคืออย่าตั้งค่าความสูง ซึ่งจะช่วยให้เนื้อหาในหน้าต่างกำหนดความสูงได้เพื่อให้ข้อมูลทั้งหมดแสดง

สีการระบุแหล่งที่มา

ข้อกำหนดในการให้บริการของ Google Maps กำหนดให้คุณใช้สีของแบรนด์ 1 ใน 3 สีสำหรับการระบุแหล่งที่มาของ Google Maps การระบุแหล่งที่มานี้ต้องมองเห็นได้และเข้าถึงได้เมื่อมีการเปลี่ยนแปลงการปรับแต่ง

เรามีสีของแบรนด์ให้เลือก 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
  )