前言

在做无人机项目时分,有一个功用是,手机制作雷达图,将人与飞机的方位显现在雷达图中,在图中,飞机是点,人是箭头,当人回身时分,地图跟着旋转,当人朝向飞机的时分,箭头指向飞机方位。当时感觉开发比较复杂,就去掉该功用。现在想假如真要做该怎样完成,该功用作用图如下:

Android自定义控件之雷达地图(飞机与人员位置显示)

参阅博客

屏幕坐标系转化为数学坐标系
两点经纬度获取与正北方向夹角

注意

1.需求在设置>运用>运用管理,给运用敞开方位权限才能获取到经纬度
2.原生方法获取经纬度不太准确
3.下面功用中,获取两点经纬度间隔与正北方向夹角会有差错。

功用剖析

1.制作雷达图网格
2.人员箭头
3.飞机图标点制作
4.手机经纬度,方向角获取

功用1:制作雷达图网格比较简单,在画布制作圆与线段即可。

功用2:人员箭头,箭头方向指向手机顶部,当人员回身超越必定视点时箭头细微颤抖,颤抖作用能够运用ObjectAnimator动画对箭头图片进行操作。

功用3:飞机图标点制作,比较复杂的一点是,怎样确认飞机制作点方位,当获取到飞机的经纬度怎样转换为屏幕上方位。现在思路是,以人员方位作为屏幕的中点,先获取两个点经纬度的间隔,之后连接两点做线段,获取该线段与正北方向夹角,之后能够经过sin与cos函数,以及显现间隔与屏幕像素点对应份额,计算出屏幕上xy值,从而获取飞机在屏幕的制作点。因为无人机的飞翔间隔一般在5km左右,且不要求高精度,现在想到这种完成方法(个人感觉直接运用第三方地图服务会精准许多,但当时需求想完成这种方法,就很难受,尤其让自己来处理经纬度来进行换算很头疼)。关于人员回身飞机点旋转,完成想法是运用方向传感器获取手机的方向角,当人回身时,依据方向视点数,来旋转画布,从而完成地图上飞机点移动。

代码完成

完好代码在Github上,下面介绍的代码选自关于功用的主要部分。

制作雷达图网格

雷达图网格制作,是在画布中制作调用画布drawCircle和drawLine办法制作,圆圈与直线。(关于代码中centerX,centerY这种变量放在后边完好代码中展示。这儿是功用完成主要代码),第一个drawCircle制作灰色实心圆背景,for循环里边drawCircle制作白灰色的圆环。drawLine第一个制作竖线,第二个横线。至此雷达图网格制作好了

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //制作雷达图线
        canvas.drawCircle(centerX, centerY, radiusUnit * CIRCLE_NUM, bgPaint);
        for (int i = 1; i <= CIRCLE_NUM; i++) {
            canvas.drawCircle(centerX, centerY, radiusUnit * i - LINE_WIDTH/2, linePaint);
        }
        canvas.drawLine(centerX, 0, centerX, viewHeight, linePaint);
        canvas.drawLine(0, centerY, viewWidth, centerY, linePaint);
       //飞机点制作
       //...
    }

人员箭头

因为人员箭头想完成手机滚动大于必定视点时分微微颤抖的功用,直接在View的onDraw办法内欠好时分,后边采用组合控件的办法经过约束布局使箭头在布局中居中,后边经过ObjectAnimator给箭头图片增加动画作用。 给箭头图片增加动画代码如下:

public void setNavDirectionDegree(float degree) {
    if (imgAnimator == null || !imgAnimator.isRunning()) {
        imgAnimator = ObjectAnimator.ofFloat(imgNavDirection, "rotation", degree, -degree*2,degree);
        imgAnimator.setDuration(100);
        imgAnimator.start();
    }
}

上面代码中imgNavDirection指的是箭头图片控件。关于手机转向超越必定视点,是经过获取方向角数据完成的,方向角获取代码在飞机图标点制作功用介绍中。

飞机图标点制作

前面说到飞机图标点制作难点在于怎样将经纬度转换为屏幕坐标,关于完成的思路,在前面写的功用剖析的功用3部分。剖析里边个人感觉关键点在于,怎样获取两点经纬度的间隔,怎样依据两点经纬度得出与正北方向的夹角。受制于自身能力,现在不懂地图经纬度专业知识(搜索了下相关的专业是地理信息系统专业GIS),依据上述功用剖析里边思路完成会与现实国际中有差错。 关于间隔与夹角的获取是从网上搜索到的办法。经纬度转换的办法如下:

/**
 * 经纬度操作相关工具类
 */
public class GeoUtils {
    private static final double EARTH_RADIUS = 6371; // 地球半径,单位:千米
    /**
     * 计算两点间间隔,返回单位km,有差错
     */
    public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
        double dLat = Math.toRadians(lat2 - lat1);
        double dLon = Math.toRadians(lon2 - lon1);
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
                        Math.sin(dLon / 2) * Math.sin(dLon / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        double distance = EARTH_RADIUS * c;
        return distance;
    }
    /**
     * 获取AB连线与正北方向的视点
     *
     * @param A A点的经纬度
     * @param B B点的经纬度
     * @return AB连线与正北方向的视点(0~360)
     */
    public static double getAngle(MyLatLng A, MyLatLng B) {
        double dx = (B.m_RadLo - A.m_RadLo) * A.Ed;
        double dy = (B.m_RadLa - A.m_RadLa) * A.Ec;
        double angle = 0.0;
        angle = Math.atan(Math.abs(dx / dy)) * 180. / Math.PI;
        double dLo = B.m_Longitude - A.m_Longitude;
        double dLa = B.m_Latitude - A.m_Latitude;
        if (dLo > 0 && dLa <= 0) {
            angle = (90. - angle) + 90;
        } else if (dLo <= 0 && dLa < 0) {
            angle = angle + 180.;
        } else if (dLo < 0 && dLa >= 0) {
            angle = (90. - angle) + 270;
        }
        return angle;
    }
    public static class MyLatLng {
        final static double Rc = 6378137;
        final static double Rj = 6356725;
        double m_LoDeg, m_LoMin, m_LoSec;
        double m_LaDeg, m_LaMin, m_LaSec;
        double m_Longitude, m_Latitude;
        double m_RadLo, m_RadLa;
        double Ec;
        double Ed;
        public MyLatLng(double longitude, double latitude) {
            m_LoDeg = (int) longitude;
            m_LoMin = (int) ((longitude - m_LoDeg) * 60);
            m_LoSec = (longitude - m_LoDeg - m_LoMin / 60.) * 3600;
            m_LaDeg = (int) latitude;
            m_LaMin = (int) ((latitude - m_LaDeg) * 60);
            m_LaSec = (latitude - m_LaDeg - m_LaMin / 60.) * 3600;
            m_Longitude = longitude;
            m_Latitude = latitude;
            m_RadLo = longitude * Math.PI / 180.;
            m_RadLa = latitude * Math.PI / 180.;
            Ec = Rj + (Rc - Rj) * (90. - m_Latitude) / 90.;
            Ed = Ec * Math.cos(m_RadLa);
        }
    }
}

关于两点经纬度间间隔与正北方向夹角计算原理不懂,还请大佬能够指点下。
得到两点经纬度间间隔,与正北方向夹角后,能够在View上进行制作了。在制作时分需求注意下,为了便利飞机点制作,咱们在制作完雷达图的网格后,需求对坐标系进行操作,将View坐标系变为以View中心为原点的数学坐标系(数学坐标系便是y轴正向向上,x轴正方向向右)。这儿我是在onDraw()办法中进行操作,代码如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //制作雷达图线
    canvas.drawCircle(centerX, centerY, radiusUnit * CIRCLE_NUM, bgPaint);
    for (int i = 1; i <= CIRCLE_NUM; i++) {
        canvas.drawCircle(centerX, centerY, radiusUnit * i - LINE_WIDTH/2, linePaint);
    }
    canvas.drawLine(centerX, 0, centerX, viewHeight, linePaint);
    canvas.drawLine(0, centerY, viewWidth, centerY, linePaint);
    canvas.save();//画布保存
    //矩阵转换,变为数学坐标系
    canvas.translate(centerX,centerY);
    canvas.rotate(180);
    canvas.scale(-1,1);
    canvas.rotate(degree,0,0);//依据方向角,旋转画布,改动飞机对应方位
    //飞机点制作
    canvas.drawCircle(pointX,pointY,15,pointPaint);
    canvas.restore();//画布康复
}

