As the title says, I'm trying to add long press functionality to items in my app's navigation drawer. These items are added dynamically (not inflated from navigation_drawer_menu.xml), so I can't solve this by specifying some attributes in the xml file.
I've looked at several questions on Stackoverflow, particularly this one: How to set a long click listener on a MenuItem (on a NavigationView)?. I've implemented the setActionView solution, but I end up getting a blank button on the right edge of the nav drawer item. When I long press the text, nothing happens. When I long press the little blank button, I get what I want.
How can I set a OnLongClickListener for the whole menuItem, and not just for its (I'm assuming it's a button) on its right side? Thank you for reading, and if any more info is needed, I'm happy to help you help me:)
I had the same problem and managed to solve it by digging into the NavigationView's view hierarchy.
The first step is to understand the view hierarchy of your NavigationView. You can use the piece of code from this post to print out the NavigationView's view hierarchy.
Then start digging for the view you're targeting. In my case:
#Override
public boolean onPrepareOptionsMenu(Menu menu) {
// Start digging into the view hierarchy until the correct view is found
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
ViewGroup navigationMenuView = (ViewGroup)navigationView.getChildAt(0);
ViewGroup navigationMenuItemView = (ViewGroup)navigationMenuView.getChildAt(2);
View appCompatCheckedTextView = navigationMenuItemView.getChildAt(0);
// Attach click listener
appCompatCheckedTextView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
Log.i("test", "LONG CLICK");
return true;
}
});
return super.onPrepareOptionsMenu(menu);
}
We choose to do these things not because they are easy, but because they are hard. And because my UI would have come unglued if I couldn't do this.
Import the NavigationItemLongPressInterceptor class given below into your project.
Menu items for the NavigationView are declared as normal, with two additional attributes to add long-press behaviour.
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:showIn="navigation_view">
<group android:id="#+id/home_views">
<item
android:id="#+id/nav_item_1"
android:icon="#drawable/ic_item_1"
android:title="Item 1"
android:checkable="true"
app:actionViewClass=
"com.twoplay.netplayer.controls.NavigationItemLongPressInterceptor"
app:showAsAction="always"
/>
<item
android:id="#+id/nav_item_2"
android:icon="#drawable/ic_item_2"
android:title="Item 2"
android:checkable="true"
app:actionViewClass=
"com.twoplay.netplayer.controls.NavigationItemLongPressInterceptor"
app:showAsAction="always"
/>
</group>
</menu>
Add an implementation for NavigationItemLongPressInterceptor.OnNavigationItemLongClickListener to your activity,
and implement the onNavigationItemLongClick method:
public class MainActivity extends MediaActivity
implements
NavigationView.OnNavigationItemSelectedListener,
NavigationItemLongPressInterceptor.OnNavigationItemLongClickListener
. . .
#Override
public void onNavigationItemLongClick(
NavigationItemLongPressInterceptor.SelectedItem selectedItem,
View view)
{
// supply your NavigationView as an argument.
int menItemId = selectedItem.getItemId(mNavigationView);
switch (id) {
...
case R.id.nav_local_device:
case R.id.nav_upnp_devices: {
showNavigationItemSetAsHomePopupMenu(id,view);
}
break;
}
}
}
You probably have to add com.twoplay.netplayer.controls.NavigationItemLongPressInterceptor to your proguard rules.
NavigationItemLongPressInterceptor.java:
package com.twoplay.netplayer.controls;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.util.AttributeSet;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import com.google.android.material.navigation.NavigationView;
import androidx.annotation.Nullable;
/**
* Attach a long-click handler to a menu item in a NavigationView menu.
*
* To handle long-click of a Navigator menu item, declare the item as normal, and append
* app:actionViewClass="com.twoplay.netplayer.controls.NavigationItemLongPressInterceptor" and
* app:showAsAction="always" attributes:
*
* <menu xmlns:tools="http://schemas.android.com/tools"
* xmlns:app="http://schemas.android.com/apk/res-auto"
* >
*
* <group android:id="#+id/home_views">
* <item
* android:id="#+id/nav_item_1"
* android:icon="#drawable/ic_item_1"
* android:title="Item 1"
* android:checkable="true"
* app:actionViewClass=
* "com.twoplay.netplayer.controls.NavigationItemLongPressInterceptor"
* app:showAsAction="always"
* </item>
* </group>
*
* Your Application class must implement <L NavigationItemLongPressInterceptor.OnNavigationItemLongClickListener/>
* in order to receive notification of long pressed menu items.
*
* You can retrieve the item id of the menu by calling <L SelectedItem.getItemId/> on the
* <L SelectedItem/> provided as an argument to <L NavigationItemLongPressInterceptor.onNavigationItemLongClick/>
*
* />
*
*/
#SuppressWarnings("unused")
public class NavigationItemLongPressInterceptor extends View {
public NavigationItemLongPressInterceptor(Context context) {
super(context);
init();
}
public NavigationItemLongPressInterceptor(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public NavigationItemLongPressInterceptor(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init()
{
setVisibility(View.INVISIBLE);
setLayoutParams(new ViewGroup.LayoutParams(0,0));
}
public interface OnNavigationItemLongClickListener {
void onNavigationItemLongClick(SelectedItem itemHandle, View view);
}
public static class SelectedItem {
private final View actionView;
private SelectedItem(View actionView) {
this.actionView = actionView;
}
public int getItemId(NavigationView navigationView)
{
return getItemId(navigationView.getMenu());
}
private int getItemId(Menu menu) {
for (int i = 0; i < menu.size(); ++i) {
MenuItem item = menu.getItem(i);
if (item.getActionView() == actionView) {
return item.getItemId();
}
SubMenu subMenu = item.getSubMenu();
if (subMenu != null) {
int itemId = getItemId(subMenu);
if (itemId != -1) {
return itemId;
}
}
}
return -1;
}
}
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
View parent = getMenuItemParent();
parent.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
OnNavigationItemLongClickListener receiver = getReceiver();
if (receiver == null)
{
throw new RuntimeException("Your main activity must implement NavigationViewLongPressInterceptorView.OnNavigationItemLongClickListener");
}
View parent = getMenuItemParent();
receiver.onNavigationItemLongClick(
new SelectedItem(NavigationItemLongPressInterceptor.this),parent);
return true;
}
});
}
private Activity getActivity() {
Context context = getContext();
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return (Activity)context;
}
context = ((ContextWrapper)context).getBaseContext();
}
return null;
}
private OnNavigationItemLongClickListener getReceiver() {
Activity activity = getActivity();
if (activity == null) return null;
if (activity instanceof OnNavigationItemLongClickListener)
{
return (OnNavigationItemLongClickListener)activity;
}
return null;
}
private View getMenuItemParent() {
View parent = (View)getParent();
while (true)
{
if (parent.isClickable())
{
return parent;
}
parent = (View) parent.getParent();
}
}
}
This code targets androidx, but it can be trivially backported to AppCompat. Just delete the androidx imports, and replace them with corresponding AppCompat imports. Hopefully older versions of NavigationView lay out actionViews the same way.
Tested with 'androidx.appcompat:appcompat:1.0.2', 'com.google.android.material:material:1.0.0'. I'm reasonably confident that it is version-safe. Let me know if getMenuItemParent()needs adjustments for other AppCompat versions, and I will incorporate changes here.
I had to do similar and went with onItemLongClick
https://developer.android.com/reference/android/widget/AdapterView.OnItemLongClickListener.html
my implementation was a little different though as I had an expandable list in the navigation drawer and each item had to have an onClick as well as an onLongClick method call, each item on the navigation drawer was also added dynamically by the user of the app.
listView.setOnItemLongClickListener(new View.OnItemLongClickListener() {
#Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(HomeActivity.this, id +"", Toast.LENGTH_LONG).show();
return true;
}
});
Related
I have implemented the new architecture component on my android app, but unfortunately handling the states of these fragments has become a nightmare for me. Whenever I press the icon of the fragment, the fragment is recreated every time I navigate. How can I handle this or rather save these fragments states?
Here is my main activity handling the five fragments:
public class MainActivityCenterofInformation extends AppCompatActivity {
BottomNavigationView bottomNavigationView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate (savedInstanceState);
setContentView (R.layout.activity_maincict);
setUpNavigation ();
}
public void setUpNavigation(){
bottomNavigationView = findViewById (R.id.bottom_nav_cict);
NavHostFragment navHostFragment = (NavHostFragment)getSupportFragmentManager ()
.findFragmentById (R.id.nav_host_fragment_cict);
NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());
}
//adding animations to the fragment
}
I can't read Kotlin so please direct me to Java, thanks.
TL;DR: skip to the JUST SHOW ME THE STEPS ALREADY !!! section
That is the normal behaviour of the fragments. They are suppose to be recreated every time they are removed or replaced and you are suppose to restore their states using onSaveInstanceState.
Here is a nice article that describes how to do it : Saving Fragment States
Other than that you can use View Model which is the part of the following recommended android architecture. They are a great way to retain and restore UI data.
You can learn how to implement this architecture by following this step by step code lab
EDIT : Solution
Note : The solution assumes one doesn't wants to use ViewModels and simply wants to hide or show fragments using the Navigation UI
It covers the following points
Keeping fragment alive during navigation
Implement custom navigation behavior on back key press (Optional)
Background :
Android Navigation component has a NavController class that you can use to navigate to different destinations. NavController uses a Navigator that actually does the navigation. Navigator is an abstract class and anyone can extend/inherit it to provide custom navigation depending on the type of destination. When using fragments as destinations the NavHostFragment uses a FragmentNavigator whose default implementation replaces the fragments whenever we navigate using FragmentTransaction.replace() which completely destroys the previous fragment and adds a new fragment. So we have to create our own navigator and instead of using FragmentTransaction.replace() we will use a combination of FragmentTransaction.hide() and FragmentTransaction.show() to avoid fragments from being destroyed.
Default behavior of Navigation UI :
By default whenever you navigate to any other fragment other than the home fragment they won't get added to backstack so lets say if you select fragments in the following order
A -> B -> C -> D -> E
your back stack will have only
[A, E]
as you can see the fragments B, C, D weren't added to backstack so pressing back press will always get you to fragment A which is the home fragment
The behavior we want for now :
We want a simple yet effective behavior. We wan't all fragments to get added to backstack but if the fragment is already in backstack we want to pop all fragments upto the selected fragment.
Lets say I select fragment in following order
A -> B -> C -> D -> E
the backstack should also be
[A, B, C, D, E]
upon pressing back only the last fragment should be popped and backstack should be like this
[A, B, C, D]
but if we navigate to lets say fragment B, since B is already in the stack then all the fragments above B should be popped and our backstack should look like this
[A, B]
I hope this behavior makes sense. This behavior is easy to implement using global actions as you will see below and is better than the default one.
OK Hotshot! now what ? :
Now we have two options
extend FragmentNavigator
copy/paste FragmentNavigator
Well I personally wanted to just extend FragmentNavigator and override navigate() method but since all its member variables are private I couldn't implement proper navigation.
So I decided to copy paste the entire FragmentNavigator class and just change the name in entire code from "FragmentNavigator" to whatever I want to call it.
JUST SHOW ME THE STEPS ALREADY !!! :
Create custom navigator
Use custom tag
Add global actions
Use global actions
Add the custom navigator to the NavController
STEP 1: Create custom navigator
Here is my custom navigator called StickyCustomNavigator. All the code is same as FragmentNavigator except the navigate() method. As you can see it uses hide() , show() and add() method instead of replace(). The logic is simple. Hide the previous fragment and show the destination fragment. If this is our first time going to a specific destination fragment then add the fragment instead of showing it.
#Navigator.Name("sticky_fragment")
public class StickyFragmentNavigator extends Navigator<StickyFragmentNavigator.Destination> {
private static final String TAG = "StickyFragmentNavigator";
private static final String KEY_BACK_STACK_IDS = "androidx-nav-fragment:navigator:backStackIds";
private final Context mContext;
#SuppressWarnings("WeakerAccess") /* synthetic access */
final FragmentManager mFragmentManager;
private final int mContainerId;
#SuppressWarnings("WeakerAccess") /* synthetic access */
ArrayDeque<Integer> mBackStack = new ArrayDeque<>();
#SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mIsPendingBackStackOperation = false;
private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener =
new FragmentManager.OnBackStackChangedListener() {
#SuppressLint("RestrictedApi")
#Override
public void onBackStackChanged() {
// If we have pending operations made by us then consume this change, otherwise
// detect a pop in the back stack to dispatch callback.
if (mIsPendingBackStackOperation) {
mIsPendingBackStackOperation = !isBackStackEqual();
return;
}
// The initial Fragment won't be on the back stack, so the
// real count of destinations is the back stack entry count + 1
int newCount = mFragmentManager.getBackStackEntryCount() + 1;
if (newCount < mBackStack.size()) {
// Handle cases where the user hit the system back button
while (mBackStack.size() > newCount) {
mBackStack.removeLast();
}
dispatchOnNavigatorBackPress();
}
}
};
public StickyFragmentNavigator(#NonNull Context context, #NonNull FragmentManager manager,
int containerId) {
mContext = context;
mFragmentManager = manager;
mContainerId = containerId;
}
#Override
protected void onBackPressAdded() {
mFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener);
}
#Override
protected void onBackPressRemoved() {
mFragmentManager.removeOnBackStackChangedListener(mOnBackStackChangedListener);
}
#Override
public boolean popBackStack() {
if (mBackStack.isEmpty()) {
return false;
}
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already"
+ " saved its state");
return false;
}
if (mFragmentManager.getBackStackEntryCount() > 0) {
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
mIsPendingBackStackOperation = true;
} // else, we're on the first Fragment, so there's nothing to pop from FragmentManager
mBackStack.removeLast();
return true;
}
#NonNull
#Override
public StickyFragmentNavigator.Destination createDestination() {
return new StickyFragmentNavigator.Destination(this);
}
#NonNull
public Fragment instantiateFragment(#NonNull Context context,
#SuppressWarnings("unused") #NonNull FragmentManager fragmentManager,
#NonNull String className, #Nullable Bundle args) {
return Fragment.instantiate(context, className, args);
}
#Nullable
#Override
public NavDestination navigate(#NonNull StickyFragmentNavigator.Destination destination, #Nullable Bundle args,
#Nullable NavOptions navOptions, #Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
final FragmentTransaction ft = mFragmentManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
String tag = Integer.toString(destination.getId());
Fragment primaryNavigationFragment = mFragmentManager.getPrimaryNavigationFragment();
if(primaryNavigationFragment != null)
ft.hide(primaryNavigationFragment);
Fragment destinationFragment = mFragmentManager.findFragmentByTag(tag);
if(destinationFragment == null) {
destinationFragment = instantiateFragment(mContext, mFragmentManager, className, args);
destinationFragment.setArguments(args);
ft.add(mContainerId, destinationFragment , tag);
}
else
ft.show(destinationFragment);
ft.setPrimaryNavigationFragment(destinationFragment);
final #IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size() > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
mFragmentManager.popBackStackImmediate(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()), 0);
mIsPendingBackStackOperation = false;
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
mIsPendingBackStackOperation = true;
isAdded = true;
}
if (navigatorExtras instanceof FragmentNavigator.Extras) {
FragmentNavigator.Extras extras = (FragmentNavigator.Extras) navigatorExtras;
for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
#Override
#Nullable
public Bundle onSaveState() {
Bundle b = new Bundle();
int[] backStack = new int[mBackStack.size()];
int index = 0;
for (Integer id : mBackStack) {
backStack[index++] = id;
}
b.putIntArray(KEY_BACK_STACK_IDS, backStack);
return b;
}
#Override
public void onRestoreState(#Nullable Bundle savedState) {
if (savedState != null) {
int[] backStack = savedState.getIntArray(KEY_BACK_STACK_IDS);
if (backStack != null) {
mBackStack.clear();
for (int destId : backStack) {
mBackStack.add(destId);
}
}
}
}
#NonNull
private String generateBackStackName(int backStackIndex, int destId) {
return backStackIndex + "-" + destId;
}
private int getDestId(#Nullable String backStackName) {
String[] split = backStackName != null ? backStackName.split("-") : new String[0];
if (split.length != 2) {
throw new IllegalStateException("Invalid back stack entry on the "
+ "NavHostFragment's back stack - use getChildFragmentManager() "
+ "if you need to do custom FragmentTransactions from within "
+ "Fragments created via your navigation graph.");
}
try {
// Just make sure the backStackIndex is correctly formatted
Integer.parseInt(split[0]);
return Integer.parseInt(split[1]);
} catch (NumberFormatException e) {
throw new IllegalStateException("Invalid back stack entry on the "
+ "NavHostFragment's back stack - use getChildFragmentManager() "
+ "if you need to do custom FragmentTransactions from within "
+ "Fragments created via your navigation graph.");
}
}
#SuppressWarnings("WeakerAccess") /* synthetic access */
boolean isBackStackEqual() {
int fragmentBackStackCount = mFragmentManager.getBackStackEntryCount();
// Initial fragment won't be on the FragmentManager's back stack so +1 its count.
if (mBackStack.size() != fragmentBackStackCount + 1) {
return false;
}
// From top to bottom verify destination ids match in both back stacks/
Iterator<Integer> backStackIterator = mBackStack.descendingIterator();
int fragmentBackStackIndex = fragmentBackStackCount - 1;
while (backStackIterator.hasNext() && fragmentBackStackIndex >= 0) {
int destId = backStackIterator.next();
try {
int fragmentDestId = getDestId(mFragmentManager
.getBackStackEntryAt(fragmentBackStackIndex--)
.getName());
if (destId != fragmentDestId) {
return false;
}
} catch (NumberFormatException e) {
throw new IllegalStateException("Invalid back stack entry on the "
+ "NavHostFragment's back stack - use getChildFragmentManager() "
+ "if you need to do custom FragmentTransactions from within "
+ "Fragments created via your navigation graph.");
}
}
return true;
}
#NavDestination.ClassType(Fragment.class)
public static class Destination extends NavDestination {
private String mClassName;
public Destination(#NonNull NavigatorProvider navigatorProvider) {
this(navigatorProvider.getNavigator(StickyFragmentNavigator.class));
}
public Destination(#NonNull Navigator<? extends StickyFragmentNavigator.Destination> fragmentNavigator) {
super(fragmentNavigator);
}
#CallSuper
#Override
public void onInflate(#NonNull Context context, #NonNull AttributeSet attrs) {
super.onInflate(context, attrs);
TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.FragmentNavigator);
String className = a.getString(R.styleable.FragmentNavigator_android_name);
if (className != null) {
setClassName(className);
}
a.recycle();
}
#NonNull
public final StickyFragmentNavigator.Destination setClassName(#NonNull String className) {
mClassName = className;
return this;
}
#NonNull
public final String getClassName() {
if (mClassName == null) {
throw new IllegalStateException("Fragment class was not set");
}
return mClassName;
}
}
public static final class Extras implements Navigator.Extras {
private final LinkedHashMap<View, String> mSharedElements = new LinkedHashMap<>();
Extras(Map<View, String> sharedElements) {
mSharedElements.putAll(sharedElements);
}
#NonNull
public Map<View, String> getSharedElements() {
return Collections.unmodifiableMap(mSharedElements);
}
public static final class Builder {
private final LinkedHashMap<View, String> mSharedElements = new LinkedHashMap<>();
#NonNull
public StickyFragmentNavigator.Extras.Builder addSharedElements(#NonNull Map<View, String> sharedElements) {
for (Map.Entry<View, String> sharedElement : sharedElements.entrySet()) {
View view = sharedElement.getKey();
String name = sharedElement.getValue();
if (view != null && name != null) {
addSharedElement(view, name);
}
}
return this;
}
#NonNull
public StickyFragmentNavigator.Extras.Builder addSharedElement(#NonNull View sharedElement, #NonNull String name) {
mSharedElements.put(sharedElement, name);
return this;
}
#NonNull
public StickyFragmentNavigator.Extras build() {
return new StickyFragmentNavigator.Extras(mSharedElements);
}
}
}
}
STEP 2: Use custom tag
Now open up your navigation.xml file and rename the fragment tags related to your bottom navigation with whatever name you gave in #Navigator.Name() earlier.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mobile_navigation"
app:startDestination="#+id/navigation_home">
<sticky_fragment
android:id="#+id/navigation_home"
android:name="com.example.bottomnavigationlogic.ui.home.HomeFragment"
android:label="#string/title_home"
tools:layout="#layout/fragment_home" />
</navigation>
STEP 3: Add global action
Global actions are a way to navigate to destination from anywhere in your app. You can use the visual editor or directly use xml to add global actions. Set global action on each fragment with the following settings
destination : self
popUpTo : self
singleTop : true/checked
This your how your navigation.xml should look like after adding global actions
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mobile_navigation"
app:startDestination="#+id/navigation_home">
<sticky_fragment
android:id="#+id/navigation_home"
android:name="com.example.bottomnavigationlogic.ui.home.HomeFragment"
android:label="#string/title_home"
tools:layout="#layout/fragment_home" />
<sticky_fragment
android:id="#+id/navigation_images"
android:name="com.example.bottomnavigationlogic.ui.images.ImagesFragment"
android:label="#string/title_images"
tools:layout="#layout/fragment_images" />
<sticky_fragment
android:id="#+id/navigation_videos"
android:name="com.example.bottomnavigationlogic.ui.videos.VideosFragment"
android:label="#string/title_videos"
tools:layout="#layout/fragment_videos" />
<sticky_fragment
android:id="#+id/navigation_songs"
android:name="com.example.bottomnavigationlogic.ui.songs.SongsFragment"
android:label="#string/title_songs"
tools:layout="#layout/fragment_songs" />
<sticky_fragment
android:id="#+id/navigation_notifications"
android:name="com.example.bottomnavigationlogic.ui.notifications.NotificationsFragment"
android:label="#string/title_notifications"
tools:layout="#layout/fragment_notifications" />
<action
android:id="#+id/action_global_navigation_home"
app:destination="#id/navigation_home"
app:launchSingleTop="true"
app:popUpTo="#id/navigation_home" />
<action
android:id="#+id/action_global_navigation_notifications"
app:destination="#id/navigation_notifications"
app:launchSingleTop="true"
app:popUpTo="#id/navigation_notifications" />
<action
android:id="#+id/action_global_navigation_songs"
app:destination="#id/navigation_songs"
app:launchSingleTop="true"
app:popUpTo="#id/navigation_songs" />
<action
android:id="#+id/action_global_navigation_videos"
app:destination="#id/navigation_videos"
app:launchSingleTop="true"
app:popUpTo="#id/navigation_videos" />
</navigation>
STEP 4: Use global actions
When you wrote
NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());
then inside setupWithNavController() NavigationUI uses bottomNavigationView.setOnNavigationItemSelectedListener() to navigate to proper fragments depending on id of the menu item that was clicked. It's default behavior is as I mentioned before. We will add our own implementation to it and use global actions to achieve our desired back press behavior.
Here is how you do it simply in MainActivity
bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem menuItem) {
int id = menuItem.getItemId();
if (menuItem.isChecked()) return false;
switch (id)
{
case R.id.navigation_home :
navController.navigate(R.id.action_global_navigation_home);
break;
case R.id.navigation_images :
navController.navigate(R.id.action_global_navigation_images);
break;
case R.id.navigation_videos :
navController.navigate(R.id.action_global_navigation_videos);
break;
case R.id.navigation_songs :
navController.navigate(R.id.action_global_navigation_songs);
break;
case R.id.navigation_notifications :
navController.navigate(R.id.action_global_navigation_notifications);
break;
}
return true;
}
});
FINAL STEP 5: Add your custom navigator to NavController
Add your navigator as follow in your MainActivity. Make sure you are passing childFragmentManager of the NavHostFragment.
navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment));
Also add the navigation graph to NavController here as well using setGraph() method as shown below.
This is how my MainActivity looks like after step 4 and step 5
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BottomNavigationView navView = findViewById(R.id.nav_view);
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
R.id.navigation_home, R.id.navigation_images, R.id.navigation_videos,R.id.navigation_songs,R.id.navigation_notifications)
.build();
NavHostFragment navHostFragment = (NavHostFragment)getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
final NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
navController.getNavigatorProvider().addNavigator(new StickyFragmentNavigator(this, navHostFragment.getChildFragmentManager(),R.id.nav_host_fragment));
navController.setGraph(R.navigation.mobile_navigation);
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
NavigationUI.setupWithNavController(navView,navController);
navView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem menuItem) {
int id = menuItem.getItemId();
if (menuItem.isChecked()) return false;
switch (id)
{
case R.id.navigation_home :
navController.navigate(R.id.action_global_navigation_home);
break;
case R.id.navigation_images :
navController.navigate(R.id.action_global_navigation_images);
break;
case R.id.navigation_videos :
navController.navigate(R.id.action_global_navigation_videos);
break;
case R.id.navigation_songs :
navController.navigate(R.id.action_global_navigation_songs);
break;
case R.id.navigation_notifications :
navController.navigate(R.id.action_global_navigation_notifications);
break;
}
return true;
}
});
}
}
Hope this helps.
I am not entirely sure if this is the answer you are looking for but if you are concerned about managing state the modern way of managing state is by using something called a view model. View models are a component of the MVVM architecture. Their purpose is to hold and expose data to your fragment/activity to display. With the navigation architecture, if you store the data related to each fragment in this view model appropriately your state will be retained in the view model.
That being said I would personally suggest looking into MVVM architecture as well as view models specifically. Otherwise, a brute force for retaining state is by using the savedInstance state throughout fragments and manually saving and recovering important data.
Links:
- Android View Model Component
- Android architecture guide
I think you might need to prevent recreating the fragment upon clicking bottom nav view item twice. bottomNavigationView.setOnNavigationItemReselectedListener { /*Nothing to ignore reselection*/} after NavigationUI.setupWithNavController (bottomNavigationView, navHostFragment.getNavController ());
Hi I've created a settings Activity and when I am in the settings menu and press the back button of the phone i get directed to the "home screen" which is correct but when I am pressing the top arrow of the settings menu nothings happends, it seems to be just a button. Please see imgur to see which arrow i mean. Back arrow in the settings activity, press here. I've looked around in the java code and seems to have found the button? All help is appreciated!!
Here is the whole java file of the settings menu.
public class SettingsActivity extends AppCompatPreferenceActivity {
/**
* A preference value change listener that updates the preference's summary
* to reflect its new value.
*/
private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
#Override
public boolean onPreferenceChange(Preference preference, Object value) {
String stringValue = value.toString();
if (preference instanceof RingtonePreference) {
// For ringtone preferences, look up the correct display value
// using RingtoneManager.
if (TextUtils.isEmpty(stringValue)) {
// Empty values correspond to 'silent' (no ringtone).
preference.setSummary(R.string.pref_ringtone_silent);
} else {
Ringtone ringtone = RingtoneManager.getRingtone(
preference.getContext(), Uri.parse(stringValue));
if (ringtone == null) {
// Clear the summary if there was a lookup error.
preference.setSummary(null);
} else {
// Set the summary to reflect the new ringtone display
// name.
String name = ringtone.getTitle(preference.getContext());
preference.setSummary(name);
}
}
} else {
// For all other preferences, set the summary to the value's
// simple string representation.
preference.setSummary(stringValue);
}
return true;
}
};
/**
* Helper method to determine if the device has an extra-large screen. For
* example, 10" tablets are extra-large.
*/
private static boolean isXLargeTablet(Context context) {
return (context.getResources().getConfiguration().screenLayout
& Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
}
/**
* Binds a preference's summary to its value. More specifically, when the
* preference's value is changed, its summary (line of text below the
* preference title) is updated to reflect the value. The summary is also
* immediately updated upon calling this method. The exact display format is
* dependent on the type of preference.
*
* #see #sBindPreferenceSummaryToValueListener
*/
private static void bindPreferenceSummaryToValue(Preference preference) {
// Set the listener to watch for value changes.
preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
// Trigger the listener immediately with the preference's
// current value.
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager
.getDefaultSharedPreferences(preference.getContext())
.getString(preference.getKey(), ""));
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setupActionBar();
}
/**
* Set up the {#link android.app.ActionBar}, if the API is available.
*/
private void setupActionBar() {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
// Show the Up button in the action bar.
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
/**
* {#inheritDoc}
*/
#Override
public boolean onIsMultiPane() {
return isXLargeTablet(this);
}
/**
* {#inheritDoc}
*/
#Override
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.pref_headers, target);
}
/**
* This method stops fragment injection in malicious applications.
* Make sure to deny any unknown fragments here.
*/
protected boolean isValidFragment(String fragmentName) {
return PreferenceFragment.class.getName().equals(fragmentName)
|| GeneralPreferenceFragment.class.getName().equals(fragmentName);
}
/**
* This fragment shows general preferences only. It is used when the
* activity is showing a two-pane settings UI.
*/
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class GeneralPreferenceFragment extends PreferenceFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_general);
setHasOptionsMenu(true);
// Bind the summaries of EditText/List/Dialog/Ringtone preferences
// to their values. When their values change, their summaries are
// updated to reflect the new value, per the Android Design
// guidelines.
bindPreferenceSummaryToValue(findPreference("example_text"));
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
startActivity(new Intent(getActivity(), SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
}
}
I think this is the button?
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setupActionBar();
}
/**
* Set up the {#link android.app.ActionBar}, if the API is available.
*/
private void setupActionBar() {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
// Show the Up button in the action bar.
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
Open AndroidManifest.xml file. You need to define parent Activity for your SettingsActivity using:
<meta-data android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity"/>
then in your class:
/**
* Set up the {#link android.app.ActionBar}, if the API is available.
*/
private void setupActionBar() {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
// Show the Up button in the action bar.
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowHomeEnabled(true);
}
}
if you're using already AppCompatActivity with Toolbar, check this solution:
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// add back arrow to toolbar
if (getSupportActionBar() != null){
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
If you're also want to handle this event or change the backButton icon, go to: Display Back Arrow on Toolbar Android
Edit: add to your activity class this code:
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
Hope it will help
In your AndroidManifest.xml , add this in the Activity tag
<meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".HomeScreenActivity"/>
Combining the answers above
SettingsActivity.java
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getFragmentManager().beginTransaction().replace(android.R.id.content, new MainSettingsFragment()).commit();
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
}
return super.onOptionsItemSelected(item);
}
#Override
public void onBackPressed() {
super.onBackPressed();
this.finish();
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
}
AndroidManifest.xml
<activity android:name=".SettingsActivity"
android:label="#string/title_settings">
<meta-data android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity"/>
</activity>
You Should try it.
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// handle arrow click here
if (item.getItemId() == android.R.id.home) {
finish(); // close this activity and return to preview activity (if there is any)`enter code here`
}
return super.onOptionsItemSelected(item);
}
I really like the new PopupMenu we got in 3.0, but I just can't display any icons next to the menu items in it. I'm inflating the menu from the .xml below:
<item android:id="#+id/menu_delete_product"
android:icon="#drawable/sym_action_add"
android:title="delete"
android:showAsAction="ifRoom|withText" />
<item android:id="#+id/menu_modify_product"
android:icon="#drawable/sym_action_add"
android:title="modify"
android:showAsAction="ifRoom|withText" />
<item android:id="#+id/menu_product_details"
android:icon="#drawable/sym_action_add"
android:title="details"
android:showAsAction="ifRoom|withText" />
With this code:
image.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
PopupMenu pop = new PopupMenu(getActivity(), v);
pop.getMenuInflater().inflate(R.menu.shelves_details_menu, pop.getMenu());
pop.show();
}
});
I can't get the icons to show up, am I missing something?
Contribution to the solution provided by Gaelan Bolger.
Use this code if you get a "IllegalAccessException: access to field not allowed".
PopupMenu popup = new PopupMenu(mContext, view);
try {
Field[] fields = popup.getClass().getDeclaredFields();
for (Field field : fields) {
if ("mPopup".equals(field.getName())) {
field.setAccessible(true);
Object menuPopupHelper = field.get(popup);
Class<?> classPopupHelper = Class.forName(menuPopupHelper
.getClass().getName());
Method setForceIcons = classPopupHelper.getMethod(
"setForceShowIcon", boolean.class);
setForceIcons.invoke(menuPopupHelper, true);
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
prepareMenu(popup.getMenu());
popup.show();
text
If you're willing to be a bit adventurous, look at Google's source code for PopupMenu. Create your own class i.e. MyPopupMenu that is the same as Google's PopupMenu class, but make one slight change.
In PopupMenu's constructor:
public MyPopupMenu(Context context, View anchor) {
// TODO Theme?
mContext = context;
mMenu = new MenuBuilder(context);
mMenu.setCallback(this);
mAnchor = anchor;
mPopup = new MenuPopupHelper(context, mMenu, anchor);
mPopup.setCallback(this);
mPopup.setForceShowIcon(true); //ADD THIS LINE
}
use the method setForceShowIcon to force it to show the icon. You can also just expose a public method to set this flag as well depending on your needs.
I was able to show the icons using reflection. It may not be the most elegant solution but it works.
try {
Class<?> classPopupMenu = Class.forName(popupMenu
.getClass().getName());
Field mPopup = classPopupMenu.getDeclaredField("mPopup");
mPopup.setAccessible(true);
Object menuPopupHelper = mPopup.get(popupMenu);
Class<?> classPopupHelper = Class.forName(menuPopupHelper
.getClass().getName());
Method setForceIcons = classPopupHelper.getMethod(
"setForceShowIcon", boolean.class);
setForceIcons.invoke(menuPopupHelper, true);
} catch (Exception e) {
e.printStackTrace();
}
We can use sub-menu model. So, we don't need to write method for showing popup menu, it will be showing automacally. Have a look:
menu.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="#+id/action_more"
android:icon="#android:drawable/ic_menu_more"
android:orderInCategory="1"
android:showAsAction="always"
android:title="More">
<menu>
<item
android:id="#+id/action_one"
android:icon="#android:drawable/ic_popup_sync"
android:title="Sync"/>
<item
android:id="#+id/action_two"
android:icon="#android:drawable/ic_dialog_info"
android:title="About"/>
</menu>
</item>
</menu>
in MainActivity.java
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
The result is:
before use method popup.show(),make a MenuPopupHelper instance and call method setForceShowIcon(true),like this
try {
Field mFieldPopup=popupMenu.getClass().getDeclaredField("mPopup");
mFieldPopup.setAccessible(true);
MenuPopupHelper mPopup = (MenuPopupHelper) mFieldPopup.get(popupMenu);
mPopup.setForceShowIcon(true);
} catch (Exception e) {
}
The easiest way I found is that to use MenuBuilder and MenuPopupHelper.
MenuBuilder menuBuilder =new MenuBuilder(this);
MenuInflater inflater = new MenuInflater(this);
inflater.inflate(R.menu.menu, menuBuilder);
MenuPopupHelper optionsMenu = new MenuPopupHelper(this, menuBuilder, view);
optionsMenu.setForceShowIcon(true);
// Set Item Click Listener
menuBuilder.setCallback(new MenuBuilder.Callback() {
#Override
public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
switch (item.getItemId()) {
case R.id.opt1: // Handle option1 Click
return true;
case R.id.opt2: // Handle option2 Click
return true;
default:
return false;
}
}
#Override
public void onMenuModeChange(MenuBuilder menu) {}
});
// Display the menu
optionsMenu.show();
menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/opt1"
android:icon="#mipmap/ic_launcher"
android:title="option 1" />
<item
android:id="#+id/opt2"
android:icon="#mipmap/ic_launcher"
android:title="option 2" />
</menu>
I found a native solution for this, using MenuPopupHelper.setForceShowIcon(true).
private void createMenu(int menuRes, View anchor, MenuBuilder.Callback callback) {
Context context = anchor.getContext();
NavigationMenu navigationMenu = new NavigationMenu(context);
navigationMenu.setCallback(callback);
SupportMenuInflater supportMenuInflater = new SupportMenuInflater(context);
supportMenuInflater.inflate(menuRes, navigationMenu);
MenuPopupHelper menuPopupHelper = new MenuPopupHelper(context, navigationMenu, anchor);
menuPopupHelper.setForceShowIcon(true);
menuPopupHelper.show();
}
Usage
private void initMenu(View view) {
view.findViewById(R.id.myButton).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
createMenu(R.menu.help_menu, view, new MenuBuilder.Callback() {
#Override
public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
switch (item.getItemId()) {
case R.id.id1:
// Do something
break;
case R.id.id2:
// Do something
break;
case R.id.id3:
// Do something
break;
}
return true;
}
#Override
public void onMenuModeChange(MenuBuilder menu) {
}
});
}
});
}
Along the line of using reflection and without the need to use MenuPopupHelper, you can add
if (popup.getMenu() instanceof MenuBuilder) {
//noinspection RestrictedApi
((MenuBuilder) popup.getMenu()).setOptionalIconsVisible(true);
}
prior to inflating the menu
PopupMenu will not display icons. You can use an ActionBar.
http://developer.android.com/guide/topics/ui/actionbar.html
Some of the solutions above will work with the reflection hack,
Just sharing this: I've recently came across the same issues, but I also wanted to create a more customized thing (adding custom view in the menu) so I created the following lib.
https://github.com/shehabic/Droppy
If you're using AndroidX, which changed the visibility of MenuPopupHelper to package-private, you can avoid the cost of reflection by creating a wrapper class with the same package name.
This exposes package-private members to public.
package androidx.appcompat.widget // Create this package in your project's /src/main/java
import android.annotation.SuppressLint
class PopupMenuWrapper(val t: PopupMenu) {
#SuppressLint("RestrictedApi")
fun setForceShowIcon(show: Boolean) { // Public method
t.mPopup.setForceShowIcon(show)
}
}
fun PopupMenu.wrap() = PopupMenuWrapper(this)
Then call the hidden function as you normally would.
val popup = PopupMenu(anchor.context, anchor)
popup.wrap().setForceShowIcon(true)
popup.show()
If you want to prevent using RestrictedApi use this extention function:
fun PopupMenu.forcePopUpMenuToShowIcons() {
try {
val method = menu.javaClass.getDeclaredMethod(
"setOptionalIconsVisible",
Boolean::class.javaPrimitiveType
)
method.isAccessible = true
method.invoke(menu, true)
} catch (e: Exception) {
e.printStackTrace()
}
}
You can use the setForceShowIcon (true)
PopupMenu(context, view).apply {
setForceShowIcon(true)
menuInflater.inflate(R.menu.menu_edit_professional_experience, menu)
setOnMenuItemClickListener { item ->
Toast.makeText(view.context, "YOU clcick", Toast.LENGTH_LONG).show()
true
}
}.show()
Use setForceShowIcon(true)
The PopupMenu cannot be fully customized. Below you find a general solution to make your PopupMenu customizable via a custom layout. Having that, you can experiment a lot more with different layouts. Cheers.
1 - The Custom PopupMenu class:
public class PopupMenuCustomLayout {
private PopupMenuCustomOnClickListener onClickListener;
private Context context;
private PopupWindow popupWindow;
private int rLayoutId;
private View popupView;
public PopupMenuCustomLayout(Context context, int rLayoutId, PopupMenuCustomOnClickListener onClickListener) {
this.context = context;
this.onClickListener = onClickListener;
this.rLayoutId = rLayoutId;
LayoutInflater inflater = (LayoutInflater) context.getSystemService(LAYOUT_INFLATER_SERVICE);
popupView = inflater.inflate(rLayoutId, null);
int width = LinearLayout.LayoutParams.WRAP_CONTENT;
int height = LinearLayout.LayoutParams.WRAP_CONTENT;
boolean focusable = true;
popupWindow = new PopupWindow(popupView, width, height, focusable);
popupWindow.setElevation(10);
LinearLayout linearLayout = (LinearLayout) popupView;
for (int i = 0; i < linearLayout.getChildCount(); i++) {
View v = linearLayout.getChildAt(i);
v.setOnClickListener( v1 -> { onClickListener.onClick( v1.getId()); popupWindow.dismiss(); });
}
}
public void setAnimationStyle( int animationStyle) {
popupWindow.setAnimationStyle(animationStyle);
}
public void show() {
popupWindow.showAtLocation( popupView, Gravity.CENTER, 0, 0);
}
public void show( View anchorView, int gravity, int offsetX, int offsetY) {
popupWindow.showAsDropDown( anchorView, 0, -2 * (anchorView.getHeight()));
}
public interface PopupMenuCustomOnClickListener {
public void onClick(int menuItemId);
}
}
2 - Your custom layout, e.g. linearlayout with horizontal layout. In this case I use a simple LinearLayout with TextView items. You can use Buttons, etc.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/white"
android:orientation="horizontal">
<TextView
android:id="#+id/popup_menu_custom_item_a"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="A"
android:textAppearance="?android:textAppearanceMedium" />
<TextView
android:id="#+id/popup_menu_custom_item_b"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="B"
android:textAppearance="?android:textAppearanceMedium" />
// ...
</LinearLayout>
3 - Using the Custom PopupMenu like the normal PopupMenu.
PopupMenuCustomLayout popupMenu = new PopupMenuCustomLayout(
MainActivity.mainActivity, R.layout.popup_menu_custom_layout,
new PopupMenuCustomLayout.PopupMenuCustomOnClickListener() {
#Override
public void onClick(int itemId) {
// log statement: "Clicked on: " + itemId
switch (itemId) {
case R.id.popup_menu_custom_item_a:
// log statement: "Item A was clicked!"
break;
}
}
});
// Method 1: popupMenu.show();
// Method 2: via an anchor view:
popupMenu.show( anchorView, Gravity.CENTER, 0, 0);
I am currently using a switch in my Android application as sort of options in my settings class. However, when I change the state of the switch, from say false to true, and go back to the menu, and then go back to the settings activity, the state of the switch goes back to the default value. Is there a key line that basically saves the switch's state so that when a user returns to it, the state is the same? I don't think this question requires code, but if you think it's helpful, please let me know and I'll attach it. Thanks!
here is my activity code:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context="com.example.speedytext.Options" >
<TextView
android:id="#+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Switch
android:id="#+id/switch1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/textView1"
android:layout_marginLeft="46dp"
android:layout_marginTop="48dp"
android:layout_toRightOf="#+id/textView1"
android:text="Switch" />
<Switch
android:id="#+id/switch2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignRight="#+id/switch1"
android:layout_below="#+id/switch1"
android:layout_marginTop="66dp"
android:text="Switch" />
</RelativeLayout>
here is my java code:
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.Switch;
public class Options extends ActionBarActivity {
private static Switch ding;
private Switch countdown;
public static boolean isDingChecked;
public static boolean isCountdownChecked;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_options);
ding = (Switch) findViewById(R.id.switch1);
ding.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// do something, the isChecked will be
// true if the switch is in the On position
isDingChecked = isChecked;
System.out.println(isDingChecked);
System.out.println(isDingChecked());
}
});
countdown = (Switch) findViewById(R.id.switch2);
countdown.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
// do something, the isChecked will be
// true if the switch is in the On position
isCountdownChecked = isChecked;
System.out.println(isCountDownChecked());
}
});
isDingChecked = ding.isChecked();
isCountdownChecked= countdown.isChecked();
}
public static boolean isDingChecked()
{
return isDingChecked;
}
public static boolean isCountDownChecked()
{
return isCountdownChecked;
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.options, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
In onCreate(), call setChecked() with your stored "is checked" value to set the default to whatever was stored previously.
Also, consider storing the settings to e.g. shared preferences. static variables do not persist over application relaunches and often make your code harder to maintain.
Shared Preference Class
public class AppPreferences {
private SharedPreferences appSharedPrefs;
private Editor prefsEditor;
public AppPreferences(Context context, String Preferncename) {
this.appSharedPrefs = context.getSharedPreferences(Preferncename,
Activity.MODE_PRIVATE);
this.prefsEditor = appSharedPrefs.edit();
}
/****
*
* getdata() get the value from the preference
*
* */
public String getData(String key) {
return appSharedPrefs.getString(key, "");
}
/****
*
* SaveData() save the value to the preference
*
* */
public void SaveData(String text, String Tag) {
prefsEditor.putString(Tag, text);
prefsEditor.commit();
}
public void clear()
{
prefsEditor.clear();
prefsEditor.commit();
}
}
Usage of Shared Preference in activity
AppPreferences appPref;
appPref = new AppPreferences(getApplicationContext(), "PREFS");
To save the state
When the state of the switch is changed save the state true or false to a tag in shared preference
You can use this line to save the state
appPref.SaveData("Value", "Tag");
so it will be like this for checked state appPref.SaveData("true", "state");
and appPref.SaveData("false", "state"); for unchecked state
in the oncreate of your activity get the value from the shared preference
using
appPref.getData("state");
if(appPref.getData("state").equals("true"))
{
//set ur switch button to checked state
}
else if(appPref.getData("state").equals("false"))
{
//set ur switch button to unchecked state
}
then check the result string for true or false
and set the switch accordingly
I have created my tabs using the following example code and in the comments it was noted that many people could no longer call upon the search dialog and I have been able to no longer have my program force close when you click on the hardware search button but when you click on the button it doesn't pop up anything anymore.
Would anyone know how to fix this?
Multiple Android Activities with TabActivity
import java.util.ArrayList;
import android.app.Activity;
import android.app.ActivityGroup;
import android.app.LocalActivityManager;
import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.Window;
/**
* The purpose of this Activity is to manage the activities in a tab.
* Note: Child Activities can handle Key Presses before they are seen here.
* #author Eric Harlow
*/
public class TabGroupActivity extends ActivityGroup {
private ArrayList<String> mIdList;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (mIdList == null) mIdList = new ArrayList<String>();
}
/**
* This is called when a child activity of this one calls its finish method.
* This implementation calls {#link LocalActivityManager#destroyActivity} on the child activity
* and starts the previous activity.
* If the last child activity just called finish(),this activity (the parent),
* calls finish to finish the entire group.
*/
#Override
public void finishFromChild(Activity child) {
LocalActivityManager manager = getLocalActivityManager();
int index = mIdList.size()-1;
if (index < 1) {
finish();
return;
}
manager.destroyActivity(mIdList.get(index), true);
mIdList.remove(index);
index--;
String lastId = mIdList.get(index);
Intent lastIntent = manager.getActivity(lastId).getIntent();
Window newWindow = manager.startActivity(lastId, lastIntent);
setContentView(newWindow.getDecorView());
}
/**
* Starts an Activity as a child Activity to this.
* #param Id Unique identifier of the activity to be started.
* #param intent The Intent describing the activity to be started.
* #throws android.content.ActivityNotFoundException.
*/
public void startChildActivity(String Id, Intent intent) {
Window window = getLocalActivityManager().startActivity(Id,intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
if (window != null) {
mIdList.add(Id);
setContentView(window.getDecorView());
}
}
/**
* The primary purpose is to prevent systems before android.os.Build.VERSION_CODES.ECLAIR
* from calling their default KeyEvent.KEYCODE_BACK during onKeyDown.
*/
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
//preventing default implementation previous to android.os.Build.VERSION_CODES.ECLAIR
return true;
}
return super.onKeyDown(keyCode, event);
}
/**
* Overrides the default implementation for KeyEvent.KEYCODE_BACK
* so that all systems call onBackPressed().
*/
#Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
onBackPressed();
return true;
}
return super.onKeyUp(keyCode, event);
}
/**
* If a Child Activity handles KeyEvent.KEYCODE_BACK.
* Simply override and add this method.
*/
#Override
public void onBackPressed () {
int length = mIdList.size();
if ( length > 1) {
Activity current = getLocalActivityManager().getActivity(mIdList.get(length-1));
current.finish();
}
}
}
#Override
public void onBackPressed() {
TabGroupActivity parentActivity = (TabGroupActivity)getParent();
parentActivity.onBackPressed();
}
The following section explains how I used this ActivityGroup. The first thing I did was subclass this ActivityGroup like this.
import android.content.Intent;
import android.os.Bundle;
public class TabGroup1Activity extends TabGroupActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startChildActivity("OptionsActivity", new Intent(this,OptionsActivity.class));
}
}
I added this TabGroup1Activity into my TabActivity like this.
TabHost tabHost = getTabHost();
tabHost.addTab(tabHost.newTabSpec("tab1")
.setIndicator("MESSAGES")
.setContent(new Intent(this, TabGroup1Activity.class)));
Then any Activity that you want to start in the ActivityGroup can be done in a way similar to this.
Intent frequentMessages = new Intent(getParent(), FrequentMessageActivity.class);
TabGroupActivity parentActivity = (TabGroupActivity)getParent();
parentActivity.startChildActivity("FrequentMessageActivity", frequentMessages);
I have made all my activities searchable in the Android Manifest and I followed the tutorial on implementing search.
Creating a Search Interface
Prior to modifying my code to work with the TabActivity the search was working as intended but something in the code has broken the search functionality.
I have found a solution which I am working on deciphering but I was hoping that someone with more know how would help in implementing it based on the example given in the tutorial example posted in the question.
Here is the solution that I found but haven't applied
Nested TabHosts