本章要点
作为一名专业的Android开发人员,能够同时兼容手机和平板的开发时我们必须要做到的事情。
4.1 碎片是什么
碎片(Fragment)是一种可以嵌套在活动当中的UI片段,它能让程序更加合理和充分的利用大屏幕的控件。
4.2 碎片的使用方式
开始我们的碎片之旅,创建FragmentTest项目。
4.2.1 碎片的简易用法
最简单碎片,在一个活动中添加两个碎片,并让这两个碎片平分活动控件。
新建左碎片布局left_fragment.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<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/btn"
android:text="Button"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
新建右侧碎片,right_fragment.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<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 right fragment"/>
</LinearLayout>
新建LeftFragment类和RrightFragment类,并让它们继承自support-v4包下的Fragment,重写onCreateView()来加载布局。
LeftFragment,代码如下:
public class LeftFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.left_fragment, container, false);
return view;
}
}
RightFragment,代码如下:
public class RightFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.right_fragment, container, false);
return view;
}
}
修改activity_main.xml中的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.example.hjw.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.example.hjw.fragmenttest.RightFragment"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
</LinearLayout>
属性:
android:name 指明添加碎片的类名(包名+类名)。
运行效果:
4.2.2 动态添加碎片
新建another_right_fragment.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<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:textSize="20sp"
android:text="This is another right fragment"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
创建AnotherRightFragment类,代码如下:
public class AnotherRightFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.another_right_fragment, container, false);
return view;
}
}
修改activity_main.xml中的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/left_fragment"
android:name="com.example.hjw.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<FrameLayout
android:id="@+id/right_layout"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"></FrameLayout>
</LinearLayout>
实现动态添加Fragment,修改MainActivity中的代码如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn= (Button) findViewById(R.id.btn);
btn.setOnClickListener(this);
replaceFragmetn(new RightFragment());
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn:
replaceFragmetn(new AnotherRightFragment());
break;
default:
break;z
}
}
public void replaceFragmetn(Fragment fragment){
FragmentManager fragmentManager=getSupportFragmentManager();
FragmentTransaction transaction=fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout,fragment);
transaction.commit();
}
}
动态添加布局5步骤:
- 创建待添加碎片的实例。
- 获取FragmentManager,在活动中直接通过调用getSupportFragmentManager()得到。
- 开启一个事务,通过beginTransaction()开启。
- 向容器添加或替换布局,一般使用replace()实现,需要传入容器的id和待添加布局的实例。
- 提交事务,commit()。
重启,点击一下按钮,效果图:
4.2.3 在碎片中模拟返回栈
模仿类似于返回栈的效果好,Back返回上一个碎片。
FragmentTransaction提供了addToBackStack()方法,一般传入null。修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
......
public void replaceFragmetn(Fragment fragment){
FragmentManager fragmentManager=getSupportFragmentManager();
FragmentTransaction transaction=fragmentManager.beginTransaction();
transaction.replace(R.id.right_layout,fragment);
transaction.addToBackStack(null);
transaction.commit();
}
}
重新运行程序,点击按钮将AnotherRightFragment添加到活动中,按下Back键,会RightFragment,再按下Back键,RightFragment也会消失,再按下Back键,程序才会退出。
4.2.4 碎片与活动之间进行通信
活动中调用碎片的方法,FragmentManager提供了一个类似于findFragmentById()的方法,从布局中获取碎片的实例,代码如下:
RightFragment rightFragment = (RightFragment) getSupportFragmentManager().findFragmentById(R.id.right_fragment);
碎片中调用活动的方法,调用getActivity()方法来得到和当前碎片相关联的活动,代码如下:
MainActivity activity = (MainActivity) getActivity();
这样,碎片就可以调用活动中的方法了,当碎片需要使用Context对象时,也可以使用getActivity()方法,因为获取到的活动本身就是一个Context对象。
4.3 碎片的生命周期
碎片自己的生命周期。
4.3.1 碎片的状态和回调
碎片和活动一样,生命周期会有4中状态:
- 运行状态
当一个碎片时可见的,并且它关联的活动正处于运行状态时,该对片也处于运行状态。 - 暂停状态
当一个活动进入暂停状态时,与它相关联的可见碎片就会进入到暂停状态。 - 停止状态
当一个活动进入停止状态时,与它相关联的碎片就会进入停止状态,或调用FragmentTransaction的revome(),replace()将碎片从活动中移除, 如果在提交事务之前调用addToBackStack()方法,碎片也会进入停止状态。 - 销毁状态
碎片总是依附于活动而存在的,当活动销毁时,与它关联的碎片也会销毁状态。或调用FragmentTransaction的revome(),replace()将碎片从活动中移除, 如果在提交事务之前并没有调用addToBackStack()方法,这时的碎片就会进入销毁状态。
活动中的回调方法,碎片几乎都有,碎片还提供了一些附加的回调方法:
- onAttach()。 碎片和活动建立关联的时候调用。
- onCreateView()。 碎片创建视图(加载布局)时调用。
- onActivityCreated()。 确保与碎片相关联的活动一定已经创建完毕调用。
- onDestroyView()。 当与碎片相关联的视图被移除的时候调用。
- onDetach()。 当碎片与活动解除关联的时候调用。
碎片完整的生命周期图:
4.3.2 体验碎片的生命周期
修改RightFragment中的代码,如下:
public class RightFragment extends Fragment {
private static final String TAG = "RightFragment";
@Override
public void onAttach(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(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.d(TAG, "onCreateView: ");
View view = LayoutInflater.from(getContext()).inflate(R.layout.right_fragment, container, false);
MainActivity activity = (MainActivity) getActivity();
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: ");
}
}
运行程序,观察logcat打印信息:
点击LeftFragment中的按钮,观察logcat打印信息:
按下Back键,RightFragment重新回到运行状态,观察logcat打印信息:
再次按下Back键,观察logcat打印信息:
4.4 动态加载布局的技巧
Android中动态加载布局的计技巧。
4.4.1 使用限定符
判断程序应该是单页还是双页模式:限定符(Qualifiers)。
修改FragmentTest项目中的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">
<fragment
android:id="@+id/left_fragment"
android:name="com.example.hjw.fragmenttest.LeftFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
在res目录下layout_large文件夹,新建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">
<fragment
android:id="@+id/left_fragment"
android:name="com.example.hjw.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.example.hjw.fragmenttest.RightFragment"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_height="match_parent"/>
</LinearLayout>
large是一个限定符,修改Macitivty中的代码:replaceFragment()代码注掉。运行平板效果如下:
在启动一个手机模拟器,运行效果如下:
限定符的参数:
4.4.2 使用最小宽度限定符
最小限定符(Smallest-width-Qualifier)
在res目录新建layout-sw600dp文件夹,新建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">
<fragment
android:id="@+id/left_fragment"
android:name="com.example.hjw.fragmenttest.LeftFragment"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<fragment
android:id="@+id/right_fragment"
android:name="com.example.hjw.fragmenttest.RightFragment"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_height="match_parent"/>
</LinearLayout>
屏幕宽度大于600dp,会加载layout-600dp中的布局,小于600加载默认布局。
4.5 碎片的最佳实践——一个简易版的新闻应用
新闻应用
新建FragmentBestPractice项目。
添加依赖:
compile 'com.android.support:recyclerview-v7:24.2.1'
新建News实体类:
public class News {
private String title;
private String content;
public void setTitle(String title) {
this.title = title;
}
public void setContent(String content) {
this.content = content;
}
public String getTitle() {
return title;
}
public String getContent() {
return content;
}
}
新建布局文件news_content_frag.xml,作为新闻内容的布局,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<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">
<TextView
android:id="@+id/tv_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/tv_news_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:background="#000" />
</RelativeLayout>
新建NewsContentFragment类加载news_content_frag布局,继承自Fragment,代码如下:
public class NewsContentFragment extends Fragment {
private View view;
private TextView tv_news_title,tv_news_content;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
view = LayoutInflater.from(getContext()).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);
tv_news_title= (TextView) view.findViewById(R.id.tv_news_title);
tv_news_content= (TextView) view.findViewById(R.id.tv_news_content);
tv_news_title.setText(newsTitle); //刷新新闻的标题
tv_news_content.setText(newsContent); //刷新新闻的头部
}
}
单页模式使用,创建NewsContentActivity,指定布局名为news_content,引入NewsContentFragment布局,修改布局文件如下:
<?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">
<fragment
android:id="@+id/news_content_fragment"
android:name="com.example.hjw.fragmentbestpractice.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
修改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);
Intent intent = getIntent();
String news_title = intent.getStringExtra("news_title"); //获取新闻的标题
String news_content = intent.getStringExtra("news_content"); //获取新闻的内容
NewsContentFragment newsContentFragment= (NewsContentFragment) getSupportFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news_title,news_content); //刷新NewsContentFragment界面
}
}
新建news_title_frag.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">
<android.support.v7.widget.RecyclerView
android:id="@+id/news_title_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
新闻标题的子布局,新建news_item.xml作为RecyclerView中子项的布局,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_news_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingBottom="15dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="15dp"
android:singleLine="true"
android:textSize="18sp" />
新建NewsTitleFragment作为展示新闻列表的碎片,onCreateView加载news_title_frag.xml布局,代码如下:
public class NewsTitleFragment extends Fragment{
private boolean isTowPane;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).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){
isTowPane=true; //可以找到news_content_layout布局,为双页
}else{
isTowPane=false; //找不到news_content_layout布局,为单页
}
}
接下来我们修改main_activity.xml,单页模式中的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<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.hjw.fragmentbestpractice.NewsTitleFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
新建layout-600dp文件夹,新建main_activity.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="horizontal">
<fragment
android:id="@+id/news_title_fragment"
android:name="com.example.hjw.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.hjw.fragmentbestpractice.NewsContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</LinearLayout>
接下来在NewsTitleFragment中通过RecyclerView展示新闻列表,在NewsTitleFragment中新建内部类NewsAdapter作为RecyclerView的适配器,代码如下所示:
public class NewsTitleFragment extends Fragment{
private boolean isTowPane;
......
class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder>{
private List<News> mData;
public NewsAdapter(List<News> mData) {
this.mData = mData;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view=LayoutInflater.from(parent.getContext()).inflate(R.layout.news_item,parent,false);
final ViewHolder holder=new ViewHolder(view);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
News news = mData.get(holder.getAdapterPosition());
if (isTowPane){
//如果是双页,刷新NewsContentFragment中的内容
NewsContentFragment newsContentFragment= (NewsContentFragment) getFragmentManager().findFragmentById(R.id.news_content_fragment);
newsContentFragment.refresh(news.getTitle(),news.getContent());
}else{
//如果是单页,直接启动NewsContentActivity
NewsContentActivity.actionStart(getContext(),news.getTitle(),news.getContent());
}
}
});
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
News news = mData.get(position);
holder.tv_news_title.setText(news.getTitle());
}
@Override
public int getItemCount() {
return mData.size();
}
class ViewHolder extends RecyclerView.ViewHolder{
TextView tv_news_title;
public ViewHolder(View itemView) {
super(itemView);
tv_news_title= (TextView) itemView.findViewById(R.id.tv_news_title);
}
}
}
}
通过onCreateViewHolder方法中注册的点击事件,获取到点击项News的实例,通过isTwoPane判断是单页还是双页,更新里面的数据,修改NewsTitleFragment中的代码如下:
public class NewsTitleFragment extends Fragment{
private boolean isTowPane;
private RecyclerView news_title_recycler_view;
List<News> newsList=new ArrayList<>();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.news_title_frag, container, false);
news_title_recycler_view = (RecyclerView) view.findViewById(R.id.news_title_recycler_view);
LinearLayoutManager layoutManager=new LinearLayoutManager(getContext());
news_title_recycler_view.setLayoutManager(layoutManager);
NewsAdapter adapter = new NewsAdapter(getNews());
news_title_recycler_view.setAdapter(adapter);
return view;
}
public List<News> getNews() {
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();
}
...
}
运行效果:
单页模式的运行效果:
点击子选项跳转:
双页模式的运行效果图:
4.5 小结与点评
本章我们了解碎片的基本概念,以及使用场景,掌握了碎片的常用方法,学习了碎片的生命周期,以及动态加载布局。