【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">

    <LinearLayout
        android: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">

        <LinearLayout
            android: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">
            <TextView
                android:id="@+id/tv_clock"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@android:color/white"
                android:textSize="20sp"
                android:text="拍照打卡"
                />
            <TextClock
                android: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>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_marginTop="10dp"
            android:paddingStart="10dp"
            android:paddingEnd="10dp"
            android:orientation="horizontal"
            android:layout_height="wrap_content">

            <TextView
                android: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>


        <TextView
            android:id="@+id/tv_distance"
            android:layout_marginTop="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14sp"
            android:text=""/>

        <TextView
            android: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>

    <RelativeLayout
        android: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">
        <TextView
            android: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"/>
        <ImageView
            android: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 MyLocationListener
            implements LocationListener {
        /**
         * 当坐标改变时触发此函数,如果Provider传进相同的坐标,它就不会被触发
         *
         * @param location 坐标
         */
        @Override
        public void onLocationChanged(Location location) {
            if (mListener != null) {
                mListener.onLocationChanged(location);
            }
        }

        /**
         * provider的在可用、暂时不可用和无服务三个状态直接切换时触发此函数
         *
         * @param provider 提供者
         * @param status   状态
         * @param extras   provider可选包
         */
        @Override
        public 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被打开
         */
        @Override
        public void onProviderEnabled(String provider) {
            if (mListener != null) {
                mListener.onProviderEnabled(provider);
            }
        }

        /**
         * provider被disable时触发此函数,比如GPS被关闭
         */
        @Override
        public 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=true
            ll_clock.isEnabled=true
            tv_clock.text="拍照打卡"
        }else{
            ll_clock.isClickable=false
            ll_clock.isEnabled=false
            tv_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.948047
    var dest_longitude=116.360548
    //最大可打卡距离 200m内
    var clockDistance:Int=200

    //安卓8 获取地址有明显的时延,合理的方式是在工作线程中处理GeoCoder
    private 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-09
        return 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-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
            implements LocationListener {
        /**
         * 当坐标改变时触发此函数,如果Provider传进相同的坐标,它就不会被触发
         *
         * @param location 坐标
         */
        @Override
        public void onLocationChanged(Location location) {
            if (mListener != null) {
                mListener.onLocationChanged(location);
            }
        }

        /**
         * provider的在可用、暂时不可用和无服务三个状态直接切换时触发此函数
         *
         * @param provider 提供者
         * @param status   状态
         * @param extras   provider可选包
         */
        @Override
        public 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被打开
         */
        @Override
        public void onProviderEnabled(String provider) {
            if (mListener != null) {
                mListener.onProviderEnabled(provider);
            }
        }

        /**
         * provider被disable时触发此函数,比如GPS被关闭
         */
        @Override
        public 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.948047
    var dest_longitude=116.360548
    //最大可打卡距离 200m内
    var clockDistance:Int=200

    //安卓8 获取地址有明显的时延,合理的方式是在工作线程中处理GeoCoder
    private 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=true
            ll_clock.isEnabled=true
            tv_clock.text="拍照打卡"
        }else{
            ll_clock.isClickable=false
            ll_clock.isEnabled=false
            tv_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()

    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

sdwan是硬件还是网络协议?

SD-WAN&#xff08;Software-Defined Wide Area Network&#xff0c;软件定义广域网&#xff09;并不是一个硬件产品或单一的网络协议&#xff0c;而是结合了软件、硬件和网络技术的一种解决方案。SD-WAN的核心在于其软件定义的特性&#xff0c;它通过软件来控制和管理广域网的…

如何压缩pdf文件大小,怎么压缩pdf文件大小

在数字化时代&#xff0c;pdf文件因其稳定的格式和跨平台兼容性&#xff0c;成为了工作与学习中不可或缺的一部分。然而&#xff0c;随着pdf文件内容的丰富&#xff0c;pdf文件的体积也随之增大&#xff0c;给传输和存储带来了不少挑战。本文将深入探讨如何高效压缩pdf文件大小…

@RequestPart 与 @RequestBody、@RequestParam 注解的异同点

前言 RequestPart 注解是我们在JavaEE 开发中&#xff0c;比较常见的一个注解。它经常会与 RequestBody 、RequestParam 注解进行比较&#xff0c;这篇博文我们以案例和源码相结合&#xff0c;分析这几个注解的异同点。 案例演示 创建实体类 User Data NoArgsConstructor A…

Python requests爬虫

Python的requests库是一个强大且易于使用的HTTP库&#xff0c;用于发送HTTP请求和处理响应。它是Python中最受欢迎的网络爬虫框架之一&#xff0c;被广泛用于从网页中提取数据、爬取网站和进行API调用。 使用requests库&#xff0c;你可以轻松地发送各种HTTP请求&#xff0c;包…

提示词工程(Prompt Engineering)是什么?

一、定义 Prompt Engineering 提示词工程&#xff08;Prompt Engineering&#xff09;是一项通过优化提示词&#xff08;Prompt&#xff09;和生成策略&#xff0c;从而获得更好的模型返回结果的工程技术。 二、System message 系统指令 System message可以被广泛应用在&am…

linux自动化内存监控与告警

文章目录 前言一、脚本实现1. shell脚本实现2. 脚本功能概览 二、设置定时执行1. 编辑cron任务表2. 设置定时任务 三、通知结果示例总结 前言 在当今数字化与网络化日益普及的时代&#xff0c;系统管理与维护成为了确保业务连续性和数据安全的关键环节。其中&#xff0c;监控系…

大模型时代:人工智能与大数据平台的深度融合

在当今的大数据时代&#xff0c;数据已经成为驱动业务增长和创新的关键因素。与此同时&#xff0c;随着人工智能技术的不断进步&#xff0c;AI在大规模数据处理和分析方面的能力日益强大。因此&#xff0c;将人工智能与大数据平台相结合&#xff0c;可以为企业带来巨大的商业价…

linux信息收集与提权

目录 版本信息收集 kali得一些exp网站 kali自带的searchsploit工具 脏牛提权漏洞&#xff08;改写没有写权限的文件&#xff09; 测试靶场下载链接 sudo提权 上传恶意C脚本进行编译生成dirty的elf文件&#xff0c;也可以在攻击机编译好上传 启动&#xff0c;123456是设…

网站地址显示不安全怎么办

当网址栏显示不安全时&#xff0c;通常是因为网站使用的是HTTP而不是HTTPS协议&#xff0c;或者因为网站的SSL证书存在问题。以下是一些解决方法&#xff1a;1、迁移到HTTPS&#xff1a;如果您是网站所有者&#xff0c;最好的解决方法是将网站迁移到HTTPS。HTTPS通过使用SSL/TL…

室内精准定位是什么?室内精准定位的方式有哪些?

说到室内精准定位很多人可能会比较陌生&#xff0c;因为这一说法并没有大范围推广&#xff0c;又或者说只是很多相关行业的人才知道这样的说法。但是定位这一问题大家都知道吧&#xff1f;尤其是要到一个地方去&#xff0c;都会进行定位导航。那么这一般都是户外定位&#xff0…

案例分享:Qt modbusTcp调试工具(读写Byte、Int、DInt、Real、DReal)(当前v1.0.0)

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://blog.csdn.net/qq21497936/article/details/140313789 红胖子(红模仿)的博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片…

哈喽GPT-4o,程序员如何通过GPT-4o提高办公效率

目录 一、编写工作汇报Prompt&#xff1a;我是一名Java开发工程师&#xff0c;请写一份工作总结&#xff0c;工作内容是一个SpringBootVue实现的图书管理系统&#xff0c;按下面的结构来撰写&#xff1a;1. 工作背景&#xff1b;2. 工作内容&#xff1b;3. 工作建议&#xff1b…

fastadmin框架后台列表固定第一行列表固定头部

在列表中,如果列表字段很多,并且每页数量很多,往下拉的时候就不好辨别数据是哪个字段的,对用户造成不好的浏览体验。 通过以下方法,可以实现将列表的第一行,也就是头部,固定在第一行显示,这样就能轻松辨别每个数据对应是哪个字段的,增加用户的使用体验。 打开项目的…

【漏洞复现】WordPress插件Recall CVE-2024-32709 SQL注入漏洞

0x01 产品简介 WordPress是一款免费开源的内容管理系统(CMS)&#xff0c;最初是一个博客平台&#xff0c;但后来发展成为一个功能强大的网站建设工具&#xff0c;适用于各种类型的网站&#xff0c;包括个人博客、企业网站、电子商务网站等&#xff0c;并逐步演化成一款内容管理…

PHP酒店宾馆民宿多商户版系统小程序源码

解锁酒店新境界&#xff01;揭秘多商户版系统的无限可能&#x1f3e8;✨ &#x1f680; 开篇&#xff1a;酒店业的新革命&#xff0c;多商户版系统来袭&#xff01; 你是否梦想过将你的酒店打造成一个集餐饮、娱乐、购物于一体的综合型休闲空间&#xff1f;现在&#xff0c;这…

传统剪纸遇上AI绘画:一场跨时代的艺术对话

本文由 ChatMoney团队出品 剪纸&#xff0c;听起来就很有画面感&#xff0c;承载着中国几千年的文化。一把剪刀、一张红纸&#xff0c;轻轻剪几剪&#xff0c;就能幻化出各种栩栩如生的图案。这门艺术不仅仅是视觉上的享受&#xff0c;更是一种感情的传递&#xff0c;一种文化的…

Objective-C 中的 isa 不再是简单的结构体指针

了解 Objective-C 中的 isa 指针内存结构 在 Objective-C 中&#xff0c;isa 指针是对象和类之间的重要桥梁。它不仅帮助运行时系统识别对象的类型&#xff0c;还参与了一些内存和性能优化。本文将深入讲解 isa 指针的内存结构&#xff0c;包括其在早期和现代实现中的演变。 …

【3】A-Frame上手开发

一、JavaScript、事件、DOM API 由于 A-Frame 只是 HTML&#xff0c;因此我们可以使用 JavaScript 和 DOM API 来控制场景及其实体&#xff0c;就像我们在普通 Web 开发中所做的那样。 场景中的每个元素&#xff0c;甚至诸如 <a-box> 或 <a-sky> 之类的元素&…

SpringBootV12和mybatis全部知识点

框架: 快速开发项目的一个架子 ssh ssm spring --> applicationContext.xml配置文件(spring不是业务层,是管理其他框架的) springmvc --> springmvc.xml配置文件 (对应之前servlet) mybatis —> mybatis-config.xml配置文件(对应之前jdbc) —> springboot优化…

平凉崆峒区特产美食亮相兰洽会文旅康养展馆

在第三十届兰洽会的文旅康养展馆中&#xff0c;平凉的特产无疑是一道亮丽的风景线。作为促进区域经济合作与交流的重要平台&#xff0c;每年都吸引着众多企业和特色产品的亮相。 走进这个充满活力与特色的展馆&#xff0c;首先映入眼帘的是琳琅满目的特色产品。平凉红牛的…