عند ملء عنوان تسليم أو معلومات فوترة أو معلومات حدث، يساعد تفعيل النماذج باستخدام ميزة الملء التلقائي للأماكن المستخدمين في تقليل عدد نقرات المفاتيح والأخطاء عند إدخال معلومات العنوان. يشرح هذا الدليل التعليمي الخطوات اللازمة لتفعيل حقل إدخال باستخدام ميزة "الإكمال التلقائي للأماكن" وملء حقول نموذج العنوان بمكونات العنوان من العنوان الذي يختاره المستخدم، وعرض العنوان المحدّد على خريطة للمساعدة في التأكيد البصري.
فيديوهات: تحسين نماذج العناوين باستخدام ميزة "الإكمال التلقائي للأماكن"
نماذج العناوين
Android
iOS
الويب
توفّر Google Maps Platform أداة "إكمال تلقائي للأماكن" لمنصّات الويب والأجهزة المتوافقة مع اللمس. يوفّر التطبيق المصغّر، المعروض في الصور السابقة، مربع حوار بحث يتضمّن وظيفة إكمال تلقائي مضمّنة يمكنك تحسينها للبحث على مستوى الموقع الجغرافي.
الحصول على الشفرة
استنسِخ أو نزِّل مستودع "حِزم تطوير برامج Google Places لنظام التشغيل Android" من GitHub.
عرض إصدار Java للنشاط:
/* * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.placesdemo; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.ViewStub; import android.widget.Button; import android.widget.CheckBox; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import com.example.placesdemo.databinding.AutocompleteAddressActivityBinding; 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.GoogleMapOptions; 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.MapStyleOptions; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.android.libraries.places.api.Places; import com.google.android.libraries.places.api.model.AddressComponent; import com.google.android.libraries.places.api.model.AddressComponents; import com.google.android.libraries.places.api.model.Place; import com.google.android.libraries.places.api.model.TypeFilter; import com.google.android.libraries.places.api.net.PlacesClient; import com.google.android.libraries.places.widget.Autocomplete; import com.google.android.libraries.places.widget.model.AutocompleteActivityMode; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static android.Manifest.permission.ACCESS_FINE_LOCATION; import static com.google.maps.android.SphericalUtil.computeDistanceBetween; /** * Activity for using Place Autocomplete to assist filling out an address form. */ @SuppressWarnings("FieldCanBeLocal") public class AutocompleteAddressActivity extends AppCompatActivity implements OnMapReadyCallback { private static final String TAG = "ADDRESS_AUTOCOMPLETE"; private static final String MAP_FRAGMENT_TAG = "MAP"; private LatLng coordinates; private boolean checkProximity = false; private SupportMapFragment mapFragment; private GoogleMap map; private Marker marker; private PlacesClient placesClient; private View mapPanel; private LatLng deviceLocation; private static final double acceptedProximity = 150; private AutocompleteAddressActivityBinding binding; View.OnClickListener startAutocompleteIntentListener = view -> { view.setOnClickListener(null); startAutocompleteIntent(); }; private final ActivityResultLauncher<Intent> startAutocomplete = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == Activity.RESULT_OK) { Intent intent = result.getData(); if (intent != null) { Place place = Autocomplete.getPlaceFromIntent(intent); // Write a method to read the address components from the Place // and populate the form with the address components Log.d(TAG, "Place: " + place.getAddressComponents()); fillInAddress(place); } } else if (result.getResultCode() == Activity.RESULT_CANCELED) { // The user canceled the operation. Log.i(TAG, "User canceled autocomplete"); } }); @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent intent) { super.onActivityResult(requestCode, resultCode, intent); binding.autocompleteAddress1.setOnClickListener(startAutocompleteIntentListener); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = AutocompleteAddressActivityBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); // Retrieve a PlacesClient (previously initialized - see MainActivity) placesClient = Places.createClient(this); // Attach an Autocomplete intent to the Address 1 EditText field binding.autocompleteAddress1.setOnClickListener(startAutocompleteIntentListener); // Update checkProximity when user checks the checkbox CheckBox checkProximityBox = findViewById(R.id.checkbox_proximity); checkProximityBox.setOnCheckedChangeListener((view, isChecked) -> { // Set the boolean to match user preference for when the Submit button is clicked checkProximity = isChecked; }); // Submit and optionally check proximity Button saveButton = findViewById(R.id.autocomplete_save_button); saveButton.setOnClickListener(v -> saveForm()); // Reset the form Button resetButton = findViewById(R.id.autocomplete_reset_button); resetButton.setOnClickListener(v -> clearForm()); } private void startAutocompleteIntent() { // Set the fields to specify which types of place data to // return after the user has made a selection. List<Place.Field> fields = Arrays.asList(Place.Field.ADDRESS_COMPONENTS, Place.Field.LAT_LNG, Place.Field.VIEWPORT); // Build the autocomplete intent with field, country, and type filters applied Intent intent = new Autocomplete.IntentBuilder(AutocompleteActivityMode.OVERLAY, fields) .setCountries(Arrays.asList("US")) .setTypesFilter(new ArrayList<String>() {{ add(TypeFilter.ADDRESS.toString().toLowerCase()); }}) .build(this); startAutocomplete.launch(intent); } @Override public void onMapReady(@NonNull GoogleMap googleMap) { map = googleMap; try { // Customise the styling of the base map using a JSON object defined // in a string resource. boolean success = map.setMapStyle( MapStyleOptions.loadRawResourceStyle(this, R.raw.style_json)); if (!success) { Log.e(TAG, "Style parsing failed."); } } catch (Resources.NotFoundException e) { Log.e(TAG, "Can't find style. Error: ", e); } map.moveCamera(CameraUpdateFactory.newLatLngZoom(coordinates, 15f)); marker = map.addMarker(new MarkerOptions().position(coordinates)); } private void fillInAddress(Place place) { AddressComponents components = place.getAddressComponents(); StringBuilder address1 = new StringBuilder(); StringBuilder postcode = new StringBuilder(); // Get each component of the address from the place details, // and then fill-in the corresponding field on the form. // Possible AddressComponent types are documented at https://goo.gle/32SJPM1 if (components != null) { for (AddressComponent component : components.asList()) { String type = component.getTypes().get(0); switch (type) { case "street_number": { address1.insert(0, component.getName()); break; } case "route": { address1.append(" "); address1.append(component.getShortName()); break; } case "postal_code": { postcode.insert(0, component.getName()); break; } case "postal_code_suffix": { postcode.append("-").append(component.getName()); break; } case "locality": binding.autocompleteCity.setText(component.getName()); break; case "administrative_area_level_1": { binding.autocompleteState.setText(component.getShortName()); break; } case "country": binding.autocompleteCountry.setText(component.getName()); break; } } } binding.autocompleteAddress1.setText(address1.toString()); binding.autocompletePostal.setText(postcode.toString()); // After filling the form with address components from the Autocomplete // prediction, set cursor focus on the second address line to encourage // entry of sub-premise information such as apartment, unit, or floor number. binding.autocompleteAddress2.requestFocus(); // Add a map for visual confirmation of the address showMap(place); } private void showMap(Place place) { coordinates = place.getLatLng(); // It isn't possible to set a fragment's id programmatically so we set a tag instead and // search for it using that. mapFragment = (SupportMapFragment) getSupportFragmentManager().findFragmentByTag(MAP_FRAGMENT_TAG); // We only create a fragment if it doesn't already exist. if (mapFragment == null) { mapPanel = ((ViewStub) findViewById(R.id.stub_map)).inflate(); GoogleMapOptions mapOptions = new GoogleMapOptions(); mapOptions.mapToolbarEnabled(false); // To programmatically add the map, we first create a SupportMapFragment. mapFragment = SupportMapFragment.newInstance(mapOptions); // Then we add it using a FragmentTransaction. getSupportFragmentManager() .beginTransaction() .add(R.id.confirmation_map, mapFragment, MAP_FRAGMENT_TAG) .commit(); mapFragment.getMapAsync(this); } else { updateMap(coordinates); } } private void updateMap(LatLng latLng) { marker.setPosition(latLng); map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f)); if (mapPanel.getVisibility() == View.GONE) { mapPanel.setVisibility(View.VISIBLE); } } private void saveForm() { Log.d(TAG, "checkProximity = " + checkProximity); if (checkProximity) { checkLocationPermissions(); } else { Toast.makeText( this, R.string.autocomplete_skipped_message, Toast.LENGTH_SHORT) .show(); } } private void clearForm() { binding.autocompleteAddress1.setText(""); binding.autocompleteAddress2.getText().clear(); binding.autocompleteCity.getText().clear(); binding.autocompleteState.getText().clear(); binding.autocompletePostal.getText().clear(); binding.autocompleteCountry.getText().clear(); if (mapPanel != null) { mapPanel.setVisibility(View.GONE); } binding.autocompleteAddress1.requestFocus(); } // Register the permissions callback, which handles the user's response to the // system permissions dialog. Save the return value, an instance of // ActivityResultLauncher, as an instance variable. private final ActivityResultLauncher<String> requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { if (isGranted) { // Since ACCESS_FINE_LOCATION is the only permission in this sample, // run the location comparison task once permission is granted. // Otherwise, check which permission is granted. getAndCompareLocations(); } else { // Fallback behavior if user denies permission Log.d(TAG, "User denied permission"); } }); private void checkLocationPermissions() { if (ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { getAndCompareLocations(); } else { requestPermissionLauncher.launch( ACCESS_FINE_LOCATION); } } @SuppressLint("MissingPermission") private void getAndCompareLocations() { // TODO: Detect and handle if user has entered or modified the address manually and update // the coordinates variable to the Lat/Lng of the manually entered address. May use // Geocoding API to convert the manually entered address to a Lat/Lng. LatLng enteredLocation = coordinates; map.setMyLocationEnabled(true); FusedLocationProviderClient fusedLocationClient = LocationServices.getFusedLocationProviderClient(this); fusedLocationClient.getLastLocation() .addOnSuccessListener(this, location -> { // Got last known location. In some rare situations this can be null. if (location == null) { return; } deviceLocation = new LatLng(location.getLatitude(), location.getLongitude()); Log.d(TAG, "device location = " + deviceLocation); Log.d(TAG, "entered location = " + enteredLocation.toString()); // Use the computeDistanceBetween function in the Maps SDK for Android Utility Library // to use spherical geometry to compute the distance between two Lat/Lng points. double distanceInMeters = computeDistanceBetween(deviceLocation, enteredLocation); if (distanceInMeters <= acceptedProximity) { Log.d(TAG, "location matched"); // TODO: Display UI based on the locations matching } else { Log.d(TAG, "location not matched"); // TODO: Display UI based on the locations not matching } }); } }
تفعيل واجهات برمجة التطبيقات
لتنفيذ هذه الاقتراحات، عليك تفعيل واجهات برمجة التطبيقات التالية في Google Cloud Console:
- حزمة تطوير البرامج بالاستناد إلى بيانات "خرائط Google" لتطبيقات Android (أو واجهة برمجة التطبيقات للنظام الأساسي الذي تختاره)
- Places API
لمزيد من المعلومات عن الإعداد، يُرجى الاطّلاع على إعداد مشروعك على Google Cloud.
إضافة ميزة الإكمال التلقائي إلى حقول الإدخال
يصف هذا القسم كيفية إضافة ميزة "الإكمال التلقائي للأماكن" إلى نموذج عنوان.
إضافة التطبيق المصغّر "الإكمال التلقائي للأماكن"
في Android، يمكنك إضافة التطبيق المصغّر لإكمال البيانات تلقائيًا باستخدام Intent لإكمال البيانات تلقائيًا الذي يطلق ميزة "إكمال بيانات الأماكن تلقائيًا" من حقل إدخال "سطر العنوان 1"، حيث سيبدأ المستخدم في إدخال عنوانه. عندما يبدأ المستخدمون الكتابة، سيصبح بإمكانهم اختيار عنوانهم من قائمة التوقّعات في ميزة "الإكمال التلقائي".
أولاً، حضِّر مشغّل أنشطة باستخدام ActivityResultLauncher
، والذي يستمع إلى نتيجة
من النشاط الذي تم تشغيله. سيحتوي ردّ الاتصال بالنتيجة على عنصر مكان
يتوافق مع العنوان الذي يختاره المستخدم من اقتراحات
الإكمال التلقائي.
private final ActivityResultLauncher<Intent> startAutocomplete = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == Activity.RESULT_OK) { Intent intent = result.getData(); if (intent != null) { Place place = Autocomplete.getPlaceFromIntent(intent); // Write a method to read the address components from the Place // and populate the form with the address components Log.d(TAG, "Place: " + place.getAddressComponents()); fillInAddress(place); } } else if (result.getResultCode() == Activity.RESULT_CANCELED) { // The user canceled the operation. Log.i(TAG, "User canceled autocomplete"); } });
بعد ذلك، حدِّد سمات الحقول والموقع الجغرافي والنوع لهدف
إكمال الأماكن تلقائيًا وأنشئ هذا الهدف باستخدام
Autocomplete.IntentBuilder
.
أخيرًا، يمكنك إطلاق النية باستخدام ActivityResultLauncher
المحدّد في
نموذج الرمز السابق.
private void startAutocompleteIntent() { // Set the fields to specify which types of place data to // return after the user has made a selection. List<Place.Field> fields = Arrays.asList(Place.Field.ADDRESS_COMPONENTS, Place.Field.LAT_LNG, Place.Field.VIEWPORT); // Build the autocomplete intent with field, country, and type filters applied Intent intent = new Autocomplete.IntentBuilder(AutocompleteActivityMode.OVERLAY, fields) .setCountries(Arrays.asList("US")) .setTypesFilter(new ArrayList<String>() {{ add(TypeFilter.ADDRESS.toString().toLowerCase()); }}) .build(this); startAutocomplete.launch(intent); }
التعامل مع العنوان الذي يعرضه ميزة "الإكمال التلقائي للأماكن"
من خلال تحديد ActivityResultLauncher
في وقت سابق، تم أيضًا تحديد الإجراء الذي يجب
اتّخاذه عند عرض نتيجة النشاط في دالة الاستدعاء. إذا اختَر المستخدِم
توقّعًا، سيتم عرضه في النيّة الواردة في
عنصر النتيجة. بما أنّه تم إنشاء النية بواسطة Autocomplete.IntentBuilder
،
يمكن للطريقة Autocomplete.getPlaceFromIntent()
استخراج عنصر "الموقع الجغرافي"
منها.
private final ActivityResultLauncher<Intent> startAutocomplete = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == Activity.RESULT_OK) { Intent intent = result.getData(); if (intent != null) { Place place = Autocomplete.getPlaceFromIntent(intent); // Write a method to read the address components from the Place // and populate the form with the address components Log.d(TAG, "Place: " + place.getAddressComponents()); fillInAddress(place); } } else if (result.getResultCode() == Activity.RESULT_CANCELED) { // The user canceled the operation. Log.i(TAG, "User canceled autocomplete"); } });
من هناك، يمكنك الاتصال بالرقم Place.getAddressComponents()
ومطابقة كل عنصر
في العنوان مع حقل الإدخال المقابل له في نموذج العنوان، وملء حقل
بالقيمة من المكان الذي اختاره المستخدم.
يتمّ مشاركة مثال على تنفيذ تعبئة حقول نموذج العنوان في طريقة fillInAddress
من نموذج الرمز البرمجي المقدّم ضمن قسم الحصول على الرمز في هذه الصفحة.
إنّ تسجيل بيانات العنوان من التوقّع بدلاً من إدخال عنوان يدوياً يساعد في ضمان دقة العنوان، ويضمن أنّ العنوان معروف ويمكن تسليم الرسالة إليه، ويقلل من عدد نقرات المستخدم على لوحة المفاتيح.
الاعتبارات الواجب مراعاتها عند تنفيذ ميزة "الإكمال التلقائي للأماكن"
تتوفّر ميزة "إكمال العنوان تلقائيًا" في عدد من الخيارات التي تتيح لها المرونة في تنفيذها إذا كنت تريد استخدام أكثر من التطبيق المصغّر فقط. يمكنك استخدام مجموعة من الخدمات للحصول على ما تحتاجه بالضبط لمطابقة موقع جغرافي بالطريقة الصحيحة.
بالنسبة إلى نموذج ADDRESS، اضبط المَعلمة types على
address
لتقييد المطابقات بعناوين الشوارع الكاملة. اطّلِع على مزيد من المعلومات حول الأنواع المتوافقة في طلبات "الإكمال التلقائي للأماكن".اضبط القيود والانحيازات المناسبة إذا لم تكن بحاجة إلى البحث على مستوى العالم. هناك عدد من المَعلمات التي يمكن استخدامها لتوجيه أي مطابقة إلى مناطق معيّنة فقط أو حصرها بها.
استخدِم
RectangularBounds
لضبط الحدود المستطيلة لتقييد منطقة معيّنة، واستخدِمsetLocationRestriction()
للتأكّد من عرض العناوين في هذه المناطق فقط.استخدِم
setCountries()
لتقييد الردود على مجموعة معيّنة من البلدان.
اترك الحقول قابلة للتعديل في حال عدم تطابق حقول معيّنة، واسمح للعملاء بتعديل العنوان إذا لزم الأمر. بما أنّ معظم العناوين التي تعرِضها ميزة "الإكمال التلقائي للأماكن" لا تحتوي على أرقام المواقع الفرعية، مثل أرقام الشقة أو الجناح أو الوحدة، يمكنك نقل التركيز إلى "سطر العنوان 2" لتحفيز المستخدم على ملء هذه المعلومات إذا لزم الأمر.
تقديم تأكيد مرئي للعنوان
كجزء من إدخال العنوان، امنح المستخدمين تأكيدًا مرئيًا على العنوان على الخريطة. ويمنح ذلك المستخدمين تأكيدًا إضافيًا على أنّ العنوان صحيح.
يعرض الشكل التالي خريطة أسفل العنوان مع دبوس في العنوان الذي تم إدخاله.
يتّبع المثال التالي الخطوات الأساسية لإضافة خريطة في Android. يُرجى الرجوع إلى المستندات للحصول على مزيد من التفاصيل.
- إضافة
SupportMapFragment
(في هذه الحالة، إضافة مقتطف ديناميكيًا) - الحصول على معرّف للجزء وتسجيل دالة الاستدعاء
- تصميم علامة وإضافة علامة إلى الخريطة
- إيقاف عناصر التحكّم في الخريطة
إضافة SupportMapFragment
أولاً، أضِف مقتطفًا SupportMapFragment
إلى
ملف XML الخاص بالتنسيق.
<fragment android:name="com.google.android.gms.maps.SupportMapFragment" android:id="@+id/confirmation_map" android:layout_width="match_parent" android:layout_height="match_parent"/>
بعد ذلك، أضِف المقتطف آليًا إذا لم يكن متوفّرًا بعد.
private void showMap(Place place) { coordinates = place.getLatLng(); // It isn't possible to set a fragment's id programmatically so we set a tag instead and // search for it using that. mapFragment = (SupportMapFragment) getSupportFragmentManager().findFragmentByTag(MAP_FRAGMENT_TAG); // We only create a fragment if it doesn't already exist. if (mapFragment == null) { mapPanel = ((ViewStub) findViewById(R.id.stub_map)).inflate(); GoogleMapOptions mapOptions = new GoogleMapOptions(); mapOptions.mapToolbarEnabled(false); // To programmatically add the map, we first create a SupportMapFragment. mapFragment = SupportMapFragment.newInstance(mapOptions); // Then we add it using a FragmentTransaction. getSupportFragmentManager() .beginTransaction() .add(R.id.confirmation_map, mapFragment, MAP_FRAGMENT_TAG) .commit(); mapFragment.getMapAsync(this); } else { updateMap(coordinates); } }
الحصول على معرّف للجزء وتسجيل دالة الاستدعاء
للحصول على معرّف للعنصر، يمكنك استدعاء الأسلوب
FragmentManager.findFragmentById
ونقله رقم تعريف المورد للعنصر في ملف التنسيق. إذا أضفت المقتطف ديناميكيًا، يمكنك تخطّي هذه الخطوة لأنّك سبق أن استرددت الاسم المعرِّف.استخدِم الطريقة
getMapAsync
لضبط callback على المقتطف.
على سبيل المثال، إذا أضفت المقتطف بشكل ثابت:
Kotlin
val mapFragment = supportFragmentManager .findFragmentById(R.id.map) as SupportMapFragment mapFragment.getMapAsync(this)
Java
SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager() .findFragmentById(R.id.map); mapFragment.getMapAsync(this);
تصميم علامة وإضافة علامة إلى الخريطة
عندما تصبح الخريطة جاهزة، اضبط النمط واضبط الكاميرا في منتصف الشاشة وأضِف علامة عند إحداثيات العنوان الذي أدخلته. تستخدِم التعليمة البرمجية التالية تنسيقًا محددًا في عنصر JSON، أو يمكنك بدلاً من ذلك تحميل معرّف خريطة تم تحديده باستخدام تصميم الخرائط المستند إلى السحابة الإلكترونية.
@Override public void onMapReady(@NonNull GoogleMap googleMap) { map = googleMap; try { // Customise the styling of the base map using a JSON object defined // in a string resource. boolean success = map.setMapStyle( MapStyleOptions.loadRawResourceStyle(this, R.raw.style_json)); if (!success) { Log.e(TAG, "Style parsing failed."); } } catch (Resources.NotFoundException e) { Log.e(TAG, "Can't find style. Error: ", e); } map.moveCamera(CameraUpdateFactory.newLatLngZoom(coordinates, 15f)); marker = map.addMarker(new MarkerOptions().position(coordinates)); }
(الاطّلاع على نموذج الرمز البرمجي الكامل)
إيقاف عناصر التحكّم في الخريطة
للحفاظ على بساطة الخريطة من خلال عرض الموقع الجغرافي بدون عناصر تحكّم إضافية في الخريطة (مثل البوصلة أو شريط الأدوات أو ميزات مدمجة أخرى)، ننصحك بإيقاف عناصر التحكّم التي لا تجدها ضرورية. على أجهزة Android، يمكنك تفعيل الوضع البسيط لتوفير تفاعل محدود.