diff --git a/compose-core/src/main/java/com/qmuiteam/compose/core/ui/DefaultConfig.kt b/compose-core/src/main/java/com/qmuiteam/compose/core/ui/DefaultConfig.kt index 42f373df..e23e70f4 100644 --- a/compose-core/src/main/java/com/qmuiteam/compose/core/ui/DefaultConfig.kt +++ b/compose-core/src/main/java/com/qmuiteam/compose/core/ui/DefaultConfig.kt @@ -18,6 +18,7 @@ val qmuiScrollAlphaChangeMaxOffset = 20.dp val qmuiDialogVerEdgeProtectionMargin = 44.dp +val qmuiToastVerEdgeProtectionMargin = 96.dp diff --git a/compose/src/main/java/com/qmuiteam/compose/modal/ModalImpl.kt b/compose/src/main/java/com/qmuiteam/compose/modal/ModalImpl.kt index 2b86ffac..d3cf09d6 100644 --- a/compose/src/main/java/com/qmuiteam/compose/modal/ModalImpl.kt +++ b/compose/src/main/java/com/qmuiteam/compose/modal/ModalImpl.kt @@ -27,7 +27,7 @@ internal abstract class QMUIModalPresent( private val onBackPressedDispatcher: OnBackPressedDispatcher, val mask: Color = DefaultMaskColor, val systemCancellable: Boolean = true, - val maskCancellable: Boolean = true + val maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss, ) : QMUIModal { private val onShowListeners = arrayListOf() @@ -65,6 +65,9 @@ internal abstract class QMUIModalPresent( composeLayout.disposeComposition() rootLayout.removeView(composeLayout) onBackPressedCallback.remove() + onDismissListeners.forEach { + it.invoke(this) + } } @Composable @@ -74,15 +77,19 @@ internal abstract class QMUIModalPresent( return isShown } - override fun show() { + override fun show(): QMUIModal { if (isShown || isDismissing) { - return + return this } isShown = true rootLayout.addView(composeLayout, generateLayoutParams()) composeLayout.visibility = View.VISIBLE visibleFlow.value = true onBackPressedDispatcher.addCallback(onBackPressedCallback) + onShowListeners.forEach { + it.invoke(this) + } + return this } open fun generateLayoutParams(): FrameLayout.LayoutParams { @@ -101,20 +108,24 @@ internal abstract class QMUIModalPresent( visibleFlow.value = false } - override fun doOnShow(listener: QMUIModal.Action) { + override fun doOnShow(listener: QMUIModal.Action): QMUIModal { onShowListeners.add(listener) + return this } - override fun doOnDismiss(listener: QMUIModal.Action) { + override fun doOnDismiss(listener: QMUIModal.Action): QMUIModal { onDismissListeners.add(listener) + return this } - override fun removeOnShowAction(listener: QMUIModal.Action) { + override fun removeOnShowAction(listener: QMUIModal.Action): QMUIModal { onShowListeners.remove(listener) + return this } - override fun removeOnDismissAction(listener: QMUIModal.Action) { + override fun removeOnDismissAction(listener: QMUIModal.Action): QMUIModal { onDismissListeners.remove(listener) + return this } } @@ -123,9 +134,9 @@ internal class StillModalImpl( onBackPressedDispatcher: OnBackPressedDispatcher, mask: Color = DefaultMaskColor, systemCancellable: Boolean = true, - maskCancellable: Boolean = true, + maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss, val content: @Composable (modal: QMUIModal) -> Unit -) : QMUIModalPresent(rootLayout, onBackPressedDispatcher, mask, systemCancellable, maskCancellable) { +) : QMUIModalPresent(rootLayout, onBackPressedDispatcher, mask, systemCancellable, maskTouchBehavior) { @Composable override fun ModalContent(visible: Boolean, dismissFinishAction: () -> Unit) { @@ -134,12 +145,18 @@ internal class StillModalImpl( modifier = Modifier .fillMaxSize() .background(mask) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - enabled = maskCancellable - ) { - dismiss() + .let { + if (maskTouchBehavior == MaskTouchBehavior.penetrate) { + it + } else { + it.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + enabled = maskTouchBehavior == MaskTouchBehavior.dismiss + ) { + dismiss() + } + } } ) } else { @@ -157,10 +174,10 @@ internal class AnimateModalImpl( onBackPressedDispatcher: OnBackPressedDispatcher, mask: Color = DefaultMaskColor, systemCancellable: Boolean = true, - maskCancellable: Boolean = true, + maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss, val durationMillis: Int = 300, val content: @Composable AnimatedVisibilityScope.(modal: QMUIModal) -> Unit -) : QMUIModalPresent(rootLayout, onBackPressedDispatcher, mask, systemCancellable, maskCancellable) { +) : QMUIModalPresent(rootLayout, onBackPressedDispatcher, mask, systemCancellable, maskTouchBehavior) { @Composable override fun ModalContent(visible: Boolean, dismissFinishAction: () -> Unit) { @@ -172,13 +189,20 @@ internal class AnimateModalImpl( Box(modifier = Modifier .fillMaxSize() .background(mask) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - enabled = maskCancellable - ) { - dismiss() - }) + .let { + if (maskTouchBehavior == MaskTouchBehavior.penetrate) { + it + } else { + it.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + enabled = maskTouchBehavior == MaskTouchBehavior.dismiss + ) { + dismiss() + } + } + } + ) content(this@AnimateModalImpl) DisposableEffect("") { onDispose { diff --git a/compose/src/main/java/com/qmuiteam/compose/modal/QMUIDialog.kt b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIDialog.kt index 646c206a..c6bf9ff1 100644 --- a/compose/src/main/java/com/qmuiteam/compose/modal/QMUIDialog.kt +++ b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIDialog.kt @@ -324,7 +324,7 @@ fun QMUIDialogAction( fun View.qmuiDialog( mask: Color = DefaultMaskColor, systemCancellable: Boolean = true, - maskCancellable: Boolean = true, + maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss, modalHostProvider: ModalHostProvider = DefaultModalHostProvider, animationDurationMillis: Int = 300, horEdge: Dp = qmuiCommonHorSpace, @@ -334,7 +334,7 @@ fun View.qmuiDialog( background: Color = Color.White, content: @Composable (QMUIModal) -> Unit ): QMUIModal { - return qmuiModal(mask, systemCancellable, maskCancellable, animationDurationMillis, modalHostProvider) { modal -> + return qmuiModal(mask, systemCancellable, maskTouchBehavior, animationDurationMillis, modalHostProvider = modalHostProvider) { modal -> QMUIDialog(modal, horEdge, verEdge, widthLimit, radius, background, content) } } @@ -342,7 +342,7 @@ fun View.qmuiDialog( fun View.qmuiStillDialog( mask: Color = DefaultMaskColor, systemCancellable: Boolean = true, - maskCancellable: Boolean = true, + maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss, modalHostProvider: ModalHostProvider = DefaultModalHostProvider, horEdge: Dp = 20.dp, verEdge: Dp = 20.dp, @@ -351,7 +351,7 @@ fun View.qmuiStillDialog( background: Color = Color.White, content: @Composable (QMUIModal) -> Unit ): QMUIModal { - return qmuiStillModal(mask, systemCancellable, maskCancellable, modalHostProvider) { modal -> + return qmuiStillModal(mask, systemCancellable, maskTouchBehavior, modalHostProvider = modalHostProvider) { modal -> QMUIDialog(modal, horEdge, verEdge, widthLimit, radius, background, content) } } \ No newline at end of file diff --git a/compose/src/main/java/com/qmuiteam/compose/modal/QMUIModal.kt b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIModal.kt index 12590bd8..01ee2d32 100644 --- a/compose/src/main/java/com/qmuiteam/compose/modal/QMUIModal.kt +++ b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIModal.kt @@ -1,5 +1,6 @@ package com.qmuiteam.compose.modal +import android.os.SystemClock import android.view.View import android.view.Window import android.widget.FrameLayout @@ -12,10 +13,15 @@ import androidx.compose.runtime.DisposableEffectResult import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView +import com.qmuiteam.compose.R import com.qmuiteam.compose.core.ui.qmuiPrimaryColor val DefaultMaskColor = Color.Black.copy(alpha = 0.5f) +enum class MaskTouchBehavior{ + dismiss, penetrate, none +} + private class ModalHolder(var current: QMUIModal? = null) class QMUIModalAction( @@ -25,15 +31,20 @@ class QMUIModalAction( val onClick: (QMUIModal) -> Unit ) +private class ShowingModals { + val modals = mutableMapOf() +} + @Composable fun QMUIModal( isVisible: Boolean, mask: Color = DefaultMaskColor, durationMillis: Int = 300, systemCancellable: Boolean = true, - maskCancellable: Boolean = true, + maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss, doOnShow: QMUIModal.Action? = null, doOnDismiss: QMUIModal.Action? = null, + uniqueId: Long = SystemClock.elapsedRealtimeNanos(), modalHostProvider: ModalHostProvider = DefaultModalHostProvider, content: @Composable AnimatedVisibilityScope.(QMUIModal) -> Unit ) { @@ -42,7 +53,7 @@ fun QMUIModal( } if (isVisible) { if (modalHolder.current == null) { - val modal = LocalView.current.qmuiModal(mask, systemCancellable, maskCancellable, durationMillis, modalHostProvider, content) + val modal = LocalView.current.qmuiModal(mask, systemCancellable, maskTouchBehavior, durationMillis, uniqueId, modalHostProvider, content) doOnShow?.let { modal.doOnShow(it) } doOnDismiss?.let { modal.doOnDismiss(it) } modalHolder.current = modal @@ -60,14 +71,14 @@ fun QMUIModal( } interface QMUIModal { - fun show() + fun show(): QMUIModal fun dismiss() fun isShowing(): Boolean - fun doOnShow(listener: Action) - fun doOnDismiss(listener: Action) - fun removeOnShowAction(listener: Action) - fun removeOnDismissAction(listener: Action) + fun doOnShow(listener: Action): QMUIModal + fun doOnDismiss(listener: Action): QMUIModal + fun removeOnShowAction(listener: Action): QMUIModal + fun removeOnDismissAction(listener: Action): QMUIModal fun interface Action { fun invoke(modal: QMUIModal) @@ -92,8 +103,9 @@ val DefaultModalHostProvider = ActivityHostModalProvider() fun View.qmuiModal( mask: Color = DefaultMaskColor, systemCancellable: Boolean = true, - maskCancellable: Boolean = true, + maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss, durationMillis: Int = 300, + uniqueId: Long = SystemClock.elapsedRealtimeNanos(), modalHostProvider: ModalHostProvider = DefaultModalHostProvider, content: @Composable AnimatedVisibilityScope.(QMUIModal) -> Unit ): QMUIModal { @@ -101,16 +113,21 @@ fun View.qmuiModal( throw RuntimeException("View is not attached to window") } val modalHost = modalHostProvider.provide(this) - return AnimateModalImpl( + val modal = AnimateModalImpl( modalHost.first, modalHost.second, - mask, systemCancellable, maskCancellable, durationMillis, content) + mask, systemCancellable, maskTouchBehavior, durationMillis, content + ) + val hostView = modalHost.first + handleModelUnique(hostView, modal, uniqueId) + return modal } fun View.qmuiStillModal( mask: Color = DefaultMaskColor, systemCancellable: Boolean = true, - maskCancellable: Boolean = true, + maskTouchBehavior: MaskTouchBehavior = MaskTouchBehavior.dismiss, + uniqueId: Long = SystemClock.elapsedRealtimeNanos(), modalHostProvider: ModalHostProvider = DefaultModalHostProvider, content: @Composable (QMUIModal) -> Unit ): QMUIModal { @@ -118,5 +135,24 @@ fun View.qmuiStillModal( throw RuntimeException("View is not attached to window") } val modalHost = modalHostProvider.provide(this) - return StillModalImpl(modalHost.first, modalHost.second, mask, systemCancellable, maskCancellable, content) + val modal = StillModalImpl(modalHost.first, modalHost.second, mask, systemCancellable, maskTouchBehavior, content) + val hostView = modalHost.first + handleModelUnique(hostView, modal, uniqueId) + return modal +} + +private fun handleModelUnique(hostView: FrameLayout, modal: QMUIModal, uniqueId: Long) { + val showingModals = (hostView.getTag(R.id.qmui_modals) as? ShowingModals) ?: ShowingModals().also { + hostView.setTag(R.id.qmui_modals, it) + } + + modal.doOnShow { + showingModals.modals.put(uniqueId, it)?.dismiss() + } + + modal.doOnDismiss { + if (showingModals.modals[uniqueId] == it) { + showingModals.modals.remove(uniqueId) + } + } } \ No newline at end of file diff --git a/compose/src/main/java/com/qmuiteam/compose/modal/QMUIToast.kt b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIToast.kt new file mode 100644 index 00000000..8454ce4d --- /dev/null +++ b/compose/src/main/java/com/qmuiteam/compose/modal/QMUIToast.kt @@ -0,0 +1,117 @@ +package com.qmuiteam.compose.modal + +import android.view.View +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.qmuiteam.compose.core.ui.qmuiCommonHorSpace +import com.qmuiteam.compose.core.ui.qmuiToastVerEdgeProtectionMargin +import kotlinx.coroutines.* + +private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + +@Composable +fun QMUIToast( + modal: QMUIModal, + radius: Dp = 8.dp, + background: Color = Color.DarkGray, + content: @Composable BoxScope.(QMUIModal) -> Unit +) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(radius)) + .background(background) + ) { + content(modal) + } +} + +fun View.qmuiToast( + text: String, + textColor: Color = Color.White, + fontSize: TextUnit = 16.sp, + duration: Long = 1000, + modalHostProvider: ModalHostProvider = DefaultModalHostProvider, + animationDurationMillis: Int = 300, + alignment: Alignment = Alignment.BottomCenter, + horEdge: Dp = qmuiCommonHorSpace, + verEdge: Dp = qmuiToastVerEdgeProtectionMargin, + radius: Dp = 8.dp, + background: Color = Color.Black +): QMUIModal { + return qmuiToast(duration, modalHostProvider, animationDurationMillis, alignment, horEdge, verEdge, radius, background) { + Text( + text = text, + color = textColor, + fontSize = fontSize, + modifier = Modifier + .padding(horizontal = 24.dp, vertical = 16.dp) + .align(Alignment.Center) + ) + } +} + +@OptIn(ExperimentalAnimationApi::class) +fun View.qmuiToast( + duration: Long = 1000, + modalHostProvider: ModalHostProvider = DefaultModalHostProvider, + animationDurationMillis: Int = 300, + alignment: Alignment = Alignment.BottomCenter, + horEdge: Dp = qmuiCommonHorSpace, + verEdge: Dp = qmuiToastVerEdgeProtectionMargin, + radius: Dp = 8.dp, + background: Color = Color.Black, + content: @Composable BoxScope.(QMUIModal) -> Unit +): QMUIModal { + var job: Job? = null + return qmuiModal( + Color.Transparent, + false, + MaskTouchBehavior.penetrate, + animationDurationMillis, + -1, + modalHostProvider + ) { modal -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = horEdge, vertical = verEdge), + contentAlignment = alignment + ) { + Box( + modifier = Modifier + .animateEnterExit( + enter = slideInVertically(initialOffsetY = { it }), + exit = slideOutVertically(targetOffsetY = { it }) + ) + ) { + QMUIToast(modal, radius, background, content) + } + } + }.doOnShow { + job = scope.launch { + delay(duration) + job = null + it.dismiss() + } + }.doOnDismiss { + job?.cancel() + job = null + }.show() +} \ No newline at end of file diff --git a/compose/src/main/res/values/ids.xml b/compose/src/main/res/values/ids.xml new file mode 100644 index 00000000..a402c628 --- /dev/null +++ b/compose/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index db0ba7ac..8fbc6851 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,6 +19,7 @@ org.gradle.jvmargs=-Xmx1536m -XX:+UseParallelGC android.injected.testOnly=false android.useAndroidX=true +android.disableAutomaticComponentCreation=true android.defaults.buildfeatures.buildconfig=false android.defaults.buildfeatures.aidl=false android.defaults.buildfeatures.shaders=false diff --git a/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt b/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt index 0ad3b155..d3b4cfc5 100644 --- a/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt +++ b/plugin/src/main/java/com/qmuiteam/plugin/Dep.kt @@ -18,7 +18,7 @@ object Dep { const val typeVer = "0.1.0" const val composeCoreVer = "0.0.1" const val composeVer = "0.0.1" - const val photoVer = "0.0.1" + const val photoVer = "0.0.1.1" } object Coroutines { diff --git a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDDialogFragment.kt b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDDialogFragment.kt index a3b8a38a..deaa6a0d 100644 --- a/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDDialogFragment.kt +++ b/qmuidemo/src/main/java/com/qmuiteam/qmuidemo/fragment/QDDialogFragment.kt @@ -194,6 +194,17 @@ class QDDialogFragment() : ComposeBaseFragment() { }.show() } } + + item { + QMUIItem( + title = "Toast", + drawBehind = { + drawBottomSeparator(insetStart = qmuiCommonHorSpace, insetEnd = qmuiCommonHorSpace) + } + ) { + view.qmuiToast("这只是个 Toast!") + } + } } } }