Skip to content

Commit 64cfb07

Browse files
committed
Improve auto-scroll to current tab item
It should center on target item. We gave up on the speed tweaks for now. Close #668.
1 parent 95da4f3 commit 64cfb07

File tree

3 files changed

+126
-27
lines changed

3 files changed

+126
-27
lines changed

app/src/main/java/fulguris/activity/WebBrowserActivity.kt

+25-22
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ import java.util.*
131131
import javax.inject.Inject
132132
import kotlin.math.abs
133133
import kotlin.system.exitProcess
134+
import kotlin.time.TimeSource
134135

135136

136137
/**
@@ -463,7 +464,6 @@ abstract class WebBrowserActivity : ThemedBrowserActivity(),
463464

464465
// Hook in buttons with onClick handler
465466
iBindingToolbarContent.buttonReload.setOnClickListener(this)
466-
467467
}
468468

469469
/**
@@ -510,9 +510,6 @@ abstract class WebBrowserActivity : ThemedBrowserActivity(),
510510
mainHandler.postDelayed({
511511
setupToolBar()
512512
setupPullToRefresh(aConfig)
513-
// For embedded tab bars modes
514-
tryScrollToCurrentTab()
515-
516513
iBinding.drawerLayout.requestLayout()
517514
},500);
518515

@@ -610,7 +607,9 @@ abstract class WebBrowserActivity : ThemedBrowserActivity(),
610607
// See: https://github.com/material-components/material-components-android/issues/2168
611608
tabsDialog = createBottomSheetDialog(tabsView as View)
612609
// Once our bottom sheet is open we want it to scroll to current tab
613-
tabsDialog.setOnShowListener { tryScrollToCurrentTab() }
610+
tabsDialog.setOnShowListener {
611+
tryScrollToCurrentTab()
612+
}
614613
/*
615614
tabsDialog.behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
616615
override fun onStateChanged(bottomSheet: View, newState: Int) {
@@ -2901,7 +2900,7 @@ abstract class WebBrowserActivity : ThemedBrowserActivity(),
29012900
showActionBar()
29022901
// Make sure current tab is visible in tab list
29032902
tryScrollToCurrentTab()
2904-
//mainHandler.postDelayed({ scrollToCurrentTab() }, 0)
2903+
//mainHandler.postDelayed({ tryScrollToCurrentTab() }, 0)
29052904

29062905
// Current tab was already set by the time we get here
29072906
tabsManager.currentTab?.let {
@@ -4092,32 +4091,36 @@ abstract class WebBrowserActivity : ThemedBrowserActivity(),
40924091
}
40934092

40944093
/**
4095-
* Never call that function direction.
4094+
* Never call that function directly.
40964095
* Just call [tryScrollToCurrentTab] instead.
40974096
*/
40984097
private fun scrollToCurrentTab() {
40994098
/*if (userPreferences.useBottomSheets && tabsView is TabsDrawerView && !(tabsDialog.isShowing && tabsDialog.behavior.state == BottomSheetBehavior.STATE_EXPANDED)) {
4100-
return
4101-
}*/
4099+
return
4100+
}*/
4101+
//Thread.dumpStack()
41024102

4103-
val tabListView = (tabsView as ViewGroup).findViewById<RecyclerView>(R.id.tabs_list)
4104-
// Set focus
41054103
// Find our recycler list view
4104+
val tabListView = (tabsView as ViewGroup).findViewById<RecyclerView>(R.id.tabs_list)
4105+
41064106
tabListView?.apply {
4107-
// Get current tab index and layout manager
4108-
val index = tabsManager.indexOfCurrentTab()
4109-
val lm = layoutManager as LinearLayoutManager
4110-
// Check if current item is currently visible
4111-
if (lm.findFirstCompletelyVisibleItemPosition() <= index && index <= lm.findLastCompletelyVisibleItemPosition()) {
4107+
if (smoothScrollToPositionEx(tabsManager.indexOfCurrentTab())) {
4108+
// Our current item is not completely visible, we need to scroll then
4109+
// Once scroll is complete we will focus our current item
4110+
val timeSource = TimeSource.Monotonic
4111+
val mark = timeSource.markNow();
4112+
onceOnScrollStateIdle {
4113+
// For some reason we need to try again to scroll to current tab as sometimes it fails on first or even second try,
4114+
// and lands somewhere else when we have over 600 tabs in our bottom sheet. Hopefully it won't get stuck in endless tries.
4115+
val elapsed = timeSource.markNow() - mark;
4116+
Timber.d("Scroll time: ${elapsed.inWholeMilliseconds} ms")
4117+
tryScrollToCurrentTab()
4118+
findViewHolderForAdapterPosition(tabsManager.indexOfCurrentTab())?.itemView?.requestFocus()
4119+
}
4120+
} else {
41124121
// We don't need to scroll as current item is already visible
41134122
// Just focus our current item then for best keyboard navigation experience
41144123
findViewHolderForAdapterPosition(tabsManager.indexOfCurrentTab())?.itemView?.requestFocus()
4115-
} else {
4116-
// Our current item is not completely visible, we need to scroll then
4117-
// Once scroll is complete we will focus our current item
4118-
onceOnScrollStateIdle { findViewHolderForAdapterPosition(tabsManager.indexOfCurrentTab())?.itemView?.requestFocus() }
4119-
// Trigger scroll
4120-
smoothScrollToPosition(index)
41214124
}
41224125
}
41234126
}

app/src/main/java/fulguris/browser/tabs/TabsDrawerView.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.recyclerview.widget.DefaultItemAnimator
1717
import androidx.recyclerview.widget.ItemTouchHelper
1818
import androidx.recyclerview.widget.LinearLayoutManager
1919
import androidx.recyclerview.widget.RecyclerView
20+
import timber.log.Timber
2021

2122

