mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Material design for Message UI
This commit is contained in:
parent
de80afe5d7
commit
8ea58fd08c
5 changed files with 115 additions and 56 deletions
|
|
@ -813,7 +813,8 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
// Init our messages table with the service's record of past text messages (ignore all other message types)
|
// Init our messages table with the service's record of past text messages (ignore all other message types)
|
||||||
val msgs =
|
val msgs =
|
||||||
service.oldMessages.filter { p -> p.dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE }
|
service.oldMessages.filter { p -> p.dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE }
|
||||||
debug("Service provided ${msgs.size} messages")
|
debug("Service provided ${msgs.size} messages and myNodeNum ${service.myNodeInfo?.myNodeNum}")
|
||||||
|
model.myNodeInfo.value = service.myNodeInfo
|
||||||
model.messagesState.setMessages(msgs)
|
model.messagesState.setMessages(msgs)
|
||||||
val connectionState =
|
val connectionState =
|
||||||
MeshService.ConnectionState.valueOf(service.connectionState())
|
MeshService.ConnectionState.valueOf(service.connectionState())
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
|
@ -15,6 +16,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.geeksville.android.Logging
|
import com.geeksville.android.Logging
|
||||||
import com.geeksville.mesh.DataPacket
|
import com.geeksville.mesh.DataPacket
|
||||||
import com.geeksville.mesh.MessageStatus
|
import com.geeksville.mesh.MessageStatus
|
||||||
|
import com.geeksville.mesh.NodeInfo
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
|
import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
|
||||||
import com.geeksville.mesh.databinding.MessagesFragmentBinding
|
import com.geeksville.mesh.databinding.MessagesFragmentBinding
|
||||||
|
|
@ -22,6 +24,7 @@ import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.service.MeshService
|
import com.geeksville.mesh.service.MeshService
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
import java.text.ParseException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
// Allows usage like email.on(EditorInfo.IME_ACTION_NEXT, { confirm() })
|
// Allows usage like email.on(EditorInfo.IME_ACTION_NEXT, { confirm() })
|
||||||
|
|
@ -39,20 +42,35 @@ fun EditText.on(actionId: Int, func: () -> Unit) {
|
||||||
class MessagesFragment : ScreenFragment("Messages"), Logging {
|
class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
|
|
||||||
private var _binding: MessagesFragmentBinding? = null
|
private var _binding: MessagesFragmentBinding? = null
|
||||||
|
|
||||||
// This property is only valid between onCreateView and onDestroyView.
|
// This property is only valid between onCreateView and onDestroyView.
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private val model: UIViewModel by activityViewModels()
|
private val model: UIViewModel by activityViewModels()
|
||||||
|
|
||||||
private val dateTimeFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
private val dateTimeFormat: DateFormat =
|
||||||
|
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
|
||||||
|
private val timeFormat: DateFormat =
|
||||||
|
DateFormat.getTimeInstance(DateFormat.SHORT)
|
||||||
|
|
||||||
|
private fun getShortDateTime(time : Date): String {
|
||||||
|
// return time if within 24 hours, otherwise date/time
|
||||||
|
val one_day = 60*60*24*100L
|
||||||
|
if (System.currentTimeMillis() - time.time > one_day) {
|
||||||
|
return dateTimeFormat.format(time)
|
||||||
|
} else return timeFormat.format(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Provide a direct reference to each of the views within a data item
|
// 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
|
// Used to cache the views within the item layout for fast access
|
||||||
class ViewHolder(itemView: AdapterMessageLayoutBinding) : RecyclerView.ViewHolder(itemView.root) {
|
class ViewHolder(itemView: AdapterMessageLayoutBinding) :
|
||||||
|
RecyclerView.ViewHolder(itemView.root) {
|
||||||
val username: Chip = itemView.username
|
val username: Chip = itemView.username
|
||||||
val messageText: TextView = itemView.messageText
|
val messageText: TextView = itemView.messageText
|
||||||
val messageTime: TextView = itemView.messageTime
|
val messageTime: TextView = itemView.messageTime
|
||||||
val messageStatusIcon: ImageView = itemView.messageStatusIcon
|
val messageStatusIcon: ImageView = itemView.messageStatusIcon
|
||||||
|
val card: CardView = itemView.Card
|
||||||
}
|
}
|
||||||
|
|
||||||
private val messagesAdapter = object : RecyclerView.Adapter<ViewHolder>() {
|
private val messagesAdapter = object : RecyclerView.Adapter<ViewHolder>() {
|
||||||
|
|
@ -122,14 +140,32 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
*/
|
*/
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val msg = messages[position]
|
val msg = messages[position]
|
||||||
|
|
||||||
val nodes = model.nodeDB.nodes.value!!
|
val nodes = model.nodeDB.nodes.value!!
|
||||||
|
|
||||||
// If we can't find the sender, just use the ID
|
|
||||||
val node = nodes.get(msg.from)
|
val node = nodes.get(msg.from)
|
||||||
val user = node?.user
|
// Determine if this is my message (originated on this device).
|
||||||
holder.username.text = user?.shortName ?: msg.from
|
val isMe = model.myNodeInfo.value?.myNodeNum == node?.num
|
||||||
|
|
||||||
|
// Set cardview offset and color.
|
||||||
|
val marginParams = holder.card.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
val messageOffset = resources.getDimensionPixelOffset(R.dimen.message_offset)
|
||||||
|
if (isMe) {
|
||||||
|
marginParams.leftMargin = messageOffset
|
||||||
|
marginParams.rightMargin = 0
|
||||||
|
holder.card.setCardBackgroundColor(resources.getColor(R.color.colorMyMsg))
|
||||||
|
} else {
|
||||||
|
marginParams.rightMargin = messageOffset
|
||||||
|
marginParams.leftMargin = 0
|
||||||
|
holder.card.setCardBackgroundColor(resources.getColor(R.color.colorMsg))
|
||||||
|
}
|
||||||
|
// Hide the username chip for my messages
|
||||||
|
if (isMe) {
|
||||||
|
holder.username.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
holder.username.visibility = View.VISIBLE
|
||||||
|
// If we can't find the sender, just use the ID
|
||||||
|
val user = node?.user
|
||||||
|
holder.username.text = user?.shortName ?: msg.from
|
||||||
|
}
|
||||||
if (msg.errorMessage != null) {
|
if (msg.errorMessage != null) {
|
||||||
// FIXME, set the style to show a red error message
|
// FIXME, set the style to show a red error message
|
||||||
holder.messageText.text = msg.errorMessage
|
holder.messageText.text = msg.errorMessage
|
||||||
|
|
@ -137,7 +173,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
holder.messageText.text = msg.text
|
holder.messageText.text = msg.text
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.messageTime.text = dateTimeFormat.format(Date(msg.time))
|
holder.messageTime.text = getShortDateTime(Date(msg.time))
|
||||||
|
|
||||||
val icon = when (msg.status) {
|
val icon = when (msg.status) {
|
||||||
MessageStatus.QUEUED -> R.drawable.ic_twotone_cloud_upload_24
|
MessageStatus.QUEUED -> R.drawable.ic_twotone_cloud_upload_24
|
||||||
|
|
@ -150,6 +186,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
holder.messageStatusIcon.setImageResource(icon)
|
holder.messageStatusIcon.setImageResource(icon)
|
||||||
holder.messageStatusIcon.visibility = View.VISIBLE
|
holder.messageStatusIcon.visibility = View.VISIBLE
|
||||||
|
|
||||||
} else
|
} else
|
||||||
holder.messageStatusIcon.visibility = View.INVISIBLE
|
holder.messageStatusIcon.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
|
|
@ -194,6 +231,11 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
layoutManager.stackFromEnd = true // We want the last rows to always be shown
|
layoutManager.stackFromEnd = true // We want the last rows to always be shown
|
||||||
binding.messageListView.layoutManager = layoutManager
|
binding.messageListView.layoutManager = layoutManager
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
model.messagesState.messages.observe(viewLifecycleOwner, Observer {
|
model.messagesState.messages.observe(viewLifecycleOwner, Observer {
|
||||||
debug("New messages received: ${it.size}")
|
debug("New messages received: ${it.size}")
|
||||||
messagesAdapter.onMessagesChanged(it)
|
messagesAdapter.onMessagesChanged(it)
|
||||||
|
|
@ -211,6 +253,12 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
|
||||||
binding.textInputLayout.isEnabled =
|
binding.textInputLayout.isEnabled =
|
||||||
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED && myId != null
|
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED && myId != null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
model.myNodeInfo.observe(viewLifecycleOwner, Observer { myNodeInfo ->
|
||||||
|
// If our Id changed, the UI may need to be updated
|
||||||
|
messagesAdapter.notifyDataSetChanged()
|
||||||
|
debug("New id received ${myNodeInfo?.myNodeNum}")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,68 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="4dp"
|
android:contentDescription="Message delivery status">
|
||||||
android:clipToPadding="false"
|
|
||||||
android:contentDescription="Message delivery status"
|
|
||||||
android:elevation="2dp">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/Card"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginEnd="@dimen/message_offset"
|
||||||
|
app:cardBackgroundColor="@color/colorMsg"
|
||||||
|
app:cardCornerRadius="12dp"
|
||||||
|
app:cardElevation="2dp"
|
||||||
|
app:cardUseCompatPadding="true">
|
||||||
|
|
||||||
<com.google.android.material.chip.Chip
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/username"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:text="@string/some_username"
|
|
||||||
app:chipIcon="@drawable/ic_twotone_person_24"
|
|
||||||
app:layout_constraintEnd_toEndOf="@+id/messageTime"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/messageTime"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
<com.google.android.material.chip.Chip
|
||||||
android:id="@+id/messageText"
|
android:id="@+id/username"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:text="@string/some_username"
|
||||||
android:autoLink="all"
|
android:visibility="visible"
|
||||||
android:text="@string/sample_message"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:textIsSelectable="true"
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/username"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/messageTime"
|
android:id="@+id/messageText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="8dp"
|
||||||
android:text="3 min"
|
android:layout_marginTop="8dp"
|
||||||
app:layout_constraintStart_toStartOf="@+id/username"
|
android:layout_marginEnd="8dp"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/username" />
|
android:autoLink="all"
|
||||||
|
android:text="@string/sample_message"
|
||||||
|
android:textIsSelectable="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/username"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/messageStatusIcon"
|
android:id="@+id/messageStatusIcon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/messageText"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintTop_toBottomOf="@id/messageText"
|
||||||
app:srcCompat="@drawable/cloud_on" />
|
app:srcCompat="@drawable/cloud_on" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
<TextView
|
||||||
|
android:id="@+id/messageTime"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="3 minutes ago"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/messageStatusIcon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/messageText" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
@ -3,4 +3,6 @@
|
||||||
<color name="colorPrimary">#3700B3</color>
|
<color name="colorPrimary">#3700B3</color>
|
||||||
<color name="colorPrimaryDark">#3700B3</color>
|
<color name="colorPrimaryDark">#3700B3</color>
|
||||||
<color name="colorAccent">#D81B60</color>
|
<color name="colorAccent">#D81B60</color>
|
||||||
|
<color name="colorMsg">#07000000</color>
|
||||||
|
<color name="colorMyMsg">#403700B3</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
<resources>
|
<resources>
|
||||||
<dimen name="fab_margin">16dp</dimen>
|
<dimen name="fab_margin">16dp</dimen>
|
||||||
|
<dimen name="message_offset">64dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue