refactor: migrate QuickChatFragment RecyclerView to Compose

This commit is contained in:
andrekir 2024-07-04 09:23:24 -03:00
parent 15861c1389
commit 56d9f03748
4 changed files with 207 additions and 208 deletions

View file

@ -1,68 +0,0 @@
package com.geeksville.mesh.ui
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.QuickChatAction
class QuickChatActionAdapter internal constructor(
private val context: Context,
private val onEdit: (action: QuickChatAction) -> Unit,
private val repositionAction: (fromPos: Int, toPos: Int) -> Unit,
private val commitAction: () -> Unit,
) : RecyclerView.Adapter<QuickChatActionAdapter.ActionViewHolder>(), DragManageAdapter.SwapAdapter {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var actions = emptyList<QuickChatAction>()
inner class ActionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val container: View = itemView.findViewById(R.id.quickChatActionContainer)
val actionName: TextView = itemView.findViewById(R.id.quickChatActionName)
val actionValue: TextView = itemView.findViewById(R.id.quickChatActionValue)
val actionEdit: View = itemView.findViewById(R.id.quickChatActionEdit)
val actionInstant: View = itemView.findViewById(R.id.quickChatActionInstant)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActionViewHolder {
val itemView = inflater.inflate(R.layout.adapter_quick_chat_action_layout, parent, false)
return ActionViewHolder(itemView)
}
override fun onBindViewHolder(holder: ActionViewHolder, position: Int) {
val current = actions[position]
holder.actionName.text = current.name
holder.actionValue.text = current.message
val isInstant = current.mode == QuickChatAction.Mode.Instant
holder.actionInstant.visibility = if (isInstant) View.VISIBLE else View.INVISIBLE
if (isInstant) {
holder.container.backgroundTintList = ContextCompat.getColorStateList(context, R.color.colorMyMsg)
} else {
holder.container.backgroundTintList = null
}
holder.actionEdit.setOnClickListener {
onEdit(current)
}
}
internal fun setActions(actions: List<QuickChatAction>) {
this.actions = actions
notifyDataSetChanged()
}
override fun getItemCount() = actions.size
override fun swapItems(fromPosition: Int, toPosition: Int) {
repositionAction(fromPosition, toPosition)
notifyItemMoved(fromPosition, toPosition)
}
override fun commitSwaps() {
commitAction()
}
}

View file

