mirror of
https://github.com/AdrianKuta/Unbound-Drag-Drop.git
synced 2025-04-19 22:49:02 +02:00
Cleanup
This commit is contained in:
parent
6f93d0c234
commit
ee9000b98a
21
.idea/deploymentTargetSelector.xml
generated
Normal file
21
.idea/deploymentTargetSelector.xml
generated
Normal 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>
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.github.adriankuta.unbounddragdrop.model
|
||||
|
||||
data class Task(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val taskStatus: TaskStatus
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package com.github.adriankuta.unbounddragdrop.model
|
||||
|
||||
enum class TaskStatus {
|
||||
TODO,
|
||||
IN_PROGRESS
|
||||
}
|
@ -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)
|
57
app/src/main/res/layout/activity_main.xml
Normal file
57
app/src/main/res/layout/activity_main.xml
Normal 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>
|
21
app/src/main/res/layout/text_item.xml
Normal file
21
app/src/main/res/layout/text_item.xml
Normal 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>
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user