Leanback Search fragment closing after gaining focus on results - android

In the leanback search fragment when we have committed a search with the keyboard and we have search results the fragment is closing when the search query is submitted with the back button. We can see that on action down of the back button the results are gaining focus and the keyboard is hiding(as expected) but on action up the screen is closing.
I believe this is a bug in the leanback framework since this is reproducing in the leanback showcase.
I have also posted an issue https://github.com/googlesamples/leanback-showcase/issues/58
Is there a workaround to disable the closing of the screen?

I have found the source of the bug.
In the SearchSupportFragment there is a searchBarListener that is giving focus on results in onKeyboardDismiss, but this is called before dispatchKeyEvent and when the back button is handled the results are already focused and the screen is closing.
I have found a hacky sololution of this until the Leanback team fix it. In onViewCreated I have set my own searchBarListener and I have delayed the focus change
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//this is overriding the default searchResultProvider, because of a bug in it
view.findViewById<SearchBar>(R.id.lb_search_bar).setSearchBarListener(object : SearchBar.SearchBarListener {
override fun onSearchQueryChange(query: String?) {
onQueryTextChange(query)
}
override fun onSearchQuerySubmit(query: String?) {
onQueryTextSubmit(query)
}
override fun onKeyboardDismiss(query: String?) {
searchHandler.postDelayed({ focusOnResults() }, 200)
}
})
}

Related

Android: Override Navigate Up in App Bar Defined in Fragment

I have an app bar defined from my fragment rather than activity by using
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.apply {
//add menu
inflateMenu(R.menu.menu_fragment)
//setup with navcontroller/navgraph
setupWithNavController(findNavController())
}
}
The problem I'm facing is trying to implement a warning message when a user clicks the Navigate Up button using the app bar. I want this behaviour only in one fragment.
I've found solutions online pertaining to app bars defined in an activity but they don't seem to work for me (such as using override fun onSupportNavigateUp().
Any ideas if I may be able to accomplish this?
Update
Initially, I implemented the chosen answer which worked but was causing some memory leaks. The kind individual who answered this question also found a workaround for the memory leaks here . Unfortunately, it didn't work so great for me (I believe because I am using navigation components) but it may work for you.
I later realized that I could easily override the navigate up default behaviour by adding this piece of line to my toolbar code:
binding.toolbar.apply {
//add menu
inflateMenu(R.menu.menu_fragment)
//setup with navcontroller/navgraph
setupWithNavController(findNavController())
//****************ADD THIS******************
setNavigationOnClickListener { view ->
//do what you want after user clicks navigate up button
}
}
The problem I'm facing is trying to implement a warning message when a user clicks the Navigate Up button using the app bar. I want this behaviour only in one fragment.
So, you just need to catch the event of hitting the UP button of the app bar for that particular fragment.
You can enable the options menu for that fragment:
setHasOptionsMenu(true)
And override onOptionsItemSelected to catch the UP button id:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
// Handle the UP button here
Toast.makeText(requireContext(), "UP button clicked", Toast.LENGTH_SHORT).show()
return true
}
return super.onOptionsItemSelected(item)
}
Note: if you want to use a unique toolbar for that fragment other than the default one, check this answer.
now I am unable to inflate my menu using inflateMenu(R.menu.menu_fragment). Any ideas?
You can remove this inflation, and instead override onCreateOptionsMenu for that:
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_fragment, menu)
}
onCreateOptionsMenu() didn't work for me,
I have write this code part in onCreate() for Activity; (navigationView is id my NavigationView)
for (i in 0 until navigationView.menu!!.size()) {
val item = navigationView.menu.getItem(i)
val s = SpannableString(item.title)
s.setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, s.length, 0)
item.title = s
}

BottomNavigation State gets lost after opening browser and then navigating back

Today, I discovered a very weird bug in my app. It goes the following: When the user navigates to the "profile screen", then opens the "information screen" and after that clicks on "agb", my app opens a browser and navigates the user to my website. Now the weird thing: When the user navigates back (to the "information screen", the bottomnavigation indicates, that he is currently on the home tab. Navigating back again (to the "profile screen"), then solves this issue. So it goes:
"Profile Screen" -> "Information Screen" -> Clicking on link -> "Opening Browser"
-> Clicking Back -> State gets lost -> "Information Screen" -> Clicking Back -> State gets restored -> "Profile Screen".
I added some pictures that show the error (red indicates the state, blue a action)
Navigation
Profile Screen (correct state)
Information Screen (correct state)
Browser (Probably state destroyed now)
Information Screen (state destroyed)
Profile Screen (state restored)
Fragment (Information Screen)
class UserInformationFragment : Fragment(R.layout.fragment_user_information) {
private var _binding: FragmentUserInformationBinding? = null
private val binding: FragmentUserInformationBinding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentUserInformationBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUserToolbar()
with(binding) {
agbCv.setOnClickListener { startBrowser(INTERNET_AGB) }
dataProtectionCv.setOnClickListener { startBrowser(INTERNET_DATA_PROTECTION) }
impressumCv.setOnClickListener { startBrowser(INTERNET_IMPRESSUM) }
}
}
private fun startBrowser(url: String) {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(browserIntent)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
One Idea I had was to set the state manually to the profile screen again after the user navigates back. But that would be a real mess and weird to do..
Navigation Version
navigation_version = 2.3.5
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
You have to use Deep link.
Suppose that you have fragment one that you want to come back from browser to it.
Add deep link tag to it like below:
mobile_navigation.xml
<fragment
android:id="#+id/fragment_one"
android:name="one"
android:label="one"
>
<deepLink
android:id="#+id/deepLink"
app:uri="www.Deeplinkuri.com" />
<action
</fragment>
You get www.Deeplinkuri.com example URI from back end team and add to your fragment.
You should ask a link from server side.
NOW WHEN YOU BACK FROM THE BROWSER YOU CAN OPEN YOUR OWN PAGE.
Simplest way is keep your last selected state in local static variable and onResume() apply that selected state from the static variable.
companion object {
const val LAST_SELECTED_STATE = SHOULD_BE_LAST_SELECTED_MENU_ID
}
override fun onResume() {
yourBottomNavigation. setSelectedItemId = LAST_SELECTED_STATE
}
Okay, I've managed to solve this problem. The problem had nothing to do with opening the browser, but with the recreation of my activity and reinflating of my bottomnavigation#menu. Before I had this:
private fun setUpBottomNav(newMenu: Int) {
with(binding.bottomNavigationView) {
menu.clear()
inflateMenu(newMenu)
setupWithNavController(findNavController(R.id.fragment_container))
// fix blinking when re selecting bottom nav item
setOnItemReselectedListener {}
}
}
The problem was that setUpBottomNav() was called, after getting back from the browser to my app, because the activity was recreated. The new solution is this:
private fun setUpBottomProfile(newMenu: Int) {
val controller = (supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment).navController
with(binding.bottomNavigationView) {
val currentSelectedItem = selectedItemId
menu.clear()
inflateMenu(newMenu)
selectedItemId = currentSelectedItem
setupWithNavController(controller)
// fix blinking when re selecting bottom nav item
setOnItemReselectedListener {}
}
}

TextView selection, event for the moment when selection is done?

How can I get notified at the moment when the selection menu appears for TextView?
See the screen recording below. If you select some text, context menu (copy/share/select all) appears. If you begin to drag the blue drag handle, the context menu disappears, and once you release the handle, the menu appears again. So, basically, the menu appears only when selection is done, not whilst you are still selecting the text.
I want to get notified when selection is done (i.e., the same time as the menu appears). I thought that onPrepareActionMode would be called when the selection is done and the menu appears, but after testing with the code below, it seemed that onPrepareActionMode is continuously called whilst I am dragging the handle, even when the selection menu is not visible. Also, it often got called twice for a single dragging. So onPrepareActionMode does not seem to be then answer. Then what is?
object: ActionMode.Callback{
#SuppressLint("ResourceType")
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean
{
Log.d("test", "onCreateActionMode");
return true;
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean
{
Log.d("test", "onPrepareActionMode");
return false
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean
{
Log.d("test", "onActionItemClicked");
return false;
}
override fun onDestroyActionMode(mode: ActionMode?)
{
Log.d("test", "onDestroyActionMode");
}
};
No answer? I thought that this would be a common requirement to do something automatically when text is selected. Anyway, I spent a lot of time trying to find a way and it all failed. But the hint came from a random thing: I have noticed that whenever the text selection is done and the menu appears,
W/androidtc: TextClassifier called on main thread
is printed in the Logcat. That is how I found that there is TextClassifier in TextView. So, I tried the following code
val tc = object:TextClassifier{
override fun classifyText(request: TextClassification.Request): TextClassification
{
Log.d("test", "classifyText")
return super.classifyText(request)
}
}
text2.setTextClassifier(tc);
, and as I suspected, classifyText was called when the selection is done, not during I was dragging the selection handle. This is also called when the selection is closed (tap elsewhere), but I guess I can probably filter that out by checking if the selected text length is 0. I will use this workaround until someone who knows better posts a more elegant and correct solution.
PS: The code above works on Android 10/11/12, but caused a runtime exception on Android 8.1. I guess that it is because there are no default implementations for methods on Android 8.1.
For Android 8.1, I have tried the following and it worked. If you do not care about classifier itself, I guess you do not have to pass the default classifier and use the commented-out dummy return values.
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textClassificationManager = getSystemService(Context.TEXT_CLASSIFICATION_SERVICE) as TextClassificationManager;
val defaultOne = textClassificationManager.textClassifier;
val txt = findViewById<TextView>(R.id.textview1);
txt.setTextClassifier(MyTextClassfier(defaultOne));
}
inner class MyTextClassfier(private val fallback:TextClassifier) : TextClassifier by fallback
{
override fun suggestSelection(
text: CharSequence,
selectionStartIndex: Int,
selectionEndIndex: Int,
defaultLocales: LocaleList?
): TextSelection
{
return fallback.suggestSelection(text, selectionStartIndex, selectionEndIndex, defaultLocales);
//return TextSelection.Builder(selectionStartIndex, request.getEndIndex()).build();
}
override fun classifyText(
text: CharSequence,
startIndex: Int,
endIndex: Int,
defaultLocales: LocaleList?
): TextClassification
{
//Selection ended. User has lifted his finger.
return fallback.classifyText(text, startIndex, endIndex, defaultLocales);
//return TextClassification.Builder().build();
}
}

Exposed Dropdown Menu not showing items

Exposed Dropdown Menu doesn't show items after user selection and fragment transition.
Following is the basic xml declaration:
<com.google.android.material.textfield.TextInputLayout
...
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
<AutoCompleteTextView
....
android:id="#+id/dropdown"
android:dropDownHeight="300dp"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
And, the declared code on the fragment (inside onViewCreated()):
val items = listOf("Material", "Design", "Components", "Android")
val adapter = ArrayAdapter(requireContext(), R.layout.item_menu, items)
dropdown.setAdapter(adapter)
dropdown.setText(items[0], false)
As mentioned here, it was set on AutoCompleteTextView's setText method (dropdown.setText("", false)) the filter parameter as false. However, after navigating to a next fragment and coming back to it only the pre-selected text is shown on the dropdown.
Fragments are changed using navigation component (v. 2.3.2).
The fragment's view gets destroyed when using the navigation component. (maybe not always, but it will certainly happen some of the time as you experienced)
I think you might be able to make it work simply by adding a condition:
if (savedInstanceState == null) {
dropdown.setText(items[0], false)
}
So that the default is only set when not restoring the view state.
Otherwise it's just a matter saving the state as usual. Here's a documentation article about it if you're unsure what I'm talking about. It will essentially amount to adding the following code to your fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val item = savedInstanceState?.getInt("selectedPos", 0) ?: 0
dropdown.setText(items[item], false)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("selectedPos", dropdown.getListSelection())
}
If you're using the MVVM architecture, you can save the selected position using SavedStateHandle in your ViewModel, when it gets changed.
I had the same problem. I searched for issues on github page. I found this https://github.com/material-components/material-components-android/issues/2012#issuecomment-808853621 work around for now. It works.
Create an extension like below
fun AutoCompleteTextView.showDropdown(adapter: ArrayAdapter<String>?) {
if(!TextUtils.isEmpty(this.text.toString())){
adapter?.filter?.filter(null)
}
}
Then on click of dropdown
binding.quaters.setOnClickListener {
binding.quaters.showDropdown(arrayAdapter)
}
That's all it should work. This seems to be a bug which should be fixed hopefully.
This is a temprorary solution that is working for me -
https://github.com/material-components/material-components-android/issues/2012#issuecomment-868181589
Write the setup code for ExposedDropdownMenu in onResume() of a fragment,
instead of onCreateView()/onViewCreated()
override fun onResume() {
super.onResume()
val sortingArtist = resources.getStringArray(R.array.sortingArtist)
val arrayAdapterArtist = ArrayAdapter(requireContext(), R.layout.dropdown_items_artist, sortingArtist)
binding?.autoCompleteTextViewArtist?.setAdapter(arrayAdapterArtist)
binding?.autoCompleteTextViewArtist?.setText(sortingArtist[0], false)
}
For reference - https://material.io/components/menus/android#exposed-dropdown-menus

Prevent views from being clicked until animation added to action via navigation component completes

Given 2 Fragments A and B, A moves to B (so A -> B), via navigation component action with enter animation has been added. How to prevent views in Fragment B being clickable while enter animation is running? I've found this question How to add listener to android Navigation Architecture Component action animation but unfortunately there're no answers.
What I found in the documentation is that I could get resource ID of that animation through NavOptions object hooked onto the NavAction, but not the Animation object itself.
You can start by having your views as disabled in xml android:enabled="false" then in your fragment's onViewCreated you can set a delay with the animation duration using coroutines:
override fun onViewCreated(view: View, savedState: Bundle?) {
super.onViewCreated(view, savedState)
// Initialize views here.
lifecycleScope.launch {
delay(resources.getInteger(R.integer.anim_duration).toLong())
// Enable views here
myView.isEnabled = true
}
}
While I originally solved the problem using coroutines I faced the same problem
once again :] so I investigated a bit and stumbled upon this topic Disable clicks when fragment adding animation playing that helped me to figure out the right solution.
Apparently those action animations added through the navigation graph are
set by FragmentTransaction.setCustomAnimation(enter, exit, popEnter, popExit)
and these can be accessed by overriding onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int). Where nextAnim actually represents the action animations we added. For the fragment A it would be either exit or popEnter and for the fragment B it would be either enter or popExit.
The problem of views being clicked happens when fragment is entering (either enter or popEnter) so one can use an if statement to check enter and if true create Animation based on the nextAnim and then one can set listener to it. In case of home (starting) fragment one should exclude the case of nextAnim = 0 since it's also entering animation.
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (nextAnim == 0 || !enter) return super.onCreateAnimation(transit, enter, nextAnim)
else {
return AnimationUtils.loadAnimation(requireContext(), nextAnim).apply {
setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation?) {
disableClicking()
}
override fun onAnimationEnd(animation: Animation?) {
enableClicking()
}
override fun onAnimationRepeat(animation: Animation?) {
}
})
}
}
}
EDIT: For non-home fragments to avoid disabling clicks at the start of the animation, we can start with views being unclickable in xml layout and only enable clicking when the animation ends. To remove a bug where views remain unclickable if a device rotation happens we can introduce a boolean variable that we will set to true when animation ends and preserve it by overriding onSaveInstanceState(outState: Bundle) and reinstating it in onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) and check if it was true before device rotation to re-enable clicking once again.

Categories

Resources