From 56d9f037488b186aaf0241268f036c193b1f7ad4 Mon Sep 17 00:00:00 2001 From: andrekir Date: Thu, 4 Jul 2024 09:23:24 -0300 Subject: [PATCH] refactor: migrate `QuickChatFragment` RecyclerView to Compose --- .../mesh/ui/QuickChatActionAdapter.kt | 68 ------ .../mesh/ui/QuickChatSettingsFragment.kt | 230 ++++++++++++++---- .../adapter_quick_chat_action_layout.xml | 73 ------ .../layout/quick_chat_settings_fragment.xml | 44 ++-- 4 files changed, 207 insertions(+), 208 deletions(-) delete mode 100644 app/src/main/java/com/geeksville/mesh/ui/QuickChatActionAdapter.kt delete mode 100644 app/src/main/res/layout/adapter_quick_chat_action_layout.xml diff --git a/app/src/main/java/com/geeksville/mesh/ui/QuickChatActionAdapter.kt b/app/src/main/java/com/geeksville/mesh/ui/QuickChatActionAdapter.kt deleted file mode 100644 index dd60d5b0f..000000000 --- a/app/src/main/java/com/geeksville/mesh/ui/QuickChatActionAdapter.kt +++ /dev/null @@ -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(), DragManageAdapter.SwapAdapter { - - private val inflater: LayoutInflater = LayoutInflater.from(context) - private var actions = emptyList() - - 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) { - 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() - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/QuickChatSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/QuickChatSettingsFragment.kt index d4c8c71f4..99e162e46 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/QuickChatSettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/QuickChatSettingsFragment.kt @@ -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 - 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) } -} \ No newline at end of file + + 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, + ), + ) + } +} diff --git a/app/src/main/res/layout/adapter_quick_chat_action_layout.xml b/app/src/main/res/layout/adapter_quick_chat_action_layout.xml deleted file mode 100644 index d37bae544..000000000 --- a/app/src/main/res/layout/adapter_quick_chat_action_layout.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/quick_chat_settings_fragment.xml b/app/src/main/res/layout/quick_chat_settings_fragment.xml index 6cffcd915..92800655a 100644 --- a/app/src/main/res/layout/quick_chat_settings_fragment.xml +++ b/app/src/main/res/layout/quick_chat_settings_fragment.xml @@ -2,24 +2,37 @@ - + - + + + + + + + - + app:layout_constraintTop_toBottomOf="@id/quickChatSettingsToolbar" /> - \ No newline at end of file