feat: compose init and modal and flipper.

This commit is contained in:
cgspine
2021-12-17 14:40:46 +08:00
parent b00dd5d57c
commit e2595fc444
17 changed files with 657 additions and 4 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ buildscript {
mavenLocal()
}
dependencies {
classpath("com.android.tools.build:gradle:7.0.3")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
classpath("com.android.tools.build:gradle:7.0.4")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.0")
}
}
+1
View File
@@ -0,0 +1 @@
/build
+79
View File
@@ -0,0 +1,79 @@
import com.qmuiteam.plugin.Dep
import java.io.FileInputStream
import java.util.*
plugins {
id("com.android.library")
kotlin("android")
`maven-publish`
}
android {
compileSdk = Dep.compileSdk
buildFeatures {
compose = true
}
defaultConfig {
minSdk = Dep.minSdk
targetSdk = Dep.targetSdk
}
buildTypes {
getByName("release"){
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = Dep.javaVersion
targetCompatibility = Dep.javaVersion
}
kotlinOptions {
jvmTarget = Dep.kotlinJvmTarget
}
composeOptions {
kotlinCompilerExtensionVersion = Dep.Compose.version
}
}
dependencies {
api(Dep.AndroidX.annotation)
api(Dep.AndroidX.appcompat)
api(Dep.AndroidX.coreKtx)
api(Dep.Compose.ui)
api(Dep.Compose.animation)
api(Dep.Compose.material)
api(Dep.Compose.compiler)
}
afterEvaluate {
val file = rootProject.file("gradle/deploy.properties")
if(file.exists()) {
val properties = Properties()
properties.load(FileInputStream(file))
publishing {
publications {
create<MavenPublication>("release") {
from(components.getByName("release"))
groupId = Dep.QMUI.group
artifactId = project.name
version = Dep.QMUI.composeVer
}
}
repositories {
maven {
setUrl(properties.getProperty("maven.url"))
credentials {
username = properties.getProperty("maven.username")
password = properties.getProperty("maven.password")
}
}
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.qmuiteam.compose
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.qmuiteam.compose", appContext.packageName)
}
}
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.qmuiteam.compose">
</manifest>
@@ -0,0 +1,188 @@
package com.qmuiteam.compose.modal
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.OnBackPressedCallback
import androidx.activity.OnBackPressedDispatcher
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.isVisible
import kotlinx.coroutines.flow.MutableStateFlow
internal abstract class QMUIModalPresent(
private val rootLayout: FrameLayout,
private val onBackPressedDispatcher: OnBackPressedDispatcher,
val mask: Color = DefaultMaskColor,
val systemCancellable: Boolean = true,
val maskCancellable: Boolean = true
) : QMUIModal {
private val onShowListeners = arrayListOf<QMUIModal.Action>()
private val onDismissListeners = arrayListOf<QMUIModal.Action>()
private val visibleFlow = MutableStateFlow(false)
private var isShown = false
private var isDismissing = false
private val composeLayout = ComposeView(rootLayout.context).apply {
visibility = View.GONE
}
private val onBackPressedCallback = object : OnBackPressedCallback(systemCancellable) {
override fun handleOnBackPressed() {
dismiss()
}
}
init {
composeLayout.setContent {
Box(modifier = Modifier.fillMaxSize()) {
val visible by visibleFlow.collectAsState(initial = false)
ModalContent(visible = visible) {
if (isDismissing) {
doAfterDismiss()
}
}
}
}
}
private fun doAfterDismiss() {
isDismissing = false
composeLayout.isVisible = false
composeLayout.disposeComposition()
rootLayout.removeView(composeLayout)
onBackPressedCallback.remove()
}
@Composable
abstract fun ModalContent(visible: Boolean, dismissFinishAction: () -> Unit)
override fun isShowing(): Boolean {
return isShown
}
override fun show() {
if (isShown || isDismissing) {
return
}
isShown = true
rootLayout.addView(composeLayout, generateLayoutParams())
composeLayout.visibility = View.VISIBLE
visibleFlow.value = true
onBackPressedDispatcher.addCallback(onBackPressedCallback)
}
open fun generateLayoutParams(): FrameLayout.LayoutParams {
return FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
override fun dismiss() {
if (!isShown) {
return
}
isShown = false
isDismissing = true
visibleFlow.value = false
}
override fun doOnShow(listener: QMUIModal.Action) {
onShowListeners.add(listener)
}
override fun doOnDismiss(listener: QMUIModal.Action) {
onDismissListeners.add(listener)
}
override fun removeOnShowAction(listener: QMUIModal.Action) {
onShowListeners.remove(listener)
}
override fun removeOnDismissAction(listener: QMUIModal.Action) {
onDismissListeners.remove(listener)
}
}
internal class StillModalImpl(
rootLayout: FrameLayout,
onBackPressedDispatcher: OnBackPressedDispatcher,
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskCancellable: Boolean = true,
val content: @Composable (modal: QMUIModal) -> Unit
) : QMUIModalPresent(rootLayout, onBackPressedDispatcher, mask, systemCancellable, maskCancellable) {
@Composable
override fun ModalContent(visible: Boolean, dismissFinishAction: () -> Unit) {
if (visible) {
var modifier = Modifier
.fillMaxSize()
.background(mask)
if (maskCancellable) {
modifier = modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
dismiss()
}
}
Box(modifier = modifier)
} else {
dismissFinishAction()
}
}
}
internal class AnimateModalImpl(
rootLayout: FrameLayout,
onBackPressedDispatcher: OnBackPressedDispatcher,
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskCancellable: Boolean = true,
val content: @Composable AnimatedVisibilityScope.(modal: QMUIModal) -> Unit
) : QMUIModalPresent(rootLayout, onBackPressedDispatcher, mask, systemCancellable, maskCancellable) {
@Composable
override fun ModalContent(visible: Boolean, dismissFinishAction: () -> Unit) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(300), 0f),
exit = fadeOut(tween(300), 0f)
) {
var modifier = Modifier
.fillMaxSize()
.background(mask)
if (maskCancellable) {
modifier = modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
dismiss()
}
}
Box(modifier = modifier)
content(this@AnimateModalImpl)
DisposableEffect("") {
onDispose {
dismissFinishAction()
}
}
}
}
}
@@ -0,0 +1,156 @@
package com.qmuiteam.compose.modal
import android.view.View
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
val DefaultDialogPaddingHor = 20.dp
@Composable
fun QMUIDialog(
modal: QMUIModal,
horEdge: Dp = 20.dp,
verEdge: Dp = 20.dp,
widthLimit: Dp = 360.dp,
radius: Dp = 12.dp,
background: Color = Color.White,
content: @Composable (QMUIModal) -> Unit
) {
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = horEdge, vertical = verEdge),
contentAlignment = Alignment.Center
) {
var modifier = if (widthLimit < maxWidth) {
Modifier.width(widthLimit)
} else {
Modifier.fillMaxWidth()
}
if (radius > 0.dp) {
modifier = modifier.clip(RoundedCornerShape(radius))
}
modifier = modifier
.background(background)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) { }
Box(modifier = modifier) {
content(modal)
}
}
}
@Composable
fun QMUIDialogTitle(
text: String,
fontSize: TextUnit = 15.sp,
textAlign: TextAlign? = null,
color: Color = Color.Black,
fontWeight: FontWeight? = FontWeight.Bold,
fontFamily: FontFamily? = null,
maxLines: Int = Int.MAX_VALUE,
lineHeight: TextUnit = 20.sp,
) {
Text(
text = text,
modifier = Modifier
.fillMaxWidth()
.padding(
top = 24.dp,
start = DefaultDialogPaddingHor,
end = DefaultDialogPaddingHor,
),
textAlign = textAlign,
color = color,
fontSize = fontSize,
fontWeight = fontWeight,
fontFamily = fontFamily,
maxLines = maxLines,
lineHeight = lineHeight
)
}
@Composable
fun QMUIDialogMessageContent(
text: String,
fontSize: TextUnit = 14.sp,
textAlign: TextAlign? = null,
color: Color = Color.Black,
fontWeight: FontWeight? = FontWeight.Bold,
fontFamily: FontFamily? = null,
maxLines: Int = Int.MAX_VALUE,
lineHeight: TextUnit = 16.sp,
) {
Text(
text = text,
modifier = Modifier
.fillMaxWidth()
.padding(
start = DefaultDialogPaddingHor,
end = DefaultDialogPaddingHor,
top = 16.dp,
bottom = 24.dp
),
textAlign = textAlign,
color = color,
fontSize = fontSize,
fontWeight = fontWeight,
fontFamily = fontFamily,
maxLines = maxLines,
lineHeight = lineHeight
)
}
fun View.qmuiDialog(
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskCancellable: Boolean = true,
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
horEdge: Dp = 20.dp,
verEdge: Dp = 20.dp,
widthLimit: Dp = 360.dp,
radius: Dp = 12.dp,
background: Color = Color.White,
content: @Composable (QMUIModal) -> Unit
): QMUIModal {
return qmuiModal(mask, systemCancellable, maskCancellable, modalHostProvider) { modal ->
QMUIDialog(modal, horEdge, verEdge, widthLimit, radius, background, content)
}
}
fun View.qmuiStillDialog(
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskCancellable: Boolean = true,
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
horEdge: Dp = 20.dp,
verEdge: Dp = 20.dp,
widthLimit: Dp = 360.dp,
radius: Dp = 12.dp,
background: Color = Color.White,
content: @Composable (QMUIModal) -> Unit
): QMUIModal {
return qmuiStillModal(mask, systemCancellable, maskCancellable, modalHostProvider) { modal ->
QMUIDialog(modal, horEdge, verEdge, widthLimit, radius, background, content)
}
}
@@ -0,0 +1,109 @@
package com.qmuiteam.compose.modal
import android.view.View
import android.view.Window
import android.widget.FrameLayout
import androidx.activity.OnBackPressedDispatcher
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
val DefaultMaskColor = Color.Black.copy(alpha = 0.5f)
private class ModalHolder(var current: QMUIModal? = null)
@Composable
fun QMUIModal(
isVisible: Boolean,
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskCancellable: Boolean = true,
doOnShow: QMUIModal.Action? = null,
doOnDismiss: QMUIModal.Action? = null,
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
content: @Composable AnimatedVisibilityScope.(QMUIModal) -> Unit
) {
val modalHolder = remember {
ModalHolder(null)
}
if (isVisible) {
if (modalHolder.current == null) {
val modal = LocalView.current.qmuiModal(mask, systemCancellable, maskCancellable, modalHostProvider, content)
doOnShow?.let { modal.doOnShow(it) }
doOnDismiss?.let { modal.doOnDismiss(it) }
modalHolder.current = modal
}
} else {
modalHolder.current?.dismiss()
}
DisposableEffect("") {
object : DisposableEffectResult {
override fun dispose() {
modalHolder.current?.dismiss()
}
}
}
}
interface QMUIModal {
fun show()
fun dismiss()
fun isShowing(): Boolean
fun doOnShow(listener: Action)
fun doOnDismiss(listener: Action)
fun removeOnShowAction(listener: Action)
fun removeOnDismissAction(listener: Action)
fun interface Action {
fun invoke(modal: QMUIModal)
}
}
fun interface ModalHostProvider {
fun provide(view: View): Pair<FrameLayout, OnBackPressedDispatcher>
}
class ActivityHostModalProvider : ModalHostProvider {
override fun provide(view: View): Pair<FrameLayout, OnBackPressedDispatcher> {
val contentLayout =
view.rootView.findViewById<FrameLayout>(Window.ID_ANDROID_CONTENT) ?: throw RuntimeException("View is not attached to Activity")
val activity = contentLayout.context as? AppCompatActivity ?: throw RuntimeException("view's rootView's context is not AppCompatActivity")
return contentLayout to activity.onBackPressedDispatcher
}
}
val DefaultModalHostProvider = ActivityHostModalProvider()
fun View.qmuiModal(
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskCancellable: Boolean = true,
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
content: @Composable AnimatedVisibilityScope.(QMUIModal) -> Unit
): QMUIModal {
if (!isAttachedToWindow) {
throw RuntimeException("View is not attached to window")
}
val modalHost = modalHostProvider.provide(this)
return AnimateModalImpl(modalHost.first, modalHost.second, mask, systemCancellable, maskCancellable, content)
}
fun View.qmuiStillModal(
mask: Color = DefaultMaskColor,
systemCancellable: Boolean = true,
maskCancellable: Boolean = true,
modalHostProvider: ModalHostProvider = DefaultModalHostProvider,
content: @Composable (QMUIModal) -> Unit
): QMUIModal {
if (!isAttachedToWindow) {
throw RuntimeException("View is not attached to window")
}
val modalHost = modalHostProvider.provide(this)
return StillModalImpl(modalHost.first, modalHost.second, mask, systemCancellable, maskCancellable, content)
}
@@ -0,0 +1,17 @@
package com.qmuiteam.compose
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
plugins {
`java-gradle-plugin`
idea
kotlin("jvm") version "1.5.31"
kotlin("jvm") version "1.6.0"
}
View File
@@ -6,7 +6,6 @@ object Dep {
val javaVersion = JavaVersion.VERSION_11
const val kotlinJvmTarget = "11"
const val kotlinVer = "1.5.31"
const val compileSdk = 31
const val minSdk = 21
const val targetSdk = 31
@@ -17,6 +16,7 @@ object Dep {
const val qmuiVer = "2.0.1"
const val archVer = "2.0.1"
const val typeVer = "0.0.14"
const val composeVer = "0.0.1"
}
object AndroidX {
@@ -28,6 +28,20 @@ object Dep {
val fragment = "androidx.fragment:fragment:1.4.0"
}
object Compose {
val version = "1.1.0-rc01"
val animation = "androidx.compose.animation:animation:$version"
val ui = "androidx.compose.ui:ui:$version"
val material = "androidx.compose.material:material:$version"
val compiler = "androidx.compose.compiler:compiler:$version"
}
object Flipper {
private const val version = "0.96.1"
const val soLoader = "com.facebook.soloader:soloader:0.10.1"
const val flipper = "com.facebook.flipper:flipper:$version"
}
object MaterialDesign {
const val material = "com.google.android.material:material:1.4.0"
}
+11
View File
@@ -43,6 +43,14 @@ android {
targetCompatibility = Dep.javaVersion
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = Dep.Compose.version
}
defaultConfig {
applicationId = "com.qmuiteam.qmuidemo"
minSdk = Dep.minSdk
@@ -70,6 +78,9 @@ dependencies {
implementation(project(":qmui"))
implementation(project(":arch"))
implementation(project(":type"))
implementation(project(":compose"))
implementation(Dep.Flipper.soLoader)
implementation(Dep.Flipper.flipper)
kapt(project(":compiler"))
kapt(project(":arch-compiler"))
}
@@ -24,6 +24,11 @@ import android.util.Log;
import androidx.annotation.NonNull;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.core.FlipperClient;
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
import com.facebook.soloader.SoLoader;
import com.qmuiteam.qmui.QMUILog;
import com.qmuiteam.qmui.arch.QMUISwipeBackActivityManager;
import com.qmuiteam.qmui.qqface.QMUIQQFaceCompiler;
@@ -78,6 +83,11 @@ public class QDApplication extends Application {
QMUISwipeBackActivityManager.init(this);
QMUIQQFaceCompiler.setDefaultQQFaceManager(QDQQFaceManager.getInstance());
QDSkinManager.install(this);
SoLoader.init(this, false);
FlipperClient client = AndroidFlipperClient.getInstance(this);
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
client.start();
}
@Override
@@ -17,8 +17,15 @@ package com.qmuiteam.qmuidemo.fragment.components
import android.view.LayoutInflater
import android.view.View
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.ui.Modifier
import butterknife.BindView
import butterknife.ButterKnife
import com.qmuiteam.compose.modal.QMUIDialogMessageContent
import com.qmuiteam.compose.modal.QMUIDialogTitle
import com.qmuiteam.compose.modal.qmuiDialog
import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord
import com.qmuiteam.qmui.arch.effect.MapEffect
import com.qmuiteam.qmui.kotlin.onClick
import com.qmuiteam.qmui.kotlin.skin
@@ -30,6 +37,7 @@ import com.qmuiteam.qmuidemo.lib.annotation.Widget
import com.qmuiteam.qmuidemo.manager.QDDataManager
import com.qmuiteam.qmuidemo.model.QDItemDescription
@LatestVisitRecord
@Widget(name = "QMUIRoundButton", iconRes = R.mipmap.icon_grid_button)
class QDButtonFragment : BaseFragment() {
@BindView(R.id.topbar)
@@ -56,6 +64,15 @@ class QDButtonFragment : BaseFragment() {
private fun initTopBar() {
mTopBar.addLeftBackImageButton().onClick { popBackStack() }
mTopBar.addRightTextButton("test", View.generateViewId()).onClick {
it.qmuiDialog {
Column(modifier = Modifier.fillMaxWidth()) {
QMUIDialogTitle(text = "这是标题")
QMUIDialogMessageContent(text = "这是一点点有意思的内容")
}
}.show()
}
mTopBar.setTitle(mQDItemDescription.name)
+1
View File
@@ -6,6 +6,7 @@ include(":arch")
include(":arch-compiler")
include(":arch-annotation")
include(":type")
include(":compose")
includeBuild("./plugin")