@@ -4,17 +4,50 @@ import android.os.Bundle
44import android.view.View
55import androidx.appcompat.app.AppCompatActivity
66import androidx.fragment.app.Fragment
7+ import androidx.viewpager2.adapter.FragmentStateAdapter
8+ import androidx.viewpager2.widget.ViewPager2
9+ import com.google.android.material.button.MaterialButton
710import com.mg4.tasker.BuildConfig
811import com.mg4.tasker.R
912import com.mg4.tasker.databinding.ActivityMainBinding
1013import com.mg4.tasker.store.UpdateLaunchStore
1114
1215class MainActivity : AppCompatActivity () {
1316
17+ /* *
18+ * One screen of the application, in the order the top bar lists them.
19+ *
20+ * [tabId] is the top-bar button that selects it — the same button the page-change
21+ * callback marks as current, and the id the adapter keys its fragments on. The pair
22+ * being declared once is what keeps the button and the swipe agreeing about where
23+ * they lead.
24+ */
25+ private class Screen (val tabId : Int , val create : () -> Fragment )
26+
1427 private lateinit var binding: ActivityMainBinding
1528 private val consoleUnlock = ConsoleUnlock ()
1629 private var consoleVisible = BuildConfig .CONSOLE_VISIBLE_BY_DEFAULT
1730
31+ /* * Rebuilt by [rebuildScreens]; Console is absent until it is unlocked. */
32+ private var screens: List <Screen > = emptyList()
33+
34+ /* *
35+ * Reads [screens] on every call rather than capturing it, so unlocking the console
36+ * only needs a notify — the pages already built keep their fragments.
37+ *
38+ * The ids are the tab ids, not the positions: inserting Console in the middle of the
39+ * list shifts Configuration by one, and a position-keyed adapter would hand
40+ * Configuration's fragment to the console's page.
41+ */
42+ private val pagerAdapter by lazy {
43+ object : FragmentStateAdapter (this ) {
44+ override fun getItemCount () = screens.size
45+ override fun createFragment (position : Int ) = screens[position].create()
46+ override fun getItemId (position : Int ) = screens[position].tabId.toLong()
47+ override fun containsItem (itemId : Long ) = screens.any { it.tabId.toLong() == itemId }
48+ }
49+ }
50+
1851 override fun onCreate (savedInstanceState : Bundle ? ) {
1952 super .onCreate(savedInstanceState)
2053 binding = ActivityMainBinding .inflate(layoutInflater)
@@ -23,30 +56,34 @@ class MainActivity : AppCompatActivity() {
2356 consoleVisible = BuildConfig .CONSOLE_VISIBLE_BY_DEFAULT ||
2457 (savedInstanceState?.getBoolean(STATE_CONSOLE_VISIBLE ) == true )
2558 updateConsoleVisibility()
59+ rebuildScreens()
2660
27- // Stable is the offline channel: keep its support console out of the normal driver
28- // navigation, while still leaving it reachable to an on-vehicle diagnostician.
29- binding.tabDiagnostic.setOnClickListener {
30- if (! consoleVisible && consoleUnlock.onDiagnosticTap()) {
31- consoleVisible = true
32- updateConsoleVisibility()
33- }
34- }
61+ binding.content.adapter = pagerAdapter
62+ // Only the neighbours are kept alive. Holding all five would mean the diagnostic
63+ // and console fragments polling in the background of a screen nobody is reading.
64+ binding.content.offscreenPageLimit = 1
65+ binding.content.registerOnPageChangeCallback(object : ViewPager2 .OnPageChangeCallback () {
66+ override fun onPageSelected (position : Int ) = markCurrentPage(position)
67+ })
3568
36- binding.tabGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->
37- if (! isChecked) return @addOnButtonCheckedListener
38- show(
39- when (checkedId) {
40- R .id.tabHistory -> HistoryFragment ()
41- R .id.tabDiagnostic -> DiagnosticFragment ()
42- R .id.tabConsole -> ConsoleFragment ()
43- R .id.tabConfig -> ConfigFragment ()
44- else -> RulesFragment ()
69+ TAB_IDS .forEach { tabId ->
70+ tab(tabId).setOnClickListener {
71+ // Stable is the offline channel: keep its support console out of the normal
72+ // driver navigation, while still leaving it reachable to an on-vehicle
73+ // diagnostician. The tap that unlocks it still opens Diagnostic, so the
74+ // gesture never costs the diagnostician the screen they asked for.
75+ if (tabId == R .id.tabDiagnostic && ! consoleVisible &&
76+ consoleUnlock.onDiagnosticTap()
77+ ) {
78+ unlockConsole()
4579 }
46- )
80+ // Animated, so the button does the same thing the swipe does: the direction
81+ // of travel is what tells the driver where they are in the row.
82+ goTo(tabId, smooth = true )
83+ }
4784 }
4885
49- // Rotation restores the fragment via the FragmentManager : only re-select the
86+ // Rotation restores the page from the pager's own saved state : only pick the
5087 // initial tab on first show, otherwise the screen jumps back to the rules.
5188 if (savedInstanceState == null ) {
5289 // After an upgrade, make the new build's live vehicle/capability verdicts the
@@ -57,8 +94,11 @@ class MainActivity : AppCompatActivity() {
5794 } else {
5895 R .id.tabRules
5996 }
60- binding.tabGroup.check(initialTab)
97+ // Without animation: at startup there is no movement to explain, only a
98+ // starting screen.
99+ goTo(initialTab, smooth = false )
61100 }
101+ markCurrentPage(binding.content.currentItem)
62102
63103 // Unstable builds check for a newer pre-release; the stable flavor's UpdateHook is
64104 // a no-op and the stable APK contains no updater code at all.
@@ -70,17 +110,61 @@ class MainActivity : AppCompatActivity() {
70110 super .onSaveInstanceState(outState)
71111 }
72112
113+ private fun rebuildScreens () {
114+ screens = buildList {
115+ add(Screen (R .id.tabRules) { RulesFragment () })
116+ add(Screen (R .id.tabHistory) { HistoryFragment () })
117+ add(Screen (R .id.tabDiagnostic) { DiagnosticFragment () })
118+ if (consoleVisible) add(Screen (R .id.tabConsole) { ConsoleFragment () })
119+ add(Screen (R .id.tabConfig) { ConfigFragment () })
120+ }
121+ }
122+
123+ private fun unlockConsole () {
124+ consoleVisible = true
125+ updateConsoleVisibility()
126+ rebuildScreens()
127+ val index = screens.indexOfFirst { it.tabId == R .id.tabConsole }
128+ if (index >= 0 ) pagerAdapter.notifyItemInserted(index)
129+ }
130+
73131 private fun updateConsoleVisibility () {
74132 binding.tabConsole.visibility = if (consoleVisible) View .VISIBLE else View .GONE
75133 }
76134
77- private fun show ( fragment : Fragment ) {
78- supportFragmentManager.beginTransaction()
79- .replace( R .id.content, fragment)
80- .commit( )
135+ /* * Moves the pager to [tabId]'s page; a no-op for a tab that is not a page. */
136+ private fun goTo ( tabId : Int , smooth : Boolean ) {
137+ val index = screens.indexOfFirst { it.tabId == tabId }
138+ if (index >= 0 ) binding.content.setCurrentItem(index, smooth )
81139 }
82140
141+ /* *
142+ * Marks the top-bar tab of the page on screen.
143+ *
144+ * Only `isSelected` is set: fill, text, icon and stroke come from the
145+ * res/color/nav_tab_*.xml selectors applied by the Widget.MG4.NavTab style. Painting
146+ * the colors here as well would declare them twice, and it is the copy in code that
147+ * would win — including on the states (pressed, disabled) it knows nothing about.
148+ * `isSelected` also serves TalkBack, which announces the current destination.
149+ */
150+ private fun markCurrentPage (position : Int ) {
151+ screens.forEachIndexed { index, screen ->
152+ tab(screen.tabId).isSelected = index == position
153+ }
154+ }
155+
156+ private fun tab (tabId : Int ): MaterialButton = findViewById(tabId)
157+
83158 private companion object {
84159 const val STATE_CONSOLE_VISIBLE = " consoleVisible"
160+
161+ /* * Every tab in the bar, including the console's while it is still hidden. */
162+ val TAB_IDS = listOf (
163+ R .id.tabRules,
164+ R .id.tabHistory,
165+ R .id.tabDiagnostic,
166+ R .id.tabConsole,
167+ R .id.tabConfig
168+ )
85169 }
86170}
0 commit comments