Chapter4:与Fragment交互

做~自己de王妃 提交于 2020-01-16 20:51:42

Chapter4:与Fragment交互

4.1 屏幕界面管理

  • 到现在为止,我们认为在我们的app中每个Activity都关联一个屏幕界面(screen),而Fragment则是关联一个屏幕界面中的一部分。举例而言,我们之前创建的书目浏览的例子中,当屏幕界面为横向时(wide-display),我们的app显示的是一个包含两个Fragment的Activity,一个Fragment显示书目标题,另一个Fragment显示被选中书目的内容描述。我们通过一个Activity管理和展示两个Fragment,使它们同时显示在一个屏幕界面上。当手机朝向切换为竖直时,书目标题和被选中书目的内容描述被分到两个屏幕界面显示,这时我们通过两个Activity分别管理这两个不同时显示在同一屏幕界面的两个Fragment。
  • 横屏竖屏两种情况下我们的应用做的是一样的任务,不同的是,在同一时间我们要在屏幕界面上显示多少的信息。因为这点不同,我们额外创建了一个新的Activity。因为启动一个新的Activity要比更新一个Activity中的Fragment复杂的多,所以新增的Activity增加了我们应用的复杂性。而且两个Activity在显示被选中书目的内容描述的功能上存在代码的冗余
  • 我们在Chapter 1中提过,使用Fragment会帮助我们减少不必要的不必要的复杂、Activity的增加、逻辑的重复。现在,我们正在体验这种Fragment带来的方便。
  • 我们需要进一步思考对UI的设计理念,我们希望的是应用中的Activity不仅仅实现的是在物理设备上呈现信息,我们还希望能够有意识的管理我们应用中的屏幕界面与相关Activity的关系。
  • 作为用户,切换到另一个屏幕界面的体验就是他们看见新的视图界面替换当前的视图界面。以前的做法,我们设计的每个Activity有一个相对固定的布局,切换到另一个屏幕界面就是展示一个新的Activity。但是Fragment给了我们另一种选择。
  • 我们不单单是可以利用Fragment来管理一个屏幕界面的子部分,我们还可以使用它们来管理整个屏幕界面的逻辑分组。我们可以动态管理在一个Activity中的Fragments,将一个Fragment更改为另一个Fragment。这在为用户呈现从一个屏幕界面切换到另一个屏幕界面的体验同时,也为我们提供了在单个Activity中管理通用用户UI界面元素的便捷。

4.2 动态管理Fragment

  • 动态管理Fragment的过程通常涉及多个步骤。这些步骤可能简单,像删除一个Fragment并添加另一个Fragment;也可能复杂,像删除和添加多个Fragments。我们需要保证无论在那种情况下,在单一的Activity中对Fragments的动态更改步骤组合起来后像一个任务,这个任务使得应用的屏幕界面从当前切换到下一个。Android通过FragmentTransaction类将这些对Fragments动态更改的步骤分组成事务,从而实现上述任务。

  • 从概念上看,FragmentTransaction类的行为方式与其他事务处理模型一致:

    • 1.开始事务。
    • 2.确认需要的变更。
    • 3.当该次任务所需的全部变更确认后一次性提交事务。
  • 当我们要做出变更时,首先通过调用Activity的FragmentManagerbeginTransaction方法来创建一个FragmentTransaction实例,beginTransaction方法会返回该FragmentTransaction实例的引用。之后我们通过这个新建的FragmentTransaction实例来确认需要做的变更。当我们处理事务时,这些变更被加入队列,但没有被执行。最后,当我们确认完所有需要的变更后,调用FragmentTransaction类的commit方法执行事务。

  • 当事务中所有的变更被执行后,我们的应用对这些变更做出回应,在用户看来就是从一个屏幕界面切换到下一个屏幕界面。尽管我们的应用执行了许多步骤来更改当前Activity中显示的Fragments,但是从用户的角度来看就像是展示了一个新的Activity。

4.2.1 事务变更的延迟执行

  • 调用commit方法并不会立即执行变更。
  • 当我们使用FragmentTransaction类添加事务时,并未与程序的UI界面有任何直接的交互,我们只是创建了一个记录了未来要做的任务的事务列表。当我们将所有需要的变更都添加到事务列表中后,调用commit方法,将事务列表打包后发送给UI主线程的消息队列。之后,UI主线程会遍历这个事务列表,代表FragmentTransaction实例执行实际的UI界面交互工作。
  • 因为调用FragmentTransaction实例的事务变更操作不会直接与UI界面交互,所以程序可以在非UI线程上安全地进行这些操作。复杂的程序可以利用这一特点,必要时在后台执行这些操作,从而实现更好响应性。
  • 在多数情况下,FragmentTransaction实例中延迟执行事务变更的方式可以带来好处。但是,如果我们的程序在刚刚执行完commit方法后立即查找某个Fragment,或者与某个新加Fragment的视图交互,那么可能会产生问题。虽然问题不是必现的,但有时确实会出现。
  • 如果我们有上述的需求,我们可以强制FragmentTransaction类立即执行事务变更,这一操作通过在调用FragmentTransaction类commit方法后调用FragmentManager类executePendingTransactions方法实现。当executePendingTransactions方法执行完返回时,所有提交的FragmentTransaction事务都已经完成了。
  • 我们只能在UI线程调用executePendingTransactions方法,因为这个方法在执行时会挂起的用户UI界面。

4.2.2 添加和删除Fragment

  • 在一个Activity中通过FragmentTransaction类操作Fragments的方法有很多,其中最基本的就是add方法remove方法

  • add方法允许我们在指定的View Group中加入新创建的Fragment,比如:

    // 创建事务
    FragmentManager fm = getFragmentManager();
    FragmentTransaction ft = fm.beginTransaction();
    // 创建Fragment并添加到事务列表中
    BookListFragment2 listFragment = new BookListFragment2();
    ft.add(R.id.layoutRoot, listFragment, "bookList");
    // 提交事务变更请求
    ft.commit();
    
  • 我们在添加Fragment时第三个参数输入了字符串"bookList",这是一个简单的标识。之后我们可以使用这个标识去定位刚才添加的Fragment。注意:在动态添加Fragment时,我们不能用id值去定位Fragment,因为id与动态添加的Fragment之间没有关联。

  • 我们可以用上述的标识来定位Fragment从而删除该Fragment:

    FragmentManager fm = getFragmentManager();
    Fragment listFragment = fm.findFragmentByTag("bookList");
    BookDescFragment bookDescFragment = new BookDescFragment();
    FragmentTransaction ft = fm.beginTransaction();
    ft.remove(listFragment);
    ft.add(R.id.layoutRoot, bookDescFragment, "bookDescription");
    ft.commit();
    
  • 当我们需要频繁的删除一个Fragment并替换为另一个Fragment时,我们可以使用FragmentTransaction类的replace方法。该方法会将ViewGrop中的旧Fragment替换为新Fragment,并为其设置标识。

    FragmentManager fm = getFragmentManager();
    bookDescFragment = new BookDescFragment();
    FragmentTransaction ft = fm.beginTransaction();
    ft.replace(R.id.layoutRoot, bookDescFragment, "bookDescription");
    ft.commit();
    

4.2.3 返回键的支持

  • 当我们使用这种管理Fragment的方式来管理应用的屏幕界面时,我们需要保持应用和用户期望的一致性。应用返回键响应这里需要额外的关注。

  • 当一个用户使用手机时,很自然的会浏览多个屏幕界面,然后用户在任意时刻点击返回键都会回到前一个浏览的屏幕界面。这是因为应用每启动一个新Activity展示时,Android系统会自动将这个Activity加入到Android返回栈中,当用户点击返回键时,上一个Activity就会出栈显示出来。

  • 这种行为建立与一个Activity对应一个屏幕界面的基础假设上,但当我们使用动态Fragment来管理屏幕时,这种基础假设便不存在了。使用动态Fragment可能会使我们在点击返回键时,一次性回到数个界面之前,中间的屏幕界面改变过程则被忽略了。

    动态管理Fragment下的回退键

  • 为了解决这个问题,我们需要在通过FragmentTransaction实例展示第二个Fragment时调用FragmentTransaction类的addToBackStack方法。addToBackStack方法将事务更改添加到返回栈的栈顶。这使得用户在通过FragmentTransaction实例管理屏幕界面的场景下使用返回键的响应,与在只使用Activity场景下使用返回键的响应一致。

  • 我们可以在调用事务commit方法前的任意时间点调用addToBackStack方法。addToBackStack方法可以接受一个字符串作为参数,该参数可用于定位返回栈中的位置。当你希望程序化操作返回键时配置这个参数将会十分有用,但是大多数情况下我们只需要传null

  • 注意:当Activity继承自AppCompatActivity时,需要通过getSupportFragmentManager方法来创建支持addToBackStack的事务。如果调用FragmentManager方法获得的事务的addToBackStack方法会无效(默默地失败)。

4.3 创建一个自适应的应用布局

  • 我们将之前的点击书目标题查看书目内容描述的例子重构为只是用单一Activity实现的程序。

4.3.1 更新布局资源文件使其支持动态Fragments

  • 首先修改屏幕竖直情况下的布局资源文件activity_main.xml。(后面不带land或w600dp)

  • 我们需要做的修改有两点:1.给LinearLayout这个View Group添加id属性以便定位;2.移除Fragment元素。修改后:

    <LinearLayout
                  android:id="@+id/layoutRoot"
                  android:orientation="vertical"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  xmlns:android="http://schemas.android.com/apk/res/android">
    </LinearLayout>
    
  • 只保留View Group而不保留内部的View是因为:只有动态添加的Fragment才能动态删除

4.3.2 适配差异的设备

  • 当我们的程序在竖屏的情况下运行时,Activity需要载入包含书目标题列表的Fragment,也即载入BookListFragment2。但是在我们载入包含书目标题列表的Fragment前,首先需要确认的是我们运行的设备是否需要动态管理Fragment。当设备是宽屏时(wide-display),我们使用的是静态管理Fragment。

  • 在我们的代码中会有一些地方根据使用的布局执行不同的逻辑,我们需要通过一个boolean值来判断是使用动态管理Fragment还是静态管理Fragment。

    boolean mIsDynamic;
    
  • 我们可以查询到设备特性,比如屏幕宽度和屏幕方向。但是要记得,Android系统在自动载入合适布局时候就已经利用了这些设备特性。我们不需要要再去查询这些设备特性,只需要简单地判断被载入的资源的信息即可。比如例子中,当设备处于横屏状态,会静态载入书目标题Fragment和书目内容描述Fragment。我们可以在onCreate周期回调中,根据是否包含上述的Fragment资源来反推当前设备的特性。

    public class MainActivity extends Activity implements BookListFragment.OnSelectedBookChangeListener {
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main_dynamic);
            // 获取书目描述Fragment
            FragmentManager fm = getFragmentManager();
            Fragment bookDescFragment = fm.findFragmentById(R.id.fragmentDescription);
            // 如果没有找到书目描述Fragment,则应为竖屏状态,使用动态管理Fragment
            mIsDynamic = bookDescFragment == null || !bookDescFragment.isInLayout();
        }
    	...
    }
    
  • 当我们调用完setContentView方法返回时,我们就可以认为设备已经载入完合适的布局资源文件了。之后我们可以使用FragmentManager实例通过id(R.id.fragmentDescription)去确认设备是竖直和水平方向(在竖直方向不存在fragmentDescription,会返回null值)。除此之外,我们还要通过isInLayout方法再次确认一下fragmentDescription是否存在,避免特殊情况

  • 上述的特殊情况是指当设备从水平方向转换为竖直方向时,fragmentDescription可能会留下一个缓存。通过调用isInLayout方法,我们可以确认在当前载入的视图资源中是否包含fragmentDescription。

4.3.3 动态载入Fragment

  • 代码如下:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_dynamic);
        // 获取书目内容描述fragment
        FragmentManager fm = getFragmentManager();
        Fragment bookDescFragment = fm.findFragmentById(R.id.fragmentDescription);
    	// 如果没有找到书目描述Fragment,则应为竖屏状态,使用动态管理Fragment
        mIsDynamic = bookDescFragment == null || !bookDescFragment.isInLayout();
        // 如果为动态管理Fragment
        if (mIsDynamic) {
            // 开始事务
            FragmentTransaction ft = fm.beginTransaction();
            // 添加Fragment
            BookListFragment2 listFragment = new BookListFragment2();
            ft.add(R.id.layoutRoot, listFragment, "bookList");
            // 提交事务
            ft.commit();
        }
    }
    

4.3.4 消除冗余的处理

  • 在原先的设计中,在屏幕竖直的情况下,我们点击书目标题,会启动一个新的Activity显示对应书目的内容描述,在Activity间通过Intent传递信息会使程序变得复杂,并且增大程序的开销。现在我们修改onSelectedBookChanged方法

    public void onSelectedBookChanged(int bookIndex) {
        BookDescFragment bookDescFragment;
        FragmentManager fm = getFragmentManager();
        // 检查当前是动态管理Fragment(竖屏)还是静态管理Fragment横屏)
        if(mIsDynamic) {
            // 动态管理Fragment实现切换屏幕界面
            FragmentTransaction ft = fm.beginTransaction();
            bookDescFragment = new BookDescFragment();
            ft.replace(R.id.layoutRoot, bookDescFragment, "bookDescription");
            ft.addToBackStack(null);
            ft.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
            ft.commit();
            ...
        }else {
            // 横屏状态下保持原先逻辑
            bookDescFragment = (BookDescFragment) fm.findFragmentById(R.id.fragmentDescription);
            bookDescFragment.setBook(bookIndex);
        }
    }
    
    • 调用addToBackStack方法可以允许我们在书目描述的屏幕界面点击返回键后回到书目标题列表的屏幕界面。
    • FragmentTransaction类的setCustomAnimations方法,允许我们设置屏幕界面转场的渐变效果。

