本文已参与「新人创造礼」活动,一同开启创造之路。

效果图:

Android GNSS 可视卫星星空图/卫星天顶图 原理及画法介绍

首先介绍一下相关的概念:


方位角:是从某点的指北(地理北极)方向线起,依顺时针方向到方针方向线之间的水平夹角。

Android GNSS 可视卫星星空图/卫星天顶图 原理及画法介绍

高度角:从一点至观测方针的方向线与水平面间的夹角。

Android GNSS 可视卫星星空图/卫星天顶图 原理及画法介绍

卫星天顶图:便是依据每一颗卫星的方位角和高度角将其画在以观测方位为中心的天顶图上。

天顶图其底图为由外向内的三个圆和四条直线。三个圆由外向内顺次代表高度角0、30、60,中心点代表90;四条直线别离表示正北-正南、东北-西南、正东-正西、东南-西北的方位角方向。

下面逐个介绍制作的过程

1 获取卫星信息


要想画出卫星图,有必要先有卫星的方位角和高度角等基本信息。在Android中,能够经过向LocationManager类注册GnssStatus监听器获取卫星PRN、方位角、高度角、星座类型、卫星载噪比等与卫星本身有关的信息。


关于怎么注册GNSS监听器,Android GNSS伪距核算 中有注册丈量监听器的具体过程,卫星状况监听器的注册方式完全类似。


不同的是在回调办法中的行为,在这儿,咱们需求运用数组将卫星信息保存起来,如下:

private float[] mElevations, mAzimuths, mSnrs;
private int[] mPrns, mConstellationTypes;
public void setGnssStatus(GnssStatus status) {
    if (mPrns == null) {
        final int MAX_LENGTH = 255;
        mPrns = new int[MAX_LENGTH];
        mElevations = new float[MAX_LENGTH];
        mAzimuths = new float[MAX_LENGTH];
        mConstellationTypes = new int[MAX_LENGTH];
        mSnrs = new float[MAX_LENGTH];
    }
    int length = status.getSatelliteCount();
    mSvCount = 0;
    while (mSvCount < length) {
        mPrns[mSvCount] = status.getSvid(mSvCount);
        mElevations[mSvCount] = status.getElevationDegrees(mSvCount);
        mAzimuths[mSvCount] = status.getAzimuthDegrees(mSvCount);
        mConstellationTypes[mSvCount] = status.getConstellationType(mSvCount);
        mSnrs[mSvCount] = status.getCn0DbHz(mSvCount);
        mSvCount++;
    }
    invalidate();
}

经过GnssStatus的参数目标status的 getSatelliteCount() 办法获取卫星数量,然后运用 getSvid()、getElevationDegrees()、getAzimuthDegrees()、getConstellationType()、getCn0DbHz() 办法别离获取卫星的 PRN、高度角、方位角、星座、载噪比


然后调用View目标的invalidate()办法重绘星空图,即更新。


2 自定义视图


由于Android中并没有一种体系控件能够让咱们方便的展现卫星图,因此咱们需求自定义一个天空图视图

public class GnssSkyView extends View { }


咱们在碎片Fragment中加载这个视图,经过在碎片上注册监听器取得不断更新的信息,并在碎片的回调办法中调用 GnssSkyView 目标的 setGnssStatus 办法将信息传给它,然后使其取得数据来历,从而进行多样化地展现

3 制作底图


底图包括三个圆、四条直线和外圈圆上的方位角刻度。

onDraw() 办法会在视图制作的过程中体系自动调用,咱们需求在这个办法中自定义制作内容。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int minScreenDimen = Math.min(mWidth, mHeight);
    float radius = minScreenDimen / 2.0f;
    drawCircle(canvas, radius);
    drawLine(canvas, minScreenDimen, radius);
    drawDegree(canvas, radius);
}


由于咱们要画的是一个圆,所以一定有一个外切正方形,上面的代码中,minScreenDimen 为视图长和宽的最小值,这个值就作为正方形的边长,也是外圈圆的直径


然后咱们别离调用三个函数drawCircle()、drawLine()、drawDegree(),这三个函数是咱们自己定义的,别离画圆、直线、刻度。


3.1 圆的画法


onDraw() 办法会给咱们一个参数canvas,这个参数是视图画布Canvas的目标,经过调用canvas的 drawCircle() 办法,就能实现圆的制作。


该办法的函数原型如下:

public void drawCircle(float cx, float cy, float radius, Paint paint) {
    super.drawCircle(cx, cy, radius, paint);
}


可见,咱们需求给出圆心的 x,y 坐标和圆的半径 r,还有画笔 paint


这儿需求留意的是,画布的坐标系是以左上角为坐标原点,水平向右为X轴正向,竖直向下为Y轴正向


