[
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n.idea\n/local.properties\n/.idea/workspace.xml\n/.idea/libraries\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n"
  },
  {
    "path": "README.md",
    "content": "# HenCoderPractice\n\nHenCoder 三维旋转动效的实现\n\n![demo gif](https://github.com/sunnyxibei/HenCoderPractice/blob/master/jpg/flipboard.gif?raw=true)\n\n>这是凯哥在 HenCoder 讲解几何变换章节提到的一个动画栗子，很炫酷，极大地激发了偶的好奇心。正好这个周末手头没啥事儿，研究了一下实现。\n\n* 首先，拆解动效，把gif图片放到手机上，咔咔咔不停地截屏，截出一帧一帧的图片\n* 然后，分析这些图片，可以发现，动效分为三部分：\n  * 开始动画，一个Y轴旋转45度动效，看过hencoder教程的童鞋应该很轻松实现\n  * 中间动画，比较复杂，后面着重分解\n  * 结束动画，一个绕X轴旋转45度动效，看过hencoder教程的童鞋应该很轻松实现\n\n重点分析中间动画，一帧一帧观察，基本判断是，先旋转canvas坐标系，再裁切图形，然后使用camera3D旋转，保存camera效果 `camera.applyToCanvas(canvas);`，然后再把canvas坐标系旋转回来。本来想画个图解释一下，奈何时间有限，你先仔细体会一下吧......\n\n实现中间动画，原理上面已经说了\n```\n\t\tcanvas.save();\n\t    camera.save();\n\t    canvas.translate(centerX, centerY);\n\t    canvas.rotate(-degreeZ);\n\t    camera.rotateY(degreeY);\n\t    camera.applyToCanvas(canvas);\n\t    //计算裁切参数时清注意，此时的canvas的坐标系已经移动\n\t    canvas.clipRect(0, -centerY, centerX, centerY);\n\t    canvas.rotate(degreeZ);\n\t    canvas.translate(-centerX, -centerY);\n\t    camera.restore();\n\t    canvas.drawBitmap(bitmap, x, y, paint);\n\t    canvas.restore();\n```\n划重点，需要注意：\n\n\t1、Canvas的几何变换，顺序是倒序\n\t2、clipRect执行时，canvas的坐标系已经被我们移动了，计算参数时要以移动后的坐标系计算\n\t3、循环播放动效时，在执行动效前，要重置几个动效相关的参数值\n\t4、第三个动效执行时，我们的canvas已经旋转了270度，所以此时的旋转<h2>实际上</h2>是绕Y轴旋转，而且旋转角度的正负也是反着的，你看下代码，再仔细体会一下\n\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "app/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.application\")\n    id(\"org.jetbrains.kotlin.android\")\n    id(\"org.jetbrains.kotlin.plugin.compose\")\n}\n\nandroid {\n    namespace = \"com.sunnyxibei.hencoderpractice\"\n    compileSdk = 35\n\n    defaultConfig {\n        applicationId = \"com.sunnyxibei.hencoderpractice\"\n        minSdk = 26\n        targetSdk = 35\n        versionCode = 1\n        versionName = \"1.0\"\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\" // Updated\n        vectorDrawables {\n            useSupportLibrary = true\n        }\n    }\n\n    buildTypes {\n        getByName(\"release\") {\n            isMinifyEnabled = false\n            proguardFiles(getDefaultProguardFile(\"proguard-android.txt\"), \"proguard-rules.pro\")\n        }\n    }\n\n    buildFeatures {\n        compose = true\n        buildConfig = true\n    }\n    composeOptions {\n        kotlinCompilerExtensionVersion = \"1.5.14\"\n    }\n    packaging {\n        resources {\n            excludes += \"/META-INF/{AL2.0,LGPL2.1}\"\n        }\n    }\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n    kotlinOptions {\n        jvmTarget = JavaVersion.VERSION_17.toString()\n    }\n}\n\ndependencies {\n    implementation(fileTree(mapOf(\"dir\" to \"libs\", \"include\" to listOf(\"*.jar\"))))\n    implementation(\"androidx.appcompat:appcompat:1.6.1\")\n\n    // Jetpack Compose BOM\n    val composeBom = platform(\"androidx.compose:compose-bom:2024.05.00\")\n    implementation(composeBom)\n    androidTestImplementation(composeBom)\n\n    // Compose specific dependencies\n    implementation(\"androidx.compose.ui:ui\")\n    implementation(\"androidx.compose.material3:material3\")\n    implementation(\"androidx.compose.ui:ui-tooling-preview\")\n    debugImplementation(\"androidx.compose.ui:ui-tooling\")\n    implementation(\"androidx.activity:activity-compose:1.9.0\")\n\n    // Test dependencies\n    androidTestImplementation(\"androidx.test.espresso:espresso-core:3.5.1\") {\n        exclude(group = \"com.android.support\", module = \"support-annotations\")\n    }\n    androidTestImplementation(\"androidx.compose.ui:ui-test-junit4\")\n    testImplementation(\"junit:junit:4.12\")\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in C:\\Develop\\Android\\sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "app/src/androidTest/java/com/sunnyxibei/hencoderpractice/ExampleInstrumentedTest.kt",
    "content": "package com.sunnyxibei.hencoderpractice\n\nimport android.support.test.runner.AndroidJUnit4\nimport androidx.test.platform.app.InstrumentationRegistry\nimport org.junit.Assert.*\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"com.sunnyxibei.hencoderpractice\", appContext.packageName)\n    }\n}\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\">\n        <activity\n            android:name=\".MainActivity\"\n            android:enabled=\"true\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/com/sunnyxibei/hencoderpractice/MainActivity.kt",
    "content": "package com.sunnyxibei.hencoderpractice\n\nimport android.animation.Animator\nimport android.animation.AnimatorListenerAdapter\nimport android.animation.AnimatorSet\nimport android.animation.ObjectAnimator\nimport android.graphics.BitmapFactory\nimport android.os.Bundle\nimport android.os.Handler\nimport android.os.Looper\nimport androidx.activity.ComponentActivity // Changed from AppCompatActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.MoreVert\nimport androidx.compose.material3.* // Using Material 3\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.viewinterop.AndroidView\n\n// It's good practice to define a theme for Compose\n// For now, a basic MaterialTheme will be used.\n// Usually, this would be in a separate Theme.kt file.\n@Composable\nfun AppTheme(content: @Composable () -> Unit) {\n    MaterialTheme(\n        colorScheme = lightColorScheme(), // Or darkColorScheme()\n        content = content\n    )\n}\n\nclass MainActivity : ComponentActivity() { // Changed from AppCompatActivity\n\n    private val handler = Handler(Looper.getMainLooper())\n    private var mapViewInstance: MapView? = null // To hold the MapView instance for animation control\n    private var animatorSet: AnimatorSet? = null // To control the animator set\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        setContent {\n            AppTheme {\n                MainScreen()\n            }\n        }\n    }\n\n    @OptIn(ExperimentalMaterial3Api::class)\n    @Composable\n    fun MainScreen() {\n        val context = LocalContext.current\n        var showMenu by remember { mutableStateOf(false) }\n\n        Scaffold(\n            topBar = {\n                TopAppBar(\n                    title = { Text(stringResource(id = R.string.app_name)) },\n                    actions = {\n                        IconButton(onClick = { showMenu = !showMenu }) {\n                            Icon(Icons.Filled.MoreVert, contentDescription = \"More options\")\n                        }\n                        DropdownMenu(\n                            expanded = showMenu,\n                            onDismissRequest = { showMenu = false }\n                        ) {\n                            DropdownMenuItem(\n                                text = { Text(\"Google Map\") },\n                                onClick = {\n                                    mapViewInstance?.setBitmap(\n                                        BitmapFactory.decodeResource(\n                                            context.resources,\n                                            R.drawable.google_map\n                                        )\n                                    )\n                                    showMenu = false\n                                }\n                            )\n                            DropdownMenuItem(\n                                text = { Text(\"FlipBoard\") },\n                                onClick = {\n                                    mapViewInstance?.setBitmap(\n                                        BitmapFactory.decodeResource(\n                                            context.resources,\n                                            R.drawable.flip_board\n                                        )\n                                    )\n                                    showMenu = false\n                                }\n                            )\n                        }\n                    }\n                )\n            }\n        ) { paddingValues ->\n            Box(modifier = Modifier.padding(paddingValues).fillMaxSize()) {\n                MapViewComposable()\n            }\n        }\n    }\n\n    @Composable\n    fun MapViewComposable() {\n        AndroidView(\n            modifier = Modifier.fillMaxSize(),\n            factory = { context ->\n                MapView(context).apply {\n                    mapViewInstance = this\n                    val animator1 = ObjectAnimator.ofFloat(this, \"degreeY\", 0f, -45f).apply {\n                        duration = 1000\n                        startDelay = 500\n                    }\n                    val animator2 = ObjectAnimator.ofFloat(this, \"degreeZ\", 0f, 270f).apply {\n                        duration = 800\n                        startDelay = 500\n                    }\n                    val animator3 = ObjectAnimator.ofFloat(this, \"fixDegreeY\", 0f, 30f).apply {\n                        duration = 500\n                        startDelay = 500\n                    }\n\n                    animatorSet = AnimatorSet().apply {\n                        addListener(object : AnimatorListenerAdapter() {\n                            override fun onAnimationEnd(animation: Animator) {\n                                super.onAnimationEnd(animation)\n                                handler.postDelayed({\n                                    mapViewInstance?.let {\n                                        if (!isDestroyed && !isFinishing) {\n                                            it.reset()\n                                            start()\n                                        }\n                                    }\n                                }, 500)\n                            }\n                        })\n                        playSequentially(animator1, animator2, animator3)\n                        start()\n                    }\n                }\n            }\n        )\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        handler.removeCallbacksAndMessages(null)\n        animatorSet?.cancel()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/sunnyxibei/hencoderpractice/MapView.kt",
    "content": "package com.sunnyxibei.hencoderpractice\n\nimport android.content.Context\nimport android.graphics.*\nimport android.graphics.drawable.BitmapDrawable\nimport android.util.AttributeSet\nimport android.view.View\nimport androidx.annotation.Keep\n\n/**\n * 整个动画拆分成了三部分\n *\n * Created by jiayuanbin on 2017-9-23.\n */\nclass MapView @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0\n) : View(context, attrs, defStyleAttr) {\n\n    //Y轴方向旋转角度\n    @set:Keep\n    var degreeY: Float = 0f\n        set(value) {\n            field = value\n            invalidate()\n        }\n\n    //不变的那一半，Y轴方向旋转角度\n    @set:Keep\n    var fixDegreeY: Float = 0f\n        set(value) {\n            field = value\n            invalidate()\n        }\n\n    //Z轴方向（平面内）旋转的角度\n    @set:Keep\n    var degreeZ: Float = 0f\n        set(value) {\n            field = value\n            invalidate()\n        }\n\n    private val paint: Paint\n    private var bitmap: Bitmap\n    private val camera: Camera\n\n    init {\n        val a = context.obtainStyledAttributes(attrs, R.styleable.MapView)\n        val drawable = a.getDrawable(R.styleable.MapView_mv_background) as? BitmapDrawable\n        a.recycle()\n\n        bitmap = drawable?.bitmap ?: BitmapFactory.decodeResource(resources, R.drawable.flip_board)\n        paint = Paint(Paint.ANTI_ALIAS_FLAG)\n        camera = Camera()\n\n        val displayMetrics = resources.displayMetrics\n        val newZ = -displayMetrics.density * 6\n        camera.setLocation(0f, 0f, newZ) // Use float values for camera\n    }\n\n    override fun onDraw(canvas: Canvas) {\n        super.onDraw(canvas)\n\n        val bitmapWidth = bitmap.width\n        val bitmapHeight = bitmap.height\n        val centerX = width / 2f // Use float for center calculations\n        val centerY = height / 2f // Use float for center calculations\n        val x = centerX - bitmapWidth / 2f\n        val y = centerY - bitmapHeight / 2f\n\n        //画变换的一半\n        //先旋转，再裁切，再使用camera执行3D动效,**然后保存camera效果**,最后再旋转回来\n        canvas.save()\n        camera.save()\n        canvas.translate(centerX, centerY)\n        canvas.rotate(-degreeZ)\n        camera.rotateY(degreeY)\n        camera.applyToCanvas(canvas)\n        //计算裁切参数时清注意，此时的canvas的坐标系已经移动\n        canvas.clipRect(0f, -centerY, centerX, centerY) // Use float values\n        canvas.rotate(degreeZ)\n        canvas.translate(-centerX, -centerY)\n        camera.restore()\n        canvas.drawBitmap(bitmap, x, y, paint)\n        canvas.restore()\n\n        //画不变换的另一半\n        canvas.save()\n        camera.save()\n        canvas.translate(centerX, centerY)\n        canvas.rotate(-degreeZ)\n        //计算裁切参数时清注意，此时的canvas的坐标系已经移动\n        canvas.clipRect(-centerX, -centerY, 0f, centerY) // Use float values\n        //此时的canvas的坐标系已经旋转，所以这里是rotateY\n        camera.rotateY(fixDegreeY)\n        camera.applyToCanvas(canvas)\n        canvas.rotate(degreeZ)\n        canvas.translate(-centerX, -centerY)\n        camera.restore()\n        canvas.drawBitmap(bitmap, x, y, paint)\n        canvas.restore()\n    }\n\n    /**\n     * 启动动画之前调用，把参数reset到初始状态\n     */\n    fun reset() {\n        degreeY = 0f\n        fixDegreeY = 0f\n        degreeZ = 0f\n        invalidate() // Invalidate after resetting properties\n    }\n\n    fun setBitmap(bitmap: Bitmap) {\n        this.bitmap = bitmap\n        invalidate()\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#3DDC84\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path android:pathData=\"M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"85.84757\"\n                android:endY=\"92.4963\"\n                android:startX=\"42.9492\"\n                android:startY=\"49.59793\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <com.sunnyxibei.hencoderpractice.MapView\n        android:id=\"@+id/map_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_centerInParent=\"true\"\n        app:mv_background=\"@drawable/flip_board\" />\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:id=\"@+id/google_map\"\n        android:title=\"Google Map\" />\n    <item\n        android:id=\"@+id/flip_board\"\n        android:title=\"FlipBoard\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n    <monochrome android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n    <monochrome android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"MapView\">\n        <attr name=\"mv_background\" format=\"reference\" />\n    </declare-styleable>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#3F51B5</color>\n    <color name=\"colorPrimaryDark\">#303F9F</color>\n    <color name=\"colorAccent\">#FF4081</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">HenCoderPractice</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/test/java/com/sunnyxibei/hencoderpractice/ExampleUnitTest.kt",
    "content": "package com.sunnyxibei.hencoderpractice\n\nimport org.junit.Assert.*\nimport org.junit.Test\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "allprojects {\n    repositories {\n        maven { url = uri(\"https://maven.aliyun.com/repository/public\") }\n        maven { url = uri(\"https://maven.aliyun.com/repository/central\") }\n        maven { url = uri(\"https://maven.aliyun.com/repository/google\") }\n        maven { url = uri(\"https://maven.aliyun.com/repository/gradle-plugin\") }\n        google()\n        mavenCentral()\n        maven { url = uri(\"https://jitpack.io\") }\n    }\n}\n\nval newBuildDir: Directory = rootProject.layout.buildDirectory.dir(\"../../build\").get()\nrootProject.layout.buildDirectory.value(newBuildDir)\n\nsubprojects {\n    val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)\n    project.layout.buildDirectory.value(newSubprojectBuildDir)\n}\nsubprojects {\n    project.evaluationDependsOn(\":app\")\n}\n\ntasks.register<Delete>(\"clean\") {\n    delete(rootProject.layout.buildDirectory)\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Sat Sep 23 22:49:00 CST 2017\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.12-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError\nandroid.useAndroidX=true\nandroid.enableJetifier=true\nandroid.nonTransitiveRClass=true\n#set gradle jdk 17\norg.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windowz variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\ngoto execute\n\n:4NT_args\n@rem Get arguments from the 4NT Shell from JP Software\nset CMD_LINE_ARGS=%$\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "pluginManagement {\n    buildscript {\n        repositories {\n            maven { url = uri(\"https://maven.aliyun.com/repository/public\") }\n            maven { url = uri(\"https://maven.aliyun.com/repository/central\") }\n            maven { url = uri(\"https://maven.aliyun.com/repository/google\") }\n            maven { url = uri(\"https://maven.aliyun.com/repository/gradle-plugin\") }\n            mavenCentral()\n        }\n    }\n\n    repositories {\n        maven { url = uri(\"https://maven.aliyun.com/repository/public\") }\n        maven { url = uri(\"https://maven.aliyun.com/repository/central\") }\n        maven { url = uri(\"https://maven.aliyun.com/repository/google\") }\n        maven { url = uri(\"https://maven.aliyun.com/repository/gradle-plugin\") }\n        google()\n        mavenCentral()\n        maven { url = uri(\"https://jitpack.io\") }\n        gradlePluginPortal()\n    }\n}\n\nplugins {\n    id(\"com.android.application\") version \"8.7.3\" apply false\n    id(\"org.jetbrains.kotlin.android\") version \"2.1.0\" apply false\n    id(\"org.jetbrains.kotlin.plugin.compose\") version \"2.1.0\" apply false\n}\n\nrootProject.name = \"HenCoderPractice\"\ninclude(\":app\")"
  }
]