4.3.5 管理异步的创建过程

  • 回顾我们设置书目的setBook方法:

    public void setBook(int bookIndex) {
        // 查找对应index的书目内容描述
        String bookDescription = mBookDescriptions[bookIndex];
        // 显示内容
        mBookDescriptionTextView.setText(bookDescription);
    }
    
  • 在动态管理Fragment是,对mBookDescriptionTextView进行操作可能会产生问题。这是因为FragmentTransaction类对用户UI界面的交互是延迟的,此时BookDescFragment实例并未创建完成,调用它的setText方法会造成引用空指针异常

  • 一种解决方式是修改setBook方法,使它能够检查当前Fragment的状态,如果BookDescFragment未完成创建,则保存书目的索引值,然后等到创建完成后再自动去设置BookDescriptionTextView。可能在一些情况下需要这种复杂的逻辑,但是Fragment提供了我们一种更简便的方法。

  • Fragment基类包含一个setArguments方法。setArguments运行我们向其传递数据,然后在Fragment的声明周期中通过getArguments方法获取到该数据。

  • 在目标类中定义所需参数的别名常量,和所需参数的默认值是一个良好的编程习惯:

    public class BookDescFragment extends Fragment {
        // 书目索引的别名
        public static final String BOOK_INDEX = "book index";
        // 书目索引默认值
        private static final int BOOK_INDEX_NOT_SET = -1;
        // 省略其他
    }
    
  • 我们可以用BOOK_INDEX常量来作为获取书目的索引值的关键词,用BOOK_INDEX_NOT_SET常量来判断书目的索引值是否被设置。为了简化创建BookDescFragment实例和传递书目索引值的过程,我们为BookDescFragment类添加一个静态的工厂方法:

    public static BookDescFragment newInstance(int bookIndex) {
        BookDescFragment fragment = new BookDescFragment();
        Bundle args = new Bundle();
        args.putInt(BOOK_INDEX, bookIndex);
        fragment.setArguments(args);
        return fragment;
    }
    
  • setArguments使用Bundle进行RPC。

  • 现在我们可以在BookDescFragment类的onCreateView方法中

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View viewHierarchy = inflater.inflate(R.layout.fragment_book_desc, container, false);
        //加载书目内容描述的数组
        mBookDescriptions = getResources().getStringArray(R.array.bookDescriptions);
        // 绑定TextView控件
        mBookDescriptionTextView = (TextView)viewHierarchy.findViewById(R.id.bookDescription);
        // 获取书目的索引(如果存在)
        Bundle args = getArguments();
        int bookIndex = args != null ? args.getInt(BOOK_INDEX, BOOK_INDEX_NOT_SET) : BOOK_INDEX_NOT_SET;
        // 如果获取到书目的索引
        if (bookIndex != BOOK_INDEX_NOT_SET){
            setBook(bookIndex);
        }
        return viewHierarchy;
    }
    
    • args.getInt方法第二个值为默认值,当不能根据BOOK_INDEX关键字检索到结果时,返回该默认值。

4.3.6 合并更改

  • 修改后的onSelectedBookChanged方法:

    public void onSelectedBookChanged(int bookIndex) {
        BookDescFragment bookDescFragment;
        FragmentManager fm = getFragmentManager();
        if(mIsDynamic) {
            FragmentTransaction ft = fm.beginTransaction();
            bookDescFragment = BookDescFragment.newInstance(bookIndex);
            ft.replace(R.id.layoutRoot, bookDescFragment, "bookDescription");
            ft.addToBackStack(null);
            ft.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
            ft.commit();
        }else {
            bookDescFragment = (BookDescFragment) fm.findFragmentById(R.id.fragmentDescription);
            bookDescFragment.setBook(bookIndex);
        }
    }
    

4.4 参考资料

  • CreatingDynamicUIwithAndroidFragments,2ndEdition
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!