65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Android* 商业应用程序中实现地图和地理围栏功能

2014年7月22日

CPOL

6分钟阅读

viewsIcon

22768

本案例研究讨论了如何在 Android* 商务应用中构建地图和地理定位功能,包括在 Google Maps* 上叠加商店位置,以及使用地理围栏在设备进入商店附近时通知用户。

摘要

本案例研究讨论了如何在 Android* 商务应用中构建地图和地理定位功能,包括在 Google Maps* 上叠加商店位置,以及使用地理围栏在设备进入商店附近时通知用户。

目录

  1. 摘要
  2. 概述
  3. 在 Google Maps 上显示商店位置
    1. Google Maps Android API v2
    2. 在应用程序清单中指定应用设置
    3. 添加地图 Fragment
  4. 发送地理围栏通知
    1. 注册和注销地理围栏
    2. 实现位置服务回调
    3. 实现 IntentService
  5. 摘要
  6. 参考文献
  7. 关于作者

概述

在本案例研究中,我们将把地图和地理定位功能集成到一款面向 Android 平板电脑的餐厅商务应用中(图 1)。用户可以从主菜单项“Locations and Geofences”(图 2)访问地理定位功能。

图 1 餐厅应用主屏幕

图 2 弹出菜单项

在 Google Maps 上显示商店位置

对于商务应用而言,在地图上显示商店位置非常直观且对用户很有帮助(图 3)。Google Maps Android API 提供了一种将 Google Maps 集成到您的 Android 应用中的便捷方法。

Google Maps Android API v2

Google Maps Android API v2 是 Google Play Services APK 的一部分。要创建使用 Google Maps Android API v2 的 Android 应用,需要通过下载和配置 Google Play Services SDK、获取 API 密钥以及在应用的 AndroidManifest.xml 文件中添加必要的设置来设置开发环境。

首先,您需要按照 https://developer.android.com.cn/google/play-services/setup.html 设置 Google Play Services SDK。

然后,您需要从 Google Developers Console https://console.developers.google.com/project 注册您的项目并获取 API 密钥。您需要在 AndroidManifest.xml 文件中添加 API 密钥。

图 3 餐厅应用在 Google Maps 上显示商店位置。

在应用程序清单中指定应用设置

要使用 Google Maps Android API v2,需要在 <manifest> 元素下指定一些权限和功能(代码示例 1)。这些包括网络连接、外部存储以及位置访问的必要权限。Google Maps Android API 还需要 OpenGL ES 版本 2 功能。

	<uses-permission android:name="android.permission.INTERNET"/>
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
	<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
	<!-- The following two permissions are not required to use
	     Google Maps Android API v2, but are recommended. -->
	<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
	<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
	<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />

      <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true"/>
代码示例 1. 使用 Google Maps Android API 的应用的建议权限。仅当您使用模拟位置测试应用时,才包含“ACCESS_MOCK_LOCATION”权限。

同样,在 <application> 元素下,我们在 <meta-data> 元素中指定 Google Play Services 版本和获取的 API 密钥(代码示例 2)。

      <meta-data
              android:name="com.google.android.gms.version"
              android:value="@integer/google_play_services_version" />
        
      <meta-data
	        android:name="com.google.android.maps.v2.API_KEY"
		  android:value="copy your API Key here"/>
代码示例 2. 指定 Google Play Services 版本和 API 密钥。

添加地图 Fragment

首先,在您的 activity 布局 xml 文件中,添加一个 MapFragment 元素(代码示例 3)。

    <fragment
        android:id="@+id/storelocationmap"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:name="com.google.android.gms.maps.MapFragment"
    />
代码示例 3. 在 Activity 布局中添加 MapFragment。

在您的 activity 类中,您可以检索 Google Maps MapFragment 对象并在每个商店位置绘制商店图标。

…
private static final LatLng CHANDLER = new LatLng(33.455,-112.0668);
…
private static final StoreLocation[] ALLRESTURANTLOCATIONS = new StoreLocation[] {
        new StoreLocation(new LatLng(33.455,-112.0668), new String("Phoenix, AZ")),
        new StoreLocation(new LatLng(33.5123,-111.9336), new String("SCOTTSDALE, AZ")),
        new StoreLocation(new LatLng(33.3333,-111.8335), new String("Chandler, AZ")),
        new StoreLocation(new LatLng(33.4296,-111.9436), new String("Tempe, AZ")),
        new StoreLocation(new LatLng(33.4152,-111.8315), new String("Mesa, AZ")),
        new StoreLocation(new LatLng(33.3525,-111.7896), new String("Gilbert, AZ"))
};
…    
      @Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.geolocation_view);
		
		mMap = ((MapFragment) getFragmentManager().findFragmentById(R.id.storelocationmap)).getMap();
		mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(CHANDLER, ZOOM_LEVEL));
		Drawable iconDrawable = getResources().getDrawable(R.drawable.ic_launcher);
		Bitmap iconBmp = ((BitmapDrawable) iconDrawable).getBitmap();
		for(int ix = 0; ix < ALLRESTURANTLOCATIONS.length; ix++) {
			mMap.addMarker(new MarkerOptions()
			    .position(ALLRESTURANTLOCATIONS[ix].mLatLng)
		        .icon(BitmapDescriptorFactory.fromBitmap(iconBmp)));
		}
…
代码示例 4. 在 Google Maps 上绘制商店图标。

发送地理围栏通知

地理围栏是由点的纬度和经度坐标和半径定义的圆形区域。Android 应用可以将地理围栏注册到 Android 位置服务。Android 应用还可以为地理围栏指定到期持续时间。每当发生地理围栏转换时,例如,当 Android 设备进入或离开注册的地理围栏时,Android 位置服务就会通知 Android 应用。

在我们的餐厅应用中,我们可以为每个商店位置定义一个地理围栏。当设备进入商店附近时,应用会发送通知,例如,“您已进入您最喜欢的餐厅附近!”(图 4)。

图 4 地理围栏定义为由兴趣点和半径构成的圆形区域

注册和注销地理围栏

在 Android SDK 中,位置服务也是 Google Play Services APK 中“Extras”目录下的一个组件。

要请求地理围栏监控,首先我们需要在应用的清单中指定“ACCESS_FINE_LOCATION”权限,这在前面的部分已经完成。

我们还需要检查 Google Play Services 的可用性(代码示例 5 中的 checkGooglePlayServices() 方法)。在调用 locationClient().connect() 并成功建立连接后,位置服务将调用 onConnected(Bundle bundle) 函数,在该函数中,位置客户端可以请求添加或删除地理围栏。

public class GeolocationActivity extends Activity implements
        GooglePlayServicesClient.ConnectionCallbacks
…
{
…	
    private LocationClient mLocationClient;
    
…

    static class StoreLocation {
        public LatLng mLatLng;
        public String mId;
        StoreLocation(LatLng latlng, String id) {
            mLatLng = latlng;
            mId = id;
        }
    }

    @Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.geolocation_view);

        mLocationClient = new LocationClient(this, this, this);

        // Create a new broadcast receiver to receive updates from the listeners and service
        mGeofenceBroadcastReceiver = new ResturantGeofenceReceiver();

        // Create an intent filter for the broadcast receiver
        mIntentFilter = new IntentFilter();

        // Action for broadcast Intents that report successful addition of geofences
        mIntentFilter.addAction(ACTION_GEOFENCES_ADDED);

        // Action for broadcast Intents that report successful removal of geofences
        mIntentFilter.addAction(ACTION_GEOFENCES_REMOVED);

        // Action for broadcast Intents containing various types of geofencing errors
        mIntentFilter.addAction(ACTION_GEOFENCE_ERROR);

        // All Location Services sample apps use this category
        mIntentFilter.addCategory(CATEGORY_LOCATION_SERVICES);

		createGeofences();

		mRegisterGeofenceButton = (Button)findViewById(R.id.geofence_switch);
		mGeofenceState = CAN_START_GEOFENCE;
    
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        // Register the broadcast receiver to receive status updates
        LocalBroadcastManager.getInstance(this).registerReceiver(
            mGeofenceBroadcastReceiver, mIntentFilter);
    }
        
    /**
     * Create a Geofence list
     */
    public void createGeofences() {
        for(int ix=0; ix > ALLRESTURANTLOCATIONS.length; ix++) {
            Geofence fence = new Geofence.Builder()
                .setRequestId(ALLRESTURANTLOCATIONS[ix].mId)
                .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
                .setCircularRegion(
                    ALLRESTURANTLOCATIONS[ix].mLatLng.latitude, ALLRESTURANTLOCATIONS[ix].mLatLng.longitude, GEOFENCERADIUS)
                .setExpirationDuration(Geofence.NEVER_EXPIRE)
                .build();
            mGeofenceList.add(fence);
        }
    }

    // callback function when the mRegisterGeofenceButton is clicked
    public void onRegisterGeofenceButtonClick(View view) {
        if (mGeofenceState == CAN_REGISTER_GEOFENCE) {
            registerGeofences();
            mGeofenceState = GEOFENCE_REGISTERED;
            mGeofenceButton.setText(R.string.unregister_geofence);
            mGeofenceButton.setClickable(true);            
        else {
            unregisterGeofences();
            mGeofenceButton.setText(R.string.register_geofence);
            mGeofenceButton.setClickable(true);
            mGeofenceState = CAN_REGISTER_GEOFENCE;
        }
    }

    private boolean checkGooglePlayServices() {
        int result = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
        if (result == ConnectionResult.SUCCESS) {
            return true;
        } 
        else {
            Dialog errDialog = GooglePlayServicesUtil.getErrorDialog(
                    result,
                    this,
                    CONNECTION_FAILURE_RESOLUTION_REQUEST);

            if (errorDialog != null) {
                errorDialog.show();
            }
        }
        return false;
   }


    public void registerGeofences() {
	
        if (!checkGooglePlayServices()) {

            return;
        }
        mRequestType = REQUEST_TYPE.ADD;

        try {
            // Try to add geofences
            requestConnectToLocationClient();
        } catch (UnsupportedOperationException e) {
            // handle the exception
        }
        
    }

    public void unregisterGeofences() {

        if (!checkGooglePlayServices()) {
            return;
        }

        // Record the type of removal
    	  mRequestType = REQUEST_TYPE.REMOVE;

        // Try to make a removal request
        try {
            mCurrentIntent = getRequestPendingIntent());
            requestConnectToLocationClient();

        } catch (UnsupportedOperationException e) {
            // handle the exception
        }
    }

    public void requestConnectToLocationServices () throws UnsupportedOperationException {
        // If a request is not already in progress
        if (!mRequestInProgress) {
            mRequestInProgress = true;

            locationClient().connect();
        } 
        else {
            // Throw an exception and stop the request
            throw new UnsupportedOperationException();
        }
    }


    /**
     * Get a location client and disconnect from Location Services
     */
    private void requestDisconnectToLocationServices() {

        // A request is no longer in progress
        mRequestInProgress = false;

        locationClient().disconnect();
        
        if (mRequestType == REQUEST_TYPE.REMOVE) {
            mCurrentIntent.cancel();
        }

    }

    /**
     * returns A LocationClient object
     */
    private GooglePlayServicesClient locationClient() {
        if (mLocationClient == null) {

            mLocationClient = new LocationClient(this, this, this);
        }
        return mLocationClient;

}

    /*
     Called back from the Location Services when the request to connect the client finishes successfully. At this point, you can
request the current location or start periodic updates
     */
    @Override
    public void onConnected(Bundle bundle) {
        if (mRequestType == REQUEST_TYPE.ADD) {
        // Create a PendingIntent for Location Services to send when a geofence transition occurs
        mGeofencePendingIntent = createRequestPendingIntent();

        // Send a request to add the current geofences
        mLocationClient.addGeofences(mGeofenceList, mGeofencePendingIntent, this);

        } 
        else if (mRequestType == REQUEST_TYPE.REMOVE){

            mLocationClient.removeGeofences(mCurrentIntent, this);        
        } 
}
…
}
代码示例 5. 通过位置服务请求地理围栏监控。

实现位置服务回调

位置服务请求通常是非阻塞的或异步调用。实际上,在前一部分的代码示例 5 中,我们已经实现了其中一个函数:在调用 locationClient().connect() 并建立连接后,位置服务将调用 onConnected(Bundle bundle) 函数。代码示例 6 列出了我们需要实现的其他位置回调函数。

public class GeolocationActivity extends Activity implements
        OnAddGeofencesResultListener,
        OnRemoveGeofencesResultListener,
        GooglePlayServicesClient.ConnectionCallbacks,
        GooglePlayServicesClient.OnConnectionFailedListener {
…	


    @Override
    public void onDisconnected() {
        mRequestInProgress = false;
        mLocationClient = null;
}

    

    /*
     * Handle the result of adding the geofences
     */
    @Override
    public void onAddGeofencesResult(int statusCode, String[] geofenceRequestIds) {

        // Create a broadcast Intent that notifies other components of success or failure
        Intent broadcastIntent = new Intent();

        // Temp storage for messages
        String msg;

        // If adding the geocodes was successful
        if (LocationStatusCodes.SUCCESS == statusCode) {

            // Create a message containing all the geofence IDs added.
            msg = getString(R.string.add_geofences_result_success,
                    Arrays.toString(geofenceRequestIds));

            // Create an Intent to broadcast to the app
            broadcastIntent.setAction(ACTION_GEOFENCES_ADDED)
                           .addCategory(CATEGORY_LOCATION_SERVICES)
                           .putExtra(EXTRA_GEOFENCE_STATUS, msg);
        // If adding the geofences failed
        } else {
            msg = getString(
                    R.string.add_geofences_result_failure,
                    statusCode,
                    Arrays.toString(geofenceRequestIds)
            );
            broadcastIntent.setAction(ACTION_GEOFENCE_ERROR)
                           .addCategory(CATEGORY_LOCATION_SERVICES)
                           .putExtra(EXTRA_GEOFENCE_STATUS, msg);
        }

        LocalBroadcastManager.getInstance(this)
            .sendBroadcast(broadcastIntent);

        // request to disconnect the location client
        requestDisconnectToLocationServices();
    }

    /*
     * Implementation of OnConnectionFailedListener.onConnectionFailed
     * If a connection or disconnection request fails, report the error
     * connectionResult is passed in from Location Services
     */
    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        mInProgress = false;
        if (connectionResult.hasResolution()) {

            try {
                connectionResult.startResolutionForResult(this,
                    CONNECTION_FAILURE_RESOLUTION_REQUEST);
            } 
            catch (SendIntentException e) {
                // log the error
            }
        } 
        else {
            Intent errorBroadcastIntent = new Intent(ACTION_CONNECTION_ERROR);
            errorBroadcastIntent.addCategory(CATEGORY_LOCATION_SERVICES)
                     .putExtra(EXTRA_CONNECTION_ERROR_CODE,
                                 connectionResult.getErrorCode());
             LocalBroadcastManager.getInstance(this)
                 .sendBroadcast(errorBroadcastIntent);
        }
    }
    
    @Override
    public void onRemoveGeofencesByPendingIntentResult(int statusCode,
            PendingIntent requestIntent) {

        // Create a broadcast Intent that notifies other components of success or failure
        Intent broadcastIntent = new Intent();

        // If removing the geofences was successful
        if (statusCode == LocationStatusCodes.SUCCESS) {

            // Set the action and add the result message
            broadcastIntent.setAction(ACTION_GEOFENCES_REMOVED);
            broadcastIntent.putExtra(EXTRA_GEOFENCE_STATUS,
                    getString(R.string.remove_geofences_intent_success));

        } 
        else {
            // removing the geocodes failed


            // Set the action and add the result message
            broadcastIntent.setAction(ACTION_GEOFENCE_ERROR);
            broadcastIntent.putExtra(EXTRA_GEOFENCE_STATUS,
                    getString(R.string.remove_geofences_intent_failure, 
                        statusCode));
        }
        LocalBroadcastManager.getInstance(this)
                .sendBroadcast(broadcastIntent);

        // request to disconnect the location client
        requestDisconnectToLocationServices();
    }

        
    public class ResturantGeofenceReceiver extends BroadcastReceiver {
  

	  @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            // Intent contains information about errors in adding or removing geofences
            if (TextUtils.equals(action, ACTION_GEOFENCE_ERROR)) {
                // handleGeofenceError(context, intent);
            } 
            else if (TextUtils.equals(action, ACTION_GEOFENCES_ADDED)
                    ||
                    TextUtils.equals(action, ACTION_GEOFENCES_REMOVED)) {
                // handleGeofenceStatus(context, intent);
            } 
            else if (TextUtils.equals(action, ACTION_GEOFENCE_TRANSITION)) {
                // handleGeofenceTransition(context, intent);
            } 
            else {
                // handle error
            }
			
        }
    }


    public PendingIntent getRequestPendingIntent() {
        return createRequestPendingIntent();
    }

    private PendingIntent createRequestPendingIntent() {

        if (mGeofencePendingIntent != null) {

            // Return the existing intent
            return mGeofencePendingIntent;

        // If no PendingIntent exists
        } else {

            // Create an Intent pointing to the IntentService
            Intent intent = new Intent(this,
                ReceiveGeofenceTransitionIntentService.class);

            return PendingIntent.getService(
                    this,
                    0,
                    intent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
        }
    }


    @Override
public void onRemoveGeofencesByRequestIdsResult(int statusCode, 
    String[] geofenceRequestIds) {

        // it should not come here because we only remove geofences by PendingIntent
        // Disconnect the location client
        requestDisconnection();
    }
代码示例 6. 实现位置服务回调。

实现 IntentService

最后,我们需要实现 IntentService 类来处理地理围栏转换(代码示例 7)。

public class ReceiveGeofenceTransitionIntentService extends IntentService {
    /**
     * Sets an identifier for the service
     */
    public ReceiveGeofenceTransitionIntentService() {
        super("ReceiveGeofenceTransitionsIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
    	
        // Create a local broadcast Intent
        Intent broadcastIntent = new Intent();

        // Give it the category for all intents sent by the Intent Service
        broadcastIntent.addCategory(CATEGORY_LOCATION_SERVICES);

   	
        // First check for errors
        if (LocationClient.hasError(intent)) {
            // Get the error code with a static method
            int errorCode = LocationClient.getErrorCode(intent);
        } 
        else {
            // Get the type of transition (entry or exit)
            int transition =
                    LocationClient.getGeofenceTransition(intent);
            
            if ((transition == Geofence.GEOFENCE_TRANSITION_ENTER)  ||
                    (transition == Geofence.GEOFENCE_TRANSITION_EXIT)) {

                // Post a notification
            } 
            else {
                // handle the error
            }
        }
    }
}
代码示例 7. 实现一个 IntentService 类来处理地理围栏转换。

总结

在本文中,我们讨论了如何将地图和地理围栏功能集成到 Android 商务应用中。这些功能支持应用中丰富的地理定位内容和强大的基于位置的服务及用例。

参考资料

关于作者

苗炜是英特尔软件与服务事业部的一名软件工程师。他目前从事英特尔® Atom™ 处理器平台优化项目。

*其他名称和品牌可能被声明为他人的财产。
**此示例源代码根据英特尔示例源代码许可协议发布

优化通知

英特尔编译器可能不会对非英特尔微处理器进行与英特尔微处理器相同的优化程度的优化,这些优化包括 SSE2、SSE3 和 SSE3 指令集以及其他优化。英特尔不保证在非英特尔制造的微处理器上任何优化的可用性、功能或有效性。

本产品中的微处理器相关优化旨在用于英特尔微处理器。某些非英特尔微体系结构特有的优化保留给英特尔微处理器。请参阅适用的产品用户和参考指南,了解有关本声明涵盖的具体指令集的更多信息。

其他相关文章和资源

© . All rights reserved.