mirror of
				https://github.com/AdrianKuta/Unbound-Drag-Drop.git
				synced 2025-10-31 00:13:39 +01:00 
			
		
		
		
	Cleanup
This commit is contained in:
		
							
								
								
									
										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") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user