Add "Refresh" accessibility menu (#3121)
* Add "Refresh" accessibility menu to TimelineFragment Per https://developer.android.com/reference/androidx/swiperefreshlayout/widget/SwipeRefreshLayout the layout does not provide accessibility events, and a menu item should be provided as an alternative method for refreshing the content. In `TimelineFragment`: - Implement the `MenuProvider` interface so it can populate the action bar menu in activities that host the fragment - Create a "Refresh" menu item, and refresh the state when it is selected `MainActivity` has to change how the menu is created, so that fragments can add items to it. In `MainActivity`: - Call `setSupportActionBar` so `mainToolbar` participates in menus - Implement the `MenuProvider` interface, and move menu creation there - Set the title via supportActionBar * Never show the refresh item as a menubar action Per guidelines in https://developer.android.com/develop/ui/views/touch-and-input/swipe/add-swipe-interface#AddRefreshAction * Add "Refresh" menu item for AccountMediaFragment Also, fix the colour of the refresh progress indicator * Implement "Refresh" for AnnouncementsActivity * Add "Refresh" menu for ConversationsFragment * Keep the tabs adapter over the life of the viewpager Make `tabs` `var` instead of `val` in `MainPagerAdapter` so it can be updated when tabs change. Then detach the `tabLayoutMediator`, update the tabs, and call `notifyItemRangeChanged` in `setupTabs()`. This fixes a bug (not sure if it's this code, or in ViewPager2) where assigning a new adapter to the view pager seemed to result in a leak of one or more fragments. This wasn't user-visible, but it's a leak, and it becomes user-visible when fragments want to display menus. This also fixes two other bugs: 1. Be on the left-most tab. Scroll down a bit. Then modify the tabs at "Account preferences > tabs", but keep the left-most tab as-is. Then go back to MainActivity. Your reading position in the left-most tab has been jumped to the top. 2. Be on any non-left-most tab. Then modify the tab list by reordering tabs (adding/removing tabs is also OK). Then go back to MainActivity. Your tab selection has been overridden, and the left-most tab has been selected. Because the fragments are not destroyed unnecessarily your reading position is retained. And it remembers the tab you had selected, and as long as that tab is still present you will be returned to it, even if it's changed position in the list. Fixes https://github.com/tuskyapp/Tusky/issues/3251 * Add "Refresh" menu for ScheduledStatusActivity * Lint * Add "Refresh" menu for SearchFragment / SearchActivity * Explicitly set the searchview width Using "collapseActionView" requires the user to press "Back" twice to exit the activity, which is not acceptable. * Move toolbar handling in to ViewThreadActivity Previous code had the toolbar in the fragment's layout. Refactor to make consistent with other activities, and move the toolbar in to the activity layout. Implement MenuProvider in ViewThreadFragment to adjust the menu in the activity. * Add "Refresh" menu to ViewThreadFragment * Implement "Refresh" for ViewEditsFragment * Lint * Add "Refresh" menu to ReportStatusesFragment * Add "Refresh" menu to NotificationsFragment * Rename menu resource files Be consistent with the layout resource files, which have an activity/fragment prefix, then the lower_snake_case name of the activity or fragment it's for. * Only enable refresh menu if swiptorefresh is enabled Some timelines don't have swipetorefresh enabled (e.g., those shown on AccountActivity). In those cases don't add the refresh menu, rely on the hosting activity to provide it. Update AccountActivity to provide the refresh menu item.
This commit is contained in:
parent
f9588b48e2
commit
1b6108ca94
32 changed files with 671 additions and 144 deletions
|
|
@ -20,8 +20,11 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.keylesspalace.tusky.BottomSheetActivity
|
||||
|
|
@ -37,7 +40,7 @@ import dagger.android.DispatchingAndroidInjector
|
|||
import dagger.android.HasAndroidInjector
|
||||
import javax.inject.Inject
|
||||
|
||||
class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||
class SearchActivity : BottomSheetActivity(), HasAndroidInjector, MenuProvider {
|
||||
@Inject
|
||||
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
||||
|
||||
|
|
@ -59,6 +62,7 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
setDisplayShowHomeEnabled(true)
|
||||
setDisplayShowTitleEnabled(false)
|
||||
}
|
||||
addMenuProvider(this)
|
||||
setupPages()
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
|
@ -81,17 +85,18 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
handleIntent(intent)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.search_toolbar, menu)
|
||||
val searchView = menu.findItem(R.id.action_search)
|
||||
.actionView as SearchView
|
||||
val searchViewMenuItem = menu.findItem(R.id.action_search)
|
||||
searchViewMenuItem.expandActionView()
|
||||
val searchView = searchViewMenuItem.actionView as SearchView
|
||||
setupSearchView(searchView)
|
||||
|
||||
searchView.setQuery(viewModel.currentQuery, false)
|
||||
}
|
||||
|
||||
return true
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
|
|
@ -116,17 +121,42 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
private fun setupSearchView(searchView: SearchView) {
|
||||
searchView.setIconifiedByDefault(false)
|
||||
|
||||
searchView.setSearchableInfo((getSystemService(Context.SEARCH_SERVICE) as? SearchManager)?.getSearchableInfo(componentName))
|
||||
|
||||
searchView.requestFocus()
|
||||
// SearchView has a bug. If it's displayed 'app:showAsAction="always"' it's too wide,
|
||||
// pushing other icons (including the options menu '...' icon) off the edge of the
|
||||
// screen.
|
||||
//
|
||||
// E.g., see:
|
||||
//
|
||||
// - https://stackoverflow.com/questions/41662373/android-toolbar-searchview-too-wide-to-move-other-items
|
||||
// - https://stackoverflow.com/questions/51525088/how-to-control-size-of-a-searchview-in-toolbar
|
||||
// - https://stackoverflow.com/questions/36976163/push-icons-away-when-expandig-searchview-in-android-toolbar
|
||||
// - https://issuetracker.google.com/issues/36976484
|
||||
//
|
||||
// The fix is to use 'app:showAsAction="ifRoom|collapseActionView"' and then immediately
|
||||
// expand it after inflating. That sets the width correctly.
|
||||
//
|
||||
// But if you do that code in AppCompatDelegateImpl activates, and when the user presses
|
||||
// the "Back" button the SearchView is first set to its collapsed state. The user has to
|
||||
// press "Back" again to exit the activity. This is clearly unacceptable.
|
||||
//
|
||||
// It appears to be impossible to override this behaviour on API level < 33.
|
||||
//
|
||||
// SearchView does allow you to specify the maximum width. So take the screen width,
|
||||
// subtract 48dp * 2 (for the menu icon and back icon on either side), convert to pixels,
|
||||
// and use that.
|
||||
val pxScreenWidth = resources.displayMetrics.widthPixels
|
||||
val pxBuffer = ((48 * 2) * resources.displayMetrics.density).toInt()
|
||||
searchView.maxWidth = pxScreenWidth - pxBuffer
|
||||
|
||||
searchView.maxWidth = Integer.MAX_VALUE
|
||||
searchView.requestFocus()
|
||||
}
|
||||
|
||||
override fun androidInjector() = androidInjector
|
||||
|
||||
companion object {
|
||||
const val TAG = "SearchActivity"
|
||||
fun getIntent(context: Context) = Intent(context, SearchActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
package com.keylesspalace.tusky.components.search.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
|
|
@ -12,6 +17,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.BottomSheetActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
|
|
@ -25,6 +31,10 @@ import com.keylesspalace.tusky.interfaces.LinkListener
|
|||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -34,7 +44,8 @@ abstract class SearchFragment<T : Any> :
|
|||
Fragment(R.layout.fragment_search),
|
||||
LinkListener,
|
||||
Injectable,
|
||||
SwipeRefreshLayout.OnRefreshListener {
|
||||
SwipeRefreshLayout.OnRefreshListener,
|
||||
MenuProvider {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
|
@ -58,6 +69,7 @@ abstract class SearchFragment<T : Any> :
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
initAdapter()
|
||||
setupSwipeRefreshLayout()
|
||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
subscribeObservables()
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +107,27 @@ abstract class SearchFragment<T : Any> :
|
|||
}
|
||||
}
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.fragment_timeline, menu)
|
||||
menu.findItem(R.id.action_refresh)?.apply {
|
||||
icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_refresh).apply {
|
||||
sizeDp = 20
|
||||
colorInt = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return when (menuItem.itemId) {
|
||||
R.id.action_refresh -> {
|
||||
binding.swipeRefreshLayout.isRefreshing = true
|
||||
onRefresh()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAdapter() {
|
||||
binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL))
|
||||
binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue