mirror of
https://github.com/Tencent/QMUI_Android.git
synced 2026-10-17 04:31:16 +08:00
feat: QMUIToast
This commit is contained in:
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="qmui_modals" type="id"/>
|
||||
</resources>
|
||||
@@ -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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user