閱讀973 返回首頁    go 阿裏雲 go 技術社區[雲棲]


安卓作品:河北空氣質量客戶端

原文:https://blog.csdn.net/icyfox_bupt/article/details/18953581comments

本文將與你一起從零開始,做一個河北省空氣質量自動發布係統的客戶端,文章麵向零基礎的、隻看過一點安卓教程的同學,對於比較基礎的內容,也會用紅字的鏈接標出,大家可以點開看詳細的介紹。

        其實做這個,完全是因為老爸的原因,河北的空氣質量太差了,所以他決定天天根據空氣質量來決定散步不散步。總是上這個網站過於複雜,於是我就有了做一個客戶端的想法。下麵分幾步介紹關於信息獲取異步獲取網絡數據數據分析界麵設計程序邏輯等內容,下麵介紹一個完整的程序是如何做出來的。

        首先需要找到程序的數據源,找到從網上獲得數據接口的網址。

        其次,要把數據從網上的格式,轉換成我們可以使用的格式。

        接下來進行布局的設計,最後把數據填充到布局裏,整個程序就完成了。

        下麵是這個係統的網站,和我做的客戶端:



1、數據獲取

        想做這個軟件我們先要有數據源,數據是河北省環境監測中心給出的,我們現在要找到它的接口。
        打開網址: https://121.28.49.85:8080/ 我們可以看到這是一個flash做的頁麵,而且有明顯的加載過程,說明瀏覽器獲取過數據。我們使用HttpAnalyze或者Smsniff來查看瀏覽器發送出去的數據包,當然最方便的是使用Chrome的功能。
        打開Chrome --> F12 --> 選擇NetWork標簽 --> 打開上麵的網絡地址,下麵會出現很多條請求的數據,我們按Size排序後找最大的,就是我們需要的數據。如下圖:


發送的請求的地址


得到的回應

        如上圖所示:打開網頁後瀏覽器發送了若幹條數據,其中有一條遠大於其它數據的包,大小為59.75k,我們可以認為這就是數據的來源了,而我們看到它指向了網址 https://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,這個方法可以模擬瀏覽器的訪問,我們輸入參數是網址,這個函數返回得到的網頁源代碼:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public static String HttpGet(String url) throws ClientProtocolException, IOException {  
  2.     //新建一個默認的連接  
  3.     DefaultHttpClient client = new DefaultHttpClient();  
  4.     //新建一個Get方法  
  5.     HttpGet get = new HttpGet(url);  
  6.     //得到網絡的回應  
  7.     HttpResponse response = client.execute(get);  
  8.     //獲得的網頁源代碼(xml)  
  9.     String content = null;  
  10.   
  11.     //如果服務器響應的是OK的話!  
  12.     if (response.getStatusLine().getStatusCode() == 200) {  
  13.         //以下是把網絡數據分段讀取下來的過程  
  14.         InputStream in = response.getEntity().getContent();  
  15.         byte[] data = new byte[1024];  
  16.         int length = 0;  
  17.         ByteArrayOutputStream bout = new ByteArrayOutputStream();  
  18.         while ((length = in.read(data)) != -1) {  
  19.             bout.write(data, 0, length);  
  20.         }  
  21.         //最後把字節流轉為字符串 轉換的編碼為utf-8.  
  22.         content = new String(bout.toByteArray(), "utf-8");  
  23.     }  
  24.     //返回得到的字符串 也就是網頁源代碼  
  25.     return content;  
  26. }  

        在上麵的Chrome信息窗口中看到,返回值是utf-8編碼的,所以在這裏我們使用了utf-8編碼,如果這裏我們使用"gbk"或者"gb-2312"就會出現亂碼,每個網站的編碼都不相同,具體情況要具體分析。        

        要注意的是,在我們的程序裏並不能直接使用這個函數,因為Android 4.0中,主線程不能進行網絡操作,所以我們需要開啟一個新的線程。在Android中,有一個成熟的類:AsyncTask(異步任務),可以完成這項工作(Thread + Handler也是一種方法,由於我們的工作比較簡單,暫時不提及它們)。

        接下來我們嚐試把XML源代碼顯示出來
        在MainActivity類中新建一個類GetSource,這個類繼承AsyncTask,用來獲取網頁上的數據;得到數據後使用Logcat(可以理解為Android上的控製台)打印出來:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. class GetSource extends AsyncTask<String, Void, String>{  
  2.       
  3.     //此函數用來處理後台的事物  
  4.     @Override  
  5.     protected String doInBackground(String... params) {  
  6.         try {  
  7.             //這裏調用了我們剛才寫的下載函數  
  8.             return Util.HttpGet("https://121.28.49.85:8080/datas/hour/130000.xml");  
  9.         } catch (IOException e) {}  
  10.         return null;  
  11.     }  
  12.       
  13.     //後台事物完成後,此函數用來更改界麵的內容  
  14.     @Override  
  15.     protected void onPostExecute(String result) {  
  16.         //讓Log輸出運行時的記錄  
  17.         Log.i("test",result);  
  18.     }  
  19.       
  20. }  

        最後在OnCreate函數的最後一行加上下麵一句,再添加網絡權限,然後我們來看看效果吧!
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. new GetSource().execute();  

3、界麵設計

        我們的目標是把軟件設計成文章開始時的那個樣式,這樣我們就要簡單地修改一下activity_main.xml:
        ·在上方放置一個TextView,背景為淺綠色,文字顏色為白色;
        ·下方是一個GridView,其中每個格子的顏色根據空氣質量來變化,格子中上方顯示城市名,下方顯示當前的AQI。
        ·同時在整個界麵上還要顯示一個轉圈的進度條ProgressBar,加載的時候顯示這個進度條
        代碼如下:
[html] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. <RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="https://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     tools:context=".MainActivity" >  
  6.   
  7.         <LinearLayout  
  8.             android:id="@+id/ll"  
  9.             android:layout_width="match_parent"  
  10.             android:layout_height="wrap_content"  
  11.             android:visibility="gone"  
  12.             android:orientation="vertical" >  
  13.   
  14.             <TextView  
  15.                 android:id="@+id/tv_time"  
  16.                 android:layout_width="fill_parent"  
  17.                 android:layout_height="wrap_content"  
  18.                 android:layout_margin="5dp"  
  19.                 android:background="#7a7"  
  20.                 android:padding="5dp"  
  21.                 android:textColor="#eee"  
  22.                 android:textSize="20sp"  
  23.                 android:textStyle="bold" />  
  24.   
  25.             <GridView  
  26.                 android:id="@+id/gv"  
  27.                 android:layout_width="match_parent"  
  28.                 android:layout_height="fill_parent"  
  29.                 android:layout_margin="5dp"  
  30.                 android:horizontalSpacing="3dp"  
  31.                 android:verticalSpacing="3dp"  
  32.                 android:numColumns="3" >  
  33.             </GridView>  
  34.         </LinearLayout>  
  35.   
  36.         <ProgressBar  
  37.             android:id="@+id/pb1"  
  38.             android:layout_width="wrap_content"  
  39.             android:layout_height="wrap_content"  
  40.             android:layout_centerHorizontal="true"  
  41.             android:visibility="visible"  
  42.             android:layout_centerVertical="true" />  
  43.   
  44. </RelativeLayout>  

        GridView 是一種網格樣式布局,在上麵的代碼裏我設置的每行格子個數為3個,還設置了格子之間的間距。每個格子中的布局需要另一個文件來控製:
gv.xml:
[html] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"  
  3.     android:id="@+id/bg"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="wrap_content"  
  6.     android:padding="8dp"  
  7.     android:orientation="vertical" >  
  8.   
  9.     <TextView  
  10.         android:id="@+id/tv_city"  
  11.         android:layout_width="wrap_content"  
  12.         android:layout_height="wrap_content"  
  13.         android:text="石家莊"  
  14.         android:textColor="#4bd"  
  15.         android:textSize="20sp"  
  16.         android:textStyle="bold" />  
  17.   
  18.     <TextView  
  19.         android:id="@+id/tv_aqi"  
  20.         android:layout_width="wrap_content"  
  21.         android:layout_height="wrap_content"  
  22.         android:text="223"  
  23.         android:textColor="#4bd"  
  24.         android:textSize="18sp"  
  25.         android:textStyle="bold" />  
  26.   
  27. </LinearLayout>  

4、數據分析

4.1 解析XML數據

        網上獲得的XML數據需要轉換成我們可以使用的結構化數據,這就使用了基於DOM的XML解析器。更改GetSource中的OnPostExecute中的代碼為:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. @Override  
  2. protected void onPostExecute(String result) {  
  3.     //建立一個解析器  
  4.     DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();  
  5.     DocumentBuilder builder;  
  6.     try {  
  7.         builder = factory.newDocumentBuilder();  
  8.         InputStream is = new ByteArrayInputStream(result.getBytes());  
  9.         Document document = builder.parse(is);  
  10.         Element element = document.getDocumentElement();  
  11.         //獲得所有的Citys節點數據  
  12.         NodeList cityList = element.getElementsByTagName("Citys");  
  13.           
  14.         //獲得mapstitle數據,並分解為兩部分  
  15.         NodeList title = element.getElementsByTagName("MapsTitle");  
  16.         String text1 = title.item(0).getTextContent();  
  17.         String t[] = text1.split("\\(");  
  18.         text1 = t[0];  
  19.         t = t[1].split(",");  
  20.         tv_time.setText(text1+ "\n" +t[0]);  
  21.           
  22.         Element citys = (Element)cityList.item(0);  
  23.         NodeList city = citys.getChildNodes();  
  24.           
  25.         for (int i=0;i < city.getLength();i++){  
  26.             //此時的city節點的item上,有的是一個城市的所有數據  
  27.             Node node = city.item(i);  
  28.             if (node.getNodeName().equalsIgnoreCase("city")) { //這是一個有效的節點  
  29.                 CityData cd = new CityData(node);  
  30.                 nodeList.add(node);  
  31.                 cdList.add(cd);  
  32.             }  
  33.         }  
  34.     } catch (Exception e) {}  

        這樣每個城市的數據就成為了一個Node,解析XML的過程比較複雜,需要不斷地去嚐試。

4.2 應用數據到布局

        接下來要編寫一個Adapter來連接布局和代碼。我們假設每一個每一個城市的數據都是一個Node(節點),GridView的數據存在一個列表裏,格子的個數與列表的長度有關。(所以如果河北省城市監測點變多了,程序也可以進行變化而自適應)分析數據源的XML可以得到,我們需要的是其中城市,和每個城市中監測點的列表。所以,在這裏新建兩個類:CityData、Pointer,其中,CityData包括一個Pointer的列表。以下是CityData類,Pointer類與這個相似:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class CityData implements Serializable{  
  2.       
  3.     /** 
  4.      * 繼承Serializable是為了在兩個不同的界麵中進行值的傳遞 
  5.      */  
  6.     private static final long serialVersionUID = -8473485404751986234L;  
  7.     //城市類包含了一個城市的數據  
  8.     public String name,dataTime,aqi,level,maxPoll,color,intro,tips;  
  9.     public List<Pointer> pointerList;  
  10.   
  11.     public CityData(Node cityNode) {  
  12.         super();  
  13.         //按標簽挨個取出相應標簽的內容  
  14.         this.name = getByTag(cityNode, "name");  
  15.         this.dataTime = getByTag(cityNode, "datatime");  
  16.         this.aqi = getByTag(cityNode, "aqi");  
  17.         this.level = getByTag(cityNode, "level");  
  18.         this.maxPoll = getByTag(cityNode, "maxpoll");  
  19.         String tmp = getByTag(cityNode, "color");  
  20.         this.color = tmp.replace("0x""#");  
  21.         this.intro = getByTag(cityNode, "intro");  
  22.         this.tips = getByTag(cityNode, "tips");  
  23.           
  24.         Element city = (Element)cityNode;  
  25.         NodeList pointers = city.getElementsByTagName("Pointer");  
  26.           
  27.         //向city的pointer列表中添加監測點  
  28.         pointerList = new ArrayList<Pointer>();  
  29.         for (int i=0;i<pointers.getLength();i++){  
  30.             Node pNode = pointers.item(i);  
  31.             pointerList.add(new Pointer(pNode));  
  32.         }  
  33.     }  
  34.   
  35.     //從XML的node中取出相應標簽中內容的function  
  36.     private String getByTag(Node node,String tag) {  
  37.         for (int i=0;i<node.getChildNodes().getLength();i++){  
  38.             if (tag.equalsIgnoreCase(node.getChildNodes().item(i).getNodeName()))  
  39.                 return node.getChildNodes().item(i).getTextContent();  
  40.         }  
  41.         return null;  
  42.     }  
  43. }  

        負責把數據填充到布局的Adapter:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. class gvAdapter extends BaseAdapter{  
  2.   
  3.     //Adapter的數據源,根據這個數據來填充網格列表  
  4.     List<CityData> cdList;  
  5.       
  6.     public gvAdapter(List<CityData> cdList) {  
  7.         super();  
  8.         this.cdList = cdList;  
  9.     }  
  10.   
  11.     //根據數據的多少返回相應的數值  
  12.     @Override  
  13.     public int getCount() {  
  14.         return cdList.size();  
  15.     }  
  16.   
  17.     @Override  
  18.     public Object getItem(int position) {  
  19.         return null;  
  20.     }  
  21.   
  22.     @Override  
  23.     public long getItemId(int position) {  
  24.         return 0;  
  25.     }  
  26.   
  27.     @Override  
  28.     public View getView(int position, View convertView, ViewGroup parent) {  
  29.         //解析之前寫好的每個網格的布局  
  30.         convertView = MainActivity.this.getLayoutInflater().inflate(R.layout.gv, null);  
  31.         //找到布局中的元素,和布局的背景  
  32.         TextView tv_city = (TextView)convertView.findViewById(R.id.tv_city);  
  33.         TextView tv_aqi = (TextView)convertView.findViewById(R.id.tv_aqi);  
  34.         View bg = convertView.findViewById(R.id.bg);  
  35.         //根據數據填充每個格子的內容和背景色  
  36.         tv_city.setText(cdList.get(position).name);  
  37.         tv_aqi.setText(cdList.get(position).aqi);  
  38.         bg.setBackgroundColor(Color.parseColor(cdList.get(position).color));  
  39.         return convertView;  
  40.     }  
  41.       
  42. }  

        最後在XML解析完成以後,添加一句:gv.setAdapter(new gvAdapter(cdList)); 即把數據填充到了網格中。

5、其它程序邏輯

        當點擊每個GridView的item的時候,跳轉到相應的城市詳細信息頁麵。
        右鍵項目的目錄 New --> Other -->Activity,給新的Activity起名為:CityActivity。依照上麵介紹的寫法給新的Activity寫好布局和邏輯。
        點擊主界麵中的網格,自動跳轉到新的界麵:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. gv.setOnItemClickListener(new OnItemClickListener() {  
  2.   
  3.     @Override  
  4.     public void onItemClick(AdapterView<?> parent, View view,int position, long id) {                                     
  5.                 //從當前的界麵跳轉到城市詳細信息界麵  
  6.     <span style="white-space:pre">  </span>Intent it = new Intent(MainActivity.this, CityActivity.class);  
  7.                 //對象本身無法使用Intent傳遞,但是我們繼承了Serializable,使傳遞成為了可能  
  8.         it.putExtra("node", cdList.get(position));  
  9.         startActivity(it);  
  10.     }  
  11.             });  

        1、在城市詳細信息界麵,點擊每個監測點,顯示該監測點的詳細數據。這裏需要用到對話框AlertDialog來顯示詳細數據。
        2、更改res --> values -->string.xml 中的內容,個性化我們的程序,如下:
[html] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. <resources>  
  2.     <string name="app_name">河北空氣質量</string>  
  3.     <string name="title_activity_city">城市數據</string>  
  4. </resources>  
        這樣,程序的名字就變成了“河北空氣質量”,在進入到詳細信息界麵的時候,標題欄也變成了“城市數據”
        3、找一個圖片,做成png格式,覆蓋res/drawable-hdpi/ic_launcher.png 文件,這樣就更改了在手機APP列表中的圖標了。




        其實做一個APP非常的簡單,隻要有想法,上麵的工作在一天內就可以完成。本文主要是想帶給大家一個思路,如何發掘身邊的一些內容,來做出自己的APP。
        可能光看講解不會太懂,那麼可以到https://download.csdn.net/detail/icyfox_bupt/6908053下載程序的源代碼

最後更新:2017-04-03 12:55:03

  上一篇:go android多屏幕分辨率適配
  下一篇:go 基於XMPP協議的Android即時通信係