1. 程式人生 > >安卓開發筆記(九)—— HttpURLConnection請求訪問Web服務,解析JSON資料,多執行緒,CardView佈局技術(bilibili的使用者視訊資訊獲取軟體)

安卓開發筆記(九)—— HttpURLConnection請求訪問Web服務,解析JSON資料,多執行緒,CardView佈局技術(bilibili的使用者視訊資訊獲取軟體)

中山大學資料科學與計算機學院本科生實驗報告

(2018年秋季學期)


一、實驗題目

WEB API

第十四周實驗目的

  1. 學會使用HttpURLConnection請求訪問Web服務
  2. 學習Android執行緒機制,學會執行緒更新UI
  3. 學會解析JSON資料
  4. 學習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]都存在資料,需要正確顯示
  • 在圖片加載出來前需要有一個載入條,不要求與載入進度同步
  • 佈局和樣式沒有強制要求,只需要展示圖片/播放數/評論/時長/建立時間/標題/簡介的內容即可,可以自由發揮
  • 佈局需要使用到CardView和RecyclerView
  • 每個item最少使用2個CardView,佈局怎樣好看可以自由發揮,不發揮也行
  • 不完成加分項的同學可以不顯示SeekBar
  • 輸入框以及按鈕需要一直處於頂部

驗收內容

  1. 圖片/播放數/評論/時長/建立時間/標題/簡介 顯示是否齊全正確,
  2. 是否存在載入條
  3. Toast資訊是否準確,特別地,針對使用者網路連線狀態和資料不存在情況的Toast要有區別
  4. 多次搜尋時是否正常
  5. 程式碼+實驗報告
  6. 好看的介面會酌情加分,不要弄得像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 的圖片
    • 分割該圖片即可完成預覽功能
  • 加分項存在一定難度,需要不少額外編碼,可不做
  • 32不存在預覽圖,可忽略或處理該異常情況

三、實驗結果

(1)實驗截圖

截圖一:開啟程式主頁面

1

截圖二:搜尋id=2的使用者資訊,使用者存在

2

截圖三:搜尋id=3的使用者資訊,使用者不存在

3

截圖四:網路關閉的情況下搜尋id=7的使用者資訊(使用者存在但網路關閉)

4

截圖五:搜尋多個使用者的資訊

5

截圖六:加分項,拖動SeekBar顯示視訊的縮圖

6

在這裡插入圖片描述

(2)實驗步驟以及關鍵程式碼

a.設計recyclerView所使用的item.xml

其中主要包括了cardView的使用,設定邊距。

主要效果如下圖所示:

8

兩個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的方法

參考連結:將InputStream讀取為String

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顯示縮圖

主要步驟:

  1. 通過api獲取縮圖的url地址與index陣列。

  2. 根據這個url獲取到圖片到應用。

  3. 將圖片切分並與index對應起來,放入recyclerObj的ImagePieces連結串列中

  4. 設定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