【Android】基于 LocationManager 原生实现定位打卡

目录

  • 前言
  • 一、实现效果
  • 二、定位原理
  • 三、具体实现
    • 1. 获取权限
    • 2. 页面绘制
    • 3. 获取经纬度
    • 4. 方法调用
    • 5. 坐标转换
    • 6. 距离计算
    • 7. 完整代码


前言

最近公司有个新需求,想要用定位进行考勤打卡,在距离打卡地一定范围内才可以进行打卡。本文将借鉴 RxTool 的 RxLocationUtils 的定位工具类,实现定位打卡功能,界面仿照如下图所示的钉钉考勤打卡。

在这里插入图片描述

RxTool 在这篇文章里面:【Android】常用的第三方开源库汇总


一、实现效果

在这里插入图片描述
页面上主要有几个重要信息:经纬度、详细地址、距打卡地的距离。

二、定位原理

在实现功能之前,我们先来了解Android是如何获取位置信息的?

Android的定位可大致分为两种:卫星定位(美国GPS、俄罗斯格洛纳斯、中国北斗)、网络定位(WiFi定位、基站定位)。

卫星定位:接收多个卫星发出的信号,通过三角定位原理计算出设备的经度、纬度和海拔信息,再将经度和纬度信息转换成具体位置。GPS至少要4颗卫星才能精准定位,所以需要有良好的卫星信号覆盖。
在这里插入图片描述

网络定位Wi-Fi定位是通过分析手机连接过的Wi-Fi网络信号来判断其所在位置的方法。这种方法的精度相对较高,可达几十米范围。基站定位是手机与附近运营商基站之间的信号传递来确定用户位置的一种方法。这种方法的精度一般在几百米范围内。但这种定位方式取决于服务器,由于大部分安卓手机没有安装谷歌官方的位置管理器库,大陆网络也不允许,即没有服务器来做这个事情,这种方式基本上不能用。

经纬度:获取到经纬度自然就能转为详细地址
在这里插入图片描述
经度描述南北方向,纬度描述东西方向,经纬度共同组成了一个地址坐标系统,这里特别注意一点:不同坐标系上的经纬度不一样,例如数学上的直角坐标系的坐标值不能直接拿到极坐标上描点。

国内主流坐标系类型主要有以下三种:

  1. WGS84:即GPS84 坐标系,一种大地坐标系,也是目前广泛使用的GPS全球卫星定位系统使用的坐标系。
  2. GCJ02:即火星坐标系,由中国国家测绘局制订的地理信息系统的坐标系统,是由WGS84坐标系经过加密后的坐标系。
  3. BD09:百度坐标系,在GCJ02坐标系基础上再二次加密。

这里为什么会有这么多种坐标系呢?因为不同国家出于安全的原因,为了保护一些比较敏感的坐标位置不得不进行加密处理。

安卓原生LocationManager获取到的经纬度是采用GPS84坐标系,百度地图SDK自然采用百度特有的坐标系,而高德地图是采用火星坐标系。若使用两种不同的坐标系,因坐标值不同,具体展示位置会有所偏移,所以在使用上必须进行坐标转换。

百度坐标系的经纬度可以用这个坐标拾取网站去查询具体位置:https://api.map.baidu.com/lbsapi/getpoint/index.html

三、具体实现

定位功能这里有两种方案去实现:

第一种是利用安卓原生的LocationManager去获取经纬度。
第二种就是使用第三方的SDK,如百度地图SDK、高德地图SDK,第三方SDK需要导入Jar包。

如果想要地图界面或者高精度定位可以选择使用第三方SDK,我们这里的需求只需要一个定位而已,就简单使用原生的定位,而且第三方有可能收费。

1. 获取权限

在app的AndroidManifest.xml中加入如下代码:

	<uses-permission android:name="android.permission.INTERNET"/><!-- 这个权限用于进行网络定位 --><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /><!-- 这个权限用于访问GPS定位 --><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

在跳转到定位打卡页面之前要先确保已经授权定位权限,授权是每个app必须注意的模块,所以具体代码就不展开了

2. 页面绘制

在这里插入图片描述

punch_main_activity.xml代码:这里有些图标因为在博客上无法下载,所以就省去了,需要可以自行找一些图标代替

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#f3f3f3"android:orientation="vertical"tools:ignore="ResourceName"><LinearLayoutandroid:layout_above="@+id/rl_button_bottom"android:layout_width="match_parent"android:layout_gravity="center"android:gravity="center"android:layout_height="match_parent"android:orientation="vertical"><LinearLayoutandroid:id="@+id/ll_clock"android:layout_width="160dp"android:background="#0085ff"android:layout_gravity="center"android:clickable="false"android:gravity="center"android:orientation="vertical"android:elevation="15dp"android:layout_height="160dp"><TextViewandroid:id="@+id/tv_clock"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textColor="@android:color/white"android:textSize="20sp"android:text="拍照打卡"/><TextClockandroid:layout_marginTop="10dp"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="14sp"android:textColor="@android:color/white"android:format12Hour="yyyy/MM/dd HH:mm:ss"android:format24Hour="yyyy/MM/dd HH:mm:ss"android:text=""/></LinearLayout><LinearLayoutandroid:layout_width="wrap_content"android:layout_marginTop="10dp"android:paddingStart="10dp"android:paddingEnd="10dp"android:orientation="horizontal"android:layout_height="wrap_content"><TextViewandroid:id="@+id/tv_location"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="15sp"android:textColor="#202020"android:maxLines="2"android:ellipsize="end"android:layout_marginStart="3dp"android:text="定位正在加载中..."/></LinearLayout><TextViewandroid:id="@+id/tv_distance"android:layout_marginTop="10dp"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="14sp"android:text=""/><TextViewandroid:id="@+id/tv_refresh"android:layout_marginTop="10dp"android:layout_width="wrap_content"android:layout_height="wrap_content"android:paddingTop="5dp"android:paddingBottom="5dp"android:paddingStart="20dp"android:paddingEnd="20dp"android:textColor="#0579ff"android:textSize="14sp"android:gravity="center"android:drawablePadding="5dp"android:text="刷新位置"/></LinearLayout><RelativeLayoutandroid:id="@+id/rl_button_bottom"android:layout_alignParentBottom="true"android:layout_width="match_parent"android:elevation="5dp"android:background="@color/white"android:layout_height="wrap_content"><TextViewandroid:id="@+id/tv_bottom_text"android:layout_centerInParent="true"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="手机定位服务被关闭,去打开"android:paddingTop="30dp"android:paddingBottom="30dp"android:textSize="18sp"android:textColor="#202020"/><ImageViewandroid:layout_alignParentEnd="true"android:layout_centerVertical="true"android:layout_marginEnd="10dp"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/duty_right_arrow"/></RelativeLayout></RelativeLayout>

3. 获取经纬度

获取经纬度我们主要用到RxLocationUtils工具类中的register方法:

public static boolean register(Context context, long minTime, long minDistance, OnLocationChangeListener listener) {if (listener == null) return false;if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1);return false;}mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);mListener = listener;if (!isLocationEnabled(context)) {RxToast.showToast(context, "无法定位,请打开定位服务", 500);return false;}String provider = mLocationManager.getBestProvider(getCriteria(), true);Location location = mLocationManager.getLastKnownLocation(provider);if (location != null) listener.getLastKnownLocation(location);if (myLocationListener == null) myLocationListener = new MyLocationListener();mLocationManager.requestLocationUpdates(provider, minTime, minDistance, myLocationListener);return true;}

我们一步步分析,首先判断权限,其次判断GPS是否打开,再去获取经纬度。

在android framework层的android.loaction包下面主要提供了如下两个类来帮助开发者来获取地理位置信息。

LocationManager:用于获取地理位置的经纬度信息
Geocoder:根据经纬度获取详细地址信息 / 根据详细地址获取经纬度信息

LocationManager的getBestProvider 返回当前设备最符合指定条件的位置提供者,第一个参数criteria用于指定条件,第二个参数表示是否返回当前设备可用的位置提供者。

getLastKnownLocation()方法一次性的获得当前最新的地理位置,它不能实时监听地理位置的变化情况。所以要使用一个接口监听类LocationListener来实时监听,在使用该监听之前必须要用LocationManager类中的requestLocationUpdates方法来注册该监听事件,这样就可以实现在GPS打开或者关闭、位置变化、间隔时间等情况下进行位置刷新。

public void requestLocationUpdates(String provider, long minTime, float minDistance,LocationListener listener)

其中,参数一:位置提供者;参数二:位置更新最短时间(单位ms);参数三:位置更新最短距离(单位m);参数四:LocationListener监听器对象。

LocationListener接口类中有如下方法:这里RxLocationUtils没有重写GPS打开或者关闭时方法,需要自己添加。

private static class MyLocationListenerimplements LocationListener {/*** 当坐标改变时触发此函数,如果Provider传进相同的坐标,它就不会被触发** @param location 坐标*/@Overridepublic void onLocationChanged(Location location) {if (mListener != null) {mListener.onLocationChanged(location);}}/*** provider的在可用、暂时不可用和无服务三个状态直接切换时触发此函数** @param provider 提供者* @param status   状态* @param extras   provider可选包*/@Overridepublic void onStatusChanged(String provider, int status, Bundle extras) {if (mListener != null) {mListener.onStatusChanged(provider, status, extras);}switch (status) {case LocationProvider.AVAILABLE:Log.d("onStatusChanged", "当前GPS状态为可见状态");break;case LocationProvider.OUT_OF_SERVICE:Log.d("onStatusChanged", "当前GPS状态为服务区外状态");break;case LocationProvider.TEMPORARILY_UNAVAILABLE:Log.d("onStatusChanged", "当前GPS状态为暂停服务状态");break;}}/*** provider被enable时触发此函数,比如GPS被打开*/@Overridepublic void onProviderEnabled(String provider) {if (mListener != null) {mListener.onProviderEnabled(provider);}}/*** provider被disable时触发此函数,比如GPS被关闭*/@Overridepublic void onProviderDisabled(String provider) {if (mListener != null) {mListener.onProviderDisabled(provider);}}}

在获取到经纬度之后,将其转化为详细地址描述。

Geocoder 用于获取地理位置的前向编码和反向编码,其中反向编码是根据经纬度获取对应的详细地址。Geocoder 请求的是一个后台服务,但是该服务不包括在标准android framework中。需要提前用Geocoder的isPresent()方法来判断当前设备是否包含地理位置服务

	/*** 根据经纬度获取地理位置** @param context   上下文* @param latitude  纬度* @param longitude 经度* @return {@link Address}*/public static Address getAddress(Context context, double latitude, double longitude) {Geocoder geocoder = new Geocoder(context, Locale.getDefault());try {List<Address> addresses = geocoder.getFromLocation(latitude, longitude, 1);if (addresses.size() > 0) return addresses.get(0);} catch (IOException e) {e.printStackTrace();}return null;}

这里返回的位置信息是一个集合Address,其中Address类中包含了各种地理位置信息,包括经纬度,国家,城市,地区,街道,国家编码,城市编码等等,根据自己需求选择。

这里有一个注意点:Geocoder获取位置信息是一个后台的耗时操作,可能导致详细地址一开始获取不到无法显示出来,这里就需要异步线程的方式来请求服务,避免阻塞主线程

4. 方法调用

在activity中使用

	override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.punch_main_activity)tv_refresh.setOnClickListener {refresh()}rl_button_bottom.setOnClickListener {RxLocationUtils.openGpsSettings(this)}ll_clock.setOnClickListener {//处理打卡逻辑}}override fun onResume() {super.onResume()window.transparentStatusBar()refresh()}fun refresh(){if(!RxLocationUtils.register(this,30*1000,1,this)){setClockClick(false)tv_location.text="定位失败"tv_distance.text=""}}private fun setClockClick(isClick:Boolean){if(isClick){ll_clock.isClickable=truell_clock.isEnabled=truetv_clock.text="拍照打卡"}else{ll_clock.isClickable=falsell_clock.isEnabled=falsetv_clock.text="无法打卡"}if(RxLocationUtils.isLocationEnabled(this)){rl_button_bottom.visibility= View.GONE}else{rl_button_bottom.visibility= View.VISIBLE}}override fun getLastKnownLocation(location: Location?) {location?.let { updateLocation(location) }}override fun onLocationChanged(location: Location) {updateLocation(location)}override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}override fun onProviderEnabled(provider: String?) {refresh()}override fun onProviderDisabled(provider: String?) {setClockClick(false)tv_location.text="定位失败,请打开GPS定位"tv_distance.text=""RxToast.showToast(this, "无法定位,请打开定位服务", 500)}

当gps关闭时跳转到打开gps的系统页面,重写监听器方法

获取到经纬度后处理,通过Handler异步处理地址:

	//位置描述var locationDes=""//目标经纬度var dest_latitude=39.948047var dest_longitude=116.360548//最大可打卡距离 200m内var clockDistance:Int=200//安卓8 获取地址有明显的时延,合理的方式是在工作线程中处理GeoCoderprivate val uiCallback by lazy {object : Handler(Looper.getMainLooper()) {override fun handleMessage(msg: Message) {tv_location.text=locationDes}}}private fun updateLocation(location: Location){// 获取当前纬度val latitude = location.latitude// 获取当前经度val longitude = location.longitude// 获取经纬度对于的位置,getFromLocation(纬度, 经度, 最多获取的位置数量)// 得到第一个经纬度位置解析信息// Address里面还有很多方法。比如具体省的名称、市的名称...val gps = RxLocationUtils.GPS84ToBD09(latitude,longitude)val distance = RxLocationUtils.getDistance(gps.wgLon,gps.wgLat,dest_longitude,dest_latitude)if(distance.toInt()<clockDistance){setClockClick(true)locationDes= "已进入考勤范围:"}else{setClockClick(false)locationDes= "未进入考勤范围:"}findLocation(latitude,longitude)tv_distance.text="当前打卡距离:${distance.toInt()}m (${clockDistance}m以内打卡)"}private fun findLocation(latitude: Double, longitude: Double){Thread{locationDes+=if(RxLocationUtils.getFeature(this,latitude,longitude).isNullOrEmpty()) "定位正在加载中..." else  RxLocationUtils.getFeature(this,latitude,longitude) // 获取街道uiCallback.sendEmptyMessage(0)}.start()}

5. 坐标转换

由于我这里的目标打卡地使用的是百度坐标系的经纬度,所以计算距离之前需要进行坐标转换,gps84要转到BD-09得经过两次转换处理:GPS85->GCJ-02->BD-09

	/*** 国际 GPS84 坐标系* 转换成* [国测局坐标系] 火星坐标系 (GCJ-02)* <p>* World Geodetic System ==> Mars Geodetic System** @param lon 经度* @param lat 纬度* @return GPS实体类*/public static Gps GPS84ToGCJ02(double lat, double lon) {if (outOfChina(lat, lon)) {return null;}double dLat = transformLat(lon - 105.0, lat - 35.0);double dLon = transformLon(lon - 105.0, lat - 35.0);double radLat = lat / 180.0 * pi;double magic = Math.sin(radLat);magic = 1 - ee * magic * magic;double sqrtMagic = Math.sqrt(magic);dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);double mgLat = lat + dLat;double mgLon = lon + dLon;return new Gps(mgLat, mgLon);}/*** 火星坐标系 (GCJ-02)* 转换成* 百度坐标系 (BD-09)** @param gg_lon 经度* @param gg_lat 纬度*/public static Gps GCJ02ToBD09(double gg_lat, double gg_lon) {double x = gg_lon, y = gg_lat;double z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * pi);double theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * pi);double bd_lon = z * Math.cos(theta) + 0.0065;double bd_lat = z * Math.sin(theta) + 0.006;return new Gps(bd_lat, bd_lon);}/*** 国际 GPS84 坐标系* 转换成* 百度坐标系 (BD-09)** @param lon 经度* @param lat 纬度*/public static Gps GPS84ToBD09(double lat, double lon) {Gps gps = GPS84ToGCJ02(lat,lon);if (gps == null) {return new Gps(lat,lon);}//GCJ-02 转 BD-09return GCJ02ToBD09(gps.getWgLat(), gps.getWgLon());}

6. 距离计算

两个地理位置之间的直线距离通过Haversine法去计算,Haversine公式是一种比勾股定理法(将地球表面直接看作平面)更精确的算法,它考虑了地球的球形结构。该算法的基本思想是将两个坐标点之间的距离看作地球表面上的一段弧长,然后根据球面三角形的定理计算弧长。Haversine公式的公式如下:
在这里插入图片描述
其中,R分为这几类:地球赤道半径6378千米,两极半径6357千米,平均半径6371千米。这里用选用赤道半径。

	private static final double EARTH_RADIUS = 6378137.0; //地球半径/*** 计算两个经纬度之间的距离** @param longitude* @param latitude* @param longitude2* @param latitude2* @return 单位米*/public static double getDistance(double longitude, double latitude, double longitude2, double latitude2) {double lat1 = rad(latitude);double lat2 = rad(latitude2);double a = lat1 - lat2;double b = rad(longitude) - rad(longitude2);double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(b / 2), 2)));s = s * EARTH_RADIUS;s = Math.round(s * 10000) / 10000; //四舍五入return s;}/*** 弧度换为角度* @param d* @return*/private static double rad(double d) {return d * Math.PI / 180.0;}

7. 完整代码

RxLocationUtils:

/*** @author ondear*         time  : 16/11/13*         desc  : 定位相关工具类*/
public class RxLocationUtils {public static double pi = 3.1415926535897932384626;public static double a = 6378245.0;public static double ee = 0.00669342162296594323;private static OnLocationChangeListener mListener;private static MyLocationListener myLocationListener;private static LocationManager mLocationManager;/*** 判断Gps是否可用** @return {@code true}: 是<br>{@code false}: 否*/public static boolean isGpsEnabled(Context context) {LocationManager lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);return lm.isProviderEnabled(LocationManager.GPS_PROVIDER);}/*** 判断定位是否可用** @return {@code true}: 是<br>{@code false}: 否*/public static boolean isLocationEnabled(Context context) {LocationManager lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);return lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) || lm.isProviderEnabled(LocationManager.GPS_PROVIDER);}/*** 打开Gps设置界面*/public static void openGpsSettings(Context context) {Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);context.startActivity(intent);}/*** 注册* <p>使用完记得调用{@link #unregister()}</p>* <p>需添加权限 {@code <uses-permission android:name="android.permission.INTERNET"/>}</p>* <p>需添加权限 {@code <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>}</p>* <p>需添加权限 {@code <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>}</p>* <p>如果{@code minDistance}为0,则通过{@code minTime}来定时更新;</p>* <p>{@code minDistance}不为0,则以{@code minDistance}为准;</p>* <p>两者都为0,则随时刷新。</p>** @param minTime     位置信息更新周期(单位:毫秒)* @param minDistance 位置变化最小距离:当位置距离变化超过此值时,将更新位置信息(单位:米)* @param listener    位置刷新的回调接口* @return {@code true}: 初始化成功<br>{@code false}: 初始化失败*/public static boolean register(Context context, long minTime, long minDistance, OnLocationChangeListener listener) {if (listener == null) return false;if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1);return false;}mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);mListener = listener;if (!isLocationEnabled(context)) {RxToast.showToast(context, "无法定位,请打开定位服务", 500);return false;}String provider = mLocationManager.getBestProvider(getCriteria(), true);Location location = mLocationManager.getLastKnownLocation(provider);if (location != null) listener.getLastKnownLocation(location);if (myLocationListener == null) myLocationListener = new MyLocationListener();mLocationManager.requestLocationUpdates(provider, minTime, minDistance, myLocationListener);return true;}/*** 注销*/public static void unregister() {if (mLocationManager != null) {if (myLocationListener != null) {mLocationManager.removeUpdates(myLocationListener);myLocationListener = null;}mLocationManager = null;}}/*** 设置定位参数** @return {@link Criteria}*/private static Criteria getCriteria() {Criteria criteria = new Criteria();//设置定位精确度 Criteria.ACCURACY_COARSE比较粗略,Criteria.ACCURACY_FINE则比较精细criteria.setAccuracy(Criteria.ACCURACY_FINE);//设置是否要求速度criteria.setSpeedRequired(false);// 设置是否允许运营商收费criteria.setCostAllowed(false);//设置是否需要方位信息criteria.setBearingRequired(false);//设置是否需要海拔信息criteria.setAltitudeRequired(false);// 设置对电源的需求criteria.setPowerRequirement(Criteria.POWER_LOW);return criteria;}/*** 根据经纬度获取地理位置** @param context   上下文* @param latitude  纬度* @param longitude 经度* @return {@link Address}*/public static Address getAddress(Context context, double latitude, double longitude) {Geocoder geocoder = new Geocoder(context, Locale.getDefault());try {List<Address> addresses = geocoder.getFromLocation(latitude, longitude, 1);if (addresses.size() > 0) return addresses.get(0);} catch (IOException e) {e.printStackTrace();}return null;}/*** 根据经纬度获取所在国家** @param context   上下文* @param latitude  纬度* @param longitude 经度* @return 所在国家*/public static String getCountryName(Context context, double latitude, double longitude) {Address address = getAddress(context, latitude, longitude);return address == null ? "unknown" : address.getCountryName();}/*** 根据经纬度获取所在地** @param context   上下文* @param latitude  纬度* @param longitude 经度* @return 所在地*/public static String getLocality(Context context, double latitude, double longitude) {Address address = getAddress(context, latitude, longitude);return address == null ? "unknown" : address.getLocality();}/*** 根据经纬度获取所在街道** @param context   上下文* @param latitude  纬度* @param longitude 经度* @return 所在街道*/public static String getStreet(Context context, double latitude, double longitude) {Address address = getAddress(context, latitude, longitude);return address == null ? "unknown" : address.getAddressLine(0);}/*** 根据经纬度获取详细地址** @param context   上下文* @param latitude  纬度* @param longitude 经度* @return 所在街道*/public static String getFeature(Context context, double latitude, double longitude) {Address address = getAddress(context, latitude, longitude);return address == null ? "未知地点" : address.getFeatureName();}//------------------------------------------坐标转换工具start--------------------------------------/*** GPS坐标 转换成 角度* 例如 113.202222 转换成 113°12′8″** @param location* @return*/public static String gpsToDegree(double location) {double degree = Math.floor(location);double minute_temp = (location - degree) * 60;double minute = Math.floor(minute_temp);
//        double second = Math.floor((minute_temp - minute)*60);String second = new DecimalFormat("#.##").format((minute_temp - minute) * 60);return (int) degree + "°" + (int) minute + "′" + second + "″";}/*** 国际 GPS84 坐标系* 转换成* [国测局坐标系] 火星坐标系 (GCJ-02)* <p>* World Geodetic System ==> Mars Geodetic System** @param lon 经度* @param lat 纬度* @return GPS实体类*/public static Gps GPS84ToGCJ02(double lat, double lon) {if (outOfChina(lat, lon)) {return null;}double dLat = transformLat(lon - 105.0, lat - 35.0);double dLon = transformLon(lon - 105.0, lat - 35.0);double radLat = lat / 180.0 * pi;double magic = Math.sin(radLat);magic = 1 - ee * magic * magic;double sqrtMagic = Math.sqrt(magic);dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);double mgLat = lat + dLat;double mgLon = lon + dLon;return new Gps(mgLat, mgLon);}/*** [国测局坐标系] 火星坐标系 (GCJ-02)* 转换成* 国际 GPS84 坐标系** @param lon 火星经度* @param lat 火星纬度*/public static Gps GCJ02ToGPS84(double lat, double lon) {Gps gps = transform(lat, lon);double lontitude = lon * 2 - gps.getWgLon();double latitude = lat * 2 - gps.getWgLat();return new Gps(latitude, lontitude);}/*** 火星坐标系 (GCJ-02)* 转换成* 百度坐标系 (BD-09)** @param gg_lon 经度* @param gg_lat 纬度*/public static Gps GCJ02ToBD09(double gg_lat, double gg_lon) {double x = gg_lon, y = gg_lat;double z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * pi);double theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * pi);double bd_lon = z * Math.cos(theta) + 0.0065;double bd_lat = z * Math.sin(theta) + 0.006;return new Gps(bd_lat, bd_lon);}/*** 国际 GPS84 坐标系* 转换成* 百度坐标系 (BD-09)** @param lon 经度* @param lat 纬度*/public static Gps GPS84ToBD09(double lat, double lon) {Gps gps = GPS84ToGCJ02(lat,lon);if (gps == null) {return new Gps(lat,lon);}//GCJ-02 转 BD-09return GCJ02ToBD09(gps.getWgLat(), gps.getWgLon());}/*** 百度坐标系 (BD-09)* 转换成* 火星坐标系 (GCJ-02)** @param bd_lon 百度*经度* @param bd_lat 百度*纬度* @return GPS实体类*/public static Gps BD09ToGCJ02(double bd_lat, double bd_lon) {double x = bd_lon - 0.0065, y = bd_lat - 0.006;double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * pi);double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * pi);double gg_lon = z * Math.cos(theta);double gg_lat = z * Math.sin(theta);return new Gps(gg_lat, gg_lon);}/*** 百度坐标系 (BD-09)* 转换成* 国际 GPS84 坐标系** @param bd_lon 百度*经度* @param bd_lat 百度*纬度* @return GPS实体类*/public static Gps BD09ToGPS84(double bd_lat, double bd_lon) {Gps gcj02 = BD09ToGCJ02(bd_lat, bd_lon);Gps map84 = GCJ02ToGPS84(gcj02.getWgLat(),gcj02.getWgLon());return map84;}/*** 不在中国范围内** @param lon 经度* @param lat 纬度* @return boolean值*/public static boolean outOfChina(double lat, double lon) {if (lon < 72.004 || lon > 137.8347)return true;return lat < 0.8293 || lat > 55.8271;}/*** 转化算法** @param lon* @param lat* @return*/public static Gps transform(double lat, double lon) {if (outOfChina(lat, lon)) {return new Gps(lat, lon);}double dLat = transformLat(lon - 105.0, lat - 35.0);double dLon = transformLon(lon - 105.0, lat - 35.0);double radLat = lat / 180.0 * pi;double magic = Math.sin(radLat);magic = 1 - ee * magic * magic;double sqrtMagic = Math.sqrt(magic);dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);double mgLat = lat + dLat;double mgLon = lon + dLon;return new Gps(mgLat, mgLon);}/*** 纬度转化算法** @param x* @param y* @return*/public static double transformLat(double x, double y) {double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y+ 0.2 * Math.sqrt(Math.abs(x));ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;return ret;}/*** 经度转化算法** @param x* @param y* @return*/public static double transformLon(double x, double y) {double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1* Math.sqrt(Math.abs(x));ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0* pi)) * 2.0 / 3.0;return ret;}public interface OnLocationChangeListener {/*** 获取最后一次保留的坐标** @param location 坐标*/void getLastKnownLocation(Location location);/*** 当坐标改变时触发此函数,如果Provider传进相同的坐标,它就不会被触发** @param location 坐标*/void onLocationChanged(Location location);/*** provider的在可用、暂时不可用和无服务三个状态直接切换时触发此函数** @param provider 提供者* @param status   状态* @param extras   provider可选包*/void onStatusChanged(String provider, int status, Bundle extras);//位置状态发生改变void onProviderEnabled(String provider);void onProviderDisabled(String provider);}private static class MyLocationListenerimplements LocationListener {/*** 当坐标改变时触发此函数,如果Provider传进相同的坐标,它就不会被触发** @param location 坐标*/@Overridepublic void onLocationChanged(Location location) {if (mListener != null) {mListener.onLocationChanged(location);}}/*** provider的在可用、暂时不可用和无服务三个状态直接切换时触发此函数** @param provider 提供者* @param status   状态* @param extras   provider可选包*/@Overridepublic void onStatusChanged(String provider, int status, Bundle extras) {if (mListener != null) {mListener.onStatusChanged(provider, status, extras);}switch (status) {case LocationProvider.AVAILABLE:Log.d("onStatusChanged", "当前GPS状态为可见状态");break;case LocationProvider.OUT_OF_SERVICE:Log.d("onStatusChanged", "当前GPS状态为服务区外状态");break;case LocationProvider.TEMPORARILY_UNAVAILABLE:Log.d("onStatusChanged", "当前GPS状态为暂停服务状态");break;}}/*** provider被enable时触发此函数,比如GPS被打开*/@Overridepublic void onProviderEnabled(String provider) {if (mListener != null) {mListener.onProviderEnabled(provider);}}/*** provider被disable时触发此函数,比如GPS被关闭*/@Overridepublic void onProviderDisabled(String provider) {if (mListener != null) {mListener.onProviderDisabled(provider);}}}//===========================================坐标转换工具end====================================private static final double EARTH_RADIUS = 6378137.0; //地球半径/*** 计算两个经纬度之间的距离** @param longitude* @param latitude* @param longitude2* @param latitude2* @return 单位米*/public static double getDistance(double longitude, double latitude, double longitude2, double latitude2) {double lat1 = rad(latitude);double lat2 = rad(latitude2);double a = lat1 - lat2;double b = rad(longitude) - rad(longitude2);double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(b / 2), 2)));s = s * EARTH_RADIUS;s = Math.round(s * 10000) / 10000; //四舍五入return s;}/*** 弧度换为角度* @param d* @return*/private static double rad(double d) {return d * Math.PI / 180.0;}
}

ClockActivity:

class ClockActivity: AppCompatActivity(),RxLocationUtils.OnLocationChangeListener {//位置描述var locationDes=""//目标经纬度var dest_latitude=39.948047var dest_longitude=116.360548//最大可打卡距离 200m内var clockDistance:Int=200//安卓8 获取地址有明显的时延,合理的方式是在工作线程中处理GeoCoderprivate val uiCallback by lazy {object : Handler(Looper.getMainLooper()) {override fun handleMessage(msg: Message) {tv_location.text=locationDes}}}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.punch_main_activity)tv_refresh.setOnClickListener {refresh()}rl_button_bottom.setOnClickListener {RxLocationUtils.openGpsSettings(this)}ll_clock.setOnClickListener {//处理打卡逻辑}}private fun setClockClick(isClick:Boolean){if(isClick){ll_clock.isClickable=truell_clock.isEnabled=truetv_clock.text="拍照打卡"}else{ll_clock.isClickable=falsell_clock.isEnabled=falsetv_clock.text="无法打卡"}if(RxLocationUtils.isLocationEnabled(this)){rl_button_bottom.visibility= View.GONE}else{rl_button_bottom.visibility= View.VISIBLE}}override fun onResume() {super.onResume()window.transparentStatusBar()refresh()}override fun onDestroy() {super.onDestroy()//记得销毁RxLocationUtils.unregister()}fun refresh(){if(!RxLocationUtils.register(this,30*1000,1,this)){setClockClick(false)tv_location.text="定位失败"tv_distance.text=""}}override fun getLastKnownLocation(location: Location?) {location?.let { updateLocation(location) }}override fun onLocationChanged(location: Location) {updateLocation(location)}override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}override fun onProviderEnabled(provider: String?) {refresh()}override fun onProviderDisabled(provider: String?) {setClockClick(false)tv_location.text="定位失败,请打开GPS定位"tv_distance.text=""RxToast.showToast(this, "无法定位,请打开定位服务", 500)}private fun updateLocation(location: Location){// 获取当前纬度val latitude = location.latitude// 获取当前经度val longitude = location.longitude// 获取经纬度对于的位置,getFromLocation(纬度, 经度, 最多获取的位置数量)// 得到第一个经纬度位置解析信息// Address里面还有很多方法。比如具体省的名称、市的名称...val gps = RxLocationUtils.GPS84ToBD09(latitude,longitude)val distance= RxLocationUtils.getDistance(gps.wgLon,gps.wgLat,dest_longitude,dest_latitude)if(distance.toInt()<clockDistance){setClockClick(true)locationDes= "已进入考勤范围:"}else{setClockClick(false)locationDes= "未进入考勤范围:"}findLocation(latitude,longitude)tv_distance.text="当前打卡距离:${distance.toInt()}m (${clockDistance}m以内打卡)"}private fun findLocation(latitude: Double, longitude: Double){Thread{locationDes+=if(RxLocationUtils.getFeature(this,latitude,longitude).isNullOrEmpty()) "定位正在加载中..." else  RxLocationUtils.getFeature(this,latitude,longitude) // 获取街道uiCallback.sendEmptyMessage(0)}.start()}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/43825.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php快速入门

前言 php是一门脚本语言&#xff0c;可以访问服务器&#xff0c;对数据库增删查改&#xff08;后台/后端语言&#xff09; 后台语言&#xff1a;php&#xff0c;java&#xff0c;c&#xff0c;c&#xff0c;python等等 注意&#xff1a;php是操作服务器&#xff0c;不能直接在…

微软开源项目GraphRAG——基于知识图谱的RAG简介

前言 在大型语言模型&#xff08;LLM&#xff09;的前沿研究中&#xff0c;一个核心挑战与机遇并存的领域是扩展它们的能力&#xff0c;以解决超出其训练数据范畴的问题。这不仅要求模型在面对全新数据时仍能保持卓越表现&#xff0c;还意味着开辟了全新的数据分析可能性&…

Hadoop-15-Hive 元数据管理与存储 Metadata 内嵌模式 本地模式 远程模式 集群规划配置 启动服务 3节点云服务器实测

章节内容 上一节我们完成了&#xff1a; Hive中数据导出&#xff1a;HDFSHQL操作上传内容至Hive、增删改查等操作 背景介绍 这里是三台公网云服务器&#xff0c;每台 2C4G&#xff0c;搭建一个Hadoop的学习环境&#xff0c;供我学习。 之前已经在 VM 虚拟机上搭建过一次&am…

简单的基追踪一维信号降噪方法(MATLAB 2018)

基追踪法是基于冗余过完备字典下的一种信号稀疏表示方法。该方法具有可提高信号的稀疏性、实现阈值降噪和提高时频分辨率等优点。基追踪法采用表示系数的范数作为信号来度量稀疏性&#xff0c;通过最小化l型范数将信号稀疏表示问题定义为一类有约束的极值问题&#xff0c;进而转…

SpringSecurity中文文档(Servlet Authorize HttpServletRequests)

Authorize HttpServletRequests SpringSecurity 允许您在请求级别对授权进行建模。例如&#xff0c;对于 Spring Security&#xff0c;可以说/admin 下的所有页面都需要一个权限&#xff0c;而其他所有页面只需要身份验证。 默认情况下&#xff0c;SpringSecurity 要求对每个…

Umi.js 项目中使用 Web Worker

1.配置 Umi.js 在 Umi.js 中&#xff0c;需要通过配置来扩展 Webpack 的功能。在项目根目录下修改 config/config.ts 文件&#xff1a; export default defineConfig({chainWebpack(config) {config.module.rule(worker).test(/\.worker\.ts$/).use(worker-loader).loader(wo…

C语言之指针的奥秘(二)

一、数组名的理解 int arr[10]{1,2,3,4,5,6,7,8,9,10}; int *p&arr[0]; 这里使用 &arr[0] 的⽅式拿到了数组第⼀个元素的地址&#xff0c;但是其实数组名本来就是地址&#xff0c;而且是数组首元素的地址。如下&#xff1a; 我们发现数组名和数组⾸元素的地址打印出…

重要文件放u盘还是硬盘?硬盘和u盘哪个适合长期存储

在数字时代&#xff0c;我们每天都会处理大量的文件。其中&#xff0c;不乏一些对我们而言至关重要的文件&#xff0c;如家庭照片、工作文档、财务记录等。面对这些重要文件的存储问题&#xff0c;我们通常会面临&#xff1a;“重要文件放U盘还是硬盘”、“硬盘和U盘哪个适合长…

Vue2打包部署后动态修改后端接口地址的解决方法

文章目录 前言一、背景二、解决方法1.在public文件夹下创建config文件夹&#xff0c;并创建config.js文件2.编写config.js内容3.在index.html中加载config.js4.在封装axios工具类的js中修改配置 总结 前言 本篇文章将介绍使用Vue2开发前后端分离项目时&#xff0c;前端打包部署…

系统架构师考点--系统安全

大家好。今天我来总结一下系统安全相关的考点&#xff0c;这类考点每年都会考到&#xff0c;一般是在上午场客观题&#xff0c;占2-4分。 一、信息安全基础知识 信息安全包括5个基本要素&#xff1a;机密性、完整性、可用性、可控性与可审查性 (1)机密性&#xff1a;确保信息…

Navicat导入sql文件

文章目录 Navicat导入SQL文件&#xff0c;使用默认导入&#xff0c;不做任何修改报错尝试一修改运行时的选择 尝试二修改my.ini的配置文件 Navicat导入SQL文件&#xff0c;使用默认导入&#xff0c;不做任何修改报错 尝试一 修改运行时的选择 取消勾选 ‘每个运行中运行多重查…

3,区块链加密(react+区块链实战)

3&#xff0c;区块链加密&#xff08;react区块链实战&#xff09; 3.1 哈希3.2 pow-pos-dpos3.3非对称加密&#xff08;1&#xff09;对称加密AES&#xff08;2&#xff09;非对称加密RSA 3.4 拜占庭将军3.5 P2P网络3.6 区块链 3.1 哈希 密码学&#xff0c;区块链的技术名词 …

【Git的基本操作】版本回退 | 撤销修改的三种情况 | 删除文件

目录 5.版本回退 5.1选项hard&后悔药 5.2后悔药&commit id 5.3版本回退的原理 6.撤销修改 6.1情况一 6.2情况二 6.3情况三 ​7.删除文件 Git重要能力之一马&#xff0c;版本回退功能。Git是版本控制系统&#xff0c;能够管理文件历史版本。本篇以ReadMe文件为…

神器!3个免费PPT成品网站推荐+3款AIPPT工具盘点!

熬夜加班做PPT却没有头绪&#xff1f;别再自己憋着想了&#xff01;现在凡事主打一个“抄作业”&#xff0c;想做ppt却没想法&#xff0c;可以去到ppt成品网站搜集PPT模板&#xff0c;或是使用时下流行的AI生成PPT工具&#xff0c;只需输入PPT主题&#xff0c;即可快速生成一份…

全网最详细的CRC讲解即计算

CRC 循环冗余码&#xff08;Cyclic Redundancy Code&#xff0c; CRC&#xff09;是一种用于校验通信链路上数字传输准确性的计算方法&#xff08;通过某种数学运算来建立数据位和校验位(CRC)的约定关系的&#xff09;。它是利用除法以及余数的原理来作错误侦测。 发送方: 使用…

客户关系管理怎么做?这4个工具一定要会用!

在商海浮沉中&#xff0c;每一位企业家和销售经理都深知&#xff0c;客户是企业生存与发展的基石。但如何有效管理这些宝贵的资源&#xff0c;让每一次互动都成为加深关系、促进成交的契机&#xff0c;却是一门艺术加科学的结合体。今天&#xff0c;咱们就来聊聊客户关系管理&a…

3SRB5016-ASEMI逆变箱专用3SRB5016

编辑&#xff1a;ll 3SRB5016-ASEMI逆变箱专用3SRB5016 型号&#xff1a;3SRB5016 品牌&#xff1a;ASEMI 封装&#xff1a;SGBJ-5 批号&#xff1a;2024 现货&#xff1a;50000 最大重复峰值反向电压&#xff1a;1600V 最大正向平均整流电流(Vdss)&#xff1a;50A 功…

Kodcloud可道云安装与一键发布上线实现远程访问详细教程

文章目录 1.前言2. Kodcloud网站搭建2.1. Kodcloud下载和安装2.2 Kodcloud网页测试 3. cpolar内网穿透的安装和注册4. 本地网页发布4.1 Cpolar云端设置4.2 Cpolar本地设置 5. 公网访问测试6.结语 1.前言 本文主要为大家介绍一款国人自研的在线Web文件管理器可道云&#xff0c;…

唐刘:当 SaaS 爱上 TiDB(一)- 行业挑战与 TiDB 的应对之道

导读 在 TiDB 8.1 发布后&#xff0c;TiDB 展现了强大的支持 SaaS 业务的能力&#xff0c;成为 SaaS 业务数据库的优先选择之一。 本文为“当 SaaS 爱上 TiDB”系列文章的第一篇&#xff0c;系列文章将从技术原理和真实用户体验两个角度深入探讨 TiDB 在 SaaS 业务中的表现&a…

太速科技-3U VPX飞腾处理器刀片计算机

3U VPX飞腾处理器刀片计算机 一 、产品概述 该产品是一款基于国产飞腾FT2000 4核或腾锐D2000 8核的高性能3U VPX刀片式计算机。产品提供了4个x4 PCIe 3.0总线接口&#xff0c;同时可配置为1个x16或2个x8 PCIe3.0接口&#xff0c;因此具有很强的扩展性&#xff0c;极大…