feat: QMUIToast

This commit is contained in:
cgspine
2022-03-16 17:57:06 +08:00
parent c65650cd8c
commit 46db4231dd
9 changed files with 235 additions and 41 deletions
@@ -18,6 +18,7 @@ val qmuiScrollAlphaChangeMaxOffset = 20.dp
val qmuiDialogVerEdgeProtectionMargin = 44.dp
val qmuiToastVerEdgeProtectionMargin = 96.dp
@@ -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<QMUIModal.Action>()
@@ -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 {
@@ -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)
}
}
@@ -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<Long, QMUIModal>()
}
@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)
}
}
}
@@ -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()
}
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="qmui_modals" type="id"/>
</resources>
+1
View File
@@ -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
@@ -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 {
@@ -194,6 +194,17 @@ class QDDialogFragment() : ComposeBaseFragment() {
}.show()
}
}
item {
QMUIItem(
title = "Toast",
drawBehind = {
drawBottomSeparator(insetStart = qmuiCommonHorSpace, insetEnd = qmuiCommonHorSpace)
}
) {
view.qmuiToast("这只是个 Toast!")
}
}
}
}
}