在Android上实现Master/Detail Flow
Master/Detail Flow包含Master和Detail两个页面,Master页面显示包含了若干项目(item)的列表,给出各个项目的大概信息。当点击某个项目时,包含了该项目详细信息的Detail页面会显示出来。这种模式的一个优势在于,可以方便地适配不同的屏幕尺寸:手机屏幕尺寸有限,一般一次只能展示一个页面,那么首先显示Master,当用户点击感兴趣的项目时,再将该项目的Detail显示出来;而平板电脑的屏幕尺寸比较大,可以将Master和Detail并排显示,避免显示空间的浪费。
在Android上实现Master/Detail Flow十分方便,新版本的Android Studio已经自带了Master/Detail Flow的模板。下面通过一个例子介绍Master/Detail Flow的基本实现方法。例子从TMDb上获取一系列电影信息,在Master页面由RecyclerView以网格的形式显示出来,当点击了某一个电影之后,显示包含了该电影详细信息的Detail页面。从TMDb获取电影信息的方法可参考Retrofit使用方法举例,RecyclerView的使用方法可参考RecyclerView使用方法举例(1)和RecyclerView使用方法举例(2)。
最终实现效果如下:
Contents
1. 页面层级和布局
1.1. Detail Activity布局
首先进行Detail页面的布局,Detail页面在小屏幕上会覆盖Master页面显示,在大屏幕上会和Master页面并排显示,无论如何显示,Detail页面本身的布局是不变的。Detail Acitvity的布局层级如下:
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" ... > <android.support.design.widget.AppBarLayout ... > <android.support.design.widget.CollapsingToolbarLayout ... > <ImageView android:id="@+id/detail_backdrop" ... /> <android.support.v7.widget.Toolbar ... /> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:id="@+id/movie_detail_container" ... /> </android.support.design.widget.CoordinatorLayout>
其中movie_detail_container 用于盛放Detail Fragment。注意这里在CollapsingToolbarLayout里面放了一个ImageView,用来显示一张电影图片。
1.2. Detail Fragment布局
Detail Fragment用来显示电影的详细信息,完整代码参考这里。
1.3. Master Activity布局
在Master页面中加入一个FrameLayout,其中include了movie_grid :
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" ... > <android.support.design.widget.AppBarLayout ... > <android.support.v7.widget.Toolbar ... /> </android.support.design.widget.AppBarLayout> <FrameLayout android:id="@+id/frameLayout" ...> <include layout="@layout/movie_grid" /> </FrameLayout> </android.support.design.widget.CoordinatorLayout>
【完整代码】
上面的movie_grid 是实现Master/Detail Flow布局自动适配屏幕尺寸的关键,在/res/layout/movie_grid.xml中,布局只有MovieGridFragment,即Master页面的Fragment,此时屏幕中只显示Master页面,适用于较小的屏幕:
<fragment ... android:id="@+id/fragment" android:name="com.nex3z.examples.masterdetail.ui.fragment.MovieGridFragment" />
【完整代码】
此外在res/layout/movie_grid_twopanes.xml中,给出Master和Detail并列显示的布局,movie_detail_container 用于盛放Detail Fragment,适用于较大的屏幕:
<LinearLayout ... "> <fragment android:id="@+id/fragment" android:name="com.nex3z.examples.masterdetail.ui.fragment.MovieGridFragment" ... /> <ScrollView android:id="@+id/movie_detail_container" ... /> </LinearLayout>
【完整代码】
最后在res/values-sw600dp/refs.xml中,以别名的形式,为最小屏幕宽度为600dp的设备适配Master和Detail并列显示的布局,即movie_grid_twopanes.xml。
<resources> <item type="layout" name="movie_grid">@layout/movie_grid_twopanes</item> </resources>
【完整代码】
1.4. Master Fragment布局
Master Fragment包含一个RecyclerView,用于以网格的形式显示电影。
2. Master/Detail的切换
2.1. 判断当前布局类型
Master/Detail模式在不同屏幕尺寸的设备上具有不同的界面布局,首先需要获取当前的布局类型,以此来决定Detail Activity的显示逻辑。
由前面Master Activity的布局可见,小屏幕和大屏幕设备布局的区别,在于movie_detail_container 的位置。小屏幕设备使用/res/layout/movie_grid.xml,其中没有movie_detail_container ,movie_detail_container 位于Detail Activity的布局里。大屏幕设备使用res/layout/movie_grid_twopanes.xml,里面有movie_detail_container ,可以直接将Detail Fragment添加进去。通过检查movie_detail_container 是否存在,可以判断当前的布局类型。在Master Activity中加入:
private boolean mTwoPane; ... @Override protected void onCreate(Bundle savedInstanceState) { ... setContentView(R.layout.activity_movie_grid); ... if (findViewById(R.id.movie_detail_container) != null) { mTwoPane = true; } else { mTwoPane = false; } }
其中mTwoPane 用于指示当前布局类型。
2.2. Detail页面的显示
当用户点击Master页面的电影时,将出现包含该电影详细信息的Detail页面。根据当前布局类型的不同,选择显示Detail Activity覆盖Master Activity(小尺寸屏幕),或者直接将Detail Fragment添加到Master页面旁边(大尺寸屏幕)。
2.1.1. 为RecyclerView Adapter添加OnClickListener
首先要让RecyclerView能够支持点击事件,在RecyclerView Adapter中加入:
public class MovieAdapter extends RecyclerView.Adapter<MovieAdapter.ViewHolder> { private static OnItemClickListener mListener; public interface OnItemClickListener { void onItemClick(int position, MovieAdapter.ViewHolder vh); } public void setOnItemClickListener(OnItemClickListener listener) { this.mListener = listener; } ... public static class ViewHolder extends RecyclerView.ViewHolder { ... public ViewHolder(View itemView) { super(itemView); ... itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mListener != null) { mListener.onItemClick(getLayoutPosition(), ViewHolder.this); } } }); } } }
之后就可以使用setOnItemClickListener() 来添加点击事件监听。
2.2.2. 传递点击事件到Master Activity
显示电影列表的RecyclerView位图Master Fragment,当点击事件发生时,Master Fragment将此事件传递给Master Activity,由Master Activity根据当前布局类型mTwoPane 决定应当如何显示Detail页面。
在Master Fragment中添加接口用于回调,Master Activity应实现该接口,然后在onAttach() 的时候从Master Activity中获取回调方法:
private Callbacks mCallbacks = sDummyCallbacks; public interface Callbacks { void onItemSelected(Movie movie, MovieAdapter.ViewHolder vh); } ... @Override public void onAttach(Context context) { super.onAttach(context); if (!(context instanceof Callbacks)) { throw new IllegalStateException("Activity must implement fragment's callbacks."); } mCallbacks = (Callbacks) context; }
在点击事件发生时,调用该回调方法:
mMovieAdapter = new MovieAdapter(mMovies); mMovieAdapter.setOnItemClickListener(new MovieAdapter.OnItemClickListener() { @Override public void onItemClick(int position, MovieAdapter.ViewHolder vh) { Log.v(LOG_TAG, "onItemClick(): position = " + position); Movie movie = mMovies.get(position); if (movie != null) { mCallbacks.onItemSelected(movie, vh); } mPosition = position; } });
【完整代码】
2.2.3. 在Master Activity处理点击事件并显示Detail页面
Master Activity实现Master Fragment定义的回调接口,当用户点击RecyclerView中的电影时,Master Fragment通过回调方法通知Master Activity。
public class MovieGridActivity extends AppCompatActivity implements MovieGridFragment.Callbacks{ ... @Override public void onItemSelected(Movie movie, MovieAdapter.ViewHolder vh) { Log.v(LOG_TAG, "onItemSelected(): movie = " + movie); if (mTwoPane) { MovieDetailFragment fragment = MovieDetailFragment.newInstance(movie); getSupportFragmentManager().beginTransaction() .replace(R.id.movie_detail_container, fragment) .commit(); } else { Intent intent = new Intent(this, MovieDetailActivity.class) .putExtra(MovieDetailActivity.MOVIE_INFO, movie); ActivityOptionsCompat activityOptions = ActivityOptionsCompat .makeSceneTransitionAnimation(this, new Pair<View, String>( vh.mPoster, getString(R.string.detail_poster_transition_name))); ActivityCompat.startActivity(this, intent, activityOptions.toBundle()); } } }
当mTwoPane 为true时,当前为大屏的双栏模式,直接将Detail Fragment添加到movie_detail_container 里面;当mTwoPane 为false时,当前为小屏,显示Detail Activity。
3. 完整代码
完整代码可以参考这里。
Thank you very much for the tutorial 🙂
can you do more tutorial in changing the sorting data from SORT_BY_POPULARITY_DESC to SORT_BY_VOTE_AVERAGE_DESC by using button or popup menu?
Hi Zaki, I’ve added a popup menu for sort options. Check it out at here.
You can also find the code here.