====================================
====== 第四章:手机平板要兼顾 — 探究碎片 ======
====================================
4.1 碎片是什么(通过都是在平板中使用)
碎片(fragment)是一种可以嵌入在活动当中的UI片段,它能让程序更加合理和充分地利用大屏幕的空间。碎片和活动是在太像了,同样能包含布局,同样有自己的声明周期。你甚至可以将碎片理解为一个迷你型的活动。
一个活动中引入两个碎片,相当于一个活动力分为左右两个屏幕。
新建一个FragmentTest
创建两个layout的xml文件,一个left一个right。
然后新建一个Fragment的类,继承自fragment,这时候发现有两个fragment供选择,一个是系统内置的android.app.Fragment,一个是support-v4库中的 ,强烈建议使用support-v4的Fragment,因为可以让碎片在所有Android系统版本中保持功能一致性。
另外,我们不需要在build.gradle文件中添加support-v4库的依赖,因为build.gradle文件中已经添加了appcompat-v7库的依赖,而这个库会将support-v4库也一起引入进来。
<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:layout_width="0dp"
android:layout_height="match_parent"
android:id="@+id/left_fragment"
android:layout_weight="1"
android:name="com.example.fragmenttest.LeftFragment"/>
<fragment
android:layout_width="0dp"
android:layout_height="match_parent"
android:id="@+id/right_fragment"
android:layout_weight="1"
android:name="com.example.fragmenttest.RightFragment"/>
</LinearLayout>
可以看出,我们使用了<fragment>标签在布局中添加碎片,,只不过这里需要通过android:name属性来显性指明要添加的碎片类型(注意这里一定要将类的包名也加上)
4.2.2 动态添加碎片
碎片的真正强大之处在于,它可以在程序运行事动态的添加到活动当中,根据实际情况动态的添加碎片,您就可以将程序界面定制更加多样化。
新增一个another_right_fragment.xml,
动态添加碎片主要分为5步:
1、创建待添加的碎片实例
2、获取FragmentManager,在活动中可以直接通过调用getSupportFragmentManger()方法得到。
3、开启一个事物,通过调用beginTransaction()方法开启
4、向容器内添加或替换碎片,一般使用replace()方法实现,需要传入容器的id和待添加的碎片实例
5、提交事务,调用commit()方法来完成。
4.2.3 在碎片中模拟返回栈
FragmentTransaction中提供了一个addToBackStack()方法,可以用于将一个事物添加到返回栈中。修改MainActivity中的代码。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
…
private void replaceFragment(Fragment fragment) {
FragmentMananger fragmengManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout, fragment);
transaction.addToBackStack(null);
transaction.commit();
}
}
addBackToStack(null)可以接受一个名字用于描述返回栈的状态,一般传入null即可。
4.2.4 碎片和活动之间进行通信
虽然现在碎片是嵌入到活动中了,但是实际上他们的关系并没有那么亲密。你看到,碎片和活动都是各自存在于独立的类中的。
为了方便碎片和活动之间进行通讯,FragmentManager提供了一个类似于findViewById()的方法,专门用于从布局文件中获取碎片的实例。如下:
RightFragment rightFragment = (RightFragment)getFragmentManager().findFragmentById(R.id.right_fragment);
那么,如何在碎片里调用活动呢?在每个碎片中都可以通过getActivity()方法来得到和当前碎片相关联的活动实例,如下:
MainActivity activity = (MainActivity) getActivity();
getActivity()获取到的本身就是一个context对象。
那么,碎片与碎片之间如何通讯呢?碎片可以得到与它关联的活动,通过活动再获取另一个碎片的实例即可。
4.3 碎片的声明周期
和活动一样,碎片也有声明周期,而且它的声明周期与活动的生命周期实在是太像了。
4.3.1 碎片的状态和回调
活动有:运行状态、暂停状态、停止状态和销毁状态四种。
以下是碎片的声明周期:
1、运行状态:
当一个碎片是可见的,并且它所关联的活动正处于运行状态时,该碎片也处于运行转台。
2、暂停状态:
当一个活动进入暂停状态(由于另一个未占满屏幕的活动被添加到了栈顶),与它相关联的可见碎片就会进入暂停状态
3、停止状态:
当一个活动进入停止状态,与它关联的碎片就会进入停止状态。或者通过调用FragmentTransaction的remove()、replace()方法将碎片从活动中移除,但如果在事物提交之前调用addToBackStack()方法,这时的碎片也会进入到停止状态。总的来说,进入停止状态的碎片对于用户来说是完全不可见的,有可能会被系统回收。
4、销毁状态:
碎片依附于活动而存在,当活动被销毁时,与他相关联的碎片就会进入到销毁状态。或者通过调用FragmentTransaction的remove()、replace()方法将碎片从活动中移除,但是在事物提交之前没有调用addBackToStack()方法,这时的碎片也会进入到销毁状态。
Fragment类中也提供了一系列的毁掉方法。以覆盖碎片生命周期的每个环节。其中,活动中有的回调方法,碎片中几乎都有,不过碎片还提供了一些附加的会调方法,重点看一下这几个会调:
onAttach():当碎片和活动建立关联的时候调用
onCreateView():为碎片创建视图(加载布局)时调用
onActivityCreate():确保与碎片相关联的活动一定已经创建完毕时调用。
onDestroyView():当与碎片关联的视图被移除的时候调用
onDetach():当碎片和活动解除关联的时候调用。
具体的碎片声明周期看书本图片:page153
添加一个碎片 —> onAttach() —> onCreate() —> onCreateView() —> onStart() —> onResume() —> 碎片已激活 —> onPause() —> onStop() —> onDestroy() —> onDetach() —> 碎片已销毁
4.3.2 体验一下碎片的声明周期
修改RightFragment中的代码
public class RightFragment extends Fragment {
public statci final String TAG = “RightFragment”;
@Override
public void onAttach(Context context) {
super.onAttach(context);
Log.d(TAG, “onAttach”);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, VIewGroup container, Bundle savedInstanceState) {
Log.d(TAG, “onCreateView”);
View view = inflater.inflater(R.layout.right_fragment, container, false);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreate(savedInstanceState);
}
@Override
public void onStart() {
super.onStart();
}
@Overrde
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onStop() {
super.onStop();
}
@Overrde
public void onDestroyView() {
super.onDestroyVew();
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public voide onDetach() {
super.onDetach();
}
}
顺便,在碎片中,你也可以通过onSaveInstanceState()方法来保存数据,因为在进入停止状态的时候碎片有可能在系统内存不足的时候被回收。保存下来的数据可以在onCreate() onCreateView() onActivityCreate() 这三个方法中重新得到,因为这三个方法中都有Bundle类型的savedInstanceState参数。
4.4 动态加载布局的技巧
动态添加碎片的功能毕竟只是一个布局文件中进行添加和替换操作。如果程序能够根据设备的分辨率或屏幕大小来运行时加载某个布局,那我们可发挥的空间就更多了。
4.4.1
使用平板电脑经常会发现分左右两页,那么如何才能在运行时判断程序应该是使用双页模式还是使用单页模式呢?这就需要借助限定符(Qualifiers)来实现。
创建两个activity_main.xml的文件。一个只有一个fragment占满屏幕,一个有两个fragment,分为左右屏。
其中,放入layout文件夹中的
另一个放在layout_large文件夹中,
其中,large就是一个限定符,那些屏幕被认为是large的设备就会自动加载layout_large文件夹下的布局,小屏幕的设备则会加载layout文件夹下的布局。
Android中常见的限定符如下所示:(参照Page159)
屏幕特征 限定符 描述
大小 small 提供给小屏幕设备的资源
normal 提供给中等屏幕设备的资源
large 提供给大屏幕设备的资源
xlarge 提供给超大屏幕设备的资源
分辨率 ldpi 提供给低分辨率设备的资源(120dpi以下)
mdpi 提供给中等分辨率设备的资源(120dpi ~ 160dpi)
hdpi 提供给高分辨率设备的资源(160dpi ~ 240dpi)
xhdpi 提供给超高分辨率设备的资源(240dpi ~ 320dpi)
xxhdpi 提供给超超高分辨率设备的资源(320dpi ~ 480dpi)
方向 land 提供给横屏设备的资源
port 提供给竖屏设备的资源
4.4.2 使用最小宽度限定符
新的问题出现了,large到底是指多大呢?
最小宽度限定符允许我们对屏幕的宽度指定一个最小值(以dp为单位),然后以这个最小值为临界点。
在res目录下新建layout-sw600dp,然后在里面新建activity_main.xml布局,这意味着,当程序运行在屏幕宽度大于600dp的设备上时,会加载layout-sw600dp/activity_main布局。
4.5 碎片的最佳实践 —> 一个简易的新闻应用
常见一个新的应用FragmentBestPractice。
1、在app/build.gradle中添加recyclerView的依赖库
2、新增一个News类,作为model
public class News {
private String title; // 标题
private String content; // 内容
public String getTitle() {
return title;
}
public String getContent() {
return content;
}
public void setTitle(String title) {
this.title = title;
}
public void setContent(String content) {
this.content = content;
}
3、新建布局,用于新闻内容的布局 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=“invisible” >
<TextView
android:id=“@+id/news_title”
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:layout_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/new_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>
4、新建一个NewsContentFragment类,继承自Fragment,代码如下
public class NewsContentFragment extends Fragment {
private VIew view;
@Override
public View onCreateVIew(LayoutInflater infalter, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflater(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 newsTitltText = (TextView) view.findViewById(R.id.news_title);
TextView newsContentText = (TextView) view.findViewById(R.id.new_content);
newsTitleText.setText(newsTitle); // 刷新新闻的标题
newsContentText.setText(newsContent); // 刷新新闻的内容
}
}
5、上面我们创建的碎片和布局都是在双页模式下使用的,现在需要再创建一个活动,右键com.example.fragmentbestpractice包 —> New —> Activity —> Empty Activity,新建一个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:layotu_height=“match_parent” />
</LinearLayout>
6、修改NewsContentFragment的代码
public class NewsContentFragment extends AppCompataActivity {
public static void actionStart(Context context, String newsTitle, String newsContent) {
Intent intend = 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().getStingExtra(“news_content”); // 获取传入的新闻内容
NewsContentFragment newsContentFragment = (NewsContentFragment) getSupportFragmengManager().findFragmentById(R.id.news_content_fragnent);
newsContentFragment.refresh(newsTitle, newsContent); // 刷新NewsContent-Fragment界面
}
}
在onCreate方法中,我们通过Intent获取到了传入的新闻标题和新闻内容
然后通过FragmentManager的findFragmentById()方法得到了NewsContentFragment的实例。接着调用它的refresh()方法。
actionStart()方法,作用忘记了。。。回看一下2.6.3小节。
7、还需要创建一个用于显示新闻列表的布局,新建news_title_frag.xml
<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:orientation=“vertical”
android:layout_width=“match_parent”
android:layout_height=“match_parent” >
<android.support.v7.widget.RecyclerView
android:id=“@+id/news_title_recycler_view”
android:layout_width=“match_parent”
android:layout_height=“match_parent” />
</LinearLayout>
8、新建news_item.xml作为RecyclerView子项的布局
<TextView xmlns:android=“http://schemas.android.com/apk/res/android”
android:id=“@+id/new_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” />
android:padding的意思表示给控件的周围加上补白,这样不至于让文本内容靠在边缘上
android:singleLine设置为true表示让TextView只能单行显示
Android:ellipsize用于设定当文本内容超出控件宽度时,文本的缩略方式,这里的end表示在尾部进行缩略。
9、用于展示新闻列表的地方。新建NewsTitleFragment作为展示新闻列表的碎片。
public class NewsTitleFragment extends Fragment {
private boolean isTwoPane;
@Override
public View onCreateView(LayoutInflater inflater, VIewGroup container, Bundle savedInstanceState) {
View view = inflater.inflater(R.id.news_title_frag, container, false);
return view;
}
@Override
public void onActivityCreate(Bundle savedInstanceState) {
super.onActivityCreate(savedInstanceState);
if (getActivity().findViewById(R.id.news_content_layout) != null ) {
isTwoPane = true; // 可以找到news_content_layout布局时,为双页模式
} else {
isTwoPane = false; // 找不到news_content_layout布局时,为单页模式
}
}
}
getActivity()方法用户在fragment中获取关联的activity
10、修改activity_main.xml文件
<FramLayout 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.fragmentbestparctice.NewsTitleFragment”
android:layout_width=“match_parent”
android:layout_height=“match_parent” />
</FrameLayout>
上面代码中,在单页模式下,只会加载一个新闻标题的碎片。
11、然后新建layout-sw600dp文件夹,在这个文件及中新建一个activity_main.xml文件,代码如下
<LinearLayout xmlns:android=“http://schemas.android.com/apl/res/android”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:orientation=“horizontal” >
<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=“math_parent” />
</FrameLayout>
</LinearLayout>
可以看到,我们在双页模式下引入了两个碎片,并将新闻内容碎片放在FrameLayout布局下,而这个布局的id正式news_content_layout,因此,能够找到这个id的时候就是双页模式。
12、在NewsTitleFragment中通过RecyclerView将新闻列表展示出来,我们在NewsTitleFragment中新建一个NewsAdapter来作为RecyclerView的适配器。
public class NewsTitltFragment extends Fragment {
private boolean isTwoPane;
…
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;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflater(R.layout.news_item, parent, false);
final ViewHolder holder = new ViewHolder(view);
view.setOnClickLinster(new View.OnClickListener() {
@Override
public void onClick(View v) {
News news = mNewsList.get(holder.getAdapterPosition());
if (isTwoPane) {
// 如果是双页模式,则刷新NewsContentFragment中的内容
NewsContentFragment newsContentFragment = (NewsContentFragment)getFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news.getTitle(), news.getContent());
} else {
// 如果是单页模式,则直接启动NewsContentActivity
NewsContentActivity.actionStart(getActigity(), news.getTitle(), news.getContent());
}
}
});
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
News news = mNewsList.get(position);
holder.newsTitleText.setText(news.getTitle());
}
@Override
public int getItemCount() {
return mNewsList.size();
}
}
需要注意的是,之前我们是将适配器写成一个独立的类,其实也可以写成内部类的。这里写成内部类的好处就是可以直接访问NewsTitleFragment的变量,比如isTwoPane变量;
13、最后的工作,向RecyclerView中填充数据。修改NewsTitleFragment中的代码
@Override
public View onCreateView (LayoutInflater inflater, VIewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.news_title_frag, container, false);
RecyclerView newsTitleRecyclerView = (RecyclerView) view.findViewById(R.id.news_title_recycler_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 = 0; 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 StrignBuilder();
for (int i = 0; i < length; i++ ) {
builder.append(content);
}
return builder.toString();
}
总结:到本章为止,已经学习完了AndroidUI相关的重要知识点。但是只是涉及了Android四大组件的第一个组件活动。