Reputation: 8231
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
Fragment
s:
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
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