I am trying to implement behavior of opening bottom sheet when clicked on overflow menu. ex: expected behavior
I may do this on an activity using onMenuOpened as suggested here,
But I want to do this on fragment.
How to achieve this behavior on a fragment?
I am using single activity pattern and navigation architecture component.
Create a interface which will be implemented by your Fragment's
ex:
public interface OnMenuOpenListener(){
boolean onMenuOpened();
}
public class MyFragment extends Fragment implements OnMenuOpenListener{
#Override
public boolean onMenuOpened(){
//open bottom sheet here
}
}
public class MyActivity extends Activity{
#Override
public boolean onMenuOpened(int featureId, Menu menu) {
if(featureId == AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR && menu != null){
//overflow menu clicked, put code here...
// As you are using navigation component
Fragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host);
//MyFragment
Fragment fragment=navHostFragment.getChildFragmentManager().getFragments().get(0);
if(fragment instanceof OnMenuOpenListener){
((OnMenuOpenListener)fragment).onMenuOpened()
return false;
}
}
return super.onMenuOpened(featureId, menu);
}
}
As Support Action Bar is attached to Activity All the event's are captured by Activity all you need to do is get the Fragment which need's the event and trigger the call using a call back.If you return false onMenuOpened will not open the overflow menu and will trigger bottom sheet menu from your fragment.
P.S- I have not written the code in Editor so might have some error's but you must have got the idea.
Reference:
https://stackoverflow.com/a/51732378/7972699
As discussed here opening bottom sheet when clicked on the overflow menu is bad UX.
Why?
Quoting from the post
Because user have to reach the top of the screen to click the oveflow
menu, then go back to the bottom to click desired action which is on
the bottom sheet.
-
According to Fitt's Law - The time to acquire a target is a function
of the distance to and size of the target. I agree that I think
distance between the menu and the bottom sheet is substantial. This
solution allows placing a lot options in one place.
-
it also doesn't match the user expectation since people are used to
the overflow menu opening in a different manner.
If you have a top action bar, use usual context menu. If you have a bottom action bar you may use bottom sheet.
**You can try the following steps to open bottom sheet dialog:**
1. Just make a function inside Activity where the fragment is replace
public Fragment getCurrentFragment() {
return getSupportFragmentManager().findFragmentById(R.id.frameContainer);
}
Fragment fragment = getCurrentFragment();
if (fragment != null) {
if (fragment instanceof RequiredFragment) {
RequiredFragment.openBottumSheetDialog();
}
}
2. In Side RequiredFragment get your function from activity:
private BottomSheetDialog mBottomSheetDialogFragment;
private void showBottomSheetFilter() {
if (mBottomSheetDialogFragment == null) {
mBottomSheetDialogFragment = mBottomSheetDialogFragment.newInstance(feedSection);
mBottomSheetDialogFragment.setCallBackListener(new OnFeedsTypeSelectedListener() {
#Override
public void onFeedsTypeSelected(int contentType) {
filterByContentTypeId(contentType);
}
}
mBottomSheetDialogFragment.show(getChildFragmentManager(),
mBottomSheetDialogFragment.getTag());
}
3. Create a BottomSheetDialog Dialog fragment.
public class BottomSheetDialog extends BottomSheetDialogFragment {
private String[] feedsFilter;
private ListView listView;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
feedsFilter = getResources().getStringArray(R.array.ideas_filter);
}
#Override
public void setupDialog(final Dialog dialog, int style) {
super.setupDialog(dialog, style);
View contentView = View.inflate(getContext(), R.layout.dialog_idea_filter_bottom_sheet, null);
dialog.setContentView(contentView);
listView = (ListView) contentView.findViewById(R.id.listView);
ArrayAdapter < String > adapter = new ArrayAdapter < String > (getActivity(),
android.R.layout.simple_list_item_1, feedsFilter);
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView < ? > parent, View view,
int position, long id) {
if (onFeedsTypeSelected != null) {
onIdeaTypeSelectedListenonFeedsTypeSelecteder.onFeedsTypeSelected(feedsFilter[position]);
}
dismiss();
}
});
}
public void setCallBackListener(onFeedsTypeSelected SelectedListener onFeedsTypeSelected) {
this.onIdeaTypeSelectedLionFeedsTypeSelectedstener = onFeedsTypeSelected;
}
}
Related
I am making an Android application which has one activity and many fragments. The activity contains a bottom app bar and that bottom bar has a navigation icon in it. Like this:
<com.google.android.material.bottomappbar.BottomAppBar
android:id="#+id/bottom_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:backgroundTint="#color/colorbottomappbar"
app:fabAlignmentMode="center"
app:navigationIcon="#drawable/ic_menu_green_24dp">
</com.google.android.material.bottomappbar.BottomAppBar>
This navigation menu icon will be shown in every fragment. However, in some fragments I want to change that navigation icon in the bottom app bar to back button/icon. How can I achieve this? Also, currently I handle the navigation icon click in the main activity. How can I handle the click in the case of the back icon? How will it know what the current fragment is and how can I determine which fragment the back icon leads to?
If you look at the documentation, you'll see that BottomAppBar extends from Toolbar and it has an inherited method called setNavigationIcon(int res).
You can implement an interface that your main Activity implements like so:
interface FramentChangedListener {
void onFragmentChanged(int type);
}
Your activity would do something like this:
public class MainActivity extends Activity implements FragmentChangedListener {
// This will keep track of what is currently shown
private int current = 0;
#Override
public void onFragmentChanged(int type) {
if (type == FirstFragment.SOME_TYPE) {
// Update the current fragment value, we're associating each fragment
// with an int value.
current = type;
bottomAppBar.setNavigationIcon(R.drawable.your_back_icon);
}
}
...
}
In your fragment you would do something like this:
public class FirstFragment extends Fragment {
private FragmentChangedListener listener;
public static final int SOME_TYPE = 1;
#Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceOf FragmentChangedListener) {
// context in this case is your activity, which implements FragmentChangedListener
listener = (FragmentChangedListener) context;
// You can call the listener now
listener.onFragmentChanged(SOME_TYPE);
}
}
}
In your Activity, add a listener to the BottomAppBar via setNavigationOnClickListener and whenever you receive the navigation icon event, you can check against the current value that we defined.
I have 10 different fragments in my application. I need to hide Navigation drawer (Drawer Layout) in few fragments, how can I access Drawer Layout from a fragment and hide it? I know we need to use in activity mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); but how to do it in fragments?
You could do something like this in your Fragment:
private MainActivity main;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
main = (MainActivity) activity;
}
You definitely should avoid this!
A mutch better solution would be to use an Interface to communicate between your Main and the Fragment. You will end up with something like this:
public interface MyInterface {
public void lockDrawer();
public void unlockDrawer();
}
Main:
public class DetailViewActivity extends AppCompatActivity implements MyInterface {
#Override
public void lockDrawer() {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
}
#Override
public void unlockDrawer() {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
}
}
Fragment:
public class MyFragment extends Fragment {
private MyInterface myInterface;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
myInterface = (MyInterface) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement MyInterface");
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
// Inflate the layout for this fragment
myInterface.lockDrawer();
return inflater.inflate(R.layout.example_fragment, container, false);
}
#Override
public void onDestroyView() {
super.onDestroyView();
myInterface.unlockDrawer();
}
}
Why this is the best solution: If you do something like ((HomeActivity) mActivity) you will not be able to reuse your Fragment.
There will be a ClassCastException. In order to reuse your Fragment you should use an Interface instead of casting you MainActivity. So every Activity which will use
your Frament can simply implement this Interface. Even if there's no DrawerLayout you can use it. So the big effort is reusability.
KOTLIN SOLUTION WITH NAVIGATION COMPONENT:
If you use a navigation component (one main activity with multiple fragment destinations) then you need to use addOnDestinationChangedListener to handle which fragment will you show and on which will hide your navigation view inside drawer layout.
Here you can see how to start with the navigation component and here is about the setDrawerLockMode method.
So your code will look something like this:
val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
val navController = findNavController(R.id.nav_host_fragment)
navController.addOnDestinationChangedListener{_, destination, _ ->
if (destination.id == R.id.nav_fragment1) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
//DRAWER LOCKED IN fragment1
} else if (destination.id == R.id.nav_fragment2) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
//DRAWER UNLOCKED IN fragment2
} else {.....
}
}
This part of the code you can put in the onCreate() method in your MainActivity.
The easiest way to make a Navigation Drawer Activity is automatically in your android studio. Just follow File -> New -> Activity -> Navigation Drawer Activity.
You can do this by following way -
Write one public method inside your activity as follows -
public void enableDisableDrawer(int mode) {
if (mDrawerLayout != null) {
mDrawerLayout.setDrawerLockMode(mode);
}
}
and then inside fragment's onResume you can call this and change Drawer lock mode as required -
((HomeActivity) mActivity).enableDisableDrawer(DrawerLayout.LOCK_MODE_UNLOCKED);
OR
((HomeActivity) mActivity).enableDisableDrawer(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
where mActivity is my activity reference.
This way is working for me.
You can use this method to lock or unlock the drawer: DrawerLayout.setDrawerLockMode(...). (There are also two other versions of this method to specify a lock mode for specific drawers.) To lock, use DrawerLayout.LOCK_MODE_LOCKED_CLOSED; to unlock, use DrawerLayout.LOCK_MODE_UNLOCKED.
If you are using the ActionBarDrawerToggle, you need to add some extra code to prevent the drawer from opening when they click the ActionBarDrawerToggle if you've locked the drawer.
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// check lock mode before passing to ActionBarDrawerToggle
// I assume your drawer is on the left; if not, use Gravity.RIGHT
int lockMode = mDrawer.getDrawerLockMode(Gravity.LEFT);
if (lockMode == DrawerLayout.LOCK_MODE_UNLOCKED &&
mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
// Handle your other action bar items...
return super.onOptionsItemSelected(item);
}
refer link for more infolink here
Create two methods in your activity. One for opening the drawer and other for closing it. See below code.
public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
.......
#Override
protected void onCreate(Bundle savedInstanceState) {
.........
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
.........
}
public void openNavigationDrawer() {
mDrawerLayout.openDrawer(Gravity.LEFT);
}
public void closeNavigationDrawer() {
mDrawerLayout.closeDrawer(Gravity.LEFT);
}
public void lockNavigationDrawer() {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
}
public void unLockNavigationDrawer() {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
}
}
Now call the above methods from your fragment like below
((MainActivity)getActivity()).closeNavigationDrawer(); // to close drawer
((MainActivity)getActivity()).openNavigationDrawer(); // to open drawer
((MainActivity)getActivity()).lockNavigationDrawer(); // to lock drawer
((MainActivity)getActivity()).unLockNavigationDrawer(); // to unlock drawer
You can use this method to lock or unlock the drawer: DrawerLayout.setDrawerLockMode(...). (There are also two other versions of this method to specify a lock mode for specific drawers.) To lock, use DrawerLayout.LOCK_MODE_LOCKED_CLOSED; to unlock, use DrawerLayout.LOCK_MODE_UNLOCKED.
If you are using the ActionBarDrawerToggle, you need to add some extra code to prevent the drawer from opening when they click the ActionBarDrawerToggle if you've locked the drawer.
enter code here
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// check lock mode before passing to ActionBarDrawerToggle
// I assume your drawer is on the left; if not, use Gravity.RIGHT
int lockMode = mDrawer.getDrawerLockMode(Gravity.LEFT);
if (lockMode == DrawerLayout.LOCK_MODE_UNLOCKED &&
mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
// Handle your other action bar items...
return super.onOptionsItemSelected(item);
}
I struggled with it for hours: setting DrawerLayout to lock mode simply didn't work for me. Even following your examples.
Finally I came up with this post
DrawerLayout.setDrawerLockMode won't work if you set layout_gravity e.g. to start|bottom in the NavigationView. Just set it to start or end
Hope this helps someone
I'm building a pretty simple app. At it's core are 2 screens:
1) list-screen: a list of items
2) detail-screen: a detailed view of an item
I used one Activity (which extends AppCompatActivity) with an Action-Bar, a Navigation-Drawer and a main-content part (a FrameLayout).
I used 2 different fragments for the 2 screens:
When opening the app I inflate the list-fragment into the main-content part.
When an item in the list is clicked I inflate the detail-fragment into the main-content part and it all works well.
On the detail-screen I want the Action-Bar to display an up-button that goes back to the list-screen.
Considering the fact that I am using fragments, rather than separate activites, how can I achieve that?
You can enable the up button each time the Detail Fragment loads, and disable it whenever any of the other Fragments load.
First define these methods in your Activity, which you can call from the Fragments in order to show/hide the up button:
public void showUpButton() {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
public void hideUpButton() {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
You will probably want to just enable the up button in on Resume() of the detail Fragment (MainActivity is just an example, change to the name of your Activity) .....
#Override
public void onResume() {
super.onResume();
MainActivity activity = (MainActivity)getActivity();
if (activity != null) {
activity.showUpButton();
}
}
Then in the other Fragments:
#Override
public void onResume() {
super.onResume();
MainActivity activity = (MainActivity)getActivity();
if (activity != null) {
activity.hideUpButton();
}
}
The next thing is to make the up button actually go back. First ensure that you're adding the Fragment with the up button to the back stack, and then add this to that Fragment.
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
((MainActivity)getActivity()).onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
Then in the Activity, override onBackPressed() and pop from the back stack if the FragmentManager has any entries:
#Override
public void onBackPressed() {
FragmentManager fragmentManager = getSupportFragmentManager();
if (fragmentManager.getBackStackEntryCount() > 1) {
fragmentManager.popBackStackImmediate();
} else {
super.onBackPressed();
}
}
in my app i am developing an activity using the actionbar in NAVIGATION_MODE_TABS-mode.
Each tab is showing a fragment (list, detail).
Initially the list-tab is visibile.
The list is implementing setMultiChoicheModeListener() and modifies the ActionBar and the title of the activity if one or more items are selected.
How can i reset the title and the ActionBar to the inital value (title and actions) when the user clicks on the detail-tab without deselecting the items?
BTW Target-Platform is > 4.1 and i am not using the support library.
Thanks.
public class MyActivity extends Activity {
#Override
protected void onCreate (Bundle savedInstanceState) {
this.actionBar = getActionBar();
this.actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
....
for (Tab tab : getTabs())
{
//here are two tabs added (List and Detail)
this.actionBar.addTab(tab);
}
....
}
protected class NavigationTabListener implements ActionBar.TabListener {
private Fragment fragment;
....
public void onTabSelected(Tab tab, FragmentTransaction ft) {
ft.replace(newFragmentResourceId, this.fragment);
}
}
}
public class MyListViewFragment extends LinearLayout implements IListViewFragment {
....
#Override
public void initialize() {
inflate(getContext(), listLayoutResourceId, this);
this.myList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL);
this.myList.setMultiChoiceModeListener(new MultiChoiceModeListener() {
....
#Override
public boolean onCreateActionMode(ActionMode mode, Menu menu)
{
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(selectedItemsMenuResourceId, menu);
return true;
}
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked)
{
if (checked)
{
this.numberItemsSelected++;
adapter.setNewSelection(position);
}
else
{
this.numberItemsSelected--;
adapter.removeSelection(position);
}
mode.setTitle(getContext().getResources().getQuantityString(
R.plurals.items_selected, this.numberItemsSelected,
Integer.valueOf(this.numberItemsSelected)));
}
....
}
}
I am trying to implement the MVP pattern, but it's still in evaluation phase. The Activity acts as the presenter, the views are in separate classes.
For each Fragment i am also implementing the MVP apttern, but i think this is not interesting to solve the problem.
Some notes to the classes:
MyActivity creates two fragments (one for List, one for Detail view, the detail view has nothing to do with the selected items).
The initial view of the activity is the fragment with the list.
If the user selects some entries I am updating the action bar and the title through the callback of MultiChoiceModeListener.
But the user can now change the fragment by clicking on the "Detail" tab without deselecting the items or clicking to the new elements in the action bar, the result is that the detail fragment is shown, but the title of the activity is still the one I modified in the MultiChoiceModeListener, and there is also the check mark of the action bar visible (auto created by the system).
So the best way is I think to get somehow the current ActionMode, so I can invoke finish() to "reset" the ActionBar and the title.
Make sure you keep reference of the ActionMode in the ActionMode.Callback methods inside your activity which has the ActionBar.TabListener.
When a new tab is selected just finish the action mode, like:
public void onTabSelected(Tab tab, FragmentTransaction ft) {
if(mActionMode != null){
mActionMode.finish();
}
ft.replace(newFragmentResourceId, this.fragment);
}
Make the ActionMode reference back to null when onDestroyActionMode(ActionMode) is called.
I'm developing for android 3+
In my action bar i have a drop-down list(see how to hide/unhide the actionbar list on android 3? for the dropdown i intend). The problem is i need to do a certain action when the user selects something, but Android calls onNavigationItemSelected() as soons as it draws the view, so no selection actually happened.
How can i detect if the user actually pressed something and it is not a fake call from android ?
public class ListDittaListener implements OnNavigationListener{
private BaseActivity activity;
private ListDittaListener()
{
}
public ListDittaListener(BaseActivity activity)
{
this.activity = activity;
}
#Override
public boolean onNavigationItemSelected(int itemPosition, long itemId)
{
MyApp appState = ((MyApp)this.activity.getApplicationContext());
appState.setDittaSelezionata( (int) itemId);
SharedPreferences settings = this.activity.getSharedPreferences(MyApp.PREFS_NAME, 0);
SharedPreferences.Editor editor = settings.edit();
editor.putInt("ditta_id_selezionata", (int) itemId);
////////restart activity this.activity.recreate();
return false;
}
}
You can easily just ignore the first call to onNavigationItemSelected if you like:
public class Whatever implements OnNavigationListener {
private boolean synthetic = true;
#Override
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
if (synthetic) {
synthetic = false;
return true;
}
// do whatever you really wanted here
}
}
Method onNavigationItemSelected(int itemPosition, long itemId) will be called anyway by the action bar.
What you may want to do is to tell action bar what itemPosition it should pass to the method on the first call. (In other words, to tell action bar what navigation item should be set after activity is created). Here is the code:
mActionBarMenuSpinnerAdapter = ...;
mActionBar = getActionBar();
mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
mActionBar.setListNavigationCallbacks(mActionBarMenuSpinnerAdapter, this);
mActionBar.setSelectedNavigationItem(###your_default_navigation_item_here###);
After doing this you can solve your problem by applying changes in the onNavigationItemSelected(int itemPosition, long itemId) if only itemPosition is different.
The android system will call onNavigationItemSelected(0, 0) after the activity is setup. (Which means later than onResume()).
As other guys mentioned, you'd better not do any hack like ignore first call, otherwise the android system won't call onNavigationItemSelected() again when you select the first index. (The system thought the first item is already selected)
My solution is call actionbar.setSelectedNavigationItem(the real item# you want) after you setup the actionbar. Then the system will call onNavigationItemSelected() twice. First onNavigationItemSelected(0, 0) and then the onNavigationItemSelected(the real item#).
Well I cannot see anything wrong in your current code.
How did you create your dropdown elements. And what element is "select" by Android after the view is created. And what are your doing in your onCreate method where the ActionBar is initialized.
I did it as instructed here and it worked for me:
http://developer.android.com/guide/topics/ui/actionbar.html#Dropdown
I have viewpager with fragments and I need set custom action bar for every fragment in pager
In desired page I have navigation list, fragment fires onNavigationItemSelected automatically when I swipe pages, want to avoid this behavior and run tasks only if I selected nav item manually.
public class MyFragment extends Fragment implements ActionBar.OnNavigationListener {
private boolead fireReady = false;
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
// every time make it false, this method invoked on swipe action
fireReady = false;
if (isVisibleToUser) {
// setup actionbar, you also can setup action bar in activity
String[] array = getActivity().getResources().getStringArray(R.array.users_order);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_spinner_item, array);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
getActivity().getActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
getActivity().getActionBar().setListNavigationCallbacks(adapter, this);
}
}
#Override
public boolean onNavigationItemSelected(int itemPosition, long itemId) {
if (fireReady) {
// task fire only when you directly press navigation item
UsersTask task = new UsersTask(getActivity());
task.setTaskListener(this);
task.execute(usersUrls[itemPosition]);
} else {
// make it true first time when page displayed
fireReady = true;
}
return false;
}
}
}