关于左边转换能够分下面几步:
第一步:canvas.translate(centerX,centerY)将坐标系原点由View左上角移动到View中心。
第二步:canvas.rotate(180)顺时针旋转画布,将坐标系变换向上y轴为正,向左x轴为正。
第三步:canvas.scale(-1,1)改动View的缩放份额,这个办法对应为canvas.scale(x,y)。因此代码对应意思便是,关于x轴,方向取反,本来对应坐标值乘以1.0(相当于大小不变),对应y轴,方向不变,本来对应坐标值乘以1.0。整体来说,假如负数方向取反,假如正数,尺度 * 数值 = 改变后尺度。经过第三步,坐标系变为向上为y正半轴,向左为x正半轴(及数学坐标系)。

Android自定义控件之雷达地图(飞机与人员位置显示)
在View内有下面的代码,MAP_MAX指的是雷达图最大圆半径对应现实间隔(单位km),viewWidth是雷达图宽度,viewWidth / 2f便是雷达图最大圆半径。GeoUtils.getAngle()办法获取两经纬度视点。

    private final int MAP_MAX = 5;//雷达图最大显现规模,单位km,指的是雷达图半径对应多少km
    private float length = 0f;
    /** (lon1,lat1)手机经纬度 (lon2,lat2)飞机经纬度*/
    public void setLatLng(double lon1,double lat1, double lon2, double lat2) {
        double distance = GeoUtils.calculateDistance(lon1,lat1,lon2,lat2);
        length = (float) (distance / MAP_MAX * viewWidth / 2f);//按照km为单位换算  一格1km
        GeoUtils.MyLatLng point1 = new GeoUtils.MyLatLng(lon1,lat1);
        GeoUtils.MyLatLng point2 = new GeoUtils.MyLatLng(lon2,lat2);
        float angle = (float) GeoUtils.getAngle(point1,point2);
        pointX = length * (float) Math.sin(angle);
        pointY = length * (float) Math.cos(angle);
        postInvalidate();
    }

获取飞机制作点的方位原理是,在一个数学坐标系上,将手机的方位放在坐标系原点上,y正半轴为北,x正半轴为东,依据GeoUtils.getAngle()办法获取的视点,以y顺时针开始滚动对应视点,之后延伸两经纬度间制作长度(这个长度指,现实间隔对应在地图上制作的长度length),获取在坐标系中的对应的x,y值,可经过三角函数sin和cos获取。

经过上面的过程获取到是静态的雷达地图,想要完成手机滚动时分,地图跟着滚动的话,还需求完成方向角监听器来获取方向角,方向角取值规模[0,359),跟着度数增大,方向顺时针旋转。地图滚动逻辑如下图:

Android自定义控件之雷达地图(飞机与人员位置显示)
图中前两个指的是静态的雷达图,最终一个是想要的可滚动雷达图。静态的雷达图当手机旋转时分,能够看到地图没有改变,而动态雷达图,当手机旋转时分,地图会跟着手机方向角跟着旋转。例如上图中开始手机顶部对准北方,接着手机旋转到东方,此刻方向角为90度,图中第二个手机界面向第三个手机界面变换的话,需求地图逆时针旋转90度。对应地图旋转完成,能够运用canvas.rotate(degree,x,y)办法完成,第一个参数是顺时针旋转度数,第二个参数是旋转点x值,第三个参数是旋转点y值。运用canvas.rotate(degree)的话,旋转点是坐标系的原点。

但实际运用会发现,当方向角90度时分,地图要逆时针旋转90度,假如将坐标系转化为数学坐标系后调用canvas.rotate(-90,0,0)。会发现地图不是逆时针旋转而是顺时针。
造成这现象的原因是,在完成将View坐标转换为数学坐标的过程三中,运用了canvas.scale(-1,1)改动View的缩放份额。使得x方向与本来相反。这个办法改动x的方向。关于这个办法能够写一个View测验一下,用于测验的TestView代码如下:

