I want to show a badge on a toolbar action. The badge number is updated by a LiveData value.
This is how I attach the badge:
BadgeUtils.attachBadgeDrawable(inboxBadgeDrawable, toolbar, R.id.menu_inbox);
I tried different places for that call, including Activity.onCreateOptionsMenu(), Activity.onPrepareOptionsMenu() and androidx.lifgecycle.Observer.onChanged().
When anything changes (toolbar or badge content), the badge is misplaced, traveling down left. Or it is duplicated to another action.
I guess attachBadgeDrawable tries to find the container view of R.id.menu_inbox inside the toolbar, inserts the badge and updates it's offsets. If the container view of the menu item changes, the old container view still has the old badge and there is no (sensible) way to remove it. Also, application of the offsets seems to stack.
So, is there any other intended way of using the BadgeDrawable on a toolbar action icon?
I understand that this feature is still experimental. Will this issue be addressed and if yes, how long will it approximately take? (I use com.google.android.material:material:1.3.0-beta01 right now.)
This question is mainly addressed to the developers of the component because usage questions should be asked here according to https://github.com/material-components/material-components-android.
EDIT: I also created an issue (feature request) on the project's tracker: https://github.com/material-components/material-components-android/issues/1967
I'm not sure it is an official solution but this is still a workaround. I ended up with detaching the BadgeDrawable on every onPrepareOptionsMenu, in case the menu items were changed or rearranged
// This is an indicator of whether we need to show the badge or not
private var isFilterOn: Boolean = false
private var filterBadge: BadgeDrawable? = null
#SuppressLint("UnsafeExperimentalUsageError")
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
val filterItem = menu.findItem(R.id.action_filter)
val toolbar = requireActivity().findViewById<Toolbar>(R.id.toolbar)
if(filterBadge != null) {
BadgeUtils.detachBadgeDrawable(filterBadge!!, toolbar, R.id.action_filter)
filterBadge = null
}
if(isFilterOn) {
filterBadge = BadgeDrawable.create(requireContext()).also {
BadgeUtils.attachBadgeDrawable(it, toolbar, R.id.action_filter)
}
}
}
Related
I am working on the accessibility and currently I want to set the Button Role on the MenuItem. We have checked but not got the proper solution for the same. I tried by setting the custom action layout and then giving the custom action layout Button Role it detect it as button but click need to be handled by setting the click listener on action layout. Which I want to avoid, is there any possibility that we can set role to MenuItem. So it will announce like "Setting Button Double Tap To Activate"
`#JvmStatic
fun View.setCustomRole(roleInfo: String) {
ViewCompat.setAccessibilityDelegate(this,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
v: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(v, info)
info.roleDescription = roleInfo
}
})
}`
Tried above method by setting action layout which work but I need to change the click handling from app which I want to avoid.
This was answered fairly recently, however I think I can clean it up somewhat.
Option 1
You can make MenuItem's buttons by default by ensuring you have the latest material library imported.
implementation 'com.google.android.material:material:1.7.0'
The sample app I created for my answer was 1.5.0 and it still had the default "button" announcement.
Option 2
In Material 1.7.0:
I didn't need any of this code to achieve the solution to the question
onInitializeAccessibilityNodeInfo host and info are not nullable!
Ensure that your MenuItem has an actionViewClass associated with it.
<item
...
android:icon="ICON_REFERENCE"
app:showAsAction="ifRoom"
app:actionViewClass="android.widget.ImageButton"
...
/>
Bonus to option 2:
To be able to customize a11y attributes, you can then get the item and assign custom role descriptions or extra actions:
// inside onCreateOptionsMenu
val menuActionView = menu
.findItem(R.id.action_settings)
.actionView as ImageButton
ViewCompat.setAccessibilityDelegate(menuActionView, object: AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View?,
info: AccessibilityNodeInfoCompat?
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info?.apply {
// not required as this is already a button
// always use a built in class as this will be localized
// automatically for you
// roleDescription = Button::class.java.simpleName
// I found I had to set this here, and not in the menu xml
contentDescription = getString(R.string.action_settings)
// to replace the term "activate" in "double tap to activate"
// in production apps, use a localized string!
addAction(
AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfoCompat.ACTION_CLICK, "Open menu"
)
)
}
}
})
Edit: see my partial solution below - but it is not satisfactory. So please do answer!
Original: I am using android navigation framework and have a custom toolbar. On the menu setup I arrange some click listeners, and can navigate successfully to other fragments using things like
[FragmentA]Directions.Action[FragmentA]To[FragmentB] action =
[FragmentA]Directions.action[FragmentA]To[FragmentB](args);
action.setArgs(args);
Navigation.findNavController(binding.[myToolbar].findViewByID([myMenuItem])).navigate(action);
Navigation.createNavigateOnClickListener(R.id.[FragmentBLayout]);
whenever myMenuItem is an actionView in the toolbar,
But when I have an overflow popup menu in my toolbar (ie those menuitems with showAsAction=never), the findViewByID in the third line returns null. That is, I can't access the popup menu views - they always return null. This is so whether I use my binding or getActivity().findViewByID.
Therefore the navigation fails.
Can someone help me solve this?
Well I have partially 'solved it' by using a) MenuProvider and b) inserting
Navigation.findNavController(getActivity(), R.id.[NavContainer]);
as in
binding.[myToolbar].addMenuProvider(new MenuProvider() {
#Override
public void onCreateMenu(#NonNull Menu menu, #NonNull MenuInflater menuInflater) {
menuInflater.inflate(R.menu.vocab_test_menu, binding.vocabTestToolbar.getMenu());
}
#Override
public boolean onMenuItemSelected(#NonNull MenuItem menuItem) {
NavController navController = Navigation.findNavController(getActivity(), R.id.fragmentContainerView);
switch (menuItem.getItemId()) {
[cases] SomeOption(navController)
}
});
...
SomeOption(NavController navController) {
[FragmentA]Directions.Action[FragmentA]To[FragmentB] action =
[FragmentA]FragmentDirections.action[FragmentA]To[FragmentB](args);
action.setArgs(args);
navController.navigate(action);
Navigation.createNavigateOnClickListener(R.id.[FragmentBLayout]);
}
Before I was just using binding.[myToolbar].inflateMenu([myMenu]) and binding.[myToolbar].setOnMenuItemClickListener(item -> { switch [cases] }). As far as I can get to grips with the documentation, using a menuprovider is superior - it has better lifecycle properties, but I'm not entirely sure about that - maybe someone could comment about that?
Edit: this is not entirely satisfactory, since it will trigger all menu items, regardless of whether I want them to navigate or not! If all of them are for navigation, then the above works, as long as all the navigation is written in for each menu case. But if one of the menuitems is not for navigation, my 'solution' will not work!
I have an Activity with a RecyclerView in a data binding layout. RecyclerView takes up the whole screen, and looking at making the UX go full screen, drawn under the status and nav bars.
I'm calling setSystemUiVisibility in activity's onCreate as below.
window.decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
)
Now the RecyclerView is drawn under the system bars, so I want to make sure it has enough padding so the items don't overlap with the system UI.
I found 2 ways of doing this, via a BindingAdapter.
Option 1
var statusBar = 0
var resourceId = view.resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
statusBar = view.resources.getDimensionPixelSize(resourceId)
}
var navBar = 0
resourceId = view.resources.getIdentifier("navigation_bar_height", "dimen", "android")
if (resourceId > 0) {
navBar = view.resources.getDimensionPixelSize(resourceId)
}
view.setPadding(0, statusBar, 0, navBar)
Option 2
var insets = view.rootWindowInsets.stableInsets
view.setPadding(0, insets.top, 0, insets.bottom)
I prefer the first, because it (with limited testing on emulators seems to) work on API 21, 28 and 29.
Option 2 only works on API 29, and also seems to get null on view.rootWindowInsets if/when the view is not attached. (So I guess I have to add a listener and wait for it to be attached before doing this)
So my question is, is there a down side to Option 1? Can I use it over the new API in 29? Is there any scenarios that Option 1 would not work?
(I think Option 1 might not work well on tablets where both nav and systems bars are on the bottom, so extra padding will be applied to the wrong side.)
A little bit late to the party, but here is the way that I've been doing, someone might need it.
For Android M and above, you can call View#rootWindowInsets directly, otherwise rely on Java's Reflection to access the private field mStableInsets
fun getStableInsets(view: View): Rect {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val windowInsets = view.rootWindowInsets
if (windowInsets != null) {
Rect(windowInsets.stableInsetLeft, windowInsets.stableInsetTop,
windowInsets.stableInsetRight, windowInsets.stableInsetBottom)
} else {
// TODO: Edge case, you might want to return a default value here
Rect(defaultInsetLeft, defaultInsetTop, defaultInsetRight, defaultInsetBottom)
}
} else {
val attachInfoField = View::class.java.getDeclaredField("mAttachInfo")
attachInfoField.isAccessible = true
val attachInfo = attachInfoField.get(view);
if (attachInfo != null) {
val stableInsetsField = attachInfo.javaClass.getDeclaredField("mStableInsets")
stableInsetsField.isAccessible = true
Rect(stableInsetsField.get(attachInfo) as Rect)
} else {
// TODO: Edge case, you might want to return a default value here
Rect(defaultInsetLeft, defaultInsetTop, defaultInsetRight, defaultInsetBottom)
}
}
}
Update:
stableInsetBottom .etc. are now deprecated with message
Use {#link #getInsetsIgnoringVisibility(int)} with {#link Type#systemBars()}
* instead.
Unfortunately systemBars() was graylisted in API 29 and is blacklisted in API 30 plus using this seems to work on API 30 emulator, however (some) real devices even running API 29 throws.
Below is logcat from Galaxy S20 FE
Accessing hidden method Landroid/view/WindowInsets$Type;->systemBars()I (blacklist, linking, denied)
2021-01-17 01:45:18.348 23013-23013/? E/AndroidRuntime: FATAL EXCEPTION: main
Process: test.app.package, PID: 23013
java.lang.NoSuchMethodError: No static method systemBars()I in class Landroid/view/WindowInsets$Type; or its super classes (declaration of 'android.view.WindowInsets$Type' appears in /system/framework/framework.jar!classes3.dex)
No answer for this it seems. Please put an answer if you find anything not covered below.
Using Option 1 I noticed on devices that do OEM specific gesture navigation atleast, when those gesture modes are active, above will still return full navigation bar height even though no visible navigation bar is present. So above will still pad the UI when it shouldn't.
Option 2 keeps returning null for insets until the view is attached so if you're doing this on a BindingAdapter, it won't work. It needs to be called after the view is attached.
My current solution is as below.
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
view.doOnAttach {
var bottom = it.rootWindowInsets?.stableInsetBottom?: 0
var top = it.rootWindowInsets?.stableInsetTop?: 0
view.setPadding(0, top, 0, bottom)
}
}
else {
// use option1, old devices don't have custom OEM specific gesture navigation.
// or.. just don't support versions below Android M ¯\_(ツ)_/¯
}
Caveats
Some OEMs (well atleast OnePlus) decided not to restart some activities, especially ones that are paused, when the navigation mode changed. So if the user decides to switch away from your app, change the navigation mode and return, your app may still overlap navigation bar until the activity is restarted.
I'm pretty new to Android development and completely new to Kotlin. I have an app with a navigation drawer, and am trying to change the color of the AppBarLayout based on what the user selects from the navigation drawer. I've tried a few different methods, and the closest I've come has been to change the toolbar color instead of the whole AppBar. This might be acceptable, but instead of actually setting it to the color I want, it always changes it to a dark grey.
when (item.itemId) {
R.id.nav_audit -> {
txtMain.text = "Audit"
toolbar.setBackgroundColor(R.color.colorAudit)
loadAudits()
}
R.id.nav_testing -> {
txtMain.text = "Testing"
toolbar.setBackgroundColor(R.color.colorTesting)
}
R.id.nav_workflow -> {
txtMain.text = "Workflow"
toolbar.setBackgroundColor(R.color.colorWorkflow)
}
R.id.nav_other -> {
txtMain.text = "Other"
toolbar.setBackgroundColor(R.color.colorPrimary)
}
}
I've also looked at possibly changing the theme, but it looks like it may not be easy to do that after the application has already loaded. Any thoughts are appreciated.
Besides changing the color of the Toolbar that you are already doing, one way to change the status bar in Kotlin is like this:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
(activity as? AppCompatActivity)?.window?.statusBarColor =
ContextCompat.getColor(context, R.color. colorTesting)
}
You could do a method that returns the color depending on itemId
I wanted to try out this funny title bar coloring, but it doesn't work for me as
getWindow().findViewById(android.R.id.title);
returns null. So I had a look at it with Hierarchy Viewer and found out that the view is called id/action_bar instead. But there's no R.id.action_bar (autocomplete doesn't offer it and there's nothing like this is R.java).
So now I'm doubly confused:
Is android.R.id.title sort of obsolete now (I'm using version 16 in my emulator)?
Where does id/action_bar come from?
What's the recommended and simple practice w.r.t. compatibility?
Should I get ActionBarSherlock? I originally just wanted to change the title bar color... not fool around with it a lot.
Use the code below to get the ActionBar's id:
val actionBarId = resources.getIdentifier("action_bar", "id", packageName)
And use findViewById you can find the action bar.
Then find the title from actionbar's children (in normal cases):
val actionbar = findViewById<ViewGroup>(actionBarId)
for (view in actionbar.children) {
if (view is TextView) {
// this is the titleView
}
}
However, if you just want to change the title view's text, just use getSupportActionBar:
supportActionBar?.apply {
// set title text
title = "Hello"
// set colored title text
val coloredTitle = SpannableString("Hello")
coloredTitle.setSpan(ForegroundColorSpan(Color.RED), 0, coloredTitle.length, 0)
title = coloredTitle
}
I would recommend using ActionBarSherlock if you're looking for compatibility with Android versions before API level 14 / Android 4.0.
Changing the background of the ActionBar is straightforward and is most easily done via styles. See the "Background" section of http://android-developers.blogspot.com/2011/04/customizing-action-bar.html
You can also change it via code. Put this in your onCreate():
GradientDrawable gradientDrawable = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[] {Color.RED, Color.GREEN});
getActionBar().setBackgroundDrawable(gradientDrawable);
Here is a screenshot of this code in action: