Skip to content

Commit 4403a60

Browse files
committed
add statistics detail select only swipe
1 parent a6ca7cd commit 4403a60

34 files changed

Lines changed: 215 additions & 49 deletions

File tree

core/src/main/java/com/example/util/simpletimetracker/core/extension/RecyclerExtensions.kt

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import android.graphics.Canvas
44
import android.graphics.Paint
55
import android.graphics.PorterDuff
66
import android.graphics.PorterDuffXfermode
7+
import android.graphics.Rect
8+
import android.graphics.Typeface
79
import android.graphics.drawable.ColorDrawable
10+
import android.graphics.drawable.Drawable
811
import androidx.annotation.ColorInt
912
import androidx.annotation.DrawableRes
1013
import androidx.core.content.ContextCompat
@@ -15,6 +18,7 @@ import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType
1518
import java.util.Collections
1619
import com.example.util.simpletimetracker.domain.extension.orZero
1720
import com.example.util.simpletimetracker.feature_views.extension.dpToPx
21+
import com.example.util.simpletimetracker.feature_views.extension.spToPx
1822

1923
fun RecyclerView.onItemMoved(
2024
getIsSelectable: (RecyclerView.ViewHolder?) -> Boolean = { true },
@@ -96,20 +100,56 @@ fun RecyclerView.onItemMoved(
96100
}
97101

98102
fun RecyclerView.onItemSwiped(
99-
@DrawableRes iconRes: Int,
103+
@DrawableRes startIconRes: Int,
104+
@DrawableRes endIconRes: Int,
100105
@ColorInt iconColor: Int,
106+
startText: String,
107+
endText: String,
108+
@ColorInt textColor: Int,
101109
@ColorInt backgroundColor: Int,
102110
getIsSelectable: (RecyclerView.ViewHolder?) -> Boolean = { true },
103-
onSwiped: (RecyclerView.ViewHolder?) -> Unit,
111+
onSwipedStart: (RecyclerView.ViewHolder?) -> Unit,
112+
onSwipedEnd: (RecyclerView.ViewHolder?) -> Unit,
104113
) {
105-
val swipeDirections = ItemTouchHelper.START
106-
val deleteIcon by lazy {
107-
ContextCompat.getDrawable(context, iconRes)?.mutate()?.apply { setTint(iconColor) }
114+
fun getIcon(@DrawableRes resId: Int): Drawable? {
115+
return ContextCompat.getDrawable(context, resId)?.mutate()?.apply { setTint(iconColor) }
108116
}
109-
val intrinsicWidth = deleteIcon?.intrinsicWidth.orZero()
110-
val intrinsicHeight = deleteIcon?.intrinsicHeight.orZero()
117+
118+
fun getTextPaint(): Paint {
119+
return Paint().apply {
120+
isAntiAlias = true
121+
color = textColor
122+
textSize = 14.spToPx().toFloat()
123+
typeface = Typeface.DEFAULT_BOLD
124+
}
125+
}
126+
127+
fun Paint.getTextHeight(text: String): Float {
128+
val bounds = Rect(0, 0, 0, 0)
129+
getTextBounds(text, 0, text.length, bounds)
130+
return bounds.height().toFloat()
131+
}
132+
133+
val swipeDirections = ItemTouchHelper.START or ItemTouchHelper.END
134+
val startIcon by lazy { getIcon(startIconRes) }
135+
val endIcon by lazy { getIcon(endIconRes) }
136+
val iconMargin = 16.dpToPx()
111137
val background = ColorDrawable()
112-
val clearPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
138+
val clearPaint = Paint().apply {
139+
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
140+
}
141+
val startTextHeight: Float
142+
val startTextPaint = getTextPaint().apply {
143+
textAlign = Paint.Align.LEFT
144+
}.also {
145+
startTextHeight = it.getTextHeight(startText)
146+
}
147+
val endTextHeight: Float
148+
val endTextPaint = getTextPaint().apply {
149+
textAlign = Paint.Align.RIGHT
150+
}.also {
151+
endTextHeight = it.getTextHeight(endText)
152+
}
113153

114154
val helper = object : ItemTouchHelper.SimpleCallback(0, 0) {
115155

@@ -129,7 +169,17 @@ fun RecyclerView.onItemSwiped(
129169
}
130170

131171
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
132-
onSwiped(viewHolder)
172+
when (direction) {
173+
ItemTouchHelper.START -> {
174+
onSwipedStart(viewHolder)
175+
}
176+
ItemTouchHelper.END -> {
177+
// Prevent item removal.
178+
ItemTouchHelper.Callback.getDefaultUIUtil().clearView(viewHolder.itemView)
179+
adapter?.notifyItemChanged(viewHolder.adapterPosition)
180+
onSwipedEnd(viewHolder)
181+
}
182+
}
133183
}
134184

135185
override fun onChildDraw(
@@ -156,27 +206,51 @@ fun RecyclerView.onItemSwiped(
156206
return
157207
}
158208

159-
// Draw the red delete background
209+
// Draw the delete background
160210
background.color = backgroundColor
161211
background.setBounds(
162-
itemView.right + dX.toInt(),
212+
if (dX > 0) itemView.left else itemView.right + dX.toInt(),
163213
itemView.top,
164-
itemView.right,
214+
if (dX > 0) itemView.left + dX.toInt() else itemView.right,
165215
itemView.bottom,
166216
)
167217
background.draw(canvas)
168218

169-
// Calculate position of delete icon
219+
// Calculate position of the icon.
220+
val iconToDraw = if (dX > 0) startIcon else endIcon
170221
val itemHeight = itemView.bottom - itemView.top
171-
val iconMargin = 16.dpToPx()
172-
val iconTop = itemView.top + (itemHeight - intrinsicHeight) / 2
173-
val iconLeft = itemView.right - iconMargin - intrinsicWidth
174-
val iconRight = itemView.right - iconMargin
175-
val iconBottom = iconTop + intrinsicHeight
176-
177-
// Draw the delete icon
178-
deleteIcon?.setBounds(iconLeft, iconTop, iconRight, iconBottom)
179-
deleteIcon?.draw(canvas)
222+
val iconTop = itemView.top + (itemHeight - iconToDraw?.intrinsicHeight.orZero()) / 2
223+
val iconBottom = iconTop + iconToDraw?.intrinsicHeight.orZero()
224+
val iconLeft: Int
225+
val iconRight: Int
226+
if (dX > 0) {
227+
iconLeft = itemView.left + iconMargin
228+
iconRight = itemView.left + iconMargin + iconToDraw?.intrinsicWidth.orZero()
229+
} else {
230+
iconLeft = itemView.right - iconMargin - iconToDraw?.intrinsicWidth.orZero()
231+
iconRight = itemView.right - iconMargin
232+
}
233+
234+
// Draw the icon.
235+
iconToDraw?.setBounds(iconLeft, iconTop, iconRight, iconBottom)
236+
iconToDraw?.draw(canvas)
237+
238+
// Draw the text.
239+
if (dX > 0) {
240+
canvas.drawText(
241+
startText,
242+
iconRight.toFloat() + iconMargin,
243+
itemView.top.toFloat() + itemView.height / 2 + startTextHeight / 2,
244+
startTextPaint,
245+
)
246+
} else {
247+
canvas.drawText(
248+
endText,
249+
iconLeft.toFloat() - iconMargin,
250+
itemView.top.toFloat() + itemView.height / 2 + endTextHeight / 2,
251+
endTextPaint,
252+
)
253+
}
180254

181255
super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
182256
}

features/feature_records_filter/api/src/main/java/com/example/util/simpletimetracker/feature_records_filter/api/RecordsFilterExcludeInteractor.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ interface RecordsFilterExcludeInteractor {
1010
currentFilters: List<RecordsFilter>,
1111
): List<RecordsFilter>
1212

13+
suspend fun excludeOther(
14+
id: Long,
15+
type: ExcludeType,
16+
): List<RecordsFilter>
17+
1318
sealed interface ExcludeType {
1419
data object Activity : ExcludeType
1520
data object Category : ExcludeType

features/feature_records_filter/src/main/java/com/example/util/simpletimetracker/feature_records_filter/interactor/RecordsFilterExcludeInteractorImpl.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,33 @@ class RecordsFilterExcludeInteractorImpl @Inject constructor(
9494
}
9595
}
9696
}
97+
98+
override suspend fun excludeOther(
99+
id: Long,
100+
type: ExcludeType,
101+
): List<RecordsFilter> {
102+
if (id == UNTRACKED_ITEM_ID) return listOf(RecordsFilter.Untracked)
103+
104+
return when (type) {
105+
is ExcludeType.Activity -> {
106+
RecordsFilter.Activity(selected = listOf(id), filtered = emptyList())
107+
}
108+
is ExcludeType.Category -> {
109+
val item = if (id == UNCATEGORIZED_ITEM_ID) {
110+
RecordsFilter.CategoryItem.Uncategorized
111+
} else {
112+
RecordsFilter.CategoryItem.Categorized(id)
113+
}
114+
RecordsFilter.Category(selected = listOf(item), filtered = emptyList())
115+
}
116+
is ExcludeType.Tag -> {
117+
val item = if (id == UNCATEGORIZED_ITEM_ID) {
118+
RecordsFilter.TagItem.Untagged
119+
} else {
120+
RecordsFilter.TagItem.Tagged(id)
121+
}
122+
RecordsFilter.Tags(selected = listOf(item), filtered = emptyList())
123+
}
124+
}.let(::listOf)
125+
}
97126
}

features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/view/StatisticsDetailFragment.kt

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.os.Bundle
44
import android.view.LayoutInflater
55
import android.view.ViewGroup
66
import androidx.fragment.app.viewModels
7+
import androidx.recyclerview.widget.RecyclerView
78
import com.example.util.simpletimetracker.core.base.BaseFragment
89
import com.example.util.simpletimetracker.core.dialog.CustomRangeSelectionDialogListener
910
import com.example.util.simpletimetracker.core.dialog.DateTimeDialogListener
@@ -220,29 +221,30 @@ class StatisticsDetailFragment :
220221
}
221222

222223
private fun initOnItemSwiped() = with(binding) {
223-
fun ViewHolderType.canBeSwiped(): Boolean {
224-
return this is StatisticsSelectableViewData
224+
fun ViewHolderType.canBeSwiped(): Boolean = this is StatisticsSelectableViewData
225+
fun Int.changeAlpha(alpha: Float): Int = ColorUtils.changeAlpha(this, alpha)
226+
fun RecyclerView.ViewHolder.getItemType(): ViewHolderType? =
227+
adapterPosition.let(contentAdapter::getItemByPosition)
228+
229+
fun RecyclerView.ViewHolder.isSelectable(): Boolean {
230+
val itemsCount = contentAdapter.currentList
231+
.filterIsInstance<StatisticsSelectableViewData>().size
232+
val currentItem = getItemType()
233+
return itemsCount > 1 && currentItem?.canBeSwiped().orFalse()
225234
}
226235

227236
val context = rvStatisticsDetailContent.context
228237
rvStatisticsDetailContent.onItemSwiped(
229-
iconRes = R.drawable.hide,
238+
startIconRes = R.drawable.show,
239+
endIconRes = R.drawable.hide,
230240
iconColor = context.getThemedAttr(R.attr.appContrastColor),
231-
backgroundColor = ColorUtils.changeAlpha(
232-
context.getThemedAttr(R.attr.appContrastColor), 0.10f,
233-
),
234-
getIsSelectable = { viewHolder ->
235-
val itemsCount = contentAdapter.currentList
236-
.filterIsInstance<StatisticsSelectableViewData>().size
237-
val currentItem = viewHolder?.adapterPosition
238-
?.let { contentAdapter.getItemByPosition(it) }
239-
itemsCount > 1 && currentItem?.canBeSwiped().orFalse()
240-
},
241-
onSwiped = { viewHolder ->
242-
viewHolder?.adapterPosition
243-
?.let { contentAdapter.getItemByPosition(it) }
244-
?.let(viewModel::onSwiped)
245-
},
241+
startText = context.getString(R.string.records_filter_exclude_other),
242+
endText = context.getString(R.string.records_filter_exclude),
243+
textColor = context.getThemedAttr(R.attr.appContrastColor).changeAlpha(0.3f),
244+
backgroundColor = context.getThemedAttr(R.attr.appContrastColor).changeAlpha(0.1f),
245+
getIsSelectable = { it?.isSelectable().orFalse() },
246+
onSwipedStart = { it?.getItemType()?.let(viewModel::onSwipedStart) },
247+
onSwipedEnd = { it?.getItemType()?.let(viewModel::onSwipedEnd) },
246248
)
247249
}
248250

features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/viewModel/StatisticsDetailViewModel.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,14 @@ class StatisticsDetailViewModel @Inject constructor(
225225
}
226226
}
227227

