简单的DEMO记录:recyclerview+Glide+okhttp 获取福利图

前言

    自学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框架的同学可以不加

其它原材料
  1. https://gank.io/api 的api: http://gank.io/api/data/福利/10/1
    福利api

  2. 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用在线工具给可视化一下):

扫描二维码关注公众号,回复: 4959761 查看本文章
{
	"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上。
项目源码
在归纳代码写博客的时候,最强烈的感觉还是对自己的代码理解得更透彻了。毕竟,你技术博客都写了,也不能唬你自己啊。
所以写技术博客还是好处多多,我会坚持下去的。

猜你喜欢

转载自blog.csdn.net/NULL_thing/article/details/85038228
今日推荐