寫在前面:出去玩免不了擠公交、等地鐵,不知道乘車方案當然不行,用官方APP吧,缺點一大堆,手機浏覽器在線查的話既慢又麻煩...為了解決這些問題,我們來做一個簡版的出行助手,嘛嘛再也不用擔心我會迷路了_\^o^/_
(一)功能需求分析
[基礎功能]
1.能夠根據起點站和終點站查詢乘車方案,並顯示多種乘車方案
2.能夠根據公交路線號查詢沿途站點(防止坐過站...)
[擴展功能]
3.GPS定位獲取起點站(距離當前位置最近的站點名)[後來放棄了,費電,費流量...]
4.顯示地圖[後來也放棄了,地圖對用戶來說好像沒什麼太大用處(當然喜歡走路的另當別論),至少對本人來說地圖沒什麼用]
(二)可實現性分析
1.百度地圖開放平台提供的API可以實現乘車方案查詢
2.3.4.同上,結論:完全可以實現需要的所有功能
(三)開發前提
1.需要BaiDuMap的開發者賬號
2.需要key(現在新版的地圖key與App唯一綁定)
3.需要官方提供的jar包
搜索一下“百度地圖開發”,上面的三件事情分分鐘搞定
[說到這裡不得不贊一下這極低的門檻了,騰訊、新浪微博...的開發者賬號就很難認證,有的甚至需要上傳身份證復印件...]
(四)研究API文檔以及Demo
API文檔說實話做得不怎麼樣,函數詳解都只有一句話,建議直接看Demo,附有大量注釋,簡單易懂
(五)開始編碼(下面給出的源碼都親測可用,並附有最詳細的注釋)
package com.ayqy.app_gowithme;
import java.util.ArrayList;
import android.app.Activity;
import android.app.AlertDialog.Builder;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.InputType;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.baidu.mapapi.BMapManager;
import com.baidu.mapapi.search.MKAddrInfo;
import com.baidu.mapapi.search.MKBusLineResult;
import com.baidu.mapapi.search.MKDrivingRouteResult;
import com.baidu.mapapi.search.MKLine;
import com.baidu.mapapi.search.MKPlanNode;
import com.baidu.mapapi.search.MKPoiInfo;
import com.baidu.mapapi.search.MKPoiResult;
import com.baidu.mapapi.search.MKRoute;
import com.baidu.mapapi.search.MKSearch;
import com.baidu.mapapi.search.MKSearchListener;
import com.baidu.mapapi.search.MKShareUrlResult;
import com.baidu.mapapi.search.MKSuggestionResult;
import com.baidu.mapapi.search.MKTransitRoutePlan;
import com.baidu.mapapi.search.MKTransitRouteResult;
import com.baidu.mapapi.search.MKWalkingRouteResult;
public class SearchPlan extends Activity{
BMapManager mBMapMan = null;
ListView list;
private MKSearch mSearch = null; // 搜索模塊,也可去掉地圖模塊獨立使用
private String city;//城市
private String start;//起點
private String end;//終點
EditText txtCity;
EditText txtStart;
EditText txtEnd;
Button btnSearch;
ArrayList<String> plans;//方案列表
ArrayList<String> details;//詳細方案
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
//獲取SDK使用權限
mBMapMan=new BMapManager(getApplication());
mBMapMan.init("MzKbxcvX7gBEqpeW2kdmOqhx", null);
//注意:上面方法的第一個參數必須要填自己申請的key
//因為App的PackageName--Shell值--key都唯一對應
//用別人的key肯定會出現授權失敗的錯誤,結果就是什麼API都別想用
this.setContentView(R.layout.query);
//關聯控件
list = (ListView) findViewById(R.id.List);
txtCity = (EditText) findViewById(R.id.txtCity);
txtStart = (EditText) findViewById(R.id.txtStart);
txtEnd = (EditText) findViewById(R.id.txtEnd);
btnSearch = (Button) findViewById(R.id.btnSearch);
//設置按鈕半透明
btnSearch.getBackground().setAlpha(204);
//
txtStart.setText("小居安");
//
btnSearch.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//搜索乘車方案
//判空
start = txtStart.getText().toString().trim();
end = txtEnd.getText().toString().trim();
city = txtCity.getText().toString().trim();
if("".equals(start) || "".equals(end) || "".equals(city))
{
Toast.makeText(SearchPlan.this, "...到底想去哪裡", Toast.LENGTH_SHORT).show();
return;
}
else
{
//搜索方案
searchPlans();
}
}
});
// 初始化搜索模塊,注冊事件監聽
mSearch = new MKSearch();
mSearch.init(mBMapMan, new MKSearchListener() {
@Override
public void onGetPoiDetailSearchResult(int type, int error) {
}
public void onGetDrivingRouteResult(MKDrivingRouteResult res,
int error) {
}
public void onGetTransitRouteResult(MKTransitRouteResult res,
int error) {
if (error != 0 || res == null) {
showError(error);
return;
}
// 得到解決方案
int maxPlanNum = res.getNumPlan();//方案數
//顯示方案列表
plans = new ArrayList<String>();
details = new ArrayList<String>();
for(int i = 0;i < maxPlanNum;i++)
{
//得到詳細方案
MKTransitRoutePlan routePlan = res.getPlan(i);
//獲取時耗
int time = routePlan.getTime() / 60;
int hour = time / 60;
int min = time % 60;
// 公交線路
MKLine mkLine = routePlan.getLine(0);
//記錄方案詳細信息
StringBuilder sb = new StringBuilder();
sb.append("[預計耗時:");
if(hour > 0)sb.append(hour + " 小時 ");
sb.append(min);
sb.append("分鐘]\n\n乘坐:");
sb.append(mkLine.getTitle());
MKPoiInfo mkOnPoiInfo = mkLine.getGetOnStop();
MKPoiInfo mkOffPoiInfo = mkLine.getGetOffStop();
sb.append("\n從");
sb.append(mkOnPoiInfo.name);
sb.append("上車,在");
sb.append(mkOffPoiInfo.name);
sb.append("下車\n[途經 ");
sb.append(mkLine.getNumViaStops());
sb.append(" 站]");
if (routePlan.getNumLines() > 0) {
// 循環當前方案公交路線
for (int j = 1; j < routePlan.getNumLines(); j++) {
// 公交線路
mkLine = routePlan.getLine(j);
sb.append("\n換乘:");
sb.append(mkLine.getTitle());
mkOnPoiInfo = mkLine.getGetOnStop();
mkOffPoiInfo = mkLine.getGetOffStop();
sb.append("\n從");
sb.append(mkOnPoiInfo.name);
sb.append("上車,在");
sb.append(mkOffPoiInfo.name);
sb.append("下車\n[途經 ");
sb.append(mkLine.getNumViaStops());
sb.append(" 站]");
}
}
//填充詳細方案列表
details.add(sb.toString());
//填充方案列表
plans.add("方案 " + (i + 1));
}
//顯示方案列表
showPlans();
}
public void onGetWalkingRouteResult(MKWalkingRouteResult res,
int error) {
}
public void onGetAddrResult(MKAddrInfo res, int error) {
}
//線路查詢相關
public void onGetPoiResult(MKPoiResult res, int type, int error) {
// 錯誤號可參考MKEvent中的定義
if (error != 0 || res == null) {
showError(error);
return;
}
// 找到公交路線poi node
MKPoiInfo curPoi = null;
int totalPoiNum = res.getNumPois();
for (int idx = 0; idx < totalPoiNum; idx++) {
curPoi = res.getPoi(idx);
// ePoiType-->poi類型,0:普通點,1:公交站,2:公交線路,3:地鐵站,4:地鐵線路
if (2 == curPoi.ePoiType) {
break;
}
}
mSearch.busLineSearch(curPoi.name, curPoi.uid);
}
//線路查詢相關
public void onGetBusDetailResult(MKBusLineResult result, int error) {
if (error != 0 || result == null) {
showError(error);
return;
}
//獲取詳細路線
MKRoute route = result.getBusRoute();
int num = route.getNumSteps();//關鍵點數量
//循環獲取關鍵點描述
String[] arrInfo = new String[num + 1];
for(int i = 0;i < num;i++)
arrInfo[i + 1] = route.getStep(i).getContent();
//獲取運營時間信息
String busName = result.getBusName();
String startTime = result.getStartTime();
String endTime = result.getEndTime();
arrInfo[0] = busName + "\n首班:" + startTime + "\n末班:" + endTime;
//將數據裝入Bundle
Bundle data = new Bundle();
data.putStringArray("routeInfo", arrInfo);
//新建Intent
Intent intent = new Intent(SearchPlan.this,ShowRoute.class);
intent.putExtras(data);
//啟動對應Activity
startActivity(intent);
}
@Override
public void onGetSuggestionResult(MKSuggestionResult res, int arg1) {
// TODO Auto-generated method stub
}
@Override
public void onGetShareUrlResult(MKShareUrlResult arg0, int arg1,
int arg2) {
// TODO Auto-generated method stub
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(android.view.MenuItem item){
if(item.getItemId() == R.id.searchRoute)
{//顯示線路查詢對話框
//創建Builder對象
Builder builder = new Builder(SearchPlan.this);
builder.setTitle("查詢線路詳情");
builder.setIcon(R.drawable.search);
//設置對話框內容
LinearLayout view = new LinearLayout(SearchPlan.this);
view.setOrientation(LinearLayout.HORIZONTAL);
TextView lbl1 = new TextView(SearchPlan.this);
lbl1.setText("查詢");
lbl1.setTextSize(18);
final EditText txtCity = new EditText(SearchPlan.this);
txtCity.setHint("城市名");
txtCity.setText("西安");
TextView lbl2 = new TextView(SearchPlan.this);
lbl2.setText("公交");
lbl2.setTextSize(18);
final EditText txtBus = new EditText(SearchPlan.this);
txtBus.setInputType(InputType.TYPE_CLASS_NUMBER);//限輸入數字
txtBus.setSingleLine();//單行
txtBus.setHint("線路名");
TextView lbl3 = new TextView(SearchPlan.this);
lbl3.setText("路");
lbl3.setTextSize(18);
view.addView(lbl1);
view.addView(txtCity);
view.addView(lbl2);
view.addView(txtBus);
view.addView(lbl3);
builder.setView(view);
//添加按鈕
builder.setPositiveButton("查詢", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//查詢路線信息
//判空
String city = txtCity.getText().toString().trim();
String bus = txtBus.getText().toString().trim();
if("".equals(city) || "".equals(bus))
{
Toast.makeText(SearchPlan.this, "...到底想查詢什麼", Toast.LENGTH_SHORT).show();
return;
}
//顯示提示信息
showTip();
//POI搜索
mSearch.poiSearchInCity(city, bus);
}
});
builder.setNegativeButton("取消", null);
//顯示對話框
builder.create().show();
}
if(item.getItemId() == R.id.exit)
finish();//退出程序
return true;
};
//自定義方法
private void searchPlans()
{//搜索乘車方案
//對起點終點的name進行賦值,也可以直接對坐標賦值,賦值坐標則將根據坐標進行搜索
MKPlanNode stNode = new MKPlanNode();
stNode.name = start;
MKPlanNode enNode = new MKPlanNode();
enNode.name = end;
//顯示提示信息
showTip();
//搜索
mSearch.transitSearch(city, stNode, enNode);
}
private void showPlans()
{
//為ListView綁定Adapter
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this
, android.R.layout.simple_list_item_multiple_choice
, plans);
list.setAdapter(adapter);
list.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
//為ListView添加ItemClick事件監聽
list.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int index,
long arg3) {
//跳轉到ListActivity,同時傳遞索引參數
//創建Intent
Intent intent = new Intent(SearchPlan.this
,ShowDetail.class);
//創建Bundle攜帶數據
Bundle bundle = new Bundle();
bundle.putInt("index", index);
bundle.putString("detail", details.get(index));
//數據綁定
intent.putExtras(bundle);
//啟動Intent對應的Activity
startActivity(intent);
}
});
}
private void showError(int error)
{
String msg = null;
switch(error)
{
case 2:msg="T_T網絡連接錯誤...";break;
case 3:msg="T_T網絡數據錯誤...";break;
case 4:msg="T_T路線搜索起點或終點有歧義...";break;
case 100:msg="T_T未找到搜索結果...";break;
case 300:msg="T_T授權驗證失敗...";break;
default:msg="T_T未知錯誤...";
}
Toast.makeText(SearchPlan.this, msg,
Toast.LENGTH_LONG).show();
}
private void showTip()
{//顯示提示信息
Toast.makeText(SearchPlan.this
, "(*>.<*)正在拼命搜索..."
, Toast.LENGTH_LONG).show();
}
//重寫baiduMap中的方法
@Override
protected void onDestroy() {
mSearch.destory();
if(mBMapMan!=null){
mBMapMan.destroy();
mBMapMan=null;
}
super.onDestroy();
}
@Override
protected void onPause(){
if(mBMapMan!=null){
mBMapMan.stop();
}
super.onPause();
}
@Override
protected void onResume(){
if(mBMapMan!=null){
mBMapMan.start();
}
super.onResume();
}
}
[
package com.ayqy.app_gowithme;
import android.app.ListActivity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
public class ShowDetail extends ListActivity{
ListView root;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
//設置背景
root = getListView();
root.setBackgroundResource(R.drawable.blue_bg);
//獲取數據
Intent intent= getIntent();
Bundle bundle = intent.getExtras();
int index = bundle.getInt("index") + 1;
String detail = bundle.getString("detail");
//設置Adapter
String[] arr = new String[]{"方案 " + index + " 詳情:", detail};
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this
, android.R.layout.simple_list_item_1
, arr);
this.setListAdapter(adapter);
}
}
package com.ayqy.app_gowithme;
import android.app.ListActivity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
public class ShowRoute extends ListActivity{
ListView root;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
//設置背景
root = getListView();
root.setBackgroundResource(R.drawable.blue_bg);
//獲取數據
Intent intent= getIntent();
Bundle bundle = intent.getExtras();
String[] arrInfo = bundle.getStringArray("routeInfo");
//設置Adapter
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this
, android.R.layout.simple_list_item_1
, arrInfo);
this.setListAdapter(adapter);
}
}
<?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:gravity="bottom"
android:background="@drawable/blue_bg"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:text="@string/city"
android:textSize="24sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<EditText
android:id="@+id/txtCity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/cityHint"
android:text="@string/XiAn"
android:singleLine="true"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:text="@string/start"
android:textSize="24sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<EditText
android:id="@+id/txtStart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/stationHint"
android:singleLine="true"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:text="@string/end"
android:textSize="24sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<EditText
android:id="@+id/txtEnd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/stationHint"
android:singleLine="true"
/>
</LinearLayout>
<Button
android:id="@+id/btnSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="@string/searchPlan"
/>
<ListView android:id="@+id/List"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
</ListView>
</LinearLayout>
P.S.源碼都在上面,如有疑問請在下方留言
(六)顯示地圖
[最先實現的就是這個(第一次開發地圖有點激動,想看看地圖長什麼樣子...),在需求中本沒打算設計,雖然後來放棄了,但下面的源碼仍然親測無誤]
package com.ayqy.app_gowithme;
import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.Menu;
import android.widget.TextView;
import android.widget.Toast;
import com.baidu.mapapi.BMapManager;
import com.baidu.mapapi.map.MKMapViewListener;
import com.baidu.mapapi.map.MapController;
import com.baidu.mapapi.map.MapPoi;
import com.baidu.mapapi.map.MapView;
import com.baidu.platform.comapi.basestruct.GeoPoint;
public class ShowMap extends Activity {
BMapManager mBMapMan = null;
MapView mMapView = null;
MapController mMapController = null;
TextView txt;
boolean pressAgain = false;
long pressTime = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBMapMan=new BMapManager(getApplication());
mBMapMan.init("MzKbxcvX7gBEqpeW2kdmOqhx", null);
//注意:請在試用setContentView前初始化BMapManager對象,否則會報錯
setContentView(R.layout.activity_main);
mMapView=(MapView)findViewById(R.id.bmapsView);
mMapController=mMapView.getController();
//txt = (TextView) findViewById(R.id.txt);
// 得到mMapView的控制權,可以用它控制和驅動平移和縮放
GeoPoint point =new GeoPoint((int)(34.151884* 1E6),(int)(108.882024* 1E6));
//34.151884,108.882024西北大學16級別
//用給定的經緯度構造一個GeoPoint,單位是微度 (度 * 1E6)
mMapController.enableClick(true);//設置地圖響應點擊事件
mMapController.setCenter(point);//設置地圖中心點
mMapController.setZoom(16);//設置地圖zoom級別
mMapView.setBuiltInZoomControls(false);//不顯示內置縮放控件
//創建MKMapViewListener
MKMapViewListener listener = new MKMapViewListener() {
@Override
public void onMapMoveFinish() {
// TODO Auto-generated method stub
}
@Override
public void onMapLoadFinish() {
}
@Override
public void onMapAnimationFinish() {
// TODO Auto-generated method stub
}
@Override
public void onGetCurrentMap(Bitmap arg0) {
// TODO Auto-generated method stub
}
@Override
public void onClickMapPoi(MapPoi arg0) {
//點到地圖標注時顯示詳細
if(arg0 != null)
{
GeoPoint point = arg0.geoPt;//獲取GEO坐標
mMapController.setCenter(point);//設置地圖中心點
mMapController.zoomIn();//放大一個級別
}
}
};
//為map注冊監聽器
mMapView.regMapViewListener(mBMapMan, listener);
}
@Override public void onBackPressed() {
//按下返回鍵,縮小地圖
float min = 12f;//設置最小縮放級別
if(mMapView.getZoomLevel() > min)
mMapController.zoomOut();//縮小一個級別
else
{
if(pressAgain && (System.currentTimeMillis() - pressTime < 1000))
super.finish();//一秒內再按結束程序
else
pressAgain = false;
if(!pressAgain)
{
Toast.makeText(ShowMap.this, "再按一次退出...", Toast.LENGTH_SHORT).show();
pressTime = System.currentTimeMillis();//獲取第一次按下的時間
pressAgain = true;
}
}
};
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(android.view.MenuItem item){
if(item.getItemId() == R.id.exit)
finish();//退出程序
return true;
};
//重寫baiduMap中的方法
@Override
protected void onDestroy() {
mMapView.destroy();
if(mBMapMan!=null){
mBMapMan.destroy();
mBMapMan=null;
}
super.onDestroy();
}
@Override
protected void onPause(){
mMapView.onPause();
if(mBMapMan!=null){
mBMapMan.stop();
}
super.onPause();
}
@Override
protected void onResume(){
mMapView.onResume();
if(mBMapMan!=null){
mBMapMan.start();
}
super.onResume();
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:scrollbars="none" >
<com.baidu.mapapi.map.MapView
android:id="@+id/bmapsView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true" />
</LinearLayout>
(七)離線地圖
離線地圖好處多多,但若是要開發需要推廣給眾多用戶的App的話,建議要麼做下載離線地圖包功能(Demo中有例程,很容易),要麼考慮在App第一次運行的時候把APK資源文件中的離線地圖復制到用戶SD卡中(理論上可以實現),當然這樣的話地圖適用范圍會受到限制,開發有明確地域限制的App可以選用(例如:西安出行助手)。