文章目录
参考书籍:《第一行代码》——郭霖
ps:写这个只是为了自己忘记回来复习用
4 兼顾平板探究碎片
作为一名专业的Android开发人员,能够同时兼顾手机和平板的开发是我们必须做到的事情。 Android自3.0版本开始引入了碎片的概念,它可以让界面在平板上更好地展示,下面我们就来一 起学习一下。
4.1碎片是什么
碎片(Fragment)是一种可以嵌人在活动当中的UI片段,它能让程序更加合理和充分地利 用大屏幕的空间,因而在平板上应用得非常广泛。
虽然碎片对你来说应该是个全新的概念,但相信你学习起来应该毫不费力,因为它和活动实在是太像了,同样都能包含布局,同样都有自己 的生命周期。你甚至可以将碎片理解成一个迷你型的活动,虽然这个迷你型的活动有可能和普通 的活动是一样大的。 那么究竟要如何使用碎片才能充分地利用平板屏幕的空间呢?
想象我们正在开发一个新闻 应用,其中一个界面使用RecyclerView展示了一组新闻的标题,当点击了其中一个标题时,就打 开另一个界面显示新闻的详细内容。如果是在手机中设计,我们可以将新闻标题列表放在一个活 动中,将新闻的详细内容放在另一个活动中,如图所示。
手机的手机方案
可是如果在平板上也这么设计,那么新闻标题列表将会被拉长至填充满整个平板的屏幕,而 新闻的标题一般都不会太长,这样将会导致界面上有大量的空白区域,如图所示。
因此,更好的设计方案是将新闻标题列表界面和新闻详细内容界面分别放在两个碎片中, 然后在同一个活动里引人这两个碎片,这样就可以将屏幕空间充分地利用起来了,如图所示。
4.2碎片的使用方式
介绍了这么多抽象的东西,也是时候学习一下碎片的具体用法了。你已经知道,碎片通常都是在平板开发中使用的,因此我们首先要做的就是创建一个平板模拟器。创建模拟器的方法我们 在第1章已经学过了,创建完成后启动平板模拟器,效果如图
所示。
1.碎片的简单用法
*这里用一个很简单的碎片使用的例子来理解碎片,在一个活动当中添加两个碎片创建两个碎片平分活动空间,
新建一个左侧碎片布局 left__fragment.xml,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
这个布局非常简单,只放置了一个按钮,并让它水平居中显示。
然后新建右侧碎片布局 right_fragment.xml,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="#00ff00"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="20sp"
android:text="This is roght fragment"/>
</LinearLayout>
可以看到,我们将这个布局的背景色设置成了绿色,并放置了一个 TextView用于显示一段文本。 接着新建一个LeftFragment类,并让它继承自Fragment。
注意,这里可能会有两个不同包 下的Fragment供你选择,一个是系统内置的 android.app.Fragment,一个是support-v4库中的 android.support.v4.app.Fragment。
这里强烈建议你使用support-v4库中的Fragment,因为它可 以让碎片在所有Android系统版本中保持功能一致性。比如说在Fragment中嵌套使用Fragment, 这个功能是在Android 4.2系统中才开始支持的,如果你使用的是系统内置的Fragment,那么很 遗憾,4.2系统之前的设备运行你的程序就会崩溃。而使用support-v4库中的Fragment就不会出 现这个问题,只要你保证使用的是最新的 support-v4库就可以了。
另外,我们并不需要在 build.gradle文件中添加support-v4库的依赖,因为build.gradle文件中已经添加了appcompat-v7库的依赖,而这个库会将support-v4库也一起引入进来。
现在编写一下LeftFragment中的代码,如下所示:
public class LeftFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.left_fragment,container,false);
return view;
}
}
这里仅仅是重写了Fragment 的onCreateView()方法,然后在这个方法中通过Layoutinflater 的inflate()方法将刚才定义的 left_fragment布局动态加载进来,整个方法简单明了。
接着我们 用同样的方法再新建一个RightFragment,代码如下所示:
public class RightFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.right_fragment,container,false);
return view;
}
}
基本上代码都是相同的,相信已经没有必要再做什么解释了。
接下来修改 activity__main.xml 中的代码,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.example.simpleusefragment.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/right_fragment"
android:name="com.example.simpleusefragment.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
可以看到,我们使用了标签在布局中添加碎片,其中指定的大多数属性都是你 熟悉的
只不过这里还需要通过android:name属性来显式指明要添加的碎片类名,注意一定要 将类的包名也加上。
这样最简单的碎片示例就已经写好了,现在运行一下程序,效果如图所示。
正如我们所期待的一样,两个碎片平分了整个活动的布局。不过这个例子实在是太简单了, 在真正的项目中很难有什么实际的作用,因此我们马上来看一看,关于碎片更加高级的使用技巧。
2.动态添加碎片
碎片的强大之处在于可以动态的添加碎片到活动中去,根据具体情况来动态地添加碎片,你就可以将程序 界面定制得更加多样化。
我们还是在上一节代码的基础上继续完善,新建another_right__fragment.xml,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="#ffff00"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="this is another right fragment"
android:textSize="20sp"/>
</LinearLayout>
这个布局文件的代码和 right_fragment.xml中的代码基本相同,只是将背景色改成了黄色,并 将显示的文字改了改。
然后新建AnotherRightFragment作为另一个右侧碎片,代码如下所示:
public class AnotherRightFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.another_right_fragment,container,false);
return view;
}
}
代码同样非常简单,在onCreateView()方法中加载了刚刚创建的 another_right_fragment 布 局。这样我们就准备好了另一个碎片,接下来看一下如何将它动态地添加到活动当中。
修改 activity__main.xml,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.example.simpleusefragment.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<!-- 将右侧碎片替换为FrameLayout,我们将碎片动态放到这个布局中-->
<FrameLayout
android:id="@+id/right_layout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
可以看到,现在将右侧碎片替换成了一个FrameLayout中,还记得这个布局吗?在上一章中 我们学过,这是Android中最简单的一种布局,所有的控件默认都会摆放在布局的左上角。由于 这里仅需要在布局里放入一个碎片,不需要任何定位,因此非常适合使用FrameLayout。
下面我们将在代码中向FrameLayout 里添加内容,从而实现动态添加碎片的功能。修改 MainActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//为左边碎片中的按钮添加一个监听事件(点击按钮动态添加AnotherRightFragment碎片)
Button button=(Button) findViewById(R.id.button);
button.setOnClickListener(this);
//用replaceFragment方法动态添加RightFragment碎片
//传入一个碎片实例参数
replaceFragment(new RightFragment());
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.button:
replaceFragment(new AnotherRightFragment());
break;
default:
break;
}
}
private void replaceFragment(Fragment fragment){
//调用getSupportFragmentManager获取fragmentManager
FragmentManager fragmentManager=getSupportFragmentManager();
//开启一个事务
FragmentTransaction transaction=fragmentManager.beginTransaction();
//向容器内添加或替换碎片
transaction.replace(R.id.right_layout,fragment);
//提交事务
transaction.commit();
}
}
可以看到,首先我们给左侧碎片中的按钮注册了一个点击事件,然后调用 replaceFragment()方法动态添加了RightFragment这个碎片。当点击左侧碎片中的按钮时,又 会调用replaceFragment()方法将右侧碎片替换成AnotherRightFragment。
结合 replaceFragment()方法中的代码可以看出,动态添加碎片主要分为5步。
(1)创建待添加的碎片实例。
(2)获取FragmentManager,在活动中可以直接通过调用getSupportFragmentManager()方 法得到。
(3)开启一个事务,通过调用beginTransaction()方法开启。
(4)向容器内添加或替换碎片,一般使用replace()方法实现,需要传人容器的d和待添加 的碎片实例。
(5)提交事务,调用commit()方法来完成。 这样就完成了在活动中动态添加碎片的功能,重新运行程序,可以看到和之前相同的界面, 然后点击一下按钮,效果如图4.6所示。
这样就完成了在活动中动态添加碎片的功能,启动程序右边碎片默认是right_fragment,点击按钮后替换为another_right_fragment
点击按钮后
3.在碎片中模拟返回栈
在上一小节中,我们成功实现了向活动中动态添加碎片的功能,不过你尝试一下就会发现, 通过点击按钮添加了一个碎片之后,这时按下Back键程序就会直接退出。如果这里我们想模仿 类似于返回栈的效果,按下Back键可以回到上一个碎片,该如何实现呢?
其实很简单,FragmentTransaction中提供了一个addToBackStack()方法,可以用于将一个 事务添加到返回栈中
修改MainActivity中的replaceFragment()方法,如下所示:*
private void replaceFragment(Fragment fragment){
FragmentManager fragmentManager=getSupportFragmentManager();
FragmentTransaction transaction=fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout,fragment);
//将事务添加到返回栈中,参数是一个用于描述返回栈的状态,一般传入null即可
transaction.addToBackStack(null);
transaction.commit();
}
这里我们在事务提交之前调用了FragmentTransaction的addToBackStack()方法,它可以接 收一个名字用于描述返回栈的状态,一般传人 null 即可。现在重新运行程序,并点击按钮将 AnotherRightFragment添加到活动中,然后按下Back键,你会发现程序并没有退出,而是回到了 RightFragment界面,继续按下Back键,RightFragment界面也会消失,再次按下Back键,程序 才会退出。
4.碎片和碎片活动之间的通信
虽然碎片都是嵌人在活动中显示的,可是实际上它们的关系并没有那么亲密。你可以看出, 碎片和活动都是各自存在于一个独立的类当中的,它们之间并没有那么明显的方式来直接进行通信。
如果想要在活动中调用碎片里的方法,或者在碎片中调用活动里的方法,应该如何实现呢? 为了方便碎片和活动之间进行通信,
FragmentManager提供了一个类似于 findViewById() 的方法,专门用于从布局文件中获取碎片的实例,代码如下所示:
LeftFragment rightFragment=(LeftFragment) getSupportFragmentManager()
.findFragmentById(R.id.left_fragment);
调用FragmentManager 的findFragmentById()方法,可以在活动中得到相应碎片的实例, 然后就能轻松地调用碎片里的方法了。
掌握了如何在活动中调用碎片里的方法,那在碎片中又该怎样调用活动里的方法呢?其实这 就更简单了,
在每个碎片中都可以通过调用 getActivity()方法来得到和当前碎片相关联的活 动实例,代码如下所示:
MainActivity mainActivity=(MainActivity) getActivity();
有了活动实例之后,在碎片中调用活动里的方法就变得轻而易举了。另外当碎片中需要使用 Context对象时,也可以使用getActivity()方法,因为获取到的活动本身就是一个Context 对象。
这时不知道你心中会不会产生一个疑问:既然碎片和活动之间的通信问题已经解决了,那么 碎片和碎片之间可不可以进行通信呢? 说实在的,这个问题并没有看上去那么复杂,它的基本思路非常简单,首先在一个碎片中可 以得到与它相关联的活动,然后再通过这个活动去获取另外一个碎片的实例,这样也就实现了不 同碎片之间的通信功能,因此这里我们的答案是肯定的。
4.3碎片的生命周期
和活动一样,碎片也有自己的生命周期,并且它和活动的生命周期非常像
1.碎片的状态和回调
还记得每个活动在其生命周期内可能会有哪几种状态吗?没错,一共有运行状态、暂停状态、 停止状态和销毁状态这4种。类似地,每个碎片在其生命周期内也可能会经历这几种状态,只不 过在一些细小的地方会有部分区别。
1. 运行状态
当一个碎片是可见的,并且它所关联的活动正处于运行状态时,该碎片也处于运行状态。
2.暂停状态
当一个活动进入暂停状态时(由于另一个未占满屏幕的活动被添加到了栈顶),与它相关联 的可见碎片就会进人到暂停状态。
3.停止状态
当一个活动进入停止状态时,与它相关联的碎片就会进入到停止状态。
或者通过调用 FragmentTransaction 的 remove()、replace()方法将碎片从活动中移除,但如果在事务提交之 前调用addToBackStack()方法,这时
的碎片也会进入到停止状态。
总的来说,进人停止状态的 碎片对用户来说是完全不可见的,有可能会被系统回收。
4.销毁状态
碎片总是依附于活动而存在的,因此当活动被销毁时,与它相关联的碎片就会进人到销毁状 态。
或者通过调用FragmentTransaction的remove()、replace()方法将碎片从活动中移除,但 在事务提交之前并没有调用addToBackStack()方法,这时的
碎片也会进入到销毁状态。
同样地,Fragment类中也提供了 一系列的回调方法,以覆盖碎片生命周期的每个环节。其中,活动中有的回调方法,碎片中几乎 都有,不过碎片还提供了一些附加的回调方法,那我们就重点看一下这几个回调。
onAttach() | 当碎片和活动建立关联的时候调用 |
---|---|
onCreateView() | 为碎片创建视图(加载布局)时调用 |
onActivityCreated() | 确保与碎片相关联的活动一定已经创建完毕的时候调用 |
onDestroyView() | 当与碎片关联的视图被移除的时候调用 |
onDetach() | 当碎片和活动解除关联的时候调用 |
碎片完整的生命周期示意图可参考下图,图片源自Android官网。
2.体验碎片的生命周期
为了让你能够更加直观地体验碎片的生命周期,我们还是通过一个例子来实践一下。例子很 简单,仍然是在FragmentTest项目的基础上改动的。
修改RightFragment中的代码,如下所示:
public class RightFragment extends Fragment {
private static final String TAG="RightFragment";
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
Log.d(TAG, "onAttach: ");
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate: ");
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.d(TAG, "onCreateView: ");
View view=inflater.inflate(R.layout.right_fragment,container,false);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.d(TAG, "onActivityCreated: ");
}
@Override
public void onStart() {
super.onStart();
Log.d(TAG, "onStart: ");
}
@Override
public void onResume() {
super.onResume();
Log.d(TAG, "onResume: ");
}
@Override
public void onPause() {
super.onPause();
Log.d(TAG, "onPause: ");
}
@Override
public void onStop() {
super.onStop();
Log.d(TAG, "onStop: ");
}
@Override
public void onDestroyView() {
super.onDestroyView();
Log.d(TAG, "onDestroyView: ");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy: ");
}
@Override
public void onDetach() {
super.onDetach();
Log.d(TAG, "onDetach: ");
}
我们在RightFragment中的每一个回调方法里都加人了打印日志的代码,然后重新运行程序, 这时观察logcat中的打印信息,如图4.8所示。
可以看到,当 RightFragment 第一次被加载到屏幕上时,会依次执行 onAttach(). onCreate()、onCreateView()、onActivityCreated()、onStart()和onResume()方法。
然后点击LeftFragment中的按钮,此时打印信息如下图所示。
由于AnotherRightFragment替换了RightFragment,此时的RightFragment进入了停止状态, 因此onPause()、onStop()和onDestroyView()方法会得到执行。当然如果在替换的时候没有调用addToBackStack()方法,此时的 RightFragment 就会进入销毁状态,onDestroy()和 onDetach()方法就会得到执行。
接着按下Back键,RightFragment会重新回到屏幕,打印信息如图4.10所示。
由于RightFragment 重新回到了运行状态,因此 onActivityCreated()、onStart()和 onResume()方法会得到执行。注意此时onCreate()和onCreateView()方法并不会执行,因为 我们借助了addToBackStack()方法使得RightFragment和它的视图并没有销毁。
再次按下 Back键退出程序,打印信息如下图所示。
依次会执行onPause()、onStop()、onDestroyView()、onDestroy()和 onDetach()方 法,最终将活动和碎片一起销毁。
这样碎片完整的生命周期你也体验了一遍,是不是理解得更加 深刻了? 另外值得一提的是,在碎片中你也是可以通过onSaveInstanceState()方法来保存数据的, 因为进入停止状态的碎片有可能在系统内存不足的时候被回收。保存下来的数据在onCreate()、 onCreateView()和 onActivityCreated()这 3 个方法中你都可以重新得到,它们都含有一个 Bundle类型的savedInstanceState参数。如果你忘记了该 如何编写,可以参考 2.4.5小节。
4.4动态加载布局的技巧
虽然动态添加碎片的功能很强大,可以解决很多实际开发中的问题,但是它毕竟只是在一个 布局文件中进行一些添加和替换操作。如果程序能够根据设备的分辨率或屏幕大小在运行时来决 定加载哪个布局,那我们可发挥的空间就更多了。因此本节我们就来探讨一下Android中动态加 载布局的技巧。
1.使用限定符
如果你经常使用平板电脑,应该会发现现在很多的平板应用都采用的是双页模式(程序会在 左侧的面板上显示一个包含子项的列表,在右侧的面板上显示内容),因为平板电脑的屏幕足够 大,完全可以同时显示下两页的内容,但手机的屏幕一次就只能显示一页的内容,因此两个页面 需要分开显示。
那么怎样才能在运行时判断程序应该是使用双页模式还是单页模式呢?这就需要借助限定 符(Qualifiers)来实现了。下面我们通过一个例子来学习一下它的用法
修改FragmentTest项目 中的activity_main.xml文件,代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 手机屏幕默认显示一个左侧碎片-->
<fragment
android:id="@+id/left_fragment"
android:name="com.example.simpleusefragment.LeftFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
这里将多余的代码都删掉,只留下一个左侧碎片,并让它充满整个父布局。
接着在res目录 下新建layout-large文件夹,在这个文件夹下新建一个布局,也叫作activity_main.xml,代码如下 所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 大屏以1:3显示两个碎片-->
<fragment
android:id="@+id/left_fragment"
android:name="com.example.simpleusefragment.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.example.simpleusefragment.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"/>
</LinearLayout>
可以看到,layout/activity_main布局只包含了一个碎片,即单页模式,而 layout-large/ activity_main布局包含了两个碎片,即双页模式。其中large就是一个限定符,那些屏幕被认为 是large的设备就会自动加载layout-large文件夹下的布局,而小屏幕的设备则还是会加载layout 文件夹下的布局。
然后将MainActivity中replaceFragment()方法里的代码注释掉,并在平板模拟器上重新 运行程序,效果如图所示。(手机单页/平板双页)
这样我们就实现了在程序运行时动态加载布局的功能。
Android中一些常见的限定符可以参考下表。
屏幕特征
small | 提供给小屏设备 |
---|---|
normal | 中屏 |
large | 大屏 |
xlarge | 超大屏 |
分辨率
ldpi | 低分辨率120dpi以下 |
---|---|
mdpi | 中等分辨率 120dpi~160dpi |
hdpi | 高分辨率~240dpi |
xhdpi | 超高分辨率~320dpi |
xxhdpi | 超超高分辨率~480dpi |
2.使用最小宽度限定符
在上一小节中我们使用large限定符成功解决了单页双页的判断问题,不过很快又有一个 新的问题出现了,large到底是指多大呢?有的时候我们希望可以更加灵活地为不同设备加载布 局,不管它们是不是被系统认定为 large,这时就可以使用最小宽度限定符(Smallest-width Qualifier)了。
最小宽度限定符允许我们对屏幕的宽度指定一个最小值(以dp为单位),然后以这个最小值 为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备就加载另一 个布局。
在res目录下新建layout-sw600dp文件夹,然后在这个文件夹下新建activity__main.xml布局, 代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 当屏幕宽度大于600dp时,以1:3显示两个碎片,否则使用layout目录下的布局文件-->
<fragment
android:id="@+id/left_fragment"
android:name="com.example.simpleusefragment.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.example.simpleusefragment.RightFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"/>
</LinearLayout>
4.5碎片的实践案例—简易版新闻应用
前面有提到过,碎片很多时候都是在平板开发当中使用的,主要是为了解决屏幕空间不能充 分利用的问题。那是不是就表明,我们开发的程序都需要提供一个手机版和一个Pad版呢?确实 有不少公司都是这么做的,但是这样会浪费很多的人力物力。因为维护两个版本的代码成本很高, 每当增加什么新功能时,需要在两份代码里各写一遍,每当发现一个bug时,需要在两份代码里 各修改一次。因此今天我们最佳实践的内容就是,编写一个同时兼容手机和平板的应用程序。
由于会在编写新闻列表时会使用到RecyclerView,因此首先需要在app/build.gradle当中添 加依赖库recyclerview-v7:28.0.0',如下所示:
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'com.android.support:recyclerview-v7:28.0.0'
}
接下来我们要准备好一个新闻的实体类,新建类News,代码如下所示:
public class News {
private String title;
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
接着新建布局文件news_content_frag.xml,用于作为新闻内容的布局:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/visibility_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="10dp"
android:textSize="20sp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000"/>
<TextView
android:id="@+id/news_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="15dp"
android:textSize="18sp"/>
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:background="#000"/>
</RelativeLayout>
新闻内容的布局主要可以分为两个部分,头部部分显示新闻标题,正文部分显示新闻内容, 中间使用一条细线分隔开。这里的细线是利用View来实现的,将View的宽或高设置为1dp,再 通过background属性给细线设置一下颜色就可以了。这里我们把细线设置成黑色。
然后再新建一个NewsContentFragment类,继承自Fragment,代码如下所示:
public class NewsContentFragment extends Fragment {
private View view;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
view=inflater.inflate(R.layout.news_content_frag,container,false);
return view;
}
//刷新新闻标题和内容
public void refresh(String newsTitle,String newsContent){
View visibilityLayout = view.findViewById(R.id.visibility_layout);
visibilityLayout.setVisibility(view.VISIBLE);
TextView newsTitleText=(TextView) view.findViewById(R.id.news_title);
TextView newsContentText=(TextView) view.findViewById(R.id.news_content);
//刷新新闻标题和内容
newsTitleText.setText(newsTitle);
newsContentText.setText(newsContent);
}
}
首先在onCreateView()方法里加载了我们刚刚创建的news_content_frag 布局。接下来又提供了一个refresh()方法,这个方法就是用于将新闻的标题和内容显示在 界面上的。这里通过findViewById()方法分别获取到新闻标题和内容的控件,然后将方法传递进来的参数设置进去。这样我们就把新闻内容的碎片和布局都创建好了
但是它们都是在双页模式中使用的,如果 想在单页模式中使用的话,我们还需要再创建一个活动。
新建一个NewsContentActivity,并将布局名指定成 news_content,然后修改 news_content.xml中的代码,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/news_content_fragment"
android:name="com.example.fragmentbestpractice.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
这里我们充分发挥了代码的复用性,直接在布局中引人了NewsContentFragment,这样也 就相当于把news_content_frag 布局的内容自动加了进来。
然后修改NewsContentActivity中的代码,如下所示:
public class NewsContentActivity extends AppCompatActivity {
//用来给其他活动传数据
public static void actionStart(Context context,String newsTitle,String newsContent){
Intent intent =new Intent(context,NewsContentActivity.class);
intent.putExtra("news_title",newsTitle);
intent.putExtra("news_content",newsContent);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_content);
//获取标题内容
String newsTitle=getIntent().getStringExtra("news_title");
String newsContent=getIntent().getStringExtra("news_content");
//获取数据
NewsContentFragment newsContentFragment=(NewsContentFragment) getSupportFragmentManager()
.findFragmentById(R.id.news_content_fragment);
//碎片刷新数据
newsContentFragment.refresh(newsTitle,newsContent);
}
}
可以看到,在 onCreate()方法中我们通过Intent获取到了传人的新闻标题和新闻内容,然后调用FragmentManager的findFragmentById()方法得到了NewsContentFragment的实例, 接着调用它的refresh()方法,并将新闻的标题和内容传入,就可以把这些数据显示出来了。
注意这里我们还提供了一个 actionStart()方法,用来给其他活动传递参数
接下来还需要再创建一个用于显示新闻列表的布局,新建 news_title_frag.xml,代码如下 所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/news_title_recycle_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
这个布局的代码就非常简单了,里面只有一个用于显示新闻列表的RecyclerView。既然要用 到RecyclerView,那么就必定少不了子项的布局。
新建news_item.xml作为RecyclerView子项的 布局,代码如下所示:
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:textSize="18sp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"/>
子项的布局也非常简单,只有一个 TextView。仔细观察TextView,你会发现其中有几个属 性是我们之前没有学过的。
android:singleLine | 文本只能单行显示 |
---|---|
android:ellipsize | 文本超出空间宽度后的显示方式 |
android:padding | 文本和空间周围的空白宽度 |
既然新闻列表和子项的布局都已经创建好了,那么接下来我们就需要一个用于展示新闻表 的地方。
这里新建NewsTitleFragment作为展示新闻列表的碎片,代码如下所示:
public class NewsTitleFragment extends Fragment {
private boolean isTwoPane;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.news_title_frag,container,false);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if(getActivity().findViewById(R.id.news_content_layout)!=null){
isTwoPane=true;
}else {
isTwoPane=false;
}
}
可以看到,NewsTitleFragment 中并没有多少代码,在onCreateView()方法中加载了 news_title_frag 布局,这个没什么好说的。我们注意看一下onActivityCreated()方法,这个方法通过在活动中能否找到一个id为new_content_layout 的View来判断当前是双页模式还是单页模式,因此我们需要让这个id为news_content_layout 的View只在双页模式中才会出现。
那么怎样才能实现这个功能呢?其实并不复杂,只需要借助我们刚刚学过的限定符就可以了。
首先修改activity_main.xml中的代码,如下所示:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/news_title_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 单页模式下只显示新闻标题-->
<fragment
android:id="@+id/news_title_fragment"
android:name="com.example.fragmentbestpractice.NewsTitleFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
上述代码表示,在单页模式下,只会加载一个新闻标题的碎片。
然后新建layout-sw600dp文件夹,在这个文件夹下再新建一个 activity_main.xml文件,代码 如下所示:
<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:id="@+id/news_title_fragment"
android:name="com.example.fragmentbestpractice.NewsTitleFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/news_content_layout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3">
<fragment
android:id="@+id/news_content_fragment"
android:name="com.example.fragmentbestpractice.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
</LinearLayout>
可以看出,在双页模式下我们同时引入了两个碎片,并将新闻内容的碎片放在了一个Frame- Layout布局下,而这个布局的id正是news_content_layout。因此,能够找到这个id的时候就是 双页模式,否则就是单面模式。
现在我们已经将绝大部分的工作都完成了,但还剩下至关重要的一点,就是在NewsTitleFragment中通过RecyclerView将新闻列表展示出来。
我们在NewsTitleFragment中新建一个 内部类NewsAdapter来作为RecyclerView的适配器,如下所示:
//适配器(在NewsTitleFragment类中)
public class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {
private List<News> mNewsList;
class ViewHolder extends RecyclerView.ViewHolder{
TextView newsTitleText;
public ViewHolder(View view){
super(view);
newsTitleText=(TextView) view.findViewById(R.id.news_title);
}
}
public NewsAdapter(List<News> newsList){
mNewsList=newsList;
}
@android.support.annotation.NonNull
@androidx.annotation.NonNull
@Override
public ViewHolder onCreateViewHolder(@android.support.annotation.NonNull @androidx.annotation.NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_item,parent,false);
final ViewHolder viewHolder=new ViewHolder(view);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
News news=mNewsList.get(viewHolder.getAdapterPosition());
if(isTwoPane){
//刷新news_content_fragment中的内容
NewsContentFragment newsContentFragment=(NewsContentFragment) getFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news.getTitle(),news.getContent());
}else{
//直接启动NewsContentActivity
NewsContentActivity.actionStart(getActivity(),news.getTitle(),news.getContent());
}
}
});
return viewHolder;
}
@Override
public void onBindViewHolder(@android.support.annotation.NonNull @NonNull ViewHolder viewHolder, int position) {
News news=mNewsList.get(position);
viewHolder.newsTitleText.setText(news.getTitle());
}
@Override
public int getItemCount() {
return mNewsList.size();
}
}
需要注意的是,之前我们都是将适配器写成一个独立的类,其实也是可以写成内部类的, 这里写成内部类的好处就是可以直接访问NewsTitleFragment的变量,比如isTwoPane。
观察一下onCreateViewHolder()方法中注册的点击事件,首先获取到了点击项的News实 例,然后通过 isTwoPane变量来判断当前是单页还是双页模式,如果是单页模式,就启动一个 新的活动去显示新闻内容,如果是双页模式,就更新新闻内容碎片里的数据。
现在还剩最后一步收尾工作,就是向RecyclerView 中填充数据了。修改 NewsTitle- Fragment中的代码,如下所示:
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view=inflater.inflate(R.layout.news_title_frag,container,false);
RecyclerView newsTitleRecyclerView=(RecyclerView) view.findViewById(R.id.news_title_recycle_view);
LinearLayoutManager layoutManager=new LinearLayoutManager(getActivity());
newsTitleRecyclerView.setLayoutManager(layoutManager);
NewsAdapter adapter=new NewsAdapter(getNews());
newsTitleRecyclerView.setAdapter(adapter);
return view;
}
private List<News> getNews() {
List<News> newsList = new ArrayList<>();
for (int i = 1; i <= 50; i++) {
News news = new News();
news.setTitle("This is news title " + i);
news.setContent(getRandomLengthContent("This is news content " + i + ". "));
newsList.add(news);
}
return newsList;
}
private String getRandomLengthContent(String content) {
Random random = new Random();
int length = random.nextInt(20) + 1;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
builder.append(content);
}
return builder.toString();
}
可以看到,onCreateView()方法中添加了RecyclerView标准的使用方法,在碎片中使用 RecyclerView和在活动中使用几乎是一模一样的,相信没有什么需要解释的。另外,这里调用 了getNews()方法来初始化50条模拟新闻数据,同样使用了一个getRandomLengthContent() 方法来随机生成新闻内容的长度,以保证每条新闻的内容差距比较大,相信你对这个方法肯定不会陌生了。