【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.pswp.cn/web/43825.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/43825.shtml
英文地址,請注明出處:http://en.pswp.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;不能直接在…

QUdpSocket 的bind函數詳解

QUdpSocket 是 Qt 框架中用于處理 UDP 網絡通信的類。bind 函數是此類中的一個重要方法&#xff0c;它用于將 QUdpSocket 對象綁定到一個特定的端口上&#xff0c;以便在該端口上接收 UDP 數據包。 函數原型 在 Qt 中&#xff0c;bind 函數的原型通常如下所示&#xff1a; b…

微軟開源項目GraphRAG——基于知識圖譜的RAG簡介

前言 在大型語言模型&#xff08;LLM&#xff09;的前沿研究中&#xff0c;一個核心挑戰與機遇并存的領域是擴展它們的能力&#xff0c;以解決超出其訓練數據范疇的問題。這不僅要求模型在面對全新數據時仍能保持卓越表現&#xff0c;還意味著開辟了全新的數據分析可能性&…

JVM 堆內存分配過程

設置堆內存大小和 OOM Java 堆用于存儲 Java 對象實例&#xff0c;那么堆的大小在 JVM 啟動的時候就確定了&#xff0c;我們可以通過 -Xmx 和 -Xms 來設定 -Xms 用來表示堆的起始內存&#xff0c;等價于 -XX:InitialHeapSize-Xmx 用來表示堆的最大內存&#xff0c;等價于 -XX…

Hadoop-15-Hive 元數據管理與存儲 Metadata 內嵌模式 本地模式 遠程模式 集群規劃配置 啟動服務 3節點云服務器實測

章節內容 上一節我們完成了&#xff1a; Hive中數據導出&#xff1a;HDFSHQL操作上傳內容至Hive、增刪改查等操作 背景介紹 這里是三臺公網云服務器&#xff0c;每臺 2C4G&#xff0c;搭建一個Hadoop的學習環境&#xff0c;供我學習。 之前已經在 VM 虛擬機上搭建過一次&am…

簡單的基追蹤一維信號降噪方法(MATLAB 2018)

基追蹤法是基于冗余過完備字典下的一種信號稀疏表示方法。該方法具有可提高信號的稀疏性、實現閾值降噪和提高時頻分辨率等優點。基追蹤法采用表示系數的范數作為信號來度量稀疏性&#xff0c;通過最小化l型范數將信號稀疏表示問題定義為一類有約束的極值問題&#xff0c;進而轉…

c++ primer plus 第15章友,異常和其他 15.3.11 有關異常的注意事項

c primer plus 第15章友&#xff0c;異常和其他 15.3.11 有關異常的注意事項 15.3.11 有關異常的注意事項 文章目錄 c primer plus 第15章友&#xff0c;異常和其他 15.3.11 有關異常的注意事項15.3.11 有關異常的注意事項 15.3.11 有關異常的注意事項 從前面關于如何使用異常…

vue實現表單輸入框數字類型校驗功能

vue實現表單輸入框數字類型校驗功能 1. 樣式代碼 <el-form-item label"訂單總價"><el-input size"small" v-model"form.totalPrice" placeholder"請輸入訂單總價 正整數或者2位數小數" input"check(form.totalPric…

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;不做任何修改報錯 嘗試一 修改運行時的選擇 取消勾選 ‘每個運行中運行多重查…

C++ 判斷語句的深入解析

C++ 判斷語句的深入解析 C++ 是一種廣泛使用的編程語言,以其高效性和靈活性著稱。在 C++ 中,判斷語句是控制程序流程的關鍵組成部分,它們允許程序根據不同的條件執行不同的代碼路徑。本文將深入探討 C++ 中的判斷語句,包括 if、else if、else 以及 switch 語句,并展示如何…

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;區塊鏈的技術名詞 …

在Spring Boot項目中集成單點登錄解決方案

在Spring Boot項目中集成單點登錄解決方案 大家好&#xff0c;我是微賺淘客系統3.0的小編&#xff0c;也是冬天不穿秋褲&#xff0c;天冷也要風度的程序猿&#xff01; 在現代的企業應用中&#xff0c;單點登錄&#xff08;Single Sign-On, SSO&#xff09;解決方案是確保用戶…

【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;即可快速生成一份…