您的位置:首页 > 其它

用MPAndoidChart展示搜索到的GPS及卫星信息

2017-01-07 22:58 281 查看
  最近项目中做了一个GPS卫星状态展示的功能,因为是第一次接触到图表的开源项目MPAndroidChart ,在开发的过程中遇到了MPAndroidChart 的使用以及其他的一些问题,所以这里总结一下。

MPAndroidChart 功能十分强大,具有以下功能:

八种不同的图表类型

两轴缩放(带触摸手势,可独立的捏缩放轴)

拖动/平移(带触摸手势)

联合图表(线 - ,柱状图,散射 - ,蜡烛数据)

双(单独)轴

可自定义的轴(x轴和y轴)

高亮值(可自定义弹出的窗口视图)

保存图表到SD卡(图像或.txt文件)

可预定义模板颜色

Legends(图例,自动生成,可定义)

动画(可在这两个X轴和Y轴建立动画)

限制线(提供额外的信息,如最大值、最小值…)

完全自定义(画笔,字体,图例,颜色,背景,手势,虚线,…)

在折线图或柱状图中,可顺畅的缩放和滚动30,000个数据

支持Gradle

直接将Realm Mobile Database中的数据绘制成图表

  各种图标所对应的控件:LineChart,BarChart,ScatterChart,CandleStickChart,PieChart,BubbleChart,RadarChart。关于用法GitHub上给的demo已经非常详细了。别人瓢都画好了,用的时候只需要按照瓢来画瓢咯(只能努力多学习多做事,希望自己什么时候能向大神看齐,画个属于自己的葫芦出来)

  由于正常情况下GPS能搜索到的卫星大概有7-12颗,且需要展示的SNR数据只有一组,所以采用单组数据的柱状图BarChart,当然它也可以绘制多组数据的图表。

下面使用BarChart来绘制一些GPS数据:

当我们正确的引入了MPAndroidChart库后,就可以直接像使用普通的控件一样的使用BarChart了(其他的图形使用也都大同小异,这也是它强大的地方),既可以直接在代码中创建BarChart的对象,也可以在布局文件中引用。引用Barchart的布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="@dimen/barchart_height">

<TextView
android:id="@+id/tv_total_statellite"
style="@style/text_statellite_num"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:text="" />

<TextView
android:id="@+id/tv_used_statellite"
style="@style/text_statellite_num"
android:layout_alignBaseline="@id/tv_total_statellite"
android:layout_alignParentRight="true"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:text="" />

<com.github.mikephil.charting.charts.BarChart
android:id="@+id/mpChart"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/tv_total_statellite" />

</RelativeLayout>

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_horizontal_margin">

<Space
android:id="@+id/space_middle_line"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_centerHorizontal="true" />

<TextView
android:id="@+id/tv_longitude"
style="@style/left_text_gps_state"
android:text="" />

<TextView
android:id="@+id/tv_latitude"
style="@style/right_text_gps_state"
android:layout_alignBottom="@id/tv_longitude"
android:text="" />

<TextView
android:id="@+id/tv_accuracy"
style="@style/left_text_gps_state"
android:layout_below="@id/tv_longitude"
android:layout_marginTop="@dimen/gps_text_divide_height"
android:text="" />

<TextView
android:id="@+id/tv_src"
style="@style/right_text_gps_state"
android:layout_alignBottom="@id/tv_accuracy"
android:text="" />

<TextView
android:id="@+id/tv_altitude"
style="@style/left_text_gps_state"
android:layout_below="@id/tv_accuracy"
android:layout_marginTop="@dimen/gps_text_divide_height"
android:text="" />

<TextView
android:id="@+id/tv_speed"
style="@style/right_text_gps_state"
android:layout_alignBaseline="@id/tv_altitude"
android:text="" />

<TextView
android:id="@+id/tv_time"
style="@style/text_gps_state"
android:layout_below="@id/tv_altitude"
android:layout_marginTop="@dimen/gps_text_divide_height"
android:text="" />
</RelativeLayout>
</LinearLayout>


