安卓開發筆記(九)—— HttpURLConnection請求訪問Web服務,解析JSON資料,多執行緒,CardView佈局技術(bilibili的使用者視訊資訊獲取軟體)
中山大學資料科學與計算機學院本科生實驗報告
(2018年秋季學期)
一、實驗題目
WEB API
第十四周實驗目的
- 學會使用HttpURLConnection請求訪問Web服務
- 學習Android執行緒機制,學會執行緒更新UI
- 學會解析JSON資料
- 學習CardView佈局技術
二、實現內容
實現一個bilibili的使用者視訊資訊獲取軟體
- 搜尋框只允許正整數int型別,不符合的需要彈Toast提示
- 當手機處於飛航模式或關閉wifi和移動資料的網路連線時,需要彈Toast提示
- 由於bilibili的API返回狀態有很多,這次我們特別的限制在以下幾點
- 基礎資訊API介面為:
https://space.bilibili.com/ajax/top/showTop?mid=<user_id>
- 圖片資訊API介面為基礎資訊API返回的URL,cover欄位
- 只針對前40的使用者id進行處理,即
user_id <= 40
- [2,7,10,19,20,24,32]都存在資料,需要正確顯示
- 基礎資訊API介面為:
- 在圖片加載出來前需要有一個載入條,不要求與載入進度同步
- 佈局和樣式沒有強制要求,只需要展示圖片/播放數/評論/時長/建立時間/標題/簡介的內容即可,可以自由發揮
- 佈局需要使用到CardView和RecyclerView
- 每個item最少使用2個CardView,佈局怎樣好看可以自由發揮,不發揮也行
- 不完成加分項的同學可以不顯示SeekBar
- 輸入框以及按鈕需要一直處於頂部
驗收內容
- 圖片/播放數/評論/時長/建立時間/標題/簡介 顯示是否齊全正確,
- 是否存在載入條
- Toast資訊是否準確,特別地,針對使用者網路連線狀態和資料不存在情況的Toast要有區別
- 多次搜尋時是否正常
- 程式碼+實驗報告
- 好看的介面會酌情加分,不要弄得像demo那麼醜= =
加分項
- 拖動前後均顯示原圖片
- 模擬bilibili網頁PC端,完成可拖動的預覽功能
- 拖動seekBar,預覽圖會相應改變
- 前40的使用者id中,32不存在預覽圖,可以忽略也可以跟demo一樣將seekbar的enable設定為false
- 需要額外使用兩個API介面,分別為
- 利用之前API獲得的資訊,得到aid傳入
https://api.bilibili.com/pvideo?aid=<aid>
- 利用
api.bilibili.com
得到的資訊,解析image欄位得到"http://i3.hdslb.com/bfs/videoshot/3668745.jpg
的圖片 - 分割該圖片即可完成預覽功能
- 利用之前API獲得的資訊,得到aid傳入
- 加分項存在一定難度,需要不少額外編碼,可不做。
- 32不存在預覽圖,可忽略或處理該異常情況
三、實驗結果
(1)實驗截圖
截圖一:開啟程式主頁面
截圖二:搜尋id=2的使用者資訊,使用者存在
截圖三:搜尋id=3的使用者資訊,使用者不存在
截圖四:網路關閉的情況下搜尋id=7的使用者資訊(使用者存在但網路關閉)
截圖五:搜尋多個使用者的資訊
截圖六:加分項,拖動SeekBar顯示視訊的縮圖
(2)實驗步驟以及關鍵程式碼
a.設計recyclerView所使用的item.xml
其中主要包括了cardView的使用,設定邊距。
主要效果如下圖所示:
兩個CardView使用線性佈局,佈局方向為垂直。而在CardView裡面使用限制性佈局,將播放、評論、時長等元素依次放置。
關於CardView整體的佈局邊距以及顏色的設定如下
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:foreground = "?attr/selectableItemBackground"
app:cardBackgroundColor = "#f0e3c4"
android:layout_height="wrap_content"
android:layout_margin="10dp"
app:cardCornerRadius="8dp"
app:contentPadding="5dp">
······
最後我還在每一組使用者資料後面添加了一條分界線,讓介面更加清晰友好。
b.建立RecyclerObj類
RecyclerObj類是用於儲存使用者的資訊以及顯示在RecyclerView中。
這個類是根據b站所提供的api所得到的json陣列所對應設計的,而其中的data就是儲存我們使用者視訊的資訊,包括封面圖,名字,時間,評價數,評論等等。
而ArrayList pieces是使用在加分項中儲存一系列預覽圖的,ImagePiece就是它的基類,包括index與圖片兩個屬性。
public class RecyclerObj {
private Boolean status;
private Data data;
private ArrayList<ImagePiece> pieces;
public static class ImagePiece{
private Bitmap bitmap;
private int index;
·····
}
public static class Data{
private String aid;
private String state;
private String cover;
private String title;
private String content;
private String play;
private String duration;
private String video_review;
private String create;
private String rec;
private String count;
private Bitmap cover_image;
······
}
······
}
c.RecyclerView的顯示
這一部分與之前第一個專案的實現類似,包括一個Holder以及一個Adapter。具體實現程式碼不再重複放置,主要邏輯是將List data傳入Adapter中,Adapter根據位置的不同來繫結不同的資料。
Holder是使用是為了在onBindViewHolder 中獲取頁面的元素,為其繫結資料。下面給出兩個簡單的程式碼展示,其他TextView的內容顯示也是如此。
public void onBindViewHolder(final MyViewHolder holder, final int position) {
······
// 給封面圖設定圖片,該圖片是從data中獲得的
((ImageView)holder.getView(R.id.web_image)).setImageBitmap(
data.get(position).getData().getCover_image());
// 同理。設定播放數量
((TextView)holder.getView(R.id.play_amount)).setText(
data.get(position).getData().getPlay());
······
}
d.為輸入按鈕繫結事件,判斷輸入資料的準確性
這裡給button設定監聽器,當點選時獲取EditText中的資料,然後利用正則匹配來解決非數字或者非整數的錯誤判斷。
除此以外,還限定了輸入的數字不能大於40或者小於0.
若無錯誤,則開始獲取使用者資訊的執行緒。
// 判斷輸入框,處理user的id
final EditText editText = findViewById(R.id.input);
Button button = findViewById(R.id.search);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String s = editText.getText().toString();
// 正則匹配,判斷是否是數字
Pattern pattern = Pattern.compile("[0-9]*");
Matcher matcher = pattern.matcher(s);
if (!matcher.matches()){
Toast.makeText(MainActivity.this,"搜尋框只允許正整數int型別,請重新輸入!",Toast.LENGTH_SHORT).show();
}
else {
user_id = Integer.parseInt(s);
if (user_id > 40 || user_id < 0){
Toast.makeText(MainActivity.this,"user的id查詢只允許小於等於40且大於0",Toast.LENGTH_SHORT).show();
}
}
// 獲取該id的資訊
thread.start();
}
});
e.通過HTTPConnection獲取資料,並解析json
由於通過HTTPConnection獲取資料是耗時操作,所以必須另開執行緒。
首先設定url,根據提供的api,以及使用者所提供的user_id來新建地址。
URL url = null;
try {
url = new URL("https://space.bilibili.com/ajax/top/showTop?mid="+user_id);
} catch (MalformedURLException e) {
//網路連線錯誤
handler.sendEmptyMessage(NETWORK_ERROR);
e.printStackTrace();
}
第二步就是通過這個url來開啟連結,使用GET方法訪問網路,然後設定它不能超過時間10s,否則回捕捉到這個異常,然後傳送訊息給handler來處理,發出toasat網路異常。
接著,利用inputStream獲取資料,利用InputStreamReader將資料解析出來,將json型別轉化成之前設計好的RecyclerObj類。
最後只需要將訊息傳遞迴handler處理,表示已經獲取完資料了,並將這個recyclerObj物件加入到data列表中,回到handler利用Adapter的notifyDataSetChanged即可實現UI介面的變化。
// 獲取連線
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 使用GET方法訪問網路
connection.setRequestMethod("GET");
// 超時時間為10秒
connection.setConnectTimeout(10000);
// 獲取返回碼
int code = connection.getResponseCode();
if (code == 200) {
InputStream inputStream = connection.getInputStream();
String result = new BufferedReader(new InputStreamReader(inputStream))
.lines().collect(Collectors.joining(System.lineSeparator()));
Message msg = Message.obtain();
// 處理字串放入列表中,用於顯示UI
RecyclerObj recyclerObj;
try {
// 處理json
recyclerObj = new Gson().fromJson(result, RecyclerObj.class);
// 獲取預覽圖
data.add(recyclerObj);
msg.obj = recyclerObj;
msg.what = GET_DATA_SUCCESS;
}
catch (Exception e){
msg.obj = null;
msg.what = GET_DATA_EMPTY;
}
handler.sendMessage(msg);
inputStream.close();
}else {
//服務啟發生錯誤
handler.sendEmptyMessage(SERVER_ERROR);
}
f.Handler的設計
Handler的作用是處理其他執行緒返還回來的資料或者資訊。
這裡用使用者資訊獲取成功作為例子講述,當我獲取完使用者的資料後,且已經將資料傳遞給recyclerObj,這時候我要做的操作是根據這個使用者提供的封面圖url來再次獲取圖片,以及完成加分項獲取多個預覽圖。這些都是耗時操作,所以我寫了別的執行緒來處理,這裡只需要去呼叫即可。
myAdapter.notifyDataSetChanged();就是在主執行緒來更新RecyclerView的顯示,因為之前已經將資料加入到了list中。
public void handleMessage(Message msg) {
switch (msg.what){
// 獲取使用者資訊成功
case GET_DATA_SUCCESS:
myAdapter.notifyDataSetChanged();
RecyclerObj recyclerObj = (RecyclerObj)msg.obj;
setImageURL(recyclerObj);
getImagePeacesByAid(recyclerObj);
break;
// 獲取不到資訊
case GET_DATA_EMPTY:
Toast.makeText(MainActivity.this,"資料庫不存在記錄",Toast.LENGTH_SHORT).show();
break;
// 網路連線失敗
case NETWORK_ERROR:
Toast.makeText(MainActivity.this,"網路連線失敗",Toast.LENGTH_SHORT).show();
break;
// 伺服器錯誤
case SERVER_ERROR:
Toast.makeText(MainActivity.this,"伺服器發生錯誤",Toast.LENGTH_SHORT).show();
break;
// 獲取封面圖成功
case GET_IMAGE_SUCCESS:
// 去除緩衝的圓圈
myAdapter.notifyDataSetChanged();
Log.i("handler","設定照片成功");
break;
// 獲取預覽圖成功
case GET_IMAGEPEACES_SUCCESS:
myAdapter.notifyDataSetChanged();
break;
}
}
g. 獲取使用者視訊的封面圖
這一個操作與獲取使用者資料類似,只不過這次是一張圖片而已。HTTPConnection部分類似,這裡只敘述如何將獲取的圖片inputStream轉化到使用者recyclerObj類中。
這裡使用工廠把網路的輸入流生產Bitmap,然後將這張bitmap賦值到recyclerObj中,這樣recyclerObj就已經有了封面圖的bitmap了,返回訊息給handler,讓它來更新ui,包括加載出封面圖以及取消ProcessBar的顯示。
InputStream inputStream = connection.getInputStream();
//使用工廠把網路的輸入流生產Bitmap
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
//利用Message把圖片發給Handler
Message msg = Message.obtain();
data.remove(recyclerObj);
recyclerObj.getData().setCover_image(bitmap);
msg.what = GET_IMAGE_SUCCESS;
data.add(recyclerObj);
handler.sendMessage(msg);
inputStream.close();
這樣,我們就可以獲得基礎的應用結果了,搜尋使用者id獲得一些資訊。
至於拖動seekBar顯示縮圖部分,留到實驗思考與感想部分敘述
(3)實驗遇到的困難以及解決思路
a.處理ProcessBar的顯示
由於網速載入速度太快,導致看不出ProcessBar的出現,而是直接出現封面圖。
我在獲取封面圖的執行緒中先讓執行緒sleep了一秒再進行獲取,這樣就可以利用這個時間差來顯示出載入條的轉動。
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
b.處理inputStream中的資訊
通過HttpURLConnection獲取的資訊存放在inputStream,我所要做的任務是如何將裡面的資訊獲取出來。這裡針對兩個方面,第一個是圖片資料,第二個是純文字json資料。
關於文字json資料,按行來獲取資料,並直接轉化成String。
String result = new BufferedReader(new InputStreamReader(inputStream)) .lines().collect(Collectors.joining(System.lineSeparator()));
關於圖片資料,使用Bitmap工廠
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
除此之外,網上還有多種處理inputStream的方法
c. 處理seekBar顯示預覽圖
這是在做加分項時候遇到的困難,一開始以為縮略大圖只有一張,index大小會小於100個。結果發現user_id = 24的時候,縮圖有兩張,這樣會使我的程式崩潰。
由於一張縮圖可以裝載100張預覽圖,即index數量可以到達100個。所以我利用這個資訊來區分是需要讀取一個縮圖還是多個,然後將這些index與縮圖對應起來寫進recyclerObj.
// 兩張縮圖
if (indexArray.length > 100){
imageUrlArray = image_url.split(",");
imageUrlArray[0] = imageUrlArray[0].substring
(2,imageUrlArray[0].length()-1);
imageUrlArray[1] = imageUrlArray[1].substring
(1,imageUrlArray[1].length()-2);
}
// 一張縮圖
else{
image_url = image_url.substring
(2,image_url.length()-2);
imageUrlArray[0] = image_url;
}
四、實驗思考及感想
a.加分項:完成拖動seekBar顯示縮圖
主要步驟:
-
通過api獲取縮圖的url地址與index陣列。
-
根據這個url獲取到圖片到應用。
-
將圖片切分並與index對應起來,放入recyclerObj的ImagePieces連結串列中
-
設定seekBar的變化監聽器,處理拖動事件與初始化
1.通過api獲取縮圖的url地址與index陣列
這一步與之前的HTTPUrlConnection一樣,沒有什麼不同。
唯一需要做的是,我這次不再需要整個json都拿去下來,而只是要拿兩個元素,所以我沒再使用json而是利用JSONObject以及它對應的屬性名就可以處理。
// 測試獲取圖片的url字串
JSONObject obj = new JSONObject(result);
String image_url = obj.getJSONObject("data").getString("image");
String index = obj.getJSONObject("data").getString("index");
獲取完成後,還要對字串進行處理,例如對於index需要切分,放到陣列當中。判斷index的個數決定有多少張縮圖。
index = index.substring(1,index.length()-1);
String[] indexArray = index.split(",");
同樣,根據縮圖的數量來處理url
// 兩張縮圖
if (indexArray.length > 100){
imageUrlArray = image_url.split(",");
imageUrlArray[0] = imageUrlArray[0].substring(2,imageUrlArray[0].length()-1);
imageUrlArray[1] = imageUrlArray[1].substring(1,imageUrlArray[1].length()-2);
}
// 一張縮圖
else{
image_url = image_url.substring(2,image_url.length()-2);
imageUrlArray[0] = image_url;
}
2.根據這個url獲取到圖片到應用。
這一步與之前獲取圖片一致,不重複
3.將圖片切分並與index對應起來,放入recyclerObj的ImagePieces連結串列中
切分的關鍵是知道原始圖的大小,以及一塊切分後的圖片的大小,我們通過api拿回來的資料可以看到原始圖是1600*900大小,且一行有十張預覽圖,一共有十行。因此,我們利用這個資訊來進行迴圈切割,每切割一份,將它與index聯絡在一起放入到ImagePieces中。
這裡主要是利用了Bitmap.createBitmap (bitmap,xValue,yValue,width,height);
- bitmap是原始圖片
- xValue是原始圖片的橫座標
- yValue是原始圖片的縱座標
- width是需要切割的寬度
- height是需要切割的高度
int width = 160;
int height = 90;
int xValue = 0;
int yValue = 0;
for (int i = 1; i <= size; i++){
Bitmap piece_bitmap = Bitmap.createBitmap
(bitmap,xValue,yValue,width,height);
xValue += width;
// 換行
if(i%10==0){
yValue += height;
xValue = 0;
}
RecyclerObj.ImagePiece piece = new RecyclerObj.ImagePiece
(piece_bitmap,Integer.valueOf(indexArray[i-1]));
imagePieces.add(piece);
}
4.設定seekBar的變化監聽器,處理拖動事件與初始化
這在Adapter的onBindViewHolder函式中來處理,初始化seekBar的最大progress為視訊的時間秒數,初始位置為0.
((SeekBar)holder.getView(R.id.seekBar)).setEnabled(true);
String timeStr = data.get(position).getData().getDuration();
String[] timeArray = new String[2];
timeArray = timeStr.split(":");
int minute = Integer.valueOf(timeArray[0]);
int second = Integer.valueOf(timeArray[1]);
int time = minute * 60 + second;
Log.i("時間長度",time+"");
((SeekBar)holder.getView(R.id.seekBar)).setMax(time);
((SeekBar)holder.getView(R.id.seekBar)).setProgress(0);
然後為其設定監聽器,當它改變的時候,檢視是否在該index中有預覽圖,若有就顯示,若無不改變
拖動結束後,要將封面圖變回原來的,且process值歸零.
((SeekBar)holder.getView(R.id.seekBar)).setOnSeekBarChangeListener
(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress