文章目录
前言
自学Android有段时间了,发现还是绕不开gank.io(的福利图),嘻嘻~
我承认我没老老实实学过java,基础很差,大家看到了我的智障代码请理智地狂喷,让我成长 #谢谢#
我之前看大佬们博客的代码真的是理不清楚的,所以在放假的时候补了点基本功,但发现还是fork代码假装做项目有意思(真香~)。最近利用课余时间在看《图解设计模式》,然后发现!!!我好像进步了,能读懂了很多之前fork的代码了!!!在此推荐一些跟我一样刚入门的同学们,可以看看这本书,有助于读懂别人的代码。
这个博客是我这次实现获取福利图Demo的记录,大概分为#喂饭#和#代码梳理#。
喂饭
创建一个新项目
在app的build.gradle中引用
implementation 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
implementation 'com.github.bumptech.glide:glide:4.3.1'
不喜欢使用butterknife框架的同学可以不加
其它原材料
-
BaseActivity.java,LogUtil.java,ScreenUtil.java 点此处自取
代码梳理
GIF演示
(注:以下用RV代指RecyclerView)
activity_main.xml 主布局
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_beauty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"/>
</android.support.v4.widget.NestedScrollView>
</android.support.constraint.ConstraintLayout>
beauty_pic_item.xml RV每个item的布局
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardCornerRadius="7dp"
app:cardElevation="3dp"
android:layout_marginStart="10dp"
android:layout_marginBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/pic_Beauty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="centerInside"
android:adjustViewBounds="true"
/>
</android.support.v7.widget.CardView>
此处imageVIew的scaleType属性很关键:
scaleType=“matrix” 是保持原图大小、从左上角的点开始,以矩阵形式绘图。
scaleType=“fitXY” 是将原图进行横方向(即XY方向)的拉伸后绘制的。
scaleType=“fitStart” 是将原图沿左上角的点(即matrix方式绘图开始的点),按比例缩放原图绘制而成的。
scaleType=“fitCenter” 是将原图沿上方居中的点(即matrix方式绘图第一行的居中的点),按比例缩放原图绘制而成的。
scaleType=“fitEnd” 是将原图沿下方居中的点(即matrix方式绘图最后一行的居中的点),按比例缩放原图绘制而成的。
scaleType=“Center” 是保持原图大小,以原图的几何中心点和ImagView的几何中心点为基准,只绘制ImagView大小的图像。
scaleType=“centerCrop” 不保持原图大小,以原图的几何中心点和ImagView的几何中心点为基准,只绘制ImagView大小的图像(以填满ImagView为目标,对原图进行裁剪)。
scaleType=“centerInside” 不保持原图大小,以原图的几何中心点和ImagView的几何中心点为基准,只绘制ImagView大小的图像(以显示完整图片为目标,对原图进行缩放)。
BeautyAdapter.java RV的适配器
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import com.example.roy.beauty_12_13.R;
import com.example.roy.beauty_12_13.entity.GirlBean;
import com.example.roy.beauty_12_13.utils.LogUtil;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
/**
* 作者 没有感情的S686
* 时间 2018/12/13 17:42
* 文件 Beauty_12_13
* 描述
*/
public class BeautyAdapter extends RecyclerView.Adapter<BeautyAdapter.ViewHolder> {
private Context mContext;
private List<GirlBean.ResultsBean> mList;
public BeautyAdapter(Context mContext, List<GirlBean.ResultsBean> list) {
this.mContext = mContext;
this.mList = list;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.beauty_pic_item, parent,false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
String url = mList.get(position).getUrl();
Glide.with(mContext)
.load(url)
.transition(new DrawableTransitionOptions().crossFade()) //过渡动画,防止图片变形
.into(viewHolder.picBeauty);
LogUtil.i("I get one!");
}
@Override
public int getItemCount() {
if (mList == null)
return 0;
return mList.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.pic_Beauty)
ImageView picBeauty;
ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
}
在此处写一个常规的RV适配器,如演示图所示RV中每个item只包含一张图片,所以我们只需要用api中每张图的url就可以用glide显示了。glide框架十分强大,并且对小白很友好,基础用法如下:
Glide.with(context,即图片要显示的activity或者fragment)
.load(uri,即图片的来源,可以是网址,文件位置等等)
.into(图片所要放置的空间,一般为ImageView);
文件中所引用的mContext是MainActivity中new BeautyAdapter(MainActivity.this, girlPicUrlList);
的MainActivity.this。
context是Android中一个非常重要的概念,翻译为上下文或者叫做场景,用于访问全局信息,几乎所有的基础组件都继承自 Context,正常我们都是获取activity或者fragment来确定所要使用的context。
GirlBean.java 实体类,用于解析json数据。
import java.util.List;
public class GirlBean {
/**
* error : false
* results : [{"_id":"5bbb0de09d21226111b86f1c","createdAt":"2018-10-08T07:57:20.978Z","desc":"2018-10-08","publishedAt":"2018-10-08T00:00:00.0Z","source":"web","type":"福利","url":"https://ws1.sinaimg.cn/large/0065oQSqly1fw0vdlg6xcj30j60mzdk7.jpg","used":true,"who":"lijinshanmx"}]
*/
private boolean error;
private List<ResultsBean> results;
public boolean isError() {
return error;
}
public void setError(boolean error) {
this.error = error;
}
public List<ResultsBean> getResults() {
return results;
}
public void setResults(List<ResultsBean> results) {
this.results = results;
}
public class ResultsBean {
/**
* _id : 5bbb0de09d21226111b86f1c
* createdAt : 2018-10-08T07:57:20.978Z
* desc : 2018-10-08
* publishedAt : 2018-10-08T00:00:00.0Z
* source : web
* type : 福利
* url : https://ws1.sinaimg.cn/large/0065oQSqly1fw0vdlg6xcj30j60mzdk7.jpg
* used : true
* who : lijinshanmx
*/
private String _id;
private String createdAt;
private String desc;
private String publishedAt;
private String source;
private String type;
private String url;
private boolean used;
private String who;
public float getScale() {
return scale;
}
public void setScale(float scale) {
this.scale = scale;
}
private float scale;
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getUrl() {
return url;
}
public boolean isUsed() {
return used;
}
public void setUsed(boolean used) {
this.used = used;
}
public String getWho() {
return who;
}
}
}
根据 http://gank.io/api/data/福利/10/1 返回的json,用插件自动生成的JavaBean(java实体类)。GsonFormat插件的使用可点击此处
返回的json如下(我做了删减,以便于分析,觉得不够直观,可以将json用在线工具给可视化一下):
{
"error": false,
"results": [{
"_id": "5c12216d9d21223f5a2baea2",
"createdAt": "2018-12-13T09:07:57.2Z",
"desc": "2018-12-13",
"publishedAt": "2018-12-13T00:00:00.0Z",
"source": "web",
"type": "福利",
"url": "https://ws1.sinaimg.cn/large/0065oQSqgy1fy58bi1wlgj30sg10hguu.jpg",
"used": true,
"who": "lijinshanmx"
}, {
"_id": "5bfe1a5b9d2122309624cbb7",
"createdAt": "2018-11-28T04:32:27.338Z",
...
}, ...]
}
生成实体类无非就是给一个类的每个属性赋予get和set方法,为了可以获得和设置其值(当然,当对象的该属性不需要被获取和被设置,可以不给它这俩个方法,例如上面“_id”,“createdAt”等属性,我本次并不想用它们,就没有设置get和set方法。这个json较难的点在于下面这个地方。
“results”: [{…},{…}…]
其实我们可以这样理解:将整个传来的json看作一个对象,将其类命名为“GirlBean”,error和results是它的俩个属性,但results中有包含数据,所以在GirlBean中把它看作集合来给它get和set方法,
public List<ResultsBean> getResults() { return results;}
public void setResults(List<ResultsBean> results) { this.results = results;}
当我们获取到results后,将它看作一个对象,将其类命名为“ResultsBean”,这样“_id”,“createdAt”等数据都是它的属性了,然而我们目前需要图片的url,其他的不是太重要。细心同学会发现这里多了一个 private float scale;
这个是图片的比例,是api没有从json中给我们的,然而它对我们的图片在RV的瀑布流中的显示又是至关重要的属性,后面我们会提到。
从json解析出合适的URL
那现在我们对json数据的分析已经做完了,可是json这种奇奇怪怪的数据格式又该怎么解析出我们要的那些图片url呢?Google推荐了Gson框架,将json中的数据提取出来:
JsonObject jsonObject = new JsonParser().parse(girlData).getAsJsonObject();
JsonArray jsonArray = jsonObject.getAsJsonArray("results");//拿到数组
for (JsonElement pic : jsonArray) {
GirlBean.ResultsBean resultsBean = new Gson().fromJson(pic,
new TypeToken<GirlBean.ResultsBean>() {
}.getType());
因为本次我们解析的json是属于相对复杂的类型,在此我分享一个学习Gson的精品博客:Android:用GSON 五招之内搞定任何JSON数组
大家可以找相应的json去练习一下。我先以本次代码为例,猜一下GSON大概用法(只对这种类型的json数据)。
“girlData”是我们获取的json完整数据,jsonObject.getAsJsonArray("results")
通过"results"我们知道这是在提取json数据中的"results": [{...},{...}...]
部分,但不包含"results",是里面重复的包含键值对的“数组”,这也就是我们上面GirlBean中的ResultsBean的对象集合,Gson将其当作JsonArray对象,之后经过遍历与new Gson().fromJson(pic, new TypeToken<GirlBean.ResultsBean>() { }.getType());
这种解析方式便拿到了每一个照片的对象“resultsBean”,用resultsBean.getUrl();就能拿到每个照片的Url了·,将Url集合给RV适配器中的Glide处理,图片就出现了,哈哈哈哈…
等等!我们哪来的json完整数据“girlData”?
接下来,有请最后的网络通信库大佬OkHttp登场。
使用okHttp获取json数据
okHttp是android开源里最出色的网络通信库,没有之一,它的用法也不难。然而我还是遇到个坑,,,,
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(API_URL)
.build();
client.newCall(request).enqueue(new Callback() {
//请求失败的回调方法
@Override
public void onFailure(Call call, IOException e) {
LogUtil.i("找不到妹子图啦,,,");
}
//请求成功的回调方法
@Override
public void onResponse(Call call, Response response) throws IOException {
String girlData = response.body().string();
LogUtil.i(girlData);
JsonObject jsonObject = new JsonParser().parse(girlData).getAsJsonObject();
JsonArray jsonArray = jsonObject.getAsJsonArray("results");//拿到数组
for (JsonElement pic : jsonArray) {
GirlBean.ResultsBean resultsBean = new Gson().fromJson(pic,
new TypeToken<GirlBean.ResultsBean>() {
}.getType());
girlPicUrlList.add(resultsBean);
}
LogUtil.i("每张妹子照片的集合" + girlPicUrlList);//每张妹子照片的集合
response.body().close();//关闭body
new Thread(new Runnable() {
@Override
public void run() {
mHandler.sendEmptyMessage(GET_PIC);
}
}).start();
}
});
这段代码看着长,其实去掉上文说的解析json数据的代码就三行,如下:
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(API_URL).build();
client.newCall(request).enqueue(new Callback() {
//请求失败的回调方法
@Override
public void onFailure(Call call, IOException e) { LogUtil.i("找不到妹子图啦,,,");}
//请求成功的回调方法
@Override
public void onResponse(Call call, Response response) throws IOException {
//解析json获取url部分,省略
response.body().close();//关闭body
new Thread(new Runnable() {
@Override
public void run() {
mHandler.sendEmptyMessage(GET_PIC);
}
}).start();
}
});
好吧好吧,要是回调里面的你们不算一行,那就不算吧。
OkHttpClient client = new OkHttpClient();
这是创建一个OkHttpClient的实例。
Request request = new Request.Builder().url(API_URL).build();
因为我们这是get方法,所以在最终的build()方法之前,加了一个url方法来获取福利图api的网络地址,给Request对象。
client.newCall(request).enqueue(new Callback() {
//请求失败的回调方法
@Override
public void onFailure(Call call, IOException e) { LogUtil.i("找不到妹子图啦,,,");}
//请求成功的回调方法
@Override
public void onResponse(Call call, Response response) throws IOException {
String girlData = response.body().string();
......//解析json获取url部分,省略
response.body().close();//关闭body
new Thread(new Runnable() {
@Override
public void run() {
mHandler.sendEmptyMessage(GET_PIC);
}
}).start();
}
});
最后这“行”虽然有点多,但是看清了方法里的参数和注释,也是很好懂的。
在newCall()中填入之前获取的Request对象,在后面加个回调方法。
okHttp之所以强大,也是因为它有强大的异步方法调用。然而因为我很少接触异步方法,所以在这个回调里卡了一段时间,也是不太清楚回调的机制所致。
之前我作死地将整个回调方法,放在AsyncTask的doInBackground方法中了,虽然AsyncTask方法有完整的异步消息处理机制来刷新UI,然而回调方法中获取的数据在另一个子线程中,在AsyncTask中使用根本就是画蛇添足。所以分析之后,在关键的地方打上log,终于找到了bug所在。
在回调方法里有个onFailure()方法,这是用来执行请求失败时的操作,onResponse()方法便是执行请求成功时操作,这个方法的第二个参数response便是从api返回的数据,用String girlData = response.body().string();
这个方法就可以得到json数据了,也就是这个girlData。
这个回调方法也是在子线程中操作,所以在需要用Handler方法将数据提出来并刷新UI(子线程中不能刷新UI,主线程中不能执行耗时操作)。
@SuppressLint("HandlerLeak")
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case GET_PIC:
beautyAdapter.notifyDataSetChanged();
LogUtil.i("每张妹子照片的集合取出来了!!!" + girlPicUrlList);//每张妹子照片的集合
break;
}
}
};
放出mainActivity完整代码
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import com.example.roy.beauty_12_13.adapter.BeautyAdapter;
import com.example.roy.beauty_12_13.base.BaseActivity;
import com.example.roy.beauty_12_13.entity.GirlBean;
import com.example.roy.beauty_12_13.utils.LogUtil;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class MainActivity extends BaseActivity {
private static final int GET_PIC = 1;
private static final String API_URL="https://gank.io/api/data/福利/10/1";
@BindView(R.id.rv_beauty)
RecyclerView rvBeauty;
private BeautyAdapter beautyAdapter;
private List<GirlBean.ResultsBean> girlPicUrlList;
@SuppressLint("HandlerLeak")
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case GET_PIC:
beautyAdapter.notifyDataSetChanged();
LogUtil.i("每张妹子照片的集合取出来了!!!" + girlPicUrlList);//每张妹子照片的集合
break;
}
}
};
@Override
protected void setContentView() {
setContentView(R.layout.activity_main);
}
@Override
protected void initView() {
StaggeredGridLayoutManager staggeredGridLayoutManager=new StaggeredGridLayoutManager(2,android.support.v7.widget.StaggeredGridLayoutManager.VERTICAL);
rvBeauty.setLayoutManager(staggeredGridLayoutManager);
beautyAdapter = new BeautyAdapter(MainActivity.this, girlPicUrlList);
rvBeauty.setAdapter(beautyAdapter);
}
@Override
protected void initData() {
String[] s = new String[]{Manifest.permission.INTERNET, Manifest.permission.WRITE_EXTERNAL_STORAGE};
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, s, 1);
}
girlPicUrlList=new ArrayList<>();
getBeauty();
}
@Override
protected void initEvent() {
}
private void getBeauty() {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(API_URL)
.build();
client.newCall(request).enqueue(new Callback() {
//请求失败的回调方法
@Override
public void onFailure(Call call, IOException e) {
LogUtil.i("找不到妹子图啦,,,");//
}
//请求成功的回调方法
@Override
public void onResponse(Call call, Response response) throws IOException {
String girlData = response.body().string();
LogUtil.i(girlData);
JsonObject jsonObject = new JsonParser().parse(girlData).getAsJsonObject();
JsonArray jsonArray = jsonObject.getAsJsonArray("results");//拿到数组
for (JsonElement pic : jsonArray) {
GirlBean.ResultsBean resultsBean = new Gson().fromJson(pic,
new TypeToken<GirlBean.ResultsBean>() {
}.getType());
//GSON 提供了 TypeToken 这个类来帮助我们捕获(capture)像 List 这样的泛型信息。
// Java编译器会把捕获到的泛型信息编译到这个匿名内部类里,然后在运行时就可以被 getType() 方法用反射的 API 提取到。
//解释的很官方,实际上就是一句 通俗但不严谨 的话,它将泛型 T 转成 .class 。
// 比如上面的resultsBean 经过 getType() 后就是 ResultsBean.class 。
girlPicUrlList.add(resultsBean);
}
LogUtil.i("每张妹子照片的集合" + girlPicUrlList);//每张妹子照片的集合
response.body().close();//关闭body
new Thread(new Runnable() {
@Override
public void run() {
mHandler.sendEmptyMessage(GET_PIC);
}
}).start();
}
});
}
}
项目源码与心得
就此,我对此次项目的梳理就完了,在写博客的过程中,也发现了很多亟需优化的地方,之后会在优化后的项目放到github上。
项目源码
在归纳代码写博客的时候,最强烈的感觉还是对自己的代码理解得更透彻了。毕竟,你技术博客都写了,也不能唬你自己啊。
所以写技术博客还是好处多多,我会坚持下去的。