由于在这个布局中有两个地方是自己平时写代码的时候没有注意到的,所以就一起贴出来了:

1. Space控件

Google文档:Space is a lightweight View subclass that may be used to create gaps between components in general purpose layouts.

意思是说Space是View的一个轻量级的子类并且一般放在各个组件之间用来分割它们,这就和我们之前在布局文件中使用View在LinearLayout布局占位一样,将其他布局挤到两边。另外在这个类中只是重写了View的onDraw()和onMeasure()方法,没有做任何其他的扩展,并且onDraw()的方法体do nothing。这说明Space根本没有显示出来,因此将Space和View做个简单的比较如下:


控件使用场景
View可见时使用:用来分隔布局(分隔线啥的);作为有背景色的占位布局
Space不可见时使用:LinearLayout中占位布局,将其他布局分隔开;作为一个标记布局(如上面布局文件中的Space)

2. RelativeLayout平分布局

像上面布局中展示的位置信息这种相对较整齐的类似表格布局的时候,我们会经常使用LinearLayout来作为根布局,但是这样有个问题就是布局变得相对较复杂,因此为了达到平分布局的效果可以采用Space作为一个标记,左右或者上下的布局都以这个Space作为基准线来排版,这样可以适当的减少布局的层次。


  好了接着BarChart的说。根据BarChart的ID得到其对象,现在就要定制自己想要的柱状图样式了,虽然方法比较多但大体上分为以下几种:x轴样式,y轴样式,矩形的样式,图例样式,手势事件,没有数据时显示的文字的样式,描述性文字的样式以及其他的一些设置,都比较简单。

  样式设置完之后就可以绘制数据到图表上了。根据需求,需要动态的显示搜索到的卫星的SNR(信噪比,数值越大说明信号越好),PRN(Pseudo Random Noise code)信息。这里用到了LocationManager API,是一个系统提供的位置服务,可以通过上下文得到LocationManager对象。虽然数据好获取,但是影响GPS的因素太多了导致搜索到的卫星数目变化比较大,在室内可能为0,在空旷的地方可能能搜索到12颗,且BarChart设置柱子的宽度的时候只能设置百分比,默认的是0.85f;当卫星的颗数比较少的时候柱子就会非常宽,这就使得柱状图一点都不美观(我想应该有什么方法来设置的,但是找了好久都没有找到……,如果谁知道请告诉我一声,下面接着说我自己的实现方式)。因此,我就设置一个默认的卫星颗数的最小值7,也就是最少显示7根柱子,如果搜索到的卫星少于7颗(卫星信号比较差的时候会出现信噪比为-1的情况,这里我就直接过滤掉这些卫星了),那就创建BarEntry对象,使得柱状图的数据总数保持至少7个。当然这些假数据是不能显示出来的(y值为0,x轴标签为\”“),而刚好可以通过给BarDataSet对象设置一个IValueFormatter的子类对象,在自定义的IValueFormatter中判断数据是否为假数据,如果是就返回空字符串。具体的实现效果图如下:



说明:由于是在室内老是接受不到卫星信号所以上面数据是随机数,实际情况下数据变化没有这么快,但是BarChart的效果是有了

其主要实现代码如下(另外,我在HTC手机上测试能够搜索到20多颗卫星,且信噪比都是0,不知道怎么回事,如果想达到上面的效果只需要修改对应的COUNT_DEFAULT_BAR到合适的值即可):

public class ChartActivity extends Activity implements GpsStatus.Listener, LocationListener {
private final static int FREQUENCY_REFRESH_DATA = 1000;
private final static int COUNT_DEFAULT_BAR = 12;
private final static float mYAxisMaximum = 105F;

private int mNumUsedStatellite;
private ArrayList<Integer> mColorList;
private List<GpsSatellite> mStatelliteList; // 卫星
private LocationManager mLocationManager = null;
private SimpleDateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.CHINA);
private DecimalFormat mDecimalFormat = new DecimalFormat("0.000");

private TextView mTvTotalStatellite = null;
private TextView mTvUsedStatellite = null;
private TextView mTvTime = null;
private TextView mTvLongtitude = null;
private TextView mTvLatitude = null;
private TextView mTvAccuracy = null;
private TextView mTvSrc = null;
private TextView mTvAltitude = null;
private TextView mTvSpeed = null;
private BarChart mChart = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

init();
}

private void init() {
initV
ec73
iew();

initData();

initListener();

if (!isGpsEnable()) {
enterSystemGpsSetting();
}
}

@Override
public void onDestroy() {
if (mLocationManager != null) {
mLocationManager.removeGpsStatusListener(this);
mLocationManager.removeUpdates(this);
}

mLocationManager = null;

super.onDestroy();
}

private void initView() {
mChart = (BarChart) findViewById(R.id.mpChart);

mTvTotalStatellite = (TextView) findViewById(R.id.tv_total_statellite);
mTvUsedStatellite = (TextView) findViewById(R.id.tv_used_statellite);
mTvTime = (TextView) findViewById(R.id.tv_time);
mTvLongtitude = (TextView) findViewById(R.id.tv_longitude);
mTvLatitude = (TextView) findViewById(R.id.tv_latitude);
mTvAccuracy = (TextView) findViewById(R.id.tv_accuracy);
mTvSrc = (TextView) findViewById(R.id.tv_src);
mTvAltitude = (TextView) findViewById(R.id.tv_altitude);
mTvSpeed = (TextView) findViewById(R.id.tv_speed);

setBarChartStyle();
}

private void setBarChartStyle() {
mChart.setScaleEnabled(false);
mChart.setDrawBarShadow(false);
mChart.setDrawValueAboveBar(true);
mChart.setNoDataText(getString(R.string.not_search_statellite));
mChart.getDescription().setEnabled(false);
mChart.setMaxVisibleValueCount(60);
mChart.setPinchZoom(false);
mChart.setDrawGridBackground(false);

XAxis xAxis = mChart.getXAxis();
xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
xAxis.setDrawGridLines(false);
xAxis.setGranularity(0f); // only intervals of 1 day
xAxis.setLabelCount(COUNT_DEFAULT_BAR, false);
xAxis.setValueFormatter(new XAxisValueFormatter());

YAxisValueFormatter yValueFormatter = new YAxisValueFormatter();

YAxis leftAxis = mChart.getAxisLeft();
leftAxis.setLabelCount(6, false);
leftAxis.setValueFormatter(yValueFormatter);
leftAxis.setPosition(YAxis.YAxisLabelPosition.OUTSIDE_CHART);
leftAxis.setSpaceTop(15f);
leftAxis.setAxisMinimum(0f); // this replaces setStartAtZero(true)
leftAxis.setAxisMaximum(mYAxisMaximum);

YAxis rightAxis = mChart.getAxisRight();
rightAxis.setDrawGridLines(false);
rightAxis.setLabelCount(6, false);
rightAxis.setValueFormatter(yValueFormatter);
rightAxis.setSpaceTop(15f);
rightAxis.setAxisMinimum(0f); // this replaces setStartAtZero(true)
rightAxis.setAxisMaximum(mYAxisMaximum);
rightAxis.setEnabled(false);

Legend l = mChart.getLegend();
l.setVerticalAlignment(Legend.LegendVerticalAlignment.BOTTOM);
l.setHorizontalAlignment(Legend.LegendHorizontalAlignment.LEFT);
l.setOrientation(Legend.LegendOrientation.HORIZONTAL);
l.setDrawInside(false);
l.setForm(Legend.LegendForm.SQUARE);
l.setFormSize(9f);
l.setTextSize(11f);
l.setXEntrySpace(4f);
l.setEnabled(false);
}

private void initData() {
mStatelliteList = new ArrayList<>(); // 卫星信号
mColorList = new ArrayList<>();

mLocationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);

updateChartData(mLocationManager.getGpsStatus(null));

setLocationData(mLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER));
}

private void initListener() {
mLocationManager.addGpsStatusListener(this);

mLocationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 0, 0, this);
}

@Override
public void onGpsStatusChanged(int event) {
if (event == GpsStatus.GPS_EVENT_SATELLITE_STATUS) {
GpsStatus gpsStatus = mLocationManager.getGpsStatus(null);
updateChartData(gpsStatus);
}
}

@Override
public void onLocationChanged(Location location) {
setLocationData(location);
}

@Override
public void onStatusChanged(String provider, int status, Bundle extras) {

}

@Override
public void onProviderEnabled(String provider) {

}

@Override
public void onProviderDisabled(String provider) {

}

private void updateChartData(GpsStatus gpsStatus) {
mStatelliteList.clear();
mNumUsedStatellite = 0;

ArrayList<BarEntry> barEntries = getBarEntries(gpsStatus);

if (mChart.getData() != null && mChart.getData().getDataSetCount() > 0) {
refreshChartData(barEntries);
} else {
setChartData(barEntries);
}
mChart.invalidate();
setStatelliteNumView();
}

/**
* 设置柱状图数据
*/
private void setChartData(ArrayList<BarEntry> barEntries) {
BarDataSet snrBarDataSet;
snrBarDataSet = new BarDataSet(barEntries, "");
snrBarDataSet.setColors(mColorList);
snrBarDataSet.setValueFormatter(new DataValueFormatter());

ArrayList<IBarDataSet> dataSets = new ArrayList<>();
dataSets.add(snrBarDataSet);

BarData data = new BarData(dataSets);

data.setValueTextSize(10f);
data.setBarWidth(0.9f);
mChart.setData(data);
}

/**
* 刷新柱状图数据
*/
private void refreshChartData(ArrayList<BarEntry> barEntries) {
BarDataSet snrBarDataSet;
snrBarDataSet = (BarDataSet) mChart.getData().getDataSetByIndex(0);
snrBarDataSet.setValues(barEntries);
mChart.getData().notifyDataChanged();
mChart.notifyDataSetChanged();
}

Random random = new Random(10);

private ArrayList<BarEntry> getBarEntries(GpsStatus status) {
ArrayList<BarEntry> barEntries = new ArrayList<>();
if (status != null) {
// GPS状态不为空
int maxSatellites = status.getMaxSatellites();
Iterator<GpsSatellite> it = status.getSatellites().iterator();
int count = 0;
mColorList.clear();

while (it.hasNext() && count <= maxSatellites) {
GpsSatellite s = it.next();
// 卫星的颗数用采用随机数
if (mStatelliteList.size() > random.nextInt(maxSatellites + 1)) {
break;
}
// 过滤掉信噪比为-1的卫星
//                if (s.getSnr() < 0) {
//                    continue;
//                }

if (s.usedInFix()) {
// 卫星正在使用
++mNumUsedStatellite;
mColorList.add(getResources().getColor(R.color.color_statellite_used));
} else {
// 么有使用
mColorList.add(getResources().getColor(R.color.color_statellite_not_used));
}

mStatelliteList.add(s);
count++;
// 信噪比为-1的卫星用随机数代替
barEntries.add(new BarEntry(count, s.getSnr() < 0 ? (random.nextInt(60) + 1) : s.getSnr()));
}
}

int size = barEntries.size();
// 卫星数不足COUNT_DEFAULT_BAR时,用假数据补足
if (size > 0 && size < COUNT_DEFAULT_BAR) {
for (int i = 0; i < COUNT_DEFAULT_BAR - size; i++) {
BarEntry barEntry = new BarEntry(size + i + 1, -1);
barEntries.add(barEntry);
}
}

return barEntries;
}

private void setStatelliteNumView() {
mTvTotalStatellite.setText(String.format(Locale.CHINA, "搜索到的卫星:%d", mStatelliteList.size()));
mTvUsedStatellite.setText(String.format(Locale.CHINA, "使用中的卫星:%d", mNumUsedStatellite));
}

/**
* 设置定位信息
*/
private void setLocationData(Location location) {
double latitude;    // 纬度
double longitude;   // 经度
float accuracy;     // 精度
String src;         // 来源
float speed;        // 速度
double altitude;    // 海拔
long time;          // 时间

if (location == null) {
latitude = 0.0;
longitude = 0.0;
accuracy = 0.0f;
src = "-";
speed = 0.0f;
altitude = 0.0f;
time = System.currentTimeMillis();
} else {
latitude = location.getLatitude();
longitude = location.getLongitude();
accuracy = location.getAccuracy();
speed = location.getSpeed();
altitude = location.getAltitude();
src = location.getProvider();
time = location.getTime();
}

mTvLatitude.setText(String.format("纬度:%s", mDecimalFormat.format(latitude)));
mTvLongtitude.setText(String.format("经度:%s", mDecimalFormat.format(longitude)));
mTvAccuracy.setText(String.format("精度:%s", mDecimalFormat.format(accuracy)));
mTvSrc.setText(String.format("源自:%s", src));
mTvAltitude.setText(String.format("海拔:%.1f m", altitude));
mTvSpeed.setText(String.format("速度:%.1f m/s", speed));
mTvTime.setText(String.format("时间:%s", mDateFormat.format(time)));
}

/**
* GPS是否开启
*
* @return true开启,fasle关闭
*/
private boolean isGpsEnable() {
return mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
}

private void enterSystemGpsSetting() {
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(intent);
}

/**
* 格式化数据
*/
private class DataValueFormatter implements IValueFormatter {
@Override
public String getFormattedValue(float v, Entry entry, int i, ViewPortHandler viewPortHandler) {
if ((int) entry.getX() > mStatelliteList.size()) {
return "";
} else {
return (int) v + "";
}
}
}

/**
* x轴数据的格式化
*/
private class XAxisValueFormatter implements IAxisValueFormatter {
@Override
public String getFormattedValue(float v, AxisBase axisBase) {
int index = (int) v;
if (mStatelliteList == null || index > mStatelliteList.size() || index < 1) {
return "";                // 假数据就返回空
} else {
GpsSatellite gpsSatellite = mStatelliteList.get(index - 1);
return gpsSatellite.getPrn() + "";  // 卫星的伪随机码值
}
}

@Override
public int getDecimalDigits() {
return 0;
}
}

/**
* 格式化Y值
*/
private class YAxisValueFormatter implements IAxisValueFormatter {

@Override
public String getFormattedValue(float v, AxisBase axisBase) {
return (int) v + "";
}

@Override
public int getDecimalDigits() {
return 0;
}
}
}


总结

  第一次写博客感觉比牙膏还难挤,特别是第一次用Markdown编辑器不太习惯,好多语法也不知道,编写边学的,不过写完这边博客之后,Markdown的基本语法及快捷键也基本上会了,再用些简单的HTML标签就可以很好的编辑博客了。当然了关于MPAndroidChart和GPS定位相关的还有许多的问题没有搞明白,比如MPAndroidChart的其他几种图表都没用过,GPS的不同定位来源的区别,不同手机搜索到的卫星数目这么大区别(用公司的机器测比较正常,但用htc就有20多颗卫星且全部为0),以及通过NMEA获取详细的位置数据等等。

  总之呢,万事开头难,既然开了这个头,我希望自己能够继续写下去,在工作之余多多的积累,不要老是玩游戏;希望自己在今天种下的这颗种子,能够在将来长成参天大树,好让我及我所爱的人有一片阴凉温馨之地。同时也将这句话送给所有看过这篇博客的朋友,2016年已经过去了,新的一年,新的开始,我们需要有所改变,并朝着好的方向坚持不懈的前行!!!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: