Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/apply changes #22

Merged
merged 9 commits into from
Apr 8, 2020
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ See [this file](https://github.com/pandulapeter/beagle/blob/master/beagle-core/s
* The library exposes the [BeagleListener](https://github.com/pandulapeter/beagle/blob/master/beagle-core/src/main/java/com/pandulapeter/beagleCore/contracts/BeagleListener.kt) interface that can be used to observe state changes. Make sure to always remove implementations that should be garbage collected.
* The library exposes the current Activity instance through the nullable, read-only Beagle.currentActivity property, which can be used to perform navigation actions in response to click events for example.
* If you don't want Beagle to be included in some of your activities, you can use the `imprint()` function's optional `excludedActivities` parameter.
* If some of your app settings can be toggled from Beagle but applying them after every change is wasteful, the Slider, Toggle, SingleSelectionList and MultipleSelectionList tricks support the "needsConfirmation" parameter which will enable a dynamic "Apply" button after changes.

### Changelog
* Check out the [Releases](https://github.com/pandulapeter/beagle/releases) page for the changes in every version. The library uses [semantic versioning](https://semver.org): *MAJOR.MINOR.PATCH* where *PATCH* changes only contain bug fixes, *MINOR* changes add new features and *MAJOR* changes introduce breaking modifications to the API.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ import kotlinx.android.parcel.Parcelize
*
* @param themeResourceId - The theme resource ID the drawers should use. If null, each drawer will take their Activity's theme. Null by default.
* @param drawerWidth - Custom width for the drawer. If null, 280dp will be used. Null by default.
* @param applyButtonText - The text on the Apply button that appears when the user makes changes that are not handled in real-time (see the "needsConfirmation" parameter of some Tricks). "Apply" by default.
* @param resetButtonText - The text on the Reset button that appears when the user makes changes that are not handled in real-time (see the "needsConfirmation" parameter of some Tricks). "Reset" by default.
* @param shouldUseItemsInsteadOfButtons - When true, clickable list items will be used instead of buttons. False by default.
*/
@Parcelize
data class Appearance(
@StyleRes val themeResourceId: Int? = null,
@Dimension val drawerWidth: Int? = null,
val applyButtonText: String = "Apply",
val resetButtonText: String = "Reset",
val shouldShowResetButton: Boolean = true,
val shouldUseItemsInsteadOfButtons: Boolean = false
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,14 @@ import androidx.annotation.ColorInt
import androidx.annotation.Dimension
import androidx.annotation.IdRes
import com.pandulapeter.beagleCore.contracts.BeagleListItemContract
import com.pandulapeter.beagleCore.implementation.ChangeEvent
import java.util.UUID

/**
* Contains all supported modules that can be added to the drawer.
*/
sealed class Trick {

//region Implementation details
abstract val id: String

interface Expandable {

val id: String
val title: CharSequence
val isInitiallyExpanded: Boolean
val isExpanded: Boolean

fun toggleExpandedState()
}
//endregion

//region Generic modules
/**
* Displays an empty space of specified size.
Expand Down Expand Up @@ -108,24 +95,28 @@ sealed class Trick {
* @param minimumValue - The minimum value supported by the slider. 0 by default.
* @param maximumValue - The maximum value supported by the slider. 10 by default.
* @param initialValue - The initial value of the slider. By default it's the same as the slider's minimum value.
* @param needsConfirmation - If true, an "Apply" button will appear after any modification and the onValueChanged lambda will only get called after the user presses that button. False by default.
* @param onValueChanged - Callback that gets invoked when the user changes the value of the slider.
*/
data class Slider(
override val id: String = UUID.randomUUID().toString(),
val name: (value: Int) -> CharSequence,
val minimumValue: Int = 0,
val maximumValue: Int = 10,
val initialValue: Int = minimumValue,
private val onValueChanged: (value: Int) -> Unit
) : Trick() {
override val initialValue: Int = minimumValue,
override val needsConfirmation: Boolean = false,
override val onValueChanged: (value: Int) -> Unit
) : Trick(), Confirmable<Int> {

var value = initialValue
override var currentValue = initialValue
set(value) {
if (field != value) {
field = value
onValueChanged(value)
onCurrentValueChanged(value)
}
}
override var savedValue = initialValue

}

/**
Expand All @@ -135,22 +126,25 @@ sealed class Trick {
* @param id - A unique ID for the module. If you don't intend to dynamically remove / modify the module, a suitable default value is auto-generated.
* @param title - The text that appears near the switch. "Keyline overlay" by default.
* @param initialValue - The initial value of the toggle. False by default.
* @param needsConfirmation - If true, an "Apply" button will appear after any modification and the onValueChanged lambda will only get called after the user presses that button. False by default.
* @param onValueChanged - Callback that gets invoked when the user changes the value of the toggle.
*/
data class Toggle(
override val id: String = UUID.randomUUID().toString(),
val title: CharSequence,
val initialValue: Boolean = false,
private val onValueChanged: (newValue: Boolean) -> Unit
) : Trick() {
override val initialValue: Boolean = false,
override val needsConfirmation: Boolean = false,
override val onValueChanged: (newValue: Boolean) -> Unit
) : Trick(), Confirmable<Boolean> {

var value = initialValue
override var currentValue = initialValue
set(value) {
if (field != value) {
field = value
onValueChanged(value)
onCurrentValueChanged(value)
}
}
override var savedValue = initialValue
}

/**
Expand All @@ -161,7 +155,6 @@ sealed class Trick {
* @param text - The text that should be displayed on the button.
* @param onButtonPressed - The callback that gets invoked when the user presses the button.
*/
//TODO: The Buttons don't look great if the app uses Material theme.
data class Button(
override val id: String = UUID.randomUUID().toString(),
val text: CharSequence,
Expand Down Expand Up @@ -231,6 +224,7 @@ sealed class Trick {
* @param items - The hardcoded list of items implementing the [BeagleListItemContract] interface.
* @param isInitiallyExpanded - Whether or not the list should be expanded when the drawer is opened for the first time. False by default.
* @param initialSelectionId - The ID of the item that is selected when the drawer is opened for the first time, or null if no selection should be made initially. Null by default.
* @param needsConfirmation - If true, an "Apply" button will appear after any modification and the onItemSelectionChanged lambda will only get called after the user presses that button. False by default.
* @param onItemSelectionChanged - The callback that will get executed when the selected item is changed.
*/
data class SingleSelectionList<T : BeagleListItemContract>(
Expand All @@ -239,21 +233,29 @@ sealed class Trick {
override val isInitiallyExpanded: Boolean = false,
val items: List<T>,
private val initialSelectionId: String? = null,
override val needsConfirmation: Boolean = false,
private val onItemSelectionChanged: (selectedItem: T) -> Unit
) : Trick(), Expandable {
) : Trick(), Expandable, Confirmable<String?> {

override val initialValue: String? = initialSelectionId
override val onValueChanged: (String?) -> Unit = { id -> onItemSelectionChanged(items.first { it.id == id }) }
override var isExpanded = isInitiallyExpanded
private set
var selectedItemId = initialSelectionId
private set
override var currentValue = initialSelectionId
set(value) {
if (field != value) {
field = value
onCurrentValueChanged(value)
}
}
override var savedValue = initialSelectionId

override fun toggleExpandedState() {
isExpanded = !isExpanded
}

fun invokeItemSelectedCallback(id: String) {
selectedItemId = id
onItemSelectionChanged(items.first { it.id == selectedItemId })
currentValue = id
}
}

Expand All @@ -267,6 +269,7 @@ sealed class Trick {
* @param items - The hardcoded list of items implementing the [BeagleListItemContract] interface.
* @param isInitiallyExpanded - Whether or not the list should be expanded when the drawer is opened for the first time. False by default.
* @param initialSelectionIds - The ID-s of the items that are selected when the drawer is opened for the first time. Empty list by default.
* @param needsConfirmation - If true, an "Apply" button will appear after any modification and the onItemSelectionChanged lambda will only get called after the user presses that button. False by default.
* @param onItemSelectionChanged - The callback that will get executed when the list of selected items is changed.
*/
data class MultipleSelectionList<T : BeagleListItemContract>(
Expand All @@ -275,27 +278,35 @@ sealed class Trick {
override val isInitiallyExpanded: Boolean = false,
val items: List<T>,
private val initialSelectionIds: List<String> = emptyList(),
override val needsConfirmation: Boolean = false,
private val onItemSelectionChanged: (selectedItems: List<T>) -> Unit
) : Trick(), Expandable {
) : Trick(), Expandable, Confirmable<List<String>> {

override val initialValue = initialSelectionIds
override val onValueChanged: (List<String>) -> Unit = { ids -> onItemSelectionChanged(items.filter { ids.contains(it.id) }) }
override var isExpanded = isInitiallyExpanded
private set
var selectedItemIds = initialSelectionIds
private set
override var currentValue = initialValue.sorted()
set(value) {
if (field != value) {
field = value
onCurrentValueChanged(value)
}
}
override var savedValue = initialValue.sorted()

override fun toggleExpandedState() {
isExpanded = !isExpanded
}

fun invokeItemSelectedCallback(id: String) {
selectedItemIds = selectedItemIds.toMutableList().apply {
currentValue = currentValue.toMutableList().apply {
if (contains(id)) {
remove(id)
} else {
add(id)
}
}
onItemSelectionChanged(items.filter { selectedItemIds.contains(it.id) })
}.sorted()
}
}

Expand Down Expand Up @@ -550,4 +561,82 @@ sealed class Trick {
}
}
//endregion

//region Implementation details
abstract val id: String

interface Expandable {

val id: String
val title: CharSequence
val isInitiallyExpanded: Boolean
val isExpanded: Boolean

fun toggleExpandedState()
}

interface Confirmable<T> {
val id: String
val needsConfirmation: Boolean
val initialValue: T
var currentValue: T
var savedValue: T
val onValueChanged: (T) -> Unit

fun updateSavedValueAndNotifyUser(value: T) {
onValueChanged(value)
savedValue = value
}

fun onCurrentValueChanged(newValue: T) {
if (needsConfirmation) {
if (currentValue == savedValue) {
removeChangeEvent(id)
} else {
addChangeEvent(ChangeEvent(
trickId = id,
apply = { updateSavedValueAndNotifyUser(newValue) },
reset = { currentValue = savedValue }
))
}
} else {
updateSavedValueAndNotifyUser(newValue)
}
}
}

//TODO: Should be encapsulated + should not be part of the noop variant. Needs refactoring.
companion object {
var changeListener: (() -> Unit)? = null
private var pendingChanges = emptyList<ChangeEvent>()
val hasPendingChanges get() = pendingChanges.isNotEmpty()

fun addChangeEvent(changeEvent: ChangeEvent) {
pendingChanges = (listOf(changeEvent) + pendingChanges).distinctBy { it.trickId }
changeListener?.invoke()
}

fun removeChangeEvent(trickId: String) {
pendingChanges = pendingChanges.filterNot { it.trickId == trickId }
changeListener?.invoke()
}

fun applyPendingChanges() {
pendingChanges.asReversed().toList().forEach { changeEvent ->
pendingChanges = pendingChanges.filterNot { it.trickId == changeEvent.trickId }
changeEvent.apply()
}
changeListener?.invoke()
}

fun resetPendingChanges() {
pendingChanges.forEach { changeEvent -> changeEvent.reset() }
}

fun clearChangeEvents() {
pendingChanges = emptyList()
changeListener?.invoke()
}
}
//endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ interface BeagleContract {

//region Public API
var isEnabled: Boolean

val currentActivity: Activity?
val hasPendingChanges: Boolean

fun imprint(
application: Application,
appearance: Appearance = Appearance(),
triggerGesture: TriggerGesture = TriggerGesture.SWIPE_AND_SHAKE,
shouldShowResetButton: Boolean = true,
packageName: String? = null,
excludedActivities: List<Class<out Activity>> = emptyList()
) = Unit
Expand Down Expand Up @@ -67,16 +68,18 @@ interface BeagleContract {
* @param application - The [Application] instance.
* @param appearance - The [Appearance] that specifies the appearance the drawer. Optional.
* @param triggerGesture - Specifies the way the drawer can be opened. [TriggerGesture.SWIPE_AND_SHAKE] by default.
* @param shouldShowResetButton - Whether or not to display a Reset button besides the Apply button that appears when the user makes changes that are not handled in real-time (see the "needsConfirmation" parameter of some Tricks). True by default.
* @param packageName - Tha base package name of the application. Beagle will only work in Activities that are under this package. If not specified, an educated guess will be made (that won't work if your setup includes product flavors for example).
* @param excludedActivities - The list of Activity classes where you specifically don't want to use Beagle. Empty by default.
*/
fun initialize(
application: Application,
appearance: Appearance = Appearance(),
triggerGesture: TriggerGesture = TriggerGesture.SWIPE_AND_SHAKE,
shouldShowResetButton: Boolean,
packageName: String? = null,
excludedActivities: List<Class<out Activity>> = emptyList()
) = imprint(application, appearance, triggerGesture, packageName, excludedActivities)
) = imprint(application, appearance, triggerGesture, shouldShowResetButton, packageName, excludedActivities)

/**
* Use this function to clear the contents of the menu and set a new list of tricks.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.pandulapeter.beagleCore.implementation

data class ChangeEvent(
val trickId: String,
val apply: () -> Unit,
val reset: () -> Unit
)
5 changes: 5 additions & 0 deletions beagle-noop/src/main/java/com/pandulapeter/beagle/Beagle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ object Beagle : BeagleContract {
* Returns null (Beagle does not work at all in the noop variant).
*/
override val currentActivity: Activity? = null

/**
* Returns false (Beagle does not work at all in the noop variant).
*/
override val hasPendingChanges: Boolean = false
}
Loading