欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Chapter2:使用Fragment灵活地构建UI

程序员文章站 2022-03-23 11:00:06
...

Chapter2:使用Fragment灵活地构建UI

2.1 选择动态的碎片式布局

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

2.1.1添加可替换布局资源

  • 添加新的资源文件:

Chapter2:使用Fragment灵活地构建UI
Chapter2:使用Fragment灵活地构建UI

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

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
    
        <!-- List of Book Titles -->
        <fragment
            android:id="@+id/fragmentTitles"
            android:name="com.virtual.learn101022.BookListFragment"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />
        <!-- Description of selected book -->
        <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

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

  • 现在,当需要维护代码时我们只需要修改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);
            }
        }
    }
    
     <!-- activity_book_desc.xml -->
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <!-- Description of selected book -->
        <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>
    
     <!-- activity_main.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <!-- List of Book Titles -->
        <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