2223
/**
@@ -110,9 +111,10 @@ class TabsDrawerView @JvmOverloads constructor(
110111
}
111112

112113
/**
113-
* TODO: this is called way to often for my taste and should be optimized somehow.
114+
* TODO: this is called way too often for my taste and should be optimized somehow.
114115
*/
115116
private fun displayTabs() {
117+
Timber.d("displayTabs");
116118
tabsAdapter.showTabs(webBrowser.getTabModel().allTabs.map(WebPageTab::asTabViewState))
117119

118120
if (fixScrollBug(iBinding.tabsList)) {

app/src/main/java/fulguris/extensions/ViewExtensions.kt

+98-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
package fulguris.extensions
22

3-
import fulguris.R
4-
import fulguris.utils.getFilteredColor
53
import android.annotation.SuppressLint
64
import android.content.Context
75
import android.content.res.Configuration
86
import android.graphics.*
97
import android.os.SystemClock
8+
import android.util.DisplayMetrics
109
import android.view.*
1110
import android.view.inputmethod.InputMethodManager
1211
import android.widget.ImageView
@@ -19,10 +18,14 @@ import androidx.core.view.isVisible
1918
import androidx.databinding.BindingAdapter
2019
import androidx.drawerlayout.widget.DrawerLayout
2120
import androidx.palette.graphics.Palette
21+
import androidx.recyclerview.widget.LinearLayoutManager
22+
import androidx.recyclerview.widget.LinearSmoothScroller
2223
import androidx.recyclerview.widget.RecyclerView
2324
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
25+
import fulguris.R
26+
import fulguris.utils.getFilteredColor
27+
import timber.log.Timber
2428
import java.lang.reflect.Method
25-
import java.util.ArrayList
2629

2730

2831
/**
@@ -214,6 +217,96 @@ inline fun RecyclerView?.onceOnScrollStateIdle(crossinline runnable: () -> Unit)
214217
})
215218
}
216219

220+
/**
221+
* If needed it triggers a scroll animation to show the specified item in this [RecyclerView].
222+
* Unfortunately [LinearSmoothScroller] won't let us define the exact duration of the animation.
223+
*
224+
* [aPosition] The index of the item you want to scroll to.
225+
* [aDurationInMs] The rough duration of the scroll animation in milliseconds.
226+
* [aOvershot] Specify if you want your scroll animation to overshot in order to put the target item roughly in the middle of this view, as opposed than on the edge.
227+
* [aSnapMode] See [LinearSmoothScroller].
228+
*
229+
* Returns true if a scroll was triggered, false otherwise.
230+
* Improved from: https://stackoverflow.com/a/65489113/3969362
231+
*/
232+
fun RecyclerView.smoothScrollToPositionEx(aPosition: Int, aDurationInMs: Int = 1000) : Boolean {
233+
234+
// First of all, check if we should be scrolling at all
235+
if (scrollState != RecyclerView.SCROLL_STATE_IDLE) {
236+
Timber.d("Already scrolling, skip it for now")
237+
return false
238+
}
239+
240+
// Can't do it without adapter
241+
adapter?.let { adaptor ->
242+
243+
val count = adaptor.itemCount
244+
var index = aPosition
245+
246+
247+
//adaptor.getItemViewType()
248+
249+
val lm = layoutManager as LinearLayoutManager
250+
// Check if current item is currently visible
251+
val minIndex = lm.findFirstCompletelyVisibleItemPosition()
252+
val maxIndex = lm.findLastCompletelyVisibleItemPosition()
253+
254+
// Check if our item is already visible
255+
if ( minIndex <= index && index <= maxIndex) {
256+
Timber.d("No need to scroll")
257+
return false
258+
}
259+
260+
val scrollDown = (index<minIndex) // && !configPrefs.toolbarsBottom
261+
val scrollFrom = if (scrollDown) minIndex else maxIndex
262+
263+
val scrollRange = if (scrollFrom>index) scrollFrom - index else index - scrollFrom
264+
Timber.d("Scroll range: $scrollRange")
265+
266+
// Trigger our scroll animation
267+
val smoothScroller = object : LinearSmoothScroller(this.context) {
268+
269+
// Center on our target item
270+
// See: https://stackoverflow.com/a/53756296/3969362
271+
override fun calculateDtToFit(viewStart: Int, viewEnd: Int, boxStart: Int, boxEnd: Int, snapPreference: Int): Int {
272+
return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
273+
}
274+
275+
// We disabled our speed tweak as it is really tricky to get it right for various use cases
276+
// Various tabs list variant, number of tabs or screen DPI...
277+
// The default implementation appears to be a good compromise after all.
278+
/*
279+
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float {
280+
// Compute our speed as function of the distance we need to scroll
281+
var speed = if ((layoutManager as? LinearLayoutManager)?.orientation == LinearLayoutManager.VERTICAL) {
282+
aDurationInMs.toFloat() / ((computeVerticalScrollRange().toFloat() / count) * scrollRange)
283+
} else {
284+
aDurationInMs.toFloat() / ((computeHorizontalScrollRange().toFloat() / count) * scrollRange)
285+
}
286+
287+
Timber.d("Scroll speed: $speed ms/pixel")
288+
289+
// Speed is expressed in ms/pixel so in fact min speed is the fastest one and max speed is the slowest one
290+
val minSpeed = 0.001f // Fastest
291+
val maxSpeed = 0.05f // Slowest
292+
// Make sure we don't go too fast or too slow, going too fast can break the LinearSmoothScroller and cause endless animation jitter
293+
if (speed<minSpeed) speed = minSpeed
294+
if (speed>maxSpeed) speed = maxSpeed
295+
296+
return speed
297+
}
298+
*/
299+
300+
}
301+
smoothScroller.targetPosition = index
302+
layoutManager?.startSmoothScroll(smoothScroller)
303+
304+
return true
305+
}
306+
307+
return false
308+
}
309+
217310
/**
218311
* Reset Swipe Refresh Layout target.
219312
* This is needed if you are changing the child scrollable view during the lifetime of your layout.
@@ -441,4 +534,5 @@ fun View.onConfigurationChange(aRunnable: () -> Unit) {
441534
// Could be useful to help understand what's going on when inspecting our views
442535
id = R.id.onConfigurationChange
443536
}) }
444-
}
537+
}
538+

0 commit comments

Comments
 (0)