Xamarin.Android:使用 GPS 和位置信息






4.70/5 (20投票s)
在本文中,您将学习如何获取设备的当前位置,并根据提供的初始位置确定您与该位置的距离(以英里为单位)。
引言
在过去的几个月里,我开始使用 Xamarin 学习移动和设备开发。首先是创建了一个可以同步可穿戴设备和手持设备之间数据的简单应用程序,然后又着手处理其他 Android 相关内容。到目前为止,过程还算愉快,但我确实遇到了一些挑战,主要是因为需要克服的学习曲线。
我写这篇文章是为了让任何对移动开发感兴趣的人都能参考,如果他们需要一个包含 GPS 和位置功能的简单可用 Android 应用。在这个特定的示例中,我将演示如何获取设备的当前位置,并根据提供的初始位置确定您与该位置的距离(以英里为单位)。
在继续之前,请确保您的系统已满足必要的先决条件,并且您的开发环境已正确配置。有关在 Visual Studio 中设置开发环境的信息,请参阅我之前的文章: 使用 Xamarin 和 Visual Studio 入门 Android 可穿戴开发
让我们开始吧!
对于此示例,我将使用 Visual Studio 2015 和截至本文撰写时最新的 Xamarin 版本。
现在,启动 Visual Studio 2015,然后选择“文件”>“新建”>“项目”来创建一个新项目。这将打开下面的对话框:
在对话框中,在“模板”下选择“Visual C#”>“Android”>“空白应用(Android)”。您可以为您的应用命名,但为了演示的简洁性,我将其命名为“MyFirstGPSApp”。现在,单击“确定”生成应用程序所需的必要文件。您应该能看到以下屏幕:
在继续之前,了解生成的文件及其用途非常重要。因此,作为回顾,这是摘自官方文档的 Xamarin.Android 应用程序的结构: 此处。
文件夹 用途
参考文献 | 包含构建和运行应用程序所需的程序集。 |
Components | Components 目录包含来自 Xamarin Components 的现成功能。 |
资产 | 包含应用程序运行所需的文件,包括字体、本地数据文件和文本文件。 |
属性 | 包含 AndroidManifest.xml 文件,该文件描述了我们 Xamarin.Android 应用程序的所有要求,包括名称、版本号和权限。 |
资源 | 包含应用程序资源,例如字符串、图像和布局。我们可以通过生成的 Resource 类在代码中访问这些资源。 |
创建经纬度类
首先需要做的是创建一个包含以下属性的类:
namespace MyFirstGPSApp
{
public class LatLng
{
public double Latitude { get; set; }
public double Longitude { get; set; }
public LatLng(double lat, double lng)
{
this.Latitude = lat;
this.Longitude = lng;
}
}
}
上面的代码非常简单,非常直接。它只是一个包含简单属性且没有任何逻辑的类。
创建辅助类
接下来需要创建一个辅助类,以便我们重用常用代码。辅助类如下所示:
using System;
namespace MyFirstGPSApp
{
static class Utils
{
public enum DistanceUnit { Miles, Kilometers };
public static double ToRadian(this double value)
{
return (Math.PI / 180) * value;
}
public static double HaversineDistance(LatLng coord1, LatLng coord2, DistanceUnit unit)
{
double R = (unit == DistanceUnit.Miles) ? 3960 : 6371;
var lat = (coord2.Latitude - coord1.Latitude).ToRadian();
var lng = (coord2.Longitude - coord1.Longitude).ToRadian();
var h1 = Math.Sin(lat / 2) * Math.Sin(lat / 2) +
Math.Cos(coord1.Latitude.ToRadian()) * Math.Cos(coord2.Latitude.ToRadian()) *
Math.Sin(lng / 2) * Math.Sin(lng / 2);
var h2 = 2 * Math.Asin(Math.Min(1, Math.Sqrt(h1)));
return R * h2;
}
}
}
ToRadian()
方法是将双精度值转换为弧度的扩展方法。HaversineDistance()
方法根据两个给定的坐标点获取半径内的距离。我参考了此 帖子 中的代码。
创建位置服务
我知道有两种实现 Android 应用中 `Location` 服务功能的方法。最简单的方法是在 `MainActivity` 中直接实现 `ILocationListener`。另一种方法是创建一个实现 ILocationListener 的 `Service`。我选择了服务实现选项,以使我们的代码更灵活和可重用,以防其他应用程序需要它。
下面是 `Location Service` 的代码块:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Locations;
namespace MyFirstGPSApp
{
[Service]
public class GPSService : Service, ILocationListener
{
private const string _sourceAddress = "TGU Tower, Cebu IT Park, Jose Maria del Mar St,Lahug, Cebu City, 6000 Cebu";
private string _location = string.Empty;
private string _address = string.Empty;
private string _remarks = string.Empty;
public const string LOCATION_UPDATE_ACTION = "LOCATION_UPDATED";
private Location _currentLocation;
IBinder _binder;
protected LocationManager _locationManager = (LocationManager)Android.App.Application.Context.GetSystemService(LocationService);
public override IBinder OnBind(Intent intent)
{
_binder = new GPSServiceBinder(this);
return _binder;
}
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
{
return StartCommandResult.Sticky;
}
public void StartLocationUpdates()
{
Criteria criteriaForGPSService = new Criteria
{
//A constant indicating an approximate accuracy
Accuracy = Accuracy.Coarse,
PowerRequirement = Power.Medium
};
var locationProvider = _locationManager.GetBestProvider(criteriaForGPSService, true);
_locationManager.RequestLocationUpdates(locationProvider, 0, 0, this);
}
public event EventHandler<LocationChangedEventArgs> LocationChanged = delegate { };
public void OnLocationChanged(Location location)
{
try
{
_currentLocation = location;
if (_currentLocation == null)
_location = "Unable to determine your location.";
else
{
_location = String.Format("{0},{1}", _currentLocation.Latitude, _currentLocation.Longitude);
Geocoder geocoder = new Geocoder(this);
//The Geocoder class retrieves a list of address from Google over the internet
IList<Address> addressList = geocoder.GetFromLocation(_currentLocation.Latitude, _currentLocation.Longitude, 10);
Address addressCurrent = addressList.FirstOrDefault();
if (addressCurrent != null)
{
StringBuilder deviceAddress = new StringBuilder();
for (int i = 0; i < addressCurrent.MaxAddressLineIndex; i++)
deviceAddress.Append(addressCurrent.GetAddressLine(i))
.AppendLine(",");
_address = deviceAddress.ToString();
}
else
_address = "Unable to determine the address.";
IList<Address> source = geocoder.GetFromLocationName(_sourceAddress, 1);
Address addressOrigin = source.FirstOrDefault();
var coord1 = new LatLng(addressOrigin.Latitude, addressOrigin.Longitude);
var coord2 = new LatLng(addressCurrent.Latitude, addressCurrent.Longitude);
var distanceInRadius = Utils.HaversineDistance(coord1, coord2, Utils.DistanceUnit.Miles);
_remarks = string.Format("Your are {0} miles away from your original location.", distanceInRadius);
Intent intent = new Intent(this, typeof(MainActivity.GPSServiceReciever));
intent.SetAction(MainActivity.GPSServiceReciever.LOCATION_UPDATED);
intent.AddCategory(Intent.CategoryDefault);
intent.PutExtra("Location", _location);
intent.PutExtra("Address", _address);
intent.PutExtra("Remarks", _remarks);
SendBroadcast(intent);
}
}
catch (Exception ex)
{
_address = "Unable to determine the address.";
}
}
public void OnStatusChanged(string provider, Availability status, Bundle extras)
{
//TO DO:
}
public void OnProviderDisabled(string provider)
{
//TO DO:
}
public void OnProviderEnabled(string provider)
{
//TO DO:
}
}
public class GPSServiceBinder : Binder
{
public GPSService Service { get { return this.LocService; } }
protected GPSService LocService;
public bool IsBound { get; set; }
public GPSServiceBinder(GPSService service) { this.LocService = service; }
}
public class GPSServiceConnection : Java.Lang.Object, IServiceConnection
{
GPSServiceBinder _binder;
public event Action Connected;
public GPSServiceConnection(GPSServiceBinder binder)
{
if (binder != null)
this._binder = binder;
}
public void OnServiceConnected(ComponentName name, IBinder service)
{
GPSServiceBinder serviceBinder = (GPSServiceBinder)service;
if (serviceBinder != null)
{
this._binder = serviceBinder;
this._binder.IsBound = true;
serviceBinder.Service.StartLocationUpdates();
if (Connected != null)
Connected.Invoke();
}
}
public void OnServiceDisconnected(ComponentName name) { this._binder.IsBound = false; }
}
}
GPSService.cs
文件基本上包含以下类:
GPSService
GPSServiceBinder
GPSServiceConnection
GPSService
是一个实现 `Service` 和 `ILocationService` 的类。这就是我们在设备位置更改时实现代码并执行某些任务的地方。`OnLocationChanged` 事件会根据注册位置监听器时提供的设置触发,`StartLocationUpdates()` 方法会处理此问题。
在 `OnLocationChanged` 事件中,我们可以放置获取设备地址、位置和备注的逻辑。您可能还会注意到我使用了 `Intent` 通过 `SendBroadcast()` 方法传递一些数据。然后可以使用 `BroadcastReciever` 检索正在传递的值。
Android 在通信服务方面提供了三个选项,具体取决于服务运行的位置。对于此示例,我使用的是 `Service Binding`。主要原因是我们的服务(`GPSService`)只是我们应用程序的一部分。这样,客户端可以通过绑定到服务来直接与其通信。绑定到客户端的服务将重写 `Bound Service` 生命周期方法,并使用 `Binder`(`GPSServiceBinder`)和 `ServiceConnection`(`GPSServiceConnection`)与客户端通信。
构建 UI
修改 Resources > Layout 文件夹下的 Main.axml,使其看起来像这样:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/txtLocation"
android:width="200dp"
android:layout_marginRight="0dp"
android:layout_gravity="right"
android:gravity="left"
android:layout_alignParentRight="true" />
<TextView
android:text="Location :"
android:layout_width="60.2dp"
android:layout_height="wrap_content"
android:id="@+id/textView1"
android:layout_toLeftOf="@id/txtLocation"
android:layout_alignTop="@id/txtLocation"
android:width="100dp"
android:layout_marginTop="0dp"
android:layout_alignParentLeft="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/txtLocation"
android:id="@+id/txtAddress"
android:width="200dp"
android:layout_alignParentRight="true" />
<TextView
android:text="Address :"
android:layout_width="60.2dp"
android:layout_height="wrap_content"
android:id="@+id/textView2"
android:layout_toLeftOf="@id/txtAddress"
android:layout_below="@id/txtLocation"
android:width="100dp"
android:layout_marginTop="0dp"
android:layout_alignParentLeft="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/txtAddress"
android:id="@+id/txtRemarks"
android:width="200dp"
android:layout_alignParentRight="true" />
<TextView
android:text="Remarks :"
android:layout_width="60.2dp"
android:layout_height="wrap_content"
android:id="@+id/textView3"
android:layout_toLeftOf="@id/txtRemarks"
android:layout_below="@id/txtAddress"
android:width="100dp"
android:layout_marginTop="0dp"
android:layout_alignParentLeft="true" />
</RelativeLayout>
上面的布局没有什么特别之处。它只包含一些 `TextViews` 来显示我们服务的结果。
主活动
现在,通过添加以下代码块来更新 `MainActivity.cs` 文件:
using Android.App;
using Android.Content;
using Android.Widget;
using Android.OS;
namespace MyFirstGPSApp
{
[Activity(Label = "MyFirstGPSApp", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : Activity
{
TextView _locationText;
TextView _addressText;
TextView _remarksText;
GPSServiceBinder _binder;
GPSServiceConnection _gpsServiceConnection;
Intent _gpsServiceIntent;
private GPSServiceReciever _receiver;
public static MainActivity Instance;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
Instance = this;
SetContentView(Resource.Layout.Main);
_addressText = FindViewById<TextView>(Resource.Id.txtAddress);
_locationText = FindViewById<TextView>(Resource.Id.txtLocation);
_remarksText = FindViewById<TextView>(Resource.Id.txtRemarks);
RegisterService();
}
private void RegisterService()
{
_gpsServiceConnection = new GPSServiceConnection(_binder);
_gpsServiceIntent = new Intent(Android.App.Application.Context, typeof(GPSService));
BindService(_gpsServiceIntent, _gpsServiceConnection, Bind.AutoCreate);
}
private void RegisterBroadcastReceiver()
{
IntentFilter filter = new IntentFilter(GPSServiceReciever.LOCATION_UPDATED);
filter.AddCategory(Intent.CategoryDefault);
_receiver = new GPSServiceReciever();
RegisterReceiver(_receiver, filter);
}
private void UnRegisterBroadcastReceiver()
{
UnregisterReceiver(_receiver);
}
public void UpdateUI(Intent intent)
{
_locationText.Text = intent.GetStringExtra("Location");
_addressText.Text = intent.GetStringExtra("Address");
_remarksText.Text = intent.GetStringExtra("Remarks");
}
protected override void OnResume()
{
base.OnResume();
RegisterBroadcastReceiver();
}
protected override void OnPause()
{
base.OnPause();
UnRegisterBroadcastReceiver();
}
[BroadcastReceiver]
internal class GPSServiceReciever : BroadcastReceiver
{
public static readonly string LOCATION_UPDATED = "LOCATION_UPDATED";
public override void OnReceive(Context context, Intent intent)
{
if (intent.Action.Equals(LOCATION_UPDATED))
{
MainActivity.Instance.UpdateUI(intent);
}
}
}
}
}
`OnCreate` 事件是初始化 `ContentViews` 和 `TextViews` 的地方。也是注册我们应用程序所需服务的地方。`RegisterService()` 注册并绑定所需的服务。`RegisterBroadcastReciever()` 方法注册广播接收器,以便我们可以访问广播中的数据。此方法将在 `OnResume` 事件重写中调用。`UnRegisterBroadcastReceiver()` 方法注销广播接收器。此方法将在活动的 `OnPause` 事件重写中调用。
`GPSServiceReciever` 类用于通过实现 `BroadcastReciever` 来处理来自广播的消息。如果您还记得,在 `GPSService` 的 `OnLocationChanged` 事件下,我们发送了一个 `Intent` 广播来传递值。然后,这些值将显示在 UI 中供客户端查看。
总结
供您参考,这是项目的实际结构:
在测试应用程序之前,请确保在 `AndroidManifest.xml` 中具有以下权限:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
运行应用程序
现在尝试在实际设备上部署该应用程序。输出应如下所示:
就是这样!希望您觉得这篇文章有用。
摘要
在本文中,我们学习了如何创建一个利用 Android 位置服务的简单应用程序。