咱们在前面现已获取了外圆的半径,而外圈圆代表的是高度角0,因此还需求核算出30、60的高度角代表的圆的半径。


然后这儿给了一个 Y_TRANSLATION ,这是预定义的Y轴偏移量,方向向下为正,目的是使最上方的卫星能够完好的显示出来

private void drawCircle(Canvas c, float radius) {
    c.drawCircle(radius, radius + Y_TRANSLATION, elevationToRadius(radius, 60.0f), mPaintCircleAndLine);
    c.drawCircle(radius, radius + Y_TRANSLATION, elevationToRadius(radius, 30.0f), mPaintCircleAndLine);
    c.drawCircle(radius, radius + Y_TRANSLATION, elevationToRadius(radius, 0.0f), mPaintCircleAndLine);
}
private float elevationToRadius(float s, float elev) {
    return s * (1.0f - (elev / 90.0f));
}

3.2 直线的画法


直线的画法与圆类似,画直线的体系函数如下,其属于canvas画布目标:

public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) {
    super.drawLine(startX, startY, stopX, stopY, paint);
}


可知咱们需求给出线段的起点坐标和终点坐标,还有画笔目标 paint

先核算出四条直线的起止点坐标,然后调用drawLine()办法

下面的函数中,s即minScreenDimen

private void drawLine(Canvas c, int s, float radius) {
    c.drawLine(radius, Y_TRANSLATION, radius, s + Y_TRANSLATION, mPaintCircleAndLine);
    c.drawLine(0, radius + Y_TRANSLATION, s, radius + Y_TRANSLATION, mPaintCircleAndLine);
    final float cos45 = (float) Math.cos(Math.PI / 4);
    float d1 = radius * (1 - cos45);
    float d2 = radius * (1 + cos45);
    c.drawLine(d1, d1 + Y_TRANSLATION, d2, d2 + Y_TRANSLATION, mPaintCircleAndLine);
    c.drawLine(d2, d1 + Y_TRANSLATION, d1, d2 + Y_TRANSLATION, mPaintCircleAndLine);
}

3.3 制作刻度


制作刻度的本质就是制作直线,核算出每一个刻度线段的起止点坐标,然后调用制作直线的函数

这儿,在正北、正东、正南、正西方向上还要给出N、E、S、W的文本,能够调用画布目标canvas的drawText()体系函数制作


drawText()体系函数原型如下:

public void drawText(String text, float x, float y, Paint paint) {
    super.drawText(text, x, y, paint);
}

只需给出文本字符串、文本方位坐标、画笔目标


这儿,运用rorate()体系函数进行旋转制作,具体原理能够自行百度

private void drawDegree(Canvas c, float radius) {
    for (int i = 0; i < 360; i += 15) {
        if (i == 45 || i == 135 || i == 225 || i == 315) {
            c.drawText(String.valueOf(i), radius, 40 + Y_TRANSLATION, mPaintDegree);
        } else if (i == 0) {
            c.drawText("N", radius, 40 + Y_TRANSLATION, mPaintDegree);
        } else if (i == 90) {
            c.drawText("E", radius, 40 + Y_TRANSLATION, mPaintDegree);
        } else if (i == 180) {
            c.drawText("S", radius, 40 + Y_TRANSLATION, mPaintDegree);
        } else if (i == 270) {
            c.drawText("W", radius, 40 + Y_TRANSLATION, mPaintDegree);
        } else {
            c.drawLine(radius, Y_TRANSLATION, radius, 20 + Y_TRANSLATION, mPaintDegree);
        }
        c.rotate(15, radius, radius + Y_TRANSLATION);
    }
}

3.4 制作成果

Android GNSS 可视卫星星空图/卫星天顶图 原理及画法介绍

4 制作卫星


4.1 核算卫星坐标

Android GNSS 可视卫星星空图/卫星天顶图 原理及画法介绍

如上图,依据高度角能够核算出卫星的半径,即到圆心的间隔 r,再依据方位角 和外圈圆的半径 R 即可确认卫星在画布坐标系中的坐标


4.2 获取卫星旗号


这个比较简单,只需求预先预备好四大体系和日本的代表旗号,然后依据星座类型来获取Bitmap目标


这儿,预备了六张PNG图片,放在./res/drawable目录下

Android GNSS 可视卫星星空图/卫星天顶图 原理及画法介绍

然后,运用BitmapFactory.decodeResource() 办法创立一个位图目标。这个办法的第一个参数为Android体系的资源目标resources,能够经过视图View目标的getResources() 办法得到;第二个参数为图片数据的资源ID


值得一提的是,一般咱们预备的图片尺度都各不相同,而且远大于在视图上展现的巨细,而为了让所有星座的卫星运用同样小尺度的图片,咱们需求对位图Bitmap进行放缩

private Bitmap getSatelliteBitmap(int constellationType) {
    Bitmap baseMap, newMap;
    int width, height;
    switch (constellationType) {
        case GnssStatus.CONSTELLATION_BEIDOU:
            baseMap = BitmapFactory.decodeResource(getResources(), R.drawable.flag_of_china);
            break;
        case GnssStatus.CONSTELLATION_GPS:
            baseMap = BitmapFactory.decodeResource(getResources(), R.drawable.flag_of_america);
            break;
        case GnssStatus.CONSTELLATION_GALILEO:
            baseMap = BitmapFactory.decodeResource(getResources(), R.drawable.flag_of_europe);
            break;
        case GnssStatus.CONSTELLATION_GLONASS:
            baseMap = BitmapFactory.decodeResource(getResources(), R.drawable.flag_of_russia);
            break;
        case GnssStatus.CONSTELLATION_QZSS:
            baseMap = BitmapFactory.decodeResource(getResources(), R.drawable.flag_of_japan);
            break;
        default:
            baseMap = BitmapFactory.decodeResource(getResources(), R.drawable.flag_of_other);
    }
    width = baseMap.getWidth();
    height = baseMap.getHeight();
    newMap = UiUtils.scaling(baseMap, (SAT_RADIUS * 2.0f) / width, (SAT_RADIUS * 2.0f) / height);
    return newMap;
}
public class UiUtils {
    public static Bitmap scaling(Bitmap bitmap, float widthScale, float heightScale) {
        Matrix matrix = new Matrix();
        matrix.postScale(widthScale, heightScale); //长和宽扩大缩小的份额
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
    }
}

4.3 制作卫星


制作卫星的代码如下,其中咱们还要生成卫星星座+卫星PRN的文本字符串:

private void drawSatellite(Canvas c, int s, float elev, float azim, float snr, int prn, int constellationType) {
    double radius, angle;
    float x, y;
    Bitmap satMap;
    satMap = getSatelliteBitmap(constellationType);
    String satText;
    satText = getSatelliteText(prn, constellationType);
    radius = elevationToRadius(s / 2.0f, elev);
    angle = (float) Math.toRadians(azim);
    x = (float) ((s / 2.0f) + (radius * Math.sin(angle)));
    y = (float) ((s / 2.0f) - (radius * Math.cos(angle)));
    c.drawBitmap(satMap, x - SAT_RADIUS, y - SAT_RADIUS + Y_TRANSLATION, null);
    c.drawText(satText, x - SAT_RADIUS, y + SAT_RADIUS * 2 + Y_TRANSLATION, mPrnIdPaint);
}
private String getSatelliteText(int prn, int constellationType) {
    StringBuilder builder = new StringBuilder();
    switch (constellationType) {
        case GnssStatus.CONSTELLATION_BEIDOU:
            builder.append("C");
            break;
        case GnssStatus.CONSTELLATION_GPS:
            builder.append("G");
            break;
        case GnssStatus.CONSTELLATION_GALILEO:
            builder.append("E");
            break;
        case GnssStatus.CONSTELLATION_GLONASS:
            builder.append("R");
            break;
        case GnssStatus.CONSTELLATION_QZSS:
            builder.append("Q");
            break;
        default:
            builder.append("S");
    }
    builder.append(prn);
    return builder.toString();
}

5 动态制作

咱们在注册卫星状况信息的监听器后,会依据注册时指定的更新参数取得一定时间间隔的卫星信息,每次卫星的状况信息得到更新后,咱们调用天空图类目标(自定义视图)的 invalidate() 办法(View及其子类的目标都有这个办法)重绘视图即可更新图上卫星的方位。

6 自定义视图巨细的控制


View的作业流程,主要是measure、layout和draw三步,measure用来丈量View的宽高,layout用来确认View(在ViewGroup中)的方位,draw则用来制作View。


决议View的巨细只需求两个值:宽具体丈量值(widthMeasureSpec)和高具体丈量值(heightMeasureSpec)。也能够把具体丈量值理解为视图View想要的巨细阐明(想要的未必就是终究巨细)。


咱们先获取屏幕的宽高,然后取较小值,由于一般是宽较小,所以将高度设为宽度+20dp,然后运用View的 setMeasuredDimension() 办法设置视图的巨细。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int windowWidth = mDisplayMetrics.widthPixels;
    int windowHeight = mDisplayMetrics.heightPixels;
    int minL = Math.min(windowWidth, windowHeight);
    setMeasuredDimension(minL, minL + 20);
}
private void init(Context context) {
    mDisplayMetrics = mContext.getResources().getDisplayMetrics();
    getViewTreeObserver().addOnPreDrawListener(
        new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                mHeight = getHeight();
                mWidth = getWidth();
                return true;
            }
        }
    );
    invalidate();
}