[置頂] 模仿百度地圖的LBS服務――離線地圖篇 Part 2 (v 3.1.1)
來源:程序員人生 發布時間:2014-12-09 08:20:19 閱讀次數:2924次
1、前言
轉載請標明出處:http://blog.csdn.net/wlwlwlwl015/article/details/41492031
這1篇blog寫的真心不容易,我只想說我這類菜鳥去高仿百度地圖去做LBS服務真心有點作死,期間本想放棄,做簡單點算了,但不能說服自己。最后通過F6去1行1行的debug(新手朋友注意這是最好的解決問題的方式沒有之1),最后成功完成了核心的功能。上1篇blog高仿了百度地圖離線地圖模塊中的“城市列表”部份(模仿百度地圖的LBS服務――離線地圖篇
Part1),城市實現里“當前城市”、“熱門城市”、“全國省市”數據信息的展現,那末本篇blog主要記錄的就是如何進行下載了,一樣的是高仿百度離線地圖的“下載管理”模塊。空話不多說,下面就分步驟進行逐一介紹了。
2、百度離線地圖“下載管理”功能分析
老規矩我們先來看看百度地圖離線地圖中“下載管理”模塊的界面和功能:


通過這上面兩幅截圖我們分析1下需要做的工作:
1.下載管理分為“正在下載”和“下載完成”兩部份,所以整體界面應當分為2個ListView。
2.可以通過進度條實時看到下載任務的進度,下載中可以進行“暫停下載”和“刪除”的操作,那末暫停的時候一定也能夠履行“開始下載”的操作。具體細節大家可以通過操作百度地圖來看。
3.已下載完成的地圖提供了“查看地圖”和“刪除”的功能。
大體上功能就上面提到的這些,具體的細節在下面的代碼中再看,下面就依照我的開發順序來告知大家先做甚么、后做甚么、具體怎樣做,對高手來講高仿這個東東可能不算甚么,但是對我來講確切難度挺大,整整兩天,吃飯睡覺都在想細節,終究之所以實現了,是由于我首先會理清思路,先斟酌我需要做甚么并將任務拆分開,然后是這些步驟應當按甚么順序去做,就這樣1點1點做,1行1行debug,終究得以完成。所以下面我就列出開發步驟,再逐1說明。
Step 1 初始化
Step 2 編寫離線地圖事件通知接口及其回調方法中的代碼
Step 3 分別編寫“正在下載”和“下載完成”的布局和Adapter
Step 4 編寫測試方法,即開始履行下載任務
Step 1
上面這個說的可能還不夠細節,但是沒關系,我會在下面通過代碼去逐一解釋清楚。首先來看看初始化,先上代碼(注意和上1篇是同1個Activity,所以重復代碼就不貼了):
// 已下載的離線地圖數據List
private ArrayList<MKOLUpdateElement> isDoingUpdateMapList = null;
// 正在下載的數據列表(包括下載中、暫停的)
private ArrayList<MKOLUpdateElement> downLoadingMapList = new ArrayList<MKOLUpdateElement>();
// 已下載完成的數據列表
private ArrayList<MKOLUpdateElement> downLoadedMapList = new ArrayList<MKOLUpdateElement>();
// 已下載的離線地圖數據Adapter和ListView
private DownLoadingListView downLoadingListView; // 正在下載的ListView
private DownLoadingAdapter mDownLoadingAdapter = new DownLoadingAdapter();
private DownLoadedListView downLoadedListView; // 下載完成的ListView
private DownLoadedAdapter mDownLoadedAdapter = new DownLoadedAdapter();
上面的是聲明部份,關于“正在下載”和“下載完成”一樣是兩個自定義的ListView去解決事件沖突問題,解決方案和上1篇blog1樣,都是重寫onMeasure方法。
一樣的聲明以后應當進行初始化工作,用來初始化之前的下載任務,比如:兩個下載任務都下了1半暫停了,這里我們需要在“正在下載”的ListView中顯示出來,參照百度地圖應包括以下信息:城市名、數據包大小、下載狀態、當前的下載進度等。下面這段代碼一樣位于initData()方法中:
// 初始化已下載的城市列表,并根據已下載和下載中進行分類
isDoingUpdateMapList = mOfflineMap.getAllUpdateInfo();
if (isDoingUpdateMapList == null) {
isDoingUpdateMapList = new ArrayList<MKOLUpdateElement>();
}
if (isDoingUpdateMapList.size() > 0) {
for (MKOLUpdateElement element : isDoingUpdateMapList) {
// 如果下載進度為100則應放入“已下載”的List
if (element.ratio == 100) {
downLoadedMapList.add(element);
mDownLoadedAdapter = new DownLoadedAdapter();
}
// 如果下載進入不為100則應放入“正在下載”的List
if (element.ratio != 100) {
downLoadingMapList.add(element);
mDownLoadingAdapter = new DownLoadingAdapter();
}
}
}
第2行的getAllUpdateInfo()方法很好用,是這個下載模塊的核心方法,官方的解釋是:返回各城市離線地圖更新信息。對這個解釋我是覺得很笼統,不明白,通過我的使用我認為這個方法的作用就是:返回當前已下載的(包括開始下載的、暫停的、正在下載的和完成的)的所有數據信息,并且是以城市為單位的List集合。根據官方文檔可以看出返回值類型是MKOLUpdateElement,這個類也正式封裝了1個下載任務應有的所有關鍵信息,而在上面的代碼中我正是通過element.ratio來分割List,由于ratio正是下載進度的意思:

所以當ratio為100的時候,我就將這個對象放到“下載完成”的ListView,否則說明還沒下載完,就放到“正在下載”的ListView。這樣我們就能夠在進入利用以后看到之前未完成的、已完成的下載記錄了。OK,初始化很簡單,結束了。
Step 2
初始化完成以后,現在就來講道說道離線地圖中唯1的1個監聽:public interface MKOfflineMapListener
一樣的來看看官方文檔中對它的解釋:
離線地圖事件通知接口。該接口返回新安裝離線地圖、下載更新、數據版本更新等結果,用戶需要實現該接口以處理相應事件。
不知道是否是我書讀的少,總覺得官方的解釋不夠通俗,看了仍然不知道怎樣用。但是官方對這個接口的回調方法還是解釋的比較清楚的:
void onGetOfflineMapState(int type,int state)
type - 事件類型: MKOfflineMap.TYPE_NEW_OFFLINE, MKOfflineMap.TYPE_DOWNLOAD_UPDATE, MKOfflineMap.TYPE_VER_UPDATE.
state - 事件狀態: 當type為TYPE_NEW_OFFLINE時,表示新安裝的離線地圖數目. 當type為TYPE_DOWNLOAD_UPDATE時,表示更新的城市ID.
上面的藍色字體都是官方文檔的原話,這里我們也清楚了這個回調方法的兩個參數都表示甚么意思了。根據需求,我們需要實時監控下載進度并反饋到UI,所以這里我們只需要關注TYPE_DOWNLOAD_UPDATE這個類型的事件便可,而正好此時的state就表示更新城市的ID,那末這個監聽的作用就很明顯了,就是通過它來實時獲得下載進度并更新我們界面上的ProgressBar便可??赡苡行┤瞬磺宄趺磿r候會觸發這個監聽,那末通過打印語句來視察控制臺不難發現,當type為TYPE_DOWNLOAD_UPDATE時,只要下載狀態產生變化,即會觸發監聽。文字解釋比較費力,下面我貼上監聽代碼,大家可以開啟1個下載任務(后面說),并像第9行1樣通過打印來視察1下都有甚么變化:
mOfflineMap.init(new MKOfflineMapListener() {
@Override
public void onGetOfflineMapState(int type, int state) {
// TODO Auto-generated method stub
switch (type) {
case MKOfflineMap.TYPE_DOWNLOAD_UPDATE:
// 得到當前正在下載的城市的具體更新信息
MKOLUpdateElement update = mOfflineMap.getUpdateInfo(state);
Log.e(TAG, update.cityName + " ," + update.ratio);
if (update != null) {
// 此監聽器在下載任務進行期間大概每下載1%觸發1次,注意是大概
List<MKOLUpdateElement> elements = downLoadingMapList;
for (MKOLUpdateElement element : elements) {
if (update.cityID == element.cityID) {
element.ratio = update.ratio;
if (update.ratio == 100) {
// 當下載進度到100時,Item從“下載中”的List移動到“已下載的List”
downLoadingMapList.remove(element);
downLoadedMapList.add(element);
mDownLoadedAdapter.notifyDataSetChanged();
}
break;
}
}
}
mDownLoadingAdapter.notifyDataSetChanged();
break;
case MKOfflineMap.TYPE_NEW_OFFLINE:
// 有新離線地圖安裝
Log.e(TAG, "TYPE_NEW_OFFLINE");
break;
case MKOfflineMap.TYPE_VER_UPDATE:
// 版本更新提示
break;
}
}
});
相信認真看了代碼的朋友肯定也大致明白了個123吧,有問題可以留言,雖然我是新手,可我自己寫的東西還是解釋的清的。
Step 3
下面就是最重要的適配器了,我們來分別看看“下載中”的Adapter和“下載完成”的Adapter:
// 下載管理――正在下載――適配器
class DownLoadingAdapter extends BaseAdapter {
@Override
public int getCount() {
// TODO Auto-generated method stub
return downLoadingMapList.size();
}
@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return downLoadingMapList.get(position);
}
@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}
@Override
public View getView(final int position, View convertView,
ViewGroup parent) {
// TODO Auto-generated method stub
final MKOLUpdateElement element = downLoadingMapList.get(position);
ViewHolder holder = null;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.down_loading_item,
null);
holder = new ViewHolder();
holder.cityName = (TextView) convertView
.findViewById(R.id.id_city_name);
holder.dataPakSize = (TextView) convertView
.findViewById(R.id.id_data_size);
holder.downLoadState = (TextView) convertView
.findViewById(R.id.id_down_state);
holder.downLoadRatio = (TextView) convertView
.findViewById(R.id.id_down_raito);
holder.downLoadProgress = (ProgressBar) convertView
.findViewById(R.id.id_down_progress);
holder.downPullIcon = (ImageButton) convertView
.findViewById(R.id.id_expand_down_icon);
holder.upPullIcon = (ImageButton) convertView
.findViewById(R.id.id_expand_up_icon);
holder.pauseDownBtn = (Button) convertView
.findViewById(R.id.id_pause_down_btn);
holder.startDownBtn = (Button) convertView
.findViewById(R.id.id_start_down_btn);
holder.deleteMapBtn = (Button) convertView
.findViewById(R.id.id_delete_map);
holder.btnGroup = (LinearLayout) convertView
.findViewById(R.id.id_btn_group);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.cityName.setText(element.cityName);
holder.dataPakSize.setText(NumberFormatUtil
.dataSizeFormatter(element.serversize) + "M");
String stateInfo = "";
// 如果是正在下載的狀態
if (element.status == MKOLUpdateElement.DOWNLOADING) {
stateInfo = "正在下載";
holder.startDownBtn.setVisibility(View.GONE);
holder.pauseDownBtn.setVisibility(View.VISIBLE);
holder.downLoadState.setTextColor(Color.BLUE);
}
//如果是暫停的狀態
if (element.status == MKOLUpdateElement.SUSPENDED) {
stateInfo = "已暫停";
holder.downLoadState.setTextColor(Color.RED);
holder.startDownBtn.setVisibility(View.VISIBLE);
holder.pauseDownBtn.setVisibility(View.GONE);
}
holder.downLoadState.setText(stateInfo);
//設置當前進度的百分數
holder.downLoadRatio.setText(element.ratio + "%");
//設置ProgressBar的進度
holder.downLoadProgress.setProgress(element.ratio);
// 暫停下載
holder.pauseDownBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
int cityId = element.cityID;
mOfflineMap.pause(cityId);
Toast.makeText(OfflineActitivty.this,
"暫停下載" + element.cityName + "離線地圖:",
Toast.LENGTH_SHORT).show();
isDoingUpdateMapList = mOfflineMap.getAllUpdateInfo();
if (isDoingUpdateMapList == null) {
isDoingUpdateMapList = new ArrayList<MKOLUpdateElement>();
}
if (isDoingUpdateMapList.size() > 0) {
downLoadingMapList.clear();
for (MKOLUpdateElement element : isDoingUpdateMapList) {
if (element.ratio != 100) {
downLoadingMapList.add(element);
}
}
}
mDownLoadingAdapter.notifyDataSetChanged();
}
});
// 開始下載
holder.startDownBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
int cityId = element.cityID;
mOfflineMap.start(cityId);
Toast.makeText(OfflineActitivty.this,
"開始下載" + element.cityName + "離線地圖:",
Toast.LENGTH_SHORT).show();
isDoingUpdateMapList = mOfflineMap.getAllUpdateInfo();
if (isDoingUpdateMapList == null) {
isDoingUpdateMapList = new ArrayList<MKOLUpdateElement>();
}
if (isDoingUpdateMapList.size() > 0) {
downLoadingMapList.clear();
for (MKOLUpdateElement element : isDoingUpdateMapList) {
if (element.ratio != 100) {
downLoadingMapList.add(element);
}
}
}
mDownLoadingAdapter.notifyDataSetChanged();
mDownLoadedAdapter.notifyDataSetChanged();
}
});
// 刪除地圖
holder.deleteMapBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
int cityId = element.cityID;
mOfflineMap.remove(cityId);
Toast.makeText(OfflineActitivty.this,
"已刪除" + element.cityName + "離線地圖:",
Toast.LENGTH_SHORT).show();
downLoadingMapList.remove(position);
mDownLoadingAdapter.notifyDataSetChanged();
}
});
}
}
});
return convertView;
}
private class ViewHolder {
TextView cityName;
TextView dataPakSize;
TextView downLoadState;
TextView downLoadRatio;
ProgressBar downLoadProgress;
ImageButton downPullIcon;
ImageButton upPullIcon;
Button pauseDownBtn;
Button startDownBtn;
Button deleteMapBtn;
LinearLayout btnGroup;
}
}
// 下載管理――已下載――適配器
class DownLoadedAdapter extends BaseAdapter {
@Override
public int getCount() {
// TODO Auto-generated method stub
return downLoadedMapList.size();
}
@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return downLoadedMapList.get(position);
}
@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}
@Override
public View getView(final int position, View convertView,
ViewGroup parent) {
// TODO Auto-generated method stub
final MKOLUpdateElement element = downLoadedMapList.get(position);
ViewHolder holder = null;
if (convertView == null) {
convertView = mInflater
.inflate(R.layout.down_loaded_item, null);
holder = new ViewHolder();
holder.cityName = (TextView) convertView
.findViewById(R.id.id_city_name);
holder.isHasNewData = (TextView) convertView
.findViewById(R.id.id_is_has_new);
holder.dataPakSize = (TextView) convertView
.findViewById(R.id.id_data_pak_size);
holder.deleteMapBtn = (Button) convertView
.findViewById(R.id.id_btn_delete_map);
holder.seeMapDetailBtn = (Button) convertView
.findViewById(R.id.id_see_map_detail);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.cityName.setText(element.cityName);
holder.dataPakSize.setText(NumberFormatUtil
.dataSizeFormatter(element.serversize) + "M");
// 刪除地圖
holder.deleteMapBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
// 刪除已下載的離線Map
int cityId = element.cityID;
mOfflineMap.remove(cityId);
Toast.makeText(OfflineActitivty.this,
"已刪除" + element.cityName + "離線地圖:",
Toast.LENGTH_SHORT).show();
downLoadedMapList.remove(position);
mDownLoadedAdapter.notifyDataSetChanged();
}
});
// 查看地圖
holder.seeMapDetailBtn
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Intent intent = new Intent();
intent.putExtra("x", element.geoPt.longitude);
intent.putExtra("y", element.geoPt.latitude);
intent.setClass(OfflineActitivty.this,
BaseMapActivity.class);
startActivity(intent);
}
});
return convertView;
}
private class ViewHolder {
private TextView cityName;
private TextView isHasNewData;
private TextView dataPakSize;
private Button deleteMapBtn;
private Button seeMapDetailBtn;
}
}
可以看到由于“下載中”有開始和暫停兩個按鈕,所以比“下載完成”稍微麻煩1些,而下載完成以后便可以查看地圖,即根據經緯坐標去加載改城市的地圖,代碼很簡單就是官方Demo中的那個BaseMapActivity。item的布局也很簡單,模仿百度地圖的樣式拼湊1下就行了,后面會貼上動態效果圖,其實到這里離線地圖模塊的核心功能都已完成,最后看看如何通過點擊Item去開啟1個下載任務吧。
Step 4
通過上面的3個步驟就已完成了離線地圖的所有準備工作了,下面只剩下點擊列表項開啟下載任務了,仍然是參考百度地圖,簡單的流程是這樣的:
點擊任意列表中的任意項(包括熱門城市的ListView、全國省市的ExpandableListView的子項),如果被點擊的城市沒有下載,那末新開1個下載任務在“正在下載”的列表,如果被點擊的城市“正在下載”或“已暫?!保悄┲恍栌们袚Q到“下載管理列表”便可,最后如果被點擊的城市“已下載完成”,那末一樣的只是切換1下便可。
下面貼上剩余全部代碼,包括初始化ListView和添加Item點擊:
// 初始化ListView
private void initListView() {
// 熱門城市
hotCitieslistView = (HotCitiesListView) findViewById(R.id.id_hotcities_lv);
mHotCityAdapter = new HotCityAdapter();
hotCitieslistView.setAdapter(mHotCityAdapter);
// 全國省市
allCitieslistView = (NationalCitiesListView) findViewById(R.id.id_allcities_exp_lv);
mNationalCityAdapter = new NationalCityAdapter();
allCitieslistView.setAdapter(mNationalCityAdapter);
// 下載管理――下載中
downLoadingListView = (DownLoadingListView) findViewById(R.id.id_download_manager_lv);
downLoadingListView.setAdapter(mDownLoadingAdapter);
// 下載管理――下載完成
downLoadedListView = (DownLoadedListView) findViewById(R.id.id_download_manager_lv_2);
downLoadedListView.setAdapter(mDownLoadedAdapter);
// 設置熱門城市的Item點擊事件
hotCitieslistView
.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
// // TODO Auto-generated method stub
OfflineMapItemBean offlineMapItemBean = mHotCityDatas
.get(position);
int cityId = offlineMapItemBean.getCityId();
String cityName = offlineMapItemBean.getCityName();
MKOLUpdateElement element = mOfflineMap
.getUpdateInfo(cityId);
// 如果進度為0,則下載。否則僅僅切換過去
if (element == null) {
// 開始下載
mOfflineMap.start(cityId);
offlineMapItemBean
.setDownloadStatus(DownLoadStatus.DOWNLOADING);
// 切換到下載管理
tb.setChecked(false);
llayout1.setVisibility(View.VISIBLE);
llayout2.setVisibility(View.GONE);
Toast.makeText(OfflineActitivty.this,
"開始下載" + cityName + "離線地圖",
Toast.LENGTH_SHORT).show();
// 更新界面顯示
isDoingUpdateMapList = mOfflineMap
.getAllUpdateInfo();
if (isDoingUpdateMapList == null) {
isDoingUpdateMapList = new ArrayList<MKOLUpdateElement>();
}
// 現在就有更新信息了
element = mOfflineMap.getUpdateInfo(cityId);
downLoadingMapList.add(element);
mDownLoadingAdapter.notifyDataSetChanged();
mHotCityAdapter.notifyDataSetChanged();
} else {
// 僅僅跳轉
// 切換到下載管理
tb.setChecked(false);
llayout1.setVisibility(View.VISIBLE);
llayout2.setVisibility(View.GONE);
}
}
});
// 設置全國省市的Item點擊事件
allCitieslistView.setOnChildClickListener(new OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View v,
int groupPosition, int childPosition, long id) {
OfflineMapItemBean offlineMapItemBean = allCityDatas
.get(groupPosition).getChildCities().get(childPosition);
int cityId = offlineMapItemBean.getCityId();
String cityName = offlineMapItemBean.getCityName();
// Toast.makeText(OfflineActitivty.this, cityId+","+cityName,
// Toast.LENGTH_SHORT).show();
MKOLUpdateElement element = mOfflineMap.getUpdateInfo(cityId);
// 如果進度為0,則下載。否則僅僅切換過去
if (element == null) {
// 開始下載
mOfflineMap.start(cityId);
offlineMapItemBean
.setDownloadStatus(DownLoadStatus.DOWNLOADING);
// 切換到下載管理
tb.setChecked(false);
llayout1.setVisibility(View.VISIBLE);
llayout2.setVisibility(View.GONE);
Toast.makeText(OfflineActitivty.this,
"開始下載" + cityName + "離線地圖:", Toast.LENGTH_SHORT)
.show();
// 更新界面顯示
isDoingUpdateMapList = mOfflineMap.getAllUpdateInfo();
if (isDoingUpdateMapList == null) {
isDoingUpdateMapList = new ArrayList<MKOLUpdateElement>();
}
// 現在就有更新信息了
element = mOfflineMap.getUpdateInfo(cityId);
downLoadingMapList.add(element);
mDownLoadingAdapter.notifyDataSetChanged();
mHotCityAdapter.notifyDataSetChanged();
} else {
// 僅僅跳轉
// 切換到下載管理
tb.setChecked(false);
llayout1.setVisibility(View.VISIBLE);
llayout2.setVisibility(View.GONE);
}
return false;
}
});
}
最后做1些說明,包括我整體代碼的不足和重點需要注意的地方:
1.肯定有的朋友發現我在寫“熱門城市”的列表的時候還通過自定義的OfflineMapItemBean對數據進行了封裝,在后面做“下載列表”的時候壓根就沒有再封裝了,而是直接用SDK中的MKOLUpdateElement對象去取值了。這塊是我做的不好,由于下載列表要拆分成“正在下載”和“已完成”兩部份,每次獲得更新對象再遍歷拆分感覺也不是個辦法,所以干脆就不封裝了,我也沒有想到更好的辦法去處理,有更好的方法的朋友可以給我指導12。
2.當ScrollView嵌套ListView以后item的點擊事件是失效的,包括ExpandableListView的子item也是1樣。對這個我選擇了在item的布局文件中添加1個屬性:android:descendantFocusability="blocksDescendants"來解決的,但這不是個好辦法,聽說會讓ViewHolder失效,問了其他朋友說是讓ScrollView中的onTouch返回false之類的,我嘗試了沒有到達預期效果,如果哪位大神知道最優的解決方案還請給小弟指導12,感激不盡。
3.由于項目周期緊,還有很多小效果沒來得及實現,比如:下載狀態(已暫停、已完成等)應當在“下載管理”和“城市列表”中能實時的同步的更新等,但是整體上看效果還是挺不錯的吧,下面就貼上效果圖:


由于在摹擬器上調試時沒法開始下載(offlineMap.start方法調用以后MKOLUpdateElement返回的更新對象的status始終是WAITING的狀態,而在真機調試時調用start以后更新對象的status會立刻變成DOWNING,摹擬器的網絡也沒有問題,不知道是否是BUG),所以這里只貼上兩張真機運行截圖好了,UI比較粗糙,但是核心功能都沒問題,到了這里離線地圖的全部內容就已算是記錄終了了。
3、總結
寫到這里關于我們項目中的LBS服務就已全部介紹終了了,在做地圖模塊的同時學到了許多新的東西,在這里也要感謝鴻洋大神對我的幫助,目前準備和同事1起再做1個以LBS為主的APP,等做好以后可能還會寫1個博客吧,這個APP的點子不錯,目前暫且先不透漏,透漏了也沒幾個人會看到,哈哈。路漫漫其修遠兮,吾將上下而求索,我的Android之路才剛剛開始,今后還應當更加努力!加油!Raito!
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