文章目录
- 要求
- 运行效果
- 任务描述
- 框架地址
- 分析
- 代码实现
- 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来实现的,这里需要实现网格布局和线型布局两种
实现分为以下四步:
- 创建一个类继承RecyclerView.Adapter
- 创建一个内部类绑定ViewHolder(继承RecyclerView.ViewHolder,在类内完成控件初始化)
- 实现Adapter的相关方法
- 设置子项点击监听
因为该类中的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());}}
}