Chapter2:使用Fragment灵活地构建UI

Chapter2:使用Fragment灵活地构建UI

文章目录

  • Chapter2:使用Fragment灵活地构建UI
    • 2.1 选择动态的碎片式布局
      • 2.1.1添加可替换布局资源
      • 2.1.2 根据屏幕大小管理Fragment布局
      • 2.1.3 使用布局别名来消除冗余的布局
    • 2.2 设计灵活的Fragment
      • 2.2.1 拒绝高耦合
      • 2.2.2 抽象Fragments之间的关系
    • 2.3 预防使用Fragment时产生的意料之外的情况
    • 2.4 参考资料

2.1 选择动态的碎片式布局

  • 为了解决设备间的差异化,我们可以根据运行的具体设备灵活的重排Fragments。
  • UI界面和程序代码之间的依赖关系越深,维护和更新应用程序就会越困难,尽管这种依赖关系无可避免,但我们希望最小化这种依赖关系,所以尽可能在布局资源中做更多与用户界面相关的工作。
  • Android资源系统有设备适应性,允许我们为应用程序设计与UI相关的不同资源,每种资源用于与一组具有特定特征的设备进行关联和优化。利用该特性结合碎片化设计,我们可以轻松地根据屏幕朝向、屏幕大小动态排布Fragments,减少了重复的代码,使得应用程序的维护变得简单。
  • Chapter1中的示例在手机横屏时就会显得不是很友好,我们更希望在横屏的时候是左右分布两个碎片而非上下分布。

2.1.1添加可替换布局资源

  • 添加新的资源文件:

Chapter2:使用Fragment灵活地构建UI_第1张图片
Chapter2:使用Fragment灵活地构建UI_第2张图片

  • 复制之前的activity_main.xml内容,并将布局修改左右布局。(activity_main.xml(land):)

    
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
    
        
        <fragment
            android:id="@+id/fragmentTitles"
            android:name="com.virtual.learn101022.BookListFragment"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />
        
        <fragment
            android:id="@+id/fragmentDescription"
            android:name="com.virtual.learn101022.BookDescFragment"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />
    LinearLayout>
    
  • 在运行期间,当MainActivity装入R.layout时。Android资源系统会返回相应的activity_main.xml资源文件。当设备旋转到不同方向时,Android会自动重新创建活动,并为新方向加载适当的资源。

Chapter2:使用Fragment灵活地构建UI_第3张图片

2.1.2 根据屏幕大小管理Fragment布局

  • 我们不止可以通过设备方向差异,还可以通过屏幕大小差异来调整UI界面。这里我们使用屏幕大小限定符将资源与特定的屏幕大小特征关联。为了避免不同种屏幕像素密度和物理屏幕大小的复杂,Android在管理屏幕大小时使用一个标准度量单位--------dp(density-independent pixel,密度无关像素)。dp单位是与设备的物理像素大小无关的单位,始终对应于160dpi设备上的像素的物理大小。Android平台会维dp与物理像素之间的映射。

  • Android提供三种屏幕尺寸限定符:

    • 1.最小屏幕宽度大小限定符:对应屏幕最窄地方的dp,与设备朝向无关。改变设备方向并不会改变设备最小宽度。选项名称:Smallest screen width
    • 2.可用屏幕宽度大小限定符:对应当前屏幕朝向从左到右的dp。改变设备方向会改变可用屏幕宽度。选项名称:Screen width
    • 3.可用屏幕高度大小限定符:对应当前屏幕朝向从下到上的dp。改变设备方向会改变可用屏幕高度。选项名称:Screen height

2.1.3 使用布局别名来消除冗余的布局

  • 随着应用程序的功能的增长,使用不同的布局资源限定符来管理资源文件会变得越来越复杂,因此我们希望为不同的限定符使用相同的布局资源文件。举例来说,在之前例子的基础上,我们希望在宽度为600dp或更大的设备上也是左右分布两个碎片,一种方式是使用可用屏幕宽度大小限定符再创建一个关联600dp的新的资源文件activity_main.xml(w600dp),然后将activity_main.xml(land)的内容复制进来,但是当需要维护的时候,我们需要在这两个文件上做相同的维护。我们可以使用布局别名(layout aliasing),来减少布局资源的重复。

  • 布局别名:我们可以告诉资源系统我们需要的资源的细节在哪些文件中。

    • 首先创建activity_main_wide.xml资源文件。(不要使用任何限定符)

    • 然后,我们将activity_main.xml(land)中的内容复制到activity_main_wide.xml中。

    • 然后,我们清空activity_main.xml(land)的内容,加入:

      <merge>
          <include layout="@layout/activity_main_wide"/>
      merge>
      
    • 然后,我们在用可用屏幕宽度大小限定符再创建一个关联600dp的新的资源文件activity_main.xml(w600dp),内容同上个代码块。

      Chapter2:使用Fragment灵活地构建UI_第4张图片

  • 现在,当需要维护代码时我们只需要修改activity_main_wide.xml资源文件就可以了。

2.2 设计灵活的Fragment

  • 通常,UI界面被划分为多个Fragment时,这些Fragment很少完全独立存在,当用户与一个Fragment的交互时,往往会对同一Activity中的其他片段产生影响。比如我们的例子中,当用户在BookListFragment中选择一本书时,为了响应用户的选择,应用程序会在BookDescFragmen中显示相应的描述。我们需要设计灵活的Fragment来有效地协调Fragments间的行为。

2.2.1 拒绝高耦合

  • 协调Fragments行为的一种方式就是允许它们之间进行通信。在我们的例子中,我们可以让BookDescFragmen和BookListFragment持有彼此的引用,但这样带来的就是高度的耦合,过高的耦合会给程序带来诸多问题。比如BookListFragment中的点击事件会改变BookDescFragmen中的显示内容,在UI界面只有BookListFragment情况下,也会对BookDescFragmen操作;改动BookDescFragmen也可能会影响BookListFragment。因此这种解决方式是不可取的。

2.2.2 抽象Fragments之间的关系

  • 为了避免创建Fragments间的直接联系,可以通过接口实现抽象。在我们的例子中,我们可以通过定义一个简单的回调接口来表示用户选择图书的行为,从而消除Fragments间的高度耦合。

  • 定义回调接口:接口应该是面向应用级(比如:选择一本书)而非实现级别的(比如:点击选项按钮);实现级别的操作应该隔离在碎片中。抽象接口时候不要去想实现的细节

    • 在我们的例子中,BookListFragment唯一感兴趣的操作就是我们选择了那本书,因此,我们只需要定义一个方法onSelectedBookChanged。我们需要接收一个标识符,来明确我们选择的是那一本书,参数的定义不要过于局限,比如:定义为书名(String)就很局限,定义为数组索引(int)就很灵活。(实际生活中,本例子中的标识符更可能是在数据存储或服务中定位图书信息的键。)

      public interface OnSelectedBookChangeListener{
      	void onSelectedBookChanged(int bookIndex);
      }
      
  • 接下来我们完善我们的例子:

    • 首先,我们要让BookListFragment实现对RadioGroup中点击事件的监听。然后定义一个确定与所选按钮对应的图书索引的方法(取名translateIdToIndex)。接着通过getActivity方法获取当前Activity,并将其转型成OnSelectedBookChangeListener接口类型,然后在点击事件的监听中将监听到的事件转换成图书索引后通过onSelectedBookChanged方法传入。

    • BookListFragment.java:

      public class BookListFragment extends Fragment {
      
          @Nullable
          @Override
          public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
              View viewHierarchy = inflater.inflate(R.layout.fragment_book_list, container, false);
              RadioGroup group = (RadioGroup) viewHierarchy.findViewById(R.id.bookSelectGroup);
              //实现对RadioGroup中点击事件的监听
              group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
                  @Override
                  public void onCheckedChanged(RadioGroup group, int checkedId) {
                      // 将监听到的事件转换成图书索引
                      int bookIndex = translateIdToIndex(checkedId);
                      // 获取当前Activity,并将其转型成OnSelectedBookChangeListener接口类型
                      OnSelectedBookChangeListener listener = (OnSelectedBookChangeListener) getActivity();
                      // 调用onSelectedBookChanged方法发送通告
                      listener.onSelectedBookChanged(bookIndex);
                  }
              });
      
              return viewHierarchy;
          }
      
          //定义的确定与所选按钮对应的图书索引的方法
          private int translateIdToIndex(int id) {
              int index = -1;
              switch (id) {
                  case R.id.rb_book1:
                      index = 0;
                      break;
                  case R.id.rb_book2:
                      index = 1;
                      break;
                  case R.id.rb_book3:
                      index = 2;
                      break;
                  default:
                      break;
              }
              return index;
          }
      }
      
    • 建立一个存储书籍内容的资源文件:(arrays.xml)

      <resources>
          <array name="book_descriptions">
              <item>欢迎阅读第一本书!item>
              <item>欢迎阅读第二本书!item>
              <item>欢迎阅读第三本书!item>
              <item>欢迎阅读第四本书!item>
              <item>欢迎阅读第五本书!item>
          array>
      resources>
      
    • 然后,我们修改BookDescFragment,添加一个setBook方法,该方法将根据bookIndex将对应内容显示到TextView上。

    • BookDescFragment.java:

      public class BookDescFragment extends Fragment {
          String[] mBookDescriptions;
          TextView mBookDescriptionTextView;
      
          @Nullable
          @Override
          public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
              View viewHierarchy = inflater.inflate(R.layout.fragment_book_desc, container, false);
              // 从资源文件中载入书籍储存内容的数组
              mBookDescriptions = getResources().getStringArray(R.array.book_descriptions);
              // 获取显示书籍内容的TextView
              mBookDescriptionTextView = viewHierarchy.findViewById(R.id.tv_description);
              return viewHierarchy;
          }
      
          public void setBook(int bookIndex) {
              // 根据bookIndex查找书籍内容
              String bookDescription = mBookDescriptions[bookIndex];
              // 将书籍内容显示到TextView上
              mBookDescriptionTextView.setText(bookDescription);
          }
      }
      
    • 最后,我们让MainActivity实现OnSelectedBookChangeListener接口。

      public class MainActivity extends AppCompatActivity implements OnSelectedBookChangeListener {
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              // 加载布局资源
              setContentView(R.layout.activity_main);
          }
          
          @Override
          public void onSelectedBookChanged(int bookIndex) {
              // 获取FragmentManager
              FragmentManager fragmentManager = getSupportFragmentManager();
              // 通过获取FragmentManager寻找当前Activity下的BookDescFragment
              BookDescFragment bookDescFragment = (BookDescFragment) fragmentManager.findFragmentById(R.id.fragmentDescription);
              // 如果当前Activity存在BookDescFragment,那么调用setBook方法设置书籍内容
              if (bookDescFragment != null) {
                  bookDescFragment.setBook(bookIndex);
              }
          }
      }
      
    • 于是,我们完成了BookListFragment和BookDescFragment之间的解耦。

2.3 预防使用Fragment时产生的意料之外的情况

  • 现在,我的的例子需要变更:当手机是竖屏的时候只是显示书籍选择的UI,当用户选择时,弹出一个新的Activity显示书籍内容;当手机是横屏时,在左侧选择书籍,右侧显示书籍内容。

  • 首先,我们创建新的Activity(BookDescActivity.java),并新建activity_book_desc.xml和修改activity_main.xml(注意这个是不带land和w600dp标识的activity_main.xml)。其中BookDescActivity是通过Intent来获取从MainActivity跳转过来时携带的bookIndex键值对。

  • Tip:别忘了在Manifest中注册BookDescActivity

    <activity android:name=".BookDescActivity"/>
    
    //BookDescActivity.java
    public class BookDescActivity extends AppCompatActivity {
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_book_desc);
            // 通过Intent获取书籍索引
            Intent intent = getIntent();
            int bookIndex = intent.getIntExtra("bookIndex", -1);
            if (bookIndex != -1) {
                // 通过FragmentManager获取当前Activity下的BookDescFragment
                FragmentManager fm = getSupportFragmentManager();
                BookDescFragment bookDescFragment = (BookDescFragment) fm.findFragmentById(R.id.fragmentDescription);
                // 根据书籍索引显示书籍内容
                bookDescFragment.setBook(bookIndex);
            }
        }
    }
    
     
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        
        <fragment
            android:id="@+id/fragmentDescription"
            android:name="com.virtual.learn101022.BookDescFragment"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1" />
    LinearLayout>
    
     
    
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        
        <fragment
            android:id="@+id/fragmentTitles"
            android:name="com.virtual.learn101022.BookListFragment"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1" />
    LinearLayout>
    
  • 之后,我们修改MainActivity.java。当选择按钮改变时,判断当前Activity是否含有BookDescFragment(竖屏时不存在,横屏时存在),如果存在,就直接在BookDescFragment显示书籍内容;如果不存在,则打开新的Activity(BookDescActivity)显示书籍内容(通过Intent带值跳转,携带bookIndex键值对)。

    //MainActivity.java
    public class MainActivity extends AppCompatActivity implements OnSelectedBookChangeListener {
    
        boolean mCreating = true;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            // 加载布局资源
            setContentView(R.layout.activity_main);
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            mCreating = false;
        }
    
        @Override
        public void onSelectedBookChanged(int bookIndex) {
            // 获取FragmentManager
            FragmentManager fragmentManager = getSupportFragmentManager();
            // 通过获取FragmentManager寻找当前Activity下的BookDescFragment
            BookDescFragment bookDescFragment = (BookDescFragment) fragmentManager.findFragmentById(R.id.fragmentDescription);
            // 检查BookDescFragment引用的有效性
            if (bookDescFragment == null || !bookDescFragment.isVisible()) {
                // 方式1.弹出新的窗口显示书籍内容
                if (!mCreating) {
                    Intent intent = new Intent(this, BookDescActivity.class);
                    intent.putExtra("bookIndex", bookIndex);
                    startActivity(intent);
                }
            } else {
                // 方式2.在当前界面的BookDescFragment中显示书籍内容
                bookDescFragment.setBook(bookIndex);
            }
        }
    }
    
    • 注意:我们判断的条件是两个,BookDescFragment的引用是不是null和BookDescFragment是否可见,这是因为在某种情况下,我们将设备由横向切换至竖向时,FragmentManager仍然可以引用到在横向时候的那个碎片(即使现在它并不可见)。
    • 注意:我们还使用了mCreating的一个boolean值用来判断生命周期,这是因为当屏幕横竖切换时,Activity会被彻底重建,被选中的radio button会被初始化成第一个,然后再恢复成之前选择的(这在onResume执行之前),如果选中的不是第一个,这会触发两次onSelectedBookChanged。

2.4 参考资料

  • CreatingDynamicUIwithAndroidFragments,2ndEdition

你可能感兴趣的:(Android学习笔记,#,使用碎片创建动态UI)