This commit is contained in:
Adrian Kuta 2024-06-26 00:24:08 +02:00
parent 6f93d0c234
commit ee9000b98a
11 changed files with 483 additions and 0 deletions

21
.idea/deploymentTargetSelector.xml generated Normal file
View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="unbounddragdrop">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-06-25T21:25:19.314095Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/adriankuta/.android/avd/Pixel_8_API_34.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

View File

@ -0,0 +1,147 @@
package com.github.adriankuta.unbounddragdrop
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.adriankuta.unbounddragdrop.databinding.ActivityMainBinding
import com.github.adriankuta.unbounddragdrop.model.TaskStatus
import dev.adriankuta.unbounddragdrop.DragDropHelper
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
private lateinit var binding: ActivityMainBinding
private lateinit var todoAdapter: SimpleGridPageAdapter
private lateinit var inProgressAdapter: SimpleGridPageAdapter
private val callback = object : DragDropHelper.Callback() {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
targetRecyclerView: RecyclerView,
targetViewHolder: RecyclerView.ViewHolder?
): Boolean {
return onMove(
recyclerView,
viewHolder.adapterPosition,
targetRecyclerView,
targetViewHolder?.adapterPosition
)
}
override fun onMoved(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
targetRecyclerView: RecyclerView,
targetViewHolder: RecyclerView.ViewHolder?
) = Unit
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
enableEdgeToEdge()
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
setupRecyclerViews()
setObservers()
}
private fun setupRecyclerViews() {
todoAdapter = SimpleGridPageAdapter()
inProgressAdapter = SimpleGridPageAdapter()
with(binding) {
setupRecyclerView(toDoRecyclerView, todoAdapter)
setupRecyclerView(inProgressRecyclerView, inProgressAdapter)
}
}
private fun setObservers() {
lifecycleScope.launch {
viewModel.uiState.collect(::render)
}
}
private fun render(uiState: MainUiState) {
todoAdapter.submitList(uiState.todo)
inProgressAdapter.submitList(uiState.inProgress)
}
private fun setupRecyclerView(
recyclerView: RecyclerView,
adapter: RecyclerView.Adapter<*>
) {
recyclerView.adapter = adapter
recyclerView.layoutManager =
GridLayoutManager(this, GRID_ROWS, GridLayoutManager.HORIZONTAL, false)
DragDropHelper(callback).attachToRecyclerView(recyclerView)
}
private fun onMove(
recyclerView: RecyclerView,
sourcePosition: Int,
targetRecyclerView: RecyclerView,
targetPosition: Int?
): Boolean {
if (recyclerView == targetRecyclerView) {
onChangeOrder(recyclerView, sourcePosition, targetPosition)
} else {
onChangeStatus(recyclerView, sourcePosition, targetRecyclerView, targetPosition)
}
return true
}
private fun onChangeOrder(
recyclerView: RecyclerView,
sourcePosition: Int,
targetPosition: Int?
) {
targetPosition ?: return
val taskStatus = when (recyclerView.id) {
R.id.to_do_recycler_view -> TaskStatus.TODO
R.id.in_progress_recycler_view -> TaskStatus.IN_PROGRESS
else -> null
} ?: return
viewModel.onChangeOrder(taskStatus, sourcePosition, targetPosition)
}
private fun onChangeStatus(
recyclerView: RecyclerView,
sourcePosition: Int,
targetRecyclerView: RecyclerView,
targetPosition: Int?
) {
val fromStatus = when (recyclerView.id) {
R.id.to_do_recycler_view -> TaskStatus.TODO
R.id.in_progress_recycler_view -> TaskStatus.IN_PROGRESS
else -> null
}
val toStatus = when (targetRecyclerView.id) {
R.id.to_do_recycler_view -> TaskStatus.TODO
R.id.in_progress_recycler_view -> TaskStatus.IN_PROGRESS
else -> null
}
if (fromStatus != null && toStatus != null) {
viewModel.onChangeStatus(fromStatus, toStatus, sourcePosition, targetPosition)
}
}
companion object {
const val GRID_ROWS = 2
}
}

View File

@ -0,0 +1,94 @@
package com.github.adriankuta.unbounddragdrop
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.adriankuta.unbounddragdrop.model.Task
import com.github.adriankuta.unbounddragdrop.model.TaskStatus
import com.github.adriankuta.unbounddragdrop.model.TaskStatus.IN_PROGRESS
import com.github.adriankuta.unbounddragdrop.model.TaskStatus.TODO
import com.github.adriankuta.unbounddragdrop.util.WhileUiSubscribed
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import java.util.Collections
import java.util.UUID
data class MainUiState(
val todo: List<Task> = emptyList(),
val inProgress: List<Task> = emptyList(),
)
class MainViewModel : ViewModel() {
private val _todoTasks = MutableStateFlow<List<Task>>(emptyList())
private val _inProgressTasks = MutableStateFlow<List<Task>>(emptyList())
init {
_todoTasks.value =
List(30) { Task(UUID.randomUUID().toString(), "Task $it", TODO) }
_inProgressTasks.value =
List(4) { Task(UUID.randomUUID().toString(), "Task $it", IN_PROGRESS) }
}
val uiState: StateFlow<MainUiState> = combine(_todoTasks, _inProgressTasks) { listA, listB ->
MainUiState(listA, listB)
}.stateIn(
scope = viewModelScope,
started = WhileUiSubscribed,
initialValue = MainUiState()
)
fun onChangeOrder(status: TaskStatus, sourcePosition: Int, targetPosition: Int) {
when (status) {
TODO -> _todoTasks.swapElements(sourcePosition, targetPosition)
IN_PROGRESS -> _inProgressTasks.swapElements(sourcePosition, targetPosition)
}
}
fun onChangeStatus(
fromStatus: TaskStatus,
toStatus: TaskStatus,
fromPosition: Int,
toPosition: Int?
) {
val itemToMove = when (fromStatus) {
TODO -> _todoTasks.removeAt(fromPosition)
IN_PROGRESS -> _inProgressTasks.removeAt(fromPosition)
}
when (toStatus) {
TODO -> _todoTasks.add(toPosition ?: _todoTasks.size(), itemToMove)
IN_PROGRESS -> _inProgressTasks.add(toPosition ?: _inProgressTasks.size(), itemToMove)
}
}
private fun <T> MutableStateFlow<List<T>>.swapElements(
sourcePosition: Int,
targetPosition: Int
) {
val newList = value.toMutableList()
Collections.swap(newList, sourcePosition, targetPosition)
value = newList
}
private fun <T> MutableStateFlow<List<T>>.removeAt(index: Int): T {
val newList = value.toMutableList()
val removedItem = newList.removeAt(index)
value = newList
return removedItem
}
private fun <T> MutableStateFlow<List<T>>.add(index: Int, item: T) {
val newList = value.toMutableList()
newList.add(index, item)
value = newList
}
private fun <T> MutableStateFlow<List<T>>.size(): Int {
return value.size
}
}

View File

@ -0,0 +1,13 @@
package com.github.adriankuta.unbounddragdrop
import android.app.Application
import timber.log.Timber
import timber.log.Timber.DebugTree
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Timber.plant(DebugTree())
}
}

View File

@ -0,0 +1,56 @@
package com.github.adriankuta.unbounddragdrop
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.github.adriankuta.unbounddragdrop.databinding.TextItemBinding
import com.github.adriankuta.unbounddragdrop.model.Task
class SimpleGridPageAdapter :
RecyclerView.Adapter<SimpleGridPageAdapter.ViewHolder>() {
private val diffUtil = object : DiffUtil.ItemCallback<Task>() {
override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: Task,
newItem: Task
): Boolean {
return oldItem == newItem
}
}
private val asyncListDiffer = AsyncListDiffer(this, diffUtil)
fun submitList(list: List<Task>) {
asyncListDiffer.submitList(list)
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(viewGroup.inflate())
}
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val data = asyncListDiffer.currentList[position]
viewHolder.bind(data)
}
override fun getItemCount() = asyncListDiffer.currentList.size
class ViewHolder(private val binding: TextItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(task: Task) {
binding.textView.text = task.title
binding.root.isLongClickable = true
}
}
private fun ViewGroup.inflate() =
TextItemBinding.inflate(LayoutInflater.from(context), this, false)
}

View File

@ -0,0 +1,7 @@
package com.github.adriankuta.unbounddragdrop.model
data class Task(
val id: String,
val title: String,
val taskStatus: TaskStatus
)

View File

@ -0,0 +1,6 @@
package com.github.adriankuta.unbounddragdrop.model
enum class TaskStatus {
TODO,
IN_PROGRESS
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.adriankuta.unbounddragdrop.util
import kotlinx.coroutines.flow.SharingStarted
private const val StopTimeoutMillis: Long = 5000
/**
* A [SharingStarted] meant to be used with a [StateFlow] to expose data to the UI.
*
* When the UI stops observing, upstream flows stay active for some time to allow the system to
* come back from a short-lived configuration change (such as rotations). If the UI stops
* observing for longer, the cache is kept but the upstream flows are stopped. When the UI comes
* back, the latest value is replayed and the upstream flows are executed again. This is done to
* save resources when the app is in the background but let users switch between apps quickly.
*/
val WhileUiSubscribed: SharingStarted = SharingStarted.WhileSubscribed(StopTimeoutMillis)

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#33aaaaaa"
android:padding="16dp"
tools:context=".MainActivity">
<TextView
android:id="@+id/header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sprint 1"
android:textSize="22sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/todo_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="To Do"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/header" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/to_do_recycler_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/todo_label" />
<TextView
android:id="@+id/in_progress_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="12dp"
android:text="In Progress"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/to_do_recycler_view" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/in_progress_recycler_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/in_progress_label" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="12dp"
app:cardCornerRadius="4dp">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="30dp"
android:textSize="18sp" />
</androidx.cardview.widget.CardView>
</FrameLayout>

View File

@ -0,0 +1,29 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import org.gradle.api.Plugin
import org.gradle.api.Project
class AndroidLibraryPublishConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.vanniktech.maven.publish")
apply("com.gradleup.nmcp")
}
}
}
}