Chapter2:使用Fragment灵活地构建UI
Chapter2:使用Fragment灵活地构建UI
文章目录
2.1 选择动态的碎片式布局
- 为了解决设备间的差异化,我们可以根据运行的具体设备灵活的重排Fragments。
- UI界面和程序代码之间的依赖关系越深,维护和更新应用程序就会越困难,尽管这种依赖关系无可避免,但我们希望最小化这种依赖关系,所以尽可能在布局资源中做更多与用户界面相关的工作。
- Android资源系统有设备适应性,允许我们为应用程序设计与UI相关的不同资源,每种资源用于与一组具有特定特征的设备进行关联和优化。利用该特性结合碎片化设计,我们可以轻松地根据屏幕朝向、屏幕大小动态排布Fragments,减少了重复的代码,使得应用程序的维护变得简单。
- Chapter1中的示例在手机横屏时就会显得不是很友好,我们更希望在横屏的时候是左右分布两个碎片而非上下分布。
2.1.1添加可替换布局资源
- 添加新的资源文件:
复制之前的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会自动重新创建活动,并为新方向加载适当的资源。
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),内容同上个代码块。
现在,当需要维护代码时我们只需要修改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