所谓知之者不如好之者,好之者不如乐之者。要想持之以恒,最佳的状态便是乐在其中。本文图文并茂,带你从Android Fragment最佳实践的“全世界”路过。
先贴上效果图:
这是一个关于水果的项目(麻雀虽小,五脏俱全嘛(;゚∀゚)=3ハァハァ):
整个界面分为两个部分,左半部分是一个列表,用于显示水果的名称。
右半部分则用来显示水果的名称及简介。
既然是面向对象编程,
我们首先就需要建立一个水果的实体类Fruit来模拟这些水果。
不过,由于我们需要对这些水果进行管理,所以这里我们考虑建立一个名为AllFruit的外部类来对这些水果进行管理。
当然,并不是说非得建立这种内外部类的关系,只是这样的设计更加自然,符合逻辑。
代码如下:
public class AllFruit {
//定义内部类Fruit
public static class Fruit{//Fruit类开始
//Fruit类的成员变量
public Integer id;
public String name;
public String description;
//Fruit类的构造器
public Fruit(Integer id, String name, String description){
this.id = id;
this.name = name;
this.description = description;
}
//toString方法可以理解为水果对自己的介绍。这里我们重写之后,
//当直接输出水果对象时会调用此方法,此处输出水果的name属性
@Override
public String toString() {
return name;
}
}//Fruit类结束
//用list集合来记录所有的水果对象
public static List<Fruit> FRUIT_LIST_ITEMS = new ArrayList<>();
//使用map集合来记录所有的水果对象
public static Map<Integer,Fruit> FRUIT_MAP_ITEMS = new HashMap<>();
static {
//通过静态初始化块来事先定义List和Map集合中的内容
//添加Fruit对象的方法是:用封装好的addItem()方法来添加:
addItem(new Fruit(1,"Apple","苹果" +
"是蔷薇科苹果亚科苹果属植物,其树为落叶乔木。" +
"苹果的果实富含矿物质和维生素,是人们最常食用的水果之一。"));
addItem(new Fruit(2,"Pear","梨是一种水果的名称,蔷薇科梨属植物," +
"多年生落叶乔木果树,叶子卵形,花多白色," +
"一般梨的颜色为外皮呈现出金黄色或暖黄色," +
"里面果肉则为通亮白色,鲜嫩多汁,口味甘甜," +
"核味微酸,是很好的水果。很多分布在华北、东北、西北及长江流域各省。"));
addItem(new Fruit(3,"Banana","香蕉,又称甘蕉、芎蕉、芽蕉,弓蕉," +
"为芭蕉科芭蕉属小果野蕉的人工栽培杂交种,为多年生草本植物。" +
"果实长有棱;果皮黄色,果肉白色,味道香甜。" +
"主要生长在热带、亚热带地区。原产于亚洲东南部热带、亚热带地区。"));
addItem(new Fruit(4,"grape","葡萄为葡萄科葡萄属木质藤本植物,小枝圆柱形,有纵棱纹," +
"无毛或被稀疏柔毛,叶卵圆形,圆锥花序密集或疏散,基部分枝发达," +
"果实球形或椭圆形,花期4-5月,果期8-9月。"+
"葡萄是世界最古老的果树树种之一,葡萄的植物化石发现于第三纪地层中," +
"说明当时已遍布于欧、亚及格陵兰。[1] 葡萄原产亚洲西部,世界各地均有栽培,[2] " +
"世界各地的葡萄约95%集中分布在北半球。"));
}
//封装在集合中添加水果对象的方法
private static void addItem(Fruit fruit){
FRUIT_LIST_ITEMS.add(fruit);
FRUIT_MAP_ITEMS.put(fruit.id,fruit);
}
}
用图示来说明的话,其逻辑是酱婶儿的:
一目了然,外部类AllFruit管理着内部类Fruit。
- AllFruit将Fruit类的信息以两种方式保存:List和Map;
- 如果别人来找AllFruit要Fruit的信息,则AllFruit就会根据具体情况来选择不同的方式以交付信息
注:关于静态初始化块的说明:
- Java 中可以通过初始化块进行数据赋值;
- 静态初始化块只在类(本例中是AllFruit类)加载时执行,且只会执行一次,同时静态初始化块只能给静态变量赋值,不能初始化普通的成员变量。
第二步:
既然已经定义好了实体类,那我们就要开始考虑项目的两个部分了。
这里我们先考虑右半部分的实现:
这部分实际是一个包含两个文本框的Fragment:
先贴出其布局文件:
<?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">
<!--定义一个TextView来显示水果名称-->
<TextView
android:id="@+id/fruit_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="40sp"
android:layout_marginBottom="5dp"
android:padding="20dp"
android:textColor="@color/colorAccent"/>
<!--再定义一个TextView来显示水果的简介-->
<TextView
android:id="@+id/fruit_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:padding="30dp"/>
就定义了两个文本框而已。十分简单,不再赘述。
重点是我们要怎样将布局文件引入Fragment呢?
Android已经为我们做好了安排:
public class FruitTitleAndDescFragment extends android.app.Fragment {
public static final String ITEM_ID = "item_id";
//在全局变量中保存 这个Fragment将要显示的Fruit对象
AllFruit.Fruit fruit;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments().containsKey(ITEM_ID)){
fruit = AllFruit.FRUIT_MAP_ITEMS
.get(getArguments().getInt(ITEM_ID));
}
}
//重写onCreateView()方法,此方法返回的view将作为Fragment显示的组件
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View parentView = inflater.inflate(
R.layout.fruit_name_and_desc,
container,
false);
//fruit为之前定义的全局变量
if (fruit!= null){
//让id为fruit_name的文本框显示水果的name
//注意setText()方法前面有几个括号
((TextView)parentView
.findViewById(R.id.fruit_name))
.setText(fruit.name);
//让id为fruit_desc的文本框显示水果的desc(描述信息)
((TextView)parentView
.findViewById(R.id.fruit_desc))
.setText(fruit.description);
}
return parentView;
}
}
这里我们定义了一个FruitTitleAndDescFragment类,并让其继承自Fragment类。
并且重写了onCreate()方法和onCreateView()方法。
其中,onCreateView()方法就是用来将Fragment的实现类与其布局进行绑定的。
具体方法为:
- 调用onCreateView方法传入的inflater对象的inflate方法,
传入三个参数,其中后两个一般都是container和false,而本例子中的R.layout.fruit_name_and_desc正是我们想要加载的布局。这样我们就得到了根布局,这里我们将其命名为parentView。 - 再通过parentView的findViewById()方法就可以得到各个 子view
- 然后我们让得到的两个文本框分别显示Fruit对象的name和description属性
- 从这里我们也可以看出来 onCreateView()方法和单纯的setContentView()方法的区别:
调用后者只是为了加载布局,而调用前者则是要兼顾加载布局和对布局中组件的行为进行设置。比如此处我们便设置了文本框显示的内容。
有一定英语基础的童鞋应该很好理解:所谓onCreateView所表达的,正是view被Create之后,该做什么! - 最后,别忘了要将parentView返回。
而onCreate()方法则是在创建Fragment对象时被调用。
因为与后面的逻辑联系较紧密,因此其中的代码我们稍后再作讨论。
第三步:
现在我们再来考虑左半部分的实现。
左半部分是一个列表。可能有童鞋的第一反应就是用ListView来实现。但其实Android中已经为我们准备好了一个ListFragment类。让我们的活动继承自这个类,就能轻松实现列表的形式:
//直接继承自ListFragment
public class FruitListFragment extends ListFragment{
//接口的实例
private Callback myCallback;
//在FruitListFragment中定义一个接口
public interface Callback{
void onItemClicked (Integer id);
}//接口结束
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//为该ListFragment设置Adapter
setListAdapter(new ArrayAdapter<AllFruit.Fruit>(
getActivity(),
android.R.layout.simple_list_item_activated_1,
android.R.id.text1,
AllFruit.FRUIT_LIST_ITEMS));
}//onCreate()方法结束
// 当Fragment被添加、显示到Activity中时,回调此方法
@Override
public void onAttach(Context context) {
super.onAttach(context);
// 如果Activity没有实现Callback接口,则抛出异常
if (!(context instanceof Callback)){
throw new IllegalStateException(
"FruitListFragment 所在的Activity必须实现Callback接口"
);
}
//把该Activity当做Callback对象
myCallback = (Callback) context;
}//onAttach()结束
// 当该Fragment从它所属的Activity中被删除时回调此方法
@Override
public void onDetach() {
super.onDetach();
// 给myCallback赋空值
myCallback = null;
}//onDetach()结束
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
// 触发myCallback的onItemclicked()方法
myCallback.onItemClicked(AllFruit.FRUIT_LIST_ITEMS.get(position).id);
}//onListItemClick()结束
}
这里我们定义了一个FruitListFragment类。
并重写了四个方法。
下面一一讲解:
- onCreate()方法很好理解:
- 其规定了本ListFragment被创建时应该执行那些操作。
- 这里我们只做了一件事: 就是调用setListAdapter()方法,为要显示的列表指定了Adapter,以绑定数据源。
- 为setListAdapter()方法传入的是ArrayAdapter的实例:
可以看到,ArrayAdapter共有5个重载的构造器,我们选用的正是第五个。
- 第一个参数是context。这个是自然;
第二个参数是resource,其实也就是列表中每一项的Layout;
第三个参数是textViewResourceId,指的是列表中每一项所包含的TextView的样子(我们的水果名正是用TextView显示出来的)。
第二个和第三个参数,在本例中都是引用的Android内置的主题。 - 最后一个参数最重要,因为其提供了数据的来源。这里我们指定为AllFruit.FRUIT_LIST_ITEMS。也就是先前在AllFruit中定义的list集合。
- onAttach()方法在Fragment被添加、显示到Activity中时回调。
- “Attach”表示附着。正体现了Fragment与Activity的联系。
- 可以看到,回调时传入了一个context对象,由于Activity是Context的子类,所以可以此处传入的context对象所指代的,正是该Fragment所依附的Activity。
- 并且这里我们将这个传入的活动赋给了一个接口对象,关于这样做的目的,以及在FruitListFragment内部定义一个名为Callback接口的意义,稍后再做阐释。现在先记住,myCallback是一个全局变量,也就是说它的作用域为整个类。
- onDetach()方法与onAttach()方法正相反
- 所以,正因为在onAttach()方法中将当前与Fragment产生联系的Activity赋给了myCallback。在onDetach()方法中,当Fragment与Activity不再发生联系时,就为myCallback赋空值以示关系的脱离。
- onListItemClick()方法顾名思义,应当在FruitListFragment中的list的子项被点击时被回调。
...
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
// 触发myCallback的onItemclicked()方法
myCallback.onItemClicked(
AllFruit.FRUIT_LIST_ITEMS.get(position).id);
}//onListItemClick()结束
可以看到,我们在其方法体中调用了 Callback 接口中定义的方法:
onItemClicked(Integer id)
旨在设置当list的子项被点击时应该执行的逻辑。
那么问题就来了。
既然onItemClicked(Integer id)方法被定义在接口中,那么它自然是一个抽象方法。
那么这个抽象方法应该由谁来实现呢?当然是由实现了接口的那个类来实现了。
那么又由谁来实现该接口呢?
答案是:MainActivity
(跑得太远,都快忘了还有MainActivity存在了)
那么这就关系到MainActivity存在的初衷了。
我们需要MainActivity来做什么呢?
先看看其布局文件:
<?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:divider="?dividerHorizontal"
android:orientation="horizontal">
<fragment
android:id="@+id/fruit_list" android:name="com.example.feverdg.fragmenttest.FruitListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:id="@+id/fruit_desc_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
</LinearLayout>
可以看到,这其中包括我们事先定义好的FruitListFragment片段。用于显示列表项。
而下面这个却不是fragment标签,而是一个FrameLayout的标签
等等,那我们之前
public class FruitTitleAndDescFragment extends android.app.Fragment { ... }
定义FruitTitleAndDescFragment片段定义了半天是为了什么?
当然是为了显示了。可是为什么不用fragment标签来引用呢?
这是因为我们并不是想要直接将这个类显示在MainActivity中。
而是要通过点击不同的FruitListFragment的子项来动态地生成FruitTitleAndDescFragment 片段。
也正是为了完成这个点击事件的回调方法,我们才定义了Callback接口,并让MainActivity必须实现这个接口:
public class MainActivity extends AppCompatActivity implements FruitListFragment.Callback {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//实现Callback接口中定义的抽象方法
@Override
public void onItemClicked(Integer id) {
//创建一个Bundle,以向因为点击事件而将要生成的Fragment中传入参数
Bundle arg = new Bundle();
arg.putInt(FruitTitleAndDescFragment.ITEM_ID, id);
// 创建FruitTitleAndDescFragment对象
FruitTitleAndDescFragment fragment = new FruitTitleAndDescFragment();
// 向fragment中传入参数
fragment.setArguments(arg);
// 用fragment替换fruit_desc_container中正在显示的Fragment
android.app.FragmentManager fragmentManager = getFragmentManager();
android.app.FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.fruit_desc_container,fragment);
transaction.addToBackStack(null);
transaction.commit();
}
}
可以看到:
- 我们首先创建一个Bundle,以向因为点击事件而将要生成的Fragment中传入参数;
- 然后调用Bundle类的putInt()方法向该Bundle对象中传入数据。
可以看到putInt()方法接收的其实就是键值对: String类型的键,int类型的值。
这里传入的
arg.putInt(FruitTitleAndDescFragment.ITEM_ID, id);
FruitTitleAndDescFragment.ITEM_ID看上去好像很复杂,其实各位童鞋如果记性好的话,会发现这不过个定义在FruitTitleAndDescFragment类中的字符串常量:
public class FruitTitleAndDescFragment extends android.app.Fragment {
public static final String ITEM_ID = "item_id";
...
键值对的键有了,那这个int类型的id值又是打哪儿来呢?
还记得我们在FruitListFragment中重写的onListItemClick()方法吗?
public void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
// 触发myCallback的onItemclicked()方法
myCallback.onItemClicked(AllFruit.FRUIT_LIST_ITEMS
.get(position).id);
}
我们首先用Fruit类对象的list集合 AllFruit.FRUIT_LIST_ITEM 调用get()方法;
为get()方法传入重写onListItemClick()方法时传入的position参数;
这就得到了具体是哪一个Fruit子项被点击,然后再得到该子项的id。
...
// 创建FruitTitleAndDescFragment对象
FruitTitleAndDescFragment fragment = new FruitTitleAndDescFragment();
// 向fragment中传入参数
fragment.setArguments(arg);
// 用fragment替换fruit_desc_container中正在显示的Fragment
android.app.FragmentManager fragmentManager = getFragmentManager();
android.app.FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.fruit_desc_container,fragment);
//将片段加入返回栈,也就是当按下back键后,会显示先前被replace掉的片段
transaction.addToBackStack(null);
transaction.commit();
准备就绪之后,我们便创建一个新的FruitTitleAndDescFragment的对象。并调用setArguments(arg)方法为其指定了参数,也就是那个带着id信息的Bundle对象。
现在知道前面FruitTitleAndDescFragment类中onCreate()方法的作用了吧:
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments().containsKey(ITEM_ID)){
fruit = AllFruit.FRUIT_MAP_ITEMS
.get(getArguments().getInt(ITEM_ID));
}
}
只要是有新的FruitTitleAndDescFragment对象被Create,onCreate()方法就会执行。
然后我们就能够通过Bundle对象的getArguments()和getInt()方法接收到被点击的水果对象的id信息。
最后再以该id为键在之前定义的水果对象的map集合中查到对应的值。
另外,替换Fragment的操作其实很简单:
无非就是先得到FragmentManager,然后再由它创建FragmentTransaction。FragmentTransaction对象再调用replace方法替换和commit方法执行就可以了。
唯一需要注意的就是replace()方法的参数。R.id.fruit_desc_container指的就是FrameLayout的id。也很简单,不再赘述。
当然,最重要的也最精彩的部分还是Callback接口的创建。
其巧妙之处就在于:
- MainActivity实现Callback接口之后,它就可以被视作是一个Callback对象了。
- 所以MainActivity自然能调用接口中的onItemCliked()方法。
- 而这个所谓的MainActivity正是我们念叨了半天的会与被创建出来的FruitTitleAndDescFragment对象不断产生和失去关联的那个Activity。
- 所以我们在onAttach()和onDetach()方法中都可以引用到这个Activity
// 当Fragment被添加、显示到Activity中时,回调此方法
@Override
public void onAttach(Context context) {
super.onAttach(context);
// 如果Activity没实现Callback接口,则抛出异常
if (!(context instanceof Callback)){
throw new IllegalStateException(
"FruitListFragment 所在的Activity必须实现Callback接口"
);
}
//把该Activity当做Callback对象
myCallback = (Callback) context;
}//onAttach()结束
// 当该Fragment从它所属的Activity中被删除时回调此方法
@Override
public void onDetach() {
super.onDetach();
// 给myCallback赋空值
myCallback = null;
}//onDetach()结束
- 而也正是因为我们在onAttach()方法中通过
//把该Activity当做Callback对象
myCallback = (Callback) context;
获得了对MainActivity的引用,我们才得以在onListItemClick()方法中通过myCallback来调用接口中定义的onItemCliked()方法。
- 如此,我们才得以在每次点击FruitListFragment列表中的子项时都能在MainActivity的FrameLayout布局部分生成一个新的FruitTitleAndDescFragment的实例。
- 看上去也就像是,点击左边列表中的水果名,就能在右边列表显示其相关的name和description信息。
--- The End
事无巨细地分析,不知不觉就有点长篇累牍了。但愿能为各位同道中人填点坑吧!
水平有限,难免纰漏文中如有不当之处欢迎批评指正。
诸君共勉:)