火車票查詢
項目源碼下載鏈接:
Github:https://github.com/VincentWYJ/TrainTicketQuery
博客文件:http://files.cnblogs.com/files/tgyf/TrainTicketQuery.rar
1. 獲取api key
API Store鏈接地址:http://apistore.baidu.com/。
1.1 通過上述鏈接進入百度API Store主頁之后,左下角有一個“旅游票務”項,選擇其中的“去哪兒網火車票”(目前該項中只有這么一個選擇,沒有汽車、飛機等票務信息);
1.2 一般我們查詢的是站與站之間的余票情況,比如“杭州”—“北京”,本文中的案例就是以“站站搜索接口”來進行實現(其他接口使用方法類似);
1.3 點擊“站站搜索接口”項,頁面右邊是對應的使用方法與參數信息,包括調試、SDK下載鏈接,請求參數、請求示例、返回結果說明;
1.4 該接口套餐為免費,而且申請權限與訪問峰值都是沒有限制的(相信大家都喜歡這點);
1.5 使用該api唯一的條件是——獲得api key,點擊頁面上的“獲取apikey”,會跳轉到相應頁面,其實也不難,只要用百度賬號登錄后完善個人信息並提交就可以了(如果沒有百度賬號馬上花一點點時間申請一個);
到此,就能夠獲取到api key與相應的sdk了。不過如果采用Android來進行開發,可以利用Java語言欄的示例代碼直接用網絡資源請求類HttpURLConnection來完成火車票信息的查詢,而不需要像Android sdk壓縮包中給出的例子那樣添加額外的jar包(“ApiStoreSDK1.0.4.jar”),大家根據需求自行選擇,只要能得到想要的結果就OK。
說實話,相比於其他api接口申請步驟與套餐策略,去“哪兒網火車票”這個算是簡單使用了(絕非打廣告,誰用誰知道,只希望不要哪一天突然不支持)。
如果有朋友想做測試,但懶得自己動手獲取的,可以直接用實例代碼中的api key(在文件MainActivity.java開頭部分,項目源碼鏈接在文章開頭已給出)。還有一點需要知道的是:獲取過一次api key,那么之后再申請調用百度API Store中的其他接口是通過的,至於套餐是不是免費就是另外一回事了。
2. 火車票查詢實現
整個查詢流程大概是這樣的:
a. 輸入始發地、目的地,選好出發日期,點擊“查詢”按鍵開始嘗試火車票的查詢;
b. 先判斷網絡是否連接,若沒有,則進入網絡設置選擇頁面(用戶自己選擇是設置“移動數據”或“無線網絡”);
c. 若用戶沒有設置好網絡就返回到查詢主頁面,那么查詢過程終止,除非再次點擊“查詢”按鍵;
d. 若已經聯網或網絡設置好返回查詢頁面,那么還會判斷一次始發地與目的地的輸入字串是否為空,若有一個為空則給出Toast提示;
e. 若都不為空,則開始查詢,如果站--站名稱無誤,就會將查詢結果顯示在列表中(結果是以出發時間為升序排列);
項目源碼通過文章開頭鏈接可以下載到,所以接下來只針對部分重要或者需要注意的代碼進行分析,歡迎大家指正與討論。先上兩張效果圖:
2.1 AndroidManifest.xml文件中添加網絡訪問與狀態獲取的權限申請:
1 <!-- 查詢火車票需要聯網 --> 2 <uses-permission android:name="android.permission.INTERNET" /> 3 <!-- 查詢前進行網絡判斷, 需要獲取網絡狀態 --> 4 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
注意:網絡相關權限不管android sdk版本是L還是M(最新的已經為N--7.0),都是應用安裝好后自動打開的。不像文件寫權限“WRITE_EXTERNAL_STORAGE”等被Google定義為危險的權限類型,需要在應用使用過程中彈出自定義的頁面讓用戶選擇是否允許打開某權限的申請。
2.2 抽象出來一個簡單實用的類Utils:
1 private static final String TAG = "MainActivity"; 2 3 public static Context mContext; 4 5 public static boolean mIsBackFromSetNetwork; 6 7 /* 8 * 判斷網絡連接情況 9 */ 10 public static boolean isNetWorkConnected(){ 11 ConnectivityManager connectivityManager = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); 12 NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); 13 14 return networkInfo != null && networkInfo.isConnected(); 15 } 16 17 /* 18 * Toast 19 */ 20 public static void showToast(String message) { 21 Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); 22 } 23 24 /* 25 * Log 26 */ 27 public static void showLog(Object message) { 28 Log.i(TAG, ""+message); 29 }
方法isNetworkConnected()判斷設備是否聯網,showToast(String message)和showLog(String message)則分別對Toast和Log信息的顯示進行了封裝(使用時傳入要顯示的信息即可,不用每次都繁瑣地去指定那些固定的參數值)。
說明:Utils類中大部分變量與方法都是public static類型,需要特別注意mContext成員的賦值方式——最好不要將Activity的上下文環境(Context)賦給全局的Context類對象,如:
1 Utils.mContext = MainActivity.this
因為當應用程序完全退出之前全局變量是一直存在的,而如果那樣賦值之后,當Activity想destroy時,由於還有其他對象在引用其環境,可能出現內存泄漏。可行的方式之一:
1 Utils.mContext = getApplicationContext()
2.3 輸入始發地與目的地並選擇好時間后,點擊界面上的“查詢”按鈕,調用方法startTicketInquireThread():
1 private void startTicketInquireThread() { 2 if(Utils.isNetWorkConnected()) { 3 new Thread(new Runnable() { 4 5 @Override 6 public void run() { 7 inquireTrainTickets(); 8 } 9 }).start(); 10 } else { 11 Intent intent = new Intent(this, AccessNetworkActivity.class); 12 startActivity(intent); 13 } 14 }
2.3.1 首先判斷設備是否聯網(包括移動與無線,具體見Utils類),若檢測到網絡不通,則跳轉到網絡設置的選擇頁面AccessNetworkActivity,界面如下:
有兩個選擇:移動網絡和無線網絡,點擊后分別進入相應的系統網絡設置頁面:
而這兩個頁面也是Activity,對應的Intent建立代碼分別如下:
1 Intent intent = null; 2 //移動數據 3 intent = new Intent(Settings.ACTION_DATA_ROAMING_SETTINGS); 4 //無線網絡 5 intent = new Intent(Settings.ACTION_WIFI_SETTINGS);
以打開移動數據為例,如圖:
連接網絡后點擊back鍵返回到查詢主界面(或沒有打開直接返回),主程序MainActivity類會在onResume()方法中會緊接着做網絡連接判斷及相應的處理:
1 public void onResume() { 2 super.onResume(); 3 4 Utils.showLog("MainActivity Resume"); 5 6 if(Utils.mIsBackFromSetNetwork) { 7 if (Utils.isNetWorkConnected()) { 8 startTicketInquireThread(); 9 } else { 10 mResultMapList.clear(); 11 mResultListAdapter.notifyDataSetChanged(); 12 } 13 14 Utils.mIsBackFromSetNetwork = false; 15 } 16 }
這里該解釋下Utils類中聲明的變量mIsBackFromSetNetwork了,用於標記應用返回MainActivity頁面時是因為網絡設置還是其他原因(如從home界面重新回到本應用等)。如果該標記為true,那么進一步判斷網絡設置是否成功,如果成功則重新調用startTicketInquireThread()嘗試火車票查詢;如果不成功則將結果列表數據清空(不管之前列表中有沒有結果),真正做到界面上的列表清空代碼是第11行,由SimpleAdapter類對象mResultListAdapter調用方法notifyDataSetChanged(),該對象為ListView對象mResultMapList的數據適配器。
2.3.2 若一開始或者從網絡設置返回到查詢頁面時網絡連接正常,那么開始真正的查詢過程,調用方法inquireTrainTickets(),該方法開始時會先判斷始發地與目的地是否都有輸入:
1 String startPlace = mStartPlaceET.getText().toString(); 2 String endPlace = mEndPlaceET.getText().toString(); 3 if(TextUtils.isEmpty(startPlace) || TextUtils.isEmpty(endPlace)) { 4 mHandler.sendEmptyMessage(ERROR_WHAT); 5 }
TextUtils是很好用的一個類,其中有不少關於字符串的方法。代碼判斷如果始發地與目的地只要有一個為空,就通過Handler類的消息機制給出提示信息。其實這里完全可以直接調用Utils.showToast(String message)方法,但是下面會給出mHandler對象的定義,把查詢成功與失敗的處理放在了一起,便於統一管理。
1 private Handler mHandler = new Handler() { 2 3 @Override 4 public void handleMessage(Message msg) { 5 super.handleMessage(msg); 6 7 int what = msg.what; 8 if(what == SUCCESS_WHAT) { 9 mResultListAdapter.notifyDataSetChanged(); 10 }else if(what == ERROR_WHAT) { 11 Utils.showToast(getString(R.string.please_input_place)); 12 } 13 } 14 };
即地點輸入不全時,利用Toast提示用戶先輸入再查詢,至於字串的定義在res/values/strings.xml文件中,這里不進行描述。效果圖:
2.3.3 若從網絡設置返回到查詢頁面,網絡連接還是失敗,則清空列表數據,這點在上面onResume()方法的描述中已經提及了。
2.4 在講解火車票數據的獲取及處理之前,還要補充一點,觀察方法startTicketInquireThread()判斷網絡連接成功后,調用方法inquireTrainTickets()是在新開的一個線程中。這樣做可以說是普遍的處理方式或是良好的習慣,因為像獲取網絡數據這種操作一般都比較費時,放在主UI線程中肯定是不妥的(哪怕某次操作非常快,利用另一個線程處理總是保險的做法)。其實說到這,大家已經知道為什么要用Handler類機制了,它很重要的作用就是幫助子線程來在主UI線程中更新組件信息,本案例中是更新利用ListView顯示的火車票信息。
2.4.1 搜索站--站火車票的關鍵代碼定義在inquireTrainTickets()方法中:
1 private final String API_KEY = "361cf2a2459552575b0e86e0f62302bc"; 2 private final String HTTP_URL = "http://apis.baidu.com/qunar/qunar_train_service/s2ssearch"; 3 private final String HTTP_ARG = "version=1.0&from=START&to=END&date=YEAR-MOUTH-DAY"; 4 5 String httpUrl; 6 String httpArg = HTTP_ARG.replace("START", startPlace) 7 .replace("END", endPlace) 8 .replace("YEAR", ""+mYear) 9 .replace("MOUTH", mMouth>9?""+mMouth:"0"+mMouth) 10 .replace("DAY", mDayOfMouth>9?""+mDayOfMouth:"0"+mDayOfMouth); 11 12 Utils.showLog(httpArg); 13 14 BufferedReader reader = null; 15 String result = null; 16 StringBuffer sbf = new StringBuffer(); 17 httpUrl = HTTP_URL + "?" + httpArg; 18 try { 19 URL url = new URL(httpUrl); 20 HttpURLConnection connection = (HttpURLConnection) url 21 .openConnection(); 22 connection.setRequestMethod("GET"); 23 connection.setRequestProperty("apikey", API_KEY); 24 connection.connect(); 25 InputStream is = connection.getInputStream(); 26 reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); 27 String strRead = null; 28 while ((strRead = reader.readLine()) != null) { 29 sbf.append(strRead); 30 sbf.append("\r\n"); 31 } 32 reader.close(); 33 result = sbf.toString(); 34 } catch (Exception e) { 35 e.printStackTrace(); 36 }
注意開頭的1-3行代碼需要放在方法體之外,這里放在一起只是方便說明,分別定義了當前api的key、站--站搜索接口鏈接、以及版本+地址+時間的格式字串。
可以看到在對時間的月和日設置時,對值小於10的數據做了加“0”處理。這是因為api的規定,接口主頁上明確給出了格式為“出發日期:格式 YYYY-MM-DD”,也就是說年為4位,月和日均為2位(等到年為5位的時候估計不坐火車了)。當然字串HTTP_ARG的定義可以將待替換的部分用占位符聲明,這樣就不用多次調用replace(String src, String dst)方法了。
1 String HTTP_ARG = "version=1.0&from=%1$s&to=%2$s&date=%3$s-%4$s-%5$s"; 2 String string2 = String.format(HTTP_ARG, startPlace, endPlace, ""+mYear, mMouth>9?""+mMouth:"0"+mMouth, mDayOfMouth>9?""+mDayOfMouth:"0"+mDayOfMouth);
代碼是不是簡潔多了,占位符序號從1開始,傳入參數時也必須按照聲明的順序。
2.4.2 獲取的結果存在字串result中,這里有兩種情況:第一種是地址錯誤,那么得到的結果為空;第二種就是成功獲取到火車票信息,需要在列表中進行顯示。不管哪種情況,都會在方法inquireTrainTickets()末尾調用了方法setTrainTicketList(String result),這里有必要貼一下代碼:
1 private void setTrainTicketList(String result) { 2 mResultMapList.clear(); 3 try { 4 JSONArray jsonArray1 = new JSONObject(result).getJSONObject("data").getJSONArray("trainList"); 5 mJsonArray = jsonArray1; 6 for(int i=0; i<jsonArray1.length(); ++i) { 7 JSONObject jsonObject1 = jsonArray1.getJSONObject(i); 8 Map<String, Object> map = new HashMap<String, Object>(); 9 map.put(START_TIME, jsonObject1.get(START_TIME)); 10 map.put(END_TIME, jsonObject1.get(END_TIME)); 11 map.put(FROM, jsonObject1.get(FROM)); 12 map.put(TO, jsonObject1.get(TO)); 13 map.put(TRAIN_NO, jsonObject1.get(TRAIN_NO)); 14 map.put(DURATION, jsonObject1.get(DURATION)); 15 JSONArray jsonArray2 = jsonObject1.getJSONArray(SEAT_INFOS); 16 JSONObject jsonObject2 = jsonArray2.getJSONObject(0); 17 map.put(SEAT, jsonObject2.get(SEAT)); 18 map.put(SEAT_PRICE, jsonObject2.get(SEAT_PRICE)+getString(R.string.train_price_unit)); 19 map.put(REMAIN_NUM, jsonObject2.get(REMAIN_NUM)+getString(R.string.train_ticket_unit)); 20 mResultMapList.add(map); 21 } 22 } catch (JSONException e) { 23 e.printStackTrace(); 24 } 25 Collections.sort(mResultMapList, mComparatorResultMap); 26 mHandler.sendEmptyMessage(SUCCESS_WHAT); 27 }
進入方法后,代碼第2行將列表數據清空,第26行則發送消息進而更新列表。由於網絡請求結果一般為JSON格式字串,所以往往需要將String對象轉化為JSONObject對象再進行具體數據的提取。和之前網絡請求過程類似,本案例中JSON字串處理的代碼較基礎,不在展開詳述了,如果對這塊不熟悉的朋友可以自學習下相關知識,入門還是不難的。
補充:關於JSON字串的處理,上面代碼雖然不算復雜也勉強能夠達到目的,但從實用、簡潔與面向對象的角度來說是落伍了。之后有時間打算用GSon+GsonFormat進行實現,GsonFormat作為一款插件,可以快速建立JSON字串對應的JavaBean類,然后利用GSON對象操作數據,非常好用,可以說它們將眼花繚亂的字串變成了我們熟悉的類。
2.4.3 前面提到列表中顯示的火車票信息是按照出發時間升序排列的,即將時間早的排在前面,這樣比較符合用戶的查詢習慣。而進列表數據進行排序的代碼為:
1 Collections.sort(mResultMapList, mComparatorResultMap);
這行代碼在上面setTrainTicketList(String result)方法的第25行,先看一下Collections中關於該方法的源碼:
1 public static void sort(List list, Comparator comparator) { 2 throw new RuntimeException("Stub!"); 3 }
第一個參數為List對象,第二個參數為Comparator對象。接着給出接口Comparator的源碼:
1 public interface Comparator { 2 public abstract int compare(Object obj, Object obj1); 3 public abstract boolean equals(Object obj); 4 }
我們的列表對象mResultMapList的類型為List<Map<String, Object>>,滿足第一個參數的List類型要求,但是其內部數據類型為Map<String, Object>,既不是String也不是Object,這時候就需要自己實現compare(Obejct obj, Object obj1)方法了。
1 private Collator mCollator = Collator.getInstance(Locale.CHINA); 2 private Comparator<Map<String, Object>> mComparatorResultMap = new Comparator<Map<String, Object>>() { 3 4 @Override 5 public int compare(Map<String, Object> lhs, Map<String, Object> rhs) { 6 return mCollator.compare(lhs.get(START_TIME), rhs.get(START_TIME)); 7 } 8 };
首先聲明一個Collator類對象,如果在中文環境中使用,在獲取實例時必須傳入Locale.CHINA類型,否則對中文字串的排序不准確或無效。其實重載該方法的關鍵就是在返回值部分,調用了Collator類的compare(Object object1, Object object2),源碼如下:
1 public int compare(Object object1, Object object2) { 2 return compare((String) object1, (String) object2); 3 }
進而調用了其自身的抽象方法compare(String string1, String string2):
1 public abstract int compare(String string1, String string2);
所以,最終進行比較的是String類型值,只要將Map<String, Object>對象中字段START_TIME對應的字串值當做參數傳入即可。順便將START_TIME等變量定義給出:
1 private final String START_TIME = "startTime"; 2 private final String END_TIME = "endTime"; 3 private final String FROM = "from"; 4 private final String TO = "to"; 5 private final String TRAIN_NO = "trainNo"; 6 private final String DURATION = "duration"; 7 private final String SEAT_INFOS = "seatInfos"; 8 private final String SEAT = "seat"; 9 private final String SEAT_PRICE = "seatPrice"; 10 private final String REMAIN_NUM = "remainNum";
很簡單的定義,這里想說明的是:如果采用本案例中的常規方式進行JSON字串的處理(不用其他庫或插件),那么就好將變量的值設置為與JSON字串中key-value部分的key值一致,這樣在以后的處理過程中比較直觀,不易出錯。
2.4.4 運行過程序的朋友就會知道,點擊列表中的某一車次信息后,會彈出一個對話框,顯示具體的座位與余票信息,這個和前一步顯示所有查詢結果類似,不再進行講解了。感興趣的可以閱讀相關代碼中方法showTicketsInfoDialog(String trainNo),通過列車號作為索引來獲取具體信息,效果圖如下:
需要注意的是:從獲取網絡請求結果到顯示在列表經歷了網絡請求->結果返回->String到JSONObject類型轉換->數據提取成Map對象添加到List列表->列表信息排序->顯示,經過排序之后列表中的火車票信息與網絡請求返回的順序很大可能是不一致的,所以在點擊某一車次獲取具體信息時不能夠以列表元素下標作為索引,而是要以列車號為索引(傳給showTicketsInfoDialog(String trainNo)方法)。還有就是對話框布局是自定義的,有三部分組成:標題(包含當前車次號)、余票(以座位類型區分)、確認按鈕。
2.4.5 始發地與目的地的互換,就是將兩個EditText組件的值進行調換,給出代碼:
1 String startPlace = mStartPlaceET.getText().toString(); 2 String endPlace = mEndPlaceET.getText().toString(); 3 mStartPlaceET.setText(endPlace); 4 mEndPlaceET.setText(startPlace);
2.4.6 關於時間的選擇,要好好地說一說。目前我國的購票政策是:從今天算起,能夠買到60天之內的票。那么我們在實現時間的選擇器的時候,就要規定好可選時間的區間(總不能老提示用戶您選的時間不對吧),獲取當前時間與計算時間上限的代碼:
1 mCalendar = Calendar.getInstance(); 2 mMinTimeMills = mCalendar.getTimeInMillis(); 3 mMaxTimeMills = mMinTimeMills+59l*24*3600*1000;
時間上下限的單位為毫秒,變量類型為long。順便提一下從Calendar實例中獲取年月日的代碼:
1 mYear = mCalendar.get(Calendar.YEAR); 2 mMouth = mCalendar.get(Calendar.MONTH)+1; 3 mDayOfMouth = mCalendar.get(Calendar.DAY_OF_MONTH);
月份需要進行+1處理,即如果這個月為8月,實際獲取的值為7。還有獲取星期幾的情況也是如此,
1 mDayOfWeek = mCalendar.get(Calendar.DAY_OF_WEEK);
獲取的值可能是1,2,3,4,5,6,7之一,對應的星期其實是日,一,二,三,四,五,六,即也存在差一的情況。
下面貼出日期選擇對話框的布局與實現代碼:
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:orientation="vertical" 6 android:gravity="center" 7 android:background="#ffffffff" > 8 9 <DatePicker 10 android:id="@+id/train_datepicker" 11 android:layout_width="wrap_content" 12 android:layout_height="wrap_content" 13 android:calendarViewShown="false" 14 android:layout_marginTop="0dp" 15 android:layout_marginBottom="10dp" /> 16 17 <View 18 style="@style/TrainLine" /> 19 20 <LinearLayout 21 android:layout_width="match_parent" 22 android:layout_height="60dp" 23 android:padding="0dp" 24 android:orientation="horizontal" 25 android:gravity="center" > 26 27 <TextView 28 android:id="@+id/date_cancel" 29 android:layout_width="0dp" 30 android:layout_height="wrap_content" 31 android:layout_weight="1" 32 android:text="@string/cancel" 33 android:textColor="#ff000000" 34 android:gravity="center" /> 35 36 <View 37 android:layout_width="0.5dp" 38 android:layout_height="match_parent" 39 android:background="#ffff0000" /> 40 41 <TextView 42 android:id="@+id/date_confirm" 43 android:layout_width="0dp" 44 android:layout_height="wrap_content" 45 android:layout_weight="1" 46 android:text="@string/confirm" 47 android:textColor="#ff000000" 48 android:gravity="center" /> 49 50 </LinearLayout> 51 52 </LinearLayout>
1 private void showDateSelectDialog() { 2 final Dialog dateDialog = new Dialog(this, R.style.DialogFixTitle); 3 dateDialog.setContentView(R.layout.train_datepicker); 4 dateDialog.setCanceledOnTouchOutside(true); 5 TextView cancel = (TextView) dateDialog.findViewById(R.id.date_cancel); 6 cancel.setOnClickListener(new OnClickListener() { 7 8 @Override 9 public void onClick(View v) { 10 dateDialog.cancel(); 11 } 12 }); 13 TextView confirm = (TextView) dateDialog.findViewById(R.id.date_confirm); 14 confirm.setOnClickListener(new OnClickListener() { 15 16 @Override 17 public void onClick(View v) { 18 dateDialog.cancel(); 19 mTargetDayTV.setText(tempTargetDay); 20 getDateParams(); 21 startTicketInquireThread(); 22 } 23 }); 24 DatePicker datePicker = (DatePicker) dateDialog.findViewById(R.id.train_datepicker); 25 datePicker.setMinDate(mMinTimeMills); 26 datePicker.setMaxDate(mMaxTimeMills); 27 datePicker.init(mYear, mMouth-1, mDayOfMouth, new OnDateChangedListener() { 28 29 @Override 30 public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) { 31 mCalendar.set(year, monthOfYear, dayOfMonth); 32 tempTargetDay = getTempTargetDay(year, monthOfYear+1, dayOfMonth, mCalendar.get(Calendar.DAY_OF_WEEK)); 33 34 Utils.showLog(tempTargetDay); 35 } 36 }); 37 dateDialog.show(); 38 }
布局文件沒什么好說的,除了兩個按鈕,較關鍵的就是DatePicker,是Android自帶的時間選擇組件組件。效果圖:
首先從布局中獲取組件實例,進行時間區間上下限的設置;接着調用init(mYear, mMouth-1, mDayOfMouth, new OnDateChangedListener())方法對DatePicker類對象dataPicker進行初始化,同樣地,傳入月份時需要做-1處理;然后在其中重載監聽器OnDateChangedListener接口的onDateChanged()方法,可以獲取到DatePicker組件選擇后的結果。
時間選擇好后,點擊“確認”按鈕會調用做兩件事情:1、更新時間選擇按鈕的信息,顯示為最新選擇的日期;2、調用方法startTicketInquireThread()開始嘗試對應時間的火車票查詢,就不用再次點擊“查詢”按鍵了。
不過這個時間選擇組件在不同機器上的顯示形式會不一樣,感覺上面最左圖好丑有沒有。如果想做到像去哪兒app(上面最右圖)那樣統一的效果,還得想其他方法(比如萬能的自定義)。
2.4.7 最后貼一下網絡請求返回數據,當地址錯誤等原因引起查詢結果為空時:
{
"ret":true,
"data":{
"trainList":null
}
}
正常的火車票信息,時間為2016年9月13日 周二,杭州到北京:
{
"ret":true,
"data":{
"trainList":[
{
"trainType":"直達特快","trainNo":"Z10","from":"杭州","to":"北京",
"startTime":"17:17","endTime":"07:34","duration":"14時17分",
"seatInfos":[
{"seat":"無座","seatPrice":"192","remainNum":278},
{"seat":"硬座","seatPrice":"192","remainNum":526},
{"seat":"硬卧","seatPrice":"328","remainNum":365},
{"seat":"軟卧","seatPrice":"515","remainNum":4}]
},
{
"trainType":"空調特快","trainNo":"T32","from":"杭州","to":"北京",
"startTime":"18:20","endTime":"10:26","duration":"16時6分",
"seatInfos":[
{"seat":"高級軟卧","seatPrice":"949","remainNum":4},
{"seat":"無座","seatPrice":"192","remainNum":236},
{"seat":"硬座","seatPrice":"192","remainNum":365},
{"seat":"硬卧","seatPrice":"328","remainNum":164},
{"seat":"軟卧","seatPrice":"515","remainNum":28},
{"seat":"一人軟包","seatPrice":"1342","remainNum":2}]
}
]
}
}
3. 總結
本文利用百度API Store中去哪兒網提供火車票查詢接口,實現了國內簡單的站--站火車票搜索功能。整個過程中,網絡數據請求其實比較簡單,難的是對獲取到的數據進行處理,以及做出美觀、交互性強的界面。所以,后續有時間會對案例進行優化,目前想到的可以做的事情包括:
a. 顯示火車票信息的列表可以用RecyclerView替代ListView,添加下拉刷新等功能;
b. 時間選擇組件的統一化,讓不同設備顯示出的畫面一致;
c. 始發地與目的地的設置,不再利用EditText組件進行文本輸入,而是像一般上線的app那樣直接提供地區選擇列表;
d. 將項目改用Android Studio實現,添加引用庫或者插件簡直so easy;
對於火車票的預訂暫時沒什么想法,畢竟涉及到支付了,不過關於這塊內容希望有開發經驗的前輩可以指點一二,沒經驗的也歡迎一起學習與討論,謝謝。