All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.svenjacobs.reveal.RevealState.kt Maven / Gradle / Ivy

The newest version!
package com.svenjacobs.reveal

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.autoSaver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

/**
 * @see rememberRevealState
 */
@Stable
@Suppress("MemberVisibilityCanBePrivate")
public class RevealState internal constructor(
	visible: Boolean,
	private val restoreCurrentRevealableKey: Key?,
) {

	public constructor() : this(
		visible = false,
		restoreCurrentRevealableKey = null,
	)

	private val mutex = Mutex()
	private var didRestoreCurrentRevealable = false
	private var visible by mutableStateOf(visible)
	private val revealables = mutableStateMapOf()
	internal var currentRevealable by mutableStateOf(null)
		private set
	internal var previousRevealable by mutableStateOf(null)
		private set

	/**
	 * Returns `true` if reveal effect is visible, else `false`
	 */
	public val isVisible: Boolean
		get() = visible

	/**
	 * Observable key of current revealable or `null` if no revealable is currently visible
	 *
	 * @see previousRevealableKey
	 */
	public val currentRevealableKey: Key?
		get() = currentRevealable?.key

	/**
	 * Observable key of previous revealable which was displayed before [currentRevealableKey]
	 *
	 * @see currentRevealableKey
	 */
	public val previousRevealableKey: Key?
		get() = previousRevealable?.key

	/**
	 * Observable set of keys known to this state instance
	 *
	 * Can be used to query when a revealable was registered via [RevealScope.revealable].
	 */
	public val revealableKeys: Set
		get() = revealables.keys

	/**
	 * Reveals revealable with given [key]
	 *
	 * Might throw [IllegalArgumentException] if the revealable item is not known to Reveal. This
	 * might happen if for example the item is in a lazy container and is currently not part of the
	 * visible area. It is the duty of the developer to ensure that a revealable item is currently
	 * visible (known to Reveal) before calling this function. Additionally [containsRevealable] or
	 * [revealableKeys] can be used to ensure this.
	 *
	 * @see tryReveal
	 * @see containsRevealable
	 * @see revealableKeys
	 * @throws IllegalArgumentException if revealable with given key was not found
	 */
	public suspend fun reveal(key: Key) {
		require(containsRevealable(key)) { "Revealable with key \"$key\" not found" }
		internalReveal(key)
	}

	/**
	 * Like [reveal] but doesn't throw exception if revealable was not found.
	 * Instead returns `false`.
	 *
	 * @see reveal
	 */
	public suspend fun tryReveal(key: Key): Boolean {
		if (!containsRevealable(key)) return false
		internalReveal(key)
		return true
	}

	private suspend fun internalReveal(key: Key) {
		mutex.withLock {
			previousRevealable = currentRevealable
			currentRevealable = revealables[key]
			visible = true
		}
	}

	/**
	 * Hides reveal effect
	 */
	public suspend fun hide() {
		mutex.withLock {
			visible = false
		}
	}

	/**
	 * Returns `true` if this state instance contains revealable with given [key]
	 */
	public fun containsRevealable(key: Key): Boolean = revealableKeys.contains(key)

	internal fun onHideAnimationFinished() {
		currentRevealable = null
		previousRevealable = null
	}

	/**
	 * Adds a [Revealable] to this state.
	 *
	 * Usually this should not be called manually but revealables registered via the
	 * [RevealScope.revealable] modifier. Only use this function when for instance you want to
	 * reveal legacy Android views.
	 *
	 * @see RevealScope.revealable
	 */
	public fun addRevealable(revealable: Revealable) {
		revealables[revealable.key] = revealable

		if (!didRestoreCurrentRevealable && restoreCurrentRevealableKey == revealable.key) {
			currentRevealable = revealable
			didRestoreCurrentRevealable = true
		}
	}

	/**
	 * @see addRevealable
	 */
	@Deprecated(
		message = "Use addRevealable()",
		replaceWith = ReplaceWith("addRevealable(revealable)"),
	)
	public fun putRevealable(revealable: Revealable): Unit = addRevealable(revealable)

	/**
	 * Removes a [Revealable] from this state.
	 *
	 * Usually this should not be called manually. The [RevealScope.revealable] modifier takes care
	 * of removing revealables when the composable is disposed.
	 */
	public fun removeRevealable(key: Key) {
		revealables.remove(key)

		// Hide effect if the current revealable left the composition.
		// currentRevealable and previousRevealable are reset via onHideAnimationFinished().
		if (currentRevealableKey == key) {
			visible = false
		}

		if (previousRevealableKey == key) {
			previousRevealable = null
		}
	}

	internal companion object {

		internal fun newSaver(keySaver: Saver): Saver = listSaver(
			save = {
				listOf(
					it.isVisible,
					it.currentRevealableKey?.let { key -> with(keySaver) { save(key) } },
				)
			},
			restore = {
				RevealState(
					visible = it[0] as Boolean,
					restoreCurrentRevealableKey = it[1]?.let { keySaveable ->
						keySaver.restore(keySaveable)
					},
				)
			},
		)
	}
}

/**
 * Creates a [RevealState] that is remembered across compositions.
 *
 * If a custom type is used for revealable keys which cannot be saved automatically by Compose,
 * a custom saver must be specified via [keySaver].
 *
 * @param keySaver Custom saver for revealable keys
 */
@Composable
public fun rememberRevealState(keySaver: Saver = autoSaver()): RevealState =
	rememberSaveable(saver = RevealState.newSaver(keySaver)) { RevealState() }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy