1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-20 07:39:09 -05:00

Add drag and drop to dashboard tiles (#1415)

This commit is contained in:
Rafał Borcz 2021-08-10 11:55:51 +02:00 committed by GitHub
parent 72ef5f428e
commit 626169de11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 162 additions and 27 deletions

View File

@ -2,9 +2,12 @@ package io.github.wulkanowy.data.repositories
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import com.squareup.moshi.Moshi import androidx.core.content.edit
import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.fredporciuncula.flow.preferences.Preference import com.fredporciuncula.flow.preferences.Preference
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
@ -21,8 +24,13 @@ import javax.inject.Singleton
class PreferencesRepository @Inject constructor( class PreferencesRepository @Inject constructor(
private val sharedPref: SharedPreferences, private val sharedPref: SharedPreferences,
private val flowSharedPref: FlowSharedPreferences, private val flowSharedPref: FlowSharedPreferences,
@ApplicationContext val context: Context @ApplicationContext val context: Context,
moshi: Moshi
) { ) {
@OptIn(ExperimentalStdlibApi::class)
private val dashboardItemsPositionAdapter: JsonAdapter<Map<DashboardItem.Type, Int>> =
moshi.adapter()
val startMenuIndex: Int val startMenuIndex: Int
get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt() get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt()
@ -160,6 +168,19 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_optional_arithmetic_average R.bool.pref_default_optional_arithmetic_average
) )
var dashboardItemsPosition: Map<DashboardItem.Type, Int>?
get() {
val json = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null
return dashboardItemsPositionAdapter.fromJson(json)
}
set(value) = sharedPref.edit {
putString(
PREF_KEY_DASHBOARD_ITEMS_POSITION,
dashboardItemsPositionAdapter.toJson(value)
)
}
val selectedDashboardTilesFlow: Flow<Set<DashboardItem.Tile>> val selectedDashboardTilesFlow: Flow<Set<DashboardItem.Tile>>
get() = selectedDashboardTilesPreference.asFlow() get() = selectedDashboardTilesPreference.asFlow()
.map { set -> .map { set ->
@ -199,4 +220,9 @@ class PreferencesRepository @Inject constructor(
private fun getBoolean(id: String, default: Int) = private fun getBoolean(id: String, default: Int) =
sharedPref.getBoolean(id, context.resources.getBoolean(default)) sharedPref.getBoolean(id, context.resources.getBoolean(default))
private companion object {
private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position"
}
} }

View File

@ -12,7 +12,6 @@ import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMarginsRelative import androidx.core.view.updateMarginsRelative
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
@ -38,10 +37,9 @@ import java.util.Timer
import javax.inject.Inject import javax.inject.Inject
import kotlin.concurrent.timer import kotlin.concurrent.timer
class DashboardAdapter @Inject constructor() : class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
ListAdapter<DashboardItem, RecyclerView.ViewHolder>(DashboardAdapterDiffCallback()) {
var lessonsTimer: Timer? = null private var lessonsTimer: Timer? = null
var onAccountTileClickListener: () -> Unit = {} var onAccountTileClickListener: () -> Unit = {}
@ -63,7 +61,23 @@ class DashboardAdapter @Inject constructor() :
var onConferencesTileClickListener: () -> Unit = {} var onConferencesTileClickListener: () -> Unit = {}
override fun getItemViewType(position: Int) = getItem(position).type.ordinal val items = mutableListOf<DashboardItem>()
fun submitList(newItems: List<DashboardItem>) {
val diffResult =
DiffUtil.calculateDiff(DiffCallback(newItems, items.toMutableList()))
with(items) {
clear()
addAll(newItems)
}
diffResult.dispatchUpdatesTo(this)
}
override fun getItemCount() = items.size
override fun getItemViewType(position: Int) = items[position].type.ordinal
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
@ -119,7 +133,7 @@ class DashboardAdapter @Inject constructor() :
} }
private fun bindAccountViewHolder(accountViewHolder: AccountViewHolder, position: Int) { private fun bindAccountViewHolder(accountViewHolder: AccountViewHolder, position: Int) {
val item = getItem(position) as DashboardItem.Account val item = items[position] as DashboardItem.Account
val student = item.student val student = item.student
val isLoading = item.isLoading val isLoading = item.isLoading
@ -147,7 +161,7 @@ class DashboardAdapter @Inject constructor() :
horizontalGroupViewHolder: HorizontalGroupViewHolder, horizontalGroupViewHolder: HorizontalGroupViewHolder,
position: Int position: Int
) { ) {
val item = getItem(position) as DashboardItem.HorizontalGroup val item = items[position] as DashboardItem.HorizontalGroup
val unreadMessagesCount = item.unreadMessagesCount val unreadMessagesCount = item.unreadMessagesCount
val attendancePercentage = item.attendancePercentage val attendancePercentage = item.attendancePercentage
val luckyNumber = item.luckyNumber val luckyNumber = item.luckyNumber
@ -221,7 +235,7 @@ class DashboardAdapter @Inject constructor() :
} }
private fun bindGradesViewHolder(gradesViewHolder: GradesViewHolder, position: Int) { private fun bindGradesViewHolder(gradesViewHolder: GradesViewHolder, position: Int) {
val item = getItem(position) as DashboardItem.Grades val item = items[position] as DashboardItem.Grades
val subjectWithGrades = item.subjectWithGrades.orEmpty() val subjectWithGrades = item.subjectWithGrades.orEmpty()
val gradeTheme = item.gradeTheme val gradeTheme = item.gradeTheme
val error = item.error val error = item.error
@ -250,7 +264,7 @@ class DashboardAdapter @Inject constructor() :
} }
private fun bindLessonsViewHolder(lessonsViewHolder: LessonsViewHolder, position: Int) { private fun bindLessonsViewHolder(lessonsViewHolder: LessonsViewHolder, position: Int) {
val item = getItem(position) as DashboardItem.Lessons val item = items[position] as DashboardItem.Lessons
val timetableFull = item.lessons val timetableFull = item.lessons
val binding = lessonsViewHolder.binding val binding = lessonsViewHolder.binding
@ -519,7 +533,7 @@ class DashboardAdapter @Inject constructor() :
} }
private fun bindHomeworkViewHolder(homeworkViewHolder: HomeworkViewHolder, position: Int) { private fun bindHomeworkViewHolder(homeworkViewHolder: HomeworkViewHolder, position: Int) {
val item = getItem(position) as DashboardItem.Homework val item = items[position] as DashboardItem.Homework
val homeworkList = item.homework.orEmpty() val homeworkList = item.homework.orEmpty()
val error = item.error val error = item.error
val isLoading = item.isLoading val isLoading = item.isLoading
@ -557,7 +571,7 @@ class DashboardAdapter @Inject constructor() :
announcementsViewHolder: AnnouncementsViewHolder, announcementsViewHolder: AnnouncementsViewHolder,
position: Int position: Int
) { ) {
val item = getItem(position) as DashboardItem.Announcements val item = items[position] as DashboardItem.Announcements
val schoolAnnouncementList = item.announcement.orEmpty() val schoolAnnouncementList = item.announcement.orEmpty()
val error = item.error val error = item.error
val isLoading = item.isLoading val isLoading = item.isLoading
@ -594,7 +608,7 @@ class DashboardAdapter @Inject constructor() :
} }
private fun bindExamsViewHolder(examsViewHolder: ExamsViewHolder, position: Int) { private fun bindExamsViewHolder(examsViewHolder: ExamsViewHolder, position: Int) {
val item = getItem(position) as DashboardItem.Exams val item = items[position] as DashboardItem.Exams
val exams = item.exams.orEmpty() val exams = item.exams.orEmpty()
val error = item.error val error = item.error
val isLoading = item.isLoading val isLoading = item.isLoading
@ -630,7 +644,7 @@ class DashboardAdapter @Inject constructor() :
conferencesViewHolder: ConferencesViewHolder, conferencesViewHolder: ConferencesViewHolder,
position: Int position: Int
) { ) {
val item = getItem(position) as DashboardItem.Conferences val item = items[position] as DashboardItem.Conferences
val conferences = item.conferences.orEmpty() val conferences = item.conferences.orEmpty()
val error = item.error val error = item.error
val isLoading = item.isLoading val isLoading = item.isLoading
@ -703,13 +717,20 @@ class DashboardAdapter @Inject constructor() :
val adapter by lazy { DashboardConferencesAdapter() } val adapter by lazy { DashboardConferencesAdapter() }
} }
class DashboardAdapterDiffCallback : DiffUtil.ItemCallback<DashboardItem>() { private class DiffCallback(
private val newList: List<DashboardItem>,
private val oldList: List<DashboardItem>
) : DiffUtil.Callback() {
override fun areItemsTheSame(oldItem: DashboardItem, newItem: DashboardItem) = override fun getNewListSize() = newList.size
oldItem.type == newItem.type
override fun areContentsTheSame(oldItem: DashboardItem, newItem: DashboardItem) = override fun getOldListSize() = oldList.size
oldItem == newItem
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
newList[newItemPosition] == oldList[oldItemPosition]
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
newList[newItemPosition].type == oldList[oldItemPosition].type
} }
private companion object { private companion object {

View File

@ -8,6 +8,7 @@ import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
@ -68,6 +69,12 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
override fun initView() { override fun initView() {
val mainActivity = requireActivity() as MainActivity val mainActivity = requireActivity() as MainActivity
val itemTouchHelper = ItemTouchHelper(
DashboardItemMoveCallback(
dashboardAdapter,
presenter::onDragAndDropEnd
)
)
dashboardAdapter.apply { dashboardAdapter.apply {
onAccountTileClickListener = { mainActivity.pushView(AccountFragment.newInstance()) } onAccountTileClickListener = { mainActivity.pushView(AccountFragment.newInstance()) }
@ -104,6 +111,8 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
adapter = dashboardAdapter adapter = dashboardAdapter
(itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false (itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
} }
itemTouchHelper.attachToRecyclerView(dashboardRecycler)
} }
} }

View File

@ -0,0 +1,55 @@
package io.github.wulkanowy.ui.modules.dashboard
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import java.util.Collections
class DashboardItemMoveCallback(
private val dashboardAdapter: DashboardAdapter,
private var onUserInteractionEndListener: (List<DashboardItem>) -> Unit = {}
) : ItemTouchHelper.Callback() {
override fun isLongPressDragEnabled() = true
override fun isItemViewSwipeEnabled() = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
//Not implemented
}
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = if (viewHolder.bindingAdapterPosition != 0) {
ItemTouchHelper.UP or ItemTouchHelper.DOWN
} else 0
return makeMovementFlags(dragFlags, 0)
}
override fun canDropOver(
recyclerView: RecyclerView,
current: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
) = target.bindingAdapterPosition != 0
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val list = dashboardAdapter.items.toMutableList()
Collections.swap(list, viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
dashboardAdapter.submitList(list)
return true
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
onUserInteractionEndListener(dashboardAdapter.items.toList())
}
}

View File

@ -68,6 +68,16 @@ class DashboardPresenter @Inject constructor(
.launch("dashboard_pref") .launch("dashboard_pref")
} }
fun onDragAndDropEnd(list: List<DashboardItem>) {
dashboardItemLoadedList.clear()
dashboardItemLoadedList.addAll(list)
val positionList =
list.mapIndexed { index, dashboardItem -> Pair(dashboardItem.type, index) }.toMap()
preferencesRepository.dashboardItemsPosition = positionList
}
fun loadData(forceRefresh: Boolean = false, tilesToLoad: Set<DashboardItem.Tile>) { fun loadData(forceRefresh: Boolean = false, tilesToLoad: Set<DashboardItem.Tile>) {
val oldDashboardDataToLoad = dashboardTilesToLoad val oldDashboardDataToLoad = dashboardTilesToLoad
@ -622,6 +632,7 @@ class DashboardPresenter @Inject constructor(
private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) { private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) {
val isForceRefreshError = forceRefresh && dashboardItem.error != null val isForceRefreshError = forceRefresh && dashboardItem.error != null
val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition
with(dashboardItemLoadedList) { with(dashboardItemLoadedList) {
removeAll { it.type == dashboardItem.type && !isForceRefreshError } removeAll { it.type == dashboardItem.type && !isForceRefreshError }
@ -636,7 +647,12 @@ class DashboardPresenter @Inject constructor(
} }
} }
dashboardItemLoadedList.sortBy { tile -> dashboardItemsToLoad.single { it == tile.type }.ordinal } dashboardItemLoadedList.sortBy { tile ->
dashboardItemsPosition?.getOrDefault(
tile.type,
tile.type.ordinal + 100
) ?: tile.type.ordinal
}
val isItemsLoaded = val isItemsLoaded =
dashboardItemsToLoad.all { type -> dashboardItemLoadedList.any { it.type == type } } dashboardItemsToLoad.all { type -> dashboardItemLoadedList.any { it.type == type } }

View File

@ -4,8 +4,9 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp" android:paddingHorizontal="12dp"
android:layout_marginVertical="2dp"> android:layout_marginVertical="2dp"
android:clipToPadding="false">
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/dashboard_horizontal_group_item_lucky_container" android:id="@+id/dashboard_horizontal_group_item_lucky_container"

View File

@ -10,10 +10,12 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1" android:maxLines="1"
android:textSize="13sp" android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="@id/dashboard_grades_subitem_grade_container"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="@id/dashboard_grades_subitem_grade_container"
tools:text="Urządzenia techniki kompu..." /> tools:text="Urządzenia techniki kompu..." />
<LinearLayout <LinearLayout
@ -25,6 +27,11 @@
android:orientation="horizontal" android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/dashboard_grades_subitem_title" app:layout_constraintStart_toEndOf="@id/dashboard_grades_subitem_title"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent">
tools:ignore="UselessLeaf" />
<include
layout="@layout/subitem_dashboard_small_grade"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -501,7 +501,7 @@
</plurals> </plurals>
<string name="dashboard_timetable_third_time">until %1$s</string> <string name="dashboard_timetable_third_time">until %1$s</string>
<string name="dashboard_timetable_no_lessons">No upcoming lessons</string> <string name="dashboard_timetable_no_lessons">No upcoming lessons</string>
<string name="dashboard_timetable_error">An error occurred while loading the lesson</string> <string name="dashboard_timetable_error">An error occurred while loading the lessons</string>
<string name="dashboard_homework_title">Homework</string> <string name="dashboard_homework_title">Homework</string>
<string name="dashboard_homework_no_homework">No homework to do</string> <string name="dashboard_homework_no_homework">No homework to do</string>