From 0ff97ba3c4301e1b36c067c4c11c7203840341bc Mon Sep 17 00:00:00 2001 From: geeksville Date: Wed, 8 Apr 2020 15:25:57 -0700 Subject: [PATCH] node list view kinda works --- app/build.gradle | 2 + .../java/com/geeksville/mesh/MainActivity.kt | 16 +- .../java/com/geeksville/mesh/model/UIState.kt | 16 -- .../ui/{Channel.kt => ChannelFragment.kt} | 5 +- .../main/java/com/geeksville/mesh/ui/Users.kt | 137 ++++++++++++++++-- .../main/res/layout/adapter_node_layout.xml | 25 ++++ app/src/main/res/layout/nodelist_fragment.xml | 19 +++ app/src/main/res/values/styles.xml | 1 + 8 files changed, 182 insertions(+), 39 deletions(-) rename app/src/main/java/com/geeksville/mesh/ui/{Channel.kt => ChannelFragment.kt} (99%) create mode 100644 app/src/main/res/layout/adapter_node_layout.xml create mode 100644 app/src/main/res/layout/nodelist_fragment.xml diff --git a/app/build.gradle b/app/build.gradle index d35d8cb8f..25dbdee10 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -77,6 +77,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.2.0' implementation "androidx.fragment:fragment-ktx:1.2.4" + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.viewpager2:viewpager2:1.0.0' diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index c0c68e8bf..f423cfaaa 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -30,6 +30,7 @@ import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.service.* import com.geeksville.mesh.ui.ChannelFragment import com.geeksville.mesh.ui.MapFragment +import com.geeksville.mesh.ui.UsersFragment import com.geeksville.util.Exceptions import com.geeksville.util.exceptionReporter import com.google.android.gms.auth.api.signin.GoogleSignIn @@ -112,6 +113,11 @@ class MainActivity : AppCompatActivity(), Logging, // private val tabIndexes = generateSequence(0) { it + 1 } FIXME, instead do withIndex or zip? to get the ids below, also stop duplicating strings private val tabInfos = arrayOf( + TabInfo( + "Users", + R.drawable.ic_twotone_people_24, + UsersFragment() + ), TabInfo( "Channel", R.drawable.ic_twotone_contactless_24, @@ -128,10 +134,7 @@ class MainActivity : AppCompatActivity(), Logging, R.drawable.ic_twotone_message_24, ComposeFragment("Messages", 1) { MessagesContent() }), - TabInfo( - "Users", - R.drawable.ic_twotone_people_24, - ComposeFragment("Users", 3) { UsersContent() }), + TabInfo( "Settings", R.drawable.ic_twotone_settings_applications_24, @@ -470,9 +473,12 @@ class MainActivity : AppCompatActivity(), Logging, ServiceClient({ com.geeksville.mesh.IMeshService.Stub.asInterface(it) }) { - override fun onConnected(service: com.geeksville.mesh.IMeshService) { + override fun onConnected(service: com.geeksville.mesh.IMeshService) = exceptionReporter { model.meshService = service + debug("Getting latest radioconfig from service") + model.radioConfig.value = MeshProtos.RadioConfig.parseFrom(service.radioConfig) + // We don't start listening for packets until after we are connected to the service registerMeshReceiver() diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 68085e524..1b5e7b435 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -59,22 +59,6 @@ class UIViewModel : ViewModel(), Logging { /// various radio settings (including the channel) val radioConfig = object : MutableLiveData(null) { - /** - * Called when the number of active observers change to 1 from 0. - * - * - * This callback can be used to know that this LiveData is being used thus should be kept - * up to date. - */ - override fun onActive() { - super.onActive() - - // Get the current radio config from the service - meshService?.let { - debug("Getting latest radioconfig from service") - value = MeshProtos.RadioConfig.parseFrom(it.radioConfig) - } - } } override fun onCleared() { diff --git a/app/src/main/java/com/geeksville/mesh/ui/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt similarity index 99% rename from app/src/main/java/com/geeksville/mesh/ui/Channel.kt rename to app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt index ec8a66a2a..4d8136f2f 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -17,9 +17,6 @@ import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.channel_fragment.* -object ChannelLog : Logging - - class ChannelFragment : ScreenFragment("Channel"), Logging { private val model: UIViewModel by activityViewModels() @@ -67,7 +64,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { channelNameEdit.visibility = View.VISIBLE channelNameEdit.setText(channel.name) editableCheckbox.isEnabled = true - + qrView.setImageBitmap(channel.getChannelQR()) // Share this particular channel if someone clicks share shareButton.setOnClickListener { diff --git a/app/src/main/java/com/geeksville/mesh/ui/Users.kt b/app/src/main/java/com/geeksville/mesh/ui/Users.kt index a02b18036..035803dde 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Users.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Users.kt @@ -1,24 +1,133 @@ package com.geeksville.mesh.ui -/* -import androidx.compose.Composable -import androidx.ui.core.ContextAmbient -import androidx.ui.foundation.Text -import androidx.ui.layout.Column -import androidx.ui.layout.LayoutPadding -import androidx.ui.layout.Row -import androidx.ui.material.Button -import androidx.ui.unit.dp + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.geeksville.android.Logging +import com.geeksville.mesh.NodeInfo import com.geeksville.mesh.R -import com.geeksville.mesh.model.NodeDB -import com.geeksville.mesh.model.UIState -import com.geeksville.mesh.service.MeshService -import com.geeksville.mesh.service.RadioInterfaceService -import com.geeksville.mesh.service.SoftwareUpdateService +import com.geeksville.mesh.model.UIViewModel +import kotlinx.android.synthetic.main.adapter_node_layout.view.* +import kotlinx.android.synthetic.main.nodelist_fragment.* +class UsersFragment : ScreenFragment("Users"), Logging { + + private val model: UIViewModel by activityViewModels() + + // Provide a direct reference to each of the views within a data item + // Used to cache the views within the item layout for fast access + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val nodeNameView = itemView.nodeNameView + } + + private val nodesAdapter = object : RecyclerView.Adapter() { + + /** + * Called when RecyclerView needs a new [ViewHolder] of the given type to represent + * an item. + * + * + * This new ViewHolder should be constructed with a new View that can represent the items + * of the given type. You can either create a new View manually or inflate it from an XML + * layout file. + * + * + * The new ViewHolder will be used to display items of the adapter using + * [.onBindViewHolder]. Since it will be re-used to display + * different items in the data set, it is a good idea to cache references to sub views of + * the View to avoid unnecessary [View.findViewById] calls. + * + * @param parent The ViewGroup into which the new View will be added after it is bound to + * an adapter position. + * @param viewType The view type of the new View. + * + * @return A new ViewHolder that holds a View of the given view type. + * @see .getItemViewType + * @see .onBindViewHolder + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(requireContext()) + + // Inflate the custom layout + + // Inflate the custom layout + val contactView: View = inflater.inflate(R.layout.adapter_node_layout, parent, false) + + // Return a new holder instance + return ViewHolder(contactView) + } + + /** + * Returns the total number of items in the data set held by the adapter. + * + * @return The total number of items in this adapter. + */ + override fun getItemCount(): Int = nodes.size + + /** + * Called by RecyclerView to display the data at the specified position. This method should + * update the contents of the [ViewHolder.itemView] to reflect the item at the given + * position. + * + * + * Note that unlike [android.widget.ListView], RecyclerView will not call this method + * again if the position of the item changes in the data set unless the item itself is + * invalidated or the new position cannot be determined. For this reason, you should only + * use the `position` parameter while acquiring the related data item inside + * this method and should not keep a copy of it. If you need the position of an item later + * on (e.g. in a click listener), use [ViewHolder.getAdapterPosition] which will + * have the updated adapter position. + * + * Override [.onBindViewHolder] instead if Adapter can + * handle efficient partial bind. + * + * @param holder The ViewHolder which should be updated to represent the contents of the + * item at the given position in the data set. + * @param position The position of the item within the adapter's data set. + */ + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val n = nodes[position] + + holder.nodeNameView.text = n.user?.longName ?: n.user?.id ?: "Unknown node" + } + + private var nodes = arrayOf() + + /// Called when our node DB changes + fun onNodesChanged(nodesIn: Collection) { + nodes = nodesIn.toTypedArray() + notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.nodelist_fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + nodeListView.adapter = nodesAdapter + nodeListView.layoutManager = LinearLayoutManager(requireContext()) + + model.nodeDB.nodes.observe(viewLifecycleOwner, Observer { it -> + nodesAdapter.onNodesChanged(it.values) + }) + } +} +/* @Composable fun UsersContent() { Column { diff --git a/app/src/main/res/layout/adapter_node_layout.xml b/app/src/main/res/layout/adapter_node_layout.xml new file mode 100644 index 000000000..c5e83a04f --- /dev/null +++ b/app/src/main/res/layout/adapter_node_layout.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/nodelist_fragment.xml b/app/src/main/res/layout/nodelist_fragment.xml new file mode 100644 index 000000000..4e26df295 --- /dev/null +++ b/app/src/main/res/layout/nodelist_fragment.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ce0f6f1de..3431ac63d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -28,4 +28,5 @@ 48dp +