文章目录
要求
利用OkHttp一部获取慕课网电影数据(20条),使用Glide加载图片并显示在每个电影Item中,并且可以通过电影名称、类型进行搜索并展示。
运行效果
任务描述
一、首页显示内容:
- 影片搜索区域(名字,spinner列表,搜索按钮)
- 影片展示区域(RecyclerView 2列展示)
二、电影详情
- 影片详情(详情UI布局)
三、搜索功能实现
- 电影名搜索
- 电影类型搜索
- 叠加搜索(名字 + 类型)
注意:电影数据的访问地址:www.imooc.com/api/movie
名字和类型可在地址后面添加参数来完成访问,如:
www.imooc.com/api/movie?title=银河护卫队2&types=动作
框架地址
分析
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来实现的,这里需要实现网格布局和线型布局两种
实现分为以下四步:
- 创建一个类继承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
@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());
}
}
}