├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── equationl │ │ └── calculator_compose │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── equationl │ │ │ └── calculator_compose │ │ │ ├── App.kt │ │ │ ├── MainActivity.kt │ │ │ ├── dataModel │ │ │ ├── HistoryData.kt │ │ │ └── KeyBoardData.kt │ │ │ ├── database │ │ │ ├── DatabaseModule.kt │ │ │ ├── HistoryConverters.kt │ │ │ ├── HistoryDao.kt │ │ │ └── HistoryDb.kt │ │ │ ├── overlay │ │ │ ├── ComposeOverlayViewService.kt │ │ │ ├── OverlayService.kt │ │ │ └── ViewReadyService.kt │ │ │ ├── ui │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Shape.kt │ │ │ │ ├── Size.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ ├── utils │ │ │ ├── Utils.kt │ │ │ └── VibratorHelper.kt │ │ │ ├── view │ │ │ ├── HIstoryWidget.kt │ │ │ ├── HomeScreen.kt │ │ │ ├── OverlayScreen.kt │ │ │ ├── ProgrammerScreen.kt │ │ │ ├── StandardScreen.kt │ │ │ └── widgets │ │ │ │ ├── Animation.kt │ │ │ │ └── AutoSizeFont.kt │ │ │ └── viewModel │ │ │ ├── HomeViewModel.kt │ │ │ ├── OverLayViewModel.kt │ │ │ ├── ProgrammerViewModel.kt │ │ │ └── StandardViewModel.kt │ └── res │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── equationl │ └── calculator_compose │ └── ExampleUnitTest.kt ├── build.gradle ├── docs └── img │ ├── screenshot1.jpg │ ├── screenshot2.jpg │ ├── screenshot3.jpg │ ├── screenshot4.jpg │ ├── screenshot5.jpg │ ├── screenshot6.jpg │ ├── screenshot7.jpg │ └── screenshot8.jpg ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /app/src/main/java/com/equationl/calculator_compose/test.kt 17 | /app/release/ 18 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | calculator-Compose -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Calculator-Compose 2 | 3 | 这是一款完全使用 Jetpack Compose 实现的计算器 APP。 4 | 5 | ## 功能特性 6 | 7 | | 是否支持 | 功能 | 8 | | :----: | :------: | 9 | | ✔ | 基础四则运算(标准、程序员) | 10 | | ✔ | 无限输入(标准) | 11 | | ✔ | % , 1/x , x² , √x 扩展运算(标准)| 12 | | ✔ | 运算过程历史记录(标准) | 13 | | ✔ | 二进制、八进制、十进制、十六进制随意切换并实时换算(程序员) | 14 | | ✔ | 位运算:左移、右移(程序员) | 15 | | ✔ | 逻辑运算:AND、OR、NOT、XOR(程序员) | 16 | | ✔ | 无限连续计算(标准、程序员) | 17 | | ✔ | 支持悬浮窗计算器,可调整位置、大小、透明度(标准) | 18 | | ✔ | 符合人体握持习惯的横屏键盘 | 19 | | ✔ | 旋转手机自动切换标准和程序员键盘 | 20 | | ✔ | 深色模式 | 21 | | ✔ | 酷炫的数字动效与振动反馈 | 22 | 23 | **注意:** 24 | 25 | 1. 标准模式使用 BigDecimal 计算,所以理论支持无限位数数字计算 26 | 2. 程序员模式因为涉及到二进制计算,所以采用 64 位储存大小,故不支持无限位数计算 27 | 3. 程序员模式不支持带小数运算,如果运算结果有小数,则会直接抛弃小数部分 28 | 29 | ## 截图 30 | 31 | | 浅色 | 深色 | 32 | | :----: | :----: | 33 | | ![1](./docs/img/screenshot1.jpg)
标准模式 | ![2](./docs/img/screenshot2.jpg)
标准模式 | 34 | | ![3](./docs/img/screenshot3.jpg)
历史记录 | ![4](./docs/img/screenshot4.jpg)
历史记录 | 35 | | ![5](./docs/img/screenshot5.jpg)
程序员模式 | ![6](./docs/img/screenshot6.jpg)
程序员模式 | 36 | | ![5](./docs/img/screenshot7.jpg)
悬浮窗 | ![6](./docs/img/screenshot8.jpg)
悬浮窗 | 37 | 38 | ## 其他 39 | 处理逻辑参考了 *微软计算器* 40 | 41 | 布局参考了 *小米计算器* 与 *微软计算器* 42 | 43 | 实现细节:[使用 Jetpack Compose 实现一个计算器APP](http://www.likehide.com/blogs/android/using_compose_made_a_calculator_app/) 44 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'dagger.hilt.android.plugin' 6 | } 7 | 8 | android { 9 | compileSdk 32 10 | 11 | defaultConfig { 12 | applicationId "com.equationl.calculator_compose" 13 | minSdk 24 14 | targetSdk 32 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | vectorDrawables { 20 | useSupportLibrary true 21 | } 22 | } 23 | 24 | buildTypes { 25 | release { 26 | minifyEnabled true 27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 28 | } 29 | } 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | kotlinOptions { 35 | jvmTarget = '1.8' 36 | } 37 | buildFeatures { 38 | compose true 39 | } 40 | composeOptions { 41 | kotlinCompilerExtensionVersion compose_version 42 | } 43 | packagingOptions { 44 | resources { 45 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 46 | } 47 | } 48 | } 49 | 50 | // 用于忽略使用 OptIn 注解的警告 51 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 52 | kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" 53 | } 54 | 55 | dependencies { 56 | 57 | implementation 'androidx.activity:activity-compose:1.4.0' 58 | implementation 'androidx.appcompat:appcompat:1.4.1' 59 | implementation 'androidx.core:core-ktx:1.7.0' 60 | implementation "androidx.compose.ui:ui:$compose_version" 61 | implementation "androidx.compose.material:material:$compose_version" 62 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 63 | implementation "androidx.compose.material:material-icons-extended:$compose_version" 64 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" 65 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" 66 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" 67 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 68 | implementation "androidx.lifecycle:lifecycle-service:$lifecycle_version" 69 | implementation "androidx.room:room-runtime:$room_version" 70 | implementation "androidx.room:room-ktx:$room_version" 71 | kapt "androidx.room:room-compiler:$room_version" 72 | implementation "com.google.dagger:hilt-android:2.42" 73 | kapt "com.google.dagger:hilt-compiler:2.42" 74 | implementation "androidx.hilt:hilt-navigation-compose:1.0.0" 75 | 76 | implementation "com.google.accompanist:accompanist-systemuicontroller:0.25.1" 77 | 78 | testImplementation 'junit:junit:4.13.2' 79 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 80 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 81 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 82 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 83 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" 84 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/equationl/calculator_compose/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.equationl.calculator_compose", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/App.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose 2 | 3 | import android.app.Application 4 | import com.equationl.calculator_compose.utils.VibratorHelper 5 | import dagger.hilt.android.HiltAndroidApp 6 | 7 | @HiltAndroidApp 8 | class App: Application() { 9 | override fun onCreate() { 10 | super.onCreate() 11 | VibratorHelper.instance.init(this) 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose 2 | 3 | import android.content.Intent 4 | import android.os.Build 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.material.Surface 11 | import androidx.compose.runtime.SideEffect 12 | import androidx.compose.ui.Modifier 13 | import com.equationl.calculator_compose.overlay.OverlayService 14 | import com.equationl.calculator_compose.ui.theme.CalculatorComposeTheme 15 | import com.equationl.calculator_compose.view.HomeScreen 16 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 17 | import dagger.hilt.android.AndroidEntryPoint 18 | 19 | @AndroidEntryPoint 20 | class MainActivity : ComponentActivity() { 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | 24 | setContent { 25 | CalculatorComposeTheme { 26 | val backgroundColor = MaterialTheme.colors.background 27 | 28 | Surface( 29 | modifier = Modifier.fillMaxSize(), 30 | color = backgroundColor 31 | ) { 32 | val systemUiController = rememberSystemUiController() 33 | val useDarkIcons = MaterialTheme.colors.isLight 34 | 35 | SideEffect { 36 | systemUiController.setSystemBarsColor( 37 | color = backgroundColor, 38 | darkIcons = useDarkIcons 39 | ) 40 | } 41 | 42 | HomeScreen() 43 | } 44 | } 45 | } 46 | } 47 | 48 | override fun onResume() { 49 | super.onResume() 50 | 51 | // 每次打开主页都要把悬浮界面关闭 52 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 53 | stopService(Intent(this, OverlayService::class.java)) 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/dataModel/HistoryData.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.dataModel 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "history") 8 | data class HistoryData( 9 | @PrimaryKey(autoGenerate = true) 10 | val id: Int = 0, 11 | @ColumnInfo(name = "show_text") 12 | val showText: String, 13 | @ColumnInfo(name = "left_number") 14 | val lastInputText: String, 15 | @ColumnInfo(name = "right_number") 16 | val inputText: String, 17 | @ColumnInfo(name = "operator") 18 | val operator: Operator, 19 | @ColumnInfo(name = "result") 20 | val result: String, 21 | @ColumnInfo(name = "create_time") 22 | val createTime: Long = System.currentTimeMillis(), 23 | ) 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/dataModel/KeyBoardData.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.dataModel 2 | 3 | import androidx.compose.material.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | 7 | // 数字按键 8 | const val KeyIndex_0 = 0 9 | const val KeyIndex_1 = 1 10 | const val KeyIndex_2 = 2 11 | const val KeyIndex_3 = 3 12 | const val KeyIndex_4 = 4 13 | const val KeyIndex_5 = 5 14 | const val KeyIndex_6 = 6 15 | const val KeyIndex_7 = 7 16 | const val KeyIndex_8 = 8 17 | const val KeyIndex_9 = 9 18 | const val KeyIndex_A = 17 // 不按照顺序往下编号是因为在程序员键盘中使用的是 ascii 索引, 而数字 9 和 A 间隔了 7 位 19 | const val KeyIndex_B = 18 20 | const val KeyIndex_C = 19 21 | const val KeyIndex_D = 20 22 | const val KeyIndex_E = 21 23 | const val KeyIndex_F = 22 24 | 25 | // 运算按键 26 | const val KeyIndex_Add = 100 27 | const val KeyIndex_Minus = 101 28 | const val KeyIndex_Multiply = 102 29 | const val KeyIndex_Divide = 103 30 | const val KeyIndex_NegativeNumber = 104 31 | const val KeyIndex_Point = 105 32 | const val KeyIndex_Reciprocal = 106 33 | const val KeyIndex_Pow2 = 107 34 | const val KeyIndex_Sqrt = 108 35 | const val KeyIndex_Percentage = 109 36 | const val KeyIndex_Lsh = 110 37 | const val KeyIndex_Rsh = 111 38 | const val KeyIndex_And = 112 39 | const val KeyIndex_Or = 113 40 | const val KeyIndex_Not = 114 41 | const val KeyIndex_XOr = 117 42 | 43 | // 功能按键 44 | const val KeyIndex_Equal = 1000 45 | const val KeyIndex_CE = 1001 46 | const val KeyIndex_Clear = 1002 47 | const val KeyIndex_Back = 1003 48 | 49 | 50 | @Composable 51 | fun numberColor(): Color = Color.Unspecified // MaterialTheme.colors.secondary 52 | 53 | @Composable 54 | fun functionColor(): Color = MaterialTheme.colors.primary 55 | 56 | @Composable 57 | fun equalColor(): Color = MaterialTheme.colors.primaryVariant 58 | 59 | @Composable 60 | fun standardKeyBoardBtn(): List> = listOf( 61 | listOf( 62 | KeyBoardData("%", functionColor(), KeyIndex_Percentage), 63 | KeyBoardData("CE", functionColor(), KeyIndex_CE), 64 | KeyBoardData("C", functionColor(), KeyIndex_Clear), 65 | KeyBoardData("⇦", functionColor(), KeyIndex_Back), 66 | ), 67 | listOf( 68 | KeyBoardData("1/x", functionColor(), KeyIndex_Reciprocal), 69 | KeyBoardData("x²", functionColor(), KeyIndex_Pow2), 70 | KeyBoardData("√x", functionColor(), KeyIndex_Sqrt), 71 | KeyBoardData(Operator.Divide.showText, functionColor(), KeyIndex_Divide), 72 | ), 73 | listOf( 74 | KeyBoardData("7", numberColor(), KeyIndex_7), 75 | KeyBoardData("8", numberColor(), KeyIndex_8), 76 | KeyBoardData("9", numberColor(), KeyIndex_9), 77 | KeyBoardData(Operator.MULTIPLY.showText, functionColor(), KeyIndex_Multiply), 78 | ), 79 | listOf( 80 | KeyBoardData("4", numberColor(), KeyIndex_4), 81 | KeyBoardData("5", numberColor(), KeyIndex_5), 82 | KeyBoardData("6", numberColor(), KeyIndex_6), 83 | KeyBoardData(Operator.MINUS.showText, functionColor(), KeyIndex_Minus), 84 | ), 85 | listOf( 86 | KeyBoardData("1", numberColor(), KeyIndex_1), 87 | KeyBoardData("2", numberColor(), KeyIndex_2), 88 | KeyBoardData("3", numberColor(), KeyIndex_3), 89 | KeyBoardData(Operator.ADD.showText, functionColor(), KeyIndex_Add), 90 | ), 91 | listOf( 92 | KeyBoardData("+/-", numberColor(), KeyIndex_NegativeNumber), 93 | KeyBoardData("0", numberColor(), KeyIndex_0), 94 | KeyBoardData(".", numberColor(), KeyIndex_Point), 95 | KeyBoardData("=", equalColor(), KeyIndex_Equal, isFilled = true), 96 | ) 97 | ) 98 | 99 | @Composable 100 | fun programmerNumberKeyBoardBtn(): List> = listOf( 101 | listOf( 102 | KeyBoardData("D", numberColor(), KeyIndex_D), 103 | KeyBoardData("E", numberColor(), KeyIndex_E), 104 | KeyBoardData("F", numberColor(), KeyIndex_F) 105 | ), 106 | listOf( 107 | KeyBoardData("A", numberColor(), KeyIndex_A), 108 | KeyBoardData("B", numberColor(), KeyIndex_B), 109 | KeyBoardData("C", numberColor(), KeyIndex_C) 110 | ), 111 | listOf( 112 | KeyBoardData("7", numberColor(), KeyIndex_7), 113 | KeyBoardData("8", numberColor(), KeyIndex_8), 114 | KeyBoardData("9", numberColor(), KeyIndex_9) 115 | ), 116 | listOf( 117 | KeyBoardData("4", numberColor(), KeyIndex_4), 118 | KeyBoardData("5", numberColor(), KeyIndex_5), 119 | KeyBoardData("6", numberColor(), KeyIndex_6) 120 | ), 121 | listOf( 122 | KeyBoardData("1", numberColor(), KeyIndex_1), 123 | KeyBoardData("2", numberColor(), KeyIndex_2), 124 | KeyBoardData("3", numberColor(), KeyIndex_3) 125 | ), 126 | listOf( 127 | KeyBoardData("<<", functionColor(), KeyIndex_Lsh), 128 | KeyBoardData("0", numberColor(), KeyIndex_0), 129 | KeyBoardData(">>", functionColor(), KeyIndex_Rsh) 130 | ) 131 | ) 132 | 133 | @Composable 134 | fun programmerFunctionKeyBoardBtn(): List> = listOf( 135 | listOf( 136 | KeyBoardData("C", functionColor(), KeyIndex_Clear), 137 | KeyBoardData("⇦", functionColor(), KeyIndex_Back) 138 | ), 139 | listOf( 140 | KeyBoardData("CE", functionColor(), KeyIndex_CE), 141 | KeyBoardData(Operator.Divide.showText, functionColor(), KeyIndex_Divide) 142 | ), 143 | listOf( 144 | KeyBoardData("NOT", functionColor(), KeyIndex_Not), 145 | KeyBoardData(Operator.MULTIPLY.showText, functionColor(), KeyIndex_Multiply) 146 | ), 147 | listOf( 148 | KeyBoardData("XOR", functionColor(), KeyIndex_XOr), 149 | KeyBoardData(Operator.MINUS.showText, functionColor(), KeyIndex_Minus) 150 | ), 151 | listOf( 152 | KeyBoardData("AND", functionColor(), KeyIndex_And), 153 | KeyBoardData(Operator.ADD.showText, functionColor(), KeyIndex_Add) 154 | ), 155 | listOf( 156 | KeyBoardData("OR", functionColor(), KeyIndex_Or), 157 | KeyBoardData("=", equalColor(), KeyIndex_Equal, isFilled = true) 158 | ) 159 | ) 160 | 161 | @Composable 162 | fun overlayKeyBoardBtn(): List> = listOf( 163 | listOf( 164 | KeyBoardData("CE", functionColor(), KeyIndex_CE), 165 | KeyBoardData("C", functionColor(), KeyIndex_Clear), 166 | KeyBoardData("⇦", functionColor(), KeyIndex_Back), 167 | KeyBoardData(Operator.Divide.showText, functionColor(), KeyIndex_Divide) 168 | ), 169 | listOf( 170 | KeyBoardData("7", numberColor(), KeyIndex_7), 171 | KeyBoardData("8", numberColor(), KeyIndex_8), 172 | KeyBoardData("9", numberColor(), KeyIndex_9), 173 | KeyBoardData(Operator.MULTIPLY.showText, functionColor(), KeyIndex_Multiply), 174 | ), 175 | listOf( 176 | KeyBoardData("4", numberColor(), KeyIndex_4), 177 | KeyBoardData("5", numberColor(), KeyIndex_5), 178 | KeyBoardData("6", numberColor(), KeyIndex_6), 179 | KeyBoardData(Operator.MINUS.showText, functionColor(), KeyIndex_Minus), 180 | ), 181 | listOf( 182 | KeyBoardData("1", numberColor(), KeyIndex_1), 183 | KeyBoardData("2", numberColor(), KeyIndex_2), 184 | KeyBoardData("3", numberColor(), KeyIndex_3), 185 | KeyBoardData(Operator.ADD.showText, functionColor(), KeyIndex_Add), 186 | ), 187 | listOf( 188 | KeyBoardData("±", numberColor(), KeyIndex_NegativeNumber), 189 | KeyBoardData("0", numberColor(), KeyIndex_0), 190 | KeyBoardData(".", numberColor(), KeyIndex_Point), 191 | KeyBoardData("=", equalColor(), KeyIndex_Equal, isFilled = true), 192 | ) 193 | ) 194 | 195 | val BitOperationList = listOf( 196 | Operator.NOT, 197 | Operator.AND, 198 | Operator.OR, 199 | Operator.XOR, 200 | Operator.LSH, 201 | Operator.RSH 202 | ) 203 | 204 | data class KeyBoardData( 205 | val text: String, 206 | /** 207 | * 设置按钮颜色,设置范围取决于 [isFilled] 208 | * */ 209 | val background: Color, 210 | val index: Int, 211 | /** 212 | * 是否填充该按钮,如果为 true 则 [background] 用于填充该按钮背景;否则,[background] 用于设置该按钮字体颜色 213 | * */ 214 | val isFilled: Boolean = false, 215 | val isAvailable: Boolean = true 216 | ) 217 | 218 | enum class Operator(val showText: String) { 219 | ADD("+"), 220 | MINUS("-"), 221 | MULTIPLY("×"), 222 | Divide("÷"), 223 | SQRT("√"), 224 | POW2("²"), 225 | NOT("NOT"), 226 | AND(" AND "), 227 | OR(" OR "), 228 | XOR(" XOR "), 229 | LSH(" Lsh "), 230 | RSH(" Rsh "), 231 | NUll("") 232 | } 233 | 234 | enum class InputBase(val number: Int, val forbidBtn: List) { 235 | HEX(16, listOf()), 236 | DEC(10, listOf( 237 | KeyIndex_A, 238 | KeyIndex_B, 239 | KeyIndex_C, 240 | KeyIndex_D, 241 | KeyIndex_E, 242 | KeyIndex_F 243 | )), 244 | OCT(8, listOf( 245 | KeyIndex_A, 246 | KeyIndex_B, 247 | KeyIndex_C, 248 | KeyIndex_D, 249 | KeyIndex_E, 250 | KeyIndex_F, 251 | KeyIndex_8, 252 | KeyIndex_9, 253 | )), 254 | BIN(2, listOf( 255 | KeyIndex_A, 256 | KeyIndex_B, 257 | KeyIndex_C, 258 | KeyIndex_D, 259 | KeyIndex_E, 260 | KeyIndex_F, 261 | KeyIndex_9, 262 | KeyIndex_8, 263 | KeyIndex_7, 264 | KeyIndex_6, 265 | KeyIndex_5, 266 | KeyIndex_4, 267 | KeyIndex_3, 268 | KeyIndex_2 269 | )) 270 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/database/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.database 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | //这里使用了SingletonComponent,因此 NetworkModule 绑定到 Application 的整个生命周期 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object DataBaseModule { 15 | 16 | @Singleton 17 | @Provides 18 | fun provideHistoryDataBase(@ApplicationContext app: Context): HistoryDb = HistoryDb.create(app) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/database/HistoryConverters.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.database 2 | 3 | import androidx.room.TypeConverter 4 | import com.equationl.calculator_compose.dataModel.Operator 5 | 6 | class HistoryConverters { 7 | @TypeConverter 8 | fun fromOperator(operator: Operator): String { 9 | return operator.name 10 | } 11 | 12 | @TypeConverter 13 | fun toOperator(operator: String): Operator { 14 | return try { 15 | Operator.valueOf(operator) 16 | } catch (e: IllegalArgumentException) { 17 | Operator.NUll 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/database/HistoryDao.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.database 2 | 3 | import androidx.room.* 4 | import com.equationl.calculator_compose.dataModel.HistoryData 5 | 6 | @Dao 7 | interface HistoryDao { 8 | @Query("select * from history order by id DESC") 9 | fun getAll(): List 10 | 11 | @Insert(onConflict = OnConflictStrategy.REPLACE) 12 | fun insert(item: HistoryData) 13 | 14 | @Update 15 | fun update(item: HistoryData) 16 | 17 | @Delete 18 | fun delete(item: HistoryData) 19 | 20 | @Query("DELETE FROM history") 21 | fun deleteAll() 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/database/HistoryDb.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.database 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import androidx.room.TypeConverters 8 | import com.equationl.calculator_compose.dataModel.HistoryData 9 | 10 | @Database( 11 | entities = [HistoryData::class], 12 | version = 2, 13 | exportSchema = false 14 | ) 15 | @TypeConverters(HistoryConverters::class) 16 | abstract class HistoryDb : RoomDatabase() { 17 | companion object { 18 | fun create(context: Context, useInMemory: Boolean = false): HistoryDb { 19 | val databaseBuilder = if (useInMemory) { 20 | Room.inMemoryDatabaseBuilder(context, HistoryDb::class.java) 21 | } else { 22 | Room.databaseBuilder(context, HistoryDb::class.java, "history.db") 23 | } 24 | return databaseBuilder 25 | .fallbackToDestructiveMigration() 26 | .build() 27 | } 28 | } 29 | 30 | abstract fun history(): HistoryDao 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/overlay/ComposeOverlayViewService.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.overlay 2 | 3 | import android.graphics.PixelFormat 4 | import android.os.Build 5 | import android.view.Gravity 6 | import android.view.WindowManager 7 | import androidx.annotation.RequiresApi 8 | import androidx.compose.foundation.gestures.detectDragGestures 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.BoxScope 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.setValue 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.geometry.Offset 17 | import androidx.compose.ui.input.pointer.pointerInput 18 | import androidx.compose.ui.platform.ComposeView 19 | import androidx.lifecycle.ViewTreeLifecycleOwner 20 | import androidx.lifecycle.ViewTreeViewModelStoreOwner 21 | import androidx.savedstate.setViewTreeSavedStateRegistryOwner 22 | import com.equationl.calculator_compose.utils.getScreenSize 23 | import kotlin.math.roundToInt 24 | 25 | /** 26 | * Service that is ready to display compose overlay view 27 | * @author Quentin Nivelais 28 | * @link https://gist.github.com/KONFeature/2f84436e1c0a1926505cac934d470f90 29 | * 30 | * Edit by equationl (likehide.com) 31 | */ 32 | @RequiresApi(Build.VERSION_CODES.R) 33 | abstract class ComposeOverlayViewService : ViewReadyService() { 34 | 35 | // Build the layout param for our popup 36 | private val layoutParams by lazy { 37 | WindowManager.LayoutParams().apply { 38 | format = PixelFormat.TRANSLUCENT 39 | flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 40 | gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL 41 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 42 | @Suppress("DEPRECATION") 43 | type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT 44 | } else { 45 | type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 46 | } 47 | 48 | val screenSize = getScreenSize(this@ComposeOverlayViewService) 49 | 50 | width = screenSize.x / 3 51 | height = screenSize.y / 3 52 | } 53 | } 54 | 55 | // The current offset of our overlay composable 56 | private var overlayOffset by mutableStateOf(Offset.Zero) 57 | 58 | // Access our window manager 59 | private val windowManager by lazy { 60 | overlayContext.getSystemService(WindowManager::class.java) 61 | } 62 | 63 | // Build our compose view 64 | private val composeView by lazy { 65 | ComposeView(overlayContext) 66 | } 67 | 68 | override fun onCreate() { 69 | super.onCreate() 70 | 71 | // Bound the compose lifecycle, view model and view tree saved state, into our view service 72 | ViewTreeLifecycleOwner.set(composeView, this) 73 | ViewTreeViewModelStoreOwner.set(composeView) { viewModelStore } 74 | composeView.setViewTreeSavedStateRegistryOwner(this) 75 | 76 | // Set the content of our compose view 77 | composeView.setContent { Content() } 78 | 79 | // Push the compose view into our window manager 80 | windowManager.addView(composeView, layoutParams) 81 | } 82 | 83 | override fun onDestroy() { 84 | super.onDestroy() 85 | // Remove our compose view from the window manager 86 | windowManager.removeView(composeView) 87 | } 88 | 89 | @Composable 90 | abstract fun Content() 91 | 92 | /** 93 | * Draggable box container (not used by default, since not every overlay should be draggable) 94 | */ 95 | @Composable 96 | internal fun OverlayDraggableContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) = 97 | Box( 98 | modifier = modifier.pointerInput(Unit) { 99 | detectDragGestures { change, dragAmount -> 100 | change.consume() 101 | 102 | // Update our current offset 103 | val newOffset = overlayOffset + dragAmount 104 | overlayOffset = newOffset 105 | 106 | // Update the layout params, and then the view 107 | layoutParams.apply { 108 | x = overlayOffset.x.roundToInt() 109 | y = overlayOffset.y.roundToInt() 110 | } 111 | windowManager.updateViewLayout(composeView, layoutParams) 112 | } 113 | }, 114 | content = content 115 | ) 116 | 117 | internal fun updateSize(scale: Float) { 118 | val screenSize = getScreenSize(this@ComposeOverlayViewService) 119 | layoutParams.apply { 120 | width = (screenSize.x / scale).roundToInt() 121 | height = (screenSize.y / scale).roundToInt() 122 | } 123 | windowManager.updateViewLayout(composeView, layoutParams) 124 | } 125 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/overlay/OverlayService.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.overlay 2 | 3 | import android.os.Build 4 | import androidx.annotation.RequiresApi 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Surface 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.LaunchedEffect 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.alpha 12 | import com.equationl.calculator_compose.database.HistoryDb 13 | import com.equationl.calculator_compose.ui.theme.CalculatorComposeTheme 14 | import com.equationl.calculator_compose.view.OverlayScreen 15 | import com.equationl.calculator_compose.viewModel.OverLayViewModel 16 | import com.equationl.calculator_compose.viewModel.OverlayEvent 17 | 18 | @RequiresApi(Build.VERSION_CODES.R) 19 | class OverlayService : ComposeOverlayViewService() { 20 | val viewModel: OverLayViewModel by lazy { 21 | OverLayViewModel( 22 | HistoryDb.create(this@OverlayService) 23 | ) 24 | } 25 | 26 | @Composable 27 | override fun Content() = OverlayDraggableContainer { 28 | val overlayState = viewModel.overlayState 29 | 30 | LaunchedEffect(Unit) { 31 | viewModel.viewEvents.collect { 32 | if (it is OverlayEvent.ChangeSize) { 33 | updateSize(it.scale) 34 | } 35 | } 36 | } 37 | 38 | CalculatorComposeTheme { 39 | Surface( 40 | modifier = Modifier 41 | .fillMaxSize() 42 | .alpha(overlayState.backgroundAlpha), 43 | color = MaterialTheme.colors.background.copy(alpha = overlayState.backgroundAlpha) 44 | ) { 45 | OverlayScreen(viewModel) 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/overlay/ViewReadyService.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.overlay 2 | 3 | import android.content.Context 4 | import android.hardware.display.DisplayManager 5 | import android.os.Build 6 | import android.view.Display 7 | import android.view.WindowManager 8 | import androidx.annotation.RequiresApi 9 | import androidx.lifecycle.LifecycleService 10 | import androidx.lifecycle.ViewModelStore 11 | import androidx.lifecycle.ViewModelStoreOwner 12 | import androidx.savedstate.SavedStateRegistry 13 | import androidx.savedstate.SavedStateRegistryController 14 | import androidx.savedstate.SavedStateRegistryOwner 15 | 16 | /** 17 | * Service that is ready to display view, provide a ui context on the primary screen, and all the tools needed to built a view with state managment, view model etc 18 | * @author Quentin Nivelais 19 | * @link https://gist.github.com/KONFeature/2f84436e1c0a1926505cac934d470f90 20 | */ 21 | @RequiresApi(Build.VERSION_CODES.R) 22 | abstract class ViewReadyService : LifecycleService(), SavedStateRegistryOwner, ViewModelStoreOwner { 23 | 24 | /** 25 | * Build our saved state registry controller 26 | */ 27 | private val savedStateRegistryController: SavedStateRegistryController by lazy(LazyThreadSafetyMode.NONE) { 28 | SavedStateRegistryController.create(this) 29 | } 30 | 31 | /** 32 | * Build our view model store 33 | */ 34 | private val internalViewModelStore: ViewModelStore by lazy { 35 | ViewModelStore() 36 | } 37 | 38 | /** 39 | * Context dedicated to the view 40 | */ 41 | internal val overlayContext: Context by lazy { 42 | // Get the default display 43 | val defaultDisplay: Display = getSystemService(DisplayManager::class.java).getDisplay(Display.DEFAULT_DISPLAY) 44 | // Create a display context, and then the window context 45 | createDisplayContext(defaultDisplay) 46 | .createWindowContext(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, null) 47 | } 48 | 49 | override fun onCreate() { 50 | super.onCreate() 51 | // Restore the last saved state registry 52 | savedStateRegistryController.performRestore(null) 53 | } 54 | 55 | override fun onDestroy() { 56 | super.onDestroy() 57 | } 58 | 59 | override val savedStateRegistry: SavedStateRegistry 60 | get() = savedStateRegistryController.savedStateRegistry 61 | 62 | override fun getViewModelStore(): ViewModelStore = internalViewModelStore 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Amber200 = Color(0xFFFFE082) 6 | val Amber500 = Color(0xFFFFC107) 7 | val Amber700 = Color(0xFFFFA000) 8 | val AmberA200 = Color(0xFFFFD740) 9 | 10 | val Grey200 = Color(0xFFEEEEEE) 11 | val Grey500 = Color(0xFF9E9E9E) 12 | val Grey700 = Color(0xFF616161) -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/ui/theme/Size.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.ui.theme 2 | 3 | import androidx.compose.ui.unit.sp 4 | 5 | val InputTitleContentSize = 26.sp 6 | val InputNormalFontSize = 20.sp 7 | 8 | val InputLargeFontSize = 32.sp 9 | val ShowNormalFontSize = 22.sp 10 | val ShowSmallFontSize = 18.sp 11 | 12 | val OverlayNormalTextSize = 20.sp 13 | val OverlayLargeTextSize = 24.sp -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = Grey500, 11 | primaryVariant = Grey700, 12 | secondary = Grey200, 13 | //background = Color.LightGray 14 | ) 15 | 16 | private val LightColorPalette = lightColors( 17 | primary = Amber500, 18 | primaryVariant = Amber700, 19 | secondary = AmberA200, 20 | //background = Color.White, 21 | ) 22 | 23 | @Composable 24 | fun CalculatorComposeTheme( 25 | darkTheme: Boolean = isSystemInDarkTheme(), 26 | content: @Composable () -> Unit 27 | ) { 28 | val colors = if (darkTheme) { 29 | DarkColorPalette 30 | } else { 31 | LightColorPalette 32 | } 33 | 34 | MaterialTheme( 35 | colors = colors, 36 | typography = Typography, 37 | shapes = Shapes, 38 | content = content 39 | ) 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.utils 2 | 3 | import android.app.Service 4 | import android.content.Context 5 | import android.graphics.Point 6 | import android.os.Build 7 | import android.util.DisplayMetrics 8 | import android.view.WindowManager 9 | import androidx.compose.foundation.clickable 10 | import androidx.compose.foundation.interaction.MutableInteractionSource 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.composed 14 | import com.equationl.calculator_compose.dataModel.Operator 15 | import java.math.BigDecimal 16 | import java.math.RoundingMode 17 | 18 | /** 19 | * BigDecimal 的开平方 20 | * 21 | * @link https://stackoverflow.com/a/19743026 22 | * */ 23 | fun BigDecimal.sqrt(scale: Int): BigDecimal { 24 | val two = BigDecimal.valueOf(2) 25 | var x0 = BigDecimal("0") 26 | var x1 = BigDecimal(kotlin.math.sqrt(this.toDouble())) 27 | while (x0 != x1) { 28 | x0 = x1 29 | x1 = this.divide(x0, scale, BigDecimal.ROUND_HALF_UP) 30 | x1 = x1.add(x0) 31 | x1 = x1.divide(two, scale, BigDecimal.ROUND_HALF_UP) 32 | } 33 | return x1 34 | } 35 | 36 | /** 37 | * 格式化显示的数字 38 | * 39 | * @param addSplitChar 添加的分隔符 40 | * @param splitLength 间隔多少个字符添加分割符 41 | * @param isAddLeadingZero 是否在不满足 [splitLength] 一组的数字前添加 0 42 | * @param formatDecimal 是否格式化小数部分(移除末尾多余的0) 43 | * @param formatInteger 是否格式化整数部分(添加分隔符或前导0) 44 | * */ 45 | fun String.formatNumber( 46 | addSplitChar: String = ",", 47 | splitLength: Int = 3, 48 | isAddLeadingZero: Boolean = false, 49 | formatDecimal: Boolean = false, 50 | formatInteger: Boolean = true 51 | ): String { 52 | // 如果是错误提示信息则不做处理 53 | if (this.length >= 3 && this.substring(0, 3) == "Err") return this 54 | 55 | val stringBuilder = StringBuilder(this) 56 | 57 | val pointIndex = stringBuilder.indexOf('.') 58 | 59 | val integer: StringBuilder 60 | val decimal: StringBuilder 61 | 62 | if (pointIndex == -1) { 63 | integer = stringBuilder // 整数部分 64 | decimal = StringBuilder() // 小数部分 65 | } 66 | else { 67 | val stringList = stringBuilder.split('.') 68 | integer = StringBuilder(stringList[0]) // 整数部分 69 | decimal = StringBuilder(stringList[1]) // 小数部分 70 | decimal.insert(0, '.') 71 | } 72 | 73 | var addCharCount = 0 74 | 75 | if (formatInteger) { 76 | // 给整数部分添加逗号分隔符 77 | if (integer.length > splitLength) { 78 | val end = if (integer[0] == '-') 2 else 1 // 判断是否有前导符号 79 | for (i in integer.length-splitLength downTo end step splitLength) { 80 | integer.insert(i, addSplitChar) 81 | addCharCount++ 82 | } 83 | } 84 | 85 | if (isAddLeadingZero) { // 添加前导 0 补满一组 86 | val realLength = integer.length - addCharCount 87 | if (realLength % splitLength != 0) { 88 | repeat(4 - realLength % splitLength) { 89 | integer.insert(0, '0') 90 | } 91 | } 92 | } 93 | } 94 | 95 | if (formatDecimal) { 96 | // 移除小数部分末尾占位的 0 97 | if (decimal.isNotEmpty()) { 98 | while (decimal.last() == '0') { 99 | decimal.deleteAt(decimal.lastIndex) 100 | } 101 | if (decimal.length == 1) { // 上面我们给小数部分首位添加了点号 ”.“ ,所以如果长度为 1 则表示不存在有效小数,则将点号也删除掉 102 | decimal.deleteAt(0) 103 | } 104 | } 105 | } 106 | 107 | return integer.append(decimal).toString() 108 | } 109 | 110 | 111 | fun calculate(leftValue: String, rightValue: String, operator: Operator, scale: Int = 16): Result { 112 | val left = BigDecimal(leftValue) 113 | val right = BigDecimal(rightValue) 114 | 115 | when (operator) { 116 | Operator.ADD -> { 117 | return Result.success(left.add(right)) 118 | } 119 | Operator.MINUS -> { 120 | return Result.success(left.minus(right)) 121 | } 122 | Operator.MULTIPLY -> { 123 | return Result.success(left.multiply(right)) 124 | } 125 | Operator.Divide -> { 126 | if (right.signum() == 0) { 127 | return Result.failure(ArithmeticException("Err: 除数不能为零")) 128 | } 129 | return Result.success(left.divide(right, scale, RoundingMode.HALF_UP).stripTrailingZeros()) 130 | } 131 | Operator.SQRT -> { 132 | if (left.signum() == -1) { 133 | return Result.failure(ArithmeticException("Err: 无效输入")) 134 | } 135 | return Result.success(left.sqrt(scale).stripTrailingZeros()) 136 | } 137 | Operator.POW2 -> { 138 | val result = left.pow(2) 139 | if (result.toString().length > 5000) { 140 | return Result.failure(NumberFormatException("Err: 数字过大,无法显示")) 141 | } 142 | 143 | return Result.success(result) 144 | } 145 | Operator.NUll -> { 146 | return Result.success(left) 147 | } 148 | Operator.NOT, 149 | Operator.AND, 150 | Operator.OR , 151 | Operator.XOR, 152 | Operator.LSH, 153 | Operator.RSH -> { // 这些值不会调用这个方法计算,所以直接返回错误 154 | return Result.failure(NumberFormatException("Err: 错误的调用")) 155 | } 156 | } 157 | } 158 | 159 | inline fun Modifier.noRippleClickable(crossinline onClick: ()->Unit): Modifier = composed { 160 | clickable(indication = null, 161 | interactionSource = remember { MutableInteractionSource() }) { 162 | onClick() 163 | } 164 | } 165 | 166 | /** 167 | * 获取屏幕尺寸(减去虚拟按键的尺寸) 168 | * */ 169 | fun getScreenSize(context: Context): Point { 170 | val mWindowManager = context.getSystemService(Service.WINDOW_SERVICE) as WindowManager 171 | 172 | val mScreenWidth: Int 173 | val mScreenHeight: Int 174 | 175 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 176 | mScreenHeight = mWindowManager.currentWindowMetrics.bounds.height() 177 | mScreenWidth = mWindowManager.currentWindowMetrics.bounds.width() 178 | return Point(mScreenWidth, mScreenHeight) 179 | } 180 | else { 181 | val metrics = DisplayMetrics() 182 | @Suppress("DEPRECATION") 183 | val display = mWindowManager.defaultDisplay 184 | @Suppress("DEPRECATION") 185 | display.getMetrics(metrics) 186 | val point = Point() 187 | @Suppress("DEPRECATION") 188 | display.getRealSize(point) 189 | return point 190 | } 191 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/utils/VibratorHelper.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.utils 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.os.VibrationEffect 6 | import android.os.Vibrator 7 | import android.os.VibratorManager 8 | import androidx.appcompat.app.AppCompatActivity 9 | 10 | /** 11 | * FileName: VibratorHelper.kt 12 | * Author: equationl 13 | * Email: admin@likehide.com 14 | * Date: 2020/3/1 18:30 15 | * Description: Vibrator帮助类,用于解决旧版本兼容问题 16 | */ 17 | class VibratorHelper { 18 | private var vibrator: Vibrator? = null 19 | 20 | companion object { 21 | val instance by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { 22 | VibratorHelper() 23 | } 24 | } 25 | 26 | fun init(context: Context) { 27 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 28 | val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager 29 | this.vibrator = vibratorManager.defaultVibrator 30 | } 31 | else { 32 | @Suppress("DEPRECATION") 33 | this.vibrator = context.getSystemService(AppCompatActivity.VIBRATOR_SERVICE) as Vibrator 34 | } 35 | } 36 | 37 | fun init(vibrator: Vibrator) { 38 | this.vibrator = vibrator 39 | } 40 | 41 | fun cancel() { 42 | vibrator?.cancel() 43 | } 44 | 45 | fun hasVibrator(): Boolean { 46 | return vibrator?.hasVibrator() == true 47 | } 48 | 49 | fun hasAmplitudeControl(): Boolean { 50 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 51 | return false 52 | } 53 | return vibrator?.hasAmplitudeControl() == true 54 | } 55 | 56 | fun vibrate(timings: LongArray, amplitudes: IntArray, repeat: Int) { 57 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 58 | val vibrationEffect = VibrationEffect.createWaveform(timings, amplitudes, repeat) 59 | vibrator?.vibrate(vibrationEffect) 60 | } 61 | else { 62 | val pattern = mutableListOf() 63 | var isCloseMotor = false 64 | var duration = 0L 65 | for (i in amplitudes.indices) { 66 | if ((amplitudes[i] > 0) == isCloseMotor) { 67 | duration += timings[i] 68 | } 69 | else { 70 | pattern.add(duration) 71 | isCloseMotor = amplitudes[i] > 0 72 | duration = timings[i] 73 | } 74 | } 75 | pattern.add(duration) 76 | 77 | val patternA = pattern.toLongArray() 78 | @Suppress("DEPRECATION") 79 | vibrator?.vibrate(patternA, repeat) 80 | } 81 | } 82 | 83 | fun vibrateOneShot(milliseconds: Long, amplitude: Int) { 84 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 85 | val vibrationEffect = VibrationEffect.createOneShot(milliseconds, amplitude) 86 | vibrator?.vibrate(vibrationEffect) 87 | } 88 | else { 89 | @Suppress("DEPRECATION") 90 | vibrator?.vibrate(milliseconds) 91 | } 92 | } 93 | 94 | fun vibratePredefined(predefined: Int) { 95 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 96 | val vibrationEffect = VibrationEffect.createPredefined(predefined) 97 | vibrator?.vibrate(vibrationEffect) 98 | } 99 | else { 100 | // 系统预设效果就暂时不适配了 101 | throw UnsupportedOperationException("该系统不支持系统预设振动") 102 | } 103 | } 104 | 105 | /** 106 | * 计算错误时的振动效果 107 | * */ 108 | fun vibrateOnError() { 109 | val timings = longArrayOf(10,180,10,90, 4, 90, 7, 80,2, 120, 4,50,2,40,1,40, 4,50,2,40,1,40, 4,50,2,40,1,40) 110 | val amplitudes = intArrayOf(255,0, 255,0, 240,0, 240,0, 240,0, 230,0,230,0,230,0, 220,0,220,0,220,0, 210,0,210,0,210,0) 111 | instance.vibrate(timings, amplitudes, -1) 112 | } 113 | 114 | /** 115 | * 清除时的振动效果 116 | * */ 117 | fun vibrateOnClear() { 118 | val timings = longArrayOf(10,180,10,90, 4, 90, 7, 80,2, 120 ) 119 | val amplitudes = intArrayOf(255,0, 255,0, 240,0, 240,0, 240,0) 120 | instance.vibrate(timings, amplitudes, -1) 121 | } 122 | 123 | /** 124 | * 开始计算时的振动效果 125 | * */ 126 | fun vibrateOnEqual() { 127 | instance.vibrateOneShot(50, 150) 128 | } 129 | 130 | /** 131 | * 按下按键时的振动效果 132 | * */ 133 | fun vibrateOnClick() { 134 | instance.vibrateOneShot(5, 255) 135 | } 136 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/view/HIstoryWidget.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.view 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.combinedClickable 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.lazy.LazyColumn 9 | import androidx.compose.foundation.lazy.items 10 | import androidx.compose.material.Icon 11 | import androidx.compose.material.MaterialTheme 12 | import androidx.compose.material.Text 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.outlined.Delete 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import com.equationl.calculator_compose.dataModel.HistoryData 23 | import com.equationl.calculator_compose.dataModel.Operator 24 | import com.equationl.calculator_compose.ui.theme.CalculatorComposeTheme 25 | import java.text.SimpleDateFormat 26 | import java.util.* 27 | 28 | /** 29 | * @param onDelete 如果 item 为 null 则表示删除所有历史记录,否则删除指定的 item 30 | * */ 31 | @OptIn(ExperimentalFoundationApi::class) 32 | @Composable 33 | fun HistoryWidget( 34 | historyList: List, 35 | onClick: (item: HistoryData) -> Unit, 36 | onDelete: (item: HistoryData?) -> Unit 37 | ) { 38 | 39 | Column( 40 | Modifier 41 | .fillMaxSize() 42 | .background(MaterialTheme.colors.background)) { 43 | LazyColumn(modifier = Modifier.weight(9f)) { 44 | items( 45 | items = historyList, 46 | key = { it.id }, 47 | ) { 48 | Column( 49 | horizontalAlignment = Alignment.End, 50 | modifier = Modifier 51 | .fillMaxWidth() 52 | .animateItemPlacement() 53 | .padding(8.dp) 54 | .combinedClickable( 55 | onClick = { onClick(it) }, 56 | onLongClick = { onDelete(it) } 57 | )) { 58 | Row( 59 | Modifier 60 | .fillMaxWidth() 61 | .padding(start = 8.dp), horizontalArrangement = Arrangement.Start) { 62 | val simpleDateFormat = SimpleDateFormat("MM-dd HH:mm:ss", Locale.CHINA) 63 | Text(text = simpleDateFormat.format(Date(it.createTime))) 64 | } 65 | Text(text = it.showText,fontSize = 22.sp, fontWeight = FontWeight.Light) 66 | Text(text = it.result, fontSize = 32.sp, fontWeight = FontWeight.Bold) 67 | } 68 | } 69 | } 70 | 71 | Row( 72 | Modifier 73 | .fillMaxSize() 74 | .weight(1f) 75 | .padding(16.dp), 76 | horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.Bottom 77 | ) { 78 | Icon( 79 | imageVector = Icons.Outlined.Delete, 80 | contentDescription = "delete", 81 | Modifier 82 | .fillMaxHeight() 83 | .clickable { 84 | onDelete(null) 85 | }) 86 | } 87 | } 88 | 89 | 90 | } 91 | 92 | @Preview(showSystemUi = true) 93 | @Composable 94 | fun HistoryPreview() { 95 | val testList = listOf( 96 | HistoryData(0, "1+1=", "1", "1", Operator.ADD, "2"), 97 | HistoryData(1, "1+1=", "1", "1", Operator.ADD, "2"), 98 | HistoryData(2, "1+1=", "1", "1", Operator.ADD, "2"), 99 | HistoryData(3, "1+1=", "1", "1", Operator.ADD, "2"), 100 | HistoryData(5, "1+1=", "1", "1", Operator.ADD, "2"), 101 | HistoryData(6, "1+1=", "1", "1", Operator.ADD, "2"), 102 | HistoryData(7, "1+1=", "1", "1", Operator.ADD, "2"), 103 | HistoryData(8, "1+1=", "1", "1", Operator.ADD, "2"), 104 | HistoryData(9, "1+1=", "1", "1", Operator.ADD, "2"), 105 | HistoryData(10, "1+1=", "1", "1", Operator.ADD, "2"), 106 | HistoryData(11, "1+1=", "1", "1", Operator.ADD, "2"), 107 | HistoryData(12, "1+1=", "1", "1", Operator.ADD, "2"), 108 | HistoryData(13, "1+1=", "1", "1", Operator.ADD, "2"), 109 | HistoryData(14, "1+1=", "1", "1", Operator.ADD, "2"), 110 | HistoryData(15, "1+1=", "1", "1", Operator.ADD, "2"), 111 | HistoryData(16, "1+1=", "1", "1", Operator.ADD, "2"), 112 | HistoryData(17, "1+1=", "1", "1", Operator.ADD, "2"), 113 | HistoryData(18, "1+1=", "1", "1", Operator.ADD, "2"), 114 | HistoryData(19, "1+1=", "1", "1", Operator.ADD, "2"), 115 | HistoryData(20, "1+1=", "1", "1", Operator.ADD, "2"), 116 | HistoryData(21, "1+1=", "1", "1", Operator.ADD, "2"), 117 | HistoryData(22, "1+1=", "1", "1", Operator.ADD, "2"), 118 | HistoryData(23, "1+1=", "1", "1", Operator.ADD, "2"), 119 | HistoryData(24, "1+1=", "1", "1", Operator.ADD, "2"), 120 | HistoryData(25, "1+1=", "1", "1", Operator.ADD, "2"), 121 | HistoryData(26, "1+1=", "1", "1", Operator.ADD, "2"), 122 | HistoryData(27, "1+1=", "1", "1", Operator.ADD, "2"), 123 | HistoryData(28, "1+1=", "1", "1", Operator.ADD, "2"), 124 | HistoryData(29, "1+1=", "1", "1", Operator.ADD, "2"), 125 | HistoryData(30, "1+1=", "1", "1", Operator.ADD, "2"), 126 | ) 127 | 128 | CalculatorComposeTheme(false) { 129 | HistoryWidget( 130 | historyList = testList, 131 | onClick = {}, 132 | onDelete = {} 133 | ) 134 | } 135 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/view/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.view 2 | 3 | import android.content.pm.ActivityInfo 4 | import android.content.res.Configuration 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material.Icon 8 | import androidx.compose.material.Text 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.outlined.FullscreenExit 11 | import androidx.compose.material.icons.outlined.History 12 | import androidx.compose.material.icons.outlined.ScreenRotation 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.LocalConfiguration 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import androidx.hilt.navigation.compose.hiltViewModel 23 | import com.equationl.calculator_compose.viewModel.HomeAction 24 | import com.equationl.calculator_compose.viewModel.HomeViewModel 25 | import com.equationl.calculator_compose.viewModel.StandardAction 26 | import com.equationl.calculator_compose.viewModel.StandardViewModel 27 | 28 | @Composable 29 | fun HomeScreen( 30 | homeViewModel: HomeViewModel = hiltViewModel(), 31 | standardViewModel: StandardViewModel = hiltViewModel() 32 | ) { 33 | val configuration = LocalConfiguration.current 34 | val context = LocalContext.current 35 | 36 | Column( 37 | Modifier 38 | .fillMaxSize() 39 | ) { 40 | 41 | MenuTitle( 42 | configuration = configuration, 43 | onClickMenu = { 44 | homeViewModel.dispatch( 45 | HomeAction.ClickMenu( 46 | orientation = 47 | if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) 48 | ActivityInfo.SCREEN_ORIENTATION_PORTRAIT 49 | else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, 50 | context = context 51 | ) 52 | ) 53 | }, 54 | onClickHistory = { 55 | standardViewModel.dispatch(StandardAction.ToggleHistory()) 56 | }, 57 | onClickOverlay = { 58 | homeViewModel.dispatch(HomeAction.ClickOverlay(context)) 59 | } 60 | ) 61 | 62 | if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { 63 | ProgrammerScreen() 64 | } 65 | else { 66 | StandardScreen() 67 | } 68 | 69 | } 70 | } 71 | 72 | @Composable 73 | private fun MenuTitle( 74 | configuration: Configuration, 75 | onClickMenu: () -> Unit, 76 | onClickHistory: () -> Unit, 77 | onClickOverlay: () -> Unit 78 | ) { 79 | Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { 80 | Row( 81 | verticalAlignment = Alignment.CenterVertically, 82 | modifier = Modifier.clickable { onClickMenu() } 83 | ) { 84 | Icon(imageVector = Icons.Outlined.ScreenRotation, 85 | contentDescription = "ScreenRotation", 86 | modifier = Modifier.padding(4.dp)) 87 | Text( 88 | text = if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) "程序员" else "标准", 89 | fontSize = 18.sp, 90 | fontWeight = FontWeight.Bold 91 | ) 92 | } 93 | if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { 94 | Row { 95 | Icon(imageVector = Icons.Outlined.History, 96 | contentDescription = "history", 97 | modifier = Modifier 98 | .padding(4.dp) 99 | .clickable { onClickHistory() } 100 | ) 101 | Icon(imageVector = Icons.Outlined.FullscreenExit, 102 | contentDescription = "overlay View", 103 | modifier = Modifier 104 | .padding(4.dp) 105 | .clickable { onClickOverlay() } 106 | ) 107 | } 108 | } 109 | } 110 | } 111 | 112 | @Preview(showSystemUi = true) 113 | @Composable 114 | fun PreviewMenuTitle() { 115 | val configuration = LocalConfiguration.current 116 | 117 | MenuTitle(configuration = configuration, onClickMenu = { }, onClickHistory = {}, onClickOverlay = {}) 118 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/view/OverlayScreen.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.view 2 | 3 | import androidx.compose.foundation.* 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.* 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.outlined.* 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.LaunchedEffect 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.text.font.FontWeight 15 | import androidx.compose.ui.tooling.preview.Preview 16 | import androidx.compose.ui.unit.dp 17 | import com.equationl.calculator_compose.dataModel.overlayKeyBoardBtn 18 | import com.equationl.calculator_compose.database.HistoryDb 19 | import com.equationl.calculator_compose.ui.theme.CalculatorComposeTheme 20 | import com.equationl.calculator_compose.ui.theme.OverlayLargeTextSize 21 | import com.equationl.calculator_compose.ui.theme.OverlayNormalTextSize 22 | import com.equationl.calculator_compose.utils.formatNumber 23 | import com.equationl.calculator_compose.view.widgets.scrollToLeftAnimation 24 | import com.equationl.calculator_compose.viewModel.OverLayViewModel 25 | import com.equationl.calculator_compose.viewModel.OverlayAction 26 | import com.equationl.calculator_compose.viewModel.StandardAction 27 | 28 | @Composable 29 | fun OverlayScreen( 30 | viewModel: OverLayViewModel 31 | ) { 32 | Column(Modifier.fillMaxSize()) { 33 | // 菜单 34 | TopMenu(viewModel) 35 | 36 | // 显示数据 37 | ShowScreen(viewModel) 38 | 39 | Divider(modifier = Modifier 40 | .fillMaxWidth() 41 | .padding(horizontal = 16.dp, vertical = 0.dp)) 42 | 43 | // 键盘 44 | StandardKeyBoard(viewModel) 45 | } 46 | } 47 | 48 | @Composable 49 | private fun TopMenu(viewModel: OverLayViewModel) { 50 | val context = LocalContext.current 51 | 52 | Row( 53 | Modifier.fillMaxWidth(), 54 | verticalAlignment = Alignment.CenterVertically, 55 | horizontalArrangement = Arrangement.SpaceBetween 56 | ) { 57 | Row { 58 | Icon( 59 | imageVector = Icons.Outlined.FormatSize, 60 | contentDescription = "adjust size", 61 | Modifier.clickable { 62 | viewModel.dispatch(OverlayAction.ClickAdjustSize) 63 | } 64 | ) 65 | 66 | Icon( 67 | imageVector = Icons.Outlined.InvertColors, 68 | contentDescription = "adjust transparent", 69 | Modifier.clickable { 70 | viewModel.dispatch(OverlayAction.ClickAdjustAlpha) 71 | } 72 | ) 73 | } 74 | 75 | Row { 76 | Icon( 77 | imageVector = Icons.Outlined.Fullscreen, 78 | contentDescription = "Back Full Screen", 79 | Modifier.clickable { 80 | viewModel.dispatch(OverlayAction.ClickBackFullScreen(context)) 81 | } 82 | ) 83 | 84 | Icon( 85 | imageVector = Icons.Outlined.Close, 86 | contentDescription = "close", 87 | Modifier.clickable { 88 | viewModel.dispatch(OverlayAction.ClickClose(context)) 89 | } 90 | ) 91 | } 92 | } 93 | } 94 | 95 | @Composable 96 | private fun ShowScreen(viewModel: OverLayViewModel) { 97 | val viewState = viewModel.viewStates 98 | val inputScrollerState = rememberScrollState() 99 | val showTextScrollerState = rememberScrollState() 100 | 101 | Column( 102 | Modifier.fillMaxWidth(), 103 | horizontalAlignment = Alignment.End, 104 | verticalArrangement = Arrangement.Center 105 | ) { 106 | Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { 107 | if (showTextScrollerState.value != showTextScrollerState.maxValue) { 108 | Icon( 109 | imageVector = Icons.Outlined.ArrowLeft, 110 | contentDescription = "scroll left", 111 | modifier = Modifier.absoluteOffset(x = scrollToLeftAnimation(-10f).dp) 112 | ) 113 | } 114 | Row( 115 | modifier = Modifier 116 | .padding(vertical = 8.dp) 117 | .padding(end = 8.dp) 118 | .horizontalScroll(showTextScrollerState, reverseScrolling = true) 119 | ) { 120 | Text( 121 | text = viewState.showText, 122 | fontSize = OverlayNormalTextSize, 123 | fontWeight = FontWeight.Light, 124 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 125 | ) 126 | } 127 | } 128 | 129 | Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { 130 | if (inputScrollerState.value != inputScrollerState.maxValue) { 131 | Icon( 132 | imageVector = Icons.Outlined.ArrowLeft, 133 | contentDescription = "scroll left", 134 | modifier = Modifier.absoluteOffset(x = scrollToLeftAnimation(-10f).dp) 135 | ) 136 | } 137 | 138 | Row(modifier = Modifier 139 | .padding(vertical = 8.dp) 140 | .padding(end = 8.dp) 141 | .horizontalScroll(inputScrollerState, reverseScrolling = true) 142 | ) { 143 | Text( 144 | text = viewState.inputValue.formatNumber(formatDecimal = viewState.isFinalResult), 145 | fontSize = OverlayLargeTextSize, 146 | fontWeight = FontWeight.Bold, 147 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 148 | ) 149 | LaunchedEffect(Unit) { 150 | inputScrollerState.scrollTo(0) 151 | } 152 | } 153 | } 154 | } 155 | } 156 | 157 | @Composable 158 | private fun StandardKeyBoard(viewModel: OverLayViewModel) { 159 | Column(modifier = Modifier.fillMaxSize()) { 160 | for (btnRow in overlayKeyBoardBtn()) { 161 | Row(modifier = Modifier 162 | .fillMaxWidth() 163 | .weight(1f)) { 164 | for (btn in btnRow) { 165 | Row(modifier = Modifier.weight(1f)) { 166 | KeyBoardButton( 167 | text = btn.text, 168 | onClick = { viewModel.dispatch(StandardAction.ClickBtn(btn.index)) }, 169 | backGround = btn.background, 170 | paddingValues = PaddingValues(0.5.dp), 171 | isFilled = btn.isFilled 172 | ) 173 | } 174 | } 175 | } 176 | } 177 | } 178 | } 179 | 180 | @OptIn(ExperimentalMaterialApi::class) 181 | @Composable 182 | private fun KeyBoardButton( 183 | text: String, 184 | onClick: () -> Unit, 185 | backGround: Color = Color.White, 186 | isFilled: Boolean = false, 187 | paddingValues: PaddingValues = PaddingValues(0.dp) 188 | ) { 189 | Card( 190 | onClick = { onClick() }, 191 | modifier = Modifier 192 | .fillMaxSize() 193 | .padding(paddingValues), 194 | backgroundColor = if (isFilled) backGround else MaterialTheme.colors.surface, 195 | shape = MaterialTheme.shapes.large, 196 | elevation = 0.dp, 197 | border = BorderStroke(0.dp, Color.Transparent) 198 | ) { 199 | Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { 200 | Text(text, fontSize = OverlayLargeTextSize, color = if (isFilled) Color.Unspecified else backGround) 201 | } 202 | } 203 | } 204 | 205 | @Preview(showSystemUi = true) 206 | @Composable 207 | fun PreviewOverlayScreen() { 208 | CalculatorComposeTheme(false) { 209 | Column( 210 | Modifier 211 | .fillMaxSize() 212 | .background(MaterialTheme.colors.background)) { 213 | OverlayScreen(OverLayViewModel(HistoryDb.create(LocalContext.current, false))) 214 | } 215 | } 216 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/view/ProgrammerScreen.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.view 2 | 3 | import androidx.compose.animation.* 4 | import androidx.compose.foundation.BorderStroke 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.text.selection.SelectionContainer 9 | import androidx.compose.material.* 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.text.font.FontWeight 15 | import androidx.compose.ui.tooling.preview.Devices 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | import androidx.hilt.navigation.compose.hiltViewModel 20 | import com.equationl.calculator_compose.dataModel.InputBase 21 | import com.equationl.calculator_compose.dataModel.programmerFunctionKeyBoardBtn 22 | import com.equationl.calculator_compose.dataModel.programmerNumberKeyBoardBtn 23 | import com.equationl.calculator_compose.ui.theme.* 24 | import com.equationl.calculator_compose.utils.formatNumber 25 | import com.equationl.calculator_compose.view.widgets.AutoSizeText 26 | import com.equationl.calculator_compose.viewModel.ProgrammerAction 27 | import com.equationl.calculator_compose.viewModel.ProgrammerViewModel 28 | 29 | @Composable 30 | fun ProgrammerScreen( 31 | viewModel: ProgrammerViewModel = hiltViewModel() 32 | ) { 33 | Row(modifier = Modifier.fillMaxWidth(), 34 | horizontalArrangement = Arrangement.SpaceBetween) { 35 | // 左侧键盘 36 | Row(modifier = Modifier.weight(1.3f)) { 37 | FunctionKeyBoard(viewModel = viewModel) 38 | } 39 | 40 | Divider(modifier = Modifier 41 | .fillMaxHeight() 42 | .width(1.dp) 43 | .padding(vertical = 16.dp, horizontal = 0.dp)) 44 | 45 | // 显示数据 46 | Row(modifier = Modifier.weight(2f)) { 47 | CenterScreen(viewModel = viewModel) 48 | } 49 | 50 | Divider(modifier = Modifier 51 | .fillMaxHeight() 52 | .width(1.dp) 53 | .padding(vertical = 16.dp, horizontal = 0.dp)) 54 | 55 | // 右侧键盘 56 | Row(modifier = Modifier.weight(1.5f)) { 57 | NumberBoard(viewModel = viewModel) 58 | } 59 | } 60 | } 61 | 62 | @OptIn(ExperimentalAnimationApi::class) 63 | @Composable 64 | private fun CenterScreen(viewModel: ProgrammerViewModel) { 65 | val viewState = viewModel.viewStates 66 | Column( 67 | Modifier 68 | .fillMaxSize(), 69 | verticalArrangement = Arrangement.SpaceAround 70 | ) { 71 | Column( 72 | modifier = Modifier.fillMaxWidth(), 73 | horizontalAlignment = Alignment.End 74 | ) { 75 | // 计算公式 76 | AnimatedContent(targetState = viewState.showText) { targetState: String -> 77 | SelectionContainer { 78 | Text( 79 | text = targetState, 80 | modifier = Modifier.padding(8.dp), 81 | fontSize = ShowNormalFontSize, 82 | fontWeight = FontWeight.Light, 83 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 84 | ) 85 | } 86 | } 87 | // 输入值或计算结果 88 | AnimatedContent( 89 | targetState = viewState.inputValue, 90 | transitionSpec = { 91 | if (targetState.length > initialState.length) { 92 | slideInVertically { height -> height } + fadeIn() with 93 | slideOutVertically { height -> -height } + fadeOut() 94 | } else { 95 | slideInVertically { height -> -height } + fadeIn() with 96 | slideOutVertically { height -> height } + fadeOut() 97 | }.using( 98 | SizeTransform(clip = false) 99 | ) 100 | } 101 | ) { targetState: String -> 102 | Row(modifier = Modifier.padding(8.dp)) { 103 | SelectionContainer { 104 | AutoSizeText( 105 | text = targetState.formatNumber( 106 | formatDecimal = false, // 程序员计算没有小数 107 | addSplitChar = if (viewState.inputBase == InputBase.DEC) "," else " ", 108 | splitLength = if (viewState.inputBase == InputBase.HEX || viewState.inputBase == InputBase.BIN) 4 else 3, 109 | isAddLeadingZero = false, // 即使是二进制,在输入时也不应该有前导0 110 | formatInteger = true 111 | ) 112 | , 113 | fontSize = InputLargeFontSize, 114 | fontWeight = FontWeight.Bold, 115 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 116 | ) 117 | } 118 | } 119 | } 120 | } 121 | 122 | 123 | Column( 124 | modifier = Modifier.fillMaxWidth(), 125 | horizontalAlignment = Alignment.Start 126 | ) { 127 | Row(verticalAlignment = Alignment.CenterVertically, 128 | modifier = Modifier 129 | .padding(2.dp) 130 | .clickable { viewModel.dispatch(ProgrammerAction.ChangeInputBase(InputBase.HEX)) } 131 | ) { 132 | Text( 133 | text = "HEX", 134 | fontSize = 135 | if (viewState.inputBase == InputBase.HEX) InputTitleContentSize 136 | else InputNormalFontSize, 137 | fontWeight = if (viewState.inputBase == InputBase.HEX) FontWeight.Bold else null, 138 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 139 | ) 140 | 141 | SelectionContainer { 142 | Text( 143 | text = viewState.inputHexText.formatNumber(addSplitChar = " ", splitLength = 4), 144 | fontSize = InputNormalFontSize, 145 | modifier = Modifier.padding(start = 8.dp), 146 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 147 | ) 148 | } 149 | } 150 | Row(verticalAlignment = Alignment.CenterVertically, 151 | modifier = Modifier 152 | .padding(2.dp) 153 | .clickable { viewModel.dispatch(ProgrammerAction.ChangeInputBase(InputBase.DEC)) } 154 | ) { 155 | Text( 156 | text = "DEC", 157 | fontSize = 158 | if (viewState.inputBase == InputBase.DEC) InputTitleContentSize 159 | else InputNormalFontSize, 160 | fontWeight = if (viewState.inputBase == InputBase.DEC) FontWeight.Bold else null, 161 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 162 | ) 163 | 164 | SelectionContainer { 165 | Text( 166 | text = viewState.inputDecText.formatNumber(), 167 | fontSize = InputNormalFontSize, 168 | modifier = Modifier.padding(start = 8.dp), 169 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 170 | ) 171 | } 172 | 173 | } 174 | Row(verticalAlignment = Alignment.CenterVertically, 175 | modifier = Modifier 176 | .padding(2.dp) 177 | .clickable { viewModel.dispatch(ProgrammerAction.ChangeInputBase(InputBase.OCT)) } 178 | ) { 179 | Text( 180 | text = "OCT", 181 | fontSize = 182 | if (viewState.inputBase == InputBase.OCT) InputTitleContentSize 183 | else InputNormalFontSize, 184 | fontWeight = if (viewState.inputBase == InputBase.OCT) FontWeight.Bold else null, 185 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 186 | ) 187 | 188 | SelectionContainer { 189 | Text( 190 | text = viewState.inputOctText.formatNumber(addSplitChar = " "), 191 | fontSize = InputNormalFontSize, 192 | modifier = Modifier.padding(start = 8.dp), 193 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 194 | ) 195 | } 196 | 197 | } 198 | Row(verticalAlignment = Alignment.CenterVertically, 199 | modifier = Modifier 200 | .padding(2.dp) 201 | .clickable { viewModel.dispatch(ProgrammerAction.ChangeInputBase(InputBase.BIN)) } 202 | ) { 203 | Text( 204 | text = "BIN", 205 | fontSize = 206 | if (viewState.inputBase == InputBase.BIN) InputTitleContentSize 207 | else InputNormalFontSize, 208 | fontWeight = if (viewState.inputBase == InputBase.BIN) FontWeight.Bold else null, 209 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 210 | ) 211 | 212 | SelectionContainer { 213 | Text( 214 | text = viewState.inputBinText.formatNumber(addSplitChar = " ", splitLength = 4, isAddLeadingZero = viewState.inputBinText != "0"), 215 | fontSize = InputNormalFontSize, 216 | modifier = Modifier 217 | .padding(start = 8.dp), 218 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 219 | ) 220 | } 221 | } 222 | } 223 | } 224 | } 225 | 226 | @Composable 227 | private fun NumberBoard(viewModel: ProgrammerViewModel) { 228 | val viewState = viewModel.viewStates 229 | 230 | Column(modifier = Modifier.fillMaxSize()) { 231 | for (btnRow in programmerNumberKeyBoardBtn()) { 232 | Row(modifier = Modifier 233 | .fillMaxWidth() 234 | .weight(1f)) { 235 | for (btn in btnRow) { 236 | val isAvailable = if (btn.isAvailable) { 237 | btn.index !in viewState.inputBase.forbidBtn 238 | } 239 | else { 240 | false 241 | } 242 | 243 | Row(modifier = Modifier.weight(1f)) { 244 | KeyBoardButton( 245 | text = btn.text, 246 | onClick = { viewModel.dispatch(ProgrammerAction.ClickBtn(btn.index)) }, 247 | isAvailable = isAvailable, 248 | backGround = btn.background, 249 | isFilled = btn.isFilled, 250 | paddingValues = PaddingValues(0.5.dp) 251 | ) 252 | } 253 | } 254 | } 255 | } 256 | } 257 | } 258 | 259 | @Composable 260 | private fun FunctionKeyBoard(viewModel: ProgrammerViewModel) { 261 | val viewState = viewModel.viewStates 262 | 263 | Column(modifier = Modifier.fillMaxSize()) { 264 | for (btnRow in programmerFunctionKeyBoardBtn()) { 265 | Row(modifier = Modifier 266 | .fillMaxWidth() 267 | .weight(1f)) { 268 | for (btn in btnRow) { 269 | val isAvailable = if (btn.isAvailable) { 270 | btn.index !in viewState.inputBase.forbidBtn 271 | } 272 | else { 273 | false 274 | } 275 | 276 | Row(modifier = Modifier.weight(1f)) { 277 | KeyBoardButton( 278 | text = btn.text, 279 | onClick = { viewModel.dispatch(ProgrammerAction.ClickBtn(btn.index)) }, 280 | isAvailable = isAvailable, 281 | backGround = btn.background, 282 | isFilled = btn.isFilled, 283 | paddingValues = PaddingValues(0.5.dp) 284 | ) 285 | } 286 | } 287 | } 288 | } 289 | } 290 | } 291 | 292 | @OptIn(ExperimentalMaterialApi::class) 293 | @Composable 294 | private fun KeyBoardButton( 295 | text: String, 296 | onClick: () -> Unit, 297 | isAvailable: Boolean = true, 298 | backGround: Color = Color.White, 299 | isFilled: Boolean = false, 300 | paddingValues: PaddingValues = PaddingValues(0.dp) 301 | ) { 302 | Card( 303 | onClick = { onClick() }, 304 | modifier = Modifier 305 | .fillMaxSize() 306 | .padding(paddingValues), 307 | backgroundColor = if (isFilled) backGround else MaterialTheme.colors.surface, 308 | shape = MaterialTheme.shapes.large, 309 | elevation = 0.dp, 310 | border = BorderStroke(0.dp, Color.Transparent), 311 | enabled = isAvailable 312 | ) { 313 | Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { 314 | Text( 315 | text, 316 | fontSize = 24.sp, 317 | color = if (isAvailable) { 318 | if (isFilled) Color.Unspecified else backGround 319 | } else { 320 | if (MaterialTheme.colors.isLight) Color.LightGray else Color.DarkGray 321 | } 322 | ) 323 | } 324 | } 325 | } 326 | 327 | @Preview(showSystemUi = true, device = Devices.AUTOMOTIVE_1024p, widthDp = 1024, heightDp = 720) 328 | @Composable 329 | fun PreviewProgrammerScreen() { 330 | CalculatorComposeTheme(false) { 331 | Column( 332 | Modifier 333 | .fillMaxSize() 334 | .background(MaterialTheme.colors.background)) { 335 | ProgrammerScreen(ProgrammerViewModel()) 336 | } 337 | } 338 | } 339 | 340 | @Preview(showSystemUi = true, device = Devices.AUTOMOTIVE_1024p, widthDp = 1024, heightDp = 720) 341 | @Composable 342 | fun PreviewProgrammerScreenDark() { 343 | CalculatorComposeTheme(true) { 344 | Column( 345 | Modifier 346 | .fillMaxSize() 347 | .background(MaterialTheme.colors.background)) { 348 | ProgrammerScreen(ProgrammerViewModel()) 349 | } 350 | } 351 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/view/StandardScreen.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.view 2 | 3 | import androidx.compose.animation.* 4 | import androidx.compose.foundation.BorderStroke 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.horizontalScroll 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.rememberScrollState 9 | import androidx.compose.foundation.text.selection.SelectionContainer 10 | import androidx.compose.material.* 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.outlined.ArrowLeft 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.LaunchedEffect 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.alpha 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.tooling.preview.Preview 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import androidx.hilt.navigation.compose.hiltViewModel 25 | import com.equationl.calculator_compose.dataModel.standardKeyBoardBtn 26 | import com.equationl.calculator_compose.database.HistoryDb 27 | import com.equationl.calculator_compose.ui.theme.CalculatorComposeTheme 28 | import com.equationl.calculator_compose.ui.theme.InputLargeFontSize 29 | import com.equationl.calculator_compose.ui.theme.ShowNormalFontSize 30 | import com.equationl.calculator_compose.ui.theme.ShowSmallFontSize 31 | import com.equationl.calculator_compose.utils.formatNumber 32 | import com.equationl.calculator_compose.utils.noRippleClickable 33 | import com.equationl.calculator_compose.view.widgets.AutoSizeText 34 | import com.equationl.calculator_compose.view.widgets.scrollToLeftAnimation 35 | import com.equationl.calculator_compose.viewModel.StandardAction 36 | import com.equationl.calculator_compose.viewModel.StandardViewModel 37 | 38 | @Composable 39 | fun StandardScreen( 40 | viewModel: StandardViewModel = hiltViewModel() 41 | ) { 42 | val viewState = viewModel.viewStates 43 | 44 | // 显示数据 45 | ShowScreen(viewModel) 46 | 47 | Divider(modifier = Modifier 48 | .fillMaxWidth() 49 | .padding(horizontal = 16.dp, vertical = 0.dp)) 50 | 51 | // 键盘与历史记录 52 | Box(Modifier.fillMaxSize()) { 53 | val isShowKeyBoard = viewState.historyList.isEmpty() 54 | 55 | StandardKeyBoard(viewModel) 56 | 57 | AnimatedVisibility( 58 | visible = !isShowKeyBoard, 59 | enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), 60 | exit = slideOutVertically(targetOffsetY = { it }) + fadeOut() 61 | ) { 62 | HistoryWidget( 63 | historyList = viewState.historyList, 64 | onClick = { viewModel.dispatch(StandardAction.ReadFromHistory(it)) }, 65 | onDelete = { viewModel.dispatch(StandardAction.DeleteHistory(it)) }) 66 | } 67 | } 68 | } 69 | 70 | @OptIn(ExperimentalAnimationApi::class) 71 | @Composable 72 | private fun ShowScreen(viewModel: StandardViewModel) { 73 | val viewState = viewModel.viewStates 74 | val inputScrollerState = rememberScrollState() 75 | val showTextScrollerState = rememberScrollState() 76 | 77 | Column( 78 | Modifier 79 | .fillMaxWidth() 80 | .fillMaxHeight(0.4f) 81 | .noRippleClickable { viewModel.dispatch(StandardAction.ToggleHistory(true)) } 82 | , 83 | horizontalAlignment = Alignment.End, 84 | verticalArrangement = Arrangement.SpaceAround 85 | ) { 86 | // 上一个计算结果 87 | AnimatedContent(targetState = viewState.lastShowText) { targetState: String -> 88 | SelectionContainer { 89 | AutoSizeText( 90 | text = targetState, 91 | fontSize = ShowSmallFontSize, 92 | fontWeight = FontWeight.Light, 93 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary, 94 | modifier = Modifier 95 | .padding(horizontal = 12.dp) 96 | .padding(bottom = 16.dp) 97 | .alpha(0.5f), 98 | minSize = 10.sp 99 | ) 100 | } 101 | } 102 | 103 | Column(horizontalAlignment = Alignment.End) { 104 | // 计算公式 105 | AnimatedContent(targetState = viewState.showText) { targetState: String -> 106 | Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { 107 | if (showTextScrollerState.value != showTextScrollerState.maxValue) { 108 | Icon( 109 | imageVector = Icons.Outlined.ArrowLeft, 110 | contentDescription = "scroll left", 111 | modifier = Modifier.absoluteOffset(x = scrollToLeftAnimation(-10f).dp) 112 | ) 113 | } 114 | Row( 115 | modifier = Modifier 116 | .padding(vertical = 8.dp) 117 | .padding(end = 8.dp) 118 | .horizontalScroll(showTextScrollerState, reverseScrolling = true) 119 | ) { 120 | SelectionContainer { 121 | Text( 122 | text = if (targetState.length > 5000) "数字过长" else targetState, 123 | fontSize = ShowNormalFontSize, 124 | fontWeight = FontWeight.Light, 125 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 126 | ) 127 | } 128 | } 129 | } 130 | } 131 | 132 | // 输入值或计算结果 133 | AnimatedContent( 134 | targetState = viewState.inputValue, 135 | transitionSpec = { 136 | if (targetState.length > initialState.length) { 137 | slideInVertically { height -> height } + fadeIn() with 138 | slideOutVertically { height -> -height } + fadeOut() 139 | } else { 140 | slideInVertically { height -> -height } + fadeIn() with 141 | slideOutVertically { height -> height } + fadeOut() 142 | }.using( 143 | SizeTransform(clip = false) 144 | ) 145 | } 146 | ) { targetState: String -> 147 | Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { 148 | if (inputScrollerState.value != inputScrollerState.maxValue) { 149 | Icon( 150 | imageVector = Icons.Outlined.ArrowLeft, 151 | contentDescription = "scroll left", 152 | modifier = Modifier.absoluteOffset(x = scrollToLeftAnimation(-10f).dp) 153 | ) 154 | } 155 | 156 | Row(modifier = Modifier 157 | .padding(vertical = 8.dp) 158 | .padding(end = 8.dp) 159 | .horizontalScroll(inputScrollerState, reverseScrolling = true) 160 | ) { 161 | SelectionContainer { 162 | Text( 163 | text = targetState.formatNumber(formatDecimal = viewState.isFinalResult), 164 | fontSize = InputLargeFontSize, 165 | fontWeight = FontWeight.Bold, 166 | color = if (MaterialTheme.colors.isLight) Color.Unspecified else MaterialTheme.colors.primary 167 | ) 168 | } 169 | LaunchedEffect(Unit) { 170 | inputScrollerState.scrollTo(0) 171 | } 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | 179 | @Composable 180 | private fun StandardKeyBoard(viewModel: StandardViewModel) { 181 | Column(modifier = Modifier.fillMaxSize()) { 182 | for (btnRow in standardKeyBoardBtn()) { 183 | Row(modifier = Modifier 184 | .fillMaxWidth() 185 | .weight(1f)) { 186 | for (btn in btnRow) { 187 | Row(modifier = Modifier.weight(1f)) { 188 | KeyBoardButton( 189 | text = btn.text, 190 | onClick = { viewModel.dispatch(StandardAction.ClickBtn(btn.index)) }, 191 | backGround = btn.background, 192 | paddingValues = PaddingValues(0.5.dp), 193 | isFilled = btn.isFilled 194 | ) 195 | } 196 | } 197 | } 198 | } 199 | } 200 | } 201 | 202 | @OptIn(ExperimentalMaterialApi::class) 203 | @Composable 204 | private fun KeyBoardButton( 205 | text: String, 206 | onClick: () -> Unit, 207 | backGround: Color = Color.White, 208 | isFilled: Boolean = false, 209 | paddingValues: PaddingValues = PaddingValues(0.dp) 210 | ) { 211 | Card( 212 | onClick = { onClick() }, 213 | modifier = Modifier 214 | .fillMaxSize() 215 | .padding(paddingValues), 216 | backgroundColor = if (isFilled) backGround else MaterialTheme.colors.surface, 217 | shape = MaterialTheme.shapes.large, 218 | elevation = 0.dp, 219 | border = BorderStroke(0.dp, Color.Transparent) 220 | ) { 221 | Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { 222 | Text(text, fontSize = 32.sp, color = if (isFilled) Color.Unspecified else backGround) 223 | } 224 | } 225 | } 226 | 227 | @Preview(showSystemUi = true) 228 | @Composable 229 | fun PreviewStandardScreen() { 230 | CalculatorComposeTheme(false) { 231 | Column( 232 | Modifier 233 | .fillMaxSize() 234 | .background(MaterialTheme.colors.background)) { 235 | StandardScreen(StandardViewModel(HistoryDb.create(LocalContext.current, false))) 236 | } 237 | } 238 | } 239 | 240 | @Preview(showSystemUi = true) 241 | @Composable 242 | fun PreviewStandardScreenDark() { 243 | CalculatorComposeTheme(true) { 244 | Column( 245 | Modifier 246 | .fillMaxSize() 247 | .background(MaterialTheme.colors.background)) { 248 | StandardScreen(StandardViewModel(HistoryDb.create(LocalContext.current, false))) 249 | } 250 | } 251 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/view/widgets/Animation.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.view.widgets 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.getValue 6 | 7 | @Composable 8 | fun scrollToLeftAnimation(targetValue: Float = -5f): Float { 9 | val infiniteTransition = rememberInfiniteTransition() 10 | val slipUpYAnimation by infiniteTransition.animateFloat( 11 | initialValue = 0f, 12 | targetValue = targetValue, 13 | animationSpec = infiniteRepeatable( 14 | animation = tween(2000), 15 | repeatMode = RepeatMode.Restart 16 | ) 17 | ) 18 | return slipUpYAnimation 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/view/widgets/AutoSizeFont.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.view.widgets 2 | 3 | import androidx.compose.foundation.layout.BoxWithConstraints 4 | import androidx.compose.material.LocalTextStyle 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.platform.LocalDensity 10 | import androidx.compose.ui.platform.LocalFontFamilyResolver 11 | import androidx.compose.ui.text.ParagraphIntrinsics 12 | import androidx.compose.ui.text.TextLayoutResult 13 | import androidx.compose.ui.text.TextStyle 14 | import androidx.compose.ui.text.font.FontFamily 15 | import androidx.compose.ui.text.font.FontStyle 16 | import androidx.compose.ui.text.font.FontWeight 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.text.style.TextDecoration 19 | import androidx.compose.ui.text.style.TextOverflow 20 | import androidx.compose.ui.unit.TextUnit 21 | import androidx.compose.ui.unit.sp 22 | 23 | /** 24 | * @author Róbert Nagy 25 | * 26 | * @link https://stackoverflow.com/a/69735469 27 | * 28 | * Edit by equationl (http://likehide.com) 29 | * 30 | * */ 31 | @Composable 32 | fun AutoSizeText( 33 | text: String, 34 | modifier: Modifier = Modifier, 35 | color: Color = Color.Unspecified, 36 | fontSize: TextUnit = TextUnit.Unspecified, 37 | fontStyle: FontStyle? = null, 38 | fontWeight: FontWeight? = null, 39 | fontFamily: FontFamily? = null, 40 | letterSpacing: TextUnit = TextUnit.Unspecified, 41 | textDecoration: TextDecoration? = null, 42 | textAlign: TextAlign? = null, 43 | lineHeight: TextUnit = TextUnit.Unspecified, 44 | onTextLayout: (TextLayoutResult) -> Unit = {}, 45 | style: TextStyle = LocalTextStyle.current, 46 | minSize: TextUnit = 12.sp 47 | ) { 48 | BoxWithConstraints { 49 | var shrunkFontSize = fontSize 50 | 51 | if (shrunkFontSize >= minSize) { 52 | val calculateIntrinsics = @Composable { 53 | ParagraphIntrinsics( 54 | text, TextStyle( 55 | color = color, 56 | fontSize = shrunkFontSize, 57 | fontWeight = fontWeight, 58 | textAlign = textAlign, 59 | lineHeight = lineHeight, 60 | fontFamily = fontFamily, 61 | textDecoration = textDecoration, 62 | fontStyle = fontStyle, 63 | letterSpacing = letterSpacing 64 | ), 65 | listOf(), listOf(), LocalDensity.current, 66 | LocalFontFamilyResolver.current 67 | ) 68 | } 69 | 70 | var intrinsics = calculateIntrinsics() 71 | with(LocalDensity.current) { 72 | while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) { 73 | shrunkFontSize *= 0.9 74 | if (shrunkFontSize < minSize) { 75 | shrunkFontSize = minSize 76 | break 77 | } 78 | intrinsics = calculateIntrinsics() 79 | } 80 | } 81 | } 82 | Text( 83 | text = text, 84 | modifier = modifier, 85 | color = color, 86 | fontSize = shrunkFontSize, 87 | fontStyle = fontStyle, 88 | fontWeight = fontWeight, 89 | fontFamily = fontFamily, 90 | letterSpacing = letterSpacing, 91 | textDecoration = textDecoration, 92 | textAlign = textAlign, 93 | lineHeight = lineHeight, 94 | onTextLayout = onTextLayout, 95 | style = style, 96 | maxLines = 1, 97 | overflow = if (shrunkFontSize <= minSize) TextOverflow.Ellipsis else TextOverflow.Clip 98 | ) 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/viewModel/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.viewModel 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import android.content.Intent 7 | import android.net.Uri 8 | import android.os.Build 9 | import android.provider.Settings 10 | import android.widget.Toast 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.setValue 14 | import androidx.lifecycle.ViewModel 15 | import com.equationl.calculator_compose.overlay.OverlayService 16 | import com.equationl.calculator_compose.utils.VibratorHelper 17 | import dagger.hilt.android.lifecycle.HiltViewModel 18 | import javax.inject.Inject 19 | 20 | @HiltViewModel 21 | class HomeViewModel @Inject constructor( 22 | ) : ViewModel() { 23 | 24 | var viewStates by mutableStateOf(HomeState()) 25 | private set 26 | 27 | fun dispatch(action: HomeAction) { 28 | when (action) { 29 | is HomeAction.ClickMenu -> changeScreenOrientation(action.orientation, action.context) 30 | is HomeAction.ClickOverlay -> clickOverlay(action.context) 31 | } 32 | } 33 | 34 | private fun clickOverlay(context: Context) { 35 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 36 | if (Settings.canDrawOverlays(context)) { 37 | context.startService(Intent(context, OverlayService::class.java)) 38 | 39 | // 返回主页 40 | Intent(Intent.ACTION_MAIN).apply{ 41 | addCategory(Intent.CATEGORY_HOME) 42 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 43 | }.let { context.startActivity(it) } 44 | } 45 | else { 46 | Toast.makeText(context, "请授予“显示在其他应用上层”权限后重试", Toast.LENGTH_LONG).show() 47 | val intent = Intent( 48 | Settings.ACTION_MANAGE_OVERLAY_PERMISSION, 49 | Uri.parse("package:${context.packageName}") 50 | ) 51 | context.startActivity(intent) 52 | } 53 | } 54 | else { 55 | Toast.makeText(context, "当前系统不支持!", Toast.LENGTH_LONG).show() 56 | } 57 | } 58 | 59 | private fun changeScreenOrientation(orientation: Int, context: Context) { 60 | VibratorHelper.instance.vibrateOnClick() 61 | val activity = context.findActivity() ?: return 62 | activity.requestedOrientation = orientation 63 | } 64 | 65 | private fun Context.findActivity(): Activity? = when (this) { 66 | is Activity -> this 67 | is ContextWrapper -> baseContext.findActivity() 68 | else -> null 69 | } 70 | } 71 | 72 | data class HomeState( 73 | val test: String = "" 74 | ) 75 | 76 | sealed class HomeAction { 77 | data class ClickOverlay(val context: Context): HomeAction() 78 | data class ClickMenu(val orientation: Int, val context: Context): HomeAction() 79 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/viewModel/OverLayViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.viewModel 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Build 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.setValue 9 | import androidx.lifecycle.viewModelScope 10 | import com.equationl.calculator_compose.MainActivity 11 | import com.equationl.calculator_compose.database.HistoryDb 12 | import com.equationl.calculator_compose.overlay.OverlayService 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.channels.Channel 15 | import kotlinx.coroutines.flow.receiveAsFlow 16 | import kotlinx.coroutines.launch 17 | import javax.inject.Inject 18 | 19 | @HiltViewModel 20 | class OverLayViewModel @Inject constructor( 21 | dataBase: HistoryDb 22 | ): StandardViewModel(dataBase) { 23 | 24 | var overlayState by mutableStateOf(OverlayState()) 25 | private set 26 | 27 | private val _viewEvents = Channel(Channel.BUFFERED) 28 | val viewEvents = _viewEvents.receiveAsFlow() 29 | 30 | private var viewScale: Float = 3f 31 | 32 | override fun dispatch(action: StandardAction) { 33 | super.dispatch(action) 34 | 35 | when (action) { 36 | is OverlayAction.ClickClose -> clickClose(action.context) 37 | is OverlayAction.ClickAdjustSize -> clickAdjustSize() 38 | is OverlayAction.ClickAdjustAlpha -> clickAdjustAlpha() 39 | is OverlayAction.ClickBackFullScreen -> clickBackFullScreen(action.context) 40 | else -> { 41 | 42 | } 43 | } 44 | } 45 | 46 | private fun clickBackFullScreen(context: Context) { 47 | context.startActivity( 48 | Intent(context, MainActivity::class.java).apply { 49 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 50 | } 51 | ) 52 | } 53 | 54 | private fun clickAdjustAlpha() { 55 | var alpha = overlayState.backgroundAlpha 56 | 57 | alpha += 0.2f 58 | 59 | if (alpha > 1f) { 60 | alpha = 0.2f 61 | } 62 | 63 | overlayState = overlayState.copy(backgroundAlpha = alpha) 64 | } 65 | 66 | private fun clickClose(context: Context) { 67 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 68 | context.stopService(Intent(context, OverlayService::class.java)) 69 | } 70 | } 71 | 72 | private fun clickAdjustSize() { 73 | viewScale += 0.5f 74 | if (viewScale > 3) viewScale = 1f 75 | 76 | viewModelScope.launch { 77 | _viewEvents.send(OverlayEvent.ChangeSize(viewScale)) 78 | } 79 | } 80 | 81 | } 82 | 83 | data class OverlayState( 84 | val backgroundAlpha: Float = 1f 85 | ) 86 | 87 | sealed class OverlayAction: StandardAction() { 88 | object ClickAdjustSize: OverlayAction() 89 | object ClickAdjustAlpha: OverlayAction() 90 | data class ClickClose(val context: Context): OverlayAction() 91 | data class ClickBackFullScreen(val context: Context): OverlayAction() 92 | } 93 | 94 | sealed class OverlayEvent { 95 | data class ChangeSize(val scale: Float): OverlayEvent() 96 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/viewModel/ProgrammerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.viewModel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import com.equationl.calculator_compose.dataModel.* 8 | import com.equationl.calculator_compose.utils.VibratorHelper 9 | import com.equationl.calculator_compose.utils.calculate 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import java.math.BigInteger 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class ProgrammerViewModel @Inject constructor(): ViewModel() { 16 | 17 | var viewStates by mutableStateOf(ProgrammerState()) 18 | private set 19 | 20 | fun dispatch(action: ProgrammerAction) { 21 | when (action) { 22 | is ProgrammerAction.ChangeInputBase -> changeInputBase(action.inputBase) 23 | is ProgrammerAction.ClickBtn -> clickBtn(action.no) 24 | } 25 | } 26 | 27 | /**标记第一个值输入后,是否开始输入第二个值*/ 28 | private var isInputSecondValue: Boolean = false 29 | /**标记是否已计算最终结果*/ 30 | private var isCalculated: Boolean = false 31 | /**标记是否添加了非四则运算的“高级”运算符*/ 32 | private var isAdvancedCalculated: Boolean = false 33 | /**标记是否处于错误状态*/ 34 | private var isErr: Boolean = false 35 | 36 | private fun changeInputBase(inputBase: InputBase) { 37 | VibratorHelper.instance.vibrateOnClick() 38 | viewStates = when (inputBase) { 39 | InputBase.HEX -> { 40 | if (viewStates.lastInputValue.isNotEmpty()) { 41 | viewStates.copy( 42 | inputBase = inputBase, 43 | inputValue = viewStates.inputHexText, 44 | lastInputValue = viewStates.lastInputValue.baseConversion(inputBase) 45 | ) 46 | } 47 | else { 48 | viewStates.copy(inputBase = inputBase, inputValue = viewStates.inputHexText) 49 | } 50 | } 51 | InputBase.DEC -> { 52 | if (viewStates.lastInputValue.isNotEmpty()) { 53 | viewStates.copy(inputBase = inputBase, 54 | inputValue = viewStates.inputDecText, 55 | lastInputValue = viewStates.lastInputValue.baseConversion(inputBase) 56 | ) 57 | } 58 | else { 59 | viewStates.copy(inputBase = inputBase, inputValue = viewStates.inputDecText) 60 | } 61 | } 62 | InputBase.OCT -> { 63 | if (viewStates.lastInputValue.isNotEmpty()) { 64 | viewStates.copy(inputBase = inputBase, 65 | inputValue = viewStates.inputOctText, 66 | lastInputValue = viewStates.lastInputValue.baseConversion(inputBase) 67 | ) 68 | 69 | } 70 | else { 71 | viewStates.copy(inputBase = inputBase, inputValue = viewStates.inputOctText) 72 | } 73 | } 74 | InputBase.BIN -> { 75 | if (viewStates.lastInputValue.isNotEmpty()) { 76 | viewStates.copy(inputBase = inputBase, 77 | inputValue = viewStates.inputBinText, 78 | lastInputValue = viewStates.lastInputValue.baseConversion(inputBase) 79 | ) 80 | } 81 | else { 82 | viewStates.copy(inputBase = inputBase, inputValue = viewStates.inputBinText) 83 | } 84 | } 85 | } 86 | } 87 | 88 | private fun clickBtn(no: Int) { 89 | if (isErr) { 90 | viewStates = ProgrammerState(inputBase = viewStates.inputBase) 91 | isErr = false 92 | isAdvancedCalculated = false 93 | isCalculated = false 94 | isInputSecondValue = false 95 | } 96 | 97 | // 48 == '0'.code 98 | if (no in KeyIndex_0..KeyIndex_F) { 99 | VibratorHelper.instance.vibrateOnClick() 100 | val newValue: String = 101 | if (viewStates.inputValue == "0") { 102 | if (viewStates.inputOperator != Operator.NUll) isInputSecondValue = true 103 | if (isAdvancedCalculated && viewStates.inputOperator == Operator.NUll) { // 如果在输入高级运算符后直接输入数字,则重置状态 104 | isAdvancedCalculated = false 105 | isCalculated = false 106 | isInputSecondValue = false 107 | viewStates = ProgrammerState(inputBase = viewStates.inputBase) 108 | no.toString() 109 | } 110 | 111 | (48 + no).toChar().toString() 112 | } 113 | else if (viewStates.inputOperator != Operator.NUll && !isInputSecondValue) { 114 | isCalculated = false 115 | isInputSecondValue = true 116 | (48+no).toChar().toString() 117 | } 118 | else if (isCalculated) { 119 | isCalculated = false 120 | isInputSecondValue = false 121 | viewStates = ProgrammerState(inputBase = viewStates.inputBase) 122 | (48+no).toChar().toString() 123 | } 124 | else if (isAdvancedCalculated&& viewStates.inputOperator == Operator.NUll) { // 如果在输入高级运算符后直接输入数字,则重置状态 125 | isAdvancedCalculated = false 126 | isCalculated = false 127 | isInputSecondValue = false 128 | viewStates = ProgrammerState(inputBase = viewStates.inputBase) 129 | no.toString() 130 | } 131 | else viewStates.inputValue + (48+no).toChar().toString() 132 | 133 | // 溢出判断 134 | try { 135 | newValue.toLong(viewStates.inputBase.number) 136 | } catch (e: NumberFormatException) { 137 | return 138 | } 139 | 140 | viewStates = viewStates.copy( 141 | inputValue = newValue, 142 | inputHexText = newValue.baseConversion(InputBase.HEX), 143 | inputDecText = newValue.baseConversion(InputBase.DEC), 144 | inputOctText = newValue.baseConversion(InputBase.OCT), 145 | inputBinText = newValue.baseConversion(InputBase.BIN), 146 | isFinalResult = false) 147 | } 148 | 149 | when (no) { 150 | KeyIndex_Add -> { // "+" 151 | clickArithmetic(Operator.ADD) 152 | } 153 | KeyIndex_Minus -> { // "-" 154 | clickArithmetic(Operator.MINUS) 155 | } 156 | KeyIndex_Multiply -> { // "×" 157 | clickArithmetic(Operator.MULTIPLY) 158 | } 159 | KeyIndex_Divide -> { // "÷" 160 | clickArithmetic(Operator.Divide) 161 | } 162 | KeyIndex_And -> { 163 | clickArithmetic(Operator.AND) 164 | } 165 | KeyIndex_Or -> { 166 | clickArithmetic(Operator.OR) 167 | } 168 | KeyIndex_XOr -> { 169 | clickArithmetic(Operator.XOR) 170 | } 171 | KeyIndex_Lsh -> { 172 | clickArithmetic(Operator.LSH) 173 | } 174 | KeyIndex_Rsh -> { 175 | clickArithmetic(Operator.RSH) 176 | } 177 | KeyIndex_Not -> { 178 | VibratorHelper.instance.vibrateOnClick() 179 | clickNot() 180 | } 181 | KeyIndex_CE -> { // "CE" 182 | VibratorHelper.instance.vibrateOnClear() 183 | if (isCalculated) { 184 | clickClear() 185 | } 186 | else { 187 | clickCE() 188 | } 189 | } 190 | KeyIndex_Clear -> { // "C" 191 | VibratorHelper.instance.vibrateOnClear() 192 | clickClear() 193 | } 194 | KeyIndex_Back -> { // "←" 195 | VibratorHelper.instance.vibrateOnClick() 196 | if (viewStates.inputValue != "0") { 197 | var newValue = viewStates.inputValue.substring(0, viewStates.inputValue.length - 1) 198 | if (newValue.isEmpty()) newValue = "0" 199 | viewStates = viewStates.copy( 200 | inputValue = newValue, 201 | inputHexText = newValue.baseConversion(InputBase.HEX), 202 | inputDecText = newValue.baseConversion(InputBase.DEC), 203 | inputOctText = newValue.baseConversion(InputBase.OCT), 204 | inputBinText = newValue.baseConversion(InputBase.BIN), 205 | ) 206 | } 207 | } 208 | KeyIndex_Equal -> { // "=" 209 | clickEqual() 210 | } 211 | } 212 | } 213 | 214 | private fun clickCE() { 215 | viewStates = viewStates.copy( 216 | inputValue = "0", 217 | inputHexText = "0", 218 | inputDecText = "0", 219 | inputOctText = "0", 220 | inputBinText = "0", 221 | ) 222 | } 223 | 224 | private fun clickClear() { 225 | isInputSecondValue = false 226 | isCalculated = false 227 | isAdvancedCalculated = false 228 | isErr = false 229 | viewStates = ProgrammerState(inputBase = viewStates.inputBase) 230 | } 231 | 232 | private fun String.baseConversion(target: InputBase, current: InputBase = viewStates.inputBase): String { 233 | if (current == target) return this 234 | 235 | // 如果直接转会出现无法直接转成有符号 long 的问题,所以这里使用 BigInteger 来转 236 | // 见: https://stackoverflow.com/questions/47452924/kotlin-numberformatexception 237 | val long = BigInteger(this, current.number).toLong() 238 | 239 | if (target == InputBase.BIN) { 240 | return java.lang.Long.toBinaryString(long) 241 | } 242 | 243 | if (target == InputBase.HEX) { 244 | return java.lang.Long.toHexString(long).uppercase() 245 | } 246 | 247 | if (target == InputBase.OCT) { 248 | return java.lang.Long.toOctalString(long) 249 | } 250 | 251 | // 如果直接使用 toString 会造成直接添加 - 号表示负数,例如十进制的 -10 转为二进制会变成 -1010 252 | // 这里需要的是无符号的表示方式,即 -10 的二进制数应该用 1111111111111111111111111111111111111111111111111111111111110110 表示 253 | return long.toString(target.number).uppercase() 254 | 255 | //return this.toLong(current.number).toString(target.number).uppercase() 256 | } 257 | 258 | private fun clickNot() { 259 | // 转换成十进制的 long 类型来计算, 然后转回当前进制 260 | val result = viewStates.inputValue.baseConversion(InputBase.DEC).toLong() // 转至十进制 long 261 | .inv().toString() // 计算 262 | .baseConversion(viewStates.inputBase, InputBase.DEC) // 转回当前进制 263 | 264 | val newState = viewStates.copy( 265 | inputValue = result, 266 | inputHexText = result.baseConversion(InputBase.HEX), 267 | inputDecText = result.baseConversion(InputBase.DEC), 268 | inputOctText = result.baseConversion(InputBase.OCT), 269 | inputBinText = result.baseConversion(InputBase.BIN), 270 | ) 271 | 272 | if (isInputSecondValue) { 273 | viewStates = newState.copy( 274 | showText = "${viewStates.lastInputValue}${viewStates.inputOperator.showText}${Operator.NOT.showText}(${viewStates.inputValue})", 275 | isFinalResult = false 276 | ) 277 | } 278 | else { 279 | viewStates = newState.copy( 280 | inputOperator = Operator.NUll, 281 | lastInputValue = result, 282 | showText = "${Operator.NOT.showText}(${viewStates.inputValue})", 283 | isFinalResult = false 284 | ) 285 | isInputSecondValue = true 286 | } 287 | 288 | isAdvancedCalculated = true 289 | } 290 | 291 | private fun clickArithmetic(operator: Operator) { 292 | VibratorHelper.instance.vibrateOnClick() 293 | var newState = viewStates.copy( 294 | inputOperator = operator, 295 | lastInputValue = viewStates.inputValue, 296 | isFinalResult = false 297 | ) 298 | if (isCalculated) { 299 | isCalculated = false 300 | isInputSecondValue = false 301 | } 302 | 303 | if (isAdvancedCalculated) { 304 | isInputSecondValue = false 305 | 306 | if (viewStates.inputOperator == Operator.NUll) { // 第一次添加操作符 307 | newState = newState.copy( 308 | showText = "${viewStates.showText}${operator.showText}" 309 | ) 310 | } 311 | else { // 不是第一次添加操作符,则需要把计算结果置于左边,并去掉高级运算的符号 312 | isCalculated = false 313 | isInputSecondValue = false 314 | 315 | clickEqual() 316 | 317 | newState = newState.copy( 318 | lastInputValue = viewStates.inputValue, 319 | showText = "${viewStates.inputValue}${operator.showText}", 320 | inputValue = viewStates.inputValue 321 | ) 322 | } 323 | } 324 | else { 325 | if (viewStates.inputOperator == Operator.NUll) { // 第一次添加操作符 326 | newState = newState.copy( 327 | showText = "${viewStates.inputValue}${operator.showText}" 328 | ) 329 | } 330 | else { // 不是第一次添加操作符,则应该把结果算出来后放到左边 331 | isCalculated = false 332 | isInputSecondValue = false 333 | 334 | clickEqual() 335 | 336 | newState = newState.copy( 337 | lastInputValue = viewStates.inputValue, 338 | showText = "${viewStates.inputValue}${operator.showText}", 339 | inputValue = viewStates.inputValue 340 | ) 341 | } 342 | } 343 | 344 | viewStates = newState 345 | } 346 | 347 | 348 | private fun clickEqual() { 349 | if (viewStates.inputOperator == Operator.NUll) { 350 | VibratorHelper.instance.vibrateOnEqual() 351 | viewStates = if (isAdvancedCalculated) { 352 | viewStates.copy( 353 | lastInputValue = viewStates.inputValue, 354 | showText = "${viewStates.showText}=", 355 | isFinalResult = true 356 | ) 357 | } else { 358 | viewStates.copy( 359 | lastInputValue = viewStates.inputValue, 360 | showText = "${viewStates.inputValue}=", 361 | isFinalResult = true 362 | ) 363 | } 364 | 365 | isCalculated = true 366 | } 367 | else { 368 | val result = programmerCalculate() 369 | 370 | if (result.isSuccess) { 371 | VibratorHelper.instance.vibrateOnEqual() 372 | val resultText : String = try { 373 | result.getOrNull().toString().baseConversion(viewStates.inputBase, InputBase.DEC) 374 | } catch (e: NumberFormatException) { 375 | viewStates = viewStates.copy( 376 | inputValue = "Err: 溢出", 377 | inputHexText = "溢出", 378 | inputDecText = "溢出", 379 | inputOctText = "溢出", 380 | inputBinText = "溢出", 381 | showText = "", 382 | isFinalResult = true 383 | ) 384 | isCalculated = false 385 | isErr = true 386 | return 387 | } 388 | val inputValue = if (viewStates.inputValue.substring(0, 1) == "-") "(${viewStates.inputValue})" else viewStates.inputValue 389 | if (isAdvancedCalculated) { 390 | val index = viewStates.showText.indexOf(viewStates.inputOperator.showText) 391 | viewStates = if (index != -1 && index == viewStates.showText.lastIndex) { 392 | viewStates.copy( 393 | inputValue = resultText, 394 | inputHexText = resultText.baseConversion(InputBase.HEX), 395 | inputDecText = resultText.baseConversion(InputBase.DEC), 396 | inputOctText = resultText.baseConversion(InputBase.OCT), 397 | inputBinText = resultText.baseConversion(InputBase.BIN), 398 | showText = "${viewStates.showText}$inputValue=", 399 | isFinalResult = true 400 | ) 401 | } else { 402 | viewStates.copy( 403 | inputValue = resultText, 404 | inputHexText = resultText.baseConversion(InputBase.HEX), 405 | inputDecText = resultText.baseConversion(InputBase.DEC), 406 | inputOctText = resultText.baseConversion(InputBase.OCT), 407 | inputBinText = resultText.baseConversion(InputBase.BIN), 408 | showText = "${viewStates.showText}=", 409 | isFinalResult = true 410 | ) 411 | } 412 | } 413 | else { 414 | viewStates = viewStates.copy( 415 | inputValue = resultText, 416 | inputHexText = resultText.baseConversion(InputBase.HEX), 417 | inputDecText = resultText.baseConversion(InputBase.DEC), 418 | inputOctText = resultText.baseConversion(InputBase.OCT), 419 | inputBinText = resultText.baseConversion(InputBase.BIN), 420 | showText = "${viewStates.lastInputValue}${viewStates.inputOperator.showText}$inputValue=", 421 | isFinalResult = true 422 | ) 423 | } 424 | isCalculated = true 425 | } 426 | else { 427 | VibratorHelper.instance.vibrateOnError() 428 | viewStates = viewStates.copy( 429 | inputValue = result.exceptionOrNull()?.message ?: "Err", 430 | inputHexText = "Err", 431 | inputDecText = "Err", 432 | inputOctText = "Err", 433 | inputBinText = "Err", 434 | showText = "", 435 | isFinalResult = true 436 | ) 437 | isCalculated = false 438 | isErr = true 439 | } 440 | } 441 | 442 | isAdvancedCalculated = false 443 | } 444 | 445 | /** 446 | * 该方法会将输入字符转换成十进制数字计算,并返回计算完成后的十进制数字的字符串形式 447 | * */ 448 | private fun programmerCalculate(): Result { 449 | val leftNumber = viewStates.lastInputValue.baseConversion(InputBase.DEC) 450 | val rightNumber = viewStates.inputValue.baseConversion(InputBase.DEC) 451 | 452 | if (viewStates.inputOperator in BitOperationList) { 453 | when (viewStates.inputOperator) { 454 | Operator.AND -> { 455 | return Result.success( 456 | (leftNumber.toLong() and rightNumber.toLong()).toString() 457 | ) 458 | } 459 | Operator.OR -> { 460 | return Result.success( 461 | (leftNumber.toLong() or rightNumber.toLong()).toString() 462 | ) 463 | } 464 | Operator.XOR -> { 465 | return Result.success( 466 | (leftNumber.toLong() xor rightNumber.toLong()).toString() 467 | ) 468 | } 469 | Operator.LSH -> { 470 | return try { 471 | Result.success( 472 | (leftNumber.toLong() shl rightNumber.toInt()).toString() 473 | ) 474 | } catch (e: NumberFormatException) { 475 | Result.failure(NumberFormatException("Err: 结果未定义")) 476 | } 477 | } 478 | Operator.RSH -> { 479 | return try { 480 | Result.success( 481 | (leftNumber.toLong() shr rightNumber.toInt()).toString() 482 | ) 483 | } catch (e: NumberFormatException) { 484 | Result.failure(NumberFormatException("Err: 结果未定义")) 485 | } 486 | } 487 | else -> { 488 | // 剩下的操作不应该由此处计算,所以直接返回错误 489 | return Result.failure(NumberFormatException("Err: 错误的调用2")) 490 | } 491 | } 492 | } 493 | else { 494 | calculate( 495 | leftNumber, 496 | rightNumber, 497 | viewStates.inputOperator, 498 | scale = 0 499 | ).fold({ 500 | try { 501 | it.toPlainString().toLong() 502 | } catch (e: NumberFormatException) { 503 | e.printStackTrace() 504 | return Result.failure(NumberFormatException("Err: 结果溢出")) 505 | } 506 | return Result.success(it.toPlainString()) 507 | }, { 508 | return Result.failure(it) 509 | }) 510 | } 511 | } 512 | } 513 | 514 | data class ProgrammerState( 515 | val showText: String = "", 516 | val inputOperator: Operator = Operator.NUll, 517 | val lastInputValue: String = "", 518 | val inputValue: String = "0", 519 | val inputHexText: String = "0", 520 | val inputDecText: String = "0", 521 | val inputOctText: String = "0", 522 | val inputBinText: String = "0", 523 | val inputBase: InputBase = InputBase.DEC, 524 | val isFinalResult: Boolean = false 525 | ) 526 | 527 | sealed class ProgrammerAction { 528 | data class ChangeInputBase(val inputBase: InputBase): ProgrammerAction() 529 | data class ClickBtn(val no: Int): ProgrammerAction() 530 | } -------------------------------------------------------------------------------- /app/src/main/java/com/equationl/calculator_compose/viewModel/StandardViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose.viewModel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.equationl.calculator_compose.dataModel.* 9 | import com.equationl.calculator_compose.database.HistoryDb 10 | import com.equationl.calculator_compose.utils.VibratorHelper 11 | import com.equationl.calculator_compose.utils.calculate 12 | import com.equationl.calculator_compose.utils.formatNumber 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.launch 16 | import kotlinx.coroutines.withContext 17 | import javax.inject.Inject 18 | 19 | @HiltViewModel 20 | open class StandardViewModel @Inject constructor( 21 | private val dataBase: HistoryDb 22 | ): ViewModel() { 23 | 24 | var viewStates by mutableStateOf(StandardState()) 25 | private set 26 | 27 | open fun dispatch(action: StandardAction) { 28 | when (action) { 29 | is StandardAction.ClickBtn -> clickBtn(action.no) 30 | is StandardAction.ToggleHistory -> toggleHistory(action.forceClose) 31 | is StandardAction.ReadFromHistory -> readFromHistory(action.item) 32 | is StandardAction.DeleteHistory -> deleteHistory(action.item) 33 | else -> { } 34 | } 35 | } 36 | 37 | /**标记第一个值输入后,是否开始输入第二个值*/ 38 | private var isInputSecondValue: Boolean = false 39 | /**标记是否已计算最终结果*/ 40 | private var isCalculated: Boolean = false 41 | /**标记是否添加了非四则运算的“高级”运算符*/ 42 | private var isAdvancedCalculated: Boolean = false 43 | /**标记是否处于错误状态*/ 44 | private var isErr: Boolean = false 45 | 46 | private fun toggleHistory(forceClose: Boolean) { 47 | VibratorHelper.instance.vibrateOnClick() 48 | if (viewStates.historyList.isNotEmpty() || forceClose) { 49 | viewStates = viewStates.copy(historyList = listOf()) 50 | } 51 | else { 52 | viewStates = viewStates.copy(historyList = listOf( 53 | HistoryData(-1, showText = "加载中……", "null", "null", Operator.NUll, "请稍候") 54 | )) 55 | 56 | viewModelScope.launch { 57 | withContext(Dispatchers.IO) { 58 | var list = dataBase.history().getAll() 59 | if (list.isEmpty()) { 60 | list = listOf( 61 | HistoryData(-1, showText = "", "null", "null", Operator.NUll, "没有历史记录") 62 | ) 63 | } 64 | viewStates = viewStates.copy(historyList = list) 65 | } 66 | } 67 | } 68 | } 69 | 70 | private fun readFromHistory(item: HistoryData) { 71 | if (item.id != -1) { 72 | VibratorHelper.instance.vibrateOnEqual() 73 | viewStates = StandardState( 74 | inputValue = item.result, 75 | lastInputValue = item.lastInputText, 76 | inputOperator = item.operator, 77 | showText = item.showText, 78 | isFinalResult = true 79 | ) 80 | } 81 | } 82 | 83 | private fun deleteHistory(item: HistoryData?) { 84 | viewModelScope.launch { 85 | withContext(Dispatchers.IO) { 86 | VibratorHelper.instance.vibrateOnError() 87 | viewStates = if (item == null) { 88 | dataBase.history().deleteAll() 89 | viewStates.copy(historyList = listOf()) 90 | } else { 91 | VibratorHelper.instance.vibrateOnClick() 92 | dataBase.history().delete(item) 93 | val newList = mutableListOf() 94 | newList.addAll(viewStates.historyList) 95 | newList.remove(item) 96 | 97 | viewStates.copy(historyList = newList) 98 | } 99 | 100 | } 101 | } 102 | } 103 | 104 | private fun clickBtn(no: Int) { 105 | if (isErr) { 106 | viewStates = StandardState() 107 | isErr = false 108 | isAdvancedCalculated = false 109 | isCalculated = false 110 | isInputSecondValue = false 111 | } 112 | 113 | if (no in KeyIndex_0..KeyIndex_9) { 114 | VibratorHelper.instance.vibrateOnClick() 115 | val newValue = 116 | if (viewStates.inputValue == "0") { 117 | if (viewStates.inputOperator != Operator.NUll) isInputSecondValue = true 118 | if (isAdvancedCalculated && viewStates.inputOperator == Operator.NUll) { // 如果在输入高级运算符后直接输入数字,则重置状态 119 | isAdvancedCalculated = false 120 | isCalculated = false 121 | isInputSecondValue = false 122 | viewStates = StandardState() 123 | no.toString() 124 | } 125 | no.toString() 126 | } 127 | else if (viewStates.inputOperator != Operator.NUll && !isInputSecondValue) { 128 | isCalculated = false 129 | isInputSecondValue = true 130 | no.toString() 131 | } 132 | else if (isCalculated) { 133 | isCalculated = false 134 | isInputSecondValue = false 135 | viewStates = StandardState( 136 | lastShowText = 137 | if (!isAdvancedCalculated) 138 | viewStates.showText+viewStates.inputValue 139 | else viewStates.lastShowText 140 | ) 141 | no.toString() 142 | } 143 | else if (isAdvancedCalculated && viewStates.inputOperator == Operator.NUll) { // 如果在输入高级运算符后直接输入数字,则重置状态 144 | isAdvancedCalculated = false 145 | isCalculated = false 146 | isInputSecondValue = false 147 | viewStates = StandardState() 148 | no.toString() 149 | } 150 | else viewStates.inputValue + no.toString() 151 | 152 | viewStates = viewStates.copy(inputValue = newValue, isFinalResult = false) 153 | } 154 | 155 | when (no) { 156 | KeyIndex_Add -> { // "+" 157 | clickArithmetic(Operator.ADD) 158 | } 159 | KeyIndex_Minus -> { // "-" 160 | clickArithmetic(Operator.MINUS) 161 | } 162 | KeyIndex_Multiply -> { // "×" 163 | clickArithmetic(Operator.MULTIPLY) 164 | } 165 | KeyIndex_Divide -> { // "÷" 166 | clickArithmetic(Operator.Divide) 167 | } 168 | KeyIndex_NegativeNumber -> { // "+/-" 169 | VibratorHelper.instance.vibrateOnClick() 170 | if (viewStates.inputValue != "0") { 171 | val newValue: String = 172 | if (viewStates.inputValue.substring(0, 1) == "-") viewStates.inputValue.substring(1, viewStates.inputValue.length) 173 | else "-" + viewStates.inputValue 174 | viewStates = viewStates.copy(inputValue = newValue, isFinalResult = false) 175 | } 176 | } 177 | KeyIndex_Point -> { // "." 178 | VibratorHelper.instance.vibrateOnClick() 179 | if (viewStates.inputValue.indexOf('.') == -1) { 180 | viewStates = viewStates.copy(inputValue = viewStates.inputValue + ".") 181 | } 182 | } 183 | KeyIndex_Reciprocal -> { // "1/x" 184 | VibratorHelper.instance.vibrateOnClick() 185 | clickReciprocal() 186 | } 187 | KeyIndex_Pow2 -> { // "x²" 188 | VibratorHelper.instance.vibrateOnClick() 189 | clickPow2() 190 | } 191 | KeyIndex_Sqrt -> { // "√x" 192 | VibratorHelper.instance.vibrateOnClick() 193 | clickSqrt() 194 | } 195 | KeyIndex_Percentage -> { // "%" 196 | if (isInputSecondValue && viewStates.lastInputValue != "" && viewStates.inputOperator != Operator.NUll) { 197 | VibratorHelper.instance.vibrateOnClick() 198 | var result: String = calculate(viewStates.inputValue, "100", Operator.Divide).getOrNull().toString() 199 | result = calculate(viewStates.lastInputValue, result, Operator.MULTIPLY).getOrNull().toString() 200 | 201 | viewStates = viewStates.copy( 202 | inputValue = result, 203 | showText = "${viewStates.lastInputValue}${viewStates.inputOperator.showText}" + 204 | result.formatNumber(formatDecimal = true, formatInteger = false), 205 | isFinalResult = true 206 | ) 207 | } 208 | else { 209 | VibratorHelper.instance.vibrateOnClear() 210 | viewStates = viewStates.copy( 211 | inputValue = "0", 212 | showText = "0", 213 | lastInputValue = "", 214 | inputOperator = Operator.NUll 215 | ) 216 | } 217 | } 218 | KeyIndex_Equal -> { // "=" 219 | clickEqual() 220 | } 221 | KeyIndex_CE -> { // "CE" 222 | VibratorHelper.instance.vibrateOnClear() 223 | if (isCalculated) { 224 | clickClear() 225 | } 226 | else { 227 | viewStates = viewStates.copy(inputValue = "0") 228 | } 229 | } 230 | KeyIndex_Clear -> { // "C" 231 | VibratorHelper.instance.vibrateOnClear() 232 | clickClear() 233 | } 234 | KeyIndex_Back -> { // "←" 235 | VibratorHelper.instance.vibrateOnClick() 236 | if (viewStates.inputValue != "0") { 237 | var newValue = viewStates.inputValue.substring(0, viewStates.inputValue.length - 1) 238 | if (newValue.isEmpty()) newValue = "0" 239 | viewStates = viewStates.copy(inputValue = newValue) 240 | } 241 | } 242 | } 243 | } 244 | 245 | private fun clickClear() { 246 | isInputSecondValue = false 247 | isCalculated = false 248 | isAdvancedCalculated = false 249 | isErr = false 250 | viewStates = StandardState() 251 | } 252 | 253 | private fun clickReciprocal() { 254 | val result = calculate("1", viewStates.inputValue, Operator.Divide) 255 | val resultText = if (result.isSuccess) { 256 | result.getOrNull()?.toPlainString() ?: "Null" 257 | } else { 258 | VibratorHelper.instance.vibrateOnError() 259 | isErr = true 260 | result.exceptionOrNull()?.message ?: "Err" 261 | } 262 | 263 | val newState = viewStates.copy( 264 | inputValue = resultText 265 | ) 266 | 267 | if (isInputSecondValue) { 268 | viewStates = newState.copy( 269 | showText = "${viewStates.lastInputValue}${viewStates.inputOperator.showText}1/(${viewStates.inputValue})", 270 | isFinalResult = false, 271 | lastShowText = 272 | if (viewStates.showText.indexOf("=") != -1) 273 | viewStates.showText+viewStates.inputValue 274 | else viewStates.lastShowText 275 | ) 276 | } 277 | else { 278 | viewStates = newState.copy( 279 | inputOperator = Operator.NUll, 280 | lastInputValue = viewStates.inputValue, 281 | showText = "1/(${viewStates.inputValue})", 282 | isFinalResult = false 283 | ) 284 | // isInputSecondValue = true 285 | } 286 | 287 | isAdvancedCalculated = true 288 | } 289 | 290 | private fun clickSqrt() { 291 | val result = calculate(viewStates.inputValue, "0", Operator.SQRT) 292 | 293 | val resultText = if (result.isSuccess) { 294 | result.getOrNull()?.toPlainString() ?: "Null" 295 | } else { 296 | VibratorHelper.instance.vibrateOnError() 297 | isErr = true 298 | result.exceptionOrNull()?.message ?: "Err" 299 | } 300 | 301 | val newState = viewStates.copy( 302 | inputValue = resultText 303 | ) 304 | 305 | if (isInputSecondValue) { 306 | viewStates = newState.copy( 307 | showText = "${viewStates.lastInputValue}${viewStates.inputOperator.showText}${Operator.SQRT.showText}(${viewStates.inputValue})", 308 | isFinalResult = false, 309 | lastShowText = 310 | if (viewStates.showText.indexOf("=") != -1) 311 | viewStates.showText+viewStates.inputValue 312 | else viewStates.lastShowText 313 | ) 314 | } 315 | else { 316 | viewStates = newState.copy( 317 | inputOperator = Operator.NUll, 318 | lastInputValue = resultText, 319 | showText = "${Operator.SQRT.showText}(${viewStates.inputValue})", 320 | isFinalResult = false 321 | ) 322 | //isInputSecondValue = true 323 | } 324 | 325 | isAdvancedCalculated = true 326 | } 327 | 328 | private fun clickPow2() { 329 | val result = calculate(viewStates.inputValue, "0", Operator.POW2) 330 | 331 | val resultText = if (result.isSuccess) { 332 | result.getOrNull().toString() 333 | } else { 334 | VibratorHelper.instance.vibrateOnError() 335 | isErr = true 336 | result.exceptionOrNull()?.message ?: "Err" 337 | } 338 | 339 | val newState = viewStates.copy( 340 | inputValue = resultText 341 | ) 342 | 343 | if (isInputSecondValue) { 344 | viewStates = newState.copy( 345 | showText = "${viewStates.lastInputValue}${viewStates.inputOperator.showText}(${viewStates.inputValue})${Operator.POW2.showText}", 346 | isFinalResult = false, 347 | lastShowText = 348 | if (viewStates.showText.indexOf("=") != -1) 349 | viewStates.showText+viewStates.inputValue 350 | else viewStates.lastShowText 351 | ) 352 | } 353 | else { 354 | viewStates = newState.copy( 355 | inputOperator = Operator.NUll, 356 | lastInputValue = result.getOrNull().toString(), 357 | showText = "(${viewStates.inputValue})${Operator.POW2.showText}", 358 | isFinalResult = false 359 | ) 360 | //isInputSecondValue = true 361 | } 362 | 363 | isAdvancedCalculated = true 364 | } 365 | 366 | private fun clickEqual() { 367 | val inputValueCache = viewStates.inputValue 368 | 369 | if (viewStates.inputOperator == Operator.NUll) { 370 | VibratorHelper.instance.vibrateOnEqual() 371 | viewStates = if (isAdvancedCalculated) { 372 | viewStates.copy( 373 | lastInputValue = viewStates.inputValue, 374 | showText = "${viewStates.showText}=", 375 | isFinalResult = true 376 | ) 377 | } else { 378 | viewStates.copy( 379 | lastInputValue = viewStates.inputValue, 380 | showText = "${viewStates.inputValue}=", 381 | isFinalResult = true 382 | ) 383 | } 384 | 385 | isCalculated = true 386 | } 387 | else { 388 | val result = calculate(viewStates.lastInputValue, viewStates.inputValue, viewStates.inputOperator) 389 | if (result.isSuccess) { 390 | VibratorHelper.instance.vibrateOnEqual() 391 | val resultText = result.getOrNull()?.toPlainString() ?: "Null" 392 | val inputValue = if (viewStates.inputValue.substring(0, 1) == "-") "(${viewStates.inputValue})" else viewStates.inputValue 393 | if (isAdvancedCalculated) { 394 | val index = viewStates.showText.indexOf(viewStates.inputOperator.showText) 395 | viewStates = if (index != -1 && index == viewStates.showText.lastIndex) { 396 | viewStates.copy( 397 | inputValue = resultText, 398 | showText = "${viewStates.showText}$inputValue=", 399 | isFinalResult = true 400 | ) 401 | } else { 402 | viewStates.copy( 403 | inputValue = resultText, 404 | showText = "${viewStates.showText}=", 405 | isFinalResult = true 406 | ) 407 | } 408 | } 409 | else { 410 | viewStates = viewStates.copy( 411 | inputValue = resultText, 412 | showText = "${viewStates.lastInputValue}${viewStates.inputOperator.showText}$inputValue=", 413 | isFinalResult = true 414 | ) 415 | } 416 | isCalculated = true 417 | } 418 | else { 419 | VibratorHelper.instance.vibrateOnError() 420 | viewStates = viewStates.copy( 421 | inputValue = result.exceptionOrNull()?.message ?: "Err", 422 | showText = "", 423 | isFinalResult = true 424 | ) 425 | isCalculated = false 426 | isErr = true 427 | } 428 | } 429 | 430 | isAdvancedCalculated = false 431 | 432 | // 将计算内容保存到数据库 433 | viewModelScope.launch { 434 | withContext(Dispatchers.IO) { 435 | if (!isErr) { // 不保存错误结果 436 | dataBase.history().insert( 437 | HistoryData( 438 | showText = viewStates.showText, 439 | lastInputText = viewStates.lastInputValue, 440 | operator = viewStates.inputOperator, 441 | result = viewStates.inputValue, 442 | inputText = inputValueCache 443 | ) 444 | ) 445 | } 446 | } 447 | } 448 | } 449 | 450 | private fun clickArithmetic(operator: Operator) { 451 | VibratorHelper.instance.vibrateOnClick() 452 | var newState = viewStates.copy( 453 | inputOperator = operator, 454 | lastInputValue = viewStates.inputValue, 455 | isFinalResult = false 456 | ) 457 | if (isCalculated) { 458 | isCalculated = false 459 | isInputSecondValue = false 460 | newState = newState.copy( 461 | lastShowText = 462 | if (!isAdvancedCalculated) 463 | viewStates.showText+viewStates.inputValue 464 | else viewStates.lastShowText 465 | ) 466 | } 467 | 468 | if (isAdvancedCalculated) { 469 | isInputSecondValue = false 470 | 471 | if (viewStates.inputOperator == Operator.NUll) { // 第一次添加操作符 472 | newState = newState.copy( 473 | showText = "${viewStates.showText}${operator.showText}" 474 | ) 475 | } 476 | else { // 不是第一次添加操作符,则需要把计算结果置于左边,并去掉高级运算的符号 477 | isCalculated = false 478 | isInputSecondValue = false 479 | 480 | clickEqual() 481 | 482 | newState = newState.copy( 483 | lastInputValue = viewStates.inputValue, 484 | showText = "${viewStates.inputValue}${operator.showText}", 485 | inputValue = viewStates.inputValue 486 | ) 487 | } 488 | 489 | } 490 | else { 491 | if (viewStates.inputOperator == Operator.NUll) { // 第一次添加操作符 492 | newState = newState.copy( 493 | showText = "${viewStates.inputValue}${operator.showText}" 494 | ) 495 | } 496 | else { // 不是第一次添加操作符,则应该把结果算出来后放到左边 497 | isCalculated = false 498 | isInputSecondValue = false 499 | 500 | clickEqual() 501 | 502 | newState = newState.copy( 503 | lastInputValue = viewStates.inputValue, 504 | showText = "${viewStates.inputValue}${operator.showText}", 505 | inputValue = viewStates.inputValue 506 | ) 507 | } 508 | } 509 | 510 | viewStates = newState 511 | } 512 | } 513 | 514 | data class StandardState( 515 | val inputValue: String = "0", 516 | val inputOperator: Operator = Operator.NUll, 517 | val lastInputValue: String = "", 518 | val showText: String = "", 519 | val isFinalResult: Boolean = false, 520 | val historyList: List = listOf(), 521 | val lastShowText: String = "" 522 | ) 523 | 524 | sealed class StandardAction { 525 | data class ToggleHistory(val forceClose: Boolean = false): StandardAction() 526 | data class ClickBtn(val no: Int): StandardAction() 527 | data class ReadFromHistory(val item: HistoryData): StandardAction() 528 | data class DeleteHistory(val item: HistoryData?): StandardAction() 529 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFC107 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 隐云计算器 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/test/java/com/equationl/calculator_compose/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.equationl.calculator_compose 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | compose_version = '1.2.0' 4 | lifecycle_version = '2.5.1' 5 | room_version = "2.4.2" 6 | } 7 | dependencies { 8 | classpath 'com.google.dagger:hilt-android-gradle-plugin:2.42' 9 | } 10 | }// Top-level build file where you can add configuration options common to all sub-projects/modules. 11 | plugins { 12 | id 'com.android.application' version '7.2.2' apply false 13 | id 'com.android.library' version '7.2.2' apply false 14 | id 'org.jetbrains.kotlin.android' version '1.7.0' apply false 15 | } 16 | 17 | task clean(type: Delete) { 18 | delete rootProject.buildDir 19 | } -------------------------------------------------------------------------------- /docs/img/screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/docs/img/screenshot1.jpg -------------------------------------------------------------------------------- /docs/img/screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/docs/img/screenshot2.jpg -------------------------------------------------------------------------------- /docs/img/screenshot3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/docs/img/screenshot3.jpg -------------------------------------------------------------------------------- /docs/img/screenshot4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/docs/img/screenshot4.jpg -------------------------------------------------------------------------------- /docs/img/screenshot5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/docs/img/screenshot5.jpg -------------------------------------------------------------------------------- /docs/img/screenshot6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/docs/img/screenshot6.jpg -------------------------------------------------------------------------------- /docs/img/screenshot7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/docs/img/screenshot7.jpg -------------------------------------------------------------------------------- /docs/img/screenshot8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/docs/img/screenshot8.jpg -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/equationl/calculatorCompose/f2392c7e6dc2f4a06238114fbd150dfe6795adb3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Aug 08 09:40:36 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "calculator-Compose" 16 | include ':app' 17 | --------------------------------------------------------------------------------