After spending a lot of time and not finding a suitable solution, I decided to turn to this service for help.
I have an example of the simplest application that implements Drawer. This example contains only menu items without any implementations. In the menu item Share, I added a Switch and a listener to it:
main_nav.xml
<item
android:id="#+id/nav_share"
android:icon="#drawable/ic_menu_share"
app:actionViewClass="androidx.appcompat.widget.SwitchCompat"
android:title="#string/menu_share" />
MainActivity.kt
val menuItem = navigation_view.menu.findItem(R.id.nav_share)
val switch_id = menuItem.actionView as SwitchCompat
switch_id.isChecked = true
switch_id.setOnClickListener {
// TODO: empty scope
}
The switch works smoothly and with animation as shown below:
But now I need to turn off the Home menu item if the Switch is off:
switch_id.setOnClickListener {
navigation_view.menu.findItem(R.id.nav_home).isEnabled = switch_id.isChecked
}
After adding this line, my Switch animation breaks!
I spent two days solving this issue and in the end to no avail.
My question: WTF with android menu in 2021? Why does accessing a menu item break the animation and how can I fix it? I would be grateful for any help !!
I created an issue with this question in Android Tracker and got an answer:
Either way, this is likely to be a non-trivial fix for an issue that
is not a regression and not impacting correctness of the UI. At this
time, we have no plans to address the issue.
And then a workaround was suggested:
Something along the lines of adding a setOnCheckedChangeListener that
posts a delayed Runnable to effect the change. I'd recommend asking on
StackOverflow if you need a detailed example.
E.g.
How to delay "runOnUiThread" in android?
I added a postDelay(200) and the Switch animation works fine:
switch.setOnCheckedChangeListener { _, isChecked ->
Handler(Looper.getMainLooper()).postDelayed({
navigationView.menu.findItem(R.id.nav_home).isEnabled = isChecked
}, 200)
}
Related
Given a button with a Modifier:
TextButton(modified = Modifier.testTag("abc123"))
when you want to create a test to click it, you do:
composeTestRule.onNodeWithTag("abc123").performClick()
but when I'm having a:
Switch(modifier = Modifier.testTag("abc123"))
I'm trying every single perform gesture but I can't get the Switch to toggle, and can't get any documentation from Android.
What's the correct way to toggle it automatically in order to test it?
I had issues toggling a switch with performClick(), but it turned out the switch wasn't visible on screen, performClick() will then simply click the coordinates (0,0) without any error.
So to ensure it's displayed first:
composeTestRule.onNodeWithTag("abc123")
.assertIsDisplayed()
.performClick()
OLD ANSWER (can still be used if you need to click something which is not displayed)
This seems to be a working way to toggle a material Switch in a Jetpack Compose test:
composeTestRule.onNodeWithTag("abc123")
.performSemanticsAction(SemanticsActions.OnClick)
I don't know if you are still struggling with this, but the following seems to work for me:
composeTestRule
.onNodeWithTag("abc123")
.performGesture { swipeLeft() } // or swipeRight to turn it on
I am writing UI Automation tests in Espresso for Android & came across a scenario for which I haven't got any solution so far.
In one Fragment, I have OptionsMenu with a single item. The state of that MenuItem is set according to value from API response.
#Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.clear();
getActivity().getMenuInflater().inflate(R.menu.menu_cancel_order, menu);
MenuItem cancelMenuItem = menu.findItem(R.id.cancel_order);
if(something) { // something can be a boolean value from server
cancelMenuItem.setEnabled(true);
} else {
cancelMenuItem.setEnabled(false);
}
}
For UI testing, I need to write test case for checking whether this MenuItem is enabled/disabled.
For clicking on the overflowmenu,
ViewInteraction actionMenuItemView = onView(
allOf(withId(R.id.action_settings), withContentDescription("Settings"), isDisplayed()));
actionMenuItemView.perform(click());
And so far what I have tried to check the Assertion is given below.
onView(allOf(withText("Cancel Order"), withId(R.id.cancel_order))).check(matches(not(isEnabled())));
But this fires NoMatchingViewException with message
NoMatchingViewException: No views in hierarchy found matching: (with
text: is "Cancel Order" and with id:
com.equinix.ecp.betatest:id/cancel_order)
So I tried changing it to
onView(allOf(withText("Cancel Order"))).check(matches(not(isEnabled())));
Somehow this matched the view but it was not a MenuItem but the TextView inside the MenuItem & since I am setting setEnabled() to MenuItem, check() Assertion won't work as expected since it is a TextView.
So my question is how to write Test for checking enabled/disabled state of MenuItem.
it would be a good idea to make use of uiautomatorviewer, to put a breakpoint in at the point your test fails, and then inspect your app's layout for clues
it sounds to me that you have two views. One with the id of R.id.cancel_order and another with text "Cancel Order" which probably has another id (or could/should).
So together they return NoMatchingView, because they aren't the same view.
They could be sibling views, or possibly one is a descendent of another. This is where uiautomatorviewer is very handy for figuring out what's happening on screen
as long as you've installed "Android SDK Platform-Tools" and "Android SDK Tools"
from Terminal:
cd /Users/<user name>/Library/Android/sdk/tools/bin
./uiautomatorviewer
(it's also helpful to save this as a script and just use an alias shortcut for convenience)
as for your matcher, i would try :
onView(allOf(
withId(R.id.cancel_order),
hasSibling(withText("Cancel Order"))
)).check(matches(not(isEnabled())));
or change hasSibling(_) to hasDescendent(_) or isDescendentOfA(_), depending on their relationship (which you can find out by using uiautomatorviewer)
I would suggest you use the IDs of the menu items to perform your checks.
I tried it with this menu:
<menu 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"
tools:context="at.hellobank.hellomarkets.symbols.DetailActivity">
<item
android:id="#+id/action_1"
android:icon="#android:drawable/arrow_down_float"
android:title="Menu1"
app:showAsAction="always" />
<item
android:id="#+id/action_2"
android:enabled="false"
android:icon="#android:drawable/arrow_down_float"
android:title="Menu2"
app:showAsAction="always" />
</menu>
So one menu item is enabled one is disabled. My test to check this looks like this and is working as expected:
#Test
public void testMenuItemsStatus() throws Exception {
onView(withId(R.id.action_1)).check(matches(isEnabled()));
onView(withId(R.id.action_2)).check(matches(not(isEnabled())));
}
Generally using IDs in tests is better imho because you are more independent of typos and general language. withText("Cancel Order") probably will not work if you test the app localized in another language.
I am attempting to use the BottomNavigationView from the design library. Everything is working except I want each navigation item to start an activity, and therefore I want to uncheck all items in the nav so they look the same. I have tried several solutions, most of which do not work, and the last of which does work but feels very hacky.
First I did this:
ViewGroup nav = (ViewGroup) bottomNav;
for(int i=0; i < nav.getChildCount(); i++) {
nav.getChildAt(i).setSelected(false);
}
Which seemed to do nothing.
Then I tried:
int size = bottomNav.getMenu().size();
for (int i = 0; i < size; i++) {
bottomNav.getMenu().getItem(i).setChecked(false);
}
Which only made the last item checked instead of the first.
And finally I tried adding a dummy item to the menu and doing:
bottomNav.getMenu().findItem(R.id.dummmy_item).setChecked(true);
bottomNav.findViewById(R.id.dummmy_item).setVisibility(View.GONE);
Which almost works, but it hides the title underneath, which are important for context in my case.
Then I found this answer: https://stackoverflow.com/a/41372325/4888701 and edited my above solution to include that. Specifically I added the proguard rule, and I used that exact helper class and called the method. It looks correct, seems to work. But it feels very hacky to me because:
I am using a dummy menu item to allow no visible item to be checked
It adds quite a bit of code for what should be a small visual fix.
I have read before that reflection should be avoided if at all possible.
Is there any other, preferably simpler way to achieve this, or is this the best we have with the current version of the library?
(As a side note, I am wondering if the proguard rule in this solution is necessary and what it does? I don't know really anything about proguard, but this project is inherited from someone else who had enabled it.)
After plenty of trial and error, this worked for me (using Kotlin)
(menu.getItem(i) as? MenuItemImpl)?.let {
it.isExclusiveCheckable = false
it.isChecked = it.itemId == actionId
it.isExclusiveCheckable = true
}
The #Joe Van der Vee solutions works for me. I have made extension methods from it. But I consider whether this doesn't have some downsides like #RestirctedApi suppressing!
#SuppressLint("RestrictedApi")
fun BottomNavigationView.deselectAllItems() {
val menu = this.menu
for(i in 0 until menu.size()) {
(menu.getItem(i) as? MenuItemImpl)?.let {
it.isExclusiveCheckable = false
it.isChecked = false
it.isExclusiveCheckable = true
}
}
}
If I've understood your question correctly (which it's possible I haven't) then a better solution might be to flip this problem around. These are my assumptions about your question:
You have a set of activities
Each Activity has its own BottomNavigationView
When you click the BNV on one activity, the item clicked becomes selected
You want to deselect the clicked item because when the new Activity starts nothing is selected
If my assumptions are correct there are two better solutions:
Use Fragments not Activities (Recommended)
They the BNV stays on one activity, the fragment within the activity changes
Don't deselect clicked item
Each activity when started selects the correct tile to match
That said, if you do want to do it your way I think the code below will achieve it, by just changing the affected item when it changes. (You should avoid Reflection whenever possible, it's generally indicative of another architectural problem with your design)
bnv.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
item.getActionView().setSelected(false);
return false;
}
});
I know the question asked to not use reflection, however, I have not found another way to get the desired effect without using it. There is a code fix for allowing to disable the shifting mode in the git repo but who knows when that will be released. So for the time being (26.0.1), this code works for me. Also the reason people say don't use reflection is because it is slow on Android (especially on older devices). However, for this one call it won't be an impact on performance. You should avoid it when parsing/serializing a large amount of data though.
The reason for needing the proguard rule is because proguard obfuscates your code. Which means it can change method names, truncate names, break up classes and whatever else it sees fit to prevent someone from being able to read your source code. This rule prevents this field variable name from changing so that when you call it via reflection, it still exists.
Proguard rule:
-keepclassmembers class android.support.design.internal.BottomNavigationMenuView {
boolean mShiftingMode;
}
Updated method:
static void removeShiftMode(BottomNavigationView view)
{
BottomNavigationMenuView menuView = (BottomNavigationMenuView) view.getChildAt(0);
try
{
Field shiftingMode = menuView.getClass().getDeclaredField("mShiftingMode");
shiftingMode.setAccessible(true);
shiftingMode.setBoolean(menuView, false);
shiftingMode.setAccessible(false);
for (int i = 0; i < menuView.getChildCount(); i++)
{
BottomNavigationItemView item = (BottomNavigationItemView) menuView.getChildAt(i);
item.setShiftingMode(false);
item.setChecked(false); // <-- Changed this line
item.setCheckable(false); // <-- Added this line
}
}
catch (NoSuchFieldException e)
{
Log.e("ERROR NO SUCH FIELD", "Unable to get shift mode field");
}
catch (IllegalAccessException e)
{
Log.e("ERROR ILLEGAL ALG", "Unable to change value of shift mode");
}
}
I just run into this problem. I found working solution here with casting menuItem to MenuItemImp which is annotated with #RestrictTo(LIBRARY_GROUP_PREFIX). If you don't want to use this restricted class, you can stick to standard MenuItem and instead of using isExclusiveCheckable just use isCheckable.
Example:
navigation_view?.menu?.let {
for(menuItem in it.iterator()){
menuItem.isCheckable = false
menuItem.isChecked = false
menuItem.isCheckable = true
}
}
I'm using AppCompat and trying to recall the ImageView for the up/back button belonging to the toolbar.
I know R.android.id.home exists, because I can manage its click as a Menu item:
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
//this works
}
return super.onOptionsItemSelected(item);
}
Apart from that, whenever I try to call findViewById(android.R.id.home) - be it onCreate, be it onClick of a custom button - I get null.
I even get null if, in the sample above, I call findViewById(item.getItemId()).
Why is it?
This question has been asked before here, most times regarding ActionBarSherlock (which I am not using). Another time it was suggested to use:
getWindow().getDecorView().findViewById(android.R.id.home)
But it isn't working. In that question the OP also says findViewById(android.R.id.home) works on API>3.0, but that's not true for me. Any ideas?
Whether or not the "home" icon is a widget, and what class of widget it is, and what its ID is (if any), is up to the implementation of the action bar. The native action bar may do this differently for different API levels, and all of that may be different than the way appcompat-v7 does it. Let alone ActionBarSherlock or other action bar implementations.
Specifically, android.R.id.home is a menu ID, which is why you can use it in places like onOptionsItemSelected(). It is not necessarily a widget ID, which is why it may or may not work with findViewById().
Ideally, you do not attempt to mess with the internal implementation of a UI that you did not construct yourself.
do one really has to make his own Up button to style it?
I do not know, as I have never tried to style it.
As CommonsWare said android.R.id.home is a menu ID, not a widget ID. But if you want to access this home button you could do it. For example I needed it to highlight home button in in-app tutorial:
fun AppCompatActivity.getToolbarHomeIcon(): View? =
this.findViewById<Toolbar?>(R.id.toolbar)?.let { toolbar ->
val contentDescription: CharSequence = toolbar.navigationContentDescription.let {
if (it.isNullOrEmpty()) {
this.getString(R.string.abc_action_bar_up_description)
} else {
it
}
}
// Here home button should be created even if it doesn't exist before
toolbar.navigationContentDescription = contentDescription
ArrayList<View>().let { potentialViews ->
toolbar.findViewsWithText(
potentialViews,
contentDescription,
View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION
)
potentialViews.getOrNull(0)
}
}
My android application targets the latest platform. I am new to the platform, and read bit conflicting information on actionbar. The way I was using it for navigation was.
menu.xml
<menu>
<item android:id="#+id/action_sort_size"
android:icon="#android:drawable/ic_menu_sort_by_size"
android:title="#string/action_barabc"
android:onClick="abc" />
<item android:id="#+id/action_sort_alpha"
....
In my activity
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
public void abc(MenuItem item) {
//...
}
this works, but the back/up navigation is not working correctly. could be unrelated, still like to confirm.
But, I also see implementation like here
where it switches on item.
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menuitem1:
Toast.makeText(this, "Menu Item 1 selected", Toast.LENGTH_SHORT)
.show();
break;
case R.id.menuitem2:
....
}
Which is the better approach?
The better approach, in my opinion, is the switch approach. There aren't many reasons why, but I'll list them:
The code is centralized. You don't have x amount of methods that basically do the same thing. It keeps your code more readable; it is "cleaner". You also get a default statement using the switch, this can help if you mess up and forget to make a case specifically for an element in the layout.
If you really wanted to have a centralized method using xml, you would have onClick reference the same method and check the ids of the View parameter. Which is essentially the same asonOptionsItemSelected.
It is a part of the API. The Android engineers would not have made it a part of the API if they didn't want the developer to use it. Yes the XML is techinally API, but XML should be used more for layouts and visuals, not for logic.
Everyone uses it. All the tutorials I have seen and everyone's code uses this method. It is now more of a convention.
It's largely personal, but if it looks like it's a convention, and everyone uses it, I'd adhere to it. Especially if you're working as part of a team. Different coding styles for such arbritrary things should be avoided.
And concerning your back/up navigation, it shouldn't make a difference which way you do it, since you have to implement the same code to get that navigation type.