• 札记:Fragment基础


    Fragment概述

    在Fragment出现之前,Activity是app中界面的基本组成单位,值得一提的是,作为四大组件之一,它是需要“注册”的。组件的特性使得一个Activity可以在整个app甚至是不同app间被复用。
    随着android 3.0中安卓平板的新增,app对不同尺寸屏幕的适配需求更加突出,Fragment大概也因为这样的需要被引入。虽然可以为Activity动态指定不同的layout,但也仅仅是解决一些简单的适配。像手机和平板这样的显著不同的尺寸下,是需要完全不同的界面设计的。此时,界面需要是不同几个部分的组成,根据实际的屏幕大小,它们动态的组成一个界面或者是分离到不同界面中,经典的案例说明就是“列表-详情”界面。虽然Activity可以相互嵌套,但在支持上(设计初衷)显得很笨重,因此,sdk中引入了Fragment,它作为对Activity的一个模块化拆分,类似Activity那样的包含逻辑和View布局的“sub activity”,具有Activity那样的生命周期和回退栈,可以接收事件等。通过Fragment来合理地分解一个复杂界面为多个模块化的子界面,可以满足一些动态的界面组合适配需求,同时也让界面代码更好的复用,并因为更加细分而设计清晰。此外,Fragment是无需注册的,这样它比Activity更加具备动态创建的可能性,基于此甚至出现了一些单一Activity这样的app框架设计。更多它的特性,接下来就一起来探索吧。

    基本使用

    Fragment是API 11(android 3.0)中引入的,而support v4库中提供了向前兼容的实现。下面为了突出重点,不使用对应的兼容实现,假设app的最低版本为api 11以上。
    从设计上,Fragment应该是一个独立完整的模块化界面组件,包含自身的layout和界面交互逻辑。但在使用上,它离不开Activity,必须内嵌到一个Activity实例中。因为它就是设计来作为部分化的Activity,它的生命周期就是基于所在Activity的生命周期,没必要独立存在。
    一个Fragment需要依附到Activity来进行显示。可以通过在Activity的layout中使用标签或在代码中动态创建Fragment来完成在Activity中显示Fragment。

    定义Fragment子类

    类似定义Activity那样,通过继承Fragment类来定义一个fragment类型。特殊的是,“通常”唯一需要定义的方法是onCreateView(),它用来提供fragment关联的layout,除非是一个无界面的Fragment。

    下面是一个极简单的Fragment定义:

    import android.os.Bundle;
    import android.support.v4.app.Fragment;
    import android.view.LayoutInflater;
    import android.view.ViewGroup;
    
    public class ArticleListFragment extends Fragment {
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
            // Inflate the layout for this fragment
            return inflater.inflate(R.layout.article_list, container, false);
        }
    }
    

    方法onCreateView()返回的View就是Fragment对应layout(ViewHierarchy)的root。
    参数container是来自所在Activity的layout的一个ViewGroup,最终Fragment的布局被添加作为此
    container的childView。inflate()方法最后一个参数表示是否将加载的view添加到container中,因为onCreateView()返回的view默认就会被Activity添加到container中,这里就传递false后方法inflate()返回布局的根View,否则inflate()会返回container参数,具体细节见方法inflate()
    的说明。

    方法inflate()原型:

    /**
     * Inflate a new view hierarchy from the specified xml resource. Throws
     * {@link InflateException} if there is an error.
     *
     * @param resource ID for an XML layout resource to load (e.g.,
     *        <code>R.layout.main_page</code>)
     * @param root Optional view to be the parent of the generated hierarchy (if
     *        <em>attachToRoot</em> is true), or else simply an object that
     *        provides a set of LayoutParams values for root of the returned
     *        hierarchy (if <em>attachToRoot</em> is false.)
     * @param attachToRoot Whether the inflated hierarchy should be attached to
     *        the root parameter? If false, root is only used to create the
     *        correct subclass of LayoutParams for the root view in the XML.
     * @return The root View of the inflated hierarchy. If root was supplied and
     *         attachToRoot is true, this is root; otherwise it is the root of
     *         the inflated XML file.
     */
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
    

    一般的,定义一个Fragment时需要重写Fragment的几个生命周期中的回调方法包括:

    • onCreateView()
      系统调用此方法用来在第一次创建Fragment时,获得其layout。
      方法原型:
    /**
     * Called to have the fragment instantiate its user interface view.
     * This is optional, and non-graphical fragments can return null (which
     * is the default implementation).  This will be called between
     * {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}.
     *
     * <p>If you return a View from here, you will later be called in
     * {@link #onDestroyView} when the view is being released.
     *
     * @param inflater The LayoutInflater object that can be used to inflate
     * any views in the fragment,
     * @param container If non-null, this is the parent view that the fragment's
     * UI should be attached to.  The fragment should not add the view itself,
     * but this can be used to generate the LayoutParams of the view.
     * @param savedInstanceState If non-null, this fragment is being re-constructed
     * from a previous saved state as given here.
     *
     * @return Return the View for the fragment's UI, or null.
     */
    @Nullable
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
            @Nullable Bundle savedInstanceState) {
        return null;
    }
    
    • onCreate()
      系统在开始创建Fragment时调用,这里做一些和Fragment相关状态的初始化。

    • onPause()
      指示用户离开Fragment所在Activity的使用调用,在这里做一些状态保存的操作。

    上面几个方法基本就是Activity对应生命周期回调方法的一个调用传递,后面会在“Fragment生命周期”中详细介绍各个回调方法的用途,接下来就看看如何在Activity中使用Fragment。

    在layout中添加Fragment

    接下来的示例是一个文章列表和文章详情页面的场景。app会在不同屏幕尺寸时动态选择在同一个Activity中同时显示文章列表和对应选择的文章的详情信息,或者单独的一个列表界面,选择一个文章后打开新Activity来显示文章详情。
    下面是ArticleListActivity对应的布局文件,它里面同时声明了2个Fragment作为其界面组成:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <fragment android:name="com.example.news.ArticleListFragment"
                android:id="@+id/list"
                android:layout_weight="1"
                android:layout_width="0dp"
                android:layout_height="match_parent" />
        <fragment android:name="com.example.news.ArticleDetailFragment"
                android:id="@+id/viewer"
                android:layout_weight="2"
                android:layout_width="0dp"
                android:layout_height="match_parent" />
    </LinearLayout>
    

    通过上面这种在layout中使用<fragment>标签的方式来为Activity添加Fragment的话,在Fragment中调用方法isInLayout()返回true,该方法用来指示当前Fragment实例是否处在一个View布局中。此外,可以重写方法onInflate()来获得标签中定义的attr。

    标签<fragment>指定的Fragment在Activity的布局加载中会被实例化,onCreateView()返回的View将替换<fragment>元素在layout中的位置。
    对应有View的Fragment,需要为它提供一个标识来访问它,供系统在Activity的重建过程中使用,在对Fragment执行删除,替换等操作时也要用到。提供标识的方式有:

    • 提供 android:id 属性来指定一个唯一的整数ID,类似其它layout中的View那样。
    • 提供 android:tag 来指定一个唯一的字符串标识。
    • 如果以上2者均未提供,默认会使用<fragment>标签所在容器View的ID。

    代码添加Fragment

    在Activity中通过代码来添加Fragment时需要用到FragmentManager和FragmentTransaction。在Activity运行期间,对Fragment的操作是以事务的形式进行的,可以在一次事务中执行多个Fragment相关操作,最后提交事务。

    示例如下:

    FragmentManager fragmentManager = getFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    
    ArticleFragment fragment = new ArticleFragment();
    fragmentTransaction.add(R.id.layout_article_detail, fragment);
    fragmentTransaction.commit();
    

    上面的代码完成了向布局中id为R.id.layout_article_detail的ViewGroup中添加一个fragment的操作。总之,带界面的Fragment添加到Activity时,就需要指定其View所在的ViewGroup。

    无界面Fragment

    一些情况下,可以定义一个没有界面的Fragment,此时它拥有关联的Activity的各种回掉,状态保存和恢复等。可以完成一些Activity运行期间的后台操作。
    无界面Fragment没用关联的View,不需要重写onCreateView()方法。此时只会以代码的方式添加它到Activity,并且使用的是FragmentTransaction.add(Fragment fragment, String tag)方法,此时fragment实例不需要ID来标识它,因为它不存在于layout中,使用一个String类型的tag来标记它,之后可以通过FragmentManager.findFragmentByTag(String tag)来访问它。

    Fragment的管理

    上面简单的在代码中添加了一个Fragment到当前Activity。这里对可以针对Fragment执行的各种操作进行总结。

    FragmentManager

    首先通过getFragmentManager() 得到一个FragmentManager对象。使用它可以完成以下操作:

    • 获得activity中的fragment实例。findFragmentById()方法用来获取提供UI到Activity的layout中的fragment。findFragmentByTag()用来获取无界面的fragment。
    • 使用方法popBackStack()对Activity管理的Fragment的回退栈执行出栈操作。
    • 提供方法addOnBackStackChangedListener()来添加一个监听器来响应回退栈的变化。

    FragmentTransaction和回退栈

    在Activity运行期间,可以动态地添加,移除fragment来改变界面显示和行为。这些关于Fragment的修改操作,需要通过FragmentTransaction 来完成,像下面这样获得它:

    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    

    FragmentTransaction提供的有关Fragment的操作有:

    • add()
      add系列方法用来添加一个Fragment到Activity。如add(int containerViewId, Fragment fragment, String tag)用来将参数fragment对象添加到containerViewId表示的layout中的ViewGroup的位置。可选的tag字符串用来对fragment进行标识。这样以后可以通过FragmentManager.findFragmentByTag(String tag)来获取它(因为fragment也会发生restore这样的过程,不能像普通对象那样简单的保持其引用来保持它)。

    • hide(Fragment fragment)
      当参数fragment的View已经添加到布局容器中时,可以通过此方法来隐藏对应的View。
      Hides an existing fragment. This is only relevant for fragments whose views have been added to a container, as this will cause the view to be hidden.

    • remove(Fragment fragment)
      Remove an existing fragment. If it was added to a container, its view is also removed from that container.

    • replace (int containerViewId, Fragment fragment, String tag)
      Replace an existing fragment that was added to a container. This is essentially the same as calling remove(Fragment) for all currently added fragments that were added with the same containerViewId and then add(int, Fragment, String) with the same arguments given here.

    • show (Fragment fragment)
      Shows a previously hidden fragment. This is only relevant for fragments whose views have been added to a container, as this will cause the view to be shown.

    每一次事务包含一或多个相关Fragment的修改,如add(), remove(), replace()。事务可以记录到activity关联的Fragment的回退栈中。之后用户按下返回键时,本次事务将“回滚”。

    // Create new fragment and transaction
    Fragment newFragment = new ExampleFragment();
    FragmentTransaction transaction = getFragmentManager().beginTransaction();
    
    // Replace whatever is in the fragment_container view with this fragment,
    // and add the transaction to the back stack
    transaction.replace(R.id.fragment_container, newFragment);
    transaction.addToBackStack(null);
    
    // Commit the transaction
    transaction.commit();
    

    上面的代码,使用newFragment替换掉了对应ViewGroup处的fragment,并将事务添加到了回退栈中,这使得以前的fragment不会被销毁,不执行onDestroyView()而仅仅执行了onStop()方法。之后若用户按下返回键,那么newFragment关闭后先前对应的fragment会继续恢复显示。

    在一次fragment事务中,最后必须调用commit()来提交事务,若添加多个fragment到一个布局容器中,那么添加顺序决定了它们对应View的展示顺序,类似ViewGroup.addView(View child)那样。

    Fragment切换动画

    为了让fragment切换时具有动画效果,可以调用setTransition()、setCustomAnimations()等来指定切换动画。

    commit()

    commit()的调用不会立即引起fragment变化操作的执行,而是将事务安排到UI线程中——UI线程准备好就会执行它。可以通过调用FragmentManager.executePendingTransactions()让提交的事务立即执行,如果其它异步任务有这样的需求的话。

    commitAllowingStateLoss().

    当用户离开Activity时(打开其它界面,返回桌面等),Activity的生命周期回调会执行状态保存操作,此时如果执行fragment事务的commit()将引发异常,原因是当Activity之后发生重建过程,事务提交后的状态可能丢失,如果这在设计上不是问题的话,可以选择执行commitAllowingStateLoss()。

    Fragment的生命周期

    Fragment的设计目标就是表现得像一个Activity的一部分,实现上它必须添加到Activity中运行。重要的一点,Fragment的View是插入到Activity的layout的一部分存在的。
    因为界面组件的属性,Fragment具备像Activity那样的生命周期回调方法,大多数方法本身就是Activity对应方法的一个调用传递,另一些方法是和Fragment的界面生成相关,或和宿主Activity进行交互的。

    有关Fragment的完整生命周期还是蛮复杂的,这个github项目是关于它的一个完整的演示。通常app只用到一些常见的生命周期方法,也是api文档中着重说明的,下面就学习下这些常见的回调。

    Lifecycle图解

    下图是Activity运行时期(resumed状态),Fragment从添加到移除过程中各个生命周期回调的执行状况:

    fragment_lifecycle

    作为对比,下图表明了宿主Activity生命周期对Fragment的影响:

    activity_fragment_lifecycle

    如图所示,Activity生命周期的变化会直接影响其包含的Fragment。假设以layout文件中标签的形式添加一个Fragment到Activity(或在onCreate()方法中添加fragment),那么在Activity从创建到resumed状态(可交互)的过程中,fragment会依次执行下面的一系列回调。

    在Activity的onCreate阶段

    • onAttach()
      原型:public void onAttach (Activity activity)
      在Fragment和它寄宿的Activity关联时调用,方法参数就是关联的Activity。此时Activity可能处在onCreate阶段或resumed阶段。

    • onCreate()
      原型:public void onCreate (Bundle savedInstanceState)
      创建Fragment时执行,在此作一些状态初始化操作。方法在onAttach(Activity) 之后在 onCreateView(LayoutInflater, ViewGroup, Bundle)之前执行。
      此时宿主Activity可能处于创建过程(onCreate执行期间,或者是出于resumed阶段),对于ViewHierarchy还未创建完成,所以不要做和View相关的操作,稍后的onActivityCreated()方法是Activity.onCreate()执行完毕后调用,可以在那里做一些Activity创建完成后的初始化操作。

    类似Activity那样,如果Fragment是从之前的状态恢复重建,则参数savedInstanceState携带了之前保存的状态数据。

    • onCreateView()
      原型:public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
      Activity获取Fragment提供的View时执行。

    • onViewCreated()
      原型:public void onViewCreated(View view, Bundle savedInstanceState)
      onCreateView()返回的view hierarchy创建完成,但还未被添加到Activity布局中的ViewGroup中。可以在此访问Fragment的各个View,进行设置。

    • onActivityCreated()
      原型:public void onActivityCreated (Bundle savedInstanceState)
      宿主Activity已完成创建,其onCreate()执行完毕。Fragment的View准备就绪,可以在此执行创建过程的最后初始化操作,如获得View对象,恢复状态等。

    经过上面几个方法的执行,宿主Activity及Fragment的创建过程已经完成。显然,这些创建过程的回调方法仅执行一次
    如果是在Activity创建时添加Fragment,那么上述方法会严格受到宿主Activity的onCreate()执行的影响。
    大多数情况下,对Fragment的使用正是通过标签,或在onCreate()中代码添加。前者setContentView()也还是在onCreate()中执行的,所以两种情况均是在Activity的onCreate阶段添加fragment。此时,Activity的onCreate阶段依次回调了上述Fragment的方法。

    onStart,onResume,onPause,onStop

    在用户离开和返回宿主Activity,或对Fragment执行replace,hide等操作时,它对应执行以下回调。

    • onStart()
      当fragment对用户可见时调用,前提是宿主Activity已处于started状态。同时也是宿主Activity.onStart的一个绑定调用。

    • onResume()
      fragment的界面可见且可以交互时调用,前提是宿主Activity已处于resumed状态。同时也是宿主Activity.onResume()的一个绑定调用。

    • onPause()
      Fragment不再处于resumed状态,一般是宿主Activity转为paused,或是正被通过fragment事务所修改。

    • onStop()
      fragment不再可见,由于宿主Activity转为stopped,或它正被执行fragment相关操作。

    在Activity的onDestroy阶段

    当Fragment被移除,或者宿主Activity销毁时,它将依次经历下面的回调。

    • onDestroyView()
      通知Fragment其关联的View将被移除宿主Activity,下次显示是重新执行onCreateView()创建View。如果fragment提供了View的话,可以在此完成一些有关View的清理操作,此时View的状态已保存还未从其parent中移除。

    • onDestroy()
      fragment不再被使用,执行最后的清理。

    • onDetach ()
      fragment不再和Activity关联。

    Fragment的状态

    类似Activity那样,一个Fragment实例处在运行中时,只能是以下状态之一。

    • Resumed
      The fragment is visible in the running activity.

    • Paused
      Another activity is in the foreground and has focus, but the activity in which this fragment lives is still visible (the foreground activity is partially transparent or doesn't cover the entire screen).

    • Stopped
      The fragment is not visible. Either the host activity has been stopped or the fragment has been removed from the activity but added to the back stack. A stopped fragment is still alive (all state and member information is retained by the system). However, it is no longer visible to the user and will be killed if the activity is killed.

    回调方法中需要注意的

    由于Fragment对象是一个具有生命周期的特殊对象,所以在它的代码中时刻注意一些操作的调用时机,下面列举一些。

    • getActivity()
      一般在Fragment中会调用getActivity()来获得关联的Avtivity当作Context来使用。而这个方法在Fragment还未attached或已经detached后 期间均返回null。

    • resumed状态下fragment的操作
      当Activity处于resumed 状态后,此时可以通过FragmentTransaction来执行各种fragment操作。此时fragment实例的生命周期回调是独立与宿主Activity的。比如点击一个按钮来为Activity添加Fragment,此时fragment实例直接依次执行onAttach、onCreate...onResume等一系列方法,而Activity则一直处于resumed状态。
      一旦Activity执行onPause离开resumed状态时,其包含的各个Fragment的相应生命周期回调继续被绑定执行。

    状态保持和回退栈

    作为一个“模块化的界面组件”,Fragment有类似Activity那样的状态保持和恢复机制:当一个未被显式结束的Activity处在后台时,由于内存问题它临时被回收掉,之后若用户再次回来时,对应任务栈中的Activity会被重新创建,而框架提供了和此Activity关联的一些状态保存和恢复的方法。

    在Fragment中,重写onSaveInstanceState()来保存一些状态。
    之后在 onCreate(), onCreateView(),或 onActivityCreated()中获取保存的状态,进行恢复设置。

    另一个Fragment的特性就是“回退栈”。
    在通过FragmentTransaction来执行有关fragment的事务时,可以通过addToBackStack()来添加此次事务的操作到回退栈中,这样以后,用户按下返回键后Activity的Fragment状态会恢复为上一次的情形,当回退栈中没有任何Fragment时,才执行Activity本身的onBackPressed()逻辑。

    更多关于回退栈的用法,参见FragmentManager的API。

    其它方法

    • isInLayout()和onInflate()
      若使用<fragment>标签添加一个Fragment实例,那么在Fragment的代码中可以通过方法isInLayout()来验证这一点,通常上述方式时方法会返回true。而且可以重写 onInflate (Activity activity, AttributeSet attrs, Bundle savedInstanceState)方法来获得布局文件中标签定义的一些attrs。
      例外的情况是,当Activity重建时采用了不同的layout,之前layout中的fragment还会被重新实例化,但此时此fragment对象的View已经不再使用了,此时它的onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)调用时参数container为null,此时方法无需返回任何View。而此时isInLayout()返回false。

    和Activity通信

    Fragment难免需要和宿主Activity进行交互,从设计原则上看,因为Fragment表示一个模块化的可复用界面组件,所以它应该可以被放置在不同的Activity中。出于这样的原因,它和其宿主Activity的交互只能是通过接口来进行抽象。

    • 首先在Fragment中定义一个接口来抽象需要交互的Activity。
    • 之后宿主Activity实现此接口,在onAttach()回调中(或者其它创建阶段的回调方法中调用getActivity)可以将得到的Activity实例保存到字段,作为接口实例。
    • 其它地方通过此接口实例来完成和Activity的交互。

    如果没有特殊的抽象需要,Activity本身是完全可以直接调用Fragment的公开方法的。可以获得FragmentManager,然后通过findFragmentById() 或 findFragmentByTag()得到目标Fragment。
    当然,在Fragment中也可以getActivity()得到Activity示例然后强转为已知的Activity类型去使用。

    Fragment也可以为宿主Activity提供Options Menu、Context Menu和Action Bar菜单,并处理这些动作响应。

    和其它Activity通信

    不同Fragment之间不能直接进行交互,因为它们应该可以进行各种不同的界面动态组合。最好的情况是,这些Fragment之间是相互不知道的,它们各自具备其独立的界面和行为。
    各个Fragment都定义自己的接口来和Activity交互,而它们之间的交互逻辑是Activity本身负责的。
    例如,ArticleListFragment表示的文章列表中一个文章被选中后,它通知Activity,然后Activity在根据此Article数据来操作ArticleFragment去显示对应文章内容。

    Fragment的参数

    虽然可以像普通对象那样,在创建Fragment后直接通过property来设置一些状态。但是,Fragment具有像Activity那样的,在intent中携带一些数据的能力。

    可以通过setArguments()来为Fragment设置一些额外数据,像下面这样:

    ArticleFragment f = new ArticleFragment();
    
    // Supply index input as an argument.
    Bundle args = new Bundle();
    args.putSerializable("article", article);
    f.setArguments(args);
    
    return f;
    

    之后,Fragment中使用下面的方式获得这些数据:

    Article article = (Article) getArguments().getSerializable ("article");
    

    通常,Activity在添加一个fragment时,可以直接把它的intent的Bundle数据设置给fragment,这在启动Activity的intent中携带的数据是给目标fragment时非常方便:

    fragment.setArguments(getIntent().getExtras());
    

    通过setArguments设置的数据,在fragment销毁后重建时也可以获取到。不像普通对象那样,只存在于内存中。
    这点类似于Activity的intent的数据,起到类似状态保持那样的效果——构建参数(construction arguments )的保持。

    列表-详情案例

    接下来就给出显示“文章列表,文章详情界面”示例的完整代码,列表和详情界面分别由两个Fragment表示,在大尺寸平板这样的屏幕时它们处在同一个Activity中。小尺寸手机设备上时,分别处在两个Activity中。界面效果如下:

    fragments_for_screens

    Article

    数据实体类Article的定义:

    public class Article implements Serializable {
        public String title;
        public String content;
    }
    

    一个文章包含标题和内容。

    列表:ArticleListFragment

    定义ArticleListFragment来显示文章列表,列表项是文章标题。
    使用RecyclerView来显示列表,列表项用TextView显示。

    ArticleListFragment对应布局文件fragment_article_list.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/list"
        android:name="com.example.android.ItemFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    

    ArticleListFragment定义:

    public class ArticleFragment extends Fragment {
        private OnArticleCheckedListener mListener;
        private ArrayList<Article> mArticles;
    
        public ArticleFragment() {
            mArticles = new ArrayList<>();
    
            for (int i = 0; i < 24; i++) {
                Article article = new Article();
                article.title = "标题-" + i;
                article.content = "文章内容-" + i;
    
                mArticles.add(article);
            }
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_article_list, container, false);
    
            if (view instanceof RecyclerView) {
                RecyclerView recyclerView = (RecyclerView) view;
                recyclerView.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
                recyclerView.setAdapter(new ArticleListViewAdapter(mArticles, mListener));
            }
            return view;
        }
    
        @Override
        public void onAttach(Context context) {
            super.onAttach(context);
            if (context instanceof OnArticleCheckedListener) {
                mListener = (OnArticleCheckedListener) context;
            }
        }
    
        @Override
        public void onDetach() {
            super.onDetach();
            mListener = null;
        }
    
        public interface OnArticleCheckedListener {
            void onArticleChecked(Article article);
        }
    }
    

    接口OnArticleCheckedListener定义了某个文章标题被点击查看时的回调。
    onAttach()得到的context正是宿主Activity,按照设计,宿主Activity应该实现了OnArticleCheckedListener,这样ArticleListFragment列表项的点击事件就传递给了宿主Activity。

    适配器ArticleListViewAdapter的定义:

    public class ArticleListViewAdapter extends RecyclerView.Adapter<ArticleListViewAdapter.ViewHolder> {
    
        private final List<Article> mArticles;
        private final ArticleFragment.OnArticleCheckedListener mListener;
    
        public ArticleListViewAdapter(List<Article> items, ArticleFragment.OnArticleCheckedListener listener) {
            mArticles = items;
            mListener = listener;
        }
    
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.fragment_article_title, parent, false);
            return new ViewHolder(view);
        }
    
        @Override
        public void onBindViewHolder(final ViewHolder holder, int position) {
            holder.mArticle = mArticles.get(position);
            holder.mContentView.setText(mArticles.get(position).content);
    
            holder.mView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (null != mListener) {
                        mListener.onArticleChecked(holder.mArticle);
                    }
                }
            });
        }
    
        @Override
        public int getItemCount() {
            return mArticles.size();
        }
    
        public class ViewHolder extends RecyclerView.ViewHolder {
            public final View mView;
            public final TextView mContentView;
            public Article mArticle;
    
            public ViewHolder(View view) {
                super(view);
                mView = view;
                mContentView = (TextView) view.findViewById(R.id.content);
            }
        }
    }
    

    布局文件fragment_article_title.xml简单的定义了一个TextView用来显示标题:

    <?xml version="1.0" encoding="utf-8"?>
    <TextView   xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/content"
        android:textSize="32sp"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="40dp"/>
    

    以上代码完成了ArticleListFragment的布局和类型定义。

    ArticleDetailFragment

    ArticleDetailFragment用来显示具体文章的内容,它的布局文件fragment_article_detail.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/article_content"
        android:name="com.example.android.ItemFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    

    布局中使用一个TextView简单都显示文章内容。

    ArticleDetailFragment类型定义:

    public class ArticleDetailFragment extends Fragment {
        public static final String ARG_ARTICLE = "ARG_ARTICLE";
        private Article mArticle;
    
        public ArticleDetailFragment() {
        }
    
        public static ArticleDetailFragment newInstance(Article article) {
            ArticleDetailFragment fragment = new ArticleDetailFragment();
            Bundle bundle = new Bundle();
            bundle.putSerializable(ARG_ARTICLE, article);
            fragment.setArguments(bundle);
    
            return fragment;
        }
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            mArticle = (Article) getArguments().getSerializable(ARG_ARTICLE);
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_article_detail, container, false);
    
            if (mArticle != null) {
                TextView textView = (TextView) view.findViewById(R.id.article_content);
                textView.setText(mArticle.content);
            }
    
            return view;
        }
    }
    

    方法newInstance()方便创建一个携带参数的实例。

    ArticleListActivity

    对应宿主ArticleListActivity,它在大尺寸屏幕下可以同时显示列表和文章详情,在小尺寸屏幕下只显示标题列表。所以在res/layout-large/和
    res/layout/两个目录下分别为它定义不同的布局文件。

    • res/layout/activity_articles.xml

    <fragment
        android:id="@+id/list_fragment"
        android:name="com.example.android.ArticleListFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    

    在小尺寸屏幕时,Activity只显示ArticleListFragment,上面利用标签的方式添加Fragment实例。

    • res/layout-large/activity_articles.xml
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/layout_root"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        tools:context="com.example.android.ArticleListActivity">
    
        <fragment
            android:id="@+id/list_fragment"
            android:layout_weight="2"
            android:name="com.example.android.ArticleListFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    
        <FrameLayout
            android:id="@+id/detail_container"
            android:layout_weight="2"
            android:layout_width="match_parent"
            android:layout_height="match_parent"></FrameLayout>
    
    </LinearLayout>
    

    在大尺寸屏幕下,布局中同时提供了R.id.detail_container作为ArticleDetailFragment的容器,这样在选择查看某个标题时,
    就会在同一个界面使用ArticleDetailFragment来显示文章内容。

    下面是ArticleListActivity的定义,它的onCreate()中根据布局文件是否包含R.id.detail_container来判断当前屏幕是处于大屏幕——显示两个内容面板(towPanel)还是小屏幕(singlePanel)。

    public class ArticleListActivity extends Activity implements ArticleListFragment.OnArticleCheckedListener {
        private boolean mIsTwoPanel = false;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_article_list);
    
            View container = findViewById(R.id.detail_container);
            mIsTwoPanel = container != null;
        }
    
        @Override
        public void onArticleChecked(Article article) {
            if (mIsTwoPanel) {
                Fragment fragment = ArticleDetailFragment.newInstance(article);
                getFragmentManager().beginTransaction().replace(R.id.detail_container, fragment).commit();
            } else {
                Intent intent = new Intent(this, ArticleDetailActivity.class);
                intent.putExtra(ArticleDetailFragment.ARG_ARTICLE, article);
                startActivity(intent);
            }
        }
    }
    

    ArticleListActivity实现了OnArticleCheckedListener接口,在方法onArticleChecked()中,若当前Activity是大屏幕对应的布局,
    则直接替换R.id.detail_container处的fragment用来显示文章内容。否则,启动一个新的ArticleDetailActivity来显示文章内容。
    ArticleDetailActivity的定义非常简单,它作为ArticleDetailFragment的宿主,将article参数设置给它。

    (本文使用Atom编写)

  • 相关阅读:
    2020/10/29
    2020/10/24
    2020/10/28
    2020/10/31周报
    linux shell 中判断字符串为空的正确方法
    20201107 千锤百炼软工人
    20201103 千锤百炼软工人
    20201109 千锤百炼软工人
    20201111 千锤百炼软工人
    20201105 千锤百炼软工人
  • 原文地址:https://www.cnblogs.com/everhad/p/6209630.html
Copyright © 2020-2023  润新知