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

打印 上一主题 下一主题

主题 538|帖子 538|积分 1614


媒介

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

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

一、实现效果


页面上主要有几个重要信息:经纬度、详细地址、距打卡地的距离。
二、定位原理

在实现功能之前,我们先来相识Android是怎样获取位置信息的?
Android的定位可大致分为两种:卫星定位(美国GPS、俄罗斯格洛纳斯、中国北斗)、网络定位(WiFi定位、基站定位)。
卫星定位:吸收多个卫星发出的信号,通过三角定位原理盘算出装备的经度、纬度和海拔信息,再将经度和纬度信息转换成详细位置。GPS至少要4颗卫星才能精准定位,以是需要有精良的卫星信号覆盖。

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

经度形貌南北方向,纬度形貌东西方向,经纬度共同组成了一个地址坐标体系,这里特别留意一点:不同坐标系上的经纬度不一样,例如数学上的直角坐标系的坐标值不能直接拿到极坐标上描点。
国内主流坐标系类型主要有以下三种:

  • WGS84:即GPS84 坐标系,一种大地坐标系,也是目前广泛利用的GPS全球卫星定位体系利用的坐标系。
  • GCJ02:即火星坐标系,由中国国家测绘局制订的地理信息体系的坐标体系,是由WGS84坐标系经过加密后的坐标系。
  • 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中加入如下代码:
  1.         <uses-permission android:name="android.permission.INTERNET"/>
  2.         <!-- 这个权限用于进行网络定位 -->
  3.     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  4.     <!-- 这个权限用于访问GPS定位 -->
  5.     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
复制代码
在跳转到定位打卡页面之前要先确保已经授权定位权限,授权是每个app必须留意的模块,以是详细代码就不展开了
2. 页面绘制


punch_main_activity.xml代码:这里有些图标由于在博客上无法下载,以是就省去了,需要可以自行找一些图标代替
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3.     xmlns:app="http://schemas.android.com/apk/res-auto"
  4.     xmlns:tools="http://schemas.android.com/tools"
  5.     android:layout_width="match_parent"
  6.     android:layout_height="match_parent"
  7.     android:background="#f3f3f3"
  8.     android:orientation="vertical"
  9.     tools:ignore="ResourceName">
  10.     <LinearLayout
  11.         android:layout_above="@+id/rl_button_bottom"
  12.         android:layout_width="match_parent"
  13.         android:layout_gravity="center"
  14.         android:gravity="center"
  15.         android:layout_height="match_parent"
  16.         android:orientation="vertical">
  17.         <LinearLayout
  18.             android:id="@+id/ll_clock"
  19.             android:layout_width="160dp"
  20.             android:background="#0085ff"
  21.             android:layout_gravity="center"
  22.             android:clickable="false"
  23.             android:gravity="center"
  24.             android:orientation="vertical"
  25.             android:elevation="15dp"
  26.             android:layout_height="160dp">
  27.             <TextView
  28.                 android:id="@+id/tv_clock"
  29.                 android:layout_width="wrap_content"
  30.                 android:layout_height="wrap_content"
  31.                 android:textColor="@android:color/white"
  32.                 android:textSize="20sp"
  33.                 android:text="拍照打卡"
  34.                 />
  35.             <TextClock
  36.                 android:layout_marginTop="10dp"
  37.                 android:layout_width="wrap_content"
  38.                 android:layout_height="wrap_content"
  39.                 android:textSize="14sp"
  40.                 android:textColor="@android:color/white"
  41.                 android:format12Hour="yyyy/MM/dd HH:mm:ss"
  42.                 android:format24Hour="yyyy/MM/dd HH:mm:ss"
  43.                 android:text=""/>
  44.         </LinearLayout>
  45.         <LinearLayout
  46.             android:layout_width="wrap_content"
  47.             android:layout_marginTop="10dp"
  48.             android:paddingStart="10dp"
  49.             android:paddingEnd="10dp"
  50.             android:orientation="horizontal"
  51.             android:layout_height="wrap_content">
  52.             <TextView
  53.                 android:id="@+id/tv_location"
  54.                 android:layout_width="wrap_content"
  55.                 android:layout_height="wrap_content"
  56.                 android:textSize="15sp"
  57.                 android:textColor="#202020"
  58.                 android:maxLines="2"
  59.                 android:ellipsize="end"
  60.                 android:layout_marginStart="3dp"
  61.                 android:text="定位正在加载中..."/>
  62.         </LinearLayout>
  63.         <TextView
  64.             android:id="@+id/tv_distance"
  65.             android:layout_marginTop="10dp"
  66.             android:layout_width="wrap_content"
  67.             android:layout_height="wrap_content"
  68.             android:textSize="14sp"
  69.             android:text=""/>
  70.         <TextView
  71.             android:id="@+id/tv_refresh"
  72.             android:layout_marginTop="10dp"
  73.             android:layout_width="wrap_content"
  74.             android:layout_height="wrap_content"
  75.             android:paddingTop="5dp"
  76.             android:paddingBottom="5dp"
  77.             android:paddingStart="20dp"
  78.             android:paddingEnd="20dp"
  79.             android:textColor="#0579ff"
  80.             android:textSize="14sp"
  81.             android:gravity="center"
  82.             android:drawablePadding="5dp"
  83.             android:text="刷新位置"/>
  84.     </LinearLayout>
  85.     <RelativeLayout
  86.         android:id="@+id/rl_button_bottom"
  87.         android:layout_alignParentBottom="true"
  88.         android:layout_width="match_parent"
  89.         android:elevation="5dp"
  90.         android:background="@color/white"
  91.         android:layout_height="wrap_content">
  92.         <TextView
  93.             android:id="@+id/tv_bottom_text"
  94.             android:layout_centerInParent="true"
  95.             android:layout_width="wrap_content"
  96.             android:layout_height="wrap_content"
  97.             android:text="手机定位服务被关闭,去打开"
  98.             android:paddingTop="30dp"
  99.             android:paddingBottom="30dp"
  100.             android:textSize="18sp"
  101.             android:textColor="#202020"/>
  102.         <ImageView
  103.             android:layout_alignParentEnd="true"
  104.             android:layout_centerVertical="true"
  105.             android:layout_marginEnd="10dp"
  106.             android:layout_width="wrap_content"
  107.             android:layout_height="wrap_content"
  108.             android:src="@drawable/duty_right_arrow"/>
  109.     </RelativeLayout>
  110. </RelativeLayout>
复制代码
3. 获取经纬度

获取经纬度我们主要用到RxLocationUtils工具类中的register方法:
  1. public static boolean register(Context context, long minTime, long minDistance, OnLocationChangeListener listener) {
  2.         if (listener == null) return false;
  3.         if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
  4.             ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
  5.             ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1);
  6.             return false;
  7.         }
  8.         mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
  9.         mListener = listener;
  10.         if (!isLocationEnabled(context)) {
  11.             RxToast.showToast(context, "无法定位,请打开定位服务", 500);
  12.             return false;
  13.         }
  14.         String provider = mLocationManager.getBestProvider(getCriteria(), true);
  15.         Location location = mLocationManager.getLastKnownLocation(provider);
  16.         if (location != null) listener.getLastKnownLocation(location);
  17.         if (myLocationListener == null) myLocationListener = new MyLocationListener();
  18.         mLocationManager.requestLocationUpdates(provider, minTime, minDistance, myLocationListener);
  19.         return true;
  20.     }
复制代码
我们一步步分析,起首判断权限,其次判断GPS是否打开,再去获取经纬度。
在android framework层的android.loaction包下面主要提供了如下两个类来资助开辟者来获取地理位置信息。
   LocationManager:用于获取地理位置的经纬度信息
Geocoder:根据经纬度获取详细地址信息 / 根据详细地址获取经纬度信息
  LocationManager的getBestProvider 返回当前装备最符合指定条件的位置提供者,第一个参数criteria用于指定条件,第二个参数表示是否返回当前装备可用的位置提供者。
getLastKnownLocation()方法一次性的得到当前最新的地理位置,它不能实时监听地理位置的变化情况。以是要利用一个接口监听类LocationListener来实时监听,在利用该监听之前必须要用LocationManager类中的requestLocationUpdates方法来注册该监听事件,如许就可以实如今GPS打开或者关闭、位置变化、间隔时间等情况下举行位置刷新。
  1. public void requestLocationUpdates(String provider, long minTime, float minDistance,
  2.             LocationListener listener)
复制代码
其中,参数一:位置提供者;参数二:位置更新最短时间(单位ms);参数三:位置更新最短距离(单位m);参数四:LocationListener监听器对象。
LocationListener接口类中有如下方法:这里RxLocationUtils没有重写GPS打开或者关闭时方法,需要自己添加。
  1. private static class MyLocationListener
  2.             implements LocationListener {
  3.         /**
  4.          * 当坐标改变时触发此函数,如果Provider传进相同的坐标,它就不会被触发
  5.          *
  6.          * @param location 坐标
  7.          */
  8.         @Override
  9.         public void onLocationChanged(Location location) {
  10.             if (mListener != null) {
  11.                 mListener.onLocationChanged(location);
  12.             }
  13.         }
  14.         /**
  15.          * provider的在可用、暂时不可用和无服务三个状态直接切换时触发此函数
  16.          *
  17.          * @param provider 提供者
  18.          * @param status   状态
  19.          * @param extras   provider可选包
  20.          */
  21.         @Override
  22.         public void onStatusChanged(String provider, int status, Bundle extras) {
  23.             if (mListener != null) {
  24.                 mListener.onStatusChanged(provider, status, extras);
  25.             }
  26.             switch (status) {
  27.                 case LocationProvider.AVAILABLE:
  28.                     Log.d("onStatusChanged", "当前GPS状态为可见状态");
  29.                     break;
  30.                 case LocationProvider.OUT_OF_SERVICE:
  31.                     Log.d("onStatusChanged", "当前GPS状态为服务区外状态");
  32.                     break;
  33.                 case LocationProvider.TEMPORARILY_UNAVAILABLE:
  34.                     Log.d("onStatusChanged", "当前GPS状态为暂停服务状态");
  35.                     break;
  36.             }
  37.         }
  38.         /**
  39.          * provider被enable时触发此函数,比如GPS被打开
  40.          */
  41.         @Override
  42.         public void onProviderEnabled(String provider) {
  43.             if (mListener != null) {
  44.                 mListener.onProviderEnabled(provider);
  45.             }
  46.         }
  47.         /**
  48.          * provider被disable时触发此函数,比如GPS被关闭
  49.          */
  50.         @Override
  51.         public void onProviderDisabled(String provider) {
  52.             if (mListener != null) {
  53.                 mListener.onProviderDisabled(provider);
  54.             }
  55.         }
  56.     }
复制代码
在获取到经纬度之后,将其转化为详细地址形貌。
Geocoder 用于获取地理位置的前向编码和反向编码,其中反向编码是根据经纬度获取对应的详细地址。Geocoder 请求的是一个背景服务,但是该服务不包罗在尺度android framework中。需要提前用Geocoder的isPresent()方法来判断当前装备是否包罗地理位置服务
  1.         /**
  2.      * 根据经纬度获取地理位置
  3.      *
  4.      * @param context   上下文
  5.      * @param latitude  纬度
  6.      * @param longitude 经度
  7.      * @return {@link Address}
  8.      */
  9.     public static Address getAddress(Context context, double latitude, double longitude) {
  10.         Geocoder geocoder = new Geocoder(context, Locale.getDefault());
  11.         try {
  12.             List<Address> addresses = geocoder.getFromLocation(latitude, longitude, 1);
  13.             if (addresses.size() > 0) return addresses.get(0);
  14.         } catch (IOException e) {
  15.             e.printStackTrace();
  16.         }
  17.         return null;
  18.     }
复制代码
这里返回的位置信息是一个集合Address,其中Address类中包罗了各种地理位置信息,包罗经纬度,国家,都会,地区,街道,国家编码,都会编码等等,根据自己需求选择。
这里有一个留意点:Geocoder获取位置信息是一个背景的耗时操作,可能导致详细地址一开始获取不到无法显示出来,这里就需要异步线程的方式来请求服务,避免阻塞主线程
4. 方法调用

在activity中利用
  1.         override fun onCreate(savedInstanceState: Bundle?) {
  2.         super.onCreate(savedInstanceState)
  3.         setContentView(R.layout.punch_main_activity)
  4.         
  5.         tv_refresh.setOnClickListener {
  6.             refresh()
  7.         }
  8.         rl_button_bottom.setOnClickListener {
  9.             RxLocationUtils.openGpsSettings(this)
  10.         }
  11.         ll_clock.setOnClickListener {
  12.             //处理打卡逻辑
  13.         }
  14.     }
  15.         override fun onResume() {
  16.         super.onResume()
  17.         window.transparentStatusBar()
  18.         refresh()
  19.     }
  20.         fun refresh(){
  21.         if(!RxLocationUtils.register(this,30*1000,1,this)){
  22.             setClockClick(false)
  23.             tv_location.text="定位失败"
  24.             tv_distance.text=""
  25.         }
  26.     }
  27.     private fun setClockClick(isClick:Boolean){
  28.         if(isClick){
  29.             ll_clock.isClickable=true
  30.             ll_clock.isEnabled=true
  31.             tv_clock.text="拍照打卡"
  32.         }else{
  33.             ll_clock.isClickable=false
  34.             ll_clock.isEnabled=false
  35.             tv_clock.text="无法打卡"
  36.         }
  37.         if(RxLocationUtils.isLocationEnabled(this)){
  38.             rl_button_bottom.visibility= View.GONE
  39.         }else{
  40.             rl_button_bottom.visibility= View.VISIBLE
  41.         }
  42.     }
  43.     override fun getLastKnownLocation(location: Location?) {
  44.         location?.let { updateLocation(location) }
  45.     }
  46.     override fun onLocationChanged(location: Location) {
  47.         updateLocation(location)
  48.     }
  49.     override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
  50.     }
  51.     override fun onProviderEnabled(provider: String?) {
  52.         refresh()
  53.     }
  54.     override fun onProviderDisabled(provider: String?) {
  55.         setClockClick(false)
  56.         tv_location.text="定位失败,请打开GPS定位"
  57.         tv_distance.text=""
  58.         RxToast.showToast(this, "无法定位,请打开定位服务", 500)
  59.     }
复制代码
当gps关闭时跳转到打开gps的体系页面,重写监听器方法
获取到经纬度后处理惩罚,通过Handler异步处理惩罚地址:
  1.         //位置描述
  2.         var locationDes=""
  3.         //目标经纬度
  4.         var dest_latitude=39.948047
  5.     var dest_longitude=116.360548
  6.     //最大可打卡距离 200m内
  7.     var clockDistance:Int=200
  8.     //安卓8 获取地址有明显的时延,合理的方式是在工作线程中处理GeoCoder
  9.     private val uiCallback by lazy {
  10.         object : Handler(Looper.getMainLooper()) {
  11.             override fun handleMessage(msg: Message) {
  12.                 tv_location.text=locationDes
  13.             }
  14.         }
  15.     }
  16.         private fun updateLocation(location: Location){
  17.         // 获取当前纬度
  18.         val latitude = location.latitude
  19.         // 获取当前经度
  20.         val longitude = location.longitude
  21.         // 获取经纬度对于的位置,getFromLocation(纬度, 经度, 最多获取的位置数量)
  22.         // 得到第一个经纬度位置解析信息
  23.         // Address里面还有很多方法。比如具体省的名称、市的名称...
  24.         val gps = RxLocationUtils.GPS84ToBD09(latitude,longitude)
  25.         val distance = RxLocationUtils.getDistance(gps.wgLon,gps.wgLat,dest_longitude,dest_latitude)
  26.         if(distance.toInt()<clockDistance){
  27.             setClockClick(true)
  28.             locationDes= "已进入考勤范围:"
  29.         }else{
  30.             setClockClick(false)
  31.             locationDes= "未进入考勤范围:"
  32.         }
  33.         findLocation(latitude,longitude)
  34.         tv_distance.text="当前打卡距离:${distance.toInt()}m (${clockDistance}m以内打卡)"
  35.     }
  36.     private fun findLocation(latitude: Double, longitude: Double){
  37.         Thread{
  38.             locationDes+=if(RxLocationUtils.getFeature(this,latitude,longitude).isNullOrEmpty()) "定位正在加载中..." else  RxLocationUtils.getFeature(this,latitude,longitude) // 获取街道
  39.             uiCallback.sendEmptyMessage(0)
  40.         }.start()
  41.     }
复制代码
5. 坐标转换

由于我这里的目标打卡地利用的是百度坐标系的经纬度,以是盘算距离之前需要举行坐标转换,gps84要转到BD-09得经过两次转换处理惩罚:GPS85->GCJ-02->BD-09
  1.         /**
  2.      * 国际 GPS84 坐标系
  3.      * 转换成
  4.      * [国测局坐标系] 火星坐标系 (GCJ-02)
  5.      * <p>
  6.      * World Geodetic System ==> Mars Geodetic System
  7.      *
  8.      * @param lon 经度
  9.      * @param lat 纬度
  10.      * @return GPS实体类
  11.      */
  12.     public static Gps GPS84ToGCJ02(double lat, double lon) {
  13.         if (outOfChina(lat, lon)) {
  14.             return null;
  15.         }
  16.         double dLat = transformLat(lon - 105.0, lat - 35.0);
  17.         double dLon = transformLon(lon - 105.0, lat - 35.0);
  18.         double radLat = lat / 180.0 * pi;
  19.         double magic = Math.sin(radLat);
  20.         magic = 1 - ee * magic * magic;
  21.         double sqrtMagic = Math.sqrt(magic);
  22.         dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
  23.         dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
  24.         double mgLat = lat + dLat;
  25.         double mgLon = lon + dLon;
  26.         return new Gps(mgLat, mgLon);
  27.     }
  28.         /**
  29.      * 火星坐标系 (GCJ-02)
  30.      * 转换成
  31.      * 百度坐标系 (BD-09)
  32.      *
  33.      * @param gg_lon 经度
  34.      * @param gg_lat 纬度
  35.      */
  36.     public static Gps GCJ02ToBD09(double gg_lat, double gg_lon) {
  37.         double x = gg_lon, y = gg_lat;
  38.         double z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * pi);
  39.         double theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * pi);
  40.         double bd_lon = z * Math.cos(theta) + 0.0065;
  41.         double bd_lat = z * Math.sin(theta) + 0.006;
  42.         return new Gps(bd_lat, bd_lon);
  43.     }
  44.         /**
  45.      * 国际 GPS84 坐标系
  46.      * 转换成
  47.      * 百度坐标系 (BD-09)
  48.      *
  49.      * @param lon 经度
  50.      * @param lat 纬度
  51.      */
  52.     public static Gps GPS84ToBD09(double lat, double lon) {
  53.         Gps gps = GPS84ToGCJ02(lat,lon);
  54.         if (gps == null) {
  55.             return new Gps(lat,lon);
  56.         }
  57.         //GCJ-02 转 BD-09
  58.         return GCJ02ToBD09(gps.getWgLat(), gps.getWgLon());
  59.     }
复制代码
6. 距离盘算

两个地理位置之间的直线距离通过Haversine法去盘算,Haversine公式是一种比勾股定理法(将地球表面直接看作平面)更精确的算法,它思量了地球的球形结构。该算法的基本头脑是将两个坐标点之间的距离看作地球表面上的一段弧长,然后根据球面三角形的定理盘算弧长。Haversine公式的公式如下:

其中,R分为这几类:地球赤道半径6378千米,南北极半径6357千米,均匀半径6371千米。这里用选用赤道半径。
  1.         private static final double EARTH_RADIUS = 6378137.0; //地球半径
  2.        
  3.     /**
  4.      * 计算两个经纬度之间的距离
  5.      *
  6.      * @param longitude
  7.      * @param latitude
  8.      * @param longitude2
  9.      * @param latitude2
  10.      * @return 单位米
  11.      */
  12.     public static double getDistance(double longitude, double latitude, double longitude2, double latitude2) {
  13.         double lat1 = rad(latitude);
  14.         double lat2 = rad(latitude2);
  15.         double a = lat1 - lat2;
  16.         double b = rad(longitude) - rad(longitude2);
  17.         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)));
  18.         s = s * EARTH_RADIUS;
  19.         s = Math.round(s * 10000) / 10000; //四舍五入
  20.         return s;
  21.     }
  22.     /**
  23.      * 弧度换为角度
  24.      * @param d
  25.      * @return
  26.      */
  27.     private static double rad(double d) {
  28.         return d * Math.PI / 180.0;
  29.     }
复制代码
7. 完备代码

RxLocationUtils:
  1. /** * @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) {
  2.         if (listener == null) return false;
  3.         if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
  4.             ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
  5.             ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1);
  6.             return false;
  7.         }
  8.         mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
  9.         mListener = listener;
  10.         if (!isLocationEnabled(context)) {
  11.             RxToast.showToast(context, "无法定位,请打开定位服务", 500);
  12.             return false;
  13.         }
  14.         String provider = mLocationManager.getBestProvider(getCriteria(), true);
  15.         Location location = mLocationManager.getLastKnownLocation(provider);
  16.         if (location != null) listener.getLastKnownLocation(location);
  17.         if (myLocationListener == null) myLocationListener = new MyLocationListener();
  18.         mLocationManager.requestLocationUpdates(provider, minTime, minDistance, myLocationListener);
  19.         return true;
  20.     }
  21.     /**     * 注销     */    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-09        return 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 MyLocationListener
  22.             implements LocationListener {
  23.         /**
  24.          * 当坐标改变时触发此函数,如果Provider传进相同的坐标,它就不会被触发
  25.          *
  26.          * @param location 坐标
  27.          */
  28.         @Override
  29.         public void onLocationChanged(Location location) {
  30.             if (mListener != null) {
  31.                 mListener.onLocationChanged(location);
  32.             }
  33.         }
  34.         /**
  35.          * provider的在可用、暂时不可用和无服务三个状态直接切换时触发此函数
  36.          *
  37.          * @param provider 提供者
  38.          * @param status   状态
  39.          * @param extras   provider可选包
  40.          */
  41.         @Override
  42.         public void onStatusChanged(String provider, int status, Bundle extras) {
  43.             if (mListener != null) {
  44.                 mListener.onStatusChanged(provider, status, extras);
  45.             }
  46.             switch (status) {
  47.                 case LocationProvider.AVAILABLE:
  48.                     Log.d("onStatusChanged", "当前GPS状态为可见状态");
  49.                     break;
  50.                 case LocationProvider.OUT_OF_SERVICE:
  51.                     Log.d("onStatusChanged", "当前GPS状态为服务区外状态");
  52.                     break;
  53.                 case LocationProvider.TEMPORARILY_UNAVAILABLE:
  54.                     Log.d("onStatusChanged", "当前GPS状态为暂停服务状态");
  55.                     break;
  56.             }
  57.         }
  58.         /**
  59.          * provider被enable时触发此函数,比如GPS被打开
  60.          */
  61.         @Override
  62.         public void onProviderEnabled(String provider) {
  63.             if (mListener != null) {
  64.                 mListener.onProviderEnabled(provider);
  65.             }
  66.         }
  67.         /**
  68.          * provider被disable时触发此函数,比如GPS被关闭
  69.          */
  70.         @Override
  71.         public void onProviderDisabled(String provider) {
  72.             if (mListener != null) {
  73.                 mListener.onProviderDisabled(provider);
  74.             }
  75.         }
  76.     }
  77.     //===========================================坐标转换工具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:
  1. class ClockActivity: AppCompatActivity(),RxLocationUtils.OnLocationChangeListener {
  2.     //位置描述
  3.         var locationDes=""
  4.         //目标经纬度
  5.         var dest_latitude=39.948047
  6.     var dest_longitude=116.360548
  7.     //最大可打卡距离 200m内
  8.     var clockDistance:Int=200
  9.     //安卓8 获取地址有明显的时延,合理的方式是在工作线程中处理GeoCoder
  10.     private val uiCallback by lazy {
  11.         object : Handler(Looper.getMainLooper()) {
  12.             override fun handleMessage(msg: Message) {
  13.                 tv_location.text=locationDes
  14.             }
  15.         }
  16.     }
  17.     override fun onCreate(savedInstanceState: Bundle?) {
  18.         super.onCreate(savedInstanceState)
  19.         setContentView(R.layout.punch_main_activity)
  20.         tv_refresh.setOnClickListener {
  21.             refresh()
  22.         }
  23.         rl_button_bottom.setOnClickListener {
  24.             RxLocationUtils.openGpsSettings(this)
  25.         }
  26.         ll_clock.setOnClickListener {
  27.             //处理打卡逻辑
  28.         }
  29.     }
  30.     private fun setClockClick(isClick:Boolean){
  31.         if(isClick){
  32.             ll_clock.isClickable=true
  33.             ll_clock.isEnabled=true
  34.             tv_clock.text="拍照打卡"
  35.         }else{
  36.             ll_clock.isClickable=false
  37.             ll_clock.isEnabled=false
  38.             tv_clock.text="无法打卡"
  39.         }
  40.         if(RxLocationUtils.isLocationEnabled(this)){
  41.             rl_button_bottom.visibility= View.GONE
  42.         }else{
  43.             rl_button_bottom.visibility= View.VISIBLE
  44.         }
  45.     }
  46.     override fun onResume() {
  47.         super.onResume()
  48.         window.transparentStatusBar()
  49.         refresh()
  50.     }
  51.     override fun onDestroy() {
  52.         super.onDestroy()
  53.         //记得销毁
  54.         RxLocationUtils.unregister()
  55.     }
  56.     fun refresh(){
  57.         if(!RxLocationUtils.register(this,30*1000,1,this)){
  58.             setClockClick(false)
  59.             tv_location.text="定位失败"
  60.             tv_distance.text=""
  61.         }
  62.     }
  63.     override fun getLastKnownLocation(location: Location?) {
  64.         location?.let { updateLocation(location) }
  65.     }
  66.     override fun onLocationChanged(location: Location) {
  67.         updateLocation(location)
  68.     }
  69.     override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
  70.     }
  71.     override fun onProviderEnabled(provider: String?) {
  72.         refresh()
  73.     }
  74.     override fun onProviderDisabled(provider: String?) {
  75.         setClockClick(false)
  76.         tv_location.text="定位失败,请打开GPS定位"
  77.         tv_distance.text=""
  78.         RxToast.showToast(this, "无法定位,请打开定位服务", 500)
  79.     }
  80.     private fun updateLocation(location: Location){
  81.         // 获取当前纬度
  82.         val latitude = location.latitude
  83.         // 获取当前经度
  84.         val longitude = location.longitude
  85.         // 获取经纬度对于的位置,getFromLocation(纬度, 经度, 最多获取的位置数量)
  86.         // 得到第一个经纬度位置解析信息
  87.         // Address里面还有很多方法。比如具体省的名称、市的名称...
  88.         val gps = RxLocationUtils.GPS84ToBD09(latitude,longitude)
  89.         val distance= RxLocationUtils.getDistance(gps.wgLon,gps.wgLat,dest_longitude,dest_latitude)
  90.         if(distance.toInt()<clockDistance){
  91.             setClockClick(true)
  92.             locationDes= "已进入考勤范围:"
  93.         }else{
  94.             setClockClick(false)
  95.             locationDes= "未进入考勤范围:"
  96.         }
  97.         findLocation(latitude,longitude)
  98.         tv_distance.text="当前打卡距离:${distance.toInt()}m (${clockDistance}m以内打卡)"
  99.     }
  100.     private fun findLocation(latitude: Double, longitude: Double){
  101.         Thread{
  102.             locationDes+=if(RxLocationUtils.getFeature(this,latitude,longitude).isNullOrEmpty()) "定位正在加载中..." else  RxLocationUtils.getFeature(this,latitude,longitude) // 获取街道
  103.             uiCallback.sendEmptyMessage(0)
  104.         }.start()
  105.     }
  106. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

民工心事

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表