Repository: sunnyxibei/HenCoderPractice Branch: master Commit: da5c22cd5748 Files: 27 Total size: 34.1 KB Directory structure: gitextract_mu2re4hw/ ├── .gitignore ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── sunnyxibei/ │ │ └── hencoderpractice/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── sunnyxibei/ │ │ │ └── hencoderpractice/ │ │ │ ├── MainActivity.kt │ │ │ └── MapView.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_launcher_background.xml │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ └── activity_main.xml │ │ ├── menu/ │ │ │ └── menu_item.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── values/ │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test/ │ └── java/ │ └── com/ │ └── sunnyxibei/ │ └── hencoderpractice/ │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle .idea /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures .externalNativeBuild ================================================ FILE: README.md ================================================ # HenCoderPractice HenCoder 三维旋转动效的实现 ![demo gif](https://github.com/sunnyxibei/HenCoderPractice/blob/master/jpg/flipboard.gif?raw=true) >这是凯哥在 HenCoder 讲解几何变换章节提到的一个动画栗子,很炫酷,极大地激发了偶的好奇心。正好这个周末手头没啥事儿,研究了一下实现。 * 首先,拆解动效,把gif图片放到手机上,咔咔咔不停地截屏,截出一帧一帧的图片 * 然后,分析这些图片,可以发现,动效分为三部分: * 开始动画,一个Y轴旋转45度动效,看过hencoder教程的童鞋应该很轻松实现 * 中间动画,比较复杂,后面着重分解 * 结束动画,一个绕X轴旋转45度动效,看过hencoder教程的童鞋应该很轻松实现 重点分析中间动画,一帧一帧观察,基本判断是,先旋转canvas坐标系,再裁切图形,然后使用camera3D旋转,保存camera效果 `camera.applyToCanvas(canvas);`,然后再把canvas坐标系旋转回来。本来想画个图解释一下,奈何时间有限,你先仔细体会一下吧...... 实现中间动画,原理上面已经说了 ``` canvas.save(); camera.save(); canvas.translate(centerX, centerY); canvas.rotate(-degreeZ); camera.rotateY(degreeY); camera.applyToCanvas(canvas); //计算裁切参数时清注意,此时的canvas的坐标系已经移动 canvas.clipRect(0, -centerY, centerX, centerY); canvas.rotate(degreeZ); canvas.translate(-centerX, -centerY); camera.restore(); canvas.drawBitmap(bitmap, x, y, paint); canvas.restore(); ``` 划重点,需要注意: 1、Canvas的几何变换,顺序是倒序 2、clipRect执行时,canvas的坐标系已经被我们移动了,计算参数时要以移动后的坐标系计算 3、循环播放动效时,在执行动效前,要重置几个动效相关的参数值 4、第三个动效执行时,我们的canvas已经旋转了270度,所以此时的旋转

实际上

是绕Y轴旋转,而且旋转角度的正负也是反着的,你看下代码,再仔细体会一下 ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle.kts ================================================ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.plugin.compose") } android { namespace = "com.sunnyxibei.hencoderpractice" compileSdk = 35 defaultConfig { applicationId = "com.sunnyxibei.hencoderpractice" minSdk = 26 targetSdk = 35 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // Updated vectorDrawables { useSupportLibrary = true } } buildTypes { getByName("release") { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") } } buildFeatures { compose = true buildConfig = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.14" } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } } dependencies { implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) implementation("androidx.appcompat:appcompat:1.6.1") // Jetpack Compose BOM val composeBom = platform("androidx.compose:compose-bom:2024.05.00") implementation(composeBom) androidTestImplementation(composeBom) // Compose specific dependencies implementation("androidx.compose.ui:ui") implementation("androidx.compose.material3:material3") implementation("androidx.compose.ui:ui-tooling-preview") debugImplementation("androidx.compose.ui:ui-tooling") implementation("androidx.activity:activity-compose:1.9.0") // Test dependencies androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") { exclude(group = "com.android.support", module = "support-annotations") } androidTestImplementation("androidx.compose.ui:ui-test-junit4") testImplementation("junit:junit:4.12") } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in C:\Develop\Android\sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # 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 ================================================ FILE: app/src/androidTest/java/com/sunnyxibei/hencoderpractice/ExampleInstrumentedTest.kt ================================================ package com.sunnyxibei.hencoderpractice import android.support.test.runner.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith /** * 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.sunnyxibei.hencoderpractice", appContext.packageName) } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/sunnyxibei/hencoderpractice/MainActivity.kt ================================================ package com.sunnyxibei.hencoderpractice import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.graphics.BitmapFactory import android.os.Bundle import android.os.Handler import android.os.Looper import androidx.activity.ComponentActivity // Changed from AppCompatActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.* // Using Material 3 import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView // It's good practice to define a theme for Compose // For now, a basic MaterialTheme will be used. // Usually, this would be in a separate Theme.kt file. @Composable fun AppTheme(content: @Composable () -> Unit) { MaterialTheme( colorScheme = lightColorScheme(), // Or darkColorScheme() content = content ) } class MainActivity : ComponentActivity() { // Changed from AppCompatActivity private val handler = Handler(Looper.getMainLooper()) private var mapViewInstance: MapView? = null // To hold the MapView instance for animation control private var animatorSet: AnimatorSet? = null // To control the animator set override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AppTheme { MainScreen() } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen() { val context = LocalContext.current var showMenu by remember { mutableStateOf(false) } Scaffold( topBar = { TopAppBar( title = { Text(stringResource(id = R.string.app_name)) }, actions = { IconButton(onClick = { showMenu = !showMenu }) { Icon(Icons.Filled.MoreVert, contentDescription = "More options") } DropdownMenu( expanded = showMenu, onDismissRequest = { showMenu = false } ) { DropdownMenuItem( text = { Text("Google Map") }, onClick = { mapViewInstance?.setBitmap( BitmapFactory.decodeResource( context.resources, R.drawable.google_map ) ) showMenu = false } ) DropdownMenuItem( text = { Text("FlipBoard") }, onClick = { mapViewInstance?.setBitmap( BitmapFactory.decodeResource( context.resources, R.drawable.flip_board ) ) showMenu = false } ) } } ) } ) { paddingValues -> Box(modifier = Modifier.padding(paddingValues).fillMaxSize()) { MapViewComposable() } } } @Composable fun MapViewComposable() { AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> MapView(context).apply { mapViewInstance = this val animator1 = ObjectAnimator.ofFloat(this, "degreeY", 0f, -45f).apply { duration = 1000 startDelay = 500 } val animator2 = ObjectAnimator.ofFloat(this, "degreeZ", 0f, 270f).apply { duration = 800 startDelay = 500 } val animator3 = ObjectAnimator.ofFloat(this, "fixDegreeY", 0f, 30f).apply { duration = 500 startDelay = 500 } animatorSet = AnimatorSet().apply { addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) handler.postDelayed({ mapViewInstance?.let { if (!isDestroyed && !isFinishing) { it.reset() start() } } }, 500) } }) playSequentially(animator1, animator2, animator3) start() } } } ) } override fun onDestroy() { super.onDestroy() handler.removeCallbacksAndMessages(null) animatorSet?.cancel() } } ================================================ FILE: app/src/main/java/com/sunnyxibei/hencoderpractice/MapView.kt ================================================ package com.sunnyxibei.hencoderpractice import android.content.Context import android.graphics.* import android.graphics.drawable.BitmapDrawable import android.util.AttributeSet import android.view.View import androidx.annotation.Keep /** * 整个动画拆分成了三部分 * * Created by jiayuanbin on 2017-9-23. */ class MapView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { //Y轴方向旋转角度 @set:Keep var degreeY: Float = 0f set(value) { field = value invalidate() } //不变的那一半,Y轴方向旋转角度 @set:Keep var fixDegreeY: Float = 0f set(value) { field = value invalidate() } //Z轴方向(平面内)旋转的角度 @set:Keep var degreeZ: Float = 0f set(value) { field = value invalidate() } private val paint: Paint private var bitmap: Bitmap private val camera: Camera init { val a = context.obtainStyledAttributes(attrs, R.styleable.MapView) val drawable = a.getDrawable(R.styleable.MapView_mv_background) as? BitmapDrawable a.recycle() bitmap = drawable?.bitmap ?: BitmapFactory.decodeResource(resources, R.drawable.flip_board) paint = Paint(Paint.ANTI_ALIAS_FLAG) camera = Camera() val displayMetrics = resources.displayMetrics val newZ = -displayMetrics.density * 6 camera.setLocation(0f, 0f, newZ) // Use float values for camera } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val bitmapWidth = bitmap.width val bitmapHeight = bitmap.height val centerX = width / 2f // Use float for center calculations val centerY = height / 2f // Use float for center calculations val x = centerX - bitmapWidth / 2f val y = centerY - bitmapHeight / 2f //画变换的一半 //先旋转,再裁切,再使用camera执行3D动效,**然后保存camera效果**,最后再旋转回来 canvas.save() camera.save() canvas.translate(centerX, centerY) canvas.rotate(-degreeZ) camera.rotateY(degreeY) camera.applyToCanvas(canvas) //计算裁切参数时清注意,此时的canvas的坐标系已经移动 canvas.clipRect(0f, -centerY, centerX, centerY) // Use float values canvas.rotate(degreeZ) canvas.translate(-centerX, -centerY) camera.restore() canvas.drawBitmap(bitmap, x, y, paint) canvas.restore() //画不变换的另一半 canvas.save() camera.save() canvas.translate(centerX, centerY) canvas.rotate(-degreeZ) //计算裁切参数时清注意,此时的canvas的坐标系已经移动 canvas.clipRect(-centerX, -centerY, 0f, centerY) // Use float values //此时的canvas的坐标系已经旋转,所以这里是rotateY camera.rotateY(fixDegreeY) camera.applyToCanvas(canvas) canvas.rotate(degreeZ) canvas.translate(-centerX, -centerY) camera.restore() canvas.drawBitmap(bitmap, x, y, paint) canvas.restore() } /** * 启动动画之前调用,把参数reset到初始状态 */ fun reset() { degreeY = 0f fixDegreeY = 0f degreeZ = 0f invalidate() // Invalidate after resetting properties } fun setBitmap(bitmap: Bitmap) { this.bitmap = bitmap invalidate() } } ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_item.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/values/attrs.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #3F51B5 #303F9F #FF4081 ================================================ FILE: app/src/main/res/values/strings.xml ================================================ HenCoderPractice ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: app/src/test/java/com/sunnyxibei/hencoderpractice/ExampleUnitTest.kt ================================================ package com.sunnyxibei.hencoderpractice import org.junit.Assert.* import org.junit.Test /** * 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) } } ================================================ FILE: build.gradle.kts ================================================ allprojects { repositories { maven { url = uri("https://maven.aliyun.com/repository/public") } maven { url = uri("https://maven.aliyun.com/repository/central") } maven { url = uri("https://maven.aliyun.com/repository/google") } maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } google() mavenCentral() maven { url = uri("https://jitpack.io") } } } val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() rootProject.layout.buildDirectory.value(newBuildDir) subprojects { val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) project.layout.buildDirectory.value(newSubprojectBuildDir) } subprojects { project.evaluationDependsOn(":app") } tasks.register("clean") { delete(rootProject.layout.buildDirectory) } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Sat Sep 23 22:49:00 CST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip ================================================ FILE: gradle.properties ================================================ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true android.nonTransitiveRClass=true #set gradle jdk 17 org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle.kts ================================================ pluginManagement { buildscript { repositories { maven { url = uri("https://maven.aliyun.com/repository/public") } maven { url = uri("https://maven.aliyun.com/repository/central") } maven { url = uri("https://maven.aliyun.com/repository/google") } maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } mavenCentral() } } repositories { maven { url = uri("https://maven.aliyun.com/repository/public") } maven { url = uri("https://maven.aliyun.com/repository/central") } maven { url = uri("https://maven.aliyun.com/repository/google") } maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } google() mavenCentral() maven { url = uri("https://jitpack.io") } gradlePluginPortal() } } plugins { id("com.android.application") version "8.7.3" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false } rootProject.name = "HenCoderPractice" include(":app")