mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Feature: Jump to node info from message (#844)
* Highlight the node in the node list tab when the user taps on the node chip in messages * Represent main tabs as enum for more reliable referencing * Extract tab labels to string resources for easier translation Annotate resource IDs with their corresponding Android types * Index off nodes actually in the adapter since they are sorted * Update viewmodel when tab changes to prevent jumping to other tabs in onResume * Mark strings as non-translatable for now
This commit is contained in:
parent
a88ffbc0fb
commit
2bfda9784f
7 changed files with 167 additions and 37 deletions
|
|
@ -1,6 +1,8 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.method.LinkMovementMethod
|
||||
|
|
@ -9,14 +11,18 @@ import android.view.LayoutInflater
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LinearInterpolator
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.geeksville.mesh.NodeInfo
|
||||
import com.geeksville.mesh.R
|
||||
|
|
@ -27,6 +33,9 @@ import com.geeksville.mesh.model.UIViewModel
|
|||
import com.geeksville.mesh.util.formatAgo
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URLEncoder
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
|
@ -56,11 +65,34 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
|||
val powerIcon = itemView.batteryIcon
|
||||
val signalView = itemView.signalView
|
||||
val envMetrics = itemView.envMetrics
|
||||
val background = itemView.nodeCard
|
||||
|
||||
fun blink() {
|
||||
val bg = background.backgroundTintList
|
||||
ValueAnimator.ofArgb(
|
||||
Color.parseColor("#00FFFFFF"),
|
||||
Color.parseColor("#33FFFFFF")
|
||||
).apply {
|
||||
interpolator = LinearInterpolator()
|
||||
startDelay = 500
|
||||
duration = 250
|
||||
repeatCount = 3
|
||||
repeatMode = ValueAnimator.REVERSE
|
||||
addUpdateListener {
|
||||
background.backgroundTintList = ColorStateList.valueOf(it.animatedValue as Int)
|
||||
}
|
||||
start()
|
||||
doOnEnd {
|
||||
background.backgroundTintList = bg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val nodesAdapter = object : RecyclerView.Adapter<ViewHolder>() {
|
||||
|
||||
private var nodes = arrayOf<NodeInfo>()
|
||||
var nodes = arrayOf<NodeInfo>()
|
||||
private set
|
||||
|
||||
private fun CharSequence.strike() = SpannableString(this).apply {
|
||||
setSpan(StrikethroughSpan(), 0, this.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
|
@ -347,10 +379,46 @@ class UsersFragment : ScreenFragment("Users"), Logging {
|
|||
|
||||
model.clearTracerouteResponse()
|
||||
}
|
||||
|
||||
model.focusedNode.asLiveData().observe(viewLifecycleOwner) { node ->
|
||||
val idx = nodesAdapter.nodes.indexOfFirst {
|
||||
it.user?.id == node?.user?.id
|
||||
}
|
||||
|
||||
if (idx < 1) return@observe
|
||||
|
||||
lifecycleScope.launch {
|
||||
binding.nodeListView.layoutManager?.smoothScrollToTop(idx)
|
||||
val vh = binding.nodeListView.findViewHolderForLayoutPosition(idx)
|
||||
(vh as? ViewHolder)?.blink()
|
||||
model.focusUserNode(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the recycler view until the item at [position] is at the top of the view, then waits
|
||||
* until the scrolling is finished.
|
||||
*/
|
||||
private suspend fun RecyclerView.LayoutManager.smoothScrollToTop(position: Int) {
|
||||
this.startSmoothScroll(
|
||||
object : LinearSmoothScroller(requireContext()) {
|
||||
override fun getVerticalSnapPreference(): Int {
|
||||
return SNAP_TO_START
|
||||
}
|
||||
}.apply {
|
||||
targetPosition = position
|
||||
}
|
||||
)
|
||||
withContext(Dispatchers.Default) {
|
||||
while (this@smoothScrollToTop.isSmoothScrolling) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue