原文:http://blog.csdn.net/icyfox_bupt/article/details/18953581#comments
本文将与你一起从零开始,做一个河北省空气质量自动发布系统的客户端,文章面向零基础的、只看过一点安卓教程的同学,对于比较基础的内容,也会用红字的链接标出,大家可以点开看详细的介绍。
其实做这个,完全是因为老爸的原因,河北的空气质量太差了,所以他决定天天根据空气质量来决定散步不散步。总是上这个网站过于复杂,于是我就有了做一个客户端的想法。下面分几步介绍关于信息获取,异步获取网络数据,数据分析,界面设计和程序逻辑等内容,下面介绍一个完整的程序是如何做出来的。
首先需要找到程序的数据源,找到从网上获得数据接口的网址。
其次,要把数据从网上的格式,转换成我们可以使用的格式。
接下来进行布局的设计,最后把数据填充到布局里,整个程序就完成了。
下面是这个系统的网站,和我做的客户端:


1、数据获取
想做这个软件我们先要有数据源,数据是河北省环境监测中心给出的,我们现在要找到它的接口。
打开网址:
http://121.28.49.85:8080/ 我们可以看到这是一个flash做的页面,而且有明显的加载过程,说明浏览器获取过数据。我们使用
HttpAnalyze或者
Smsniff来查看浏览器发送出去的数据包,当然最方便的是使用Chrome的功能。
打开Chrome --> F12 --> 选择NetWork标签 --> 打开上面的网络地址,下面会出现很多条请求的数据,我们按
Size排序后找最大的,就是我们需要的数据。如下图:
发送的请求的地址
得到的回应
如上图所示:打开网页后浏览器发送了若干条数据,其中有一条远大于其它数据的包,大小为59.75k,我们可以认为这就是数据的来源了,而我们看到它指向了网址 http://121.28.49.85:8080/datas/hour/130000.xml。在回复中,发现编码是
UTF-8的编码。 打开这个网址,我们可以看到如下图所示的XML数据:
下面我们就以上面的数据为基础,做一个客户端。
2、异步信息获取
2.1 新建一个Android项目
打开一个配置好ADT(
Android Developer Tools)的Eclipse(如果没有配置好点这个
教程),选择File --> New --> Android Application Project,在Application Name里给程序起一个名字比如HebeiAir,然后在最小需求SDK为API14(低一点其实也不影响),其它保持默认,确定。
建立好以后我们的程序至少会有下面这些文件:
2.2 异步获取网络数据
在第一章里,我们找到了获取数据的网址,在这里,我们要把这个网址的数据抓下来供我们使用。在src包里建立一个新的Class,名字定为Util,在里面定义一个新的静态方法:HttpGet,这个方法可以模拟浏览器的访问,我们输入参数是网址,这个函数返回得到的网页源代码:
- public static String HttpGet(String url) throws ClientProtocolException, IOException {
-
- DefaultHttpClient client = new DefaultHttpClient();
-
- HttpGet get = new HttpGet(url);
-
- HttpResponse response = client.execute(get);
-
- String content = null;
-
-
- if (response.getStatusLine().getStatusCode() == 200) {
-
- InputStream in = response.getEntity().getContent();
- byte[] data = new byte[1024];
- int length = 0;
- ByteArrayOutputStream bout = new ByteArrayOutputStream();
- while ((length = in.read(data)) != -1) {
- bout.write(data, 0, length);
- }
-
- content = new String(bout.toByteArray(), "utf-8");
- }
-
- return content;
- }
在上面的Chrome信息窗口中看到,返回值是utf-8编码的,所以在这里我们使用了utf-8编码,如果这里我们使用"gbk"或者"gb-2312"就会出现乱码,每个网站的编码都不相同,具体情况要具体分析。
要注意的是,在我们的程序里并不能直接使用这个函数,因为Android 4.0中,
主线程不能进行网络操作,所以我们需要开启一个新的线程。在Android中,有一个成熟的类:
AsyncTask(异步任务),可以完成这项工作(Thread + Handler也是一种方法,由于我们的工作比较简单,暂时不提及它们)。
接下来我们尝试把XML源代码显示出来。
在MainActivity类中新建一个类
GetSource,这个类继承AsyncTask,用来获取网页上的数据;得到数据后使用
Logcat(可以理解为Android上的控制台)打印出来:
- class GetSource extends AsyncTask<String, Void, String>{
-
-
- @Override
- protected String doInBackground(String... params) {
- try {
-
- return Util.HttpGet("http://121.28.49.85:8080/datas/hour/130000.xml");
- } catch (IOException e) {}
- return null;
- }
-
-
- @Override
- protected void onPostExecute(String result) {
-
- Log.i("test",result);
- }
-
- }
最后在OnCreate函数的最后一行加上下面一句,再
添加网络权限,然后我们来看看效果吧!
- new GetSource().execute();
3、界面设计
我们的目标是把软件设计成文章开始时的那个样式,这样我们就要简单地修改一下activity_main.xml:
·在上方放置一个TextView,背景为浅绿色,文字颜色为白色;
·下方是一个GridView,其中每个格子的颜色根据空气质量来变化,格子中上方显示城市名,下方显示当前的AQI。
·同时在整个界面上还要显示一个转圈的进度条ProgressBar,加载的时候显示这个进度条
代码如下:
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- tools:context=".MainActivity" >
-
- <LinearLayout
- android:id="@+id/ll"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:visibility="gone"
- android:orientation="vertical" >
-
- <TextView
- android:id="@+id/tv_time"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_margin="5dp"
- android:background="#7a7"
- android:padding="5dp"
- android:textColor="#eee"
- android:textSize="20sp"
- android:textStyle="bold" />
-
- <GridView
- android:id="@+id/gv"
- android:layout_width="match_parent"
- android:layout_height="fill_parent"
- android:layout_margin="5dp"
- android:horizontalSpacing="3dp"
- android:verticalSpacing="3dp"
- android:numColumns="3" >
- </GridView>
- </LinearLayout>
-
- <ProgressBar
- android:id="@+id/pb1"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerHorizontal="true"
- android:visibility="visible"
- android:layout_centerVertical="true" />
-
- </RelativeLayout>
GridView 是一种网格样式布局,在上面的代码里我设置的每行格子个数为3个,还设置了格子之间的间距。每个格子中的布局需要另一个文件来控制:
gv.xml:
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/bg"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:padding="8dp"
- android:orientation="vertical" >
-
- <TextView
- android:id="@+id/tv_city"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="石家庄"
- android:textColor="#4bd"
- android:textSize="20sp"
- android:textStyle="bold" />
-
- <TextView
- android:id="@+id/tv_aqi"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="223"
- android:textColor="#4bd"
- android:textSize="18sp"
- android:textStyle="bold" />
-
- </LinearLayout>
4、数据分析
4.1 解析XML数据
网上获得的XML数据需要转换成我们可以使用的结构化数据,这就使用了
基于DOM的XML解析器。更改
GetSource中的
OnPostExecute中的代码为:
- @Override
- protected void onPostExecute(String result) {
-
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- DocumentBuilder builder;
- try {
- builder = factory.newDocumentBuilder();
- InputStream is = new ByteArrayInputStream(result.getBytes());
- Document document = builder.parse(is);
- Element element = document.getDocumentElement();
-
- NodeList cityList = element.getElementsByTagName("Citys");
-
-
- NodeList title = element.getElementsByTagName("MapsTitle");
- String text1 = title.item(0).getTextContent();
- String t[] = text1.split("\\(");
- text1 = t[0];
- t = t[1].split(",");
- tv_time.setText(text1+ "\n" +t[0]);
-
- Element citys = (Element)cityList.item(0);
- NodeList city = citys.getChildNodes();
-
- for (int i=0;i < city.getLength();i++){
-
- Node node = city.item(i);
- if (node.getNodeName().equalsIgnoreCase("city")) {
- CityData cd = new CityData(node);
- nodeList.add(node);
- cdList.add(cd);
- }
- }
- } catch (Exception e) {}
这样每个城市的数据就成为了一个Node,解析XML的过程比较复杂,需要不断地去尝试。
4.2 应用数据到布局
接下来要编写一个Adapter来连接布局和代码。我们假设每一个每一个城市的数据都是一个
Node(节点),GridView的数据存在一个列表里,格子的个数与列表的长度有关。
(所以如果河北省城市监测点变多了,程序也可以进行变化而自适应)分析数据源的XML可以得到,我们需要的是其中城市,和每个城市中监测点的列表。所以,在这里新建两个类:
CityData、Pointer,其中,CityData包括一个Pointer的列表。以下是CityData类,Pointer类与这个相似:
- public class CityData implements Serializable{
-
-
-
-
- private static final long serialVersionUID = -8473485404751986234L;
-
- public String name,dataTime,aqi,level,maxPoll,color,intro,tips;
- public List<Pointer> pointerList;
-
- public CityData(Node cityNode) {
- super();
-
- this.name = getByTag(cityNode, "name");
- this.dataTime = getByTag(cityNode, "datatime");
- this.aqi = getByTag(cityNode, "aqi");
- this.level = getByTag(cityNode, "level");
- this.maxPoll = getByTag(cityNode, "maxpoll");
- String tmp = getByTag(cityNode, "color");
- this.color = tmp.replace("0x", "#");
- this.intro = getByTag(cityNode, "intro");
- this.tips = getByTag(cityNode, "tips");
-
- Element city = (Element)cityNode;
- NodeList pointers = city.getElementsByTagName("Pointer");
-
-
- pointerList = new ArrayList<Pointer>();
- for (int i=0;i<pointers.getLength();i++){
- Node pNode = pointers.item(i);
- pointerList.add(new Pointer(pNode));
- }
- }
-
-
- private String getByTag(Node node,String tag) {
- for (int i=0;i<node.getChildNodes().getLength();i++){
- if (tag.equalsIgnoreCase(node.getChildNodes().item(i).getNodeName()))
- return node.getChildNodes().item(i).getTextContent();
- }
- return null;
- }
- }
负责把数据填充到布局的Adapter:
- class gvAdapter extends BaseAdapter{
-
-
- List<CityData> cdList;
-
- public gvAdapter(List<CityData> cdList) {
- super();
- this.cdList = cdList;
- }
-
-
- @Override
- public int getCount() {
- return cdList.size();
- }
-
- @Override
- public Object getItem(int position) {
- return null;
- }
-
- @Override
- public long getItemId(int position) {
- return 0;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
-
- convertView = MainActivity.this.getLayoutInflater().inflate(R.layout.gv, null);
-
- TextView tv_city = (TextView)convertView.findViewById(R.id.tv_city);
- TextView tv_aqi = (TextView)convertView.findViewById(R.id.tv_aqi);
- View bg = convertView.findViewById(R.id.bg);
-
- tv_city.setText(cdList.get(position).name);
- tv_aqi.setText(cdList.get(position).aqi);
- bg.setBackgroundColor(Color.parseColor(cdList.get(position).color));
- return convertView;
- }
-
- }
最后在XML解析完成以后,添加一句:gv.setAdapter(new gvAdapter(cdList)); 即把数据填充到了网格中。
5、其它程序逻辑
当点击每个GridView的item的时候,跳转到相应的城市详细信息页面。
右键项目的目录 New --> Other -->Activity,给新的Activity起名为:CityActivity。依照上面介绍的写法给新的Activity写好布局和逻辑。
点击主界面中的网格,自动跳转到新的界面:
- gv.setOnItemClickListener(new OnItemClickListener() {
-
- @Override
- public void onItemClick(AdapterView<?> parent, View view,int position, long id) {
-
- <span style="white-space:pre"> </span>Intent it = new Intent(MainActivity.this, CityActivity.class);
-
- it.putExtra("node", cdList.get(position));
- startActivity(it);
- }
- });
1、在城市详细信息界面,点击每个监测点,显示该监测点的详细数据。这里需要用到对话框 AlertDialog来显示详细数据。
2、更改
res --> values -->string.xml 中的内容,个性化我们的程序,如下:
- <resources>
- <string name="app_name">河北空气质量</string>
- <string name="title_activity_city">城市数据</string>
- </resources>
这样,程序的名字就变成了“河北空气质量”,在进入到详细信息界面的时候,标题栏也变成了“城市数据”
3、找一个图片,做成png格式,覆盖res/drawable-hdpi/ic_launcher.png 文件,这样就更改了在手机APP列表中的图标了。
其实做一个APP非常的简单,只要有想法,上面的工作在一天内就可以完成。本文主要是想带给大家一个思路,如何发掘身边的一些内容,来做出自己的APP。
可能光看讲解不会太懂,那么可以到 http://download.csdn.net/detail/icyfox_bupt/6908053下载程序的源代码