今天查看 Fragment 的官方文档,发现2点值得细究的地方:
- Fragment 和 DetailActivity 均被定义为 static,而非定义在单独 .java 文件中;
- Activity 和 Fragment 之间以及 Fragment 和 Fragment 之间的通信方法;
关于第1点,参见另一篇博文《雪习新知识:一张图看懂 Java 内部类》。
本文探讨第2点,感觉这是一种好的设计思想。
1. 原理
官方示例中,MainActivity 里面有2个Fragment:
- 左边的是文章标题列表 TitleFragment;
- 右边的是文章正文 DetailFragment;
TitleFragment 有成员类 OnArticleSelectedListener 接口,MainActivity 实现该接口,在 TitleFragment 绑定 MainActivity 时让接口指向 MainActivity,TitleFragment 处理点击事件时调用接口的方法,结果就调用 MainActivity 实现接口的方法了,在MainActivity 实现接口的方法接收数据,或接着传递给 DetailFragment 等。
UML 类图
在 MemberClass 的构造方法中实现:
public MemberClass(Host host) {
mListener = (OnClickListener)host;
}
这样,MemberClass 中存在 Host 的引用 mListener,父类类型的 mListener 指向其子类 Host 的对象(其实这就是多态,在运行时自动调用子类实现的方法 onClick()),从而实现调用 Host 中的成员变量和方法,完成通信。
public class MainActivity extends Activity implements TitlesFragment.OnArticleSelectedListener {
···
@Override
public void onArticleSelected(Uri aticleUri) {
}
public static class TitlesFragment extends ListFragment {
OnArticleSelectedListener mListener;
···
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mListener = (OnArticleSelectedListener) activity;
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
mListener.onArticleSelected(noteUri);
}
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri aticleUri);
}
···
}
}
2. 变形
Host 不再实现接口,而在 MemberClass 中增加 setOnClickListener(OnClickListener click); 方法,该方法在 Host 中调用,同样可以使用 Host 中成员变量和方法。
3. 应用
我们应用上面的思想来实现类似大众点评APP底部的标签切换效果,大众点评APP底部的标签如下图。
我们要实现的实际效果如下:
4. 回归
对于第2节提到的变形的方式,感觉好熟悉,翻看之前自己写的代码,恍然大悟:在可点击的按钮上添加监听事件不都是这种方式吗?!
关键点在 SDK 的 View.java 源码:
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
public OnClickListener mListener;
···
public void onKeyUp(int keyCode, KeyEvent event) {
···
return performClick();
···
}
public boolean performClick() {
···
li.mOnClickListener.onClick(this);
···
}
public interface OnClickListener() {
void onClick(View v);
}
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
···
}
当点击事件的最后一个动作“KeyUp”发生时,调用 OnClickListener.onClick(),即触发监听事件。
Button 等控件继承了 View 的 interface OnClickListener 、mOnClickListener 和 setOnClickListener() 等 public 成员,这样就与变形方式保持一致了,其中 MainActivity 是 Host,Button 是 MemberClass。两种方式核心代码对比如下:
public class MainActivity extends Activity implements View.OnClickListener{
···
Button btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(this);
@Override
public void onClick(View v) {
···
}
···
}
或者是
public class MainActivity extends Activity {
···
Button btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
···
}
});
···
}
4. 引申
假如 MemberClass 是一个自定义的 View,很复杂的 View,里面有很多交互事件,包括点击、长按、滑动等等,而且有些交互是要用到 Host 的成员变量。在这种情况下,MemberClass extends ViewGroup implements View.OnClickListener,在 onClick(View v) 方法中调用 Host.setXXXListener(this/new XXXListener()) 即可。
其实,Host.setXXXListener(this) (Host implements XXXListener)和 Host.setXXXListener(new XXXListener()) 是等效的,对于 Host 是透明的,在 Host 看来,MemberClass.mXXXListener.onXXX() 调用的都是在 Host 中实现的 onXXX(){},onXXX(){} 中显然是可以使用 Host 的成员变量的。
对于 Host -> MemberClass(ViewGroup),MemberClass -> View(Buton,ImageView 等),是 2 层完全相同的层级关系,以点击事件为例,事件的传递是从内到外的,view.onClick() -> MemberClass.mXXXListner.onXXX() -> Host.onXXX()
上面说的过于抽象了,举个具体的例子,以微信朋友圈的评论功能为例。
如上图,输入评论的输入框是在 Host 即Activity (当然也可能是 Fragment)中的,而评论最终是要显示在 MembClass 即 ListView 的某个 Item 中,而且输入框中出现“回复xxx”,显然“xxx”是从 Item 中传到 Activity 中的,这就是 Host 和 MemberClass 的双向通信,可以通过本文中的方法解决,具体解决方法参见 《Android 仿微信点赞和评论弹出框》。
5. 另一种通信方式
使用静态嵌套类(static nested class)进行单向通信。
在 Host 中定义静态嵌套类 AbsFragment,然后其他的 Fragment 继承该抽象类,在 AbsFragment 定义各种 getter() 方法,返回 Host 的成员变量,子 Fragment 就能获得 Host 的成员变量了。由于只能从子 Fragment 中获得 Host 的变量,故通信是单向的。
to be continued…