├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 | | 
标准模式 | 
标准模式 |
34 | | 
历史记录 | 
历史记录 |
35 | | 
程序员模式 | 
程序员模式 |
36 | | 
悬浮窗 | 
悬浮窗 |
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 |
--------------------------------------------------------------------------------