228-
fun onSwiped(item: ViewHolderType?) {
228+
fun onSwipedStart(item: ViewHolderType?) {
229229
item ?: return
230-
dataDistributionDelegate.onStatisticsItemSwiped(item)
230+
dataDistributionDelegate.onStatisticsItemSwipedStart(item)
231+
}
232+
233+
fun onSwipedEnd(item: ViewHolderType?) {
234+
item ?: return
235+
dataDistributionDelegate.onStatisticsItemSwipedEnd(item)
231236
}
232237

233238
fun onPreviousClick() {
@@ -388,6 +393,10 @@ class StatisticsDetailViewModel @Inject constructor(
388393
override fun onStatisticsHidden(id: Long, mode: DataDistributionMode) {
389394
filterDelegate.onStatisticsHidden(id, mode)
390395
}
396+
397+
override fun onStatisticsOtherHidden(id: Long, mode: DataDistributionMode) {
398+
filterDelegate.onStatisticsOtherHidden(id, mode)
399+
}
391400
}
392401
}
393402
}

features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/viewModel/delegate/StatisticsDetailDataDistributionViewModelDelegate.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,22 @@ class StatisticsDetailDataDistributionViewModelDelegate @Inject constructor(
5959
updateViewData(animate = false)
6060
}
6161

62-
fun onStatisticsItemSwiped(item: ViewHolderType) {
62+
fun onStatisticsItemSwipedStart(item: ViewHolderType) {
6363
val id = (item as? StatisticsSelectableViewData)?.data?.id ?: return
6464
parent?.onStatisticsHidden(
6565
id = id,
6666
mode = dataDistributionMode,
6767
)
6868
}
6969

70+
fun onStatisticsItemSwipedEnd(item: ViewHolderType) {
71+
val id = (item as? StatisticsSelectableViewData)?.data?.id ?: return
72+
parent?.onStatisticsOtherHidden(
73+
id = id,
74+
mode = dataDistributionMode,
75+
)
76+
}
77+
7078
fun updateViewData(
7179
animate: Boolean = true,
7280
) {

features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/viewModel/delegate/StatisticsDetailFilterViewModelDelegate.kt

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,22 @@ class StatisticsDetailFilterViewModelDelegate @Inject constructor(
8989
}
9090

9191
fun onStatisticsHidden(id: Long, mode: DataDistributionMode) = delegateScope.launch {
92-
val type = when (mode) {
93-
DataDistributionMode.ACTIVITY -> RecordsFilterExcludeInteractor.ExcludeType.Activity
94-
DataDistributionMode.CATEGORY -> RecordsFilterExcludeInteractor.ExcludeType.Category
95-
DataDistributionMode.TAG -> RecordsFilterExcludeInteractor.ExcludeType.Tag
96-
}
9792
filter = recordsFilterExcludeInteractor.exclude(
9893
id = id,
99-
type = type,
94+
type = mapExcludeType(mode),
10095
currentFilters = filter,
10196
)
10297
onFiltersChanged()
10398
}
10499

100+
fun onStatisticsOtherHidden(id: Long, mode: DataDistributionMode) = delegateScope.launch {
101+
filter = recordsFilterExcludeInteractor.excludeOther(
102+
id = id,
103+
type = mapExcludeType(mode),
104+
)
105+
onFiltersChanged()
106+
}
107+
105108
fun provideRecords(): List<RecordBase> {
106109
return records
107110
}
@@ -153,6 +156,15 @@ class StatisticsDetailFilterViewModelDelegate @Inject constructor(
153156
)
154157
}
155158

159+
private fun mapExcludeType(mode: DataDistributionMode): RecordsFilterExcludeInteractor.ExcludeType {
160+
val type = when (mode) {
161+
DataDistributionMode.ACTIVITY -> RecordsFilterExcludeInteractor.ExcludeType.Activity
162+
DataDistributionMode.CATEGORY -> RecordsFilterExcludeInteractor.ExcludeType.Category
163+
DataDistributionMode.TAG -> RecordsFilterExcludeInteractor.ExcludeType.Tag
164+
}
165+
return type
166+
}
167+
156168
// Delay data load until screen transition finishes
157169
// to avoid lagging while recycler is inflating views.
158170
// Only done when no shared transitions, they delay onResume.

features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/viewModel/delegate/StatisticsDetailViewModelDelegate.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ interface StatisticsDetailViewModelDelegate {
2525
fun getDateFilter(): List<RecordsFilter>
2626
suspend fun onFiltersChanged()
2727
fun onStatisticsHidden(id: Long, mode: DataDistributionMode)
28+
fun onStatisticsOtherHidden(id: Long, mode: DataDistributionMode)
2829
}
2930
}

resources/src/main/res/values-ar/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@
592592
<string name="records_filter_any_comment">أي تعليق</string>
593593
<string name="records_filter_select">اختيار</string>
594594
<string name="records_filter_exclude">استبعاد</string>
595+
<string name="records_filter_exclude_other">استبعاد الأخرى</string>
595596
<string name="records_filter_invert_selection">عكس الاختيار</string>
596597
<string name="records_filter_duration_min">الحد الأدنى</string>
597598
<string name="records_filter_duration_max">الحد الأقصى</string>

resources/src/main/res/values-ca/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ Exemple:<br/>
592592
<string name="records_filter_any_comment">Qualsevol comentari</string>
593593
<string name="records_filter_select">Seleccioneu</string>
594594
<string name="records_filter_exclude">Excloure</string>
595+
<string name="records_filter_exclude_other">Excloure altres</string>
595596
<string name="records_filter_invert_selection">Inverteix la selecció</string>
596597
<string name="records_filter_duration_min">Mínim</string>
597598
<string name="records_filter_duration_max">Màxim</string>

0 commit comments

Comments
 (0)