mirror of https://github.com/tasks/tasks
Tag picker compose (#2849)
TagPickerActivity refactoring to Compose 1. state of the SearchBar moved to the viewModel 2. viewModel used as parameter to @Composables instead of number of separate ones 3. Import of TagPickerActivity is replaced by TagPickerActivityCompose through the projectpull/2850/head
parent
782f4d6d7c
commit
e6e275834a
@ -1,108 +0,0 @@
|
||||
package org.tasks.tags
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.EditText
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import org.tasks.R
|
||||
import org.tasks.Strings.isNullOrEmpty
|
||||
import org.tasks.billing.Inventory
|
||||
import org.tasks.data.TagData
|
||||
import org.tasks.databinding.ActivityTagPickerBinding
|
||||
import org.tasks.extensions.addBackPressedCallback
|
||||
import org.tasks.injection.ThemedInjectingAppCompatActivity
|
||||
import org.tasks.themes.ColorProvider
|
||||
import org.tasks.themes.Theme
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TagPickerActivity : ThemedInjectingAppCompatActivity() {
|
||||
@Inject lateinit var theme: Theme
|
||||
@Inject lateinit var inventory: Inventory
|
||||
@Inject lateinit var colorProvider: ColorProvider
|
||||
|
||||
private val viewModel: TagPickerViewModel by viewModels()
|
||||
private var taskIds: ArrayList<Long>? = null
|
||||
private lateinit var editText: EditText
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val intent = intent
|
||||
taskIds = intent.getSerializableExtra(EXTRA_TASKS) as ArrayList<Long>?
|
||||
if (savedInstanceState == null) {
|
||||
intent.getParcelableArrayListExtra<TagData>(EXTRA_SELECTED)?.let {
|
||||
viewModel.setSelected(
|
||||
it,
|
||||
intent.getParcelableArrayListExtra(EXTRA_PARTIALLY_SELECTED)
|
||||
)
|
||||
}
|
||||
}
|
||||
val binding = ActivityTagPickerBinding.inflate(layoutInflater)
|
||||
editText = binding.searchInput.apply {
|
||||
addTextChangedListener(
|
||||
onTextChanged = { text, _, _, _ -> onSearch(text) }
|
||||
)
|
||||
}
|
||||
setContentView(binding.root)
|
||||
val toolbar = binding.toolbar
|
||||
toolbar.setNavigationIcon(R.drawable.ic_outline_arrow_back_24px)
|
||||
toolbar.setNavigationOnClickListener { onBackPressed() }
|
||||
val themeColor = theme.themeColor
|
||||
themeColor.applyToNavigationBar(this)
|
||||
val recyclerAdapter = TagRecyclerAdapter(this, viewModel, inventory, colorProvider) { tagData, vh ->
|
||||
onToggle(tagData, vh)
|
||||
}
|
||||
val recyclerView = binding.recyclerView
|
||||
recyclerView.adapter = recyclerAdapter
|
||||
(recyclerView.itemAnimator as DefaultItemAnimator?)!!.supportsChangeAnimations = false
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
viewModel.observe(this) { recyclerAdapter.submitList(it) }
|
||||
editText.setText(viewModel.text)
|
||||
|
||||
addBackPressedCallback {
|
||||
if (isNullOrEmpty(viewModel.text)) {
|
||||
val data = Intent()
|
||||
data.putExtra(EXTRA_TASKS, taskIds)
|
||||
data.putParcelableArrayListExtra(
|
||||
EXTRA_PARTIALLY_SELECTED,
|
||||
viewModel.getPartiallySelected()
|
||||
)
|
||||
data.putParcelableArrayListExtra(EXTRA_SELECTED, viewModel.getSelected())
|
||||
setResult(Activity.RESULT_OK, data)
|
||||
finish()
|
||||
} else {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onToggle(tagData: TagData, vh: TagPickerViewHolder) = lifecycleScope.launch {
|
||||
val newTag = tagData.id == null
|
||||
val newState = viewModel.toggle(tagData, vh.isChecked || newTag)
|
||||
vh.updateCheckbox(newState)
|
||||
if (newTag) {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearch(text: CharSequence?) {
|
||||
viewModel.search(text?.toString() ?: "")
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
editText.setText("")
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_SELECTED = "extra_tags"
|
||||
const val EXTRA_PARTIALLY_SELECTED = "extra_partial"
|
||||
const val EXTRA_TASKS = "extra_tasks"
|
||||
}
|
||||
}
|
@ -0,0 +1,292 @@
|
||||
package org.tasks.tags
|
||||
|
||||
/*
|
||||
* TagPickerActivityCompose is a replacement for TagPickerActivity reimplemented
|
||||
* using JetPack Compose framework.
|
||||
*
|
||||
* The modification eliminates TagRecycleAdapter.
|
||||
* TriStateCheckbox from Compose is used instead of CheckBoxTriState.
|
||||
* Source code for TagRecycleAdapter and TagPickerActivity were deleted because they became incompatible
|
||||
* with modified TagPickerViewModel, but were not excluded from build due to the hilt logic.
|
||||
*
|
||||
* There is a tag "FeatureReady" in the git commits log which marks the state when the main logic was
|
||||
* already implemented via Compose but but viewModel was modified with backward compatibility so to
|
||||
* switch back to View implementation its enough to find lines like this
|
||||
* //val intent = Intent(context, TagPickerActivity::class.java)
|
||||
* val intent = Intent(context, TagPickerActivityCompose::class.java)
|
||||
* in TaskListFragment.kt and TagsControlSet.kt and move comment mark to another line.
|
||||
*/
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.TriStateCheckbox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.android.material.composethemeadapter.MdcTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import org.tasks.R
|
||||
import org.tasks.Strings
|
||||
import org.tasks.billing.Inventory
|
||||
import org.tasks.data.TagData
|
||||
import org.tasks.extensions.addBackPressedCallback
|
||||
import org.tasks.injection.ThemedInjectingAppCompatActivity
|
||||
import org.tasks.themes.ColorProvider
|
||||
import org.tasks.themes.CustomIcons
|
||||
import org.tasks.themes.Theme
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TagPickerActivityCompose : ThemedInjectingAppCompatActivity() {
|
||||
@Inject lateinit var theme: Theme
|
||||
@Inject lateinit var inventory: Inventory
|
||||
@Inject lateinit var colorProvider: ColorProvider
|
||||
|
||||
|
||||
private val viewModel: TagPickerViewModel by viewModels()
|
||||
private var taskIds: ArrayList<Long>? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val intent = intent
|
||||
taskIds = intent.getSerializableExtra(EXTRA_TASKS) as ArrayList<Long>?
|
||||
if (savedInstanceState == null) {
|
||||
val selected = intent.getParcelableArrayListExtra<TagData>(EXTRA_SELECTED)
|
||||
if ( selected != null ) {
|
||||
viewModel.setSelected(
|
||||
selected, intent.getParcelableArrayListExtra<TagData>(EXTRA_PARTIALLY_SELECTED)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
addBackPressedCallback { handleBackPressed() }
|
||||
|
||||
viewModel.search("")
|
||||
|
||||
setContent {
|
||||
MdcTheme {
|
||||
TagPicker(
|
||||
viewModel,
|
||||
onBackClicked = { handleBackPressed() },
|
||||
getTagIcon = { tagData -> getIcon(tagData) },
|
||||
getTagColor = { tagData -> getColor(tagData) }
|
||||
)
|
||||
} /* setContent */
|
||||
}
|
||||
} /* onCreate */
|
||||
|
||||
private fun handleBackPressed() {
|
||||
if (Strings.isNullOrEmpty(viewModel.searchText.value)) {
|
||||
val data = Intent()
|
||||
data.putExtra(EXTRA_TASKS, taskIds)
|
||||
data.putParcelableArrayListExtra(EXTRA_PARTIALLY_SELECTED, viewModel.getPartiallySelected())
|
||||
data.putParcelableArrayListExtra(EXTRA_SELECTED, viewModel.getSelected())
|
||||
setResult(Activity.RESULT_OK, data)
|
||||
finish()
|
||||
} else {
|
||||
viewModel.search("")
|
||||
}
|
||||
} /* handleBackPressed */
|
||||
|
||||
private fun getColor(tagData: TagData): Color {
|
||||
if (tagData.getColor() != 0) {
|
||||
val themeColor = colorProvider.getThemeColor(tagData.getColor()!!, true)
|
||||
if (inventory.purchasedThemes() || themeColor.isFree) {
|
||||
return Color(themeColor.primaryColor)
|
||||
}
|
||||
}
|
||||
return Color(getColor(R.color.icon_tint_with_alpha))
|
||||
}
|
||||
|
||||
private fun getIcon(tagData: TagData): Int
|
||||
{
|
||||
val iconIndex = tagData.getIcon()
|
||||
var iconResource = R.drawable.ic_outline_label_24px
|
||||
if ( (iconIndex != null) && (iconIndex < 1000 || inventory.hasPro) ) {
|
||||
iconResource = CustomIcons.getIconResId(iconIndex) ?: R.drawable.ic_outline_label_24px
|
||||
}
|
||||
return iconResource
|
||||
}
|
||||
|
||||
/* Copy of the TagPickerActivity's companion object */
|
||||
companion object {
|
||||
const val EXTRA_SELECTED = "extra_tags"
|
||||
const val EXTRA_PARTIALLY_SELECTED = "extra_partial"
|
||||
const val EXTRA_TASKS = "extra_tasks"
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TagPicker(
|
||||
viewModel: TagPickerViewModel,
|
||||
onBackClicked: () -> Unit,
|
||||
getTagIcon: (TagData) -> Int,
|
||||
getTagColor: (TagData) -> Color
|
||||
) {
|
||||
Box ( modifier = Modifier.fillMaxSize() )
|
||||
{
|
||||
Column (modifier = Modifier.padding(horizontal = 12.dp)) {
|
||||
Box( modifier = Modifier.fillMaxWidth() ) {
|
||||
SearchBar(viewModel, onBackClicked)
|
||||
}
|
||||
Box (
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
PickerBox(viewModel, viewModel.tagsList.observeAsState(initial = emptyList()), getTagIcon, getTagColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SearchBar(
|
||||
viewModel: TagPickerViewModel,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val searchPattern = remember { viewModel.searchText }
|
||||
val invitation = LocalContext.current.getString(R.string.enter_tag_name)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
ImageVector.vectorResource(id = R.drawable.ic_outline_arrow_back_24px),
|
||||
"Done",
|
||||
modifier = Modifier
|
||||
.padding(6.dp)
|
||||
.clickable { onBack() }
|
||||
)
|
||||
|
||||
TextField(
|
||||
value = searchPattern.value,
|
||||
onValueChange = { viewModel.search(it) },
|
||||
placeholder = { Text(invitation) },
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
textColor = MaterialTheme.colors.onBackground,
|
||||
backgroundColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.padding(start = 6.dp)
|
||||
)
|
||||
}
|
||||
} /* SearchBar */
|
||||
|
||||
@Composable
|
||||
internal fun PickerBox (
|
||||
viewModel: TagPickerViewModel,
|
||||
tags: State<List<TagData>>,
|
||||
getTagIcon: (TagData) -> Int = { R.drawable.ic_outline_label_24px },
|
||||
getTagColor: (TagData) -> Color = { Color.Gray }
|
||||
) {
|
||||
val onClick: (TagData) -> Unit = {
|
||||
viewModel.viewModelScope.launch {
|
||||
viewModel.toggle(it, viewModel.getState(it) != ToggleableState.On) }
|
||||
}
|
||||
|
||||
val newItem: (String) -> Unit = {
|
||||
viewModel.viewModelScope.launch { viewModel.createNew(it); viewModel.search("") }
|
||||
}
|
||||
|
||||
LazyColumn {
|
||||
if (viewModel.tagToCreate.value != "") {
|
||||
item(key = -1) {
|
||||
val text = LocalContext.current.getString(R.string.new_tag) + " \"${viewModel.tagToCreate.value}\""
|
||||
TagRow(
|
||||
icon = ImageVector.vectorResource(R.drawable.ic_outline_add_24px),
|
||||
iconColor = Color(LocalContext.current.getColor(R.color.icon_tint_with_alpha)),
|
||||
text = text,
|
||||
onClick = { newItem(viewModel.searchText.value) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items( tags.value, key = { tag -> tag.id!! } )
|
||||
{
|
||||
val checked = remember { mutableStateOf ( viewModel.getState(it) ) }
|
||||
val clickChecked: () -> Unit = { onClick(it); checked.value = viewModel.getState(it) }
|
||||
TagRow(
|
||||
icon = ImageVector.vectorResource(getTagIcon(it)),
|
||||
iconColor = getTagColor(it),
|
||||
text = it.name!!,
|
||||
onClick = clickChecked
|
||||
) {
|
||||
TriStateCheckbox(
|
||||
modifier = Modifier.padding(6.dp),
|
||||
state = checked.value,
|
||||
onClick = clickChecked
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} /* PickerBox */
|
||||
|
||||
@Composable
|
||||
internal fun TagRow (
|
||||
icon: ImageVector,
|
||||
iconColor: Color,
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
checkBox: @Composable RowScope.() -> Unit = {}
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth().clickable{ onClick() },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.padding(6.dp),
|
||||
tint = iconColor
|
||||
)
|
||||
Text(
|
||||
text,
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 24.dp)
|
||||
)
|
||||
checkBox()
|
||||
}
|
||||
} /* TagRow */
|
||||
|
||||
/*
|
||||
internal fun genTestTags(): List<TagData>
|
||||
{
|
||||
var idcc: Long = 1
|
||||
return "alpha beta gamma delta kappa theta alfa1 beta1 gamma1 delta1 kappa1 theta1"
|
||||
.split(" ")
|
||||
.map { name -> TagData(name).also{ it.id = idcc++ } }
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, backgroundColor = 0xffffff)
|
||||
internal fun PickerBoxPreview() {
|
||||
val list = remember { mutableStateOf( genTestTags() ) }
|
||||
PickerBox(list, getTagColor = { Color.Green })
|
||||
}
|
||||
*/
|
@ -1,55 +0,0 @@
|
||||
package org.tasks.tags
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.tasks.R
|
||||
import org.tasks.billing.Inventory
|
||||
import org.tasks.data.TagData
|
||||
import org.tasks.databinding.RowTagPickerBinding
|
||||
import org.tasks.themes.ColorProvider
|
||||
import org.tasks.themes.CustomIcons.getIconResId
|
||||
|
||||
internal class TagRecyclerAdapter(
|
||||
private val context: Context,
|
||||
private val viewModel: TagPickerViewModel,
|
||||
private val inventory: Inventory,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val callback: (TagData, TagPickerViewHolder) -> Unit
|
||||
) : RecyclerView.Adapter<TagPickerViewHolder>() {
|
||||
|
||||
private val differ: AsyncListDiffer<TagData> = AsyncListDiffer(this, TagDiffCallback())
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
TagPickerViewHolder(
|
||||
context,
|
||||
RowTagPickerBinding.inflate(LayoutInflater.from(context), parent, false),
|
||||
callback
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: TagPickerViewHolder, position: Int) {
|
||||
val tagData = differ.currentList[position]
|
||||
holder.bind(tagData, getColor(tagData), getIcon(tagData), viewModel.getState(tagData))
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = differ.currentList.size
|
||||
|
||||
private fun getColor(tagData: TagData): Int {
|
||||
if (tagData.getColor() != 0) {
|
||||
val themeColor = colorProvider.getThemeColor(tagData.getColor()!!, true)
|
||||
if (inventory.purchasedThemes() || themeColor.isFree) {
|
||||
return themeColor.primaryColor
|
||||
}
|
||||
}
|
||||
return context.getColor(R.color.icon_tint_with_alpha)
|
||||
}
|
||||
|
||||
private fun getIcon(tagData: TagData): Int? =
|
||||
tagData.getIcon()?.takeIf { it < 1000 || inventory.hasPro }?.let { getIconResId(it) }
|
||||
|
||||
fun submitList(tagData: List<TagData>?) {
|
||||
differ.submitList(tagData)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue