瀏覽代碼

ui: refactor: Use ViewPager2 instead of an ad-hoc spinner for slot selection

Peter Cai 1 年之前
父節點
當前提交
3ffd847af7

+ 48 - 65
app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt

@@ -12,28 +12,43 @@ import android.util.Log
 import android.view.Menu
 import android.view.MenuItem
 import android.view.View
-import android.widget.AdapterView
-import android.widget.ArrayAdapter
 import android.widget.ProgressBar
-import android.widget.Spinner
 import androidx.fragment.app.Fragment
-import androidx.fragment.app.commitNow
 import androidx.lifecycle.lifecycleScope
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import androidx.viewpager2.widget.ViewPager2
+import com.google.android.material.tabs.TabLayout
+import com.google.android.material.tabs.TabLayoutMediator
 import im.angry.openeuicc.common.R
 import im.angry.openeuicc.util.*
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
+@SuppressLint("NotifyDataSetChanged")
 open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
     companion object {
         const val TAG = "MainActivity"
     }
 
-    private lateinit var spinnerAdapter: ArrayAdapter<String>
-    private lateinit var spinnerItem: MenuItem
-    private lateinit var spinner: Spinner
     private lateinit var loadingProgress: ProgressBar
+    private lateinit var tabs: TabLayout
+    private lateinit var viewPager: ViewPager2
+
+    private data class Page(
+        val title: String,
+        val createFragment: () -> Fragment
+    )
+
+    private val pages: MutableList<Page> = mutableListOf()
+
+    private val pagerAdapter by lazy {
+        object : FragmentStateAdapter(this) {
+            override fun getItemCount() = pages.size
+
+            override fun createFragment(position: Int): Fragment = pages[position].createFragment()
+        }
+    }
 
     var loading: Boolean
         get() = loadingProgress.visibility == View.VISIBLE
@@ -45,8 +60,6 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
             }
         }
 
-    private val fragments = arrayListOf<Fragment>()
-
     protected lateinit var tm: TelephonyManager
 
     private val usbReceiver = object : BroadcastReceiver() {
@@ -63,10 +76,15 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
         setContentView(R.layout.activity_main)
         setSupportActionBar(requireViewById(R.id.toolbar))
         loadingProgress = requireViewById(R.id.loading)
+        tabs = requireViewById(R.id.main_tabs)
+        viewPager = requireViewById(R.id.view_pager)
 
-        tm = telephonyManager
+        viewPager.adapter = pagerAdapter
+        TabLayoutMediator(tabs, viewPager) { tab, pos ->
+            tab.text = pages[pos].title
+        }.attach()
 
-        spinnerAdapter = ArrayAdapter<String>(this, R.layout.spinner_item)
+        tm = telephonyManager
 
         registerReceiver(usbReceiver, IntentFilter().apply {
             addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
@@ -81,37 +99,6 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
 
     override fun onCreateOptionsMenu(menu: Menu): Boolean {
         menuInflater.inflate(R.menu.activity_main, menu)
-
-        if (!this::spinner.isInitialized) {
-            spinnerItem = menu.findItem(R.id.spinner)
-            spinner = spinnerItem.actionView as Spinner
-            if (spinnerAdapter.isEmpty) {
-                spinnerItem.isVisible = false
-            }
-            spinner.adapter = spinnerAdapter
-            spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
-                override fun onItemSelected(
-                    parent: AdapterView<*>?,
-                    view: View?,
-                    position: Int,
-                    id: Long
-                ) {
-                    if (position < fragments.size) {
-                        supportFragmentManager.beginTransaction()
-                            .replace(R.id.fragment_root, fragments[position]).commit()
-                    }
-                }
-
-                override fun onNothingSelected(parent: AdapterView<*>?) {
-                }
-
-            }
-        } else {
-            // Fragments may cause this menu to be inflated multiple times.
-            // Simply reuse the action view in that case
-            menu.findItem(R.id.spinner).actionView = spinner
-        }
-
         return true
     }
 
@@ -136,6 +123,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
 
     private suspend fun init() {
         loading = true
+        viewPager.visibility = View.GONE
+        tabs.visibility = View.GONE
 
         val knownChannels = withContext(Dispatchers.IO) {
             euiccChannelManager.enumerateEuiccChannels().onEach {
@@ -156,28 +145,25 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
             loading = false
 
             knownChannels.sortedBy { it.logicalSlotId }.forEach { channel ->
-                spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId))
-                fragments.add(appContainer.uiComponentFactory.createEuiccManagementFragment(channel))
+                pages.add(Page(
+                    getString(R.string.channel_name_format, channel.logicalSlotId)
+                ) { appContainer.uiComponentFactory.createEuiccManagementFragment(channel) })
             }
 
             // If USB readers exist, add them at the very last
             // We use a wrapper fragment to handle logic specific to USB readers
             usbDevice?.let {
-                spinnerAdapter.add(it.productName)
-                fragments.add(UsbCcidReaderFragment())
+                //spinnerAdapter.add(it.productName)
+                pages.add(Page(it.productName ?: "USB") { UsbCcidReaderFragment() })
             }
-
-            if (fragments.isNotEmpty()) {
-                if (this@MainActivity::spinner.isInitialized) {
-                    spinnerItem.isVisible = true
-                }
-                supportFragmentManager.beginTransaction()
-                    .replace(R.id.fragment_root, fragments.first()).commit()
-            } else {
-                supportFragmentManager.beginTransaction().replace(
-                    R.id.fragment_root,
-                    appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment()
-                ).commit()
+            pagerAdapter.notifyDataSetChanged()
+            viewPager.visibility = View.VISIBLE
+
+            if (pages.size > 1) {
+                tabs.visibility = View.VISIBLE
+            } else if (pages.isEmpty()) {
+                pages.add(Page("") { appContainer.uiComponentFactory.createNoEuiccPlaceholderFragment() })
+                pagerAdapter.notifyDataSetChanged()
             }
         }
     }
@@ -185,14 +171,11 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
     private fun refresh() {
         lifecycleScope.launch {
             loading = true
+            viewPager.visibility = View.GONE
+            tabs.visibility = View.GONE
 
-            supportFragmentManager.commitNow {
-                fragments.forEach {
-                    remove(it)
-                }
-            }
-            fragments.clear()
-            spinnerAdapter.clear()
+            pages.clear()
+            pagerAdapter.notifyDataSetChanged()
 
             init()
         }

+ 5 - 0
app-common/src/main/res/drawable/ic_refresh_black.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
+</vector>

+ 16 - 4
app-common/src/main/res/layout/activity_main.xml

@@ -14,6 +14,17 @@
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintWidth_percent="1" />
 
+    <com.google.android.material.tabs.TabLayout
+        android:id="@+id/main_tabs"
+        android:background="?attr/colorSurfaceVariant"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:tabTextColor="?attr/colorOnSurfaceVariant"
+        app:tabSelectedTextColor="?attr/colorOnSurfaceVariant"
+        app:layout_constraintTop_toBottomOf="@id/toolbar"
+        app:layout_constraintStart_toStartOf="parent" />
+
     <ProgressBar
         android:id="@+id/loading"
         android:layout_width="wrap_content"
@@ -21,16 +32,17 @@
         android:indeterminate="true"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/toolbar"
+        app:layout_constraintTop_toBottomOf="@id/main_tabs"
         app:layout_constraintBottom_toBottomOf="parent" />
 
-    <FrameLayout
-        android:id="@+id/fragment_root"
+    <androidx.viewpager2.widget.ViewPager2
+        android:id="@+id/view_pager"
         android:layout_width="0dp"
         android:layout_height="0dp"
+        android:visibility="gone"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/toolbar"/>
+        app:layout_constraintTop_toBottomOf="@id/main_tabs"/>
 
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 2 - 7
app-common/src/main/res/menu/activity_main.xml

@@ -1,16 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto">
-    <item
-        android:id="@+id/spinner"
-        android:title=""
-        app:actionViewClass="android.widget.Spinner"
-        app:showAsAction="always" />
-
     <item
         android:id="@+id/reload"
         android:title="@string/reload"
-        app:showAsAction="never" />
+        android:icon="@drawable/ic_refresh_black"
+        app:showAsAction="ifRoom" />
 
     <item
         android:id="@+id/settings"

+ 1 - 0
app-deps/build.gradle.kts

@@ -50,6 +50,7 @@ dependencies {
     api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
     api("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
     api("androidx.cardview:cardview:1.0.0")
+    api("androidx.viewpager2:viewpager2:1.1.0")
     api("androidx.datastore:datastore-preferences:1.0.0")
     api("com.journeyapps:zxing-android-embedded:4.3.0")
     testImplementation("junit:junit:4.13.2")