安卓开发笔记(九)—— 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, 
                boolean fromUser) {
                    ArrayList<RecyclerObj.ImagePiece> pieces =  data.get(position).getPieces();
                    Log.i("时间长度",progress+"");
                    for (int i = 0; i < pieces.size(); i++){
                        if (pieces.get(i).getIndex() == progress){
                            Bitmap bitmap = pieces.get(i).getBitmap();
                            ((ImageView)holder.getView(R.id.web_image)).
                            setImageBitmap(bitmap);
                            break;
                        }
                    }
                }

                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {

                }

                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {
                    // 设置回初始状态
                    seekBar.setProgress(0);
                    ((ImageView)holder.getView(R.id.web_image)).
                    setImageBitmap(data.get(position).getData().getCover_image());
                }
            });

b. 感想

这次实验利用的是Http访问api来获取数据,其中涉及了对json数据的转化,对字符串的规范化处理,以及对于图片的获取。与此同时,复习了最初的recyclerView的显示与设置,线程与handler的知识。这次还使用了新的UI组件CardView与ProcessBar,丰富了UI的元素,学习了更多的界面设计。

关于加分项的缩略图处理还是挺有难度的,花费了一个下午的时间才能完成。通过这个加分项学习到了如何切割图片,如何获取个别json数据,将图片与SeekBar联系在一起显示,感觉这个功能还是很实用的。加分项虽然比较难,但是做出来的成就感就更大了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值