在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)

  最终实现效果如下:

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. 完整代码

  完整代码可以参考这里

 

 

 

2 Comments

  1. Zaki

    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?

    1. nex3z (Post author)

      Hi Zaki, I’ve added a popup menu for sort options. Check it out at here.
      You can also find the code here.

Comments are closed.