Compare commits
50 Commits
master
...
ubrir_test
| Author | SHA1 | Date |
|---|---|---|
|
|
e3da08a47b | |
|
|
7ce02f1c24 | |
|
|
243fb8a761 | |
|
|
3dbe35fedf | |
|
|
bdffe6305d | |
|
|
63d932b944 | |
|
|
3278562ff4 | |
|
|
5beefaeb31 | |
|
|
8041b66f8d | |
|
|
d57d8d083e | |
|
|
411401a4ec | |
|
|
41690204fe | |
|
|
09161d3877 | |
|
|
c9343855f7 | |
|
|
53adefddbb | |
|
|
194f1862c6 | |
|
|
0c8b444f20 | |
|
|
4ab4161610 | |
|
|
47b1c76fdf | |
|
|
4bf9d0991b | |
|
|
58c9f12d3c | |
|
|
6cc66fd242 | |
|
|
501fddd72d | |
|
|
3a6afded87 | |
|
|
9c67b0fa18 | |
|
|
19044fefa8 | |
|
|
fd51f7f67b | |
|
|
89271b79b8 | |
|
|
e65752731f | |
|
|
949b53f79d | |
|
|
231356bb82 | |
|
|
14d099806f | |
|
|
39289f61f4 | |
|
|
a616a39f27 | |
|
|
2ddc08c22b | |
|
|
adc53e6a7b | |
|
|
2bc496562e | |
|
|
79c9336c33 | |
|
|
527df2be1c | |
|
|
b54daede13 | |
|
|
0889ed2e9b | |
|
|
a615d54f3e | |
|
|
060aa5bb6a | |
|
|
8b0d17d5cc | |
|
|
8d4c5244a4 | |
|
|
f5f8183924 | |
|
|
a710d30b67 | |
|
|
c7e24038dd | |
|
|
184140b967 | |
|
|
5edc752686 |
|
|
@ -1 +0,0 @@
|
||||||
/build
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
Alerts
|
|
||||||
=====
|
|
||||||
|
|
||||||
### Общее описание
|
|
||||||
|
|
||||||
Модуль содержит:
|
|
||||||
`AlertDialogManager` - служит для демонстрации AlertDialog с использованием View, необходимо вызвать метод `showAlertDialog`, который
|
|
||||||
в качестве агруметов может принимать:
|
|
||||||
* `context`,
|
|
||||||
* `style` - стиль для элементов дефолтного диалога (по умолчанию R.style.AlertDialogDefault),
|
|
||||||
* `title` - Заголовок диалога,
|
|
||||||
* `message` - дополнительное сообщение,
|
|
||||||
* `positiveButtonText` - текст правой кнопки (по умолчанию "ОК"),
|
|
||||||
* `onPositiveAction` - колбэк при нажатии на правую кнопку,
|
|
||||||
* `negativeBtnTitle` - текст левой кнопки (по умолчаннию null - в этом случаи не отображается),
|
|
||||||
* `onNegativeAction` - колбэк при нажатии на левую кнопку,
|
|
||||||
* `dialogLayout` - id кастомного layout (по умолчанию R.layout.dialog_alert).
|
|
||||||
|
|
||||||
---
|
|
||||||
`ComposableAlertDialog` - служит для демонстрации AlertDialog с использованием Jetpack Compose, необходимо вызвать метод `ShowAlertDialog`, который
|
|
||||||
в качестве агруметов может принимать:
|
|
||||||
* `isDialogOpen` - индикатор состояния диалога,
|
|
||||||
* `title` - Заголовок диалога,
|
|
||||||
* `message` - дополнительное сообщение,
|
|
||||||
* `positiveButtonText` - текст правой кнопки,
|
|
||||||
* `onPositiveAction` - колбэк при нажатии на правую кнопку,
|
|
||||||
* `negativeBtnTitle` - текст левой кнопки (по умолчаннию null - в этом случаи не отображается),
|
|
||||||
* `onNegativeAction` - колбэк при нажатии на левую кнопку.
|
|
||||||
|
|
||||||
Кастомизация Compose версии происходит по средствам инициализации полей: customTitle, customMessage, customConfirmBtn, customNegativeBtn
|
|
||||||
|
|
||||||
### Примеры
|
|
||||||
|
|
||||||
View версия (ViewableAlertDialog) ok/cancel диалога:
|
|
||||||
```kotlin
|
|
||||||
alertDialogManager.showAlertDialog(
|
|
||||||
context = activity,
|
|
||||||
title = "Ой, что-то пошло не так",
|
|
||||||
message = "Попробуйте ещё раз",
|
|
||||||
positiveButtonText = "Ещё раз",
|
|
||||||
onPositiveAction = { retryConnection() },
|
|
||||||
negativeBtnTitle = "Отмена"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
View версия (ViewableAlertDialog) ok диалога:
|
|
||||||
```kotlin
|
|
||||||
alertDialogManager.showOkDialog(
|
|
||||||
context = dialog?.window?.decorView?.context ?: throw Exception(),
|
|
||||||
title = "Необходимо изменить настройки",
|
|
||||||
okButtonText = "Ок",
|
|
||||||
onOkAction = {
|
|
||||||
viewModel.dispatchAction(ItemAction.ChangeSettings)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Для катомизации стилей элементов в дефолтной разметке диалога необходимо создать стиль - наследника от `ThemeOverlay.MaterialComponents.MaterialAlertDialog` и переопределить стили:
|
|
||||||
* `materialAlertDialogTitleTextStyle` - стиль для заголока (наследник от `MaterialAlertDialog.MaterialComponents.Title.Text`),
|
|
||||||
* `materialAlertDialogBodyTextStyle` - стиль для подзаголовка (наследник от `MaterialAlertDialog.MaterialComponents.Body.Text`),
|
|
||||||
* `buttonBarPositiveButtonStyle` - стиль для позитивной кнопки (наследник от `Widget.MaterialComponents.Button.TextButton.Dialog`),
|
|
||||||
* `buttonBarNegativeButtonStyle` - стиль для негативной кнопки (наследник от `Widget.MaterialComponents.Button.TextButton.Dialog`).
|
|
||||||
|
|
||||||
Compose версия (ComposableAlertDialog):
|
|
||||||
```kotlin
|
|
||||||
val isDialogOpen = remember { mutableStateOf(false)}
|
|
||||||
....
|
|
||||||
//Создание диалога
|
|
||||||
ComposableAlertDialog
|
|
||||||
.apply { customTitle = { Text(text = "Ой, что-то пошло не так", color = Color.Blue) } }
|
|
||||||
.ShowAlertDialog(isDialogOpen, message = "Проблемы с сетью", positiveButtonText = "ОК")
|
|
||||||
....
|
|
||||||
//Отображение диалога
|
|
||||||
isDialogOpen.value = true
|
|
||||||
```
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
apply from: "../android-configs/lib-config.gradle"
|
|
||||||
|
|
||||||
ext {
|
|
||||||
composeVersion = '1.1.1'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation("androidx.core:core-ktx")
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout")
|
|
||||||
implementation("com.google.android.material:material")
|
|
||||||
implementation project(":kotlin-extensions")
|
|
||||||
|
|
||||||
implementation "androidx.compose.runtime:runtime:$composeVersion"
|
|
||||||
implementation "androidx.compose.ui:ui:$composeVersion"
|
|
||||||
implementation "androidx.compose.foundation:foundation:$composeVersion"
|
|
||||||
implementation "androidx.compose.foundation:foundation-layout:$composeVersion"
|
|
||||||
implementation "androidx.compose.material:material:$composeVersion"
|
|
||||||
implementation "androidx.compose.runtime:runtime-livedata:$composeVersion"
|
|
||||||
implementation "androidx.compose.ui:ui-tooling:$composeVersion"
|
|
||||||
implementation "com.google.android.material:compose-theme-adapter:1.1.9"
|
|
||||||
|
|
||||||
constraints {
|
|
||||||
implementation("androidx.core:core-ktx") {
|
|
||||||
version {
|
|
||||||
require '1.0.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout") {
|
|
||||||
version {
|
|
||||||
require '2.2.0-alpha03'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation("com.google.android.material:material") {
|
|
||||||
version {
|
|
||||||
require '1.1.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
<manifest
|
|
||||||
package="ru.touchin.roboswag.alerts"/>
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
package ru.touchin.roboswag.composable_dialog
|
|
||||||
|
|
||||||
import androidx.compose.material.AlertDialog
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
|
|
||||||
object ComposableAlertDialog {
|
|
||||||
var customTitle: @Composable (() -> Unit)? = null
|
|
||||||
var customMessage: @Composable (() -> Unit)? = null
|
|
||||||
var customConfirmBtn: @Composable (() -> Unit)? = null
|
|
||||||
var customNegativeBtn: @Composable (() -> Unit)? = null
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ShowAlertDialog(
|
|
||||||
isDialogOpen: MutableState<Boolean>,
|
|
||||||
title: String? = null,
|
|
||||||
message: String? = null,
|
|
||||||
positiveButtonText: String? = null,
|
|
||||||
onPositiveAction: (() -> Unit)? = null,
|
|
||||||
negativeBtnTitle: String? = null,
|
|
||||||
onNegativeAction: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
if (!isDialogOpen.value) return
|
|
||||||
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { isDialogOpen.value = false },
|
|
||||||
title = customTitle ?: { Text(title.orEmpty()) },
|
|
||||||
text = customMessage ?: { Text(message.orEmpty()) },
|
|
||||||
confirmButton = customConfirmBtn ?: createButton(positiveButtonText.orEmpty()) {
|
|
||||||
onPositiveAction?.invoke()
|
|
||||||
isDialogOpen.value = false
|
|
||||||
},
|
|
||||||
dismissButton = when {
|
|
||||||
customNegativeBtn != null -> customNegativeBtn
|
|
||||||
negativeBtnTitle != null -> createButton(negativeBtnTitle) {
|
|
||||||
onNegativeAction?.invoke()
|
|
||||||
isDialogOpen.value = false
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun createButton(text: String, onClickAction: () -> Unit): @Composable (() -> Unit) =
|
|
||||||
{
|
|
||||||
TextButton(onClick = onClickAction) {
|
|
||||||
Text(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
package ru.touchin.roboswag.viewable_dialog
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import ru.touchin.roboswag.alerts.R
|
|
||||||
|
|
||||||
class AlertDialogManager {
|
|
||||||
|
|
||||||
@SuppressWarnings("detekt.LongParameterList")
|
|
||||||
fun showAlertDialog(
|
|
||||||
context: Context,
|
|
||||||
style: Int = R.style.AlertDialogDefault,
|
|
||||||
title: String? = null,
|
|
||||||
message: String? = null,
|
|
||||||
positiveButtonText: String = context.getString(R.string.positive_btn),
|
|
||||||
onPositiveAction: (() -> Unit)? = null,
|
|
||||||
negativeBtnTitle: String? = null,
|
|
||||||
onNegativeAction: (() -> Unit)? = null,
|
|
||||||
dialogLayout: Int = R.layout.dialog_alert,
|
|
||||||
cancelable: Boolean = true,
|
|
||||||
onCancelAction: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
val styledContext = ContextThemeWrapper(context, style)
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(styledContext)
|
|
||||||
.setView(LayoutInflater.from(styledContext).inflate(dialogLayout, null))
|
|
||||||
.show()
|
|
||||||
.setupAlertDialog(
|
|
||||||
title = title,
|
|
||||||
message = message,
|
|
||||||
positiveButtonText = positiveButtonText,
|
|
||||||
onPositiveClick = onPositiveAction,
|
|
||||||
negativeButtonText = negativeBtnTitle,
|
|
||||||
onNegativeClick = onNegativeAction,
|
|
||||||
cancelable = cancelable,
|
|
||||||
onCancelAction = onCancelAction
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showOkDialog(
|
|
||||||
context: Context,
|
|
||||||
style: Int = R.style.AlertDialogDefault,
|
|
||||||
title: String? = null,
|
|
||||||
message: String? = null,
|
|
||||||
okButtonText: String = context.getString(R.string.positive_btn),
|
|
||||||
onOkAction: (() -> Unit)? = null,
|
|
||||||
cancelable: Boolean = true,
|
|
||||||
onCancelAction: () -> Unit = {}
|
|
||||||
) = showAlertDialog(
|
|
||||||
context = context,
|
|
||||||
style = style,
|
|
||||||
title = title,
|
|
||||||
message = message,
|
|
||||||
positiveButtonText = okButtonText,
|
|
||||||
onPositiveAction = onOkAction,
|
|
||||||
cancelable = cancelable,
|
|
||||||
onCancelAction = onCancelAction
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun AlertDialog.setupAlertDialog(
|
|
||||||
title: String? = null,
|
|
||||||
message: String? = null,
|
|
||||||
positiveButtonText: String,
|
|
||||||
onPositiveClick: (() -> Unit)? = null,
|
|
||||||
negativeButtonText: String? = null,
|
|
||||||
onNegativeClick: (() -> Unit)? = null,
|
|
||||||
cancelable: Boolean = true,
|
|
||||||
onCancelAction: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
setCancelable(cancelable)
|
|
||||||
setOnDismissListener { onCancelAction() }
|
|
||||||
findViewById<TextView>(R.id.alert_title)?.setTextOrGone(title)
|
|
||||||
findViewById<TextView>(R.id.alert_message)?.setTextOrGone(message)
|
|
||||||
findViewById<TextView>(R.id.alert_positive_button)?.let { buttonView ->
|
|
||||||
setupButton(this, buttonView, positiveButtonText, onPositiveClick)
|
|
||||||
}
|
|
||||||
findViewById<TextView>(R.id.alert_negative_button)?.let { buttonView ->
|
|
||||||
setupButton(this, buttonView, negativeButtonText, onNegativeClick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package ru.touchin.roboswag.viewable_dialog
|
|
||||||
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import ru.touchin.extensions.setOnRippleClickListener
|
|
||||||
|
|
||||||
fun setupButton(alertDialog: AlertDialog, buttonView: TextView, text: String?, onButtonClick: (() -> Unit)?) {
|
|
||||||
buttonView.setTextOrGone(text)
|
|
||||||
buttonView.setOnRippleClickListener {
|
|
||||||
onButtonClick?.invoke()
|
|
||||||
alertDialog.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun TextView.setTextOrGone(text: CharSequence?) {
|
|
||||||
isVisible = !text.isNullOrEmpty()
|
|
||||||
setText(text)
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
<?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:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingTop="22dp"
|
|
||||||
android:paddingBottom="8dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/alert_title"
|
|
||||||
style="?attr/materialAlertDialogTitleTextStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="Header" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/alert_message"
|
|
||||||
style="?attr/materialAlertDialogBodyTextStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/alert_title"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/alert_title"
|
|
||||||
tools:text="Text" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/alert_positive_button"
|
|
||||||
style="?attr/buttonBarPositiveButtonStyle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/alert_message"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/alert_message"
|
|
||||||
tools:text="OK" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/alert_negative_button"
|
|
||||||
style="?attr/buttonBarNegativeButtonStyle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@id/alert_message"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:layout_toStartOf="@id/alert_positive_button"
|
|
||||||
android:layout_toLeftOf="@id/alert_positive_button"
|
|
||||||
app:layout_constraintRight_toLeftOf="@id/alert_positive_button"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/alert_message"
|
|
||||||
tools:text="Cancel" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="positive_btn">OK</string>
|
|
||||||
<string name="negative_btn">Cancel</string>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="AlertDialogDefault" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
|
|
||||||
<item name="colorSurface">#FFFFFF</item>
|
|
||||||
<item name="materialAlertDialogTitleTextStyle">@style/MaterialAlertDialog.MaterialComponents.Title.Text.Default</item>
|
|
||||||
<item name="buttonBarPositiveButtonStyle">@style/MaterialAlertDialog.MaterialComponents.Button.Default</item>
|
|
||||||
<item name="buttonBarNegativeButtonStyle">@style/MaterialAlertDialog.MaterialComponents.Button.Default</item>
|
|
||||||
<item name="materialAlertDialogBodyTextStyle">@style/MaterialAlertDialog.MaterialComponents.Body.Text.Default</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="MaterialAlertDialog.MaterialComponents.Title.Text.Default" parent="MaterialAlertDialog.MaterialComponents.Title.Text">
|
|
||||||
<item name="android:textColor">#383838</item>
|
|
||||||
<item name="android:textSize">15sp</item>
|
|
||||||
<item name="android:layout_marginLeft">24dp</item>
|
|
||||||
<item name="android:layout_marginRight">24dp</item>
|
|
||||||
<item name="android:paddingBottom">16dp</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="MaterialAlertDialog.MaterialComponents.Body.Text.Default" parent="MaterialAlertDialog.MaterialComponents.Body.Text">
|
|
||||||
<item name="android:textColor">#383838</item>
|
|
||||||
<item name="android:textSize">12sp</item>
|
|
||||||
<item name="android:layout_marginLeft">24dp</item>
|
|
||||||
<item name="android:layout_marginRight">24dp</item>
|
|
||||||
<item name="android:paddingBottom">28dp</item>
|
|
||||||
<item name="android:lineSpacingExtra">8dp</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="MaterialAlertDialog.MaterialComponents.Button.Default" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
|
|
||||||
<item name="android:textColor">#383838</item>
|
|
||||||
<item name="android:minWidth">56dp</item>
|
|
||||||
<item name="android:gravity">center</item>
|
|
||||||
<item name="android:textAllCaps">true</item>
|
|
||||||
<item name="android:textSize">14sp</item>
|
|
||||||
<item name="android:paddingTop">11dp</item>
|
|
||||||
<item name="android:paddingBottom">9dp</item>
|
|
||||||
<item name="android:layout_marginTop">8dp</item>
|
|
||||||
<item name="android:layout_marginRight">8dp</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -3,5 +3,5 @@ apply plugin: 'com.android.application'
|
||||||
apply from: '../RoboSwag/android-configs/common-config.gradle'
|
apply from: '../RoboSwag/android-configs/common-config.gradle'
|
||||||
|
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-parcelize'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
rootProject.ext {
|
rootProject.ext {
|
||||||
compileSdk = 30
|
compileSdk = 29
|
||||||
|
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 30
|
targetSdk = 29
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
/build
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
# Описание
|
|
||||||
|
|
||||||
Модуль содержит реализацию следующих типов фильтров:
|
|
||||||
|
|
||||||
1. Выбор одного/нескольких из доступных значений списка
|
|
||||||
2. Выбор одного/нескольких значений из перечня тегов
|
|
||||||
3. Выбор минимального и максимального значения из диапозона
|
|
||||||
|
|
||||||
# Использование
|
|
||||||
|
|
||||||
## 1. Выбор одного/нескольких из доступных значений списка
|
|
||||||
|
|
||||||
### Как использовать
|
|
||||||
``` kotlin
|
|
||||||
val selectorView = ListSelectionView<DefaultSelectionItem, SelectionItemViewHolder<DefaultSelectionItem>>(context)
|
|
||||||
.Builder()
|
|
||||||
.setItems(navArgs.items)
|
|
||||||
.addItemDecoration((TopDividerItemDecoration(
|
|
||||||
context = requireContext(),
|
|
||||||
drawableId = R.drawable.list_divider_1dp,
|
|
||||||
startMargin = START_MARGIN_DIVIDER_DP.px
|
|
||||||
)))
|
|
||||||
.withSelectionType(ListSelectionView.SelectionType.SINGLE_SELECT)
|
|
||||||
.onResultListener { items ->
|
|
||||||
viewModel.dispatchAction(SelectItemAction.SelectItem(items))
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
```
|
|
||||||
### Конфигурации
|
|
||||||
* при создании `ListSelectionView<ItemType, HolderType>` необходимо передлать `ItemType` - класс модели данных в списке, `HolderType` - класс viewHolder-а в recyclerView.
|
|
||||||
Для использования дефолтной реализации необходимо использовать типы `<DefaultSelectionItem, SelectionItemViewHolder<DefaultSelectionItem>>`
|
|
||||||
* в метод `setItems(List<ItemType>)` необходимо передать список объектов
|
|
||||||
* метод `addItemDecoration(itemDecoration: RecyclerView.ItemDecoration)` можно использовать для передачи объекта `RecyclerView.ItemDecoration`
|
|
||||||
* метод `withSelectionType(type: SelectionType)` используется для указания типа выбора:
|
|
||||||
* `SINGLE_SELECT` - <em>по умолчанию</em> - позволяет выбрать один выариант, при этом будет выбран всегда как минимум один вариант
|
|
||||||
* `MULTI_SELECT` - позволяет выбрать несколько вариантов из списка, при этом можно полностью выбрать все варианты и убрать выделение со всех вариантов
|
|
||||||
* метод `showInHolder(HolderFactoryType<ItemType>)` используется для определения кастомного viewHolder для списка с недефолтной разметкой.
|
|
||||||
``` kotlin
|
|
||||||
val selectorView = ListSelectionView<TestSelectionItem, TestItemViewHolder>(context)
|
|
||||||
.Builder()
|
|
||||||
.showInHolder { parent, clickListener, selectionType ->
|
|
||||||
TestItemViewHolder(
|
|
||||||
binding = TestSelectionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
|
||||||
onItemSelectAction = clickListener,
|
|
||||||
selectionType = selectionType
|
|
||||||
)
|
|
||||||
}
|
|
||||||
...
|
|
||||||
.build()
|
|
||||||
```
|
|
||||||
* колбэк `onSelectedItemsListener(listener: OnSelectedItemsListener<ItemType>)` можно использовать для получения списка всех элементов `ItemType` после каждого выбора
|
|
||||||
* колбэк `onSelectedItemListener(listener: OnSelectedItemListener<ItemType>)` можно использовать для получения элемента списка `ItemType`, по которому произошел клик
|
|
||||||
* после вызова конфигурационных методов обязательно необходимо вызать метод `build()`
|
|
||||||
|
|
||||||
### Кастомизация стиля дефолтной реализации ViewHolder без необходимости создания кастомного layout и viewHolder
|
|
||||||
|
|
||||||
#### 1) Определить кастомную тему и стили элементов
|
|
||||||
1. Стиль для **текста элемента списка** должен быть наследником стиля `Widget.FilterSelection.Item`
|
|
||||||
``` xml
|
|
||||||
<style name="Widget.Custom.FilterSelection.Item" parent="@style/Widget.FilterSelection.Item">
|
|
||||||
<item name="android:textAppearance">@style/Text15sp.Regular.Black</item>
|
|
||||||
<item name="android:paddingTop">2dp</item>
|
|
||||||
<item name="android:lineSpacingExtra">3sp</item>
|
|
||||||
<item name="android:translationY">-1.71sp</item>
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
2. Стиль для **индикатора выбора** должен быть наследником стиля `Widget.FilterSelection.Radio`
|
|
||||||
Передайте `selector-drawable` для кастомизации вида индикатора в конце строки
|
|
||||||
``` xml
|
|
||||||
<style name="Widget.Custom.FilterSelection.Radio" parent="@style/Widget.FilterSelection.Radio">
|
|
||||||
<item name="android:button">@drawable/selector_checkbox</item>
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
3. Создайте **тему**, которая должна быть наследником `Theme.FilterSelection`
|
|
||||||
``` xml
|
|
||||||
<style name="Theme.Custom.FilterSelection" parent="@style/Theme.FilterSelection">
|
|
||||||
<item name="sheetSelection_itemStyle">@style/Widget.Custom.FilterSelection.Item</item>
|
|
||||||
<item name="sheetSelection_radioStyle">@style/Widget.Custom.FilterSelection.Radio</item>
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
#### 2) Применить тему при создании view
|
|
||||||
При создании вью в коде можно указать тему, используя `ContextThemeWrapper`
|
|
||||||
``` kotlin
|
|
||||||
val newContext = ContextThemeWrapper(requireContext(), R.style.Theme_Custom_FilterSelection)
|
|
||||||
val selectorView = ListSelectionView(newContext)
|
|
||||||
.Builder()
|
|
||||||
...
|
|
||||||
.build()
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Выбор одного/нескольких значений из перечня тегов
|
|
||||||
|
|
||||||
* `TagLayoutView` - view-контейнер для тегов
|
|
||||||
* `TagView` - view для тега. <em>Кастомная разметка для тега должна содержать в корне `TagView`</em>
|
|
||||||
|
|
||||||
### Как использовать
|
|
||||||
``` kotlin
|
|
||||||
binding.tagItemLayout
|
|
||||||
.Builder(getFilterItem())
|
|
||||||
.setSpacing(16)
|
|
||||||
.setSelectionType(SelectionType.MULTI_SELECT) // по умолчанию
|
|
||||||
.isSingleLine(false) // по умолчанию
|
|
||||||
.onPropertySelectedAction { filterProperty: FilterProperty ->
|
|
||||||
//Do something
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
```
|
|
||||||
### Конфигурации
|
|
||||||
* метод `setSelectionType(SelectionType)` конфигурирует тип выбора:
|
|
||||||
* `SINGLE_SELECT` - выбор одного варианта сбрасывает select у всех остальных
|
|
||||||
* `MULTI_SELECT` - <em>по умолчанию</em> - мультивыбор тегов с учетом исключающих фильтров
|
|
||||||
* метод `isSingleLine(Boolean)` конфигурирует вид контейнера с тегами: `true` соответствует горизонтальному контейнеру со скроллом
|
|
||||||
* `setTagLayout(Int)` устанавливает разметку для тега. Если не задано - то используется дефолтная разметка `layout_default_tag.xml`
|
|
||||||
* `setMaxTagCount(Int)` позволяет ограничить количество отображаемых тегов. По умолчанию ограничения нет.
|
|
||||||
* `setMoreTagLayout(Int, String)` устанавливает разметку для тега, который отображается для дополнительного тега. Если не указана - то тег не будет создан
|
|
||||||
* `setSpacing(Int)`, `setSpacingHorizontal(Int)` и `setSpacingVertical(Int)` можно использовать для настройки расстояния между тегами. По умолчанию - 0
|
|
||||||
* `onMoreValuesAction(FilterMoreAction)` и `onPropertySelectedAction(PropertySelectedAction)` используются для передачи колбэков на клик по тегу типа "Еще" и обычного тега соответственно
|
|
||||||
* после вызова конфигурационных методов обязательно необходимо вызать метод `build()`
|
|
||||||
* в Builder необходимо передать объект `filterItem: FilterItem`
|
|
||||||
|
|
||||||
|
|
||||||
## 3. Выбор минимального и максимального значения из диапозона
|
|
||||||
|
|
||||||
* `RangeChoiceView` - контейнер для слайдера и редактируемых полей
|
|
||||||
* `FilterRangeSlider` - слайдер - <em>Можно использовать как отдельный элемент</em>
|
|
||||||
* `HintInputView` - view для редактируемого поля начала и окончания диапозона
|
|
||||||
|
|
||||||
### Как использовать
|
|
||||||
|
|
||||||
В разметке
|
|
||||||
``` xml
|
|
||||||
<ru.touchin.roboswag.base_filters.range.RangeChoiceView
|
|
||||||
android:id="@+id/range_values_test"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
style="@style/FilterRangeChoice" //не забудьте указать стиль
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
```
|
|
||||||
|
|
||||||
Настройка в коде
|
|
||||||
``` kotlin
|
|
||||||
fun setupValues(item: FilterRangeItem) {
|
|
||||||
binding.rangeValuesTest.setupRangeValues(
|
|
||||||
rangeFilterItem = item,
|
|
||||||
onChangeCallback = callback
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetValues() {
|
|
||||||
binding.rangeValuesTest.resetRangeValue()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
### Конфигурации
|
|
||||||
Вся конфигурация вьюх осуществляется через стили:
|
|
||||||
* Для `RangeChoiceView`:
|
|
||||||
* `filterRange_sliderMargin` - расстояние от слайдера до редактируемых полей
|
|
||||||
* `filterRange_startHint` - ссылка на строку с текстом подсказки в редактируемом поле для начального значения
|
|
||||||
* `filterRange_endHint` - ссылка на строку с текстом подсказки в редактируемом поле для конечного значения
|
|
||||||
* `filterRange_theme` - ссылка на тему
|
|
||||||
* В теме:
|
|
||||||
* атрибут `filterRange_sliderStyle` - ссылка на стиль слайдера
|
|
||||||
* атрибут `filterRange_hintViewStyle` - ссылка на стиль `HintInputView`
|
|
||||||
* атрибут `filterRange_hintTextStyle` - ссылка на стиль `TextView` внутри `HintInputView`
|
|
||||||
* атрибут `filterRange_valueEditTextStyle` - ссылка на стиль `EditText` внутри `HintInputView`
|
|
||||||
* Для `FilterRangeSlider`:
|
|
||||||
* `trackColorActive`
|
|
||||||
* `trackColorInactive`
|
|
||||||
* `trackHeight`
|
|
||||||
* `thumbElevation`
|
|
||||||
* `thumbColor`
|
|
||||||
* `labelBehavior`
|
|
||||||
* `haloRadius`
|
|
||||||
* `filterRange_stepTextAppearance`
|
|
||||||
* `filterRange_activeTickColor`
|
|
||||||
* `filterRange_inactiveTickColor`
|
|
||||||
* `filterRange_stepValueMarginTop`
|
|
||||||
* `filterRange_sliderPointSize`
|
|
||||||
* `filterRange_pointShape`
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
apply from: "../android-configs/lib-config.gradle"
|
|
||||||
apply plugin: 'kotlin-parcelize'
|
|
||||||
|
|
||||||
android {
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation project(":utils")
|
|
||||||
implementation project(":recyclerview-adapters")
|
|
||||||
implementation project(":navigation-base")
|
|
||||||
implementation project(":kotlin-extensions")
|
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib")
|
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx")
|
|
||||||
|
|
||||||
implementation("androidx.appcompat:appcompat")
|
|
||||||
implementation("com.google.android.material:material")
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout") {
|
|
||||||
version {
|
|
||||||
require '2.0.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constraints {
|
|
||||||
implementation("androidx.appcompat:appcompat") {
|
|
||||||
version {
|
|
||||||
require '1.0.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx") {
|
|
||||||
version {
|
|
||||||
require '1.0.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
<manifest
|
|
||||||
package="ru.touchin.roboswag.base_filters"/>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters
|
|
||||||
|
|
||||||
enum class SelectionType {
|
|
||||||
SINGLE_SELECT, MULTI_SELECT
|
|
||||||
}
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.range
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.Paint.Cap
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import androidx.annotation.StyleRes
|
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
|
||||||
import androidx.core.content.withStyledAttributes
|
|
||||||
import androidx.core.widget.TextViewCompat
|
|
||||||
import com.google.android.material.shape.CornerFamily
|
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel
|
|
||||||
import com.google.android.material.slider.RangeSlider
|
|
||||||
import ru.touchin.roboswag.base_filters.R
|
|
||||||
import ru.touchin.roboswag.components.utils.getColorSimple
|
|
||||||
|
|
||||||
class FilterRangeSlider @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0,
|
|
||||||
defStyleRes: Int = 0
|
|
||||||
) : RangeSlider(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
var points: List<Int>? = null
|
|
||||||
set(value) {
|
|
||||||
field = value?.sorted()?.filter { it > valueFrom && it < valueTo }
|
|
||||||
}
|
|
||||||
|
|
||||||
private val innerThumbRadius: Int = thumbRadius
|
|
||||||
|
|
||||||
private var stepValueMarginTop = 0f
|
|
||||||
private var inactiveTickColor: Int = context.getColorSimple(R.color.slider_point_inactive)
|
|
||||||
private var activeTickColor: Int = context.getColorSimple(R.color.slider_point_active)
|
|
||||||
private var sliderPointSize: Float = 5f
|
|
||||||
|
|
||||||
@StyleRes
|
|
||||||
private var stepTextAppearance: Int = -1
|
|
||||||
private var shape: Shape = Shape.CIRCLE
|
|
||||||
|
|
||||||
private var trackCenterY: Float = -1F
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Set original thumb radius to zero to draw custom one on top
|
|
||||||
thumbRadius = 0
|
|
||||||
|
|
||||||
context.withStyledAttributes(attrs, R.styleable.FilterRangeSlider, defStyleAttr, defStyleRes) {
|
|
||||||
stepValueMarginTop = getDimension(R.styleable.FilterRangeSlider_filterRange_stepValueMarginTop, stepValueMarginTop)
|
|
||||||
inactiveTickColor = getColor(R.styleable.FilterRangeSlider_filterRange_inactiveTickColor, inactiveTickColor)
|
|
||||||
activeTickColor = getColor(R.styleable.FilterRangeSlider_filterRange_activeTickColor, activeTickColor)
|
|
||||||
sliderPointSize = getDimension(R.styleable.FilterRangeSlider_filterRange_sliderPointSize, sliderPointSize)
|
|
||||||
stepTextAppearance = getResourceId(R.styleable.FilterRangeSlider_filterRange_stepTextAppearance, -1)
|
|
||||||
shape = Shape.values()[getInt(R.styleable.FilterRangeSlider_filterRange_pointShape, Shape.CIRCLE.ordinal)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val thumbDrawable = MaterialShapeDrawable().apply {
|
|
||||||
shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS
|
|
||||||
setBounds(0, 0, innerThumbRadius * 2, innerThumbRadius * 2)
|
|
||||||
elevation = thumbElevation
|
|
||||||
state = drawableState
|
|
||||||
fillColor = thumbTintList
|
|
||||||
shapeAppearanceModel = ShapeAppearanceModel
|
|
||||||
.builder()
|
|
||||||
.setAllCorners(shape.value, innerThumbRadius.toFloat())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val inactiveTicksPaint = getDefaultTickPaint().apply { color = inactiveTickColor }
|
|
||||||
|
|
||||||
private val activeTicksPaint = getDefaultTickPaint().apply { color = activeTickColor }
|
|
||||||
|
|
||||||
private fun getDefaultTickPaint() = Paint().apply {
|
|
||||||
isAntiAlias = true
|
|
||||||
style = Paint.Style.STROKE
|
|
||||||
strokeCap = Cap.ROUND
|
|
||||||
strokeWidth = sliderPointSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using TextView as a bridge to get text params
|
|
||||||
private val stepValuePaint: Paint = AppCompatTextView(context).apply {
|
|
||||||
stepTextAppearance.takeIf { it != -1 }?.let { TextViewCompat.setTextAppearance(this, it) }
|
|
||||||
}.let { textView ->
|
|
||||||
Paint().apply {
|
|
||||||
isAntiAlias = true
|
|
||||||
color = textView.currentTextColor
|
|
||||||
textSize = textView.textSize
|
|
||||||
typeface = textView.typeface
|
|
||||||
textAlign = Paint.Align.CENTER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
||||||
|
|
||||||
trackCenterY = measuredHeight / 2F
|
|
||||||
|
|
||||||
val height = trackCenterY + trackHeight / 2F + stepValueMarginTop + stepValuePaint.textSize
|
|
||||||
setMeasuredDimension(measuredWidth, height.toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
|
||||||
super.onDraw(canvas)
|
|
||||||
|
|
||||||
drawTicks(canvas)
|
|
||||||
drawThumb(canvas)
|
|
||||||
drawStepValues(canvas)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun drawTicks(canvas: Canvas) {
|
|
||||||
if (points.isNullOrEmpty()) return
|
|
||||||
|
|
||||||
val ticksCoordinates = mutableListOf<Float>()
|
|
||||||
points?.forEach { point ->
|
|
||||||
ticksCoordinates.add(normalizeValue(point.toFloat()) * trackWidth + trackSidePadding)
|
|
||||||
ticksCoordinates.add(trackCenterY)
|
|
||||||
}
|
|
||||||
|
|
||||||
val leftPointsSize = points?.count { it < values[0] } ?: 0
|
|
||||||
val rightPointSize = points?.count { it > values[1] } ?: 0
|
|
||||||
val activePointSize = (points?.size ?: 0) - leftPointsSize - rightPointSize
|
|
||||||
|
|
||||||
// Draw inactive ticks to the left of the smallest thumb.
|
|
||||||
canvas.drawPoints(ticksCoordinates.toFloatArray(), 0, leftPointsSize * 2, inactiveTicksPaint)
|
|
||||||
|
|
||||||
// Draw active ticks between the thumbs.
|
|
||||||
canvas.drawPoints(
|
|
||||||
ticksCoordinates.toFloatArray(),
|
|
||||||
leftPointsSize * 2,
|
|
||||||
activePointSize * 2,
|
|
||||||
activeTicksPaint
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw inactive ticks to the right of the largest thumb.
|
|
||||||
canvas.drawPoints(
|
|
||||||
ticksCoordinates.toFloatArray(),
|
|
||||||
leftPointsSize * 2 + activePointSize * 2,
|
|
||||||
rightPointSize * 2,
|
|
||||||
inactiveTicksPaint
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun drawThumb(canvas: Canvas) {
|
|
||||||
for (value in values) {
|
|
||||||
canvas.save()
|
|
||||||
canvas.translate(
|
|
||||||
(trackSidePadding + (normalizeValue(value) * trackWidth).toInt() - innerThumbRadius).toFloat(),
|
|
||||||
trackCenterY - innerThumbRadius
|
|
||||||
)
|
|
||||||
thumbDrawable.draw(canvas)
|
|
||||||
canvas.restore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun drawStepValues(canvas: Canvas) {
|
|
||||||
points?.forEach { point ->
|
|
||||||
canvas.drawText(
|
|
||||||
point.toString(),
|
|
||||||
normalizeValue(point.toFloat()) * trackWidth + trackSidePadding,
|
|
||||||
trackCenterY + trackHeight / 2F + stepValueMarginTop + stepValuePaint.textSize - 3F,
|
|
||||||
stepValuePaint
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun normalizeValue(value: Float) = (value - valueFrom) / (valueTo - valueFrom)
|
|
||||||
|
|
||||||
private enum class Shape(val value: Int) {
|
|
||||||
CIRCLE(CornerFamily.ROUNDED),
|
|
||||||
CUT(CornerFamily.CUT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.range
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import ru.touchin.roboswag.base_filters.databinding.ViewHintInputBinding
|
|
||||||
|
|
||||||
class HintInputView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0,
|
|
||||||
defStyleRes: Int = 0
|
|
||||||
) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
|
|
||||||
|
|
||||||
private val binding = ViewHintInputBinding.inflate(LayoutInflater.from(context), this)
|
|
||||||
|
|
||||||
var inputText: String = ""
|
|
||||||
set(value) {
|
|
||||||
setText(value)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setHint(value: String?) {
|
|
||||||
binding.startHint.text = value.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setOnEditorActionListener(listener: TextView.OnEditorActionListener) =
|
|
||||||
binding.editText.setOnEditorActionListener(listener)
|
|
||||||
|
|
||||||
private fun setText(value: String) {
|
|
||||||
binding.editText.run {
|
|
||||||
setText(value)
|
|
||||||
setSelection(text?.length ?: 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.range
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.ContextThemeWrapper
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.core.content.withStyledAttributes
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import com.google.android.material.slider.RangeSlider
|
|
||||||
import ru.touchin.roboswag.base_filters.R
|
|
||||||
import ru.touchin.roboswag.base_filters.databinding.RangeChoiceViewBinding
|
|
||||||
import ru.touchin.roboswag.base_filters.range.model.FilterRangeItem
|
|
||||||
import ru.touchin.roboswag.base_filters.range.model.SelectedValues
|
|
||||||
import kotlin.properties.Delegates
|
|
||||||
|
|
||||||
typealias FilterRangeChanged = (FilterRangeItem) -> Unit
|
|
||||||
|
|
||||||
class RangeChoiceView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0,
|
|
||||||
defStyleRes: Int = 0
|
|
||||||
) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
|
|
||||||
|
|
||||||
private val defaultTheme = R.style.Theme_FilterRangeSlider
|
|
||||||
private var binding: RangeChoiceViewBinding by Delegates.notNull()
|
|
||||||
|
|
||||||
private var valueChangedAction: FilterRangeChanged? = null
|
|
||||||
|
|
||||||
private var rangeItem: FilterRangeItem? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
|
|
||||||
binding.fromInput.inputText = value?.selectedValues?.min?.toString()
|
|
||||||
?: value?.start?.toString().orEmpty()
|
|
||||||
binding.toInput.inputText = value?.selectedValues?.max?.toString()
|
|
||||||
?: value?.end?.toString().orEmpty()
|
|
||||||
|
|
||||||
binding.rangeSlider.run {
|
|
||||||
values = listOf(
|
|
||||||
value?.selectedValues?.min?.toFloat() ?: value?.start?.toFloat(),
|
|
||||||
value?.selectedValues?.max?.toFloat() ?: value?.end?.toFloat()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
context.withStyledAttributes(attrs, R.styleable.FilterRangeChoice, defStyleAttr, defStyleRes) {
|
|
||||||
val theme = getResourceId(R.styleable.FilterRangeChoice_filterRange_theme, defaultTheme)
|
|
||||||
val themeContext = ContextThemeWrapper(context, theme)
|
|
||||||
binding = RangeChoiceViewBinding.inflate(LayoutInflater.from(themeContext), this@RangeChoiceView)
|
|
||||||
|
|
||||||
binding.fromInput.setHint(getString(R.styleable.FilterRangeChoice_filterRange_startHint))
|
|
||||||
binding.toInput.setHint(getString(R.styleable.FilterRangeChoice_filterRange_endHint))
|
|
||||||
binding.rangeSliderGuideline.updateLayoutParams<MarginLayoutParams> {
|
|
||||||
topMargin = getDimension(R.styleable.FilterRangeChoice_filterRange_sliderMargin, 0f).toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setupRangeValues(
|
|
||||||
rangeFilterItem: FilterRangeItem,
|
|
||||||
onChangeCallback: FilterRangeChanged
|
|
||||||
) {
|
|
||||||
rangeItem = rangeFilterItem
|
|
||||||
valueChangedAction = onChangeCallback
|
|
||||||
|
|
||||||
with(binding) {
|
|
||||||
addChangeValuesListener()
|
|
||||||
setupRangeSlider(rangeFilterItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetRangeValue() {
|
|
||||||
rangeItem = rangeItem?.resetSelectedValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addChangeValuesListener() {
|
|
||||||
binding.fromInput.addChangeValueListener { rangeItem?.setValue(selectedMinValue = it.toIntOrNull()) }
|
|
||||||
binding.toInput.addChangeValueListener { rangeItem?.setValue(selectedMaxValue = it.toIntOrNull()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupRangeSlider(rangeFilterItem: FilterRangeItem) {
|
|
||||||
with(binding) {
|
|
||||||
rangeSlider.apply {
|
|
||||||
valueFrom = rangeFilterItem.start.toFloat()
|
|
||||||
valueTo = rangeFilterItem.end.toFloat()
|
|
||||||
points = rangeFilterItem.intermediates
|
|
||||||
}
|
|
||||||
|
|
||||||
rangeSlider.addOnChangeListener { _, _, _ ->
|
|
||||||
fromInput.inputText = rangeSlider.values[0].toInt().toString()
|
|
||||||
toInput.inputText = rangeSlider.values[1].toInt().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
rangeSlider.addOnSliderTouchListener(object : RangeSlider.OnSliderTouchListener {
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
override fun onStartTrackingTouch(slider: RangeSlider) = Unit
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
override fun onStopTrackingTouch(slider: RangeSlider) {
|
|
||||||
binding.rangeSlider.apply {
|
|
||||||
when (focusedThumbIndex) {
|
|
||||||
0 -> {
|
|
||||||
rangeItem = rangeItem?.setValue(selectedMinValue = from().toInt())
|
|
||||||
rangeItem?.let { valueChangedAction?.invoke(it) }
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
rangeItem = rangeItem?.setValue(selectedMaxValue = to().toInt())
|
|
||||||
rangeItem?.let { valueChangedAction?.invoke(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun HintInputView.addChangeValueListener(updateValue: (String) -> FilterRangeItem?) {
|
|
||||||
setOnEditorActionListener { view, actionId, _ ->
|
|
||||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
|
||||||
rangeItem = updateValue(view.text.toString().filterNot { it.isWhitespace() })
|
|
||||||
rangeItem?.let { valueChangedAction?.invoke(it) }
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun RangeSlider.from() = values[0].toInt().toString()
|
|
||||||
|
|
||||||
private fun RangeSlider.to() = values[1].toInt().toString()
|
|
||||||
|
|
||||||
@SuppressWarnings("detekt.ComplexMethod")
|
|
||||||
private fun FilterRangeItem.setValue(
|
|
||||||
selectedMaxValue: Int? = selectedValues?.max,
|
|
||||||
selectedMinValue: Int? = selectedValues?.min
|
|
||||||
): FilterRangeItem {
|
|
||||||
val isMaxValueUpdated = selectedMaxValue != selectedValues?.max
|
|
||||||
val isMinValueUpdated = selectedMinValue != selectedValues?.min
|
|
||||||
|
|
||||||
val isMinValueOutOfRange = selectedMinValue != null && isMinValueUpdated && selectedMinValue > (selectedMaxValue ?: end)
|
|
||||||
val isMaxValueOutOfRange = selectedMaxValue != null && isMaxValueUpdated && selectedMaxValue < (selectedMinValue ?: start)
|
|
||||||
|
|
||||||
val updatedValues = when {
|
|
||||||
selectedMaxValue == end && selectedMinValue == start -> null
|
|
||||||
isMinValueOutOfRange -> SelectedValues(
|
|
||||||
max = selectedMaxValue ?: end,
|
|
||||||
min = selectedMaxValue ?: end
|
|
||||||
)
|
|
||||||
isMaxValueOutOfRange -> SelectedValues(
|
|
||||||
max = selectedMinValue ?: start,
|
|
||||||
min = selectedMinValue ?: start
|
|
||||||
)
|
|
||||||
else -> SelectedValues(
|
|
||||||
max = selectedMaxValue?.takeIf { it < end } ?: end,
|
|
||||||
min = selectedMinValue?.takeIf { it > start } ?: start
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copyWithSelectedValue(selectedValues = updatedValues)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun FilterRangeItem.resetSelectedValues() = copyWithSelectedValue(selectedValues = null)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.range.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
open class FilterRangeItem(
|
|
||||||
val id: String,
|
|
||||||
val start: Int,
|
|
||||||
val end: Int,
|
|
||||||
val title: String,
|
|
||||||
val intermediates: List<Int>? = null,
|
|
||||||
val step: Int? = null,
|
|
||||||
val selectedValues: SelectedValues? = null
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
fun isCorrectValues() = end > start
|
|
||||||
|
|
||||||
fun copyWithSelectedValue(selectedValues: SelectedValues?) = FilterRangeItem(
|
|
||||||
id = id,
|
|
||||||
start = start,
|
|
||||||
end = end,
|
|
||||||
title = title,
|
|
||||||
intermediates = intermediates,
|
|
||||||
step = step,
|
|
||||||
selectedValues = selectedValues
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class SelectedValues(
|
|
||||||
val max: Int,
|
|
||||||
val min: Int
|
|
||||||
) : Parcelable
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.select_list_item
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.ContextThemeWrapper
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
import androidx.annotation.StyleRes
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import ru.touchin.roboswag.base_filters.databinding.SelectionItemBinding
|
|
||||||
import ru.touchin.roboswag.base_filters.select_list_item.adapter.BaseSelectionViewHolder
|
|
||||||
import ru.touchin.roboswag.base_filters.select_list_item.adapter.HolderFactoryType
|
|
||||||
import ru.touchin.roboswag.base_filters.select_list_item.adapter.SelectionItemViewHolder
|
|
||||||
import ru.touchin.roboswag.base_filters.select_list_item.adapter.SheetSelectionAdapter
|
|
||||||
import ru.touchin.roboswag.base_filters.select_list_item.model.BaseSelectionItem
|
|
||||||
import ru.touchin.roboswag.base_filters.SelectionType
|
|
||||||
|
|
||||||
private typealias OnSelectedItemListener<ItemType> = (item: ItemType) -> Unit
|
|
||||||
private typealias OnSelectedItemsListener<ItemType> = (items: List<ItemType>) -> Unit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base [ListSelectionView] to use in filters screen for choosing single or multi items in list.
|
|
||||||
*
|
|
||||||
* @param ItemType Type of model's element in list.
|
|
||||||
* It must implement [BaseSelectionItem] abstract class.
|
|
||||||
*
|
|
||||||
* @param HolderType Type of viewHolder in recyclerView.
|
|
||||||
* It must extend [BaseSelectionViewHolder] abstract class.
|
|
||||||
*
|
|
||||||
**/
|
|
||||||
|
|
||||||
class ListSelectionView<ItemType, HolderType> @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : RecyclerView(context, attrs, defStyleAttr)
|
|
||||||
where ItemType : BaseSelectionItem,
|
|
||||||
HolderType : BaseSelectionViewHolder<ItemType> {
|
|
||||||
|
|
||||||
enum class SelectionType { SINGLE_SELECT, MULTI_SELECT }
|
|
||||||
|
|
||||||
constructor(context: Context, @StyleRes themeResId: Int) : this(ContextThemeWrapper(context, themeResId))
|
|
||||||
|
|
||||||
private var mutableItems: List<ItemType> = emptyList()
|
|
||||||
private var selectionType = SelectionType.SINGLE_SELECT
|
|
||||||
|
|
||||||
private var onSelectedItemChanged: OnSelectedItemListener<ItemType>? = null
|
|
||||||
private var onSelectedItemsChanged: OnSelectedItemsListener<ItemType>? = null
|
|
||||||
private var factory: HolderFactoryType<ItemType> = getDefaultFactory()
|
|
||||||
|
|
||||||
init {
|
|
||||||
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
|
||||||
layoutManager = LinearLayoutManager(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDefaultFactory(): HolderFactoryType<ItemType> = { parent, clickListener, selectionType ->
|
|
||||||
SelectionItemViewHolder(
|
|
||||||
binding = SelectionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
|
||||||
onItemSelectAction = clickListener,
|
|
||||||
selectionType = selectionType
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val selectionAdapter by lazy {
|
|
||||||
SheetSelectionAdapter(
|
|
||||||
onItemSelectAction = onItemSelectedListener,
|
|
||||||
selectionType = selectionType,
|
|
||||||
factory = factory
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val onItemSelectedListener: (item: ItemType) -> Unit = { item ->
|
|
||||||
onSelectedItemChanged?.invoke(item)
|
|
||||||
updateAfterSelection(item)
|
|
||||||
onSelectedItemsChanged?.invoke(mutableItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateItems(items: List<ItemType>) {
|
|
||||||
mutableItems = items
|
|
||||||
updateList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateList() {
|
|
||||||
selectionAdapter.submitList(mutableItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateAfterSelection(selectedItem: ItemType) {
|
|
||||||
mutableItems = mutableItems.map { item ->
|
|
||||||
when {
|
|
||||||
item.isItemTheSame(selectedItem) -> selectedItem
|
|
||||||
selectionType == SelectionType.SINGLE_SELECT -> item.copyWithSelection(isSelected = false)
|
|
||||||
else -> item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateList()
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Builder {
|
|
||||||
|
|
||||||
fun setItems(items: List<ItemType>) = apply {
|
|
||||||
mutableItems = items
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> setItems(
|
|
||||||
source: List<T>,
|
|
||||||
mapper: (T) -> ItemType
|
|
||||||
) = setItems(source.map { item -> mapper.invoke(item) })
|
|
||||||
|
|
||||||
fun showInHolder(holderFactory: HolderFactoryType<ItemType>) = apply {
|
|
||||||
factory = holderFactory
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addItemDecoration(itemDecoration: RecyclerView.ItemDecoration) = apply {
|
|
||||||
this@ListSelectionView.addItemDecoration(itemDecoration)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSelectedItemListener(listener: OnSelectedItemListener<ItemType>) = apply {
|
|
||||||
this@ListSelectionView.onSelectedItemChanged = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSelectedItemsListener(listener: OnSelectedItemsListener<ItemType>) = apply {
|
|
||||||
this@ListSelectionView.onSelectedItemsChanged = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withSelectionType(type: SelectionType) = apply {
|
|
||||||
selectionType = type
|
|
||||||
}
|
|
||||||
|
|
||||||
fun build() = this@ListSelectionView.also {
|
|
||||||
it.adapter = selectionAdapter
|
|
||||||
updateList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.select_list_item.adapter
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import ru.touchin.roboswag.base_filters.select_list_item.model.BaseSelectionItem
|
|
||||||
|
|
||||||
abstract class BaseSelectionViewHolder<ItemType : BaseSelectionItem>(val view: View)
|
|
||||||
: RecyclerView.ViewHolder(view) {
|
|
||||||
|
|
||||||
abstract fun bind(item: ItemType)
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.select_list_item.adapter
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import ru.touchin.roboswag.base_filters.databinding.SelectionItemBinding
|
|
||||||
import ru.touchin.roboswag.base_filters.select_list_item.model.BaseSelectionItem
|
|
||||||
import ru.touchin.roboswag.base_filters.SelectionType
|
|
||||||
|
|
||||||
class SelectionItemViewHolder<ItemType : BaseSelectionItem>(
|
|
||||||
private val binding: SelectionItemBinding,
|
|
||||||
private val onItemSelectAction: (ItemType) -> Unit,
|
|
||||||
private val selectionType: SelectionType
|
|
||||||
) : BaseSelectionViewHolder<ItemType>(binding.root) {
|
|
||||||
|
|
||||||
override fun bind(item: ItemType) {
|
|
||||||
binding.itemTitle.text = item.title
|
|
||||||
binding.itemRadiobutton.isChecked = item.isSelected
|
|
||||||
|
|
||||||
setupCheckListener(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupCheckListener(item: ItemType) = with(binding) {
|
|
||||||
val checkListener = View.OnClickListener {
|
|
||||||
itemRadiobutton.isChecked = true
|
|
||||||
onItemSelectAction.invoke(item.copyWithSelection(isSelected = when (selectionType) {
|
|
||||||
SelectionType.SINGLE_SELECT -> true
|
|
||||||
else -> !item.isSelected
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
root.setOnClickListener(checkListener)
|
|
||||||
itemRadiobutton.setOnClickListener(checkListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.select_list_item.adapter
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import ru.touchin.roboswag.base_filters.select_list_item.model.BaseSelectionItem
|
|
||||||
import ru.touchin.roboswag.base_filters.SelectionType
|
|
||||||
import ru.touchin.roboswag.recyclerview_adapters.adapters.DelegationListAdapter
|
|
||||||
|
|
||||||
class SheetSelectionAdapter<ItemType : BaseSelectionItem>(
|
|
||||||
onItemSelectAction: (ItemType) -> Unit,
|
|
||||||
selectionType: SelectionType,
|
|
||||||
factory: HolderFactoryType<ItemType>
|
|
||||||
) : DelegationListAdapter<BaseSelectionItem>(object : DiffUtil.ItemCallback<BaseSelectionItem>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: BaseSelectionItem, newItem: BaseSelectionItem): Boolean =
|
|
||||||
oldItem.isItemTheSame(newItem)
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: BaseSelectionItem, newItem: BaseSelectionItem): Boolean =
|
|
||||||
oldItem.isContentTheSame(newItem)
|
|
||||||
|
|
||||||
}) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
addDelegate(SheetSelectionDelegate(
|
|
||||||
onItemSelectAction = onItemSelectAction,
|
|
||||||
selectionType = selectionType,
|
|
||||||
factory = factory
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.select_list_item.adapter
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import ru.touchin.roboswag.base_filters.select_list_item.model.BaseSelectionItem
|
|
||||||
import ru.touchin.roboswag.base_filters.SelectionType
|
|
||||||
import ru.touchin.roboswag.recyclerview_adapters.adapters.ItemAdapterDelegate
|
|
||||||
|
|
||||||
typealias HolderFactoryType<ItemType> = (ViewGroup, (ItemType) -> Unit, SelectionType) -> BaseSelectionViewHolder<ItemType>
|
|
||||||
|
|
||||||
class SheetSelectionDelegate<ItemType>(
|
|
||||||
private val onItemSelectAction: (ItemType) -> Unit,
|
|
||||||
private val selectionType: SelectionType,
|
|
||||||
private val factory: HolderFactoryType<ItemType>
|
|
||||||
) : ItemAdapterDelegate<BaseSelectionViewHolder<ItemType>, ItemType>()
|
|
||||||
where ItemType : BaseSelectionItem {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup): BaseSelectionViewHolder<ItemType> =
|
|
||||||
factory.invoke(parent, onItemSelectAction, selectionType)
|
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
|
||||||
holder: BaseSelectionViewHolder<ItemType>,
|
|
||||||
item: ItemType,
|
|
||||||
adapterPosition: Int,
|
|
||||||
collectionPosition: Int,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) = holder.bind(item)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.select_list_item.model
|
|
||||||
|
|
||||||
abstract class BaseSelectionItem(
|
|
||||||
open val id: Int,
|
|
||||||
open val title: String,
|
|
||||||
open val isSelected: Boolean
|
|
||||||
) {
|
|
||||||
|
|
||||||
abstract fun isItemTheSame(compareItem: BaseSelectionItem): Boolean
|
|
||||||
|
|
||||||
abstract fun isContentTheSame(compareItem: BaseSelectionItem): Boolean
|
|
||||||
|
|
||||||
abstract fun <ItemType> copyWithSelection(isSelected: Boolean): ItemType
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.select_list_item.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class DefaultSelectionItem(
|
|
||||||
override val id: Int,
|
|
||||||
override val title: String,
|
|
||||||
override val isSelected: Boolean = false
|
|
||||||
) : BaseSelectionItem(id, title, isSelected), Parcelable {
|
|
||||||
|
|
||||||
override fun isItemTheSame(compareItem: BaseSelectionItem): Boolean = when {
|
|
||||||
compareItem is DefaultSelectionItem && id == compareItem.id -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isContentTheSame(compareItem: BaseSelectionItem): Boolean =
|
|
||||||
this == compareItem
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun <ItemType> copyWithSelection(isSelected: Boolean): ItemType =
|
|
||||||
this.copy(isSelected = isSelected) as ItemType
|
|
||||||
}
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.tags
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.LayoutRes
|
|
||||||
import androidx.core.view.children
|
|
||||||
import com.google.android.material.chip.ChipGroup
|
|
||||||
import ru.touchin.roboswag.base_filters.R
|
|
||||||
import ru.touchin.roboswag.base_filters.SelectionType
|
|
||||||
import ru.touchin.roboswag.base_filters.databinding.LayoutMultiLineTagGroupBinding
|
|
||||||
import ru.touchin.roboswag.base_filters.databinding.LayoutSingleLineTagGroupBinding
|
|
||||||
import ru.touchin.roboswag.base_filters.tags.model.FilterItem
|
|
||||||
import ru.touchin.roboswag.base_filters.tags.model.FilterProperty
|
|
||||||
import ru.touchin.roboswag.components.utils.UiUtils
|
|
||||||
import ru.touchin.roboswag.components.utils.px
|
|
||||||
import kotlin.properties.Delegates
|
|
||||||
|
|
||||||
typealias PropertySelectedAction = (FilterProperty) -> Unit
|
|
||||||
typealias FilterMoreAction = (FilterItem) -> Unit
|
|
||||||
|
|
||||||
class TagLayoutView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
private var filterItem: FilterItem by Delegates.notNull()
|
|
||||||
|
|
||||||
private var tagsContainer: ChipGroup by Delegates.notNull()
|
|
||||||
|
|
||||||
private var propertySelectedAction: PropertySelectedAction? = null
|
|
||||||
private var moreValuesAction: FilterMoreAction? = null
|
|
||||||
|
|
||||||
private var selectionType = SelectionType.MULTI_SELECT
|
|
||||||
private var isSingleLine = false
|
|
||||||
private var tagSpacingHorizontalDp: Int = 0
|
|
||||||
private var tagSpacingVerticalDp: Int = 0
|
|
||||||
|
|
||||||
@LayoutRes
|
|
||||||
private var tagLayout: Int = R.layout.layout_default_tag
|
|
||||||
|
|
||||||
private var moreTagText: String = ""
|
|
||||||
private var maxTagCount = Int.MAX_VALUE
|
|
||||||
|
|
||||||
@LayoutRes
|
|
||||||
private var moreTagLayout: Int = tagLayout
|
|
||||||
|
|
||||||
private fun inflateAndGetChipGroup(isSingleLine: Boolean): ChipGroup = when (isSingleLine) {
|
|
||||||
true -> LayoutSingleLineTagGroupBinding.inflate(LayoutInflater.from(context), this, true).tagGroup
|
|
||||||
false -> LayoutMultiLineTagGroupBinding.inflate(LayoutInflater.from(context), this, true).tagGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createTag(property: FilterProperty): TagView {
|
|
||||||
val tagView = UiUtils.inflate(tagLayout, this)
|
|
||||||
require(tagView is TagView) { "Layout for tag must contain TagView as root view" }
|
|
||||||
|
|
||||||
return tagView.apply {
|
|
||||||
text = property.title
|
|
||||||
isChecked = property.isSelected
|
|
||||||
tagId = property.id
|
|
||||||
|
|
||||||
setOnCheckAction { view, isChecked ->
|
|
||||||
when {
|
|
||||||
selectionType == SelectionType.SINGLE_SELECT && isChecked -> clearCheck(property.id)
|
|
||||||
selectionType == SelectionType.MULTI_SELECT && isChecked -> clearExcludedCheck(property)
|
|
||||||
}
|
|
||||||
view.isChecked = isChecked
|
|
||||||
propertySelectedAction?.invoke(property.copyWithSelected(isSelected = isChecked))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createMoreTag(filter: FilterItem): View {
|
|
||||||
val moreTag = UiUtils.inflate(moreTagLayout, this)
|
|
||||||
require(moreTag is TextView) { "Layout for more tag must contain TextView as root view" }
|
|
||||||
|
|
||||||
return moreTag.apply {
|
|
||||||
text = moreTagText
|
|
||||||
setOnClickListener { moreValuesAction?.invoke(filter) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearCheck(selectedId: Int) {
|
|
||||||
tagsContainer.children.forEach { tagView ->
|
|
||||||
if (tagView is TagView && tagView.tagId != selectedId) {
|
|
||||||
tagView.isChecked = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearExcludedCheck(property: FilterProperty) {
|
|
||||||
val excludingIds = property.excludes.map { it.id }
|
|
||||||
|
|
||||||
tagsContainer.children.forEach { tagView ->
|
|
||||||
if (tagView is TagView && tagView.tagId in excludingIds) {
|
|
||||||
tagView.isChecked = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Builder(private val filterItem: FilterItem) {
|
|
||||||
|
|
||||||
fun onMoreValuesAction(action: FilterMoreAction) = apply {
|
|
||||||
moreValuesAction = action
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPropertySelectedAction(action: PropertySelectedAction) = apply {
|
|
||||||
propertySelectedAction = action
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setMaxTagCount(count: Int) = apply {
|
|
||||||
maxTagCount = count
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSpacingHorizontal(horizontalSpacingDp: Int) = apply {
|
|
||||||
tagSpacingHorizontalDp = horizontalSpacingDp
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSpacingVertical(verticalSpacingDp: Int) = apply {
|
|
||||||
tagSpacingVerticalDp = verticalSpacingDp
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSpacing(value: Int) = apply {
|
|
||||||
tagSpacingHorizontalDp = value
|
|
||||||
tagSpacingVerticalDp = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectionType(type: SelectionType) = apply {
|
|
||||||
selectionType = type
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isSingleLine(value: Boolean) = apply {
|
|
||||||
isSingleLine = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTagLayout(@LayoutRes layoutId: Int) = apply {
|
|
||||||
tagLayout = layoutId
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setMoreTagLayout(@LayoutRes layoutId: Int, text: String) = apply {
|
|
||||||
moreTagLayout = layoutId
|
|
||||||
moreTagText = text
|
|
||||||
}
|
|
||||||
|
|
||||||
fun build() {
|
|
||||||
this@TagLayoutView.filterItem = filterItem
|
|
||||||
tagsContainer = inflateAndGetChipGroup(isSingleLine)
|
|
||||||
|
|
||||||
with(tagsContainer) {
|
|
||||||
removeAllViews()
|
|
||||||
|
|
||||||
this.isSingleLine = isSingleLine
|
|
||||||
|
|
||||||
chipSpacingHorizontal = tagSpacingHorizontalDp.px
|
|
||||||
chipSpacingVertical = tagSpacingVerticalDp.px
|
|
||||||
|
|
||||||
filterItem.properties.take(maxTagCount).forEach { property ->
|
|
||||||
addView(createTag(property))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterItem.properties.size > maxTagCount) {
|
|
||||||
addView(createMoreTag(filterItem))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.tags
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import androidx.appcompat.widget.AppCompatCheckBox
|
|
||||||
|
|
||||||
class TagView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
): AppCompatCheckBox(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
var tagId: Int? = null
|
|
||||||
|
|
||||||
private var action: (( view: TagView, isChecked: Boolean) -> Unit)? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
setOnClickListener {
|
|
||||||
action?.invoke(this, isChecked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setOnCheckAction(action: (view: TagView, isChecked: Boolean) -> Unit) {
|
|
||||||
this.action = action
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package ru.touchin.roboswag.base_filters.tags.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
open class FilterItem(
|
|
||||||
val id: Int,
|
|
||||||
val title: String,
|
|
||||||
val properties: List<FilterProperty>
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
open class FilterProperty(
|
|
||||||
val id: Int,
|
|
||||||
val title: String,
|
|
||||||
val excludes: List<PropertyExcludingValue>,
|
|
||||||
val isSelected: Boolean = false
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
open fun copyWithSelected(isSelected: Boolean) = FilterProperty(
|
|
||||||
id = id,
|
|
||||||
title = title,
|
|
||||||
excludes = excludes,
|
|
||||||
isSelected = isSelected
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
open class PropertyExcludingValue(
|
|
||||||
val id: Int
|
|
||||||
) : Parcelable
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_checked="true" android:color="#12B052" />
|
|
||||||
<item android:state_checked="false" android:color="#1E1E1E" />
|
|
||||||
</selector>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<corners android:radius="6dp" />
|
|
||||||
<solid android:color="@android:color/darker_gray"/>
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<corners android:radius="6dp" />
|
|
||||||
<solid android:color="@android:color/white"/>
|
|
||||||
<stroke
|
|
||||||
android:width="1dp"
|
|
||||||
android:color="#12B052" />
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_checked="true" android:drawable="@drawable/background_chip_checked" />
|
|
||||||
<item android:state_checked="false" android:drawable="@drawable/background_chip" />
|
|
||||||
</selector>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<corners android:radius="8dp" />
|
|
||||||
<solid android:color="@android:color/white"/>
|
|
||||||
<stroke
|
|
||||||
android:width="1dp"
|
|
||||||
android:color="@color/slider_point_inactive" />
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle" >
|
|
||||||
<size
|
|
||||||
android:width="1dp" />
|
|
||||||
<solid
|
|
||||||
android:color="@color/slider_thumb" />
|
|
||||||
<padding
|
|
||||||
android:top="-2sp"
|
|
||||||
android:bottom="-2sp" />
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ru.touchin.roboswag.base_filters.tags.TagView
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/background_chip_choice"
|
|
||||||
android:button="@null"
|
|
||||||
android:paddingHorizontal="16dp"
|
|
||||||
android:paddingVertical="6dp"
|
|
||||||
android:textColor="@color/color_chip_choice"
|
|
||||||
tools:text="String" />
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<com.google.android.material.chip.ChipGroup
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:id="@+id/tag_group"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:chipSpacing="8dp"
|
|
||||||
app:selectionRequired="false"
|
|
||||||
app:singleLine="false"
|
|
||||||
app:singleSelection="true" />
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<HorizontalScrollView
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:id="@+id/line_tag_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:scrollbars="none">
|
|
||||||
|
|
||||||
<com.google.android.material.chip.ChipGroup
|
|
||||||
android:id="@+id/tag_group"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:selectionRequired="true" />
|
|
||||||
|
|
||||||
</HorizontalScrollView>
|
|
||||||
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<merge
|
|
||||||
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:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Guideline
|
|
||||||
android:id="@+id/center_guideline"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
app:layout_constraintGuide_percent="0.5" />
|
|
||||||
|
|
||||||
<ru.touchin.roboswag.base_filters.range.HintInputView
|
|
||||||
android:id="@+id/from_input"
|
|
||||||
style="?attr/filterRange_hintViewStyle"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="42dp"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/center_guideline"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
|
||||||
|
|
||||||
<ru.touchin.roboswag.base_filters.range.HintInputView
|
|
||||||
android:id="@+id/to_input"
|
|
||||||
style="?attr/filterRange_hintViewStyle"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="42dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/center_guideline"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
|
||||||
|
|
||||||
<ru.touchin.roboswag.base_filters.range.FilterRangeSlider
|
|
||||||
android:id="@+id/range_slider"
|
|
||||||
style="?attr/filterRange_sliderStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/range_slider_guideline"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/range_slider_guideline"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"/>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/range_slider_guideline"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/from_input"/>
|
|
||||||
|
|
||||||
</merge>
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<?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:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/item_title"
|
|
||||||
style="?attr/sheetSelection_itemStyle"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/item_radiobutton"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
tools:text="Заголовок" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatRadioButton
|
|
||||||
android:id="@+id/item_radiobutton"
|
|
||||||
style="?attr/sheetSelection_radioStyle"
|
|
||||||
android:layout_width="22dp"
|
|
||||||
android:layout_height="22dp"
|
|
||||||
android:checked="true"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<merge
|
|
||||||
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:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/edit_text"
|
|
||||||
style="?attr/filterRange_valueEditTextStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/start_hint"
|
|
||||||
style="?attr/filterRange_hintTextStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/input_text"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/input_text" />
|
|
||||||
|
|
||||||
</merge>
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<declare-styleable name="FilterSelection">
|
|
||||||
<attr name="sheetSelection_itemStyle" format="reference" />
|
|
||||||
<attr name="sheetSelection_radioStyle" format="reference" />
|
|
||||||
</declare-styleable>
|
|
||||||
|
|
||||||
<declare-styleable name="FilterRangeSlider">
|
|
||||||
<attr name="filterRange_activeTickColor" format="color"/>
|
|
||||||
<attr name="filterRange_inactiveTickColor" format="color"/>
|
|
||||||
<attr name="filterRange_stepValueMarginTop" format="dimension"/>
|
|
||||||
<attr name="filterRange_sliderPointSize" format="dimension"/>
|
|
||||||
<attr name="filterRange_stepTextAppearance" format="reference"/>
|
|
||||||
<attr name="filterRange_pointShape" format="enum">
|
|
||||||
<enum name="circle" value="0" />
|
|
||||||
<enum name="cut" value="1" />
|
|
||||||
</attr>
|
|
||||||
</declare-styleable>
|
|
||||||
|
|
||||||
<declare-styleable name="FilterRangeChoice">
|
|
||||||
<attr name="filterRange_sliderStyle" format="reference" />
|
|
||||||
<attr name="filterRange_hintViewStyle" format="reference" />
|
|
||||||
<attr name="filterRange_editTextStyle" format="reference" />
|
|
||||||
<attr name="filterRange_sliderMargin" format="dimension" />
|
|
||||||
<attr name="filterRange_startHint" format="reference" />
|
|
||||||
<attr name="filterRange_endHint" format="reference" />
|
|
||||||
<attr name="filterRange_theme" format="reference" />
|
|
||||||
</declare-styleable>
|
|
||||||
|
|
||||||
<declare-styleable name="FilterEditTextWithHint">
|
|
||||||
<attr name="filterRange_hintTextStyle" format="reference" />
|
|
||||||
<attr name="filterRange_valueEditTextStyle" format="reference" />
|
|
||||||
</declare-styleable>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="slider_point_inactive">#B9B9B9</color>
|
|
||||||
<color name="slider_point_active">#E35100</color>
|
|
||||||
<color name="slider_track_active_part">#EE9766</color>
|
|
||||||
<color name="slider_track_inactive_part">#E7E7E7</color>
|
|
||||||
<color name="slider_thumb">#E35100</color>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="start_hint">от</string>
|
|
||||||
<string name="end_hint">до</string>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<style name="Theme.FilterSelection" parent="@style/Theme.MaterialComponents.Light.BottomSheetDialog">
|
|
||||||
<item name="sheetSelection_itemStyle">@style/Widget.FilterSelection.Item</item>
|
|
||||||
<item name="sheetSelection_radioStyle">@style/Widget.FilterSelection.Radio</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Widget.FilterSelection.Item" parent="@style/Widget.MaterialComponents.TextView">
|
|
||||||
<item name="android:layout_margin">16dp</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Widget.FilterSelection.Radio" parent="@style/Widget.MaterialComponents.CompoundButton.RadioButton">
|
|
||||||
<item name="android:layout_marginTop">16dp</item>
|
|
||||||
<item name="android:layout_marginEnd">16dp</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Widget.FilterRangeSlider.Default" parent="@style/Widget.MaterialComponents.Slider">
|
|
||||||
<item name="trackColorActive">@color/slider_track_active_part</item>
|
|
||||||
<item name="trackColorInactive">@color/slider_track_inactive_part</item>
|
|
||||||
<item name="trackHeight">3dp</item>
|
|
||||||
<item name="thumbElevation">0dp</item>
|
|
||||||
<item name="thumbColor">@color/slider_thumb</item>
|
|
||||||
<item name="labelBehavior">gone</item>
|
|
||||||
<item name="haloRadius">0dp</item>
|
|
||||||
<item name="filterRange_activeTickColor">@color/slider_point_active</item>
|
|
||||||
<item name="filterRange_inactiveTickColor">@color/slider_point_inactive</item>
|
|
||||||
<item name="filterRange_stepValueMarginTop">12dp</item>
|
|
||||||
<item name="filterRange_sliderPointSize">5dp</item>
|
|
||||||
<item name="filterRange_pointShape">circle</item>
|
|
||||||
<item name="android:layout_marginStart">11dp</item>
|
|
||||||
<item name="android:layout_marginEnd">11dp</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Theme.FilterRangeSlider" parent="@style/Theme.MaterialComponents.Light">
|
|
||||||
<item name="filterRange_sliderStyle">@style/Widget.FilterRangeSlider.Default</item>
|
|
||||||
<item name="filterRange_hintViewStyle">@style/Widget.HintInputView</item>
|
|
||||||
<item name="filterRange_hintTextStyle">@style/Widget.HintInputView.HintText</item>
|
|
||||||
<item name="filterRange_valueEditTextStyle">@style/Widget.HintInputView.EditText</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="FilterRangeChoice">
|
|
||||||
<item name="filterRange_sliderMargin">23dp</item>
|
|
||||||
<item name="filterRange_startHint">@string/start_hint</item>
|
|
||||||
<item name="filterRange_endHint">@string/end_hint</item>
|
|
||||||
<item name="filterRange_theme">@style/Theme.FilterRangeSlider</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Widget.HintInputView" parent="@style/Widget.MaterialComponents.TextView">
|
|
||||||
<item name="android:layout_marginEnd">5dp</item>
|
|
||||||
<item name="android:layout_marginTop">12dp</item>
|
|
||||||
<item name="android:layout_marginStart">16dp</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Widget.HintInputView.HintText" parent="@style/Widget.MaterialComponents.TextView">
|
|
||||||
<item name="android:layout_marginStart">12dp</item>
|
|
||||||
<item name="android:elevation">1dp</item>
|
|
||||||
<item name="android:gravity">center_vertical</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Widget.HintInputView.EditText" parent="@style/Widget.AppCompat.EditText">
|
|
||||||
<item name="android:paddingStart">34dp</item>
|
|
||||||
<item name="android:inputType">number</item>
|
|
||||||
<item name="android:imeOptions">actionDone</item>
|
|
||||||
<item name="android:background">@drawable/background_hint_input</item>
|
|
||||||
<item name="android:textCursorDrawable">@drawable/cursor_background_text_input_view</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
|
|
@ -18,10 +18,6 @@ abstract class AbstractMapManager<TMapView : View, TMap : Any, TLocation : Any>(
|
||||||
|
|
||||||
abstract fun getCameraTilt(): Float
|
abstract fun getCameraTilt(): Float
|
||||||
|
|
||||||
abstract fun getDefaultDuration(): Float
|
|
||||||
|
|
||||||
abstract fun getDefaultZoomStep(): Int
|
|
||||||
|
|
||||||
abstract fun moveCamera(
|
abstract fun moveCamera(
|
||||||
target: TLocation,
|
target: TLocation,
|
||||||
zoom: Float = getCameraZoom(),
|
zoom: Float = getCameraZoom(),
|
||||||
|
|
@ -33,17 +29,12 @@ abstract class AbstractMapManager<TMapView : View, TMap : Any, TLocation : Any>(
|
||||||
target: TLocation,
|
target: TLocation,
|
||||||
zoom: Float = getCameraZoom(),
|
zoom: Float = getCameraZoom(),
|
||||||
azimuth: Float = getCameraAzimuth(),
|
azimuth: Float = getCameraAzimuth(),
|
||||||
tilt: Float = getCameraTilt(),
|
tilt: Float = getCameraTilt()
|
||||||
animationDuration: Float = getDefaultDuration()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
abstract fun smoothMoveCamera(targets: List<TLocation>, padding: Int = 0, animationDuration: Float = getDefaultDuration())
|
abstract fun smoothMoveCamera(targets: List<TLocation>, padding: Int = 0)
|
||||||
|
|
||||||
abstract fun smoothMoveCamera(targets: List<TLocation>, width: Int, height: Int, padding: Int, animationDuration: Float = getDefaultDuration())
|
abstract fun smoothMoveCamera(targets: List<TLocation>, width: Int, height: Int, padding: Int)
|
||||||
|
|
||||||
abstract fun increaseZoom(target: TLocation, zoomIncreaseValue: Int = getDefaultZoomStep())
|
|
||||||
|
|
||||||
abstract fun decreaseZoom(target: TLocation, zoomDecreaseValue: Int = getDefaultZoomStep())
|
|
||||||
|
|
||||||
abstract fun setMapAllGesturesEnabled(enabled: Boolean)
|
abstract fun setMapAllGesturesEnabled(enabled: Boolean)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package ru.touchin.basemap
|
|
||||||
|
|
||||||
interface BaseIconGenerator<TPoint, TCluster, TViewIcon> {
|
|
||||||
|
|
||||||
fun getClusterIcon(cluster: TCluster): TViewIcon?
|
|
||||||
|
|
||||||
fun getClusterItemIcon(clusterItem: TPoint): TViewIcon?
|
|
||||||
|
|
||||||
fun getClusterItemView(clusterItem: TPoint): TViewIcon?
|
|
||||||
|
|
||||||
fun getClusterView(cluster: TCluster): TViewIcon?
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package ru.touchin.basemap
|
|
||||||
|
|
||||||
interface BaseMapItemRenderer<TPoint, TCluster, TViewIcon> {
|
|
||||||
|
|
||||||
var iconGenerator: BaseIconGenerator<TPoint, TCluster, TViewIcon>
|
|
||||||
|
|
||||||
fun getClusterItemIcon(item: TPoint): TViewIcon? = iconGenerator.getClusterItemView(item)
|
|
||||||
|
|
||||||
fun getClusterIcon(cluster: TCluster): TViewIcon? = iconGenerator.getClusterView(cluster)
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package ru.touchin.basemap
|
|
||||||
|
|
||||||
import android.util.SparseArray
|
|
||||||
|
|
||||||
inline fun <K, V> MutableMap<K, V>.getOrPutIfNotNull(key: K, defaultValue: () -> V?): V? =
|
|
||||||
get(key) ?: defaultValue()?.also { value ->
|
|
||||||
put(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <V> SparseArray<V>.getOrPutIfNotNull(key: Int, defaultValue: () -> V?): V? =
|
|
||||||
get(key) ?: defaultValue()?.also { value ->
|
|
||||||
put(key, value)
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="22dp"
|
|
||||||
android:height="22dp"
|
|
||||||
android:viewportWidth="22"
|
|
||||||
android:viewportHeight="22">
|
|
||||||
<path
|
|
||||||
android:pathData="M11,11m-11,0a11,11 0,1 1,22 0a11,11 0,1 1,-22 0"
|
|
||||||
android:fillColor="#FF5100"/>
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
/build
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
apply from: "../android-configs/lib-config.gradle"
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation project(":navigation-base")
|
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx'
|
|
||||||
implementation 'com.google.android.material:material'
|
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx") {
|
|
||||||
version {
|
|
||||||
require '1.9.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
implementation("com.google.android.material:material") {
|
|
||||||
version {
|
|
||||||
require '1.4.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
# BottomSheet Utils
|
|
||||||
|
|
||||||
- `BaseBottomSheet` - класс, содержащий парамерты `BottomSheetOptions`
|
|
||||||
|
|
||||||
- `DefaultBottomSheet` - класс с классическим хедером и скруглением, в котором нужно переопределить `createContentView()`
|
|
||||||
|
|
||||||
## BottomSheetOptions
|
|
||||||
- `styleId` - xml-стиль, в котором можно задать скругление
|
|
||||||
- `canDismiss` - может ли модалка быть срыта по тапу/свайпу/backButton
|
|
||||||
- `canTouchOutside` - возможность передавать жесты под модалкой
|
|
||||||
- `isSkipCollapsed` - убирает промежуточное состояние модалки
|
|
||||||
- `isFullscreen` - модалка откроется на весь экран, даже при маленьком контенте
|
|
||||||
- `isShiftedWithKeyboard` - модалка будет полностью подниматься при открытии клавиатуры
|
|
||||||
- `defaultDimAmount` - константное затемнение
|
|
||||||
- `animatedMaxDimAmount` - максимальное затемнение, при этом будет анимироваться в зависимости от offset
|
|
||||||
- `fadeAnimationOptions` - позволяет настроить fade анимацию при изменении высоты
|
|
||||||
- `heightStatesOptions` - позволяет задать 3 состояния высоты модалки
|
|
||||||
|
|
||||||
## ContentFadeAnimationOptions
|
|
||||||
- `foregroundRes` - drawableId, который будет показыватся сверху во время анимации
|
|
||||||
- `duration` - длительность fade анимации
|
|
||||||
- `minAlpha` - минимальная прозрачность во время анимации
|
|
||||||
|
|
||||||
## HeightStatesOptions
|
|
||||||
- `collapsedHeightPx` - высота минимального состояния
|
|
||||||
- `halfExpandedHalfPx` - высота промежуточного состояния
|
|
||||||
- `canTouchOutsideWhenCollapsed` - могут ли жесты передаватья под модалку в минимальном состоянии
|
|
||||||
|
|
||||||
Тестовый проект: https://github.com/duwna/BottomSheets
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<manifest package="ru.touchin.roboswag.bottomsheet" />
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
package ru.touchin.roboswag.bottomsheet
|
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
abstract class BaseBottomSheet : BottomSheetDialogFragment() {
|
|
||||||
|
|
||||||
protected abstract val layoutId: Int
|
|
||||||
|
|
||||||
protected open val bottomSheetOptions = BottomSheetOptions()
|
|
||||||
|
|
||||||
protected val decorView: View
|
|
||||||
get() = checkNotNull(dialog?.window?.decorView)
|
|
||||||
|
|
||||||
protected val bottomSheetView: FrameLayout
|
|
||||||
get() = decorView.findViewById(com.google.android.material.R.id.design_bottom_sheet)
|
|
||||||
|
|
||||||
protected val touchOutsideView: View
|
|
||||||
get() = decorView.findViewById(com.google.android.material.R.id.touch_outside)
|
|
||||||
|
|
||||||
protected val behavior: BottomSheetBehavior<FrameLayout>
|
|
||||||
get() = BottomSheetBehavior.from(bottomSheetView)
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
|
|
||||||
inflater.inflate(layoutId, container)
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
bottomSheetOptions.styleId?.let { setStyle(DialogFragment.STYLE_NORMAL, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
bottomSheetOptions.defaultDimAmount?.let { dialog?.window?.setDimAmount(it) }
|
|
||||||
bottomSheetOptions.animatedMaxDimAmount?.let { setupDimAmountChanges(it) }
|
|
||||||
|
|
||||||
if (bottomSheetOptions.isShiftedWithKeyboard) setupShiftWithKeyboard()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
isCancelable = bottomSheetOptions.canDismiss
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
if (bottomSheetOptions.isSkipCollapsed) {
|
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
behavior.skipCollapsed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bottomSheetOptions.isFullscreen) {
|
|
||||||
bottomSheetView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bottomSheetOptions.canTouchOutside) {
|
|
||||||
setupTouchOutside()
|
|
||||||
}
|
|
||||||
|
|
||||||
bottomSheetOptions.fadeAnimationOptions?.let {
|
|
||||||
setupFadeAnimationOnHeightChanges(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
bottomSheetOptions.heightStatesOptions?.let {
|
|
||||||
setupHeightOptions(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupDimAmountChanges(maxDimAmount: Float) {
|
|
||||||
behavior.peekHeight = 0
|
|
||||||
dialog?.window?.setDimAmount(maxDimAmount)
|
|
||||||
|
|
||||||
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
|
||||||
override fun onStateChanged(bottomSheet: View, newState: Int) = Unit
|
|
||||||
|
|
||||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
|
||||||
dialog?.window?.setDimAmount(abs(slideOffset) * maxDimAmount)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupFadeAnimationOnHeightChanges(options: ContentFadeAnimationOptions) {
|
|
||||||
val foreground = checkNotNull(
|
|
||||||
ContextCompat.getDrawable(requireContext(), options.foregroundRes)
|
|
||||||
).apply {
|
|
||||||
alpha = 0
|
|
||||||
bottomSheetView.foreground = this
|
|
||||||
}
|
|
||||||
|
|
||||||
bottomSheetView.addOnLayoutChangeListener { _, _, top, _, _, _, oldTop, _, _ ->
|
|
||||||
if (top != oldTop) showFadeAnimation(foreground, options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showFadeAnimation(foreground: Drawable, options: ContentFadeAnimationOptions) {
|
|
||||||
val maxAlpha = 255
|
|
||||||
foreground.alpha = maxAlpha
|
|
||||||
bottomSheetView.alpha = options.minAlpha
|
|
||||||
|
|
||||||
ValueAnimator.ofInt(maxAlpha, 0).apply {
|
|
||||||
duration = options.duration
|
|
||||||
addUpdateListener {
|
|
||||||
val value = it.animatedValue as Int
|
|
||||||
foreground.alpha = value
|
|
||||||
bottomSheetView.alpha = (1 - value.toFloat() / maxAlpha).coerceAtLeast(options.minAlpha)
|
|
||||||
}
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupHeightOptions(options: HeightStatesOptions) = with(behavior) {
|
|
||||||
isFitToContents = false
|
|
||||||
peekHeight = options.collapsedHeightPx
|
|
||||||
halfExpandedRatio = options.halfExpandedHalfPx / Resources.getSystem().displayMetrics.heightPixels.toFloat()
|
|
||||||
state = BottomSheetBehavior.STATE_COLLAPSED
|
|
||||||
if (options.canTouchOutsideWhenCollapsed) setupTouchOutsideWhenCollapsed()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupTouchOutsideWhenCollapsed() {
|
|
||||||
setupTouchOutside()
|
|
||||||
|
|
||||||
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
|
||||||
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
|
|
||||||
|
|
||||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
|
||||||
when (newState) {
|
|
||||||
BottomSheetBehavior.STATE_COLLAPSED -> setupTouchOutside()
|
|
||||||
else -> touchOutsideView.setOnTouchListener(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun setupShiftWithKeyboard() {
|
|
||||||
dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
private fun setupTouchOutside() {
|
|
||||||
touchOutsideView.setOnTouchListener { _, event ->
|
|
||||||
requireActivity().dispatchTouchEvent(event)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package ru.touchin.roboswag.bottomsheet
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.annotation.StyleRes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See explanation in readme
|
|
||||||
* */
|
|
||||||
data class BottomSheetOptions(
|
|
||||||
@StyleRes val styleId: Int? = null,
|
|
||||||
val canDismiss: Boolean = true,
|
|
||||||
val canTouchOutside: Boolean = false,
|
|
||||||
val isSkipCollapsed: Boolean = true,
|
|
||||||
val isFullscreen: Boolean = false,
|
|
||||||
val isShiftedWithKeyboard: Boolean = false,
|
|
||||||
val defaultDimAmount: Float? = null,
|
|
||||||
val animatedMaxDimAmount: Float? = null,
|
|
||||||
val fadeAnimationOptions: ContentFadeAnimationOptions? = null,
|
|
||||||
val heightStatesOptions: HeightStatesOptions? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ContentFadeAnimationOptions(
|
|
||||||
@DrawableRes val foregroundRes: Int,
|
|
||||||
val duration: Long,
|
|
||||||
val minAlpha: Float
|
|
||||||
)
|
|
||||||
|
|
||||||
data class HeightStatesOptions(
|
|
||||||
val collapsedHeightPx: Int,
|
|
||||||
val halfExpandedHalfPx: Int,
|
|
||||||
val canTouchOutsideWhenCollapsed: Boolean = true
|
|
||||||
)
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package ru.touchin.roboswag.bottomsheet
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import ru.touchin.roboswag.bottomsheet.databinding.DefaultBottomSheetBinding
|
|
||||||
import ru.touchin.roboswag.navigation_base.fragments.viewBinding
|
|
||||||
|
|
||||||
abstract class DefaultBottomSheet : BaseBottomSheet() {
|
|
||||||
|
|
||||||
abstract fun createContentView(inflater: LayoutInflater): View
|
|
||||||
|
|
||||||
final override val layoutId = R.layout.default_bottom_sheet
|
|
||||||
|
|
||||||
override val bottomSheetOptions = BottomSheetOptions(
|
|
||||||
styleId = R.style.RoundedBottomSheetStyle,
|
|
||||||
fadeAnimationOptions = ContentFadeAnimationOptions(
|
|
||||||
foregroundRes = R.drawable.bottom_sheet_background_rounded_16,
|
|
||||||
duration = 150,
|
|
||||||
minAlpha = 0.5f
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
protected val rootBinding by viewBinding(DefaultBottomSheetBinding::bind)
|
|
||||||
|
|
||||||
protected val contentView: View get() = rootBinding.linearRoot.getChildAt(1)
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
|
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
|
||||||
.also {
|
|
||||||
DefaultBottomSheetBinding.bind(checkNotNull(it))
|
|
||||||
.linearRoot.addView(createContentView(inflater))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
rootBinding.closeText.setOnClickListener { dismiss() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<shape
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
|
|
||||||
<corners
|
|
||||||
android:topLeftRadius="16dp"
|
|
||||||
android:topRightRadius="16dp" />
|
|
||||||
|
|
||||||
<solid android:color="@android:color/white" />
|
|
||||||
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/linear_root"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/top_icon"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="top|center"
|
|
||||||
tools:ignore="ContentDescription"
|
|
||||||
tools:src="@android:mipmap/sym_def_app_icon" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/close_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="top|left"
|
|
||||||
tools:text="Закрыть" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<style name="RoundedBottomSheetStyle" parent="Theme.Design.Light.BottomSheetDialog">
|
|
||||||
<item name="android:windowIsFloating">false</item>
|
|
||||||
<item name="bottomSheetStyle">@style/RoundedBackground</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="RoundedBackground" parent="Widget.Design.BottomSheet.Modal">
|
|
||||||
<item name="android:background">@drawable/bottom_sheet_background_rounded_16</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
|
|
@ -17,7 +17,7 @@ allprojects {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
maven {
|
maven {
|
||||||
url "https://maven.dev.touchin.ru/"
|
url "https://dl.bintray.com/touchin/touchin-tools"
|
||||||
metadataSources {
|
metadataSources {
|
||||||
artifact()
|
artifact()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
/build
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
apply from: "../android-configs/lib-config.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
def coroutinesVersion = '1.6.4'
|
|
||||||
def junitVersion = '4.13.2'
|
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
|
|
||||||
testImplementation("junit:junit")
|
|
||||||
|
|
||||||
constraints {
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") {
|
|
||||||
version {
|
|
||||||
require(coroutinesVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
testImplementation("junit:junit") {
|
|
||||||
version {
|
|
||||||
require(junitVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<manifest package="ru.touchin.roboswag.core.cart_utils" />
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package ru.touchin.roboswag.cart_utils.models
|
|
||||||
|
|
||||||
abstract class CartModel<TProductModel : ProductModel> {
|
|
||||||
|
|
||||||
abstract val products: List<TProductModel>
|
|
||||||
|
|
||||||
open val promocodeList: List<PromocodeModel> = emptyList()
|
|
||||||
|
|
||||||
open val availableBonuses: Int = 0
|
|
||||||
open val usedBonuses: Int = 0
|
|
||||||
|
|
||||||
val availableProducts: List<TProductModel>
|
|
||||||
get() = products.filter { it.isAvailable && !it.isDeleted }
|
|
||||||
|
|
||||||
val totalPrice: Int
|
|
||||||
get() = availableProducts.sumOf { it.countInCart * it.price }
|
|
||||||
|
|
||||||
val totalBonuses: Int
|
|
||||||
get() = availableProducts.sumOf { it.countInCart * (it.bonuses ?: 0) }
|
|
||||||
|
|
||||||
fun getPriceWithPromocode(): Int = promocodeList
|
|
||||||
.sortedByDescending { it.discount is PromocodeDiscount.ByPercent }
|
|
||||||
.fold(initial = totalPrice) { price, promo ->
|
|
||||||
promo.discount.applyTo(price)
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun <TCart> copyWith(
|
|
||||||
products: List<TProductModel> = this.products,
|
|
||||||
promocodeList: List<PromocodeModel> = this.promocodeList,
|
|
||||||
usedBonuses: Int = this.usedBonuses
|
|
||||||
): TCart
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <TCart> asCart() = this as TCart
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package ru.touchin.roboswag.cart_utils.models
|
|
||||||
|
|
||||||
abstract class ProductModel {
|
|
||||||
abstract val id: Int
|
|
||||||
abstract val countInCart: Int
|
|
||||||
abstract val price: Int
|
|
||||||
abstract val isAvailable: Boolean
|
|
||||||
abstract val isDeleted: Boolean
|
|
||||||
|
|
||||||
open val bonuses: Int? = null
|
|
||||||
|
|
||||||
open val variants: List<ProductModel> = emptyList()
|
|
||||||
open val selectedVariantId: Int? = null
|
|
||||||
|
|
||||||
val selectedVariant get() = variants.find { it.id == selectedVariantId }
|
|
||||||
|
|
||||||
abstract fun <TProduct> copyWith(
|
|
||||||
countInCart: Int = this.countInCart,
|
|
||||||
isDeleted: Boolean = this.isDeleted,
|
|
||||||
selectedVariantId: Int? = this.selectedVariantId
|
|
||||||
): TProduct
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <TProduct> asProduct(): TProduct = this as TProduct
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package ru.touchin.roboswag.cart_utils.models
|
|
||||||
|
|
||||||
open class PromocodeModel(
|
|
||||||
val code: String,
|
|
||||||
val discount: PromocodeDiscount,
|
|
||||||
)
|
|
||||||
|
|
||||||
abstract class PromocodeDiscount {
|
|
||||||
|
|
||||||
abstract fun applyTo(totalPrice: Int): Int
|
|
||||||
|
|
||||||
class ByValue(private val value: Int) : PromocodeDiscount() {
|
|
||||||
override fun applyTo(totalPrice: Int): Int = totalPrice - value
|
|
||||||
}
|
|
||||||
|
|
||||||
class ByPercent(private val percent: Int) : PromocodeDiscount() {
|
|
||||||
override fun applyTo(totalPrice: Int): Int = totalPrice - totalPrice * percent / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package ru.touchin.roboswag.cart_utils.repositories
|
|
||||||
|
|
||||||
import ru.touchin.roboswag.cart_utils.models.CartModel
|
|
||||||
import ru.touchin.roboswag.cart_utils.models.ProductModel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for server-side cart repository where each request should return updated [CartModel]
|
|
||||||
*/
|
|
||||||
interface IRemoteCartRepository<TCart : CartModel<TProduct>, TProduct : ProductModel> {
|
|
||||||
|
|
||||||
suspend fun getCart(): TCart
|
|
||||||
|
|
||||||
suspend fun addProduct(product: TProduct): TCart
|
|
||||||
|
|
||||||
suspend fun removeProduct(id: Int): TCart
|
|
||||||
|
|
||||||
suspend fun editProductCount(id: Int, count: Int): TCart
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
package ru.touchin.roboswag.cart_utils.repositories
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import ru.touchin.roboswag.cart_utils.models.CartModel
|
|
||||||
import ru.touchin.roboswag.cart_utils.models.ProductModel
|
|
||||||
import ru.touchin.roboswag.cart_utils.models.PromocodeModel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that contains StateFlow of current [CartModel] which can be subscribed in ViewModels
|
|
||||||
*/
|
|
||||||
class LocalCartRepository<TCart : CartModel<TProduct>, TProduct : ProductModel>(
|
|
||||||
initialCart: TCart
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val _currentCart = MutableStateFlow(initialCart)
|
|
||||||
val currentCart = _currentCart.asStateFlow()
|
|
||||||
|
|
||||||
fun updateCart(cart: TCart) {
|
|
||||||
_currentCart.value = cart
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addProduct(product: TProduct) {
|
|
||||||
updateCartProducts {
|
|
||||||
add(product)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeProduct(id: Int) {
|
|
||||||
updateCartProducts {
|
|
||||||
remove(find { it.id == id })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun editProductCount(id: Int, count: Int) {
|
|
||||||
updateCartProducts {
|
|
||||||
updateProduct(id) { copyWith(countInCart = count) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun markProductDeleted(id: Int) {
|
|
||||||
updateCartProducts {
|
|
||||||
updateProduct(id) { copyWith(isDeleted = true) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restoreDeletedProduct(id: Int) {
|
|
||||||
updateCartProducts {
|
|
||||||
updateProduct(id) { copyWith(isDeleted = false) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun applyPromocode(promocode: PromocodeModel) {
|
|
||||||
updatePromocodeList { add(promocode) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removePromocode(code: String) {
|
|
||||||
updatePromocodeList { removeAt(indexOfFirst { it.code == code }) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun useBonuses(bonuses: Int) {
|
|
||||||
require(currentCart.value.availableBonuses >= bonuses) { "Can't use bonuses more than available" }
|
|
||||||
_currentCart.update { it.copyWith(usedBonuses = bonuses) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun chooseVariant(productId: Int, variantId: Int?) {
|
|
||||||
updateCartProducts {
|
|
||||||
updateProduct(productId) {
|
|
||||||
if (variantId != null) {
|
|
||||||
check(variants.any { it.id == variantId }) {
|
|
||||||
"Product with id=$productId doesn't have variant with id=$variantId"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
copyWith(selectedVariantId = variantId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateCartProducts(updateAction: MutableList<TProduct>.() -> Unit) {
|
|
||||||
_currentCart.update { cart ->
|
|
||||||
cart.copyWith(products = cart.products.toMutableList().apply(updateAction))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePromocodeList(updateAction: MutableList<PromocodeModel>.() -> Unit) {
|
|
||||||
_currentCart.update { cart ->
|
|
||||||
cart.copyWith(promocodeList = cart.promocodeList.toMutableList().apply(updateAction))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MutableList<TProduct>.updateProduct(id: Int, updateAction: TProduct.() -> TProduct) {
|
|
||||||
val index = indexOfFirst { it.id == id }
|
|
||||||
if (index >= 0) this[index] = updateAction.invoke(this[index])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package ru.touchin.roboswag.cart_utils.requests_qeue
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.flow.consumeAsFlow
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queue for abstract requests which will be executed one after another
|
|
||||||
*/
|
|
||||||
typealias Request<TResponse> = suspend () -> TResponse
|
|
||||||
|
|
||||||
class RequestsQueue<TRequest : Request<*>> {
|
|
||||||
|
|
||||||
private val requestChannel = Channel<TRequest>(capacity = Channel.BUFFERED)
|
|
||||||
|
|
||||||
fun initRequestsExecution(
|
|
||||||
coroutineScope: CoroutineScope,
|
|
||||||
executeRequestAction: suspend (TRequest) -> Unit,
|
|
||||||
) {
|
|
||||||
requestChannel
|
|
||||||
.consumeAsFlow()
|
|
||||||
.onEach { executeRequestAction.invoke(it) }
|
|
||||||
.launchIn(coroutineScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addToQueue(request: TRequest) {
|
|
||||||
requestChannel.trySend(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearQueue() {
|
|
||||||
while (hasPendingRequests()) requestChannel.tryReceive()
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
fun hasPendingRequests() = !requestChannel.isEmpty
|
|
||||||
}
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
package ru.touchin.roboswag.cart_utils.update_manager
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import ru.touchin.roboswag.cart_utils.models.CartModel
|
|
||||||
import ru.touchin.roboswag.cart_utils.models.ProductModel
|
|
||||||
import ru.touchin.roboswag.cart_utils.repositories.IRemoteCartRepository
|
|
||||||
import ru.touchin.roboswag.cart_utils.repositories.LocalCartRepository
|
|
||||||
import ru.touchin.roboswag.cart_utils.requests_qeue.Request
|
|
||||||
import ru.touchin.roboswag.cart_utils.requests_qeue.RequestsQueue
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combines local and remote cart update actions
|
|
||||||
*/
|
|
||||||
open class CartUpdateManager<TCart : CartModel<TProduct>, TProduct : ProductModel>(
|
|
||||||
private val localCartRepository: LocalCartRepository<TCart, TProduct>,
|
|
||||||
private val remoteCartRepository: IRemoteCartRepository<TCart, TProduct>,
|
|
||||||
private val maxRequestAttemptsCount: Int = MAX_REQUEST_CART_ATTEMPTS_COUNT,
|
|
||||||
private val errorHandler: (Throwable) -> Unit = {},
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val MAX_REQUEST_CART_ATTEMPTS_COUNT = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
private val requestsQueue = RequestsQueue<Request<TCart>>()
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
var lastRemoteCart: TCart? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
fun initCartRequestsQueue(
|
|
||||||
coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
|
|
||||||
) {
|
|
||||||
requestsQueue.initRequestsExecution(coroutineScope) { request ->
|
|
||||||
runCatching {
|
|
||||||
lastRemoteCart = request.invoke()
|
|
||||||
if (!requestsQueue.hasPendingRequests()) updateLocalCartWithRemote()
|
|
||||||
}.onFailure { error ->
|
|
||||||
errorHandler.invoke(error)
|
|
||||||
requestsQueue.clearQueue()
|
|
||||||
tryToGetRemoteCartAgain()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun addProduct(product: TProduct, restoreDeleted: Boolean = false) {
|
|
||||||
with(localCartRepository) {
|
|
||||||
if (restoreDeleted) restoreDeletedProduct(product.id) else addProduct(product)
|
|
||||||
}
|
|
||||||
|
|
||||||
requestsQueue.addToQueue {
|
|
||||||
remoteCartRepository.addProduct(product)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun removeProduct(id: Int, markDeleted: Boolean = false) {
|
|
||||||
with(localCartRepository) {
|
|
||||||
if (markDeleted) markProductDeleted(id) else removeProduct(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
requestsQueue.addToQueue {
|
|
||||||
remoteCartRepository.removeProduct(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun editProductCount(id: Int, count: Int) {
|
|
||||||
localCartRepository.editProductCount(id, count)
|
|
||||||
|
|
||||||
requestsQueue.addToQueue {
|
|
||||||
remoteCartRepository.editProductCount(id, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun tryToGetRemoteCartAgain() {
|
|
||||||
repeat(maxRequestAttemptsCount) {
|
|
||||||
runCatching {
|
|
||||||
lastRemoteCart = remoteCartRepository.getCart()
|
|
||||||
updateLocalCartWithRemote()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateLocalCartWithRemote() {
|
|
||||||
val remoteCart = lastRemoteCart ?: return
|
|
||||||
val remoteProducts = remoteCart.products
|
|
||||||
val localProducts = localCartRepository.currentCart.value.products
|
|
||||||
|
|
||||||
val newProductsFromRemoteCart = remoteProducts.filter { remoteProduct ->
|
|
||||||
localProducts.none { it.id == remoteProduct.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
val mergedProducts = localProducts.mapNotNull { localProduct ->
|
|
||||||
val sameRemoteProduct = remoteProducts.find { it.id == localProduct.id }
|
|
||||||
|
|
||||||
when {
|
|
||||||
sameRemoteProduct != null -> sameRemoteProduct
|
|
||||||
localProduct.isDeleted -> localProduct
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val mergedCart = remoteCart.copyWith<TCart>(products = mergedProducts + newProductsFromRemoteCart)
|
|
||||||
localCartRepository.updateCart(mergedCart)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
/build
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
apply from: "../android-configs/lib-config.gradle"
|
|
||||||
apply plugin: 'com.huawei.agconnect'
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation "androidx.core:core"
|
|
||||||
implementation "androidx.annotation:annotation"
|
|
||||||
implementation "com.google.android.gms:play-services-base"
|
|
||||||
implementation "com.huawei.hms:base"
|
|
||||||
|
|
||||||
constraints {
|
|
||||||
implementation("androidx.core:core") {
|
|
||||||
version {
|
|
||||||
require '1.0.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation("androidx.annotation:annotation") {
|
|
||||||
version {
|
|
||||||
require '1.1.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation("com.google.android.gms:play-services-base") {
|
|
||||||
version {
|
|
||||||
require '18.0.1'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation("com.huawei.hms:base") {
|
|
||||||
version {
|
|
||||||
require '6.3.0.303'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="ru.touchin.client_services">
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package ru.touchin.client_services
|
|
||||||
|
|
||||||
enum class MobileService {
|
|
||||||
HUAWEI_SERVICE, GOOGLE_SERVICE
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
package ru.touchin.client_services
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.google.android.gms.common.ConnectionResult
|
|
||||||
import com.google.android.gms.common.GoogleApiAvailability
|
|
||||||
import com.huawei.hms.api.HuaweiApiAvailability
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class with utils for interacting with Google, Huawei services
|
|
||||||
*/
|
|
||||||
|
|
||||||
class ServicesUtils {
|
|
||||||
|
|
||||||
fun getCurrentService(context: Context): MobileService = when {
|
|
||||||
checkHuaweiServices(context) -> MobileService.HUAWEI_SERVICE
|
|
||||||
checkGooglePlayServices(context) -> MobileService.GOOGLE_SERVICE
|
|
||||||
else -> MobileService.GOOGLE_SERVICE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkHuaweiServices(context: Context): Boolean =
|
|
||||||
HuaweiApiAvailability.getInstance()
|
|
||||||
.isHuaweiMobileNoticeAvailable(context) == ConnectionResult.SUCCESS
|
|
||||||
|
|
||||||
private fun checkGooglePlayServices(context: Context): Boolean =
|
|
||||||
GoogleApiAvailability.getInstance()
|
|
||||||
.isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
/build
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
apply from: "../android-configs/lib-config.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation project(":mvi-arch")
|
|
||||||
implementation project(":lifecycle")
|
|
||||||
|
|
||||||
implementation "androidx.lifecycle:lifecycle-extensions"
|
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx")
|
|
||||||
|
|
||||||
def lifecycleVersion = "2.2.0"
|
|
||||||
|
|
||||||
constraints {
|
|
||||||
implementation("androidx.lifecycle:lifecycle-extensions") {
|
|
||||||
version {
|
|
||||||
require '2.1.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx") {
|
|
||||||
version {
|
|
||||||
require(lifecycleVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<manifest package="ru.touchin.code_confirm"/>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
package ru.touchin.code_confirm
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [CodeConfirmAction] is interface for the action that will call
|
|
||||||
* the confirmation request with entered code
|
|
||||||
*/
|
|
||||||
interface CodeConfirmAction
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [UpdatedCodeInputAction] is interface for the action, that should be called
|
|
||||||
* after each update of codeInput
|
|
||||||
* @param code Updated string with code from codeInput
|
|
||||||
*/
|
|
||||||
interface UpdatedCodeInputAction {
|
|
||||||
val code: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* [GetRefreshCodeAction] is interface for the action that will call
|
|
||||||
* the request of a repeat code after it's expired
|
|
||||||
*/
|
|
||||||
interface GetRefreshCodeAction
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
package ru.touchin.code_confirm
|
|
||||||
|
|
||||||
import ru.touchin.roboswag.mvi_arch.marker.ViewState
|
|
||||||
|
|
||||||
abstract class BaseCodeConfirmState(
|
|
||||||
open var codeLifetime: String,
|
|
||||||
open var isLoadingState: Boolean,
|
|
||||||
open var isWrongCode: Boolean,
|
|
||||||
open var isExpired: Boolean,
|
|
||||||
open var isRefreshCodeLoading: Boolean = false,
|
|
||||||
open var needSendCode: Boolean = true
|
|
||||||
) : ViewState {
|
|
||||||
|
|
||||||
val canRequestNewCode: Boolean
|
|
||||||
get() = isExpired && !isRefreshCodeLoading
|
|
||||||
|
|
||||||
abstract fun <T : BaseCodeConfirmState> copyWith(updateBlock: T.() -> Unit): T
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
package ru.touchin.code_confirm
|
|
||||||
|
|
||||||
import android.os.CountDownTimer
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import ru.touchin.code_confirm.LifeTimer.Companion.getFormattedCodeLifetimeString
|
|
||||||
import ru.touchin.lifecycle.extensions.toImmutable
|
|
||||||
import ru.touchin.lifecycle.livedata.SingleLiveEvent
|
|
||||||
import ru.touchin.roboswag.mvi_arch.core.MviViewModel
|
|
||||||
import ru.touchin.roboswag.mvi_arch.marker.ViewAction
|
|
||||||
|
|
||||||
@SuppressWarnings("detekt.TooGenericExceptionCaught")
|
|
||||||
abstract class BaseCodeConfirmViewModel<NavArgs : Parcelable, Action : ViewAction, State : BaseCodeConfirmState>(
|
|
||||||
initialState: State,
|
|
||||||
savedStateHandle: SavedStateHandle
|
|
||||||
) : MviViewModel<NavArgs, Action, State>(initialState, savedStateHandle) {
|
|
||||||
|
|
||||||
/** [requireCodeId] uses for auto-filling */
|
|
||||||
protected open var requireCodeId: String? = null
|
|
||||||
|
|
||||||
private var timer: CountDownTimer? = null
|
|
||||||
|
|
||||||
private var currentConfirmationCode: String? = null
|
|
||||||
|
|
||||||
private val _updateCodeEvent = SingleLiveEvent<String>()
|
|
||||||
val updateCodeEvent = _updateCodeEvent.toImmutable()
|
|
||||||
|
|
||||||
init {
|
|
||||||
_state.value = currentState.copyWith {
|
|
||||||
codeLifetime = getFormattedCodeLifetimeString(getTimerDuration().toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
startTimer(seconds = getTimerDuration())
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract fun getTimerDuration(): Int
|
|
||||||
protected abstract suspend fun requestNewCode(): BaseCodeResponse
|
|
||||||
protected abstract suspend fun requestCodeConfirmation(code: String)
|
|
||||||
|
|
||||||
protected open fun onRefreshCodeRequestError(e: Throwable) {}
|
|
||||||
protected open fun onCodeConfirmationError(e: Throwable) {}
|
|
||||||
protected open fun onSuccessCodeConfirmation(code: String) {}
|
|
||||||
|
|
||||||
override fun dispatchAction(action: Action) {
|
|
||||||
super.dispatchAction(action)
|
|
||||||
when (action) {
|
|
||||||
is CodeConfirmAction -> {
|
|
||||||
if (currentState.needSendCode) confirmCode()
|
|
||||||
}
|
|
||||||
is GetRefreshCodeAction -> {
|
|
||||||
getRefreshCode()
|
|
||||||
}
|
|
||||||
is UpdatedCodeInputAction -> {
|
|
||||||
val confirmationCodeChanged = currentConfirmationCode != action.code
|
|
||||||
|
|
||||||
_state.value = currentState.copyWith {
|
|
||||||
isWrongCode = isWrongCode && !confirmationCodeChanged
|
|
||||||
needSendCode = confirmationCodeChanged
|
|
||||||
}
|
|
||||||
currentConfirmationCode = action.code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun startTimer(seconds: Int) {
|
|
||||||
timer?.cancel()
|
|
||||||
timer = LifeTimer(
|
|
||||||
seconds = seconds,
|
|
||||||
tickAction = { millis ->
|
|
||||||
_state.value = currentState.copyWith {
|
|
||||||
codeLifetime = getFormattedCodeLifetimeString(millis)
|
|
||||||
isExpired = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
finishAction = {
|
|
||||||
_state.value = currentState.copyWith {
|
|
||||||
isExpired = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
timer?.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun getRefreshCode() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
_state.value = currentState.copyWith {
|
|
||||||
isRefreshCodeLoading = true
|
|
||||||
isWrongCode = false
|
|
||||||
}
|
|
||||||
val confirmationData = requestNewCode()
|
|
||||||
requireCodeId = confirmationData.codeId
|
|
||||||
|
|
||||||
startTimer(seconds = confirmationData.codeLifetime)
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
_state.value = currentState.copyWith { needSendCode = false }
|
|
||||||
onRefreshCodeRequestError(throwable)
|
|
||||||
} finally {
|
|
||||||
_state.value = currentState.copyWith { isRefreshCodeLoading = false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun confirmCode() {
|
|
||||||
currentConfirmationCode?.let { code ->
|
|
||||||
_state.value = currentState.copyWith { isLoadingState = true }
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
requestCodeConfirmation(code)
|
|
||||||
onSuccessCodeConfirmation(code)
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
_state.value = currentState.copyWith { needSendCode = false }
|
|
||||||
onCodeConfirmationError(throwable)
|
|
||||||
} finally {
|
|
||||||
_state.value = currentState.copyWith { isLoadingState = false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun autofillCode(code: String, codeId: String? = null) {
|
|
||||||
if (codeId == requireCodeId) {
|
|
||||||
_updateCodeEvent.setValue(code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
timer?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package ru.touchin.code_confirm
|
|
||||||
|
|
||||||
abstract class BaseCodeResponse(
|
|
||||||
open val codeLifetime: Int,
|
|
||||||
open val codeId: String? = null
|
|
||||||
)
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package ru.touchin.code_confirm
|
|
||||||
|
|
||||||
import android.os.CountDownTimer
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
/** [LifeTimer] is extends [CountDownTimer] for countdown in seconds and lifetime text formatting
|
|
||||||
* @param seconds Lifetime of timer in seconds
|
|
||||||
* @param tickAction Action will be called on regular interval
|
|
||||||
* @param finishAction Action will be called on finish */
|
|
||||||
class LifeTimer(
|
|
||||||
seconds: Int,
|
|
||||||
private val tickAction: (Long) -> Unit,
|
|
||||||
private val finishAction: () -> Unit
|
|
||||||
) : CountDownTimer(seconds.toLong() * 1000, 1000) {
|
|
||||||
|
|
||||||
override fun onTick(millisUntilFinished: Long) {
|
|
||||||
tickAction.invoke(millisUntilFinished / 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinish() {
|
|
||||||
finishAction.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val formatter = SimpleDateFormat("mm:ss", Locale.ROOT)
|
|
||||||
|
|
||||||
fun getFormattedCodeLifetimeString(secondsUntilFinished: Long): String =
|
|
||||||
formatter.format(Date(secondsUntilFinished * 1000L))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -4,10 +4,6 @@ dependencies {
|
||||||
api project(":base-map")
|
api project(":base-map")
|
||||||
|
|
||||||
implementation "com.google.android.gms:play-services-maps"
|
implementation "com.google.android.gms:play-services-maps"
|
||||||
implementation "com.google.maps.android:android-maps-utils"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core"
|
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx"
|
|
||||||
implementation "androidx.core:core-ktx"
|
|
||||||
|
|
||||||
constraints {
|
constraints {
|
||||||
implementation("com.google.android.gms:play-services-maps") {
|
implementation("com.google.android.gms:play-services-maps") {
|
||||||
|
|
@ -15,29 +11,5 @@ dependencies {
|
||||||
require '17.0.0'
|
require '17.0.0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation("com.google.maps.android:android-maps-utils") {
|
|
||||||
version {
|
|
||||||
require '0.4'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") {
|
|
||||||
version {
|
|
||||||
require '1.4.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx") {
|
|
||||||
version {
|
|
||||||
require '2.4.1'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx") {
|
|
||||||
version {
|
|
||||||
require '1.6.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package ru.touchin.googlemap
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.SparseArray
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
|
||||||
import com.google.android.gms.maps.model.BitmapDescriptor
|
|
||||||
import com.google.android.gms.maps.model.BitmapDescriptorFactory
|
|
||||||
import com.google.maps.android.clustering.Cluster
|
|
||||||
import com.google.maps.android.clustering.ClusterItem
|
|
||||||
import com.google.maps.android.ui.IconGenerator
|
|
||||||
import ru.touchin.basemap.BaseIconGenerator
|
|
||||||
import ru.touchin.basemap.getOrPutIfNotNull
|
|
||||||
|
|
||||||
open class GoogleIconGenerator<T : ClusterItem>(
|
|
||||||
private val context: Context
|
|
||||||
) : IconGenerator(context), BaseIconGenerator<T, Cluster<T>, BitmapDescriptor> {
|
|
||||||
|
|
||||||
private val clusterIconsCache = SparseArray<BitmapDescriptor>()
|
|
||||||
private val clusterItemIconsCache = mutableMapOf<T, BitmapDescriptor>()
|
|
||||||
|
|
||||||
fun setDefaultViewAndBackground() {
|
|
||||||
val defaultLayout = LayoutInflater.from(context).inflate(R.layout.view_google_map_cluster_item, null)
|
|
||||||
setBackground(ContextCompat.getDrawable(context, R.drawable.default_cluster_background))
|
|
||||||
setContentView(defaultLayout)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getClusterIcon(cluster: Cluster<T>): BitmapDescriptor? {
|
|
||||||
val clusterSize = cluster.size
|
|
||||||
return BitmapDescriptorFactory.fromBitmap(makeIcon(clusterSize.toString()))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getClusterItemIcon(clusterItem: T): BitmapDescriptor? {
|
|
||||||
val defaultIcon = context.getDrawable(ru.touchin.basemap.R.drawable.marker_default_icon)
|
|
||||||
return BitmapDescriptorFactory.fromBitmap(defaultIcon?.toBitmap())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getClusterItemView(clusterItem: T): BitmapDescriptor? =
|
|
||||||
clusterItemIconsCache.getOrPutIfNotNull(clusterItem) { getClusterItemIcon(clusterItem) }
|
|
||||||
|
|
||||||
override fun getClusterView(cluster: Cluster<T>): BitmapDescriptor? =
|
|
||||||
clusterIconsCache.getOrPutIfNotNull(cluster.size) { getClusterIcon(cluster) }
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package ru.touchin.googlemap
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.google.android.gms.maps.GoogleMap
|
|
||||||
import com.google.android.gms.maps.model.BitmapDescriptor
|
|
||||||
import com.google.android.gms.maps.model.MarkerOptions
|
|
||||||
import com.google.maps.android.clustering.Cluster
|
|
||||||
import com.google.maps.android.clustering.ClusterItem
|
|
||||||
import com.google.maps.android.clustering.ClusterManager
|
|
||||||
import com.google.maps.android.clustering.view.DefaultClusterRenderer
|
|
||||||
import ru.touchin.basemap.BaseIconGenerator
|
|
||||||
|
|
||||||
open class GoogleMapItemRenderer<TClusterItem : ClusterItem>(
|
|
||||||
val context: Context,
|
|
||||||
googleMap: GoogleMap,
|
|
||||||
clusterManager: ClusterManager<TClusterItem>,
|
|
||||||
private val minClusterItemSize: Int = 1
|
|
||||||
) : DefaultClusterRenderer<TClusterItem>(context, googleMap, clusterManager) {
|
|
||||||
|
|
||||||
var iconGenerator: BaseIconGenerator<TClusterItem, Cluster<TClusterItem>, BitmapDescriptor> =
|
|
||||||
GoogleIconGenerator<TClusterItem>(context).apply { setDefaultViewAndBackground() }
|
|
||||||
|
|
||||||
override fun shouldRenderAsCluster(cluster: Cluster<TClusterItem>): Boolean =
|
|
||||||
cluster.size > minClusterItemSize
|
|
||||||
|
|
||||||
override fun onBeforeClusterItemRendered(item: TClusterItem, markerOptions: MarkerOptions) {
|
|
||||||
markerOptions.icon(getMarkerIcon(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBeforeClusterRendered(cluster: Cluster<TClusterItem>, markerOptions: MarkerOptions) {
|
|
||||||
markerOptions.icon(getClusterIcon(cluster = cluster))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMarkerIcon(item: TClusterItem): BitmapDescriptor? =
|
|
||||||
iconGenerator.getClusterItemView(item)
|
|
||||||
|
|
||||||
private fun getClusterIcon(cluster: Cluster<TClusterItem>): BitmapDescriptor? =
|
|
||||||
iconGenerator.getClusterView(cluster)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -13,14 +13,9 @@ import ru.touchin.basemap.AbstractMapManager
|
||||||
@Suppress("detekt.TooManyFunctions")
|
@Suppress("detekt.TooManyFunctions")
|
||||||
class GoogleMapManager(mapView: MapView) : AbstractMapManager<MapView, GoogleMap, LatLng>(mapView) {
|
class GoogleMapManager(mapView: MapView) : AbstractMapManager<MapView, GoogleMap, LatLng>(mapView) {
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val CAMERA_ANIMATION_DURATION = 1f
|
|
||||||
private const val CAMERA_DEFAULT_STEP = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun initialize(mapListener: AbstractMapListener<MapView, GoogleMap, LatLng>?) {
|
override fun initialize(mapListener: AbstractMapListener<MapView, GoogleMap, LatLng>?) {
|
||||||
super.initialize(mapListener)
|
super.initialize(mapListener)
|
||||||
mapView.getMapAsync(this::initMap)
|
mapView.getMapAsync(::initMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initMap(map: GoogleMap) {
|
override fun initMap(map: GoogleMap) {
|
||||||
|
|
@ -82,52 +77,22 @@ class GoogleMapManager(mapView: MapView) : AbstractMapManager<MapView, GoogleMap
|
||||||
|
|
||||||
override fun getCameraTilt(): Float = map.cameraPosition.tilt
|
override fun getCameraTilt(): Float = map.cameraPosition.tilt
|
||||||
|
|
||||||
override fun getDefaultDuration(): Float = CAMERA_ANIMATION_DURATION
|
|
||||||
|
|
||||||
override fun getDefaultZoomStep(): Int = CAMERA_DEFAULT_STEP
|
|
||||||
|
|
||||||
override fun moveCamera(target: LatLng, zoom: Float, azimuth: Float, tilt: Float) {
|
override fun moveCamera(target: LatLng, zoom: Float, azimuth: Float, tilt: Float) {
|
||||||
map.moveCamera(CameraUpdateFactory.newCameraPosition(buildCameraPosition(target, zoom, azimuth, tilt)))
|
map.moveCamera(CameraUpdateFactory.newCameraPosition(buildCameraPosition(target, zoom, azimuth, tilt)))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun smoothMoveCamera(target: LatLng, zoom: Float, azimuth: Float, tilt: Float, animationDuration: Float) {
|
override fun smoothMoveCamera(target: LatLng, zoom: Float, azimuth: Float, tilt: Float) {
|
||||||
map.animateCamera(
|
map.animateCamera(CameraUpdateFactory.newCameraPosition(buildCameraPosition(target, zoom, azimuth, tilt)))
|
||||||
CameraUpdateFactory.newCameraPosition(buildCameraPosition(target, zoom, azimuth, tilt)),
|
|
||||||
animationDuration.toInt(),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun smoothMoveCamera(targets: List<LatLng>, padding: Int, animationDuration: Float) {
|
override fun smoothMoveCamera(targets: List<LatLng>, padding: Int) {
|
||||||
val boundingBox = getBoundingBox(targets)
|
val boundingBox = getBoundingBox(targets)
|
||||||
map.animateCamera(
|
map.animateCamera(CameraUpdateFactory.newLatLngBounds(boundingBox, padding))
|
||||||
CameraUpdateFactory.newLatLngBounds(boundingBox, padding),
|
|
||||||
animationDuration.toInt(),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun smoothMoveCamera(targets: List<LatLng>, width: Int, height: Int, padding: Int, animationDuration: Float) {
|
override fun smoothMoveCamera(targets: List<LatLng>, width: Int, height: Int, padding: Int) {
|
||||||
val boundingBox = getBoundingBox(targets)
|
val boundingBox = getBoundingBox(targets)
|
||||||
map.animateCamera(
|
map.animateCamera(CameraUpdateFactory.newLatLngBounds(boundingBox, width, height, padding))
|
||||||
CameraUpdateFactory.newLatLngBounds(boundingBox, width, height, padding),
|
|
||||||
animationDuration.toInt(),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun increaseZoom(target: LatLng, zoomIncreaseValue: Int) {
|
|
||||||
smoothMoveCamera(
|
|
||||||
target = target,
|
|
||||||
zoom = getCameraZoom() + zoomIncreaseValue
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun decreaseZoom(target: LatLng, zoomDecreaseValue: Int) {
|
|
||||||
smoothMoveCamera(
|
|
||||||
target = target,
|
|
||||||
zoom = getCameraZoom() - zoomDecreaseValue
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setMapAllGesturesEnabled(enabled: Boolean) = map.uiSettings.setAllGesturesEnabled(enabled)
|
override fun setMapAllGesturesEnabled(enabled: Boolean) = map.uiSettings.setAllGesturesEnabled(enabled)
|
||||||
|
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
package ru.touchin.googlemap
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.google.android.gms.maps.GoogleMap
|
|
||||||
import com.google.android.gms.maps.model.VisibleRegion
|
|
||||||
import com.google.maps.android.clustering.Cluster
|
|
||||||
import com.google.maps.android.clustering.ClusterItem
|
|
||||||
import com.google.maps.android.clustering.ClusterManager
|
|
||||||
import com.google.maps.android.clustering.algo.Algorithm
|
|
||||||
import com.google.maps.android.clustering.algo.NonHierarchicalDistanceBasedAlgorithm
|
|
||||||
import com.google.maps.android.clustering.algo.PreCachingAlgorithmDecorator
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.debounce
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import kotlinx.coroutines.flow.sample
|
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
|
||||||
|
|
||||||
class GooglePlacemarkManager<TClusterItem : ClusterItem>(
|
|
||||||
context: Context,
|
|
||||||
private val lifecycleOwner: LifecycleOwner,
|
|
||||||
private val googleMap: GoogleMap,
|
|
||||||
clusterItemTapAction: (TClusterItem) -> Boolean,
|
|
||||||
clusterTapAction: (Cluster<TClusterItem>) -> Boolean,
|
|
||||||
clusterAlgorithm: Algorithm<TClusterItem> = PreCachingAlgorithmDecorator(NonHierarchicalDistanceBasedAlgorithm())
|
|
||||||
) : ClusterManager<TClusterItem>(context, googleMap), GoogleMap.OnCameraIdleListener {
|
|
||||||
|
|
||||||
private var clusteringJob: Job? = null
|
|
||||||
private val onVisibilityChangedEvent = MutableSharedFlow<Pair<VisibleRegion?, List<TClusterItem>>>(
|
|
||||||
extraBufferCapacity = 1,
|
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
|
||||||
)
|
|
||||||
|
|
||||||
private var cameraIdleJob: Job? = null
|
|
||||||
private val onCameraIdleEvent = MutableSharedFlow<Boolean>(
|
|
||||||
extraBufferCapacity = 1,
|
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
|
||||||
)
|
|
||||||
|
|
||||||
private val markers = mutableListOf<TClusterItem>()
|
|
||||||
private var lastVisibleItems = emptyList<TClusterItem>()
|
|
||||||
|
|
||||||
var onCameraIdleListener: (() -> Unit)? = null
|
|
||||||
|
|
||||||
var clusterRenderer: GoogleMapItemRenderer<TClusterItem>? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
setRenderer(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
googleMap.setOnCameraIdleListener(this)
|
|
||||||
googleMap.setOnMarkerClickListener(this)
|
|
||||||
setAlgorithm(clusterAlgorithm)
|
|
||||||
setOnClusterClickListener(clusterTapAction)
|
|
||||||
setOnClusterItemClickListener(clusterItemTapAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun addItems(items: Collection<TClusterItem>) {
|
|
||||||
markers.addAll(items)
|
|
||||||
onDataChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun addItem(clusterItem: TClusterItem) {
|
|
||||||
markers.add(clusterItem)
|
|
||||||
onDataChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun removeItem(atmClusterItem: TClusterItem) {
|
|
||||||
markers.remove(atmClusterItem)
|
|
||||||
onDataChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun clearItems() {
|
|
||||||
markers.clear()
|
|
||||||
onDataChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCameraIdle() {
|
|
||||||
onDataChanged()
|
|
||||||
onCameraIdleEvent.tryEmit(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun setItems(items: Collection<TClusterItem>) {
|
|
||||||
markers.clear()
|
|
||||||
markers.addAll(items)
|
|
||||||
onDataChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startClustering() {
|
|
||||||
if (clusteringJob != null || cameraIdleJob != null) return
|
|
||||||
clusteringJob = lifecycleOwner.lifecycleScope.launchWhenStarted {
|
|
||||||
onVisibilityChangedEvent
|
|
||||||
.debounce(CLUSTERING_START_DEBOUNCE_MILLI)
|
|
||||||
.flowOn(Dispatchers.Default)
|
|
||||||
.onStart { emit(getData()) }
|
|
||||||
.mapNotNull { (region, items) -> findItemsInRegion(region, items) }
|
|
||||||
.sample(CLUSTERING_NEW_LIST_CONSUMING_THROTTLE_MILLIS)
|
|
||||||
.catch { emit(lastVisibleItems) }
|
|
||||||
.flowOn(Dispatchers.Main)
|
|
||||||
.collect { markersToShow ->
|
|
||||||
lastVisibleItems = markersToShow
|
|
||||||
super.clearItems()
|
|
||||||
super.addItems(markersToShow)
|
|
||||||
cluster()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
listenToCameraIdleEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun listenToCameraIdleEvents() {
|
|
||||||
cameraIdleJob = lifecycleOwner.lifecycleScope.launchWhenStarted {
|
|
||||||
onCameraIdleEvent
|
|
||||||
.debounce(CAMERA_DEBOUNCE_MILLI)
|
|
||||||
.flowOn(Dispatchers.Main)
|
|
||||||
.collect {
|
|
||||||
onCameraIdleListener?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopClustering() {
|
|
||||||
clusteringJob?.cancel()
|
|
||||||
cameraIdleJob?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDataChanged() {
|
|
||||||
onVisibilityChangedEvent.tryEmit(getData())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getData(): Pair<VisibleRegion?, List<TClusterItem>> =
|
|
||||||
googleMap.projection.visibleRegion to markers
|
|
||||||
|
|
||||||
private fun findItemsInRegion(region: VisibleRegion?, items: List<TClusterItem>): List<TClusterItem>? =
|
|
||||||
region?.let { items.filter { item -> item.position in region.latLngBounds } }
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val CAMERA_DEBOUNCE_MILLI = 50L
|
|
||||||
|
|
||||||
const val CLUSTERING_START_DEBOUNCE_MILLI = 50L
|
|
||||||
|
|
||||||
const val CLUSTERING_NEW_LIST_CONSUMING_THROTTLE_MILLIS = 350L
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="#FF5722"/>
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<TextView
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
android:minWidth="36dp"
|
|
||||||
android:minHeight="52dp"
|
|
||||||
android:padding="6dp"
|
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:textSize="17sp"/>
|
|
||||||
|
|
@ -1,19 +1,13 @@
|
||||||
package ru.touchin.extensions
|
package ru.touchin.extensions
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import ru.touchin.roboswag.core.log.Lc
|
|
||||||
import android.provider.Browser
|
import android.provider.Browser
|
||||||
|
|
||||||
fun Context.safeStartActivity(intent: Intent, options: Bundle? = null) =
|
fun Context.safeStartActivity(intent: Intent, options: Bundle? = null, resolveFlags: Int = 0): Boolean =
|
||||||
try {
|
packageManager.resolveActivity(intent, resolveFlags)?.let { startActivity(intent, options) } != null
|
||||||
startActivity(intent, options)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Lc.e(e, "Couldn't find activity with this parameters")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.openBrowser(url: String) = Intent(Intent.ACTION_VIEW)
|
fun Context.openBrowser(url: String) = Intent(Intent.ACTION_VIEW)
|
||||||
.setData(Uri.parse(url))
|
.setData(Uri.parse(url))
|
||||||
|
|
@ -34,3 +28,4 @@ fun Context.openBrowserWithHeaders(url: String, headersMap: Map<String, String>)
|
||||||
fun Context.callToPhoneNumber(phoneNumber: String) = Intent(Intent.ACTION_VIEW)
|
fun Context.callToPhoneNumber(phoneNumber: String) = Intent(Intent.ACTION_VIEW)
|
||||||
.setData(Uri.parse("tel:$phoneNumber"))
|
.setData(Uri.parse("tel:$phoneNumber"))
|
||||||
.let { intent -> safeStartActivity(intent) }
|
.let { intent -> safeStartActivity(intent) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package ru.touchin.extensions
|
|
||||||
|
|
||||||
import android.content.res.TypedArray
|
|
||||||
import androidx.annotation.StyleableRes
|
|
||||||
|
|
||||||
private const val NOT_FOUND_VALUE = -1
|
|
||||||
|
|
||||||
fun TypedArray.getResourceIdOrNull(@StyleableRes index: Int) = getResourceId(index, NOT_FOUND_VALUE)
|
|
||||||
.takeIf { it != NOT_FOUND_VALUE }
|
|
||||||
|
|
||||||
fun TypedArray.getColorOrNull(@StyleableRes index: Int) = getColor(index, NOT_FOUND_VALUE)
|
|
||||||
.takeIf { it != NOT_FOUND_VALUE }
|
|
||||||
|
|
@ -9,15 +9,13 @@ object ActionThrottler {
|
||||||
// action invoking start user may be in time to click and launch action again
|
// action invoking start user may be in time to click and launch action again
|
||||||
private const val PREVENTION_OF_CLICK_AGAIN_COEFFICIENT = 2
|
private const val PREVENTION_OF_CLICK_AGAIN_COEFFICIENT = 2
|
||||||
private const val DELAY_MS = PREVENTION_OF_CLICK_AGAIN_COEFFICIENT * RIPPLE_EFFECT_DELAY_MS
|
private const val DELAY_MS = PREVENTION_OF_CLICK_AGAIN_COEFFICIENT * RIPPLE_EFFECT_DELAY_MS
|
||||||
|
|
||||||
const val DEFAULT_THROTTLE_DELAY_MS = 500L
|
|
||||||
private var lastActionTime = 0L
|
private var lastActionTime = 0L
|
||||||
|
|
||||||
fun throttleAction(throttleDelay: Long = DELAY_MS, action: () -> Unit): Boolean {
|
fun throttleAction(action: () -> Unit): Boolean {
|
||||||
val currentTime = SystemClock.elapsedRealtime()
|
val currentTime = SystemClock.elapsedRealtime()
|
||||||
val diff = currentTime - lastActionTime
|
val diff = currentTime - lastActionTime
|
||||||
|
|
||||||
return if (diff >= throttleDelay) {
|
return if (diff >= DELAY_MS) {
|
||||||
lastActionTime = currentTime
|
lastActionTime = currentTime
|
||||||
action.invoke()
|
action.invoke()
|
||||||
true
|
true
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
package ru.touchin.lifecycle
|
|
||||||
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import kotlin.properties.ReadOnlyProperty
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delegate that allows to lazily initialize value on certain lifecycle event
|
|
||||||
* @param initializeEvent is event when value should be initialize
|
|
||||||
* @param initializer callback that handles value initialization
|
|
||||||
*/
|
|
||||||
|
|
||||||
class OnLifecycle<R : LifecycleOwner, T>(
|
|
||||||
private val lifecycleOwner: R,
|
|
||||||
private val initializeEvent: Lifecycle.Event,
|
|
||||||
private val initializer: (R) -> T
|
|
||||||
) : ReadOnlyProperty<R, T> {
|
|
||||||
|
|
||||||
private var value: T? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
lifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
|
|
||||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
|
||||||
if (initializeEvent == event && value == null) {
|
|
||||||
value = initializer.invoke(lifecycleOwner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getValue(thisRef: R, property: KProperty<*>) = value
|
|
||||||
?: throw IllegalStateException("Can't get access to value before $initializeEvent. Current is ${thisRef.lifecycle.currentState}")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -10,4 +10,13 @@ sealed class ContentEvent<out T>(open val data: T?) {
|
||||||
|
|
||||||
data class Complete<out T>(override val data: T? = null) : ContentEvent<T>(data)
|
data class Complete<out T>(override val data: T? = null) : ContentEvent<T>(data)
|
||||||
|
|
||||||
|
fun <P> transform(transformation: (T?) -> P): ContentEvent<P> {
|
||||||
|
return when(this) {
|
||||||
|
is Loading -> Loading(transformation(data))
|
||||||
|
is Success -> Success(transformation(data))
|
||||||
|
is Complete -> Complete(transformation(data))
|
||||||
|
is Error -> Error(throwable, transformation(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
package ru.touchin.lifecycle.extensions
|
|
||||||
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import ru.touchin.lifecycle.OnLifecycle
|
|
||||||
import kotlin.properties.ReadOnlyProperty
|
|
||||||
|
|
||||||
fun <R : LifecycleOwner, T> R.onCreateEvent(
|
|
||||||
initializer: (R) -> T
|
|
||||||
): ReadOnlyProperty<R, T> = OnLifecycle(this, Lifecycle.Event.ON_CREATE, initializer)
|
|
||||||
|
|
||||||
fun <R : LifecycleOwner, T> R.onStartEvent(
|
|
||||||
initializer: (R) -> T
|
|
||||||
): ReadOnlyProperty<R, T> = OnLifecycle(this, Lifecycle.Event.ON_START, initializer)
|
|
||||||
|
|
||||||
fun <R : LifecycleOwner, T> R.onResumeEvent(
|
|
||||||
initializer: (R) -> T
|
|
||||||
): ReadOnlyProperty<R, T> = OnLifecycle(this, Lifecycle.Event.ON_RESUME, initializer)
|
|
||||||
|
|
||||||
fun <R : LifecycleOwner, T> R.onLifecycle(
|
|
||||||
initializeEvent: Lifecycle.Event,
|
|
||||||
initializer: (R) -> T
|
|
||||||
): ReadOnlyProperty<R, T> = OnLifecycle(this, initializeEvent, initializer)
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package ru.touchin.lifecycle.scope
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import ru.touchin.lifecycle.R
|
|
||||||
|
|
||||||
val View.viewScope: CoroutineScope
|
|
||||||
get() {
|
|
||||||
val storedScope = getTag(R.string.view_coroutine_scope) as? CoroutineScope
|
|
||||||
if (storedScope != null) return storedScope
|
|
||||||
|
|
||||||
val newScope = ViewCoroutineScope()
|
|
||||||
if (isAttachedToWindow) {
|
|
||||||
addOnAttachStateChangeListener(newScope)
|
|
||||||
setTag(R.string.view_coroutine_scope, newScope)
|
|
||||||
} else {
|
|
||||||
newScope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
return newScope
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ViewCoroutineScope : CoroutineScope, View.OnAttachStateChangeListener {
|
|
||||||
override val coroutineContext = SupervisorJob() + Dispatchers.Main
|
|
||||||
|
|
||||||
override fun onViewAttachedToWindow(view: View) = Unit
|
|
||||||
|
|
||||||
override fun onViewDetachedFromWindow(view: View) {
|
|
||||||
coroutineContext.cancel()
|
|
||||||
view.setTag(R.string.view_coroutine_scope, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue