Android GPS 初涉
GPS(Global Positioning System,全局定位系统)是 Android 设备中用于获取地理位置信息的重要功能,通过卫星信号确定设备的经度、纬度、海拔等数据。结合 Android 动画合集的背景,GPS 数据可以驱动动态动画效果,例如在地图上显示用户位置的移动动画,或根据位置变化触发 UI 交互。本文将介绍 Android GPS 的基本概念、工作原理、使用方法及与动画结合的示例,适合初学者快速入门。
GPS 的作用与原理
- 作用:Android GPS 提供设备的位置信息(经纬度、海拔、速度等),用于导航、定位、位置分享或基于位置的服务(LBS)。它通过设备内置的 GPS 芯片接收卫星信号,结合其他定位技术(如 Wi-Fi、蜂窝网络)提高精度。
- 原理:GPS 模块与多颗卫星通信,基于三角测量计算设备位置。Android 通过
LocationManager
或 Google Play Services 的FusedLocationProviderClient
提供位置数据,融合 GPS、Wi-Fi 和基站定位以优化性能。 - 应用场景:
- 导航应用:实时显示用户位置(如 Google Maps)。
- 动画驱动:根据位置变化触发视图移动或缩放动画。
- 位置记录:跟踪运动轨迹(如跑步应用)。
- 地理围栏:检测用户进入/离开特定区域,触发动画或通知。
- AR 应用:结合位置和方向传感器实现增强现实效果。
权限要求
使用 GPS 需要以下权限,在 AndroidManifest.xml
中声明:
<!-- 粗略定位(Wi-Fi/基站) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 精确定位(GPS) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 后台定位(API 29+) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
- 动态权限(API 23+,Android 6.0+):
ACCESS_FINE_LOCATION
:精确 GPS 定位。ACCESS_COARSE_LOCATION
:粗略定位。ACCESS_BACKGROUND_LOCATION
:后台定位(API 29+)。- 使用
ActivityCompat.requestPermissions
请求权限。 - 注意:API 30+(Android 11+)需明确声明定位类型(前台/后台),并可能需要用户手动授权。
获取 GPS 数据
Android 提供两种主要方式获取位置数据:
- LocationManager(传统方式):
- 系统服务,直接访问 GPS 或网络定位。
- 适合简单场景,但功能较基础。
- FusedLocationProviderClient(推荐,Google Play Services):
- 融合 GPS、Wi-Fi 和基站定位,精度更高,功耗更低。
- 需要 Google Play Services 支持。
常用 API
以下是 GPS 相关的核心类和方法:
类/方法 | 描述 | 用法示例 |
---|---|---|
LocationManager | 系统服务,管理位置提供者(GPS、网络)。 | LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE); |
LocationManager.requestLocationUpdates() | 请求位置更新。 | lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 10, listener); |
FusedLocationProviderClient | Google Play Services 提供的高级定位 API。 | FusedLocationProviderClient client = LocationServices.getFusedLocationProviderClient(context); |
FusedLocationProviderClient.getLastLocation() | 获取最后已知位置。 | client.getLastLocation().addOnSuccessListener(location -> {}); |
FusedLocationProviderClient.requestLocationUpdates() | 请求实时位置更新。 | client.requestLocationUpdates(request, callback, null); |
- 定位提供者(
LocationManager
): GPS_PROVIDER
:GPS 定位,精度高,耗电高。NETWORK_PROVIDER
:Wi-Fi/基站定位,精度较低,耗电低。PASSIVE_PROVIDER
:被动接收其他应用的位置更新。
完整示例(FusedLocationProviderClient + 动画)
以下是一个使用 FusedLocationProviderClient
获取 GPS 位置并驱动 ImageView 移动动画的示例,显示用户位置变化。
import android.Manifest;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import android.animation.ObjectAnimator;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
public class GPSActivity extends AppCompatActivity {
private FusedLocationProviderClient fusedLocationClient;
private ImageView locationMarker;
private TextView locationText;
private ObjectAnimator translateXAnimator;
private ObjectAnimator translateYAnimator;
private LocationCallback locationCallback;
private static final int PERMISSION_REQUEST_CODE = 100;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_gps);
locationMarker = findViewById(R.id.location_marker);
locationText = findViewById(R.id.location_text);
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
// 初始化动画
translateXAnimator = ObjectAnimator.ofFloat(locationMarker, "translationX", 0f);
translateXAnimator.setDuration(500);
translateYAnimator = ObjectAnimator.ofFloat(locationMarker, "translationY", 0f);
translateYAnimator.setDuration(500);
// 检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityResultLauncher<String> permissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
isGranted -> {
if (isGranted) {
startLocationUpdates();
} else {
Toast.makeText(this, "Location permission denied", Toast.LENGTH_SHORT).show();
}
}
);
permissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION);
} else {
startLocationUpdates();
}
}
private void startLocationUpdates() {
LocationRequest locationRequest = LocationRequest.create()
.setInterval(5000) // 更新间隔 5 秒
.setFastestInterval(2000) // 最快更新间隔
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); // 高精度(GPS)
locationCallback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
if (locationResult == null) return;
Location location = locationResult.getLastLocation();
if (location != null) {
// 更新 UI
locationText.setText(String.format("Lat: %.4f, Lon: %.4f", location.getLatitude(), location.getLongitude()));
// 模拟动画:将经纬度映射到屏幕坐标
float x = (float) (location.getLongitude() * 10); // 简单映射
float y = (float) (location.getLatitude() * 10);
translateXAnimator.setFloatValues(locationMarker.getTranslationX(), x);
translateYAnimator.setFloatValues(locationMarker.getTranslationY(), -y);
translateXAnimator.start();
translateYAnimator.start();
}
}
};
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, null);
}
}
@Override
protected void onPause() {
super.onPause();
if (locationCallback != null) {
fusedLocationClient.removeLocationUpdates(locationCallback);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
translateXAnimator.cancel();
translateYAnimator.cancel();
}
}
- 布局 XML(res/layout/activity_gps.xml):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<ImageView
android:id="@+id/location_marker"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/location_icon" />
<TextView
android:id="@+id/location_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Lat: 0.0, Lon: 0.0"
android:textSize="18sp" />
</LinearLayout>
- AndroidManifest.xml(添加权限):
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
- 说明:
- 权限:动态请求
ACCESS_FINE_LOCATION
。 - 定位:使用
FusedLocationProviderClient
请求高精度位置更新(每 5 秒)。 - 动画:根据经纬度变化驱动
ImageView
的平移动画(translationX
和translationY
)。 - UI 更新:实时显示经纬度。
- 生命周期:在
onPause
停止位置更新,onDestroy
取消动画。 - 资源:需要一个位置图标(如
res/drawable/location_icon.png
)。
使用 LocationManager(传统方式)
以下是使用 LocationManager
获取 GPS 数据的简单示例:
LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
LocationListener locationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
locationText.setText(String.format("Lat: %.4f, Lon: %.4f", location.getLatitude(), location.getLongitude()));
float x = (float) (location.getLongitude() * 10);
float y = (float) (location.getLatitude() * 10);
translateXAnimator.setFloatValues(locationMarker.getTranslationX(), x);
translateYAnimator.setFloatValues(locationMarker.getTranslationY(), -y);
translateXAnimator.start();
translateYAnimator.start();
}
};
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 5000, 10, locationListener);
}
- 注销监听:
@Override
protected void onPause() {
super.onPause();
locationManager.removeUpdates(locationListener);
}
结合振动
结合 Vibrator
(前文介绍),当位置显著变化时触发振动:
private Location lastLocation;
@Override
public void onLocationChanged(Location location) {
if (lastLocation != null && location.distanceTo(lastLocation) > 50) { // 移动超过 50 米
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE));
} else {
vibrator.vibrate(100);
}
}
lastLocation = location;
}
优缺点
- 优点:
- GPS 提供高精度位置数据,适合导航和 LBS 应用。
FusedLocationProviderClient
优化了功耗和精度。- 易于与动画结合,增强交互性。
- 缺点:
- GPS 定位耗电量高,室内信号弱。
- 需动态权限,增加开发复杂性。
- 首次定位可能较慢(冷启动)。
- 替代方案:
- Network Provider:Wi-Fi/基站定位,功耗低但精度较低。
- Google Play Services Geofencing:地理围栏功能。
- Jetpack Compose:声明式 UI 显示位置数据。
- Third-party Libraries:如 Mapbox 或 OpenStreetMap。
注意事项
- 权限:API 23+ 需动态请求
ACCESS_FINE_LOCATION
。 - GPS 可用性:检查 GPS 是否启用(
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
)。 - 功耗:设置合理的更新间隔(如 5 秒),避免高频请求。
- 生命周期:在
onPause
停止位置更新,避免资源泄漏。 - 信号问题:室内或信号弱时,结合网络定位提高可靠性。
- 调试:通过 Log 检查位置数据和动画效果。
扩展:Jetpack Compose 中的实现
在 Jetpack Compose 中,结合 GPS 数据驱动动画:
import android.Manifest
import android.content.Context
import android.location.Location
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@Composable
fun GPSScreen() {
val context = LocalContext.current
val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }
var location by remember { mutableStateOf<Location?>(null) }
val xTranslation by animateFloatAsState(
targetValue = location?.longitude?.toFloat()?.times(10) ?: 0f,
animationSpec = tween(500)
)
val yTranslation by animateFloatAsState(
targetValue = location?.latitude?.toFloat()?.times(-10) ?: 0f,
animationSpec = tween(500)
)
val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
startLocationUpdates(context, fusedLocationClient) { loc -> location = loc }
}
}
LaunchedEffect(Unit) {
if (androidx.core.content.ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == android.content.pm.PackageManager.PERMISSION_GRANTED
) {
startLocationUpdates(context, fusedLocationClient) { loc -> location = loc }
} else {
permissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
}
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.location_icon),
contentDescription = "Location Marker",
modifier = Modifier
.size(50.dp)
.offset(x = xTranslation.dp, y = yTranslation.dp)
)
Text(
text = location?.let { String.format("Lat: %.4f, Lon: %.4f", it.latitude, it.longitude) } ?: "Waiting for location...",
fontSize = 18.sp
)
}
}
private fun startLocationUpdates(
context: Context,
client: FusedLocationProviderClient,
onLocationUpdate: (Location) -> Unit
) {
val locationRequest = LocationRequest.create()
.setInterval(5000)
.setFastestInterval(2000)
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
val locationCallback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.lastLocation?.let { onLocationUpdate(it) }
}
}
if (androidx.core.content.ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == android.content.pm.PackageManager.PERMISSION_GRANTED
) {
client.requestLocationUpdates(locationRequest, locationCallback, null)
}
}
- 说明:
- 使用
rememberLauncherForActivityResult
请求定位权限。 FusedLocationProviderClient
获取实时位置更新。animateFloatAsState
驱动Image
的平移动画。- 显示实时经纬度。
如果需要更复杂的功能(如地理围栏、轨迹绘制、结合 WindowManager
创建悬浮定位 UI、或与地图 SDK 集成),或想探讨其他传感器或动画效果,请告诉我!