背景
本来想着是,学了大概一年的Android,也一直是在实验室帮忙修修项目改改BUG,要不就是平常跟着 《第一行代码》《Android编程权威指南》 等参考资料做做小Demo,总的来说,知识都比较碎,而且主观的兴趣没有被积极调动起来。于是想着,自己单独做一个简单点的小应用——平时上课记记简单的便签、存一下签到网址、校园网的登录网址等等,方便生活的同时也是对自己能力的巩固和锻炼吧。这样才能发现之前学习中的漏洞,有很多东西不是想象中的那么简单。
出现问题
做到这一步——为活动设置一个全局的FloatingActiondianjiButton
,想根据当前加载的不同Fragment响应不同事件。这就需要在点击事件中进行判断,当前是哪一个Fragment。
但是在Activity中获取到当前的Fragment实例成了个难题。
尝试解决:使用FragmentManager.findFragmentById()但出现问题
最开始想在Activity中,看看FragmentManager
这个容器有没有什么好方法。
也查询了一些资料找到了findFragmentById()
这哥。这个方法传入的是Fragment显示的容器id,我这里是R.id.myPager(布局组件ViewPager)。通过显示Fragment的容器来获取到当前Fragment的实例。
我这里一共三个Fragment。在点击事件中使用了该方法后,经过调试发现,不论我翻到哪一个Fragment,FragmentManager.findFragmentById()
返回的都是按顺序最后一个Fragment。这就很奇怪。
从源码查找原因
于是我查了的源码:
@Nullable
public Fragment findFragmentById(int id) {
int i;
Fragment f;
for(i = this.mAdded.size() - 1; i >= 0; --i) {
f = (Fragment)this.mAdded.get(i);
if (f != null && f.mFragmentId == id) {
return f;
}
}
if (this.mActive != null) {
for(i = this.mActive.size() - 1; i >= 0; --i) {
f = (Fragment)this.mActive.valueAt(i);
if (f != null && f.mFragmentId == id) {
return f;
}
}
}
return null;
}
可以看出,该方法的返回值都来源于两个for循环中。其中的mAdded和mActive使用ctrl+左键查看可以看到,
final ArrayList<Fragment> mAdded = new ArrayList();
SparseArray<Fragment> mActive;
它们都是Fragment泛型的。大概就是所有已经添加的或者是所有在执行的Fragment的组合(个人理解)。
这时再细看这两个for循环的条件:
for(i = this.mAdded.size() - 1; i >= 0; --i)
for(i = this.mActive.size() - 1; i >= 0; --i)
都是在Array中从前往后找的——从最大下标一直到0。
再看在for中的内含逻辑(两个for中的内含逻辑差不多,在此就分析第一个for的逻辑):
int i;
Fragment f;
for(i = this.mAdded.size() - 1; i >= 0; --i) {
f = (Fragment)this.mAdded.get(i);
if (f != null && f.mFragmentId == id) {
return f;
}
}
由上,首先新建计数器i,紧接着新建一个Fragment对象用于for中代码f = (Fragment)this.mAdded.get(i);
赋值——获取到列表mAdded中的当前Fragment对象。紧接着判断,if (f != null && f.mFragmentId == id)
,如果该对象不为空且其f.mFragmentId
(应该指的是存放该Fragment的容器id)和id
(这个id是从findFragmentById(int id)
中传过来的参数id)。就这样从size()-1一直到0依次判断id值是否相等。
那么就事论事来说,我创建的几个Fragment,用的都是同一个容器——R.id.myPaper,按照源码中这个意思,如果我把R.id.myPaper当作参数传给findFragmentById(int id)
,这三个Fragment无论是谁都可以匹配成功。当然由于findFragmentById(int id)
中是在存储Fragment的列表中从后往前找的,所以对于这三个Fragment,最后一个被添加的,也就是在列表的最末端的,上来就中招——符合f != null && f.mFragmentId == id
,并得到返回return f;
。因此不论切换到哪个Fragment,通过FragmentManager.findFragmentById()
获得的Fragment实例都是活动中按顺序添加的最后一个Fragment的实例。
至于网上也有一些教程就是这么做的,可能是因为我用了ViewPager的原因?至少现在在我这儿这个方法行不通。
解决方案
我们要相信Android为我们封装的Adapter功能的强大。
何必在Activity来回中折腾呢?这种逐个获得Item实例的事情就是dapter最擅长的。我们完全可以在Adapter中找到合适的方法获取我们想要的实例。
设置的ViewPagerAdapter继承的是 FragmentStatePagerAdapter
(Fragment切换之后,它会销毁之前的Fragment,再切换回来时,显示的Fragment是新建的;区别于FragmentPagerAdapter
,切换Fragment时,它只是销毁了该Fragment的视图,但是实例还在FragmentManager中。)
在FragmentStatePagerAdapter中,有一个可重写方法 setPrimaryItem()
,这个方法是干什么的?到Android Developer的官网查了一下对其的解释:
* Called to inform the adapter of
* which item is currently considered to be the "primary",
* that is the one show to the user as the current page.
大致意思就是,该方法告诉适配器哪一个页当前是重要的——对用户来说就是当前页(current page)。简单来说,就是拿到了当前页。 没错,正式我们要找的。
该方法的参数有:
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object)
( @NonNull在这里表示该参数不可为null)
其中object
传来的就是当前执行的Fragment的一个实例,完全可以写出如下的判断逻辑:
if(object instanceof SchoolPage)
{
// Someting to do
}
来判断当前加载的是不是我们想要的实例。
实际操作
对我我这个项目,由于点击按钮是全局的,所以点击事件还是放在MainActivity中。当前执行的Fragment实例,在adapter中的setPrimaryItem()
获取到之后,再传递给MainActiviy。(这里为了避免AndroidStudio报警告,我就在Adapter中为获取当前Fragment实例的全局变量有设置了一个get方法,在MainActivity中调用,而没有使用static变量。)
(在Activity中都是一般的设置,就不上代码了,以下是 ViewPagerAdapter中的操作):
package com.example.myschool.adapters;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.view.ViewGroup;
import java.util.List;
/**
* @author PRETA
* 为了获取当前执行的Fragment实例,特别在该适配器中的setPrimaryItem方法为instantFragment赋值,
* 但是不知道为什么通过getItemPosition中赋值就不行——instantFragment在该方法中复制后在调试的时候就一直报空;
* 不过也有可能是适配器中根本就还没有进行到该方法的执行。getItemPosition官方文档给的解释是——
* 该方法用于查看item的位置是否发生改变。其实这样看来——这些复写的方法中都传过来了很多对象值,真的可以好好地利用一下,
* 何必一根筋地在MainActivity为了获取某些值而反复折腾呢?——其实在Adapter中都帮我们一五一十地设计好了。
*
* 而且再看setPrimaryItem的官方文档介绍:Called to inform the adapter of
* which item is currently considered to be the "primary",
* that is the one show to the user as the current page.
* ——告诉适配器哪一个页是重要的——对用户来说就是当前页(current page)。简单来说,就是拿到了当前页。
* 因此在该方法中获取当前页再合适不过了。再查看super.setPrimaryItem(container, position, object)中的大致逻辑也是如此——获取当前页。
* */
public class ViewPagerAdapter extends FragmentStatePagerAdapter{
private Context mContext;
private FragmentManager manager;
public static Fragment instantFragment;
//private Fragment replaceFragment;
private List<Fragment> tabFragments;
private List<String> tabTitles;
// private int mPosition;
// private boolean canReplace;
public ViewPagerAdapter(Context context, FragmentManager fm, List<Fragment> fragments,
List<String> titles) {
super(fm);
manager = fm;
mContext = context;
tabFragments = fragments;
tabTitles = titles;
}
@Override
public int getItemPosition(@NonNull Object object) {
// 可以即时刷新Fragment
return POSITION_NONE;
}
@Override
public Fragment getItem(int position) {
return tabFragments.get(position);
}
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
instantFragment = (Fragment) object;
super.setPrimaryItem(container, position, object);
}
public Fragment getInstantFragment(){
return instantFragment;
}
@Override
public int getCount() {
return tabFragments.size();
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return tabTitles.get(position);
}
}
在MainActivity中如下调用即可:
ViewPagerAdapter adapter = new ViewPagerAdapter(//省略参数);
......
/**浮动按钮点击事件*/
@Override
public void onClick(View v) {
if(adapter.getInstantFragment() instanceof SchoolPage){
showToast(mContext, "这是学校页面");
}
......
}
这样设置后,点击事件响应正确。