PPartisan
PPartisan

Reputation: 8231

Coordinating Floating-Action Button behaviour across Child Fragments/Activities

I have an issue coordinating the behaviour of a Floating Action Button in an Activity with its children.

In my app, I have an Activity which houses a ViewPager and an xml defined FloatingActionMenu constructed with the following library. The ViewPager contains a flexible number of Fragments, and inside each Fragment is a RecyclerView.

When clicked, the FloatingActionMenu expands into two buttons - one adds an extra page/Fragment to the ViewPager, the other adds an extra row to the RecyclerView on the page of the ViewPager the user is currently on. This feature works.

To hide the button, I use addOnItemTouchListener to the RecyclerView in my child fragments. Then, in OnInterceptTouchEvent, if the swipe gesture is large enough, I use a custom Interface to relay the event back to the hosting Activity, which conducts the animation to hide or show the button. Here is some of my code:

public class Categories extends ActionBarActivity implements
        View.OnClickListener,
        FloatingButtonInterface{

//...
// Final parameter is for the custom Interface
    mAdapter = new CategoriesViewPagerAdapter(mFragmentManager, allCategoriesCursor, this); 
    mViewPager = (ViewPager) findViewById(R.id.categories_view_pager_pager);
    mViewPager.setAdapter(mAdapter);

    menuOriginalXPos = mFloatingActionsMenu.getX();
    menuOriginalYPos = mFloatingActionsMenu.getY();

//...
// onTouchEvent is the imaginative name for my custom Interface method.
    @Override
    public void onTouchEvent(int callBackID) {
        if (callBackID == FloatingButtonInterface.MOTION_DOWN){
            if (mFloatingActionsMenu.getVisibility() == View.GONE) {
                Animation translate = new TranslateAnimation(
                        menuOriginalXPos, menuOriginalXPos, mViewPager.getHeight(), menuOriginalYPos);
                translate.setDuration(250);
                translate.setInterpolator(new LinearInterpolator());
                mFloatingActionsMenu.startAnimation(translate);
                mFloatingActionsMenu.setEnabled(true);
                mFloatingActionsMenu.setVisibility(View.VISIBLE);
            }
        } else if (callBackID == FloatingButtonInterface.MOTION_UP) {
            if (mFloatingActionsMenu.getVisibility() == View.VISIBLE) {
                Animation translate = new TranslateAnimation(
                        menuOriginalXPos, menuOriginalXPos, menuOriginalYPos, mViewPager.getHeight());
                translate.setDuration(250);
                translate.setInterpolator(new LinearInterpolator());
                mFloatingActionsMenu.startAnimation(translate);
                mFloatingActionsMenu.setEnabled(false);
                mFloatingActionsMenu.setVisibility(View.GONE);
            }
        }
    }

Here is my very straight forward interface:

public interface FloatingButtonInterface {

    public static final int MOTION_UP = 4;
    public static final int MOTION_DOWN = MOTION_UP+1;
    public static final int MOTION_NONE = 0;

    public void onTouchEvent(int callBackID);
}

Inside my ViewPager:

public class CategoriesViewPagerAdapter extends FragmentStatePagerAdapter {

private static final String LOG_TAG = "CategoriesViewPagerAd";

private Cursor mCursor;
private FloatingButtonInterface hideMenuInterface;
private FragmentManager mFragmentManager;
private boolean refreshItem = false;

public CategoriesViewPagerAdapter(FragmentManager fm, Cursor data, FloatingButtonInterface hideMenuInterface) {
    super(fm);
    mCursor = data;
    mFragmentManager = fm;
    this.hideMenuInterface = hideMenuInterface;
}

@Override
public Fragment getItem(int i) {
    mCursor.moveToPosition(i);
    int categoryFragmentIdentifier =
            mCursor.getInt(mCursor.getColumnIndex(FilesDatabaseHelper.KEY_ID));
    CategoryFragment frag = CategoryFragment.newInstance(categoryFragmentIdentifier);
    frag.setOnTouchEvent(hideMenuInterface);
    return frag;
}

And finally, one of the ViewPager Fragments:

public class CategoryFragment extends Fragment implements
        RecyclerView.OnItemTouchListener,
        View.OnClickListener{
//...
private static final int TOUCH_SLOP=200;
//...
static CategoryFragment newInstance(int categoryIdentifier){
        CategoryFragment frag = new CategoryFragment();
        Bundle bundle = new Bundle();
        bundle.putInt(CATEGORY_ID, categoryIdentifier);
        frag.setArguments(bundle);

        return frag;
    }
//...
@Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        mRecyclerView.addOnItemTouchListener(this);
//...
    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        switch (e.getAction()){
            case MotionEvent.ACTION_DOWN:
                originalFingerPos = e.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (e.getY() - originalFingerPos > TOUCH_SLOP){
                    if (hideMenuInterface != null) {
                        hideMenuInterface.onTouchEvent(FloatingButtonInterface.MOTION_DOWN);
                    } else {
                        Log.e(LOG_TAG, "hideMenuInterface is null");
                    }
                } else if (e.getY() + TOUCH_SLOP < originalFingerPos ){
                    if (hideMenuInterface != null) {
                        hideMenuInterface.onTouchEvent(FloatingButtonInterface.MOTION_UP);
                    } else {
                        Log.e(LOG_TAG, "hideMenuInterface is null");
                    }
                }
        }
        return false;
    }

The main problem I am having relates to the code I use to auto-hide the button when a user swipes up or down on the screen after a configuration change. If I change the screen orientation on a test device, when I attempt to hide the button with a swipe, some of the Fragments don't respond, and when I check LogCat I can see that hideMenuInterface is null. What can I do to address this? Why does it only effect a few of the fragments and not all of them? If I swipe to the final fragment in the ViewPager and then back again, the problem has resolved itself - likely because the fragment has been recreated.

Thanks for any insight you can offer! I only included code I felt would be relevant, as otherwise there'd be pages, but please ask if you think anything else would help.

Upvotes: 1

Views: 1892

Answers (1)

PPartisan
PPartisan

Reputation: 8231

I found an answer to this issue in the Android Developer docs. Button behaviour is far more reliable after following the instructions on this page.


Edit:

To expand on my previous answer, I applied the information from the documentation in the following way:

I altered CategoryFragment (the fragment that occupies each ViewPager item) by adding an inner interface called FloatingButtonInterface, and assigned a value to a FloatingButtonInterface variable during onAttach():

public class CategoryFragment extends Fragment implements RecyclerView.OnItemTouchListener {
//...
    private static final int TOUCH_SLOP = 200;
    private FloatingButtonInterface floatingButtonInterface;
//...
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        floatingButtonInterface = (FloatingButtonInterface) activity;
    }
//...
    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                originalFingerPos = e.getY();
                return false;
            case MotionEvent.ACTION_MOVE:
                if (e.getY() - originalFingerPos > TOUCH_SLOP) {
                    floatingButtonInterface.onTouchEvent(FloatingButtonInterface.MOTION_DOWN);
                    return false;
                } else if (e.getY() + TOUCH_SLOP < originalFingerPos) {
                    floatingButtonInterace.onTouchEvent(FloatingButtonInterface.MOTION_UP);
                    return false;
                }
                return false;
            }
            return false;
        }
//...
public interface FloatingButtonInterface {

    public static final int MOTION_UP = MOTION_NONE + 4;
    public static final int MOTION_DOWN = MOTION_UP + 1;
    public static final int MOTION_NONE = 0;

    public void onTouchEvent(int callBackId);
}

The only other change is inside the ViewPagerAdapter. As CategoryFragment is responsible for assigning the interface, any code relating to this in the adapter can be removed, so that the adapter now looks like:

public class CategoriesViewPagerAdapter extends FragmentStatePagerAdapter {

private static final String LOG_TAG = "CategoriesViewPagerAd";

private Cursor mCursor;
private FloatingButtonInterface hideMenuInterface;
private FragmentManager mFragmentManager;
private boolean refreshItem = false;

public CategoriesViewPagerAdapter(FragmentManager fm, Cursor data) {
    super(fm);
    mCursor = data;
    mFragmentManager = fm;
}

@Override
public Fragment getItem(int i) {
    mCursor.moveToPosition(i);
    int categoryFragmentIdentifier =
            mCursor.getInt(mCursor.getColumnIndex(FilesDatabaseHelper.KEY_ID));
    CategoryFragment frag = CategoryFragment.newInstance(categoryFragmentIdentifier);
    return frag;
}

Upvotes: 1

Related Questions