在Android开发领域,Groovy DSL (Domain Specific Language) 主要指的是使用Groovy语言编写的特定领域语言,特别指用于构建配置的Gradle脚本。自从Android Studio 3.0及更高版本以来,Google推荐使用Groovy DSL来编写Android项目的Gradle构建脚本,现在也引入了Kotlin DSL作为替代选项。
本文使用Groovy DSL来进行Gradle版本的升级,并且对其进行compose的环境搭建
更改Gradle分发包的下载URL
distributionUrl
属性是这个文件中最关键的一行配置,它的作用是指定Gradle分发包的下载URL。这个URL指向一个特定版本的Gradle二进制包,当你的项目需要使用Gradle进行构建时,如果本地没有对应的Gradle版本,Android Studio或命令行工具会根据distributionUrl
指定的地址自动下载该版本的Gradle。
下面使用官方的gradle下载链接,个人推荐还是使用国内镜像源的链接:
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
更新插件
在Project根目录下build.gradle中更改对应上面下载Gradle的Gradle插件,和添加跟下面要添加compose ui版本对应的kotlin版本,当然要注意Gradle版本7.0后url为http的要添加allowInsecureProtocol = true:
...............
buildscript{
repositories {
..........
maven {
url "http://xxxxxxxx.xxx"
allowInsecureProtocol = true
}
..........
}
dependencies {
........
classpath 'com.android.tools.build:gradle:7.1.3' // 使用支持Compose的Android Gradle插件版本
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10" // 使用支持Compose的Kotlin版本
........
}
}
添加kotlin环境
在Module目录下的build.gradle中添加kotlin插件,并且添加kotlin配置和compose支持。kotlinCompilerExtensionVersion版本跟上面添加的kotlin版本对应。因为要支持compose的环境,所以compileSdk也要升级到33以上:
apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'org.jetbrains.kotlin.plugin.parcelize'
android {
....................
compileSdk 34
buildFeatures {
compose true
}
kotlinOptions {
//这里使用jdk17版本
jvmTarget = "17"
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.3"
}
....................
}
要注意Gradle版本8.0后需要添加namespace,namespace跟模块名称一致:
android {
namespace = "com.alibaba.genie.chat.ai"
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
.....................
}
................
}
添加compose依赖
在Module目录下的build.gradle添加compose的依赖,下面进行的是最简单的compose框架依赖添加,在其中的
'androidx.compose:compose-bom:2023.08.00'是Jetpack Compose 使用物料清单:
dependencies {
//compose
def composeBom = platform('androidx.compose:compose-bom:2023.08.00')
implementation composeBom
androidTestImplementation composeBom
implementation 'androidx.compose.material3:material3:1.2.1'
}
不过也要注意material3里面已经包含了部分lifecycle的依赖引用,所以会跟例如下面之类的有冲突:
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
这是官方依赖清单:
dependencies {
def composeBom = platform('androidx.compose:compose-bom:2024.06.00')
implementation composeBom
androidTestImplementation composeBom
// Choose one of the following:
// Material Design 3
implementation 'androidx.compose.material3:material3'
// or Material Design 2
implementation 'androidx.compose.material:material'
// or skip Material Design and build directly on top of foundational components
implementation 'androidx.compose.foundation:foundation'
// or only import the main APIs for the underlying toolkit systems,
// such as input and measurement/layout
implementation 'androidx.compose.ui:ui'
// Android Studio Preview support
implementation 'androidx.compose.ui:ui-tooling-preview'
debugImplementation 'androidx.compose.ui:ui-tooling'
// UI Tests
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
// Optional - Included automatically by material, only add when you need
// the icons but not the material library (e.g. when using Material3 or a
// custom design system based on Foundation)
implementation 'androidx.compose.material:material-icons-core'
// Optional - Add full set of material icons
implementation 'androidx.compose.material:material-icons-extended'
// Optional - Add window size utils
implementation 'androidx.compose.material3:material3-window-size-class'
// Optional - Integration with activities
implementation 'androidx.activity:activity-compose:1.9.0'
// Optional - Integration with ViewModels
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'
// Optional - Integration with LiveData
implementation 'androidx.compose.runtime:runtime-livedata'
// Optional - Integration with RxJava
implementation 'androidx.compose.runtime:runtime-rxjava2'
}
附件
package com.alibaba.genie.chat.ai.ui.widget
import android.content.Context
import android.nfc.Tag
import android.text.TextUtils
import android.util.AttributeSet
import android.util.Log
import android.widget.FrameLayout
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.AnnotatedString.Range
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.sp
import androidx.lifecycle.MutableLiveData
import com.alibaba.genie.chat.ai.R
import kotlinx.coroutines.delay
class ComposePainterTextView: FrameLayout {
companion object {
const val TAG = "AiChatScreen"
}
private val text = MutableLiveData<String>("")
private val displayText = MutableLiveData<AnnotatedString>(AnnotatedString(""))
private val textList = mutableListOf<String>()
private var mHighlightStr = MutableLiveData<String>("")
private var isRealEnd = MutableLiveData<Boolean>(false)
private var mRange: Range<SpanStyle>? = null
private var textStyle: TextStyle = TextStyle(
fontSize = 30.sp
)
private var needPrinting: Boolean = true
private var needPausePrinting = MutableLiveData<Boolean>(false)
private val mIsPainting = MutableLiveData<Boolean>(false)
private val brush: Brush = Brush.linearGradient(
listOf(
Color.Blue,
Color.Red,
Color.Green
),
start = Offset(0f,0f),
end = Offset(1050f,0f),
TileMode.Clamp
)
private var listener: TextAnimationListener? = null
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.PrinterTextView)
val textColor = typedArray.getColor(R.styleable.PrinterTextView_textColor, resources.getColor(R.color.normalTextColor))
val textSize = typedArray.getDimension(R.styleable.PrinterTextView_textSize, 60f)
Log.i(TAG, "textColor: $textColor, textSize=$textSize")
textStyle = TextStyle(
color = Color(textColor),
fontSize = (textSize / 3).sp
)
typedArray.recycle()
initView(context)
}
private fun initView(context: Context) {
addView(
ComposeView(context).apply {
setContent {
val string = text.observeAsState()
val highlightStr = mHighlightStr.observeAsState()
val needPausePrinting by this@ComposePainterTextView.needPausePrinting.observeAsState()
TextBrushScreen(string.value ?:"", highlightStr.value ?:"", needPausePrinting ?:false)
}
}
)
}
fun setHighlightStr(highlightStr: String): Boolean {
if (mHighlightStr.value == highlightStr) {
Log.w(TAG, "setHighlightStr: return $highlightStr")
return false
}
mHighlightStr.value = highlightStr
updateHighlightSpan()
return true
}
fun setTextAnimationListener(listener: TextAnimationListener?) {
this.listener = listener
}
fun setNeedPrinting(needPrinting: Boolean) {
this.needPrinting = needPrinting
}
fun setPausePrinting(needPausePrinting: Boolean) {
Log.w(TAG, "setPausePrinting:${needPausePrinting}, text=${text.value}", )
this.needPausePrinting.value = needPausePrinting
}
fun setText(text: String, tag: String = "") {
Log.w(TAG, "setText: tag=$tag, text=${text}", )
this.textList.clear()
this.textList.add(text)
this.text.value = text
}
fun setText(textId: Int) {
try {
val text = resources.getText(textId)
Log.w(TAG, "setText: text=${text.toString()}", )
this.textList.clear()
this.textList.add(text.toString())
this.text.value = text.toString()
this.displayText.value = AnnotatedString(text.toString())
} catch (e: Exception) {
println()
return
}
}
fun setTextStyle(textStyle: TextStyle) {
this.textStyle = textStyle
}
fun setIsEnd(value : Boolean) {
Log.w(TAG, "setIsEnd: $value", )
isRealEnd.value = value
}
fun isPrintTaskEmpty(): Boolean {
return mIsPainting.value != true
}
fun clearText(): ComposePainterTextView {
textList.clear()
text.value = ""
displayText.value = AnnotatedString("")
mHighlightStr.value = ""
return this
}
private fun updateHighlightSpan() {
val highlightStr = mHighlightStr.value ?:return
var start = text.value?.indexOf(highlightStr) ?: return
Log.w(TAG, "updateHighlightSpan: start=${start},TextUtils.isEmpty(text.value)=${TextUtils.isEmpty(text.value)}", )
if (start < 0) {
if (!TextUtils.isEmpty(text.value) && mHighlightStr.value?.startsWith(text.value!!) == true) {
start = 0
} else {
appendTaskToQueue(highlightStr)
updateHighlightSpan()
return
}
}
Log.w(TAG, "setHighlightStr: highlightStr $highlightStr, start=${start}, end=${start + highlightStr.length},highlightStr.length=${highlightStr.length}")
mRange = Range(
SpanStyle(brush = brush),
start = start,
end = start + highlightStr.length
)
}
fun appendTaskToQueue(text: String) {
Log.w(TAG, "appendTaskToQueue: text=${text}", )
if (textList.contains(text)) {
return
} else {
textList.add(text)
this.text.value += text
needPrinting = true
needPausePrinting.value = false
}
}
@Composable
fun TextBrushScreen(text: String, highlightStr: String, needPausePrinting: Boolean) {
Box(modifier = Modifier.fillMaxSize()) {
// var printedText: AnnotatedString by remember { mutableStateOf(
// AnnotatedString("")
// ) }
val printedText by displayText.observeAsState(AnnotatedString(""))
var cursorText : AnnotatedString by remember { mutableStateOf(
AnnotatedString("")
) }
val isPainting by mIsPainting.observeAsState(initial = false)
LaunchedEffect(key1 = UInt) {
while (true) {
delay(200)
val needUpdateHighlight = if (mRange == null) {
false
} else if (printedText.text.length < mRange!!.start || printedText.text.length > mRange!!.end) {
false
} else {
true
}
val string = if ((isPainting || (isRealEnd.value != true && needPrinting)) && cursorText.text == " ") {
"__"
} else {
" "
}
cursorText = buildAnnotatedString {
if (needUpdateHighlight) {
withStyle(
SpanStyle(brush)
) {
append(string)
}
} else {
append(string)
}
}
}
}
LaunchedEffect(key1 = text, highlightStr, needPausePrinting) {
if (TextUtils.isEmpty(text)) {
displayText.value = AnnotatedString("")
return@LaunchedEffect
}
mIsPainting.value = true
text.forEachIndexed { index, char ->
val needUpdateText = printedText.length < index + 1 && !needPausePrinting
val needUpdateHighlight = (printedText.spanStyles.isNotEmpty() && !printedText.spanStyles.contains(mRange)) || (printedText.spanStyles.isEmpty() && mRange !=null )
val string = if (needUpdateText) {
printedText.text + char
} else {
printedText.text
}
if (needUpdateHighlight || needUpdateText) {
displayText.value = buildAnnotatedString{
if (mRange == null || mRange!!.start > index){
// Log.w(TAG, "mRange == null || mRange!!.start > index, index=$index, mRange!!.start=${mRange?.start}")
append(string)
}else if (mRange!!.start <= index) {
val start = mRange!!.start
val end = if (mRange!!.end > string.length) {
string.length
} else {
mRange!!.end
}
// Log.w(TAG, "TextBrushScreen: index=${index}, string.length=${string.length}, start=${start}, mRange?.end=${mRange?.end}, end=${end}, needPausePrinting=${needPausePrinting}")
if ( start <= string.length && end <= string.length) {
if (start > 0) {
append(string.substring(0, start))
}
withStyle(
SpanStyle(brush)
){
append(string.substring(start, end))
}
append(string.substring(end, string.length))
}
} else {
// Log.w(TAG, "TextBrushScreen: index=${index}, mRange?.start=${mRange?.start}, mRange?.end=${mRange?.end}")
}
}
if (needUpdateText && needPrinting) {
delay(100)
}
}
}
mIsPainting.value = false
listener?.animationFinish()
}
// Log.w(TAG, "TextBrushScreen: text=${text}", )
Text(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center),
text = printedText + cursorText,
style = textStyle
)
}
}
interface TextAnimationListener {
fun animationFinish()
}
}
给旧Activity添加生命周期
添加依赖
//compose
def composeBom = platform('androidx.compose:compose-bom:2023.08.00')
implementation composeBom
androidTestImplementation composeBom
implementation 'androidx.compose.material3:material3:1.2.1'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
def lifecycle_version = "2.7.0"
implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
代码:
package com.cocos.game.base
import android.app.Activity
import android.os.Bundle
import android.view.View
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.cocos.lib.CocosActivity
class BaseCocosActivity: Activity(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner{
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
private val savedStateRegistryController = SavedStateRegistryController.create(this)
private val store = ViewModelStore()
override val lifecycle: Lifecycle
get() = lifecycleRegistry
override val savedStateRegistry: SavedStateRegistry
get() = savedStateRegistryController.savedStateRegistry
override val viewModelStore: ViewModelStore
get() = store
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedStateRegistryController.performRestore(null)
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun onStart() {
super.onStart()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
}
override fun onResume() {
super.onResume()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
override fun onPause() {
super.onPause()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
}
override fun onStop() {
super.onStop()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
override fun onDestroy() {
super.onDestroy()
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
store.clear()
}
/**
* Compose uses the Window's decor view to locate the
* Lifecycle/ViewModel/SavedStateRegistry owners.
* Therefore, we need to set this class as the "owner" for the decor view.
*/
fun attachToDecorView(decorView: View?) {
decorView?.let {
it.setViewTreeViewModelStoreOwner(this)
it.setViewTreeLifecycleOwner(this)
it.setViewTreeSavedStateRegistryOwner(this)
} ?: return
}
}
完毕