public class TestView extends View {
    private Paint redPaint, greenPaint, blackPaint;
    public TestView(Context context) {
        super(context);
        init();
    }
    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    private void init() {
        redPaint = new Paint();
        redPaint.setColor(Color.RED);
        redPaint.setStrokeWidth(5);
        greenPaint = new Paint();
        greenPaint.setColor(Color.GREEN);
        greenPaint.setStrokeWidth(5);
        blackPaint = new Paint();
        blackPaint.setColor(Color.BLACK);
        blackPaint.setStrokeWidth(1);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.WHITE);
        float centerX = getWidth()/2;
        float centerY = getHeight()/2;
        //View坐标系转换为数学坐标系
        canvas.drawLine(0,centerY,getWidth(),centerY, blackPaint);
        canvas.drawLine(centerX,0,centerX,getHeight(), blackPaint);
        canvas.translate(centerX,centerY);
        canvas.rotate(180);
        //canvas.scale(-1,1);
        canvas.drawLine(0,0,0,50, redPaint);
        canvas.rotate(30,0,0);
        canvas.drawLine(0,0,0,50, greenPaint);
    }
}

下图中,第一个是注释掉canvas.scale(-1,1)作用,第二个是启用canvas.scale(-1,1)代码后的作用。

Android自定义控件之雷达地图(飞机与人员位置显示)
能够看到,没有canvas.scale()时,canvas.rotate(30,0,0)作用是顺时针,当履行canvas.scale(-1,1)后,canvas.rotate(30,0,0)看上去的作用是逆时针旋转30度。
关于这种现象,个人认为是,履行canvas.scale(-1,1)后,canvas.rotate(30,0,0)是顺时针30度(上图左一),但因为canvas.scale(-1,1)让x轴方向取反,让实际线段方位是顺时针旋转30度后按y轴对称制作出来的(上图左二),这样解说感觉不太好了解,暂时没有更好描绘,等待能够和大佬谈论下。

关于雷达地图完好View代码如下,代码中setDegree()办法是依据方向角设置地图旋转视点:

public class RadarView extends View {
    private final int CIRCLE_NUM = 5;//制作圆个数
    private final int LINE_WIDTH = 4;
    private final int MAP_MAX = 5;//雷达图最大显现规模,单位km,指的是雷达图半径对应多少km
    private Paint linePaint, bgPaint, pointPaint;
    private int defaultWidth = -1, defaultHeight = -1;
    private int viewWidth = -1, viewHeight = -1;
    private int radiusUnit = -1;//圆等分时分,单位半径,及最小圆半径
    private int centerX, centerY;
    private float length = 0f;
    private float pointX = 0f,pointY = 0f;
    private float degree;
    public RadarView(Context context) {
        super(context);
        init(context);
    }
    public RadarView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }
    public RadarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }
    private void init(Context context) {
        linePaint = new Paint();
        linePaint.setColor(Color.parseColor("#919191"));
        linePaint.setStrokeWidth(LINE_WIDTH);
        linePaint.setStyle(Paint.Style.STROKE);
        bgPaint = new Paint();
        bgPaint.setColor(Color.parseColor("#FF666666"));
        bgPaint.setStyle(Paint.Style.FILL);
        pointPaint = new Paint();
        pointPaint.setColor(Color.parseColor("#919191"));
        pointPaint.setStyle(Paint.Style.FILL);
        defaultWidth = DensityUtil.dp2px(context, 200);
        defaultHeight = DensityUtil.dp2px(context, 200);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int width, height;
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            width = defaultWidth;
        }
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            height = defaultHeight;
        }
        width = Math.min(width, height);
        height = width;
        setMeasuredDimension(width, height);
    }
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        viewWidth = w;
        viewHeight = h;
        radiusUnit = viewWidth / 2 / CIRCLE_NUM;
        centerX = viewWidth / 2;
        centerY = viewHeight / 2;
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //制作雷达图线
        canvas.drawCircle(centerX, centerY, radiusUnit * CIRCLE_NUM, bgPaint);
        for (int i = 1; i <= CIRCLE_NUM; i++) {
            canvas.drawCircle(centerX, centerY, radiusUnit * i - LINE_WIDTH/2, linePaint);
        }
        canvas.drawLine(centerX, 0, centerX, viewHeight, linePaint);
        canvas.drawLine(0, centerY, viewWidth, centerY, linePaint);
        canvas.save();
        //变为数学坐标系
        canvas.translate(centerX,centerY);
        canvas.rotate(180);
        canvas.scale(-1,1);
        canvas.rotate(degree,0,0);//手机滚动时分,点的方位
        //飞机点制作
        canvas.drawCircle(pointX,pointY,15,pointPaint);
        canvas.restore();
    }
    /** 点旋转视点 */
    public void setDegree(float degree){
        this.degree = degree;
        postInvalidate();
    }
    /** (lon1,lat1)手机经纬度 (lon2,lat2)飞机经纬度*/
    public void setLatLng(double lon1,double lat1, double lon2, double lat2) {
        double distance = GeoUtils.calculateDistance(lon1,lat1,lon2,lat2);
        length = (float) (distance / MAP_MAX * viewWidth / 2f);//按照km为单位换算  一格1km
        CustomLog.INSTANCE.d("distance:"+distance*1000);
        CustomLog.INSTANCE.d("r:"+ length);
        GeoUtils.MyLatLng point1 = new GeoUtils.MyLatLng(lon1,lat1);
        GeoUtils.MyLatLng point2 = new GeoUtils.MyLatLng(lon2,lat2);
        float angle = (float) GeoUtils.getAngle(point1,point2);
        pointX = length * (float) Math.sin(angle);
        pointY = length * (float) Math.cos(angle);
        postInvalidate();
    }
}

经纬度,方向角获取

下面看下怎样获取经纬度与方向角角,期间运用的一些办法需求在,AndroidManifest.xml文件内增加方位权限,增加权限如下:

<!--    准确方位信息权限,经过GPS获取方位信息-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!--    大致方位信息权限,经过WiFi或基站获取方位信息-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

之后在Activity或Fragment内获取经纬度和方向角,先看怎样获取经纬度(经纬度,方向角获取完好代码在该小节末尾)。

经纬度

先完成方位管理器,之后在方位监听器里边运用random来随机生成飞机方位(现实中是依据其他方法获取飞机方位的,这儿为了模拟作用,运用随机数方法生成无人机方位),在后边对敞开和封闭方位服务时分的代码处理(没有特殊处理的话,只需求在方位监听器里边重写onProviderDisabled和onProviderEnabled办法即可),最终在判别GPS与网络定位是否可用,若能够在判别是否颁发了准确方位和大略方位的权限,若满意上述条件,就能够调用locationManager.requestLocationUpdates()办法恳求方位更新。相关代码如下:

        locationManager = requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
        locationListener = object :LocationListener{
            override fun onLocationChanged(location: Location) {
                lat1 = location.latitude
                lon1 = location.longitude
                if (!hasInitialized){
                    val random = Random()
                    var randomValue = 0.02 + random.nextDouble() * (0.02 - 0.01)
//                    var randomValue = 0.0002 + random.nextDouble() * (0.0002 - 0.0001)//测验用  较近规模
                    if(random.nextBoolean()){
                        randomValue = -randomValue
                    }
                    lat2 = lat1 + randomValue
                    if(random.nextBoolean()){
                        randomValue = -randomValue
                    }
                    lon2 = lon1 + randomValue
                    hasInitialized = true
                }
                binding.layoutRadarMap.setLatLng(lon1,lat1,lon2,lat2)
                CustomLog.d("起点经纬度:($lon1,$lat1)")
                CustomLog.d("结尾经纬度:($lon2,$lat2)")
//                binding.layoutRadarMap.setLatLng(0.0,0.0,0.0,0.015)//测验用,正北方向点
                locationInfo.lon1 = lon1
                locationInfo.lat1 = lat1
                locationInfo.lon2 = lon2
                locationInfo.lat2 = lat2
                locationInfoLiveData.value = locationInfo
            }
            /** 方位提供者被禁用时调用,例如禁用了 GPS 定位功用,那么该办法就会被触发 假如禁用时没有重写该办法会导致闪退*/
            override fun onProviderDisabled(provider: String) {
                CustomLog.d(TAG,provider)
            }
            /** 方位提供者被启用时调用,之前被禁用的方位提供者(如打开了 GPS 定位功用),该办法就会被触发。假如从头启用时没有重写该办法会导致闪退*/
            override fun onProviderEnabled(provider: String) {
                CustomLog.d(TAG,provider)
            }
        }
        //判别GPS或网络定位是否启用
        if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
            //查看是否颁发了访问准确方位和大略方位的权限
            if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
                //恳求方位更新,这儿运用GPS,最小时间间隔0,最小间隔0,及对应的方位监听器
                locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, locationListener)
            }
        }

当运用封闭的时分,需求撤销监听器,一般是在onStop()或onDestroy()内完成。这儿是在onDestroy()完成监听器撤销。

override fun onDestroy() {
    super.onDestroy()
    locationManager?.removeUpdates(locationListener)
}

方向角

方向角获取,我是让fragment完成SensorEventListener接口,并重写onSensorChanged和onAccuracyChanged这两个办法,分别获取呼应传感器数据改变和精度改变,代码如下:

private var lastDegree:Float = 999f
override fun onSensorChanged(event: SensorEvent?) {
    if (event?.sensor?.type == Sensor.TYPE_ORIENTATION) {
        val degree = event?.values?.get(0) ?:0F
        binding.layoutRadarMap.setDegree(degree)
        locationInfo.degree = degree
        locationInfoLiveData.value = locationInfo
        if (abs(lastDegree) <999f && abs(lastDegree - degree) > 5f){
            val startDegree = degree / 360 * 8f
            binding.layoutRadarMap.setNavDirectionDegree(startDegree)
            lastDegree = degree
        }
        if(lastDegree == 999f){
            lastDegree = degree
        }
    }
}
override fun onAccuracyChanged(p0: Sensor?, p1: Int) {
    // 当传感器精度发生改变时的回调
}

上述代码只对传感器数据进行操作,先判别事件类型是不是方向角传感器传来的,之后经过event?.values?.get(0)获取方向角,locationInfo和locationInfoLiveData仅用于屏幕显现方位数据,能够疏忽。lastDegree是用于标志人员箭头细微颤抖的参数,当lastDegree赋值过(abs(lastDegree) <999f),且lastDegree – degree大于必定视点时分会调用binding.layoutRadarMap.setNavDirectionDegree(startDegree)办法让箭头图标细微颤抖。当lastDegree未赋值过(lastDegree == 999f),给lastDegree赋值。

之后在onResume()内声明传感器管理器,及设置传感器类型,并给传感器管理器增加监听(因为fragment完成了传感器监听器SensorEventListener),因此在增加监听器时 sensorManager.registerListener(this, rotationVectorSensor, SensorManager.SENSOR_DELAY_NORMAL),运用this即可,完好代码如下:

//方向角监听器完成
sensorManager = requireContext().getSystemService(Context.SENSOR_SERVICE) as SensorManager
val rotationVectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION)
sensorManager.registerListener(this, rotationVectorSensor, SensorManager.SENSOR_DELAY_NORMAL)

同样在页面不行见或销毁的时分,撤销监听器,代码如下:

override fun onDestroy() {
    super.onDestroy()
    // 撤销注册传感器监听器
    sensorManager.unregisterListener(this)
}

经纬度,方向角获取完好代码

class RadarFragment : BaseBindingViewFragment<FragmentRadarBinding>(R.layout.fragment_radar) ,
    SensorEventListener {
    companion object{
        const val TAG = "RadarFragment"
    }
    private lateinit var sensorManager: SensorManager
    private lateinit var locationManager: LocationManager
    private lateinit var locationListener: LocationListener
    private lateinit var locationInfo: LocationInfo
    private var locationInfoLiveData = MutableLiveData<LocationInfo>()
    private var hasInitialized = false//飞机经纬度是否初始化
    private var lastDegree:Float = 999f
    var lat1:Double = 0.0
    var lon1:Double = 0.0
    var lon2:Double = 0.015
    var lat2:Double = 0.0
    override fun initBinding(layoutInflater: LayoutInflater): FragmentRadarBinding? {
        return FragmentRadarBinding.inflate(layoutInflater)
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initView()
    }
    private fun initView() {
        locationInfo = LocationInfo()
        locationInfoLiveData.observe(viewLifecycleOwner){ it ->
            binding.locationInfo = ""+
                "手机经纬度:(${it.lon1}${it.lat1})\n" +
                    "飞机经纬度:(${it.lon2},${it.lat2})\n" +
                    "两点间隔(米):${(GeoUtils.calculateDistance(it.lon1, it.lat1, it.lon2, it.lat2)*1000).toInt()}\n"+
                    "与正北方向夹角:${GeoUtils.getAngle(GeoUtils.MyLatLng(it.lon1, it.lat1),GeoUtils.MyLatLng(it.lon2, it.lat2)).toInt()}\n"+
                    "方向角:${it.degree}"
        }
    }
    override fun onResume() {
        super.onResume()
        //地址监听
        locationManager = requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
        locationListener = object :LocationListener{
            override fun onLocationChanged(location: Location) {
                lat1 = location.latitude
                lon1 = location.longitude
                if (!hasInitialized){
                    val random = Random()
                    var randomValue = 0.02 + random.nextDouble() * (0.02 - 0.01)
//                    var randomValue = 0.0002 + random.nextDouble() * (0.0002 - 0.0001)//测验用  较近规模
                    if(random.nextBoolean()){
                        randomValue = -randomValue
                    }
                    lat2 = lat1 + randomValue
                    if(random.nextBoolean()){
                        randomValue = -randomValue
                    }
                    lon2 = lon1 + randomValue
                    hasInitialized = true
                }
                binding.layoutRadarMap.setLatLng(lon1,lat1,lon2,lat2)
                CustomLog.d("起点经纬度:($lon1,$lat1)")
                CustomLog.d("结尾经纬度:($lon2,$lat2)")
//                binding.layoutRadarMap.setLatLng(0.0,0.0,0.0,0.015)//测验用,正北方向点
                locationInfo.lon1 = lon1
                locationInfo.lat1 = lat1
                locationInfo.lon2 = lon2
                locationInfo.lat2 = lat2
                locationInfoLiveData.value = locationInfo
            }
            /** 方位提供者被禁用时调用,例如禁用了 GPS 定位功用,那么该办法就会被触发 假如禁用时没有重写该办法会导致闪退*/
            override fun onProviderDisabled(provider: String) {
                CustomLog.d(TAG,provider)
            }
            /** 方位提供者被启用时调用,之前被禁用的方位提供者(如打开了 GPS 定位功用),该办法就会被触发。假如从头启用时没有重写该办法会导致闪退*/
            override fun onProviderEnabled(provider: String) {
                CustomLog.d(TAG,provider)
            }
        }
        //判别GPS或网络定位是否启用
        if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
            //查看是否颁发了访问准确方位和大略方位的权限
            if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
                //恳求方位更新,这儿运用GPS,最小时间间隔0,最小间隔0,及对应的方位监听器
                locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, locationListener)
            }
        }
        //方向角监听器完成
        sensorManager = requireContext().getSystemService(Context.SENSOR_SERVICE) as SensorManager
        val rotationVectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION)
        sensorManager.registerListener(this, rotationVectorSensor, SensorManager.SENSOR_DELAY_NORMAL)
    }
    override fun onDestroy() {
        super.onDestroy()
        // 撤销注册传感器监听器
        sensorManager.unregisterListener(this)
        locationManager?.removeUpdates(locationListener)
    }
    override fun onSensorChanged(event: SensorEvent?) {
        if (event?.sensor?.type == Sensor.TYPE_ORIENTATION) {
            val degree = event?.values?.get(0) ?:0F
            binding.layoutRadarMap.setDegree(degree)
            locationInfo.degree = degree
            locationInfoLiveData.value = locationInfo
            if (abs(lastDegree) <999f && abs(lastDegree - degree) > 5f){
                val startDegree = degree / 360 * 8f
                binding.layoutRadarMap.setNavDirectionDegree(startDegree)
                lastDegree = degree
            }
            if(lastDegree == 999f){
                lastDegree = degree
            }
        }
    }
    override fun onAccuracyChanged(p0: Sensor?, p1: Int) {
        // 当传感器精度发生改变时的回调
    }
}

总结

1.原生获取经纬度的方法,获取经纬度改写频率不是很高,在我的手机上测验或许2s乃至更长才更新一次。而且获取的经纬度有时分差错比较大,最大时分有几十米的差错。用于实际项目时应该运用第三方SDK获取,比方百度地图。
2.方向角获取当时运用的是方向角监听器的方法完成,现在该方法已过时,官方引荐运用加速度传感器和磁场传感器的组合来完成方向角监听,这儿为了简单完成运用方向传感器获取,后边学习下加速度传感器和磁场传感器运用来替换掉。
3.canvas.scale(-1,1),导致画布旋转时分,给人感觉是逆时针旋转,关于这部分的解说,感觉解说的不到位,等待能在谈论区中能和大佬交流一下。

代码地址

GitHub