Android OkHttp + Glide + RecyclerView + ButterKnife流行框架的综合实现

要求

利用OkHttp一部获取慕课网电影数据(20条),使用Glide加载图片并显示在每个电影Item中,并且可以通过电影名称、类型进行搜索并展示。

运行效果

任务描述

一、首页显示内容:

  • 影片搜索区域(名字,spinner列表,搜索按钮)
  • 影片展示区域(RecyclerView 2列展示)

二、电影详情

  • 影片详情(详情UI布局)

三、搜索功能实现

  • 电影名搜索
  • 电影类型搜索
  • 叠加搜索(名字 + 类型)

注意:电影数据的访问地址:www.imooc.com/api/movie
名字和类型可在地址后面添加参数来完成访问,如:
www.imooc.com/api/movie?title=银河护卫队2&types=动作

框架地址

OkHttp
Glide
ButterKnife

分析

Movie: 存储每一个电影的数据
INetCallBack: 网络请求回调接口
MovieOkHttpUtils: 使用OkHttp提供从网络请求电影数据的方法
MovieRecyclerViewAdapter: 实现界面布局,提供更新UI的方法
MovieBiz: 获取网络返回结果,提供解析电影数据的方法,并实现UI布局的方法


MainActivity: 应用主界面
MovieDetailsActivity: 电影信息详情页面

代码实现

涉及到网络请求操作以及各类框架的使用,需完成网络配置以及根据框架地址来加载相关依赖

Movie

这个比较简单,声明了电影的相关属性、全参构造方法和一些get方法。为了方便,这里把大部分参数都设置为String类型,同时实现了Serializable接口,为了接下来在不同的Activity间传参做准备

public class Movie implements Serializable {
    private String _id, average, title, description, directorsName, year, types, imageUrl, castsName;
    private int stars;

    public Movie(String _id, String average, int stars, String title, String description, String directorsName, String year, String types, String castsName, String imageUrl) {
        this._id = _id;
        this.average = average;
        this.stars = stars;
        this.title = title;
        this.description = description;
        this.directorsName = directorsName;
        this.year = year;
        this.types = types;
        this.castsName = castsName;
        this.imageUrl = imageUrl;
    }
	
	// Getter和Setter...
}

INetCallBack

这里声明的是一个接口,声明两个方法用于网络请求时的回调

public interface INetCallBack {
    void onSuccess(String response);
    void onFailed(Throwable ex);
}

MovieOkHttpUtils

这个类要通过实现OkHttp来提供一个获取网络数据的doGet()方法,在此方法中完成对INetCallBack接口的回调

	// OkHttp网络请求方法,返回结果字符串
    public void doGet(String url, INetCallBack callBack){
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(url)
                .build();
        Call call = client.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                mUiHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        callBack.onFailed(e);
                    }
                });
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                String respStr = null;
                try {
                    respStr = response.body().string();
                } catch (IOException e) {
                    mUiHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            callBack.onFailed(e);
                        }
                    });
                    return;
                }
                String finalRespStr = respStr;
                mUiHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        callBack.onSuccess(finalRespStr);
                    }
                });
            }
        });
    }

这个类可以设计成单例模式,在实际开发情况下只需要一个提供doGet()方法的对象即可

MovieRecyclerViewAdapter

电影信息展示是通过RecyclerView来实现的,这里需要实现网格布局和线型布局两种
在这里插入图片描述 在这里插入图片描述
实现分为以下四步:

  1. 创建一个类继承RecyclerView.Adapter
  2. 创建一个内部类绑定ViewHolder(继承RecyclerView.ViewHolder,在类内完成控件初始化)
  3. 实现Adapter的相关方法
  4. 设置子项点击监听

因为该类中的onBindViewHolder()方法是运行在UI线程的,所以在此类中直接完成对UI的更改
这里需要使用到Glide来完成网络图片的获取及加载

public class MovieRecyclerViewAdapter extends RecyclerView.Adapter<MovieRecyclerViewAdapter.ViewHolder> {

    private Context context;
    private List<Movie> data;
    private OnItemClickListener onItemClickListener;
    private RecyclerView recyclerView;

    public MovieRecyclerViewAdapter(Context context, RecyclerView recyclerView) {
        this.context = context;
        this.data = new ArrayList<>();
        this.recyclerView = recyclerView;
    }

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    public void setData(List<Movie> data) {
        this.data = data;
        notifyDataSetChanged();
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if (recyclerView.getLayoutManager().getClass() == GridLayoutManager.class) {
            return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.itemview_movie_grid, parent, false));
        } else if (recyclerView.getLayoutManager().getClass() == LinearLayoutManager.class) {
            return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.itemview_movie_linear, parent, false));
        }
        return null;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        holder.name.setText(data.get(position).getTitle());
        holder.stars.setRating((float) data.get(position).getStars() / 10);
        holder.average.setText(data.get(position).getAverage());
        holder.directorsName.setText(data.get(position).getDirectorsName());
        holder.castName.setText(data.get(position).getCastsName());
        holder.year.setText(data.get(position).getYear());

        // 通过Glide下载图片,并显示
        Glide.with(context)
                .load(data.get(position).getImageUrl())
                .into(holder.image);

        // 设置子项点击事件
        holder.view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (onItemClickListener != null) {
                    onItemClickListener.onItemClick(position);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return data.size();
    }

    // 子项点击回调接口
    public interface OnItemClickListener {
        void onItemClick(int position);
    }
    
    class ViewHolder extends RecyclerView.ViewHolder {
        @BindView(R.id.image)
        ImageView image;
        @BindView(R.id.name)
        TextView name;
        @BindView(R.id.ratingbar)
        RatingBar stars;
        @BindView(R.id.average)
        TextView average;
        @BindView(R.id.directorsname)
        TextView directorsName;
        @BindView(R.id.castsname)
        TextView castName;
        @BindView(R.id.year)
        TextView year;

        View view;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            view = itemView;
        }
    }
}

虽然网格布局的Item控件比线性布局的Item少,不过可以两边的布局文件都添加一样的控件,设置一样的id,再根据需要来隐藏部分控件
这样就只需要通过判断传入的recyclerView对象的布局来确定使用哪个布局文件,不需要在用ButterKnife注入View的时候刻意区分不同的布局

MovieBiz

实现获取网络数据的方法并解析,实现UI布局的方法,同时也实现了RecyclerView的子项单击事件
这里传入了一个Boolean类型的参数isLoadAll,是用来区分网格布局还是线型布局的依据

public class MovieBiz {

    private Context context;
    private List<Movie> movieList;
    private RecyclerView recyclerView;
    private MovieRecyclerViewAdapter adapter;

    public MovieBiz(Context context, RecyclerView recyclerView) {
        this.context = context;
        this.recyclerView = recyclerView;
    }

    // 从网络获取数据
    // 参数1:链接地址字符串  参数2:是否获取全部数据
    public void getMovie(String url, Boolean isLoadAll) {
        movieList = new ArrayList<>();
        MovieOkHttpUtils.getInstance()
                .doGet(url, new INetCallBack() {
                    @Override
                    public void onSuccess(String response) {
                        // 解析结果
                        try {
                            JSONObject root = new JSONObject(response);
                            int total = root.optInt("total");
                            JSONArray rootArray = root.optJSONArray("movies");

                            for (int i = 0; i < total; i++) {
                                movieList.add(parseResponse(rootArray.optJSONObject(i)));
                            }
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }

                        if(isLoadAll){
                            MainActivity.movies = movieList;
                        }
                        setUiRecyclerView(movieList,isLoadAll);
                    }

                    @Override
                    public void onFailed(Throwable ex) {
                        Toast.makeText(context, "网络发生错误", Toast.LENGTH_SHORT).show();
                    }
                });
    }

    // 设置UI界面,RecyclerView网格布局
    public void setUiRecyclerView(List<Movie> movieList, Boolean isLoadAll) {
        if (isLoadAll) {
            GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 2);
            recyclerView.setLayoutManager(gridLayoutManager);
        }else{
            LinearLayoutManager linearLayoutManager = new LinearLayoutManager(context);
            recyclerView.setLayoutManager(linearLayoutManager);
        }
        adapter = new MovieRecyclerViewAdapter(context,recyclerView);
        adapter.setData(movieList);
        recyclerView.setAdapter(adapter);

        // 子项点击事件监听
        adapter.setOnItemClickListener(new MovieRecyclerViewAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(int position) {
                Movie movie = movieList.get(position);
                Intent it = new Intent(context, MovieDetailsActivity.class);
                it.putExtra("movie",movie);
                context.startActivity(it);
            }
        });
    }

    // 解析返回的字符串
    public Movie parseResponse(JSONObject root) {
        if (root != null) {
            String id = root.optString("id");
            String average = new Rating(root).getAverage();
            int stars = new Rating(root).getStars();
            String types = getTypes(root.optJSONArray("types"));
            String title = root.optString("title");
            String description = root.optString("description");
            String castsName = getCasts(root.optJSONArray("casts"));
            String directorsName = root.optJSONArray("directors").optJSONObject(0).optString("name");
            String year = root.optString("year");
            String imageUrl = root.optString("imageUrl");

            // 创建Movie对象
            Movie movie = new Movie(id, average, stars, title, description, directorsName, year, types, castsName, imageUrl);
            return movie;
        }
        return null;
    }

    // 提供主演数据字符串
    private static String getCasts(JSONArray casts) {
        int len = casts.length();
        String str = new String();
        for (int i = 0; i < len; i++) {
            JSONObject cast = casts.optJSONObject(i);
            str += cast.optString("name");
            if (i < len-1){
                str += " ";
            }
        }
        return str;
    }

    // 获取类型字符串
    private static String getTypes(JSONArray types) {
        int len = types.length();
        String str = new String();
        for (int i = 0; i < len; i++) {
            str += types.optString(i);
            if (i != len - 1) {
                str += "/";
            }
        }
        return str;
    }

    // 提供星星数据
    static class Rating {
        private String average;
        private int stars;

        public Rating(JSONObject root) {
            JSONObject rating = root.optJSONObject("rating");
            average = rating.optString("average");
            stars = rating.optInt("stars");
        }

        public String getAverage() {
            return average;
        }

        public int getStars() {
            return stars;
        }
    }
}

MainActivity

先试用ButterKnife注入View

    @BindView(R.id.toolbar)
    Toolbar toolbar;
    @BindView(R.id.fab)
    FloatingActionButton fab;
    @BindView(R.id.moviename_edit)
    EditText movieName;
    @BindView(R.id.movietypes_spinner)
    Spinner types_spinner;
    @BindView(R.id.search_img)
    ImageView search;
    @BindView(R.id.index_recyclerview)
    RecyclerView recyclerView;
    @BindView(R.id.tips_txt)
    TextView tips;
    @BindView(R.id.back_txt)
    TextView back;
    @BindArray(R.array.movieclass)
    String[] items;
    private ArrayAdapter<String> arrayAdapter_spinner;
    private MovieBiz movieBiz;
    private String type;
    private List<Movie> searchList;
    public static List<Movie> movies = new ArrayList<>();

根据提供的方法,初始化首页

    private void initView() {
        setSupportActionBar(toolbar);
        setTitle("慕课电影");

        // 初始化首页显示
        movieBiz = new MovieBiz(this, recyclerView);
        movieBiz.getMovie("http://www.imooc.com/api/movie",true);
        
        // 初始化Spinner
        arrayAdapter_spinner = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, items);
        types_spinner.setAdapter(arrayAdapter_spinner);
    }

初始化控件的点击事件
注:ButterKnife无法注入Spinner的子项点击事件

    private void initEvent() {
        // Spinner子项点击事件
        types_spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                type = items[position];
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {

            }
        });
    }

通过ButterKnife来实现单击事件

    // 搜索图片单击事件
    @OnClick(R.id.search_img)
    public void searchOnClick() {
        searchList = new ArrayList<>();
        tips.setText("搜索");
        back.setVisibility(View.VISIBLE);
        if (movieName.getText() != null && !type.equals("请选择")) {
            movieBiz.getMovie("http://www.imooc.com/api/movie?title="+movieName.getText()+"&types="+type,false);
        }else if(movieName.getText()!=null){
            movieBiz.getMovie("http://www.imooc.com/api/movie?title="+movieName.getText(),false);
        }else if(type.equals("请选择")){
            movieBiz.getMovie("http://www.imooc.com/api/movie?types="+type,false);
        }
    }

    // “返回”单击事件
    @OnClick(R.id.back_txt)
    public void backOnClick(){
        tips.setText("正在热映");
        back.setVisibility(View.GONE);
        movieBiz.setUiRecyclerView(movies,true);
    }

MovieDetailsActivity

在详情页面中比较难完成的是上半部分可滑动的Toolbar控件,它是通过CoordinatorLayout + AppBarLayout + CollapsingToolbarLayout来实现的
注意:要实现滑动效果,必须要在CollapsingToolbarLayout内,AppBarLayout的下面添加一个可滑动的控件,如:ListView、RecyclerView。如果要添加TextView这种不可滑动的控件,需要在外面套一层NestedScrollView来实现滑动


详情页布局XML

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_width="match_parent"
        android:layout_height="300dp">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsingtoolbarlayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:collapsedTitleGravity="center_vertical"
            app:contentScrim="#6200EE"
            app:expandedTitleGravity="bottom|center_horizontal"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/image_details"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="fitXY"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.8" />

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/title_details"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />
        </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <TextView
            android:id="@+id/content_details"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:textSize="16dp" />
    </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Java代码
这里只需要获取从上一个界面传过来的信息,并展示到对应控件即可

public class MovieDetailsActivity extends AppCompatActivity {

    private static final String TAG = "MovieDetails";
    @BindView(R.id.collapsingtoolbarlayout)
    CollapsingToolbarLayout collapsingToolbarLayout;
    @BindView(R.id.image_details)
    ImageView image;
    @BindView(R.id.title_details)
    androidx.appcompat.widget.Toolbar title;
    @BindView(R.id.content_details)
    TextView content;
    private Movie movie = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_details);

        ButterKnife.bind(this);

        // 获取从上一个界面传入的电影对象
        movie = (Movie) getIntent().getSerializableExtra("movie");
        initView(movie);

    }

    private void initView(Movie movie) {
        // 设置收缩时标题颜色
        collapsingToolbarLayout.setCollapsedTitleTextColor(Color.WHITE);
        // 设置扩展时标题颜色
        collapsingToolbarLayout.setExpandedTitleColor(Color.WHITE);

        // 展示信息
        if (movie != null) {
            Glide.with(this)
                    .load(movie.getImageUrl())
                    .into(image);
            title.setTitle(movie.getTitle());
            content.setText("导演:" + movie.getDirectorsName()
                    + "\n主演:" + movie.getCastsName()
                    + "\n上映时间:" + movie.getYear()
                    + "\n类型:" + movie.getTypes()
                    + "\n\n故事简介:\n" + movie.getDescription());
        }
    }
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值