@ -7,20 +7,49 @@ import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import androidx.annotation.StringRes
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentColor
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.asLiveData
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.databinding.QuickChatSettingsFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.components.dragContainer
import com.geeksville.mesh.ui.components.dragDropItemsIndexed
import com.geeksville.mesh.ui.components.rememberDragDropState
import com.geeksville.mesh.ui.theme.AppTheme
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial
import dagger.hilt.android.AndroidEntryPoint
import java.util.*
@AndroidEntryPoint
class QuickChatSettingsFragment : ScreenFragment("Quick Chat Settings"), Logging {
@ -30,8 +59,6 @@ class QuickChatSettingsFragment : ScreenFragment("Quick Chat Settings"), Logging
private val model: UIViewModel by activityViewModels()
private lateinit var actions: List<QuickChatAction>
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@ -43,8 +70,12 @@ class QuickChatSettingsFragment : ScreenFragment("Quick Chat Settings"), Logging
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.quickChatSettingsToolbar.setNavigationOnClickListener {
parentFragmentManager.popBackStack()
}
binding.quickChatSettingsCreateButton.setOnClickListener {
val builder = createEditDialog(requireContext(), getString(R.string.quick_chat_new))
val builder = createEditDialog(requireContext(), R.string.quick_chat_new)
builder.builder.setPositiveButton(R.string.add) { _, _ ->
@ -61,50 +92,37 @@ class QuickChatSettingsFragment : ScreenFragment("Quick Chat Settings"), Logging
dialog.show()
}
val quickChatActionAdapter =
QuickChatActionAdapter(requireContext(), { action: QuickChatAction ->
val builder = createEditDialog(requireContext(), getString(R.string.quick_chat_edit))
builder.nameInput.setText(action.name)
builder.messageInput.setText(action.message)
val isInstant = action.mode == QuickChatAction.Mode.Instant
builder.modeSwitch.isChecked = isInstant
builder.instantImage.visibility = if (isInstant) View.VISIBLE else View.INVISIBLE
binding.quickChatSettingsView.setContent {
val actions by model.quickChatActions.collectAsStateWithLifecycle()
builder.builder.setNegativeButton(R.string.delete) { _, _ ->
model.deleteQuickChatAction(action)
}
builder.builder.setPositiveButton(R.string.save) { _, _ ->
if (builder.isNotEmpty()) {
model.updateQuickChatAction(
action,
builder.nameInput.text.toString(),
builder.messageInput.text.toString(),
if (builder.modeSwitch.isChecked) QuickChatAction.Mode.Instant else QuickChatAction.Mode.Append
val listState = rememberLazyListState()
val dragDropState = rememberDragDropState(listState) { fromIndex, toIndex ->
val list = actions.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
model.updateActionPositions(list)
}
AppTheme {
LazyColumn(
modifier = Modifier.dragContainer(
dragDropState = dragDropState,
haptics = LocalHapticFeedback.current,
),
state = listState,
// contentPadding = PaddingValues(16.dp),
) {
dragDropItemsIndexed(
items = actions,
dragDropState = dragDropState,
key = { _, item -> item.uuid },
) { _, action, isDragging ->
val elevation by animateDpAsState(if (isDragging) 8.dp else 4.dp)
QuickChatItem(
elevation = elevation,
action = action,
onEditClick = ::onEditAction,
)
}
}
val dialog = builder.builder.create()
dialog.show()
}, { fromPos, toPos ->
Collections.swap(actions, fromPos, toPos)
}, {
model.updateActionPositions(actions)
})
val dragCallback =
DragManageAdapter(quickChatActionAdapter, ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
val helper = ItemTouchHelper(dragCallback)
binding.quickChatSettingsView.apply {
this.layoutManager = LinearLayoutManager(requireContext())
this.adapter = quickChatActionAdapter
helper.attachToRecyclerView(this)
}
model.quickChatActions.asLiveData().observe(viewLifecycleOwner) { actions ->
actions?.let {
quickChatActionAdapter.setActions(actions)
this.actions = actions
}
}
}
@ -136,7 +154,7 @@ class QuickChatSettingsFragment : ScreenFragment("Quick Chat Settings"), Logging
}
}
private fun createEditDialog(context: Context, title: String): DialogBuilder {
private fun createEditDialog(context: Context, @StringRes title: Int): DialogBuilder {
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(title)
@ -149,7 +167,7 @@ class QuickChatSettingsFragment : ScreenFragment("Quick Chat Settings"), Logging
instantImage.visibility = if (modeSwitch.isChecked) View.VISIBLE else View.INVISIBLE
// don't change action name on edits
var nameHasChanged = title == getString(R.string.quick_chat_edit)
var nameHasChanged = title == R.string.quick_chat_edit
modeSwitch.setOnCheckedChangeListener { _, _ ->
if (modeSwitch.isChecked) {
@ -175,4 +193,116 @@ class QuickChatSettingsFragment : ScreenFragment("Quick Chat Settings"), Logging
return DialogBuilder(builder, nameInput, messageInput, modeSwitch, instantImage)
}
}
private fun onEditAction(action: QuickChatAction) {
val builder = createEditDialog(requireContext(), R.string.quick_chat_edit)
builder.nameInput.setText(action.name)
builder.messageInput.setText(action.message)
val isInstant = action.mode == QuickChatAction.Mode.Instant
builder.modeSwitch.isChecked = isInstant
builder.instantImage.visibility = if (isInstant) View.VISIBLE else View.INVISIBLE
builder.builder.setNegativeButton(R.string.delete) { _, _ ->
model.deleteQuickChatAction(action)
}
builder.builder.setPositiveButton(R.string.save) { _, _ ->
if (builder.isNotEmpty()) {
model.updateQuickChatAction(
action,
builder.nameInput.text.toString(),
builder.messageInput.text.toString(),
if (builder.modeSwitch.isChecked) {
QuickChatAction.Mode.Instant
} else {
QuickChatAction.Mode.Append
}
)
}
}
val dialog = builder.builder.create()
dialog.show()
}
}
@Composable
internal fun QuickChatItem(
action: QuickChatAction,
modifier: Modifier = Modifier,
onEditClick: (QuickChatAction) -> Unit = {},
elevation: Dp = 4.dp,
) {
Card(
modifier = modifier
.fillMaxWidth()
.padding(8.dp),
elevation = elevation,
shape = RoundedCornerShape(12.dp),
) {
Surface {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
val showInstantIcon = action.mode == QuickChatAction.Mode.Instant
Icon(
painter = painterResource(id = R.drawable.ic_baseline_fast_forward_24),
contentDescription = null,
modifier = Modifier.padding(start = 8.dp),
tint = if (showInstantIcon) LocalContentColor.current else Color.Transparent,
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp)
) {
Text(
text = action.name,
fontSize = 20.sp,
modifier = Modifier.padding(top = 8.dp)
)
Text(
text = action.message,
modifier = Modifier.padding(vertical = 8.dp)
)
}
IconButton(
onClick = { onEditClick(action) },
modifier = Modifier.size(48.dp)
) {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_edit_24),
contentDescription = null
)
}
Icon(
painter = painterResource(id = R.drawable.ic_baseline_drag_handle_24),
contentDescription = null,
modifier = Modifier.padding(end = 8.dp),
)
}
}
}
}
@PreviewLightDark
@Composable
private fun QuickChatItemPreview() {
AppTheme {
QuickChatItem(
action = QuickChatAction(
uuid = 0L,
name = "TST",
message = "Test",
mode = QuickChatAction.Mode.Instant,
position = 0,
),
)
}
}