├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── sudo │ │ └── rizwan │ │ └── composecalculator │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── sudo │ │ │ └── rizwan │ │ │ └── composecalculator │ │ │ ├── AppState.kt │ │ │ ├── MainActivity.kt │ │ │ ├── Theme.kt │ │ │ ├── Utils.kt │ │ │ ├── exprk │ │ │ ├── Expressions.kt │ │ │ └── internal │ │ │ │ ├── Evaluator.kt │ │ │ │ ├── Expr.kt │ │ │ │ ├── Function.kt │ │ │ │ ├── Parser.kt │ │ │ │ ├── Scanner.kt │ │ │ │ ├── Token.kt │ │ │ │ └── TokenType.kt │ │ │ ├── model │ │ │ ├── Drag.kt │ │ │ └── Operation.kt │ │ │ └── ui │ │ │ ├── BottomView.kt │ │ │ ├── Content.kt │ │ │ ├── DimOverlay.kt │ │ │ ├── NumbersPanel.kt │ │ │ ├── SideView.kt │ │ │ └── TopView.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_keyboard_arrow_left_24.xml │ │ ├── ic_keyboard_arrow_right_24.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_more_vert_24.xml │ │ ├── ic_outline_backspace_24.xml │ │ └── ic_rounded_dash.xml │ │ ├── font │ │ └── jost_regular.ttf │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.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 │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── sudo │ └── rizwan │ └── composecalculator │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshot.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ahmed Rizwan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JetpackComposeCalculator 2 | 3 | ![alt text](https://github.com/ahmedrizwan/JetpackComposeCalculator/raw/master/screenshot.png) 4 | 5 | A jetpack compose clone of Android 10 Calculator UI 6 | 7 | Medium Article: https://link.medium.com/kNeuoINNN6 8 | 9 | Using https://github.com/Keelar/ExprK for parsing arithmetic expressions. 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-android-extensions' 5 | } 6 | 7 | android { 8 | compileSdkVersion 29 9 | 10 | defaultConfig { 11 | applicationId "com.sudo.rizwan.composecalculator" 12 | minSdkVersion 23 13 | targetSdkVersion 29 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = '1.8' 32 | } 33 | buildFeatures { 34 | compose true 35 | } 36 | composeOptions { 37 | kotlinCompilerExtensionVersion "$compose_compiler_extension_version" 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 43 | implementation 'androidx.core:core-ktx:1.2.0' 44 | implementation 'androidx.appcompat:appcompat:1.1.0' 45 | implementation 'com.google.android.material:material:1.1.0' 46 | implementation("androidx.ui:ui-framework:$compose_version") 47 | implementation("androidx.ui:ui-layout:$compose_version") 48 | implementation("androidx.ui:ui-material:$compose_version") 49 | implementation("androidx.ui:ui-foundation:$compose_version") 50 | implementation("androidx.ui:ui-animation:$compose_version") 51 | implementation "androidx.ui:ui-tooling:$compose_version" 52 | testImplementation 'junit:junit:4.+' 53 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 55 | } -------------------------------------------------------------------------------- /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/sudo/rizwan/composecalculator/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.sudo.rizwan.composecalculator", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/AppState.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator 2 | 3 | import androidx.compose.Model 4 | import androidx.ui.foundation.TextFieldValue 5 | import com.sudo.rizwan.composecalculator.model.Operation 6 | 7 | @Model 8 | object AppState { 9 | var theme = lightThemeColors 10 | var inputText = TextFieldValue(text = "") 11 | var outputText = TextFieldValue(text = "") 12 | } 13 | 14 | val operationsHistory = mutableListOf( 15 | Operation(input = "24+32", output = "56", date = "May 13, 2020"), 16 | Operation(input = "32+24", output = "56", date = "May 14, 2020"), 17 | Operation(input = "32+24-6", output = "50", date = "May 15, 2020"), 18 | Operation(input = "63+58", output = "121", date = " May 16, 2020") 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.ui.core.DensityAmbient 6 | import androidx.ui.core.WithConstraints 7 | import androidx.ui.core.setContent 8 | import androidx.ui.material.MaterialTheme 9 | import com.sudo.rizwan.composecalculator.ui.Content 10 | 11 | class MainActivity : AppCompatActivity() { 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContent { 15 | MaterialTheme(colors = lightThemeColors) { 16 | WithConstraints { constraints, _ -> 17 | val boxHeight = with(DensityAmbient.current) { constraints.maxHeight.toDp() } 18 | val boxWidth = with(DensityAmbient.current) { constraints.maxWidth.toDp() } 19 | Content( 20 | constraints = constraints, 21 | boxHeight = boxHeight, 22 | boxWidth = boxWidth 23 | ) 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator 2 | 3 | import androidx.ui.graphics.Color 4 | import androidx.ui.material.darkColorPalette 5 | import androidx.ui.material.lightColorPalette 6 | import androidx.ui.text.font.font 7 | import androidx.ui.text.font.fontFamily 8 | 9 | // TODO: Add theme switching 10 | 11 | private val lightBackgroundColor = Color(0xFFf3f3f3) 12 | private val darkBackgroundColor = Color(0xFF15202b) 13 | private val primaryColor = Color(0xFF2376e6) 14 | 15 | val grayColor = Color(0xFF636363) 16 | 17 | val lightThemeColors = lightColorPalette( 18 | primary = primaryColor, 19 | background = lightBackgroundColor, 20 | surface = lightBackgroundColor 21 | ) 22 | 23 | val darkThemeColors = darkColorPalette( 24 | primary = primaryColor, 25 | background = darkBackgroundColor, 26 | surface = darkBackgroundColor 27 | ) 28 | 29 | // Fonts 30 | 31 | val jostFontFamily = fontFamily( 32 | listOf(font(resId = R.font.jost_regular)) 33 | ) 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator 2 | 3 | import androidx.ui.foundation.TextFieldValue 4 | import com.sudo.rizwan.composecalculator.exprk.Expressions 5 | import com.sudo.rizwan.composecalculator.model.Operation 6 | import java.math.RoundingMode 7 | import java.util.* 8 | 9 | fun performCalculation() { 10 | val finalExpression = AppState.inputText.text.replace('x', '*').replace('÷', '/') 11 | val expressions = arrayOf('+', '-', '*', '/') 12 | if (!expressions.any { finalExpression.contains(it) }) { 13 | return 14 | } 15 | if (expressions.any { finalExpression.endsWith(it) }) { 16 | AppState.outputText = TextFieldValue(text = "") 17 | return 18 | } 19 | val eval = Expressions().eval(finalExpression) 20 | AppState.outputText = if (eval.toString().contains(".")) { 21 | val rounded = eval.setScale(1, RoundingMode.UP) 22 | TextFieldValue(text = rounded.toString()) 23 | } else { 24 | TextFieldValue(text = eval.toString()) 25 | } 26 | } 27 | 28 | @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") 29 | fun saveCalculationToHistory() { 30 | if (AppState.outputText.text.isNotEmpty()) { 31 | val calendar = Calendar.getInstance() 32 | val month: String = calendar.getDisplayName( 33 | Calendar.MONTH, 34 | Calendar.SHORT, 35 | Locale.getDefault() 36 | ) 37 | val day = calendar.get(Calendar.DAY_OF_MONTH) 38 | val year = calendar.get(Calendar.YEAR) 39 | operationsHistory.add( 40 | Operation( 41 | input = AppState.inputText.text, 42 | output = AppState.outputText.text, 43 | date = "$month $day, $year" 44 | ) 45 | ) 46 | } 47 | } 48 | 49 | fun isLightTheme(): Boolean { 50 | return AppState.theme.isLight 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/exprk/Expressions.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.exprk 2 | 3 | import com.sudo.rizwan.composecalculator.exprk.internal.Evaluator 4 | import com.sudo.rizwan.composecalculator.exprk.internal.Expr 5 | import com.sudo.rizwan.composecalculator.exprk.internal.Function 6 | import com.sudo.rizwan.composecalculator.exprk.internal.Parser 7 | import com.sudo.rizwan.composecalculator.exprk.internal.Scanner 8 | import com.sudo.rizwan.composecalculator.exprk.internal.Token 9 | import java.math.BigDecimal 10 | import java.math.MathContext 11 | import java.math.RoundingMode 12 | 13 | class ExpressionException(message: String) : RuntimeException(message) 14 | 15 | @Suppress("unused") 16 | class Expressions { 17 | private val evaluator = Evaluator() 18 | 19 | init { 20 | define("pi", Math.PI) 21 | define("e", Math.E) 22 | 23 | evaluator.addFunction("abs", object : Function() { 24 | override fun call(arguments: List): BigDecimal { 25 | if (arguments.size != 1) throw ExpressionException( 26 | "abs requires one argument" 27 | ) 28 | 29 | return arguments.first().abs() 30 | } 31 | }) 32 | 33 | evaluator.addFunction("sum", object : Function() { 34 | override fun call(arguments: List): BigDecimal { 35 | if (arguments.isEmpty()) throw ExpressionException( 36 | "sum requires at least one argument" 37 | ) 38 | 39 | return arguments.reduce { sum, bigDecimal -> 40 | sum.add(bigDecimal) 41 | } 42 | } 43 | }) 44 | 45 | evaluator.addFunction("floor", object : Function() { 46 | override fun call(arguments: List): BigDecimal { 47 | if (arguments.size != 1) throw ExpressionException( 48 | "abs requires one argument" 49 | ) 50 | 51 | return arguments.first().setScale(0, RoundingMode.FLOOR) 52 | } 53 | }) 54 | 55 | evaluator.addFunction("ceil", object : Function() { 56 | override fun call(arguments: List): BigDecimal { 57 | if (arguments.size != 1) throw ExpressionException( 58 | "abs requires one argument" 59 | ) 60 | 61 | return arguments.first().setScale(0, RoundingMode.CEILING) 62 | } 63 | }) 64 | 65 | evaluator.addFunction("round", object : Function() { 66 | override fun call(arguments: List): BigDecimal { 67 | if (arguments.size !in listOf(1, 2)) throw ExpressionException( 68 | "round requires either one or two arguments" 69 | ) 70 | 71 | val value = arguments.first() 72 | val scale = if (arguments.size == 2) arguments.last().toInt() else 0 73 | 74 | return value.setScale(scale, roundingMode) 75 | } 76 | }) 77 | 78 | evaluator.addFunction("min", object : Function() { 79 | override fun call(arguments: List): BigDecimal { 80 | if (arguments.isEmpty()) throw ExpressionException( 81 | "min requires at least one argument" 82 | ) 83 | 84 | return arguments.min()!! 85 | } 86 | }) 87 | 88 | evaluator.addFunction("max", object : Function() { 89 | override fun call(arguments: List): BigDecimal { 90 | if (arguments.isEmpty()) throw ExpressionException( 91 | "max requires at least one argument" 92 | ) 93 | 94 | return arguments.max()!! 95 | } 96 | }) 97 | 98 | evaluator.addFunction("if", object : Function() { 99 | override fun call(arguments: List): BigDecimal { 100 | val condition = arguments[0] 101 | val thenValue = arguments[1] 102 | val elseValue = arguments[2] 103 | 104 | return if (condition != BigDecimal.ZERO) { 105 | thenValue 106 | } else { 107 | elseValue 108 | } 109 | } 110 | }) 111 | } 112 | 113 | val precision: Int 114 | get() = evaluator.mathContext.precision 115 | 116 | val roundingMode: RoundingMode 117 | get() = evaluator.mathContext.roundingMode 118 | 119 | fun setPrecision(precision: Int): Expressions { 120 | evaluator.mathContext = MathContext(precision, roundingMode) 121 | 122 | 123 | return this 124 | } 125 | 126 | fun setRoundingMode(roundingMode: RoundingMode): Expressions { 127 | evaluator.mathContext = MathContext(precision, roundingMode) 128 | 129 | return this 130 | } 131 | 132 | fun define(name: String, value: Long): Expressions { 133 | define(name, value.toString()) 134 | 135 | return this 136 | } 137 | 138 | fun define(name: String, value: Double): Expressions { 139 | define(name, value.toString()) 140 | 141 | return this 142 | } 143 | 144 | fun define(name: String, value: BigDecimal): Expressions { 145 | define(name, value.toPlainString()) 146 | 147 | return this 148 | } 149 | 150 | fun define(name: String, expression: String): Expressions { 151 | val expr = parse(expression) 152 | evaluator.define(name, expr) 153 | 154 | return this 155 | } 156 | 157 | fun addFunction(name: String, function: Function): Expressions { 158 | evaluator.addFunction(name, function) 159 | 160 | return this 161 | } 162 | 163 | fun addFunction(name: String, func: (List) -> BigDecimal): Expressions { 164 | evaluator.addFunction(name, object : Function() { 165 | override fun call(arguments: List): BigDecimal { 166 | return func(arguments) 167 | } 168 | 169 | }) 170 | 171 | return this 172 | } 173 | 174 | fun eval(expression: String): BigDecimal { 175 | return evaluator.eval(parse(expression)) 176 | } 177 | 178 | /** 179 | * eval an expression then round it with {@link Evaluator#mathContext} and call toEngineeringString
180 | * if error will return message from Throwable 181 | * @param expression String 182 | * @return String 183 | */ 184 | fun evalToString(expression: String): String { 185 | return try { 186 | evaluator.eval(parse(expression)).round(evaluator.mathContext).stripTrailingZeros() 187 | .toEngineeringString() 188 | } catch (e: Throwable) { 189 | e.cause?.message ?: e.message ?: "unknown error" 190 | } 191 | } 192 | 193 | private fun parse(expression: String): Expr { 194 | return parse(scan(expression)) 195 | } 196 | 197 | private fun parse(tokens: List): Expr { 198 | return Parser(tokens).parse() 199 | } 200 | 201 | private fun scan(expression: String): List { 202 | return Scanner(expression, evaluator.mathContext).scanTokens() 203 | } 204 | 205 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/exprk/internal/Evaluator.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.exprk.internal 2 | 3 | import com.sudo.rizwan.composecalculator.exprk.ExpressionException 4 | import com.sudo.rizwan.composecalculator.exprk.internal.TokenType.* 5 | import java.math.BigDecimal 6 | import java.math.MathContext 7 | import java.math.RoundingMode 8 | 9 | internal class Evaluator() : ExprVisitor { 10 | internal var mathContext: MathContext = MathContext.DECIMAL64 11 | 12 | private val variables: LinkedHashMap = linkedMapOf() 13 | private val functions: MutableMap = mutableMapOf() 14 | 15 | private fun define(name: String, value: BigDecimal) { 16 | variables += name to value 17 | } 18 | 19 | fun define(name: String, expr: Expr): Evaluator { 20 | define(name.toLowerCase(), eval(expr)) 21 | 22 | return this 23 | } 24 | 25 | fun addFunction(name: String, function: Function): Evaluator { 26 | functions += name.toLowerCase() to function 27 | 28 | return this 29 | } 30 | 31 | fun eval(expr: Expr): BigDecimal { 32 | return expr.accept(this) 33 | } 34 | 35 | override fun visitAssignExpr(expr: AssignExpr): BigDecimal { 36 | val value = eval(expr.value) 37 | 38 | define(expr.name.lexeme, value) 39 | 40 | return value 41 | } 42 | 43 | override fun visitLogicalExpr(expr: LogicalExpr): BigDecimal { 44 | val left = expr.left 45 | val right = expr.right 46 | 47 | return when (expr.operator.type) { 48 | BAR_BAR -> left or right 49 | AMP_AMP -> left and right 50 | else -> throw ExpressionException( 51 | "Invalid logical operator '${expr.operator.lexeme}'" 52 | ) 53 | } 54 | } 55 | 56 | override fun visitBinaryExpr(expr: BinaryExpr): BigDecimal { 57 | val left = eval(expr.left) 58 | val right = eval(expr.right) 59 | 60 | return when (expr.operator.type) { 61 | PLUS -> left + right 62 | MINUS -> left - right 63 | STAR -> left * right 64 | SLASH -> left.divide(right, mathContext) 65 | MODULO -> left.remainder(right, mathContext) 66 | EXPONENT -> left pow right 67 | EQUAL_EQUAL -> (left == right).toBigDecimal() 68 | NOT_EQUAL -> (left != right).toBigDecimal() 69 | GREATER -> (left > right).toBigDecimal() 70 | GREATER_EQUAL -> (left >= right).toBigDecimal() 71 | LESS -> (left < right).toBigDecimal() 72 | LESS_EQUAL -> (left <= right).toBigDecimal() 73 | else -> throw ExpressionException( 74 | "Invalid binary operator '${expr.operator.lexeme}'" 75 | ) 76 | } 77 | } 78 | 79 | override fun visitUnaryExpr(expr: UnaryExpr): BigDecimal { 80 | val right = eval(expr.right) 81 | 82 | return when (expr.operator.type) { 83 | MINUS -> { 84 | right.negate() 85 | } 86 | else -> throw ExpressionException("Invalid unary operator") 87 | } 88 | } 89 | 90 | override fun visitCallExpr(expr: CallExpr): BigDecimal { 91 | val name = expr.name 92 | val function = 93 | functions[name.toLowerCase()] ?: throw ExpressionException("Undefined function '$name'") 94 | 95 | return function.call(expr.arguments.map { eval(it) }) 96 | } 97 | 98 | override fun visitLiteralExpr(expr: LiteralExpr): BigDecimal { 99 | return expr.value 100 | } 101 | 102 | override fun visitVariableExpr(expr: VariableExpr): BigDecimal { 103 | val name = expr.name.lexeme 104 | 105 | return variables[name.toLowerCase()] 106 | ?: throw ExpressionException("Undefined variable '$name'") 107 | } 108 | 109 | override fun visitGroupingExpr(expr: GroupingExpr): BigDecimal { 110 | return eval(expr.expression) 111 | } 112 | 113 | private infix fun Expr.or(right: Expr): BigDecimal { 114 | val left = eval(this) 115 | 116 | // short-circuit if left is truthy 117 | if (left.isTruthy()) return BigDecimal.ONE 118 | 119 | return eval(right).isTruthy().toBigDecimal() 120 | } 121 | 122 | private infix fun Expr.and(right: Expr): BigDecimal { 123 | val left = eval(this) 124 | 125 | // short-circuit if left is falsey 126 | if (!left.isTruthy()) return BigDecimal.ZERO 127 | 128 | return eval(right).isTruthy().toBigDecimal() 129 | } 130 | 131 | private fun BigDecimal.isTruthy(): Boolean { 132 | return this != BigDecimal.ZERO 133 | } 134 | 135 | private fun Boolean.toBigDecimal(): BigDecimal { 136 | return if (this) BigDecimal.ONE else BigDecimal.ZERO 137 | } 138 | 139 | private infix fun BigDecimal.pow(n: BigDecimal): BigDecimal { 140 | var right = n 141 | val signOfRight = right.signum() 142 | right = right.multiply(signOfRight.toBigDecimal()) 143 | val remainderOfRight = right.remainder(BigDecimal.ONE) 144 | val n2IntPart = right.subtract(remainderOfRight) 145 | val intPow = pow(n2IntPart.intValueExact(), mathContext) 146 | val doublePow = BigDecimal( 147 | Math 148 | .pow(toDouble(), remainderOfRight.toDouble()) 149 | ) 150 | 151 | var result = intPow.multiply(doublePow, mathContext) 152 | if (signOfRight == -1) result = BigDecimal 153 | .ONE.divide(result, mathContext.precision, RoundingMode.HALF_UP) 154 | 155 | return result 156 | } 157 | 158 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/exprk/internal/Expr.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.exprk.internal 2 | 3 | import java.math.BigDecimal 4 | 5 | internal sealed class Expr { 6 | 7 | abstract fun accept(visitor: ExprVisitor): R 8 | 9 | } 10 | 11 | internal class AssignExpr( 12 | val name: Token, 13 | val value: Expr 14 | ) : Expr() { 15 | 16 | override fun accept(visitor: ExprVisitor): R { 17 | return visitor.visitAssignExpr(this) 18 | } 19 | 20 | } 21 | 22 | internal class LogicalExpr( 23 | val left: Expr, 24 | val operator: Token, 25 | val right: Expr 26 | ) : Expr() { 27 | 28 | override fun accept(visitor: ExprVisitor): R { 29 | return visitor.visitLogicalExpr(this) 30 | } 31 | 32 | } 33 | 34 | internal class BinaryExpr( 35 | val left: Expr, 36 | val operator: Token, 37 | val right: Expr 38 | ) : Expr() { 39 | 40 | override fun accept(visitor: ExprVisitor): R { 41 | return visitor.visitBinaryExpr(this) 42 | } 43 | 44 | } 45 | 46 | internal class UnaryExpr( 47 | val operator: Token, 48 | val right: Expr 49 | ) : Expr() { 50 | 51 | override fun accept(visitor: ExprVisitor): R { 52 | return visitor.visitUnaryExpr(this) 53 | } 54 | 55 | } 56 | 57 | internal class CallExpr( 58 | val name: String, 59 | val arguments: List 60 | ) : Expr() { 61 | 62 | override fun accept(visitor: ExprVisitor): R { 63 | return visitor.visitCallExpr(this) 64 | } 65 | 66 | } 67 | 68 | internal class LiteralExpr(val value: BigDecimal) : Expr() { 69 | 70 | override fun accept(visitor: ExprVisitor): R { 71 | return visitor.visitLiteralExpr(this) 72 | } 73 | 74 | } 75 | 76 | internal class VariableExpr(val name: Token) : Expr() { 77 | 78 | override fun accept(visitor: ExprVisitor): R { 79 | return visitor.visitVariableExpr(this) 80 | } 81 | 82 | } 83 | 84 | internal class GroupingExpr(val expression: Expr) : Expr() { 85 | 86 | override fun accept(visitor: ExprVisitor): R { 87 | return visitor.visitGroupingExpr(this) 88 | } 89 | 90 | } 91 | 92 | internal interface ExprVisitor { 93 | 94 | fun visitAssignExpr(expr: AssignExpr): R 95 | 96 | fun visitLogicalExpr(expr: LogicalExpr): R 97 | 98 | fun visitBinaryExpr(expr: BinaryExpr): R 99 | 100 | fun visitUnaryExpr(expr: UnaryExpr): R 101 | 102 | fun visitCallExpr(expr: CallExpr): R 103 | 104 | fun visitLiteralExpr(expr: LiteralExpr): R 105 | 106 | fun visitVariableExpr(expr: VariableExpr): R 107 | 108 | fun visitGroupingExpr(expr: GroupingExpr): R 109 | 110 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/exprk/internal/Function.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.exprk.internal 2 | 3 | import java.math.BigDecimal 4 | 5 | abstract class Function { 6 | 7 | abstract fun call(arguments: List): BigDecimal 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/exprk/internal/Parser.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.exprk.internal 2 | 3 | import com.sudo.rizwan.composecalculator.exprk.ExpressionException 4 | import com.sudo.rizwan.composecalculator.exprk.internal.TokenType.* 5 | import java.math.BigDecimal 6 | 7 | internal class Parser(private val tokens: List) { 8 | 9 | private var current = 0 10 | 11 | fun parse(): Expr { 12 | val expr = expression() 13 | 14 | if (!isAtEnd()) { 15 | throw ExpressionException("Expected end of expression, found '${peek().lexeme}'") 16 | } 17 | 18 | return expr 19 | } 20 | 21 | private fun expression(): Expr { 22 | return assignment() 23 | } 24 | 25 | private fun assignment(): Expr { 26 | val expr = or() 27 | 28 | if (match(ASSIGN)) { 29 | val value = assignment() 30 | 31 | if (expr is VariableExpr) { 32 | val name = expr.name 33 | 34 | return AssignExpr(name, value) 35 | } else { 36 | throw ExpressionException("Invalid assignment target") 37 | } 38 | } 39 | 40 | return expr 41 | } 42 | 43 | private fun or(): Expr { 44 | var expr = and() 45 | 46 | while (match(BAR_BAR)) { 47 | val operator = previous() 48 | val right = and() 49 | 50 | expr = LogicalExpr(expr, operator, right) 51 | } 52 | 53 | return expr 54 | } 55 | 56 | private fun and(): Expr { 57 | var expr = equality() 58 | 59 | while (match(AMP_AMP)) { 60 | val operator = previous() 61 | val right = equality() 62 | 63 | expr = LogicalExpr(expr, operator, right) 64 | } 65 | 66 | return expr 67 | } 68 | 69 | private fun equality(): Expr { 70 | var left = comparison() 71 | 72 | while (match(EQUAL_EQUAL, NOT_EQUAL)) { 73 | val operator = previous() 74 | val right = comparison() 75 | 76 | left = BinaryExpr(left, operator, right) 77 | } 78 | 79 | return left 80 | } 81 | 82 | private fun comparison(): Expr { 83 | var left = addition() 84 | 85 | while (match(GREATER, GREATER_EQUAL, LESS, LESS_EQUAL)) { 86 | val operator = previous() 87 | val right = addition() 88 | 89 | left = BinaryExpr(left, operator, right) 90 | } 91 | 92 | return left 93 | } 94 | 95 | private fun addition(): Expr { 96 | var left = multiplication() 97 | 98 | while (match(PLUS, MINUS)) { 99 | val operator = previous() 100 | val right = multiplication() 101 | 102 | left = BinaryExpr(left, operator, right) 103 | } 104 | 105 | return left 106 | } 107 | 108 | private fun multiplication(): Expr { 109 | var left = unary() 110 | 111 | while (match(STAR, SLASH, MODULO)) { 112 | val operator = previous() 113 | val right = unary() 114 | 115 | left = BinaryExpr(left, operator, right) 116 | } 117 | 118 | return left 119 | } 120 | 121 | private fun unary(): Expr { 122 | if (match(MINUS)) { 123 | val operator = previous() 124 | val right = unary() 125 | 126 | return UnaryExpr(operator, right) 127 | } 128 | 129 | return exponent() 130 | } 131 | 132 | private fun exponent(): Expr { 133 | var left = call() 134 | 135 | if (match(EXPONENT)) { 136 | val operator = previous() 137 | val right = unary() 138 | 139 | left = BinaryExpr(left, operator, right) 140 | } 141 | 142 | return left 143 | } 144 | 145 | private fun call(): Expr { 146 | if (matchTwo(IDENTIFIER, LEFT_PAREN)) { 147 | val (name, _) = previousTwo() 148 | 149 | val arguments = mutableListOf() 150 | 151 | if (!check(RIGHT_PAREN)) { 152 | do { 153 | arguments += expression() 154 | } while (match(COMMA)) 155 | } 156 | 157 | consume(RIGHT_PAREN, "Expected ')' after function arguments") 158 | 159 | return CallExpr(name.lexeme, arguments) 160 | } 161 | 162 | return primary() 163 | } 164 | 165 | private fun primary(): Expr { 166 | if (match(NUMBER)) { 167 | return LiteralExpr(previous().literal as BigDecimal) 168 | } 169 | 170 | if (match(IDENTIFIER)) { 171 | return VariableExpr(previous()) 172 | } 173 | 174 | if (match(LEFT_PAREN)) { 175 | val expr = expression() 176 | 177 | consume(RIGHT_PAREN, "Expected ')' after '${previous().lexeme}'.") 178 | 179 | return GroupingExpr(expr) 180 | } 181 | 182 | throw ExpressionException("Expected expression after '${previous().lexeme}'.") 183 | } 184 | 185 | private fun match(vararg types: TokenType): Boolean { 186 | for (type in types) { 187 | if (check(type)) { 188 | advance() 189 | 190 | return true 191 | } 192 | } 193 | 194 | return false 195 | } 196 | 197 | private fun matchTwo(first: TokenType, second: TokenType): Boolean { 198 | val start = current 199 | 200 | if (match(first) && match(second)) { 201 | return true 202 | } 203 | 204 | current = start 205 | return false 206 | } 207 | 208 | private fun check(tokenType: TokenType): Boolean { 209 | return if (isAtEnd()) { 210 | false 211 | } else { 212 | peek().type === tokenType 213 | } 214 | } 215 | 216 | private fun consume(type: TokenType, message: String): Token { 217 | if (check(type)) return advance() 218 | 219 | throw ExpressionException(message) 220 | } 221 | 222 | private fun advance(): Token { 223 | if (!isAtEnd()) current++ 224 | 225 | return previous() 226 | } 227 | 228 | private fun isAtEnd() = peek().type == EOF 229 | 230 | private fun peek() = tokens[current] 231 | 232 | private fun previous() = tokens[current - 1] 233 | 234 | private fun previousTwo() = Pair(tokens[current - 2], tokens[current - 1]) 235 | 236 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/exprk/internal/Scanner.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.exprk.internal 2 | 3 | import com.sudo.rizwan.composecalculator.exprk.ExpressionException 4 | import com.sudo.rizwan.composecalculator.exprk.internal.TokenType.* 5 | import java.math.MathContext 6 | 7 | private fun invalidToken(c: Char) { 8 | throw ExpressionException("Invalid token '$c'") 9 | } 10 | 11 | internal class Scanner( 12 | private val source: String, 13 | private val mathContext: MathContext 14 | ) { 15 | 16 | private val tokens: MutableList = mutableListOf() 17 | private var start = 0 18 | private var current = 0 19 | 20 | fun scanTokens(): List { 21 | while (!isAtEnd()) { 22 | scanToken() 23 | } 24 | 25 | tokens.add(Token(EOF, "", null)) 26 | return tokens 27 | } 28 | 29 | private fun isAtEnd(): Boolean { 30 | return current >= source.length 31 | } 32 | 33 | private fun scanToken() { 34 | start = current 35 | val c = advance() 36 | 37 | when (c) { 38 | ' ', 39 | '\r', 40 | '\t' -> { 41 | // Ignore whitespace. 42 | } 43 | '+' -> addToken(PLUS) 44 | '-' -> addToken(MINUS) 45 | '*' -> addToken(STAR) 46 | '/' -> addToken(SLASH) 47 | '%' -> addToken(MODULO) 48 | '^' -> addToken(EXPONENT) 49 | '=' -> if (match('=')) addToken(EQUAL_EQUAL) else addToken(ASSIGN) 50 | '!' -> if (match('=')) addToken(NOT_EQUAL) else invalidToken(c) 51 | '>' -> if (match('=')) addToken(GREATER_EQUAL) else addToken(GREATER) 52 | '<' -> if (match('=')) addToken(LESS_EQUAL) else addToken(LESS) 53 | '|' -> if (match('|')) addToken(BAR_BAR) else invalidToken(c) 54 | '&' -> if (match('&')) addToken(AMP_AMP) else invalidToken(c) 55 | ',' -> addToken(COMMA) 56 | '(' -> addToken(LEFT_PAREN) 57 | ')' -> addToken(RIGHT_PAREN) 58 | else -> { 59 | when { 60 | c.isDigit() -> number() 61 | c.isAlpha() -> identifier() 62 | else -> invalidToken(c) 63 | } 64 | } 65 | } 66 | } 67 | 68 | private fun isDigit( 69 | char: Char, 70 | previousChar: Char = '\u0000', 71 | nextChar: Char = '\u0000' 72 | ): Boolean { 73 | return char.isDigit() || when (char) { 74 | '.' -> true 75 | 'e', 'E' -> previousChar.isDigit() && (nextChar.isDigit() || nextChar == '+' || nextChar == '-') 76 | '+', '-' -> (previousChar == 'e' || previousChar == 'E') && nextChar.isDigit() 77 | else -> false 78 | } 79 | } 80 | 81 | private fun number() { 82 | while (peek().isDigit()) advance() 83 | 84 | if (isDigit(peek(), peekPrevious(), peekNext())) { 85 | advance() 86 | while (isDigit(peek(), peekPrevious(), peekNext())) advance() 87 | } 88 | 89 | val value = source 90 | .substring(start, current) 91 | .toBigDecimal(mathContext) 92 | 93 | addToken(NUMBER, value) 94 | } 95 | 96 | private fun identifier() { 97 | while (peek().isAlphaNumeric()) advance() 98 | 99 | addToken(IDENTIFIER) 100 | } 101 | 102 | private fun advance() = source[current++] 103 | 104 | private fun peek(): Char { 105 | return if (isAtEnd()) { 106 | '\u0000' 107 | } else { 108 | source[current] 109 | } 110 | } 111 | 112 | private fun peekPrevious(): Char = if (current > 0) source[current - 1] else '\u0000' 113 | 114 | private fun peekNext(): Char { 115 | return if (current + 1 >= source.length) { 116 | '\u0000' 117 | } else { 118 | source[current + 1] 119 | } 120 | } 121 | 122 | private fun match(expected: Char): Boolean { 123 | if (isAtEnd()) return false 124 | if (source[current] != expected) return false 125 | 126 | current++ 127 | return true 128 | } 129 | 130 | private fun addToken(type: TokenType) = addToken(type, null) 131 | 132 | private fun addToken(type: TokenType, literal: Any?) { 133 | val text = source.substring(start, current) 134 | tokens.add(Token(type, text, literal)) 135 | } 136 | 137 | private fun Char.isAlphaNumeric() = isAlpha() || isDigit() 138 | 139 | private fun Char.isAlpha() = this in 'a'..'z' 140 | || this in 'A'..'Z' 141 | || this == '_' 142 | 143 | private fun Char.isDigit() = this == '.' || this in '0'..'9' 144 | 145 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/exprk/internal/Token.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.exprk.internal 2 | 3 | internal class Token( 4 | val type: TokenType, 5 | val lexeme: String, 6 | val literal: Any? 7 | ) { 8 | 9 | override fun toString(): String { 10 | return type.toString() + " " + lexeme + " " + literal 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/exprk/internal/TokenType.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.exprk.internal 2 | 3 | internal enum class TokenType { 4 | 5 | // Basic operators 6 | PLUS, 7 | MINUS, 8 | STAR, 9 | SLASH, 10 | MODULO, 11 | EXPONENT, 12 | ASSIGN, 13 | 14 | // Logical operators 15 | EQUAL_EQUAL, 16 | NOT_EQUAL, 17 | GREATER, 18 | GREATER_EQUAL, 19 | LESS, 20 | LESS_EQUAL, 21 | BAR_BAR, 22 | AMP_AMP, 23 | 24 | // Other 25 | COMMA, 26 | 27 | // Parentheses 28 | LEFT_PAREN, 29 | RIGHT_PAREN, 30 | 31 | // Literals 32 | NUMBER, 33 | IDENTIFIER, 34 | 35 | EOF 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/model/Drag.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.model 2 | 3 | import androidx.animation.AnimatedFloat 4 | import androidx.ui.foundation.animation.FlingConfig 5 | 6 | class Drag( 7 | val position: AnimatedFloat, 8 | val flingConfig: FlingConfig 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/model/Operation.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.model 2 | 3 | import androidx.compose.Model 4 | 5 | @Model 6 | data class Operation( 7 | val input: String, 8 | val output: String, 9 | val date: String 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/ui/BottomView.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.ui 2 | 3 | import androidx.compose.Composable 4 | import androidx.ui.core.Modifier 5 | import androidx.ui.foundation.Box 6 | import androidx.ui.foundation.ContentGravity 7 | import androidx.ui.layout.fillMaxWidth 8 | import androidx.ui.layout.preferredHeight 9 | import androidx.ui.unit.Dp 10 | import com.sudo.rizwan.composecalculator.AppState 11 | import com.sudo.rizwan.composecalculator.model.Drag 12 | 13 | @Composable() 14 | fun BottomView( 15 | boxHeight: Dp, 16 | sideDrag: Drag, 17 | topDrag: Drag 18 | ) { 19 | // Some maths to calculate alphas corresponding to the drag 20 | val sidePosition = sideDrag.position 21 | val sideDivisor = sidePosition.max / 255 22 | val alphaForSideDrag = (255 - sidePosition.value / sideDivisor).toInt() 23 | 24 | val topPosition = topDrag.position 25 | val topDivisor = topPosition.min / 255 26 | val alphaForTopDrag = (255 - topPosition.value / topDivisor).toInt() 27 | 28 | Box( 29 | modifier = Modifier.fillMaxWidth().preferredHeight(boxHeight), 30 | backgroundColor = AppState.theme.background, 31 | gravity = ContentGravity.BottomStart 32 | ) { 33 | NumbersPanel(alpha = alphaForSideDrag) 34 | SideView( 35 | boxHeight = boxHeight, 36 | drag = sideDrag 37 | ) 38 | // Overlay's alpha that corresponds to top view drag 39 | DimOverlay(alpha = alphaForTopDrag) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/ui/Content.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.ui 2 | 3 | import androidx.compose.Composable 4 | import androidx.ui.animation.animatedFloat 5 | import androidx.ui.core.Constraints 6 | import androidx.ui.core.DensityAmbient 7 | import androidx.ui.core.Modifier 8 | import androidx.ui.foundation.Box 9 | import androidx.ui.foundation.ContentGravity 10 | import androidx.ui.foundation.animation.AnchorsFlingConfig 11 | import androidx.ui.graphics.Color 12 | import androidx.ui.layout.fillMaxSize 13 | import androidx.ui.unit.Dp 14 | import androidx.ui.unit.dp 15 | import com.sudo.rizwan.composecalculator.model.Drag 16 | 17 | @Composable 18 | fun Content( 19 | constraints: Constraints, 20 | boxHeight: Dp, 21 | boxWidth: Dp 22 | ) { 23 | // Side drag 24 | val sideMin = 90.dp 25 | val sideMax = boxWidth - 30.dp 26 | val (sideMinPx, sideMaxPx) = with(DensityAmbient.current) { 27 | sideMin.toPx().value to sideMax.toPx().value 28 | } 29 | val sideFlingConfig = AnchorsFlingConfig(listOf(sideMinPx, sideMaxPx)) 30 | val sidePosition = animatedFloat(sideMaxPx) 31 | sidePosition.setBounds(sideMinPx, sideMaxPx) 32 | 33 | val sideDrag = Drag( 34 | position = sidePosition, 35 | flingConfig = sideFlingConfig 36 | ) 37 | 38 | // Top drag 39 | val topStart = -(constraints.maxHeight.value / 1.4f) 40 | val topMax = 0.dp 41 | val topMin = -(boxHeight / 1.4f) 42 | val (topMinPx, topMaxPx) = with(DensityAmbient.current) { 43 | topMin.toPx().value to topMax.toPx().value 44 | } 45 | val topFlingConfig = AnchorsFlingConfig(listOf(topMinPx, topMaxPx)) 46 | val topPosition = animatedFloat(topStart) // for dragging state 47 | topPosition.setBounds(topMinPx, topMaxPx) 48 | 49 | val topDrag = Drag( 50 | position = topPosition, 51 | flingConfig = topFlingConfig 52 | ) 53 | 54 | Box( 55 | modifier = Modifier.fillMaxSize(), 56 | backgroundColor = Color.Transparent, 57 | gravity = ContentGravity.BottomStart 58 | ) { 59 | BottomView( 60 | boxHeight = (boxHeight / 1.4f), 61 | sideDrag = sideDrag, 62 | topDrag = topDrag 63 | ) 64 | TopView( 65 | boxHeight = boxHeight, 66 | drag = topDrag 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/ui/DimOverlay.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.ui 2 | 3 | import androidx.compose.Composable 4 | import androidx.ui.core.Modifier 5 | import androidx.ui.foundation.Box 6 | import androidx.ui.graphics.Color 7 | import androidx.ui.layout.fillMaxSize 8 | 9 | @Composable 10 | fun DimOverlay(alpha: Int) { 11 | Box(modifier = Modifier.fillMaxSize(), backgroundColor = Color(50, 50, 50, alpha)) 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/ui/NumbersPanel.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.ui 2 | 3 | import androidx.compose.Composable 4 | import androidx.ui.core.Modifier 5 | import androidx.ui.foundation.* 6 | import androidx.ui.graphics.Color 7 | import androidx.ui.layout.* 8 | import androidx.ui.layout.ColumnScope.weight 9 | import androidx.ui.material.Divider 10 | import androidx.ui.material.IconButton 11 | import androidx.ui.res.vectorResource 12 | import androidx.ui.text.TextStyle 13 | import androidx.ui.text.font.font 14 | import androidx.ui.text.font.fontFamily 15 | import androidx.ui.unit.dp 16 | import androidx.ui.unit.sp 17 | import com.sudo.rizwan.composecalculator.AppState 18 | import com.sudo.rizwan.composecalculator.AppState.inputText 19 | import com.sudo.rizwan.composecalculator.R 20 | import com.sudo.rizwan.composecalculator.performCalculation 21 | import com.sudo.rizwan.composecalculator.saveCalculationToHistory 22 | 23 | private val operationsColumn = listOf("Delete", "÷", "x", "-", "+") 24 | private val numberColumns = listOf( 25 | listOf("7", "4", "1", "0"), 26 | listOf("8", "5", "2", "."), 27 | listOf("9", "6", "3", "=") 28 | ) 29 | 30 | @Composable() 31 | fun NumbersPanel(alpha: Int) { 32 | Stack { 33 | Row(modifier = Modifier.fillMaxSize()) { 34 | numberColumns.forEach { numberColumn -> 35 | Column(modifier = Modifier.weight(1f)) { 36 | numberColumn.forEach { text -> 37 | MainContentButton(text) 38 | } 39 | } 40 | } 41 | Divider( 42 | modifier = Modifier.preferredWidth(1.dp).fillMaxHeight(), 43 | color = Color(0xFFd3d3d3) 44 | ) 45 | Column(modifier = Modifier.weight(1.3f)) { 46 | operationsColumn.forEach { operation -> 47 | OperationItem(text = operation) 48 | } 49 | } 50 | Spacer(modifier = Modifier.preferredWidth(30.dp)) 51 | } 52 | DimOverlay(alpha = alpha) 53 | } 54 | } 55 | 56 | @Composable() 57 | fun MainContentButton(text: String) { 58 | IconButton(modifier = Modifier.weight(1f).fillMaxWidth(), onClick = { 59 | if (text == "=") { 60 | performCalculation() 61 | saveCalculationToHistory() 62 | } else { 63 | if (inputText.text.length < 10) { 64 | inputText = TextFieldValue(text = inputText.text + text) 65 | } 66 | 67 | performCalculation() 68 | } 69 | }) { 70 | Box(gravity = ContentGravity.Center) { 71 | Text( 72 | text = text, 73 | style = TextStyle( 74 | fontFamily = fontFamily(listOf(font(resId = R.font.jost_regular))), 75 | fontSize = 29.sp 76 | ) 77 | ) 78 | } 79 | } 80 | } 81 | 82 | @Composable() 83 | fun OperationItem(text: String) { 84 | if (text == "Delete") { 85 | IconButton( 86 | modifier = Modifier.weight(1f).fillMaxWidth(), 87 | onClick = { 88 | if (inputText.text.isNotEmpty()) { 89 | inputText = TextFieldValue( 90 | text = inputText.text.substring(0, inputText.text.length - 1) 91 | ) 92 | } 93 | performCalculation() 94 | }) { 95 | Icon( 96 | asset = vectorResource(id = R.drawable.ic_outline_backspace_24), 97 | tint = AppState.theme.primary 98 | ) 99 | } 100 | } else { 101 | IconButton(modifier = Modifier.weight(1f).fillMaxWidth(), onClick = { 102 | AppState.outputText = TextFieldValue(text = "") 103 | inputText = TextFieldValue(text = inputText.text + text) 104 | }) { 105 | Box(gravity = ContentGravity.Center) { 106 | Text( 107 | text = text, 108 | style = TextStyle( 109 | color = AppState.theme.primary, 110 | fontSize = 22.sp 111 | ) 112 | ) 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/ui/SideView.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.ui 2 | 3 | import androidx.compose.Composable 4 | import androidx.compose.state 5 | import androidx.ui.core.DensityAmbient 6 | import androidx.ui.core.Modifier 7 | import androidx.ui.foundation.Box 8 | import androidx.ui.foundation.ContentGravity 9 | import androidx.ui.foundation.Icon 10 | import androidx.ui.foundation.Text 11 | import androidx.ui.foundation.animation.fling 12 | import androidx.ui.foundation.gestures.DragDirection 13 | import androidx.ui.foundation.gestures.draggable 14 | import androidx.ui.graphics.Color 15 | import androidx.ui.layout.* 16 | import androidx.ui.layout.ColumnScope.weight 17 | import androidx.ui.material.IconButton 18 | import androidx.ui.res.vectorResource 19 | import androidx.ui.text.TextStyle 20 | import androidx.ui.text.font.FontWeight 21 | import androidx.ui.unit.Dp 22 | import androidx.ui.unit.dp 23 | import androidx.ui.unit.sp 24 | import com.sudo.rizwan.composecalculator.AppState 25 | import com.sudo.rizwan.composecalculator.model.Drag 26 | import com.sudo.rizwan.composecalculator.R 27 | 28 | @Composable() 29 | fun SideView( 30 | boxHeight: Dp, 31 | drag: Drag 32 | ) { 33 | val position = drag.position 34 | val flingConfig = drag.flingConfig 35 | val xOffset = with(DensityAmbient.current) { position.value.toDp() } 36 | val toggleAsset = state { R.drawable.ic_keyboard_arrow_left_24 } 37 | Box( 38 | Modifier.offset(x = xOffset, y = 0.dp) 39 | .fillMaxWidth() 40 | .draggable( 41 | startDragImmediately = position.isRunning, 42 | dragDirection = DragDirection.Horizontal, 43 | onDragStopped = { position.fling(flingConfig, it) } 44 | ) { delta -> 45 | position.snapTo(position.value + delta) 46 | delta 47 | } 48 | .preferredHeight(boxHeight), 49 | backgroundColor = AppState.theme.primary, 50 | gravity = ContentGravity.CenterStart 51 | ) { 52 | Box(modifier = Modifier.preferredWidth(30.dp), gravity = ContentGravity.Center) { 53 | toggleAsset.value = when (position.value) { 54 | position.max -> { 55 | R.drawable.ic_keyboard_arrow_left_24 56 | } 57 | position.min -> { 58 | R.drawable.ic_keyboard_arrow_right_24 59 | } 60 | else -> { 61 | toggleAsset.value 62 | } 63 | } 64 | 65 | IconButton(onClick = { 66 | if (toggleAsset.value == R.drawable.ic_keyboard_arrow_left_24) { 67 | position.animateTo(position.min) 68 | } else { 69 | position.animateTo(position.max) 70 | } 71 | }) { 72 | if (toggleAsset.value == R.drawable.ic_keyboard_arrow_left_24) { 73 | Icon( 74 | asset = vectorResource(id = toggleAsset.value), 75 | tint = Color.White 76 | ) 77 | } else { 78 | Icon( 79 | asset = vectorResource(id = toggleAsset.value), 80 | tint = Color.White 81 | ) 82 | } 83 | } 84 | } 85 | Options() 86 | } 87 | } 88 | 89 | @Composable 90 | private fun Options() { 91 | val optionColumns = listOf( 92 | listOf("INV", "sin", "ln", "π", "("), 93 | listOf("RAD", "cos", "log", "e", ")"), 94 | listOf("%", "tan", "√", "^", "!") 95 | ) 96 | Row(modifier = Modifier.fillMaxSize().padding(start = 20.dp, end = 110.dp)) { 97 | // Three columns 98 | optionColumns.forEach { optionColumn -> 99 | Column(modifier = Modifier.weight(1f)) { 100 | optionColumn.forEach { text -> 101 | SideViewTextItem(text = text) 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | @Composable() 109 | private fun SideViewTextItem(text: String) { 110 | IconButton(modifier = Modifier.weight(1f).fillMaxWidth(), onClick = {}) { 111 | Box(gravity = ContentGravity.Center) { 112 | Text( 113 | text = text, 114 | style = TextStyle( 115 | color = Color.White, 116 | fontSize = 17.sp, 117 | fontWeight = FontWeight.Normal 118 | ) 119 | ) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/sudo/rizwan/composecalculator/ui/TopView.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator.ui 2 | 3 | import androidx.animation.AnimatedFloat 4 | import androidx.compose.Composable 5 | import androidx.ui.core.Alignment 6 | import androidx.ui.core.DensityAmbient 7 | import androidx.ui.core.Modifier 8 | import androidx.ui.foundation.* 9 | import androidx.ui.foundation.animation.fling 10 | import androidx.ui.foundation.gestures.DragDirection 11 | import androidx.ui.foundation.gestures.draggable 12 | import androidx.ui.graphics.Color 13 | import androidx.ui.input.KeyboardType 14 | import androidx.ui.layout.* 15 | import androidx.ui.layout.ColumnScope.weight 16 | import androidx.ui.material.* 17 | import androidx.ui.material.icons.Icons 18 | import androidx.ui.material.icons.filled.ArrowBack 19 | import androidx.ui.res.vectorResource 20 | import androidx.ui.text.TextStyle 21 | import androidx.ui.text.font.FontWeight 22 | import androidx.ui.text.style.TextOverflow 23 | import androidx.ui.unit.Dp 24 | import androidx.ui.unit.dp 25 | import androidx.ui.unit.sp 26 | import com.sudo.rizwan.composecalculator.* 27 | import com.sudo.rizwan.composecalculator.AppState.inputText 28 | import com.sudo.rizwan.composecalculator.AppState.outputText 29 | import com.sudo.rizwan.composecalculator.R 30 | import com.sudo.rizwan.composecalculator.model.Drag 31 | import com.sudo.rizwan.composecalculator.model.Operation 32 | import kotlin.math.abs 33 | 34 | @Composable 35 | fun TopView( 36 | boxHeight: Dp, 37 | drag: Drag 38 | ) { 39 | val position = drag.position 40 | val flingConfig = drag.flingConfig 41 | val yOffset = with(DensityAmbient.current) { position.value.toDp() } 42 | val scrollerPosition = ScrollerPosition() 43 | // scroll the history list to bottom when dragging the top panel 44 | // 90dp history item height is an approximation 45 | scrollerPosition.smoothScrollBy(operationsHistory.size * 90.dp.value) 46 | 47 | Card( 48 | Modifier.offset(y = yOffset, x = 0.dp).fillMaxWidth() 49 | .draggable( 50 | startDragImmediately = position.isRunning, 51 | dragDirection = DragDirection.Vertical, 52 | onDragStopped = { position.fling(flingConfig, it) } 53 | ) { delta -> 54 | position.snapTo(position.value + delta) 55 | delta 56 | } 57 | .preferredHeight(boxHeight), 58 | elevation = 4.dp, 59 | shape = MaterialTheme.shapes.large, 60 | color = Color.White 61 | ) { 62 | Column( 63 | modifier = Modifier.fillMaxSize(), 64 | verticalArrangement = Arrangement.Bottom, 65 | horizontalGravity = ContentGravity.CenterHorizontally 66 | ) { 67 | HistoryTopBar() 68 | HistoryList(scrollerPosition) 69 | Spacer(modifier = Modifier.preferredHeight(2.dp)) 70 | MainContent(boxHeight, position) 71 | RoundedDash() 72 | } 73 | } 74 | } 75 | 76 | @Composable 77 | private fun HistoryTopBar() { 78 | Surface(elevation = 2.dp, color = Color.White) { 79 | Row( 80 | modifier = Modifier.fillMaxWidth().preferredHeight(56.dp), 81 | horizontalArrangement = Arrangement.SpaceBetween, 82 | verticalGravity = ContentGravity.CenterVertically 83 | ) { 84 | Row(verticalGravity = Alignment.CenterVertically) { 85 | IconButton( 86 | onClick = {} 87 | ) { 88 | Icon( 89 | asset = Icons.Filled.ArrowBack, 90 | tint = AppState.theme.primary 91 | ) 92 | } 93 | Spacer(modifier = Modifier.preferredWidth(16.dp)) 94 | Text( 95 | text = "History", 96 | style = TextStyle( 97 | color = AppState.theme.primary, 98 | fontSize = 20.sp, 99 | fontWeight = FontWeight.SemiBold, 100 | fontFamily = jostFontFamily 101 | ) 102 | ) 103 | } 104 | IconButton(onClick = {}) { 105 | Icon( 106 | asset = vectorResource(id = R.drawable.ic_more_vert_24), 107 | tint = AppState.theme.primary 108 | ) 109 | } 110 | } 111 | } 112 | } 113 | 114 | @Composable 115 | private fun HistoryList(scrollerPosition: ScrollerPosition) { 116 | VerticalScroller(scrollerPosition = scrollerPosition, modifier = Modifier.weight(1f)) { 117 | Column { 118 | operationsHistory.forEachIndexed { index, item -> 119 | HistoryItem(item) 120 | if (index != operationsHistory.size - 1) { 121 | Divider() 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | @Composable 129 | private fun HistoryItem(item: Operation) { 130 | Column( 131 | modifier = Modifier.padding(top = 16.dp, bottom = 36.dp, start = 24.dp, end = 16.dp), 132 | horizontalGravity = Alignment.End 133 | ) { 134 | Text( 135 | modifier = Modifier.fillMaxWidth(), 136 | text = item.date, style = TextStyle( 137 | color = AppState.theme.primary, fontSize = 16.sp, 138 | fontFamily = jostFontFamily 139 | ) 140 | ) 141 | Text( 142 | text = item.input, 143 | style = TextStyle( 144 | color = Color(0xFF2C2C2C), 145 | fontSize = 36.sp, 146 | fontFamily = jostFontFamily 147 | ) 148 | ) 149 | Text( 150 | text = item.output, 151 | style = TextStyle( 152 | color = Color(0xFF636363), 153 | fontSize = 36.sp, 154 | fontFamily = jostFontFamily 155 | ) 156 | ) 157 | } 158 | } 159 | 160 | @Composable 161 | private fun MainContent(boxHeight: Dp, position: AnimatedFloat) { 162 | // Some scary maths 163 | val originalHeight = (boxHeight.value / 3.7) 164 | val div = abs(position.min / originalHeight) 165 | val adjustedHeight = originalHeight + ((position.min - position.value) / div) 166 | val height = if (adjustedHeight > 0f) { 167 | adjustedHeight.toFloat() 168 | } else { 169 | 0f 170 | } 171 | val shouldDisplay = position.min - position.value == 0f 172 | Column( 173 | modifier = Modifier.preferredHeight(height.dp) 174 | .fillMaxWidth() 175 | ) { 176 | if (shouldDisplay) { 177 | // No space between top bar & text fields 178 | Column { 179 | MainTopBar() 180 | TextFields() 181 | } 182 | } 183 | } 184 | } 185 | 186 | @Composable 187 | private fun MainTopBar() { 188 | Row( 189 | modifier = Modifier.fillMaxWidth().preferredHeight(56.dp), 190 | horizontalArrangement = Arrangement.SpaceBetween, 191 | verticalGravity = ContentGravity.CenterVertically 192 | ) { 193 | Button( 194 | onClick = {}, 195 | backgroundColor = Color.Transparent, 196 | elevation = 0.dp 197 | ) { 198 | Text( 199 | text = "DEG", style = TextStyle( 200 | color = Color(0xFF636363), 201 | fontFamily = jostFontFamily, 202 | fontWeight = FontWeight.SemiBold, 203 | fontSize = 16.sp 204 | ) 205 | ) 206 | } 207 | IconButton(onClick = {}) { 208 | Icon( 209 | asset = vectorResource(id = R.drawable.ic_more_vert_24), 210 | tint = Color(0xFF636363) 211 | ) 212 | } 213 | } 214 | } 215 | 216 | @Composable 217 | private fun TextFields() { 218 | Column( 219 | modifier = Modifier.padding(start = 16.dp, end = 16.dp).fillMaxWidth(), 220 | horizontalGravity = Alignment.End 221 | ) { 222 | TextField( 223 | value = inputText, 224 | onValueChange = { textFieldValue -> 225 | if (textFieldValue.text.length < 10) { 226 | inputText = textFieldValue 227 | } 228 | }, 229 | keyboardType = KeyboardType.Number, 230 | textStyle = TextStyle(fontSize = 46.sp, fontFamily = jostFontFamily) 231 | ) 232 | Text( 233 | text = outputText.text, 234 | style = TextStyle( 235 | color = grayColor, 236 | fontSize = 36.sp, 237 | fontFamily = jostFontFamily 238 | ), 239 | overflow = TextOverflow.Ellipsis, 240 | softWrap = false, 241 | maxLines = 1 242 | ) 243 | } 244 | } 245 | 246 | @Composable 247 | private fun RoundedDash() { 248 | Box( 249 | modifier = Modifier.preferredHeight(24.dp).preferredWidth(30.dp) 250 | .padding(top = 10.dp, bottom = 10.dp) 251 | ) { 252 | Icon( 253 | asset = vectorResource(id = R.drawable.ic_rounded_dash), 254 | tint = Color(0xFFD3D3D3) 255 | ) 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keyboard_arrow_left_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keyboard_arrow_right_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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_more_vert_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_outline_backspace_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_rounded_dash.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/font/jost_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/app/src/main/res/font/jost_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #08a0e9 4 | #15202b 5 | #c0deed 6 | #000000 7 | #FFFFFF 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ComposeCalculator 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/test/java/com/sudo/rizwan/composecalculator/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.sudo.rizwan.composecalculator 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = '1.3.72' 4 | def compose_release_version = "dev09" 5 | ext.compose_version = "0.1.0-$compose_release_version" 6 | ext.compose_compiler_extension_version = "0.1.0-$compose_release_version" 7 | 8 | repositories { 9 | google() 10 | jcenter() 11 | } 12 | dependencies { 13 | classpath "com.android.tools.build:gradle:4.1.0-alpha09" 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | 16 | // NOTE: Do not place your application dependencies here; they belong 17 | // in the individual module build.gradle files 18 | } 19 | } 20 | 21 | allprojects { 22 | repositories { 23 | google() 24 | jcenter() 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } -------------------------------------------------------------------------------- /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 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 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon May 11 17:00:54 PKT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedrizwan/JetpackComposeCalculator/ac2dd91611b742c4f5624778d50141964cce1ba0/screenshot.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "ComposeCalculator" --------------------------------------------------------------------------------