当前位置: 代码迷 >> 综合 >> Android OkHttp + Glide + RecyclerView + ButterKnife流行框架的综合实现
  详细解决方案

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

热度:5   发布时间:2024-02-11 00:29:50.0

文章目录

  • 要求
  • 运行效果
  • 任务描述
  • 框架地址
  • 分析
  • 代码实现
    • Movie
    • INetCallBack
    • MovieOkHttpUtils
    • MovieRecyclerViewAdapter
    • MovieBiz
    • MainActivity
    • MovieDetailsActivity

要求

利用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() {@Overridepublic void onFailure(@NotNull Call call, @NotNull IOException e) {mUiHandler.post(new Runnable() {@Overridepublic void run() {callBack.onFailed(e);}});}@Overridepublic 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() {@Overridepublic void run() {callBack.onFailed(e);}});return;}String finalRespStr = respStr;mUiHandler.post(new Runnable() {@Overridepublic 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@Overridepublic 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;}@Overridepublic 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() {@Overridepublic void onClick(View v) {if (onItemClickListener != null) {onItemClickListener.onItemClick(position);}}});}@Overridepublic 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() {@Overridepublic 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);}@Overridepublic 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() {@Overridepublic 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);// 初始化SpinnerarrayAdapter_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() {@Overridepublic void onItemSelected(AdapterView<?> parent, View view, int position, long id) {type = items[position];}@Overridepublic 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.AppBarLayoutandroid:id="@+id/appBarLayout"android:layout_width="match_parent"android:layout_height="300dp"><com.google.android.material.appbar.CollapsingToolbarLayoutandroid: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"><ImageViewandroid: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.Toolbarandroid: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.NestedScrollViewandroid:layout_width="match_parent"android:layout_height="match_parent"app:layout_behavior="@string/appbar_scrolling_view_behavior"><TextViewandroid: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;@Overrideprotected 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());}}
}
  相关解决方案