传送门:项目源码
WEB API
第十四周实验目的
https://space.bilibili.com/ajax/top/showTop?mid=
user_id <= 40
https://api.bilibili.com/pvideo?aid=
api.bilibili.com
得到的信息,解析image字段得到"http://i3.hdslb.com/bfs/videoshot/3668745.jpg
的图片截图一:打开程序主页面
截图二:搜索id=2的用户信息,用户存在
截图三:搜索id=3的用户信息,用户不存在
截图四:网络关闭的情况下搜索id=7的用户信息(用户存在但网络关闭)
截图五:搜索多个用户的信息
截图六:加分项,拖动SeekBar显示视频的缩略图
其中主要包括了cardView的使用,设置边距。
主要效果如下图所示:
两个CardView使用线性布局,布局方向为垂直。而在CardView里面使用限制性布局,将播放、评论、时长等元素依次放置。
关于CardView整体的布局边距以及颜色的设置如下
······
最后我还在每一组用户数据后面添加了一条分界线,让界面更加清晰友好。
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;
······
}
······
}
这一部分与之前第一个项目的实现类似,包括一个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());
······
}
这里给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();
}
});
由于通过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);
}
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;
}
}
这一个操作与获取用户资料类似,只不过这次是一张图片而已。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显示缩略图部分,留到实验思考与感想部分叙述
由于网速加载速度太快,导致看不出ProcessBar的出现,而是直接出现封面图。
我在获取封面图的线程中先让线程sleep了一秒再进行获取,这样就可以利用这个时间差来显示出加载条的转动。
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
通过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
这是在做加分项时候遇到的困难,一开始以为缩略大图只有一张,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;
}
主要步骤:
通过api获取缩略图的url地址与index数组。
根据这个url获取到图片到应用。
将图片切分并与index对应起来,放入recyclerObj的ImagePieces链表中
设置seekBar的变化监听器,处理拖动事件与初始化
这一步与之前的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;
}
这一步与之前获取图片一致,不重复
切分的关键是知道原始图的大小,以及一块切分后的图片的大小,我们通过api拿回来的数据可以看到原始图是1600*900大小,且一行有十张预览图,一共有十行。因此,我们利用这个信息来进行循环切割,每切割一份,将它与index联系在一起放入到ImagePieces中。
这里主要是利用了Bitmap.createBitmap (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);
}
这在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());
}
});
这次实验利用的是Http访问api来获取数据,其中涉及了对json数据的转化,对字符串的规范化处理,以及对于图片的获取。与此同时,复习了最初的recyclerView的显示与设置,线程与handler的知识。这次还使用了新的UI组件CardView与ProcessBar,丰富了UI的元素,学习了更多的界面设计。
关于加分项的缩略图处理还是挺有难度的,花费了一个下午的时间才能完成。通过这个加分项学习到了如何切割图片,如何获取个别json数据,将图片与SeekBar联系在一起显示,感觉这个功能还是很实用的。加分项虽然比较难,但是做出来的成就感就更大了。