From b59a8f7119f75f3e478f2c8574acbca0586936e6 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 9 Nov 2024 22:29:01 +0700 Subject: [PATCH 01/29] Create canvas module --- canvas/.gitignore | 1 + canvas/build.gradle.kts | 43 +++++++++++++++++++ canvas/consumer-rules.pro | 0 canvas/proguard-rules.pro | 21 +++++++++ .../canvas/ExampleInstrumentedTest.kt | 24 +++++++++++ canvas/src/main/AndroidManifest.xml | 4 ++ .../canvas/presentation/ArkCanvasFragment.kt | 37 ++++++++++++++++ .../main/res/layout/fragment_ark_canvas.xml | 14 ++++++ canvas/src/main/res/values/strings.xml | 4 ++ .../dev/arkbuilders/canvas/ExampleUnitTest.kt | 17 ++++++++ gradle/libs.versions.toml | 11 +++++ sample/build.gradle.kts | 1 + sample/src/main/AndroidManifest.xml | 10 ++--- .../dev/arkbuilders/sample/MainActivity.kt | 6 ++- .../sample/canvas/CanvasActivity.kt | 23 ++++++++++ .../sample/canvas/CanvasFragment.kt | 38 ++++++++++++++++ .../src/main/res/layout/activity_canvas.xml | 11 +++++ sample/src/main/res/layout/activity_main.xml | 9 ++++ .../src/main/res/layout/fragment_canvas.xml | 12 ++++++ sample/src/main/res/values/strings.xml | 3 ++ settings.gradle.kts | 3 ++ 21 files changed, 286 insertions(+), 6 deletions(-) create mode 100644 canvas/.gitignore create mode 100644 canvas/build.gradle.kts create mode 100644 canvas/consumer-rules.pro create mode 100644 canvas/proguard-rules.pro create mode 100644 canvas/src/androidTest/java/dev/arkbuilders/canvas/ExampleInstrumentedTest.kt create mode 100644 canvas/src/main/AndroidManifest.xml create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt create mode 100644 canvas/src/main/res/layout/fragment_ark_canvas.xml create mode 100644 canvas/src/main/res/values/strings.xml create mode 100644 canvas/src/test/java/dev/arkbuilders/canvas/ExampleUnitTest.kt create mode 100644 sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasActivity.kt create mode 100644 sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasFragment.kt create mode 100644 sample/src/main/res/layout/activity_canvas.xml create mode 100644 sample/src/main/res/layout/fragment_canvas.xml diff --git a/canvas/.gitignore b/canvas/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/canvas/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/canvas/build.gradle.kts b/canvas/build.gradle.kts new file mode 100644 index 0000000..ed6d0af --- /dev/null +++ b/canvas/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsKotlinAndroid) +} + +android { + namespace = "dev.arkbuilders.canvas" + compileSdk = 34 + + defaultConfig { + minSdk = 29 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.android.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.espresso) +} \ No newline at end of file diff --git a/canvas/consumer-rules.pro b/canvas/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/canvas/proguard-rules.pro b/canvas/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/canvas/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/canvas/src/androidTest/java/dev/arkbuilders/canvas/ExampleInstrumentedTest.kt b/canvas/src/androidTest/java/dev/arkbuilders/canvas/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..d31254f --- /dev/null +++ b/canvas/src/androidTest/java/dev/arkbuilders/canvas/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package dev.arkbuilders.canvas + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("dev.arkbuilders.canvas.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/canvas/src/main/AndroidManifest.xml b/canvas/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/canvas/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt new file mode 100644 index 0000000..87b53fa --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt @@ -0,0 +1,37 @@ +package dev.arkbuilders.canvas.presentation + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import dev.arkbuilders.canvas.R + +private const val imagePath = "image_path_param" + +class ArkCanvasFragment : Fragment() { + private var imagePathParam: String? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + imagePathParam = it.getString(imagePath) + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_ark_canvas, container, false) + } + + companion object { + @JvmStatic + fun newInstance(param1: String) = + ArkCanvasFragment().apply { + arguments = Bundle().apply { + putString(imagePath, param1) + } + } + } +} \ No newline at end of file diff --git a/canvas/src/main/res/layout/fragment_ark_canvas.xml b/canvas/src/main/res/layout/fragment_ark_canvas.xml new file mode 100644 index 0000000..f224669 --- /dev/null +++ b/canvas/src/main/res/layout/fragment_ark_canvas.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/canvas/src/main/res/values/strings.xml b/canvas/src/main/res/values/strings.xml new file mode 100644 index 0000000..6048840 --- /dev/null +++ b/canvas/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Hello blank fragment + \ No newline at end of file diff --git a/canvas/src/test/java/dev/arkbuilders/canvas/ExampleUnitTest.kt b/canvas/src/test/java/dev/arkbuilders/canvas/ExampleUnitTest.kt new file mode 100644 index 0000000..5f8a8e9 --- /dev/null +++ b/canvas/src/test/java/dev/arkbuilders/canvas/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package dev.arkbuilders.canvas + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 19dcc50..2ebf516 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,11 @@ flexbox = "3.0.0" composeActivity = "1.9.0" composeBom = "2024.06.00" composeCompiler = "1.5.10" +activity = "1.8.0" +kotlin = "1.9.22" +constraintlayout = "2.1.4" +uiAndroid = "1.7.5" +agp = "8.2.2" [libraries] coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } @@ -47,3 +52,9 @@ androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graph androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "uiAndroid" } +[plugins] +jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 5897047..5579f68 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -72,6 +72,7 @@ android { dependencies { implementation(project(":filepicker")) implementation(project(":about")) + implementation(project(":canvas")) implementation(libraries.arklib) implementation("androidx.core:core-ktx:1.12.0") diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 4680920..d7fc38f 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -6,7 +6,6 @@ android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:ignore="ScopedStorage" /> - @@ -21,8 +20,10 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ArkComponents" - tools:targetApi="31" > - + tools:targetApi="31"> + - - + \ No newline at end of file diff --git a/sample/src/main/java/dev/arkbuilders/sample/MainActivity.kt b/sample/src/main/java/dev/arkbuilders/sample/MainActivity.kt index db201bd..b5cc7e9 100644 --- a/sample/src/main/java/dev/arkbuilders/sample/MainActivity.kt +++ b/sample/src/main/java/dev/arkbuilders/sample/MainActivity.kt @@ -18,6 +18,7 @@ import dev.arkbuilders.components.filepicker.ArkFilePickerFragment import dev.arkbuilders.components.filepicker.ArkFilePickerMode import dev.arkbuilders.components.filepicker.onArkPathPicked import dev.arkbuilders.sample.about.AboutActivity +import dev.arkbuilders.sample.canvas.CanvasActivity import dev.arkbuilders.sample.storage.StorageDemoFragment class MainActivity : AppCompatActivity() { @@ -54,11 +55,14 @@ class MainActivity : AppCompatActivity() { findViewById(R.id.btn_storage_demo).setOnClickListener { StorageDemoFragment().show(supportFragmentManager, StorageDemoFragment::class.java.name) } - findViewById(R.id.btn_about).setOnClickListener { val intent = Intent(this, AboutActivity::class.java) startActivity(intent) } + findViewById(R.id.btn_open_file_mode).setOnClickListener { + val intent = Intent(this, CanvasActivity::class.java) + startActivity(intent) + } } private fun getFilePickerConfig(mode: ArkFilePickerMode? = null) = ArkFilePickerConfig( diff --git a/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasActivity.kt b/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasActivity.kt new file mode 100644 index 0000000..365cba7 --- /dev/null +++ b/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasActivity.kt @@ -0,0 +1,23 @@ +package dev.arkbuilders.sample.canvas + +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import dev.arkbuilders.canvas.presentation.ArkCanvasFragment +import dev.arkbuilders.sample.R + +class CanvasActivity : AppCompatActivity() { + @SuppressLint("CommitTransaction") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_canvas) + val canvasFragment = ArkCanvasFragment.newInstance( + param1 = "imagePath" + ) + + supportFragmentManager + .beginTransaction() + .replace(R.id.canvas_content, canvasFragment) + .commit() + } +} \ No newline at end of file diff --git a/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasFragment.kt b/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasFragment.kt new file mode 100644 index 0000000..f1092ca --- /dev/null +++ b/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasFragment.kt @@ -0,0 +1,38 @@ +package dev.arkbuilders.sample.canvas + + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import dev.arkbuilders.sample.R + +private const val imagePath = "image_path_param" + +class CanvasFragment : Fragment() { + private var imagePathParam: String? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + imagePathParam = it.getString(imagePath) + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_canvas, container, false) + } + + companion object { + @JvmStatic + fun newInstance(param1: String) = + CanvasFragment().apply { + arguments = Bundle().apply { + putString(imagePath, param1) + } + } + } +} \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_canvas.xml b/sample/src/main/res/layout/activity_canvas.xml new file mode 100644 index 0000000..a79f577 --- /dev/null +++ b/sample/src/main/res/layout/activity_canvas.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index 09d74b6..b3864a7 100644 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -50,4 +50,13 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/btn_storage_demo"/> + + \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_canvas.xml b/sample/src/main/res/layout/fragment_canvas.xml new file mode 100644 index 0000000..5d694b3 --- /dev/null +++ b/sample/src/main/res/layout/fragment_canvas.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 0701830..9edc949 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -9,4 +9,7 @@ Delete map entry Empty Open About + Open Canvas + + Hello blank fragment \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b7b6b93..0dceab7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,8 @@ import java.net.URI +include(":canvas") + + pluginManagement { repositories { google() From 6c884b9c3b3462eccc40145947e7222319bd518d Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Tue, 12 Nov 2024 23:19:54 +0700 Subject: [PATCH 02/29] Resolve resource & setup build tool --- canvas/build.gradle.kts | 36 + .../canvas/presentation/ArkCanvasFragment.kt | 33 + .../canvas/presentation/data/ImageDefaults.kt | 23 + .../canvas/presentation/data/Preferences.kt | 92 ++ .../canvas/presentation/drawing/EditCanvas.kt | 335 ++++++ .../presentation/drawing/EditManager.kt | 716 +++++++++++ .../presentation/edit/ColorPickerDialog.kt | 254 ++++ .../presentation/edit/ConfirmClearDialog.kt | 52 + .../canvas/presentation/edit/EditScreen.kt | 1070 +++++++++++++++++ .../canvas/presentation/edit/EditViewModel.kt | 519 ++++++++ .../presentation/edit/MoreOptionsPopup.kt | 131 ++ .../edit/NewImageOptionsDialog.kt | 328 +++++ .../canvas/presentation/edit/Operation.kt | 9 + .../presentation/edit/SavePathDialog.kt | 231 ++++ .../edit/TransparencyChessBoard.kt | 91 ++ .../edit/blur/BlurIntensityPopup.kt | 56 + .../presentation/edit/blur/BlurOperation.kt | 183 +++ .../edit/crop/CropAspectRatiosMenu.kt | 241 ++++ .../presentation/edit/crop/CropOperation.kt | 53 + .../presentation/edit/crop/CropWindow.kt | 443 +++++++ .../presentation/edit/draw/DrawOperation.kt | 37 + .../presentation/edit/resize/ResizeInput.kt | 201 ++++ .../edit/resize/ResizeOperation.kt | 114 ++ .../edit/rotate/RotateOperation.kt | 47 + .../canvas/presentation/graphics/Color.kt | 22 + .../canvas/presentation/graphics/ColorCode.kt | 13 + .../canvas/presentation/graphics/SVG.kt | 303 +++++ .../canvas/presentation/graphics/Size.kt | 14 + .../presentation/picker/FilePickerScreen.kt | 155 +++ .../resourceloader/BitmapResourceLoader.kt | 60 + .../resourceloader/CanvasResourceLoader.kt | 9 + .../resourceloader/SvgResourceLoader.kt | 15 + .../canvas/presentation/theme/Color.kt | 9 + .../canvas/presentation/theme/Shape.kt | 11 + .../canvas/presentation/theme/Theme.kt | 47 + .../canvas/presentation/theme/Type.kt | 28 + .../presentation/utils/GraphicBrushExt.kt | 90 ++ .../canvas/presentation/utils/ImageHelper.kt | 33 + .../presentation/utils/PermissionsHelper.kt | 45 + .../canvas/presentation/utils/SVG.kt | 307 +++++ .../canvas/presentation/utils/Utils.kt | 105 ++ .../utils/adapters/BrushAttribute.kt | 27 + canvas/src/main/res/drawable/ic_add.xml | 13 + .../src/main/res/drawable/ic_arrow_back.xml | 5 + .../src/main/res/drawable/ic_aspect_ratio.xml | 5 + canvas/src/main/res/drawable/ic_blur_on.xml | 5 + canvas/src/main/res/drawable/ic_check.xml | 5 + canvas/src/main/res/drawable/ic_clear.xml | 5 + canvas/src/main/res/drawable/ic_crop.xml | 5 + canvas/src/main/res/drawable/ic_crop_16_9.xml | 5 + canvas/src/main/res/drawable/ic_crop_3_2.xml | 5 + canvas/src/main/res/drawable/ic_crop_5_4.xml | 5 + .../src/main/res/drawable/ic_crop_square.xml | 5 + canvas/src/main/res/drawable/ic_eraser.xml | 13 + .../src/main/res/drawable/ic_eyedropper.xml | 4 + .../src/main/res/drawable/ic_insert_photo.xml | 5 + .../src/main/res/drawable/ic_line_weight.xml | 5 + canvas/src/main/res/drawable/ic_more_vert.xml | 5 + canvas/src/main/res/drawable/ic_pan_tool.xml | 5 + canvas/src/main/res/drawable/ic_redo.xml | 5 + .../res/drawable/ic_rotate_90_degrees_ccw.xml | 5 + .../src/main/res/drawable/ic_rotate_left.xml | 5 + .../src/main/res/drawable/ic_rotate_right.xml | 5 + canvas/src/main/res/drawable/ic_save.xml | 5 + canvas/src/main/res/drawable/ic_share.xml | 5 + canvas/src/main/res/drawable/ic_undo.xml | 5 + canvas/src/main/res/drawable/ic_zoom_in.xml | 6 + .../main/res/layout/fragment_ark_canvas.xml | 12 +- canvas/src/main/res/values/strings.xml | 38 + gradle/libs.versions.toml | 2 +- sample/build.gradle.kts | 8 +- .../sample/canvas/CanvasFragment.kt | 28 +- 72 files changed, 6782 insertions(+), 35 deletions(-) create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/data/ImageDefaults.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/data/Preferences.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/ColorPickerDialog.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/ConfirmClearDialog.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/MoreOptionsPopup.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/Operation.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/SavePathDialog.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/TransparencyChessBoard.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/blur/BlurIntensityPopup.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/blur/BlurOperation.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/crop/CropAspectRatiosMenu.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/crop/CropOperation.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/crop/CropWindow.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/draw/DrawOperation.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/resize/ResizeInput.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/resize/ResizeOperation.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/rotate/RotateOperation.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Color.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/ColorCode.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/SVG.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Size.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/picker/FilePickerScreen.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceLoader.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/CanvasResourceLoader.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceLoader.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Color.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Shape.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Theme.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Type.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/GraphicBrushExt.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/ImageHelper.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/PermissionsHelper.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/Utils.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/adapters/BrushAttribute.kt create mode 100644 canvas/src/main/res/drawable/ic_add.xml create mode 100644 canvas/src/main/res/drawable/ic_arrow_back.xml create mode 100644 canvas/src/main/res/drawable/ic_aspect_ratio.xml create mode 100644 canvas/src/main/res/drawable/ic_blur_on.xml create mode 100644 canvas/src/main/res/drawable/ic_check.xml create mode 100644 canvas/src/main/res/drawable/ic_clear.xml create mode 100644 canvas/src/main/res/drawable/ic_crop.xml create mode 100644 canvas/src/main/res/drawable/ic_crop_16_9.xml create mode 100644 canvas/src/main/res/drawable/ic_crop_3_2.xml create mode 100644 canvas/src/main/res/drawable/ic_crop_5_4.xml create mode 100644 canvas/src/main/res/drawable/ic_crop_square.xml create mode 100644 canvas/src/main/res/drawable/ic_eraser.xml create mode 100644 canvas/src/main/res/drawable/ic_eyedropper.xml create mode 100644 canvas/src/main/res/drawable/ic_insert_photo.xml create mode 100644 canvas/src/main/res/drawable/ic_line_weight.xml create mode 100644 canvas/src/main/res/drawable/ic_more_vert.xml create mode 100644 canvas/src/main/res/drawable/ic_pan_tool.xml create mode 100644 canvas/src/main/res/drawable/ic_redo.xml create mode 100644 canvas/src/main/res/drawable/ic_rotate_90_degrees_ccw.xml create mode 100644 canvas/src/main/res/drawable/ic_rotate_left.xml create mode 100644 canvas/src/main/res/drawable/ic_rotate_right.xml create mode 100644 canvas/src/main/res/drawable/ic_save.xml create mode 100644 canvas/src/main/res/drawable/ic_share.xml create mode 100644 canvas/src/main/res/drawable/ic_undo.xml create mode 100644 canvas/src/main/res/drawable/ic_zoom_in.xml diff --git a/canvas/build.gradle.kts b/canvas/build.gradle.kts index ed6d0af..9aff29c 100644 --- a/canvas/build.gradle.kts +++ b/canvas/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.jetbrainsKotlinAndroid) + id("org.jetbrains.kotlin.plugin.serialization") version ("1.8.21") + id("kotlin-kapt") } android { @@ -14,6 +16,13 @@ android { consumerProguardFiles("consumer-rules.pro") } + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.10" + } buildTypes { release { isMinifyEnabled = false @@ -37,6 +46,33 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.android.material) + implementation(libs.androidx.ui.android) + implementation(project(":filepicker")) + + val compose_version = "1.5.4" + implementation("androidx.compose.ui:ui:$compose_version") + implementation("androidx.compose.material:material:$compose_version") + implementation("androidx.compose.ui:ui-tooling-preview:$compose_version") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") + implementation("androidx.activity:activity-compose:1.3.1") + implementation("com.jakewharton.timber:timber:5.0.1") + + + implementation("com.godaddy.android.colorpicker:compose-color-picker:0.7.0") + implementation("androidx.navigation:navigation-compose:2.5.2") + implementation("io.github.hokofly:hoko-blur:1.5.3") + + implementation("com.github.bumptech.glide:glide:4.16.0") + kapt("com.github.bumptech.glide:compiler:4.16.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0") + + implementation("androidx.preference:preference-ktx:1.2.0") + + implementation("androidx.preference:preference:1.2.0'") + implementation("com.google.dagger:hilt-android:2.48") + kapt("com.google.dagger:hilt-compiler:2.48") + kapt("androidx.hilt:hilt-compiler:1.0.0") + testImplementation(libs.junit) androidTestImplementation(libs.androidx.test.junit) androidTestImplementation(libs.androidx.test.espresso) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt index 87b53fa..416821d 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt @@ -5,12 +5,29 @@ import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import dev.arkbuilders.canvas.R +import dev.arkbuilders.canvas.presentation.data.Preferences +import dev.arkbuilders.canvas.presentation.data.Resolution +import dev.arkbuilders.canvas.presentation.edit.EditViewModel private const val imagePath = "image_path_param" class ArkCanvasFragment : Fragment() { private var imagePathParam: String? = null + + private val prefs = Preferences(appCtx = requireActivity().applicationContext) + + val viewModel = EditViewModel( + primaryColor = 0, + launchedFromIntent = false, + imagePath = null, + imageUri = null, + maxResolution = Resolution(720, 350), + prefs = prefs, + ) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { @@ -25,6 +42,22 @@ class ArkCanvasFragment : Fragment() { return inflater.inflate(R.layout.fragment_ark_canvas, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + super.onViewCreated(view, savedInstanceState) + val composeView = view.findViewById(R.id.compose_view) + + composeView.apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + setContent { + // Set Content here + + } + } + } + companion object { @JvmStatic fun newInstance(param1: String) = diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/data/ImageDefaults.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/data/ImageDefaults.kt new file mode 100644 index 0000000..351ff42 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/data/ImageDefaults.kt @@ -0,0 +1,23 @@ +package dev.arkbuilders.canvas.presentation.data + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.IntSize +import kotlinx.serialization.Serializable + +@Serializable +data class ImageDefaults( + val colorValue: ULong = Color.White.value, + val resolution: Resolution? = null +) + +@Serializable +data class Resolution( + val width: Int, + val height: Int +) { + fun toIntSize() = IntSize(this.width, this.height) + + companion object { + fun fromIntSize(intSize: IntSize) = Resolution(intSize.width, intSize.height) + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/data/Preferences.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/data/Preferences.kt new file mode 100644 index 0000000..9fbf674 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/data/Preferences.kt @@ -0,0 +1,92 @@ +package dev.arkbuilders.canvas.presentation.data + +import android.content.Context +import androidx.compose.ui.graphics.Color +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.IOException +import java.nio.file.Files +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.io.path.exists +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.text.Charsets.UTF_8 + +@Singleton +class Preferences @Inject constructor(private val appCtx: Context) { + + suspend fun persistUsedColors( + colors: List + ) = withContext(Dispatchers.IO) { + try { + val colorsStorage = appCtx.filesDir.resolve(COLORS_STORAGE) + .toPath() + val lines = colors.map { it.value.toString() } + Files.write(colorsStorage, lines, UTF_8) + } catch (e: IOException) { + e.printStackTrace() + } + } + + suspend fun readUsedColors(): List { + val colors = mutableListOf() + withContext(Dispatchers.IO) { + + try { + val colorsStorage = appCtx + .filesDir + .resolve(COLORS_STORAGE) + .toPath() + + if (colorsStorage.exists()) { + Files.readAllLines(colorsStorage, UTF_8).forEach { line -> + val color = Color(line.toULong()) + colors.add(color) + } + } + } catch (e: IOException) { + e.printStackTrace() + } + } + return colors + } + + suspend fun persistDefaults(color: Color, resolution: Resolution) { + withContext(Dispatchers.IO) { + val defaultsStorage = appCtx.filesDir.resolve(DEFAULTS_STORAGE) + .toPath() + val defaults = ImageDefaults( + color.value, + resolution + ) + val jsonString = Json.encodeToString(defaults) + defaultsStorage.writeText(jsonString, UTF_8) + } + } + + suspend fun readDefaults(): ImageDefaults { + var defaults = ImageDefaults() + try { + withContext(Dispatchers.IO) { + val defaultsStorage = appCtx.filesDir.resolve(DEFAULTS_STORAGE) + .toPath() + if (defaultsStorage.exists()) { + val jsonString = defaultsStorage.readText(UTF_8) + defaults = Json.decodeFromString(jsonString) + } + } + } catch (e: IOException) { + e.printStackTrace() + } + return defaults + } + + companion object { + private const val COLORS_STORAGE = "colors" + private const val DEFAULTS_STORAGE = "defaults" + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt new file mode 100644 index 0000000..d8ae454 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt @@ -0,0 +1,335 @@ +package dev.arkbuilders.canvas.presentation.drawing + +import android.graphics.Matrix +import android.graphics.PointF +import android.view.MotionEvent +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.calculatePan +import androidx.compose.foundation.gestures.calculateZoom +import androidx.compose.foundation.gestures.forEachGesture +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.ClipOp +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.toSize +import dev.arkbuilders.canvas.presentation.edit.EditViewModel +import dev.arkbuilders.canvas.presentation.edit.TransparencyChessBoardCanvas +import dev.arkbuilders.canvas.presentation.edit.crop.CropWindow.Companion.computeDeltaX +import dev.arkbuilders.canvas.presentation.edit.crop.CropWindow.Companion.computeDeltaY +import dev.arkbuilders.canvas.presentation.picker.toDp +import dev.arkbuilders.canvas.presentation.utils.calculateRotationFromOneFingerGesture + +@Composable +fun EditCanvas(viewModel: EditViewModel) { + val editManager = viewModel.editManager + var scale by remember { mutableStateOf(1f) } + var offset by remember { mutableStateOf(Offset.Zero) } + + fun resetScaleAndTranslate() { + editManager.apply { + if ( + isRotateMode.value || isCropMode.value || isResizeMode.value || + isBlurMode.value + ) { + scale = 1f; zoomScale = scale; offset = Offset.Zero + } + } + } + + Box(contentAlignment = Alignment.Center) { + val modifier = Modifier.size( + editManager.availableDrawAreaSize.value.width.toDp(), + editManager.availableDrawAreaSize.value.height.toDp() + ).graphicsLayer { + resetScaleAndTranslate() + + // Eraser leaves black line instead of erasing without this hack, it uses BlendMode.SrcOut + // https://stackoverflow.com/questions/65653560/jetpack-compose-applying-porterduffmode-to-image + // Provide a slight opacity to for compositing into an + // offscreen buffer to ensure blend modes are applied to empty pixel information + // By default any alpha != 1.0f will use a compositing layer by default + alpha = 0.99f + + scaleX = scale + scaleY = scale + translationX = offset.x + translationY = offset.y + } + TransparencyChessBoardCanvas(modifier, editManager) + BackgroundCanvas(modifier, editManager) + DrawCanvas(modifier, viewModel) + } + if ( + editManager.isRotateMode.value || editManager.isZoomMode.value || + editManager.isPanMode.value + ) { + Canvas( + Modifier.fillMaxSize() + .pointerInput(Any()) { + forEachGesture { + awaitPointerEventScope { + awaitFirstDown() + do { + val event = awaitPointerEvent() + when (true) { + (editManager.isRotateMode.value) -> { + val angle = event + .calculateRotationFromOneFingerGesture( + editManager.calcCenter() + ) + editManager.rotate(angle) + editManager.invalidatorTick.value++ + } + else -> { + if (editManager.isZoomMode.value) { + scale *= event.calculateZoom() + editManager.zoomScale = scale + } + if (editManager.isPanMode.value) { + val pan = event.calculatePan() + offset = Offset( + offset.x + pan.x, + offset.y + pan.y + ) + } + } + } + } while (event.changes.any { it.pressed }) + } + } + } + ) {} + } +} + +@Composable +fun BackgroundCanvas(modifier: Modifier, editManager: EditManager) { + Canvas(modifier) { + editManager.apply { + invalidatorTick.value + var matrix = matrix + if ( + isCropMode.value || isRotateMode.value || + isResizeMode.value || isBlurMode.value + ) + matrix = editMatrix + drawIntoCanvas { canvas -> + backgroundImage.value?.let { + canvas.nativeCanvas.drawBitmap( + it.asAndroidBitmap(), + matrix, + null + ) + } ?: run { + val rect = Rect( + Offset.Zero, + imageSize.toSize() + ) + canvas.nativeCanvas.setMatrix(matrix) + canvas.drawRect(rect, backgroundPaint) + canvas.clipRect(rect, ClipOp.Intersect) + } + } + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun DrawCanvas(modifier: Modifier, viewModel: EditViewModel) { + val context = LocalContext.current + val editManager = viewModel.editManager + var path = Path() + val currentPoint = PointF(0f, 0f) + val drawModifier = if (editManager.isCropMode.value) Modifier.fillMaxSize() + else modifier + + fun handleDrawEvent(action: Int, eventX: Float, eventY: Float) { + when (action) { + MotionEvent.ACTION_DOWN -> { + path.reset() + path.moveTo(eventX, eventY) + currentPoint.x = eventX + currentPoint.y = eventY + editManager.apply { + drawOperation.draw(path) + applyOperation() + } + } + MotionEvent.ACTION_MOVE -> { + path.quadraticBezierTo( + currentPoint.x, + currentPoint.y, + (eventX + currentPoint.x) / 2, + (eventY + currentPoint.y) / 2 + ) + currentPoint.x = eventX + currentPoint.y = eventY + } + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + // draw a dot + if (eventX == currentPoint.x && + eventY == currentPoint.y + ) { + path.lineTo(currentPoint.x, currentPoint.y) + } + + editManager.clearRedoPath() + editManager.updateRevised() + path = Path() + } + else -> {} + } + } + + fun handleCropEvent(action: Int, eventX: Float, eventY: Float) { + when (action) { + MotionEvent.ACTION_DOWN -> { + currentPoint.x = eventX + currentPoint.y = eventY + editManager.cropWindow.detectTouchedSide( + Offset(eventX, eventY) + ) + } + MotionEvent.ACTION_MOVE -> { + val deltaX = + computeDeltaX(currentPoint.x, eventX) + val deltaY = + computeDeltaY(currentPoint.y, eventY) + + editManager.cropWindow.setDelta( + Offset( + deltaX, + deltaY + ) + ) + currentPoint.x = eventX + currentPoint.y = eventY + } + } + } + + fun handleEyeDropEvent(action: Int, eventX: Float, eventY: Float) { + viewModel.applyEyeDropper(action, eventX.toInt(), eventY.toInt()) + } + + fun handleBlurEvent(action: Int, eventX: Float, eventY: Float) { + when (action) { + MotionEvent.ACTION_DOWN -> { + currentPoint.x = eventX + currentPoint.y = eventY + } + MotionEvent.ACTION_MOVE -> { + val position = Offset( + currentPoint.x, + currentPoint.y + ) + val delta = Offset( + computeDeltaX(currentPoint.x, eventX), + computeDeltaY(currentPoint.y, eventY) + ) + editManager.blurOperation.move(position, delta) + currentPoint.x = eventX + currentPoint.y = eventY + } + else -> {} + } + } + + Canvas( + modifier = drawModifier.pointerInteropFilter { event -> + val eventX = event.x + val eventY = event.y + val tmpMatrix = Matrix() + editManager.matrix.invert(tmpMatrix) + val mappedXY = floatArrayOf( + event.x / editManager.zoomScale, + event.y / editManager.zoomScale + ) + tmpMatrix.mapPoints(mappedXY) + val mappedX = mappedXY[0] + val mappedY = mappedXY[1] + + when (true) { + editManager.isResizeMode.value -> {} + editManager.isBlurMode.value -> handleBlurEvent( + event.action, + eventX, + eventY + ) + + editManager.isCropMode.value -> handleCropEvent( + event.action, + eventX, + eventY + ) + + editManager.isEyeDropperMode.value -> handleEyeDropEvent( + event.action, + event.x, + event.y + ) + + else -> handleDrawEvent(event.action, mappedX, mappedY) + } + editManager.invalidatorTick.value++ + true + } + ) { + // force recomposition on invalidatorTick change + editManager.invalidatorTick.value + drawIntoCanvas { canvas -> + editManager.apply { + var matrix = this.matrix + if (isRotateMode.value || isResizeMode.value || isBlurMode.value) + matrix = editMatrix + if (isCropMode.value) matrix = Matrix() + canvas.nativeCanvas.setMatrix(matrix) + if (isResizeMode.value) return@drawIntoCanvas + if (isBlurMode.value) { + editManager.blurOperation.draw(context, canvas) + return@drawIntoCanvas + } + if (isCropMode.value) { + editManager.cropWindow.show(canvas) + return@drawIntoCanvas + } + val rect = Rect( + Offset.Zero, + imageSize.toSize() + ) + canvas.drawRect( + rect, + Paint().also { it.color = Color.Transparent } + ) + canvas.clipRect(rect, ClipOp.Intersect) + if (drawPaths.isNotEmpty()) { + drawPaths.forEach { + canvas.drawPath(it.path, it.paint) + } + } + } + } + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt new file mode 100644 index 0000000..f991865 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt @@ -0,0 +1,716 @@ +package dev.arkbuilders.canvas.presentation.drawing + +import android.graphics.Bitmap +import android.graphics.Matrix +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.PaintingStyle +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.unit.IntSize +import dev.arkbuilders.canvas.presentation.data.ImageDefaults +import dev.arkbuilders.canvas.presentation.data.Resolution +import dev.arkbuilders.canvas.presentation.edit.Operation +import dev.arkbuilders.canvas.presentation.edit.blur.BlurOperation +import dev.arkbuilders.canvas.presentation.edit.crop.CropOperation +import dev.arkbuilders.canvas.presentation.edit.crop.CropWindow +import dev.arkbuilders.canvas.presentation.edit.draw.DrawOperation +import dev.arkbuilders.canvas.presentation.edit.resize.ResizeOperation +import dev.arkbuilders.canvas.presentation.edit.rotate.RotateOperation +import java.util.Stack + +class EditManager { + private val drawPaint: MutableState = mutableStateOf(defaultPaint()) + + private val _paintColor: MutableState = + mutableStateOf(drawPaint.value.color) + val paintColor: State = _paintColor + private val _backgroundColor = mutableStateOf(Color.Transparent) + val backgroundColor: State = _backgroundColor + + private val erasePaint: Paint = Paint().apply { + shader = null + color = backgroundColor.value + style = PaintingStyle.Stroke + blendMode = BlendMode.SrcOut + } + + val backgroundPaint: Paint + get() { + return Paint().apply { + color = backgroundImage.value?.let { + Color.Transparent + } ?: backgroundColor.value + } + } + + val blurIntensity = mutableStateOf(12f) + + val cropWindow = CropWindow(this) + + val drawOperation = DrawOperation(this) + val resizeOperation = ResizeOperation(this) + val rotateOperation = RotateOperation(this) + val cropOperation = CropOperation(this) + val blurOperation = BlurOperation(this) + + private val currentPaint: Paint + get() = when (true) { + isEraseMode.value -> erasePaint + else -> drawPaint.value + } + + val drawPaths = Stack() + + val redoPaths = Stack() + + val backgroundImage = mutableStateOf(null) + val backgroundImage2 = mutableStateOf(null) + private val originalBackgroundImage = mutableStateOf(null) + + val matrix = Matrix() + val editMatrix = Matrix() + val backgroundMatrix = Matrix() + val rectMatrix = Matrix() + + private val matrixScale = mutableStateOf(1f) + var zoomScale = 1f + lateinit var bitmapScale: ResizeOperation.Scale + private set + + val imageSize: IntSize + get() { + return if (isResizeMode.value) + backgroundImage2.value?.let { + IntSize(it.width, it.height) + } ?: originalBackgroundImage.value?.let { + IntSize(it.width, it.height) + } ?: resolution.value?.toIntSize()!! + else + backgroundImage.value?.let { + IntSize(it.width, it.height) + } ?: resolution.value?.toIntSize() ?: drawAreaSize.value + } + + private val _resolution = mutableStateOf(null) + val resolution: State = _resolution + var drawAreaSize = mutableStateOf(IntSize.Zero) + val availableDrawAreaSize = mutableStateOf(IntSize.Zero) + + var invalidatorTick = mutableStateOf(0) + + private val _isEraseMode: MutableState = mutableStateOf(false) + val isEraseMode: State = _isEraseMode + + private val _canUndo: MutableState = mutableStateOf(false) + val canUndo: State = _canUndo + + private val _canRedo: MutableState = mutableStateOf(false) + val canRedo: State = _canRedo + + private val _isRotateMode = mutableStateOf(false) + val isRotateMode: State = _isRotateMode + + private val _isResizeMode = mutableStateOf(false) + val isResizeMode: State = _isResizeMode + + private val _isEyeDropperMode = mutableStateOf(false) + val isEyeDropperMode: State = _isEyeDropperMode + + private val _isBlurMode = mutableStateOf(false) + val isBlurMode: State = _isBlurMode + + private val _isZoomMode = mutableStateOf(false) + val isZoomMode: State = _isZoomMode + private val _isPanMode = mutableStateOf(false) + val isPanMode: State = _isPanMode + + val rotationAngle = mutableStateOf(0F) + var prevRotationAngle = 0f + + private val editedPaths = Stack>() + + val redoResize = Stack() + val resizes = Stack() + val rotationAngles = Stack() + val redoRotationAngles = Stack() + + private val undoStack = Stack() + private val redoStack = Stack() + + private val _isCropMode = mutableStateOf(false) + val isCropMode = _isCropMode + + val cropStack = Stack() + val redoCropStack = Stack() + + fun applyOperation() { + val operation: Operation = + when (true) { + isRotateMode.value -> rotateOperation + isCropMode.value -> cropOperation + isBlurMode.value -> blurOperation + isResizeMode.value -> resizeOperation + else -> drawOperation + } + operation.apply() + } + + private fun undoOperation(operation: Operation) { + operation.undo() + } + + private fun redoOperation(operation: Operation) { + operation.redo() + } + + fun scaleToFit() { + val viewParams = backgroundImage.value?.let { + fitImage( + it, + drawAreaSize.value.width, + drawAreaSize.value.height + ) + } ?: run { + fitBackground( + imageSize, + drawAreaSize.value.width, + drawAreaSize.value.height + ) + } + matrixScale.value = viewParams.scale.x + scaleMatrix(viewParams) + updateAvailableDrawArea(viewParams.drawArea) + val bitmapXScale = + imageSize.width.toFloat() / viewParams.drawArea.width.toFloat() + val bitmapYScale = + imageSize.height.toFloat() / viewParams.drawArea.height.toFloat() + bitmapScale = ResizeOperation.Scale( + bitmapXScale, + bitmapYScale + ) + } + + fun scaleToFitOnEdit( + maxWidth: Int = drawAreaSize.value.width, + maxHeight: Int = drawAreaSize.value.height + ): ImageViewParams { + val viewParams = backgroundImage.value?.let { + fitImage(it, maxWidth, maxHeight) + } ?: run { + fitBackground( + imageSize, + maxWidth, + maxHeight + ) + } + scaleEditMatrix(viewParams) + updateAvailableDrawArea(viewParams.drawArea) + return viewParams + } + + private fun scaleMatrix(viewParams: ImageViewParams) { + matrix.setScale(viewParams.scale.x, viewParams.scale.y) + backgroundMatrix.setScale(viewParams.scale.x, viewParams.scale.y) + if (prevRotationAngle != 0f) { + val centerX = viewParams.drawArea.width / 2f + val centerY = viewParams.drawArea.height / 2f + matrix.postRotate(prevRotationAngle, centerX, centerY) + } + } + + private fun scaleEditMatrix(viewParams: ImageViewParams) { + editMatrix.setScale(viewParams.scale.x, viewParams.scale.y) + backgroundMatrix.setScale(viewParams.scale.x, viewParams.scale.y) + if (prevRotationAngle != 0f && isRotateMode.value) { + val centerX = viewParams.drawArea.width / 2f + val centerY = viewParams.drawArea.height / 2f + editMatrix.postRotate(prevRotationAngle, centerX, centerY) + } + } + + fun setBackgroundColor(color: Color) { + _backgroundColor.value = color + } + + fun setImageResolution(value: Resolution) { + _resolution.value = value + } + + fun initDefaults(defaults: ImageDefaults, maxResolution: Resolution) { + defaults.resolution?.let { + _resolution.value = it + } + if (resolution.value == null) + _resolution.value = maxResolution + _backgroundColor.value = Color(defaults.colorValue) + } + + fun updateAvailableDrawAreaByMatrix() { + val drawArea = backgroundImage.value?.let { + val drawWidth = it.width * matrixScale.value + val drawHeight = it.height * matrixScale.value + IntSize( + drawWidth.toInt(), + drawHeight.toInt() + ) + } ?: run { + val drawWidth = resolution.value?.width!! * matrixScale.value + val drawHeight = resolution.value?.height!! * matrixScale.value + IntSize( + drawWidth.toInt(), + drawHeight.toInt() + ) + } + updateAvailableDrawArea(drawArea) + } + fun updateAvailableDrawArea(bitmap: ImageBitmap? = backgroundImage.value) { + if (bitmap == null) { + resolution.value?.let { + availableDrawAreaSize.value = it.toIntSize() + } + return + } + availableDrawAreaSize.value = IntSize( + bitmap.width, + bitmap.height + ) + } + fun updateAvailableDrawArea(area: IntSize) { + availableDrawAreaSize.value = area + } + + internal fun clearRedoPath() { + redoPaths.clear() + } + + fun toggleEyeDropper() { + _isEyeDropperMode.value = !isEyeDropperMode.value + } + + fun updateRevised() { + _canUndo.value = undoStack.isNotEmpty() + _canRedo.value = redoStack.isNotEmpty() + } + + fun resizeDown(width: Int = 0, height: Int = 0) = + resizeOperation.resizeDown(width, height) { + backgroundImage.value = it + } + + fun rotate(angle: Float) { + val centerX = availableDrawAreaSize.value.width / 2 + val centerY = availableDrawAreaSize.value.height / 2 + if (isRotateMode.value) { + rotationAngle.value += angle + rotateOperation.rotate( + editMatrix, + angle, + centerX.toFloat(), + centerY.toFloat() + ) + return + } + rotateOperation.rotate( + matrix, + angle, + centerX.toFloat(), + centerY.toFloat() + ) + } + + fun addRotation() { + if (canRedo.value) clearRedo() + rotationAngles.add(prevRotationAngle) + undoStack.add(ROTATE) + prevRotationAngle = rotationAngle.value + updateRevised() + } + + private fun addAngle() { + rotationAngles.add(prevRotationAngle) + } + + fun addResize() { + if (canRedo.value) clearRedo() + resizes.add(backgroundImage2.value) + undoStack.add(RESIZE) + keepEditedPaths() + updateRevised() + } + + fun keepEditedPaths() { + val stack = Stack() + if (drawPaths.isNotEmpty()) { + val size = drawPaths.size + for (i in 1..size) { + stack.push(drawPaths.pop()) + } + } + editedPaths.add(stack) + } + + fun redrawEditedPaths() { + if (editedPaths.isNotEmpty()) { + val paths = editedPaths.pop() + if (paths.isNotEmpty()) { + val size = paths.size + for (i in 1..size) { + drawPaths.push(paths.pop()) + } + } + } + } + + fun addCrop() { + if (canRedo.value) clearRedo() + cropStack.add(backgroundImage2.value) + undoStack.add(CROP) + updateRevised() + } + + fun addBlur() { + if (canRedo.value) clearRedo() + undoStack.add(BLUR) + updateRevised() + } + + private fun operationByTask(task: String) = when (task) { + ROTATE -> rotateOperation + RESIZE -> resizeOperation + CROP -> cropOperation + BLUR -> blurOperation + else -> drawOperation + } + + fun undo() { + if (canUndo.value) { + val undoTask = undoStack.pop() + redoStack.push(undoTask) + undoOperation(operationByTask(undoTask)) + } + invalidatorTick.value++ + updateRevised() + } + + fun redo() { + if (canRedo.value) { + val redoTask = redoStack.pop() + undoStack.push(redoTask) + redoOperation(operationByTask(redoTask)) + invalidatorTick.value++ + updateRevised() + } + } + + fun saveRotationAfterOtherOperation() { + addAngle() + resetRotation() + } + + fun restoreRotationAfterUndoOtherOperation() { + if (rotationAngles.isNotEmpty()) { + prevRotationAngle = rotationAngles.pop() + rotationAngle.value = prevRotationAngle + } + } + + fun addDrawPath(path: Path) { + drawPaths.add( + DrawPath( + path, + currentPaint.copy().apply { + strokeWidth = drawPaint.value.strokeWidth + } + ) + ) + if (canRedo.value) clearRedo() + undoStack.add(DRAW) + } + + fun setPaintColor(color: Color) { + drawPaint.value.color = color + _paintColor.value = color + } + + private fun clearPaths() { + drawPaths.clear() + redoPaths.clear() + invalidatorTick.value++ + updateRevised() + } + + private fun clearResizes() { + resizes.clear() + redoResize.clear() + updateRevised() + } + + private fun resetRotation() { + rotationAngle.value = 0f + prevRotationAngle = 0f + } + + private fun clearRotations() { + rotationAngles.clear() + redoRotationAngles.clear() + resetRotation() + } + + fun clearEdits() { + clearPaths() + clearResizes() + clearRotations() + clearCrop() + blurOperation.clear() + undoStack.clear() + redoStack.clear() + restoreOriginalBackgroundImage() + scaleToFit() + updateRevised() + } + + private fun clearRedo() { + redoPaths.clear() + redoCropStack.clear() + redoRotationAngles.clear() + redoResize.clear() + redoStack.clear() + updateRevised() + } + + private fun clearCrop() { + cropStack.clear() + redoCropStack.clear() + updateRevised() + } + + fun setBackgroundImage2() { + backgroundImage2.value = backgroundImage.value + } + + fun redrawBackgroundImage2() { + backgroundImage.value = backgroundImage2.value + } + + fun setOriginalBackgroundImage(imgBitmap: ImageBitmap?) { + originalBackgroundImage.value = imgBitmap + } + + private fun restoreOriginalBackgroundImage() { + backgroundImage.value = originalBackgroundImage.value + updateAvailableDrawArea() + } + + fun toggleEraseMode() { + _isEraseMode.value = !isEraseMode.value + } + + fun toggleRotateMode() { + _isRotateMode.value = !isRotateMode.value + if (isRotateMode.value) editMatrix.set(matrix) + } + + fun toggleCropMode() { + _isCropMode.value = !isCropMode.value + if (!isCropMode.value) cropWindow.close() + } + + fun toggleZoomMode() { + _isZoomMode.value = !isZoomMode.value + } + + fun togglePanMode() { + _isPanMode.value = !isPanMode.value + } + + fun cancelCropMode() { + backgroundImage.value = backgroundImage2.value + editMatrix.reset() + } + + fun cancelRotateMode() { + rotationAngle.value = prevRotationAngle + editMatrix.reset() + } + + fun toggleResizeMode() { + _isResizeMode.value = !isResizeMode.value + } + + fun cancelResizeMode() { + backgroundImage.value = backgroundImage2.value + editMatrix.reset() + } + + fun toggleBlurMode() { + _isBlurMode.value = !isBlurMode.value + } + fun setPaintStrokeWidth(strokeWidth: Float) { + drawPaint.value.strokeWidth = strokeWidth + } + + fun calcImageOffset(): Offset { + val drawArea = drawAreaSize.value + val allowedArea = availableDrawAreaSize.value + val xOffset = ((drawArea.width - allowedArea.width) / 2f) + .coerceAtLeast(0f) + val yOffset = ((drawArea.height - allowedArea.height) / 2f) + .coerceAtLeast(0f) + return Offset(xOffset, yOffset) + } + + fun calcCenter() = Offset( + availableDrawAreaSize.value.width / 2f, + availableDrawAreaSize.value.height / 2f + ) + + + fun resize( + imageBitmap: ImageBitmap, + maxWidth: Int, + maxHeight: Int + ): ImageBitmap { + val bitmap = imageBitmap.asAndroidBitmap() + val width = bitmap.width + val height = bitmap.height + + val bitmapRatio = width.toFloat() / height.toFloat() + val maxRatio = maxWidth.toFloat() / maxHeight.toFloat() + + var finalWidth = maxWidth + var finalHeight = maxHeight + + if (maxRatio > bitmapRatio) { + finalWidth = (maxHeight.toFloat() * bitmapRatio).toInt() + } else { + finalHeight = (maxWidth.toFloat() / bitmapRatio).toInt() + } + return Bitmap + .createScaledBitmap(bitmap, finalWidth, finalHeight, true) + .asImageBitmap() + } + + fun fitImage( + imageBitmap: ImageBitmap, + maxWidth: Int, + maxHeight: Int + ): ImageViewParams { + val bitmap = imageBitmap.asAndroidBitmap() + val width = bitmap.width + val height = bitmap.height + + val bitmapRatio = width.toFloat() / height.toFloat() + val maxRatio = maxWidth.toFloat() / maxHeight.toFloat() + + var finalWidth = maxWidth + var finalHeight = maxHeight + + if (maxRatio > bitmapRatio) { + finalWidth = (maxHeight.toFloat() * bitmapRatio).toInt() + } else { + finalHeight = (maxWidth.toFloat() / bitmapRatio).toInt() + } + return ImageViewParams( + IntSize( + finalWidth, + finalHeight, + ), + ResizeOperation.Scale( + finalWidth.toFloat() / width.toFloat(), + finalHeight.toFloat() / height.toFloat() + ) + ) + } + + fun fitBackground( + resolution: IntSize, + maxWidth: Int, + maxHeight: Int + ): ImageViewParams { + + val width = resolution.width + val height = resolution.height + + val resolutionRatio = width.toFloat() / height.toFloat() + val maxRatio = maxWidth.toFloat() / maxHeight.toFloat() + + var finalWidth = maxWidth + var finalHeight = maxHeight + + if (maxRatio > resolutionRatio) { + finalWidth = (maxHeight.toFloat() * resolutionRatio).toInt() + } else { + finalHeight = (maxWidth.toFloat() / resolutionRatio).toInt() + } + return ImageViewParams( + IntSize( + finalWidth, + finalHeight, + ), + ResizeOperation.Scale( + finalWidth.toFloat() / width.toFloat(), + finalHeight.toFloat() / height.toFloat() + ) + ) + } + class ImageViewParams( + val drawArea: IntSize, + val scale: ResizeOperation.Scale + ) + + private companion object { + private const val DRAW = "draw" + private const val CROP = "crop" + private const val RESIZE = "resize" + private const val ROTATE = "rotate" + private const val BLUR = "blur" + } +} + +class DrawPath( + val path: Path, + val paint: Paint +) + +fun Paint.copy(): Paint { + val from = this + return Paint().apply { + alpha = from.alpha + isAntiAlias = from.isAntiAlias + color = from.color + blendMode = from.blendMode + style = from.style + strokeWidth = from.strokeWidth + strokeCap = from.strokeCap + strokeJoin = from.strokeJoin + strokeMiterLimit = from.strokeMiterLimit + filterQuality = from.filterQuality + shader = from.shader + colorFilter = from.colorFilter + pathEffect = from.pathEffect + asFrameworkPaint().apply { + maskFilter = from.asFrameworkPaint().maskFilter + } + } +} + +fun defaultPaint(): Paint { + return Paint().apply { + color = Color.White + strokeWidth = 14f + isAntiAlias = true + style = PaintingStyle.Stroke + strokeJoin = StrokeJoin.Round + strokeCap = StrokeCap.Round + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/ColorPickerDialog.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/ColorPickerDialog.kt new file mode 100644 index 0000000..8f8650a --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/ColorPickerDialog.kt @@ -0,0 +1,254 @@ +package dev.arkbuilders.canvas.presentation.edit + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowLeft +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import com.godaddy.android.colorpicker.ClassicColorPicker +import com.godaddy.android.colorpicker.HsvColor +import dev.arkbuilders.canvas.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun ColorPickerDialog( + isVisible: MutableState, + initialColor: Color, + usedColors: List = listOf(), + enableEyeDropper: Boolean, + onToggleEyeDropper: () -> Unit, + onColorChanged: (Color) -> Unit, +) { + if (!isVisible.value) return + + var currentColor by remember { + mutableStateOf(HsvColor.from(initialColor)) + } + + val finish = { + onColorChanged(currentColor.toColor()) + isVisible.value = false + } + + Dialog( + onDismissRequest = { + isVisible.value = false + } + ) { + Column( + Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(Color.White, RoundedCornerShape(5)) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (usedColors.isNotEmpty()) { + Box( + Modifier + .fillMaxWidth() + ) { + val state = rememberLazyListState() + + LazyRow( + Modifier + .align(Alignment.Center), + state = state + ) { + items(usedColors) { color -> + Box( + Modifier + .padding( + start = 5.dp, + end = 5.dp, + top = 12.dp, + bottom = 12.dp + ) + .size(25.dp) + .clip(CircleShape) + .background(color) + .clickable { + currentColor = HsvColor.from(color) + finish() + } + ) + } + } + LaunchedEffect(state) { + scrollToEnd(state, this) + } + UsedColorsFlowHint( + { enableScroll(state) }, + { checkScroll(state).first }, + { checkScroll(state).second } + ) + } + } + ClassicColorPicker( + modifier = Modifier + .fillMaxWidth() + .height(250.dp), + color = currentColor.toColor(), + onColorChanged = { + currentColor = it + } + ) + if (enableEyeDropper) { + Box(Modifier.padding(8.dp)) { + Box( + Modifier + .size(50.dp) + .clip(CircleShape) + .clickable { + onToggleEyeDropper() + isVisible.value = false + }, + contentAlignment = Alignment.Center + ) { + Icon( + ImageVector.vectorResource(R.drawable.ic_eyedropper), + "", + Modifier.size(25.dp) + ) + } + } + } + TextButton( + modifier = Modifier + .padding(top = 8.dp) + .fillMaxWidth(), + onClick = finish + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .padding(12.dp) + .size(50.dp) + .border( + 2.dp, + Color.LightGray, + CircleShape + ) + .padding(6.dp) + .clip(CircleShape) + .background(color = currentColor.toColor()) + ) + Text(text = "Pick", fontSize = 18.sp) + } + } + } + } +} + +fun scrollToEnd(state: LazyListState, scope: CoroutineScope) { + scope.launch { + if (enableScroll(state)) { + val lastIndex = state.layoutInfo.totalItemsCount - 1 + state.scrollToItem(lastIndex, 0) + } + } +} + +fun enableScroll(state: LazyListState): Boolean { + return state.layoutInfo.totalItemsCount != + state.layoutInfo.visibleItemsInfo.size +} + +fun checkScroll(state: LazyListState): Pair { + var scrollIsAtStart = true + var scrollIsAtEnd = false + if (enableScroll(state)) { + val totalItems = state.layoutInfo.totalItemsCount + val visibleItems = state.layoutInfo.visibleItemsInfo.size + val itemSize = + state.layoutInfo.visibleItemsInfo.firstOrNull()?.size + ?: 0 + val rowSize = itemSize * totalItems + val visibleRowSize = itemSize * visibleItems + val scrollValue = state.firstVisibleItemIndex * itemSize + val maxScrollValue = rowSize - visibleRowSize + scrollIsAtStart = scrollValue == 0 + scrollIsAtEnd = scrollValue == maxScrollValue + } + return scrollIsAtStart to scrollIsAtEnd +} + +@Composable +fun BoxScope.UsedColorsFlowHint( + scrollIsEnabled: () -> Boolean, + scrollIsAtStart: () -> Boolean, + scrollIsAtEnd: () -> Boolean +) { + AnimatedVisibility( + visible = scrollIsEnabled() && ( + scrollIsAtEnd() || (!scrollIsAtStart() && !scrollIsAtEnd()) + ), + enter = fadeIn(tween(500)), + exit = fadeOut(tween(500)), + modifier = Modifier + .background(Color.White) + .align(Alignment.CenterStart) + ) { + Icon( + Icons.Filled.KeyboardArrowLeft, + contentDescription = null, + Modifier.size(32.dp) + ) + } + AnimatedVisibility( + visible = scrollIsEnabled() && ( + scrollIsAtStart() || (!scrollIsAtStart() && !scrollIsAtEnd()) + ), + enter = fadeIn(tween(500)), + exit = fadeOut(tween(500)), + modifier = Modifier + .background(Color.White) + .align(Alignment.CenterEnd) + ) { + Icon( + Icons.Filled.KeyboardArrowRight, + contentDescription = null, + Modifier.size(32.dp) + ) + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/ConfirmClearDialog.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/ConfirmClearDialog.kt new file mode 100644 index 0000000..57a843b --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/ConfirmClearDialog.kt @@ -0,0 +1,52 @@ +package dev.arkbuilders.canvas.presentation.edit + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun ConfirmClearDialog( + show: MutableState, + onConfirm: () -> Unit +) { + if (!show.value) return + + AlertDialog( + onDismissRequest = { + show.value = false + }, + title = { + Text( + modifier = Modifier.padding(top = 4.dp, bottom = 8.dp), + text = "Are you sure to clear all edits?", + fontSize = 16.sp + ) + }, + confirmButton = { + Button( + onClick = { + show.value = false + onConfirm() + } + ) { + Text("Clear") + } + }, + dismissButton = { + TextButton( + onClick = { + show.value = false + } + ) { + Text("Cancel") + } + } + ) +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt new file mode 100644 index 0000000..38884dd --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -0,0 +1,1070 @@ +@file:OptIn(ExperimentalComposeUiApi::class) + +package dev.arkbuilders.canvas.presentation.edit + +import android.os.Build +import android.view.MotionEvent +import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.annotation.RequiresApi +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.clickable +import androidx.compose.foundation.background +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.Slider +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowLeft +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.viewmodel.compose.viewModel +import dev.arkbuilders.canvas.R +import dev.arkbuilders.canvas.presentation.data.Resolution +import dev.arkbuilders.canvas.presentation.drawing.EditCanvas +import dev.arkbuilders.canvas.presentation.edit.blur.BlurIntensityPopup +import dev.arkbuilders.canvas.presentation.edit.crop.CropAspectRatiosMenu +import dev.arkbuilders.canvas.presentation.edit.resize.Hint +import dev.arkbuilders.canvas.presentation.edit.resize.ResizeInput +import dev.arkbuilders.canvas.presentation.edit.resize.delayHidingHint +import dev.arkbuilders.canvas.presentation.picker.toPx +import dev.arkbuilders.canvas.presentation.theme.Gray +import dev.arkbuilders.canvas.presentation.utils.askWritePermissions +import dev.arkbuilders.canvas.presentation.utils.getActivity +import dev.arkbuilders.canvas.presentation.utils.isWritePermGranted +import java.nio.file.Path + +private const val isChangedForMemoIntegration = true +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +@Composable +fun EditScreen( + imagePath: Path?, + imageUri: String?, + fragmentManager: FragmentManager, + navigateBack: () -> Unit, + launchedFromIntent: Boolean, + maxResolution: Resolution, + onSaveSvg: () -> Unit, + viewModel: EditViewModel, +) { + val primaryColor = MaterialTheme.colors.primary.value.toLong() + + val context = LocalContext.current + val showDefaultsDialog = remember { + mutableStateOf( + imagePath == null && imageUri == null && !viewModel.isLoaded + ) + } + + if (showDefaultsDialog.value) { + viewModel.editManager.apply { + resolution.value?.let { + NewImageOptionsDialog( + it, + maxResolution, + this.backgroundColor.value, + navigateBack, + this, + persistDefaults = { color, resolution -> + viewModel.persistDefaults(color, resolution) + }, + onConfirm = { + showDefaultsDialog.value = false + } + ) + } + } + } + ExitDialog( + viewModel = viewModel, + navigateBack = { + navigateBack() + viewModel.isLoaded = false + }, + launchedFromIntent = launchedFromIntent, + onSaveSvg = onSaveSvg + ) + + BackHandler { + val editManager = viewModel.editManager + if ( + editManager.isCropMode.value || editManager.isRotateMode.value || + editManager.isResizeMode.value || editManager.isEyeDropperMode.value || + editManager.isBlurMode.value + ) { + viewModel.cancelOperation() + return@BackHandler + } + if (editManager.isZoomMode.value) { + editManager.toggleZoomMode() + return@BackHandler + } + if (editManager.isPanMode.value) { + editManager.togglePanMode() + return@BackHandler + } + if (editManager.canUndo.value) { + editManager.undo() + return@BackHandler + } + if (viewModel.exitConfirmed) { + if (launchedFromIntent) + context.getActivity()?.finish() + else + navigateBack() + return@BackHandler + } + if (!viewModel.exitConfirmed) { + Toast.makeText(context, "Tap back again to exit", Toast.LENGTH_SHORT) + .show() + viewModel.confirmExit() + return@BackHandler + } + } + + HandleImageSavedEffect(viewModel, launchedFromIntent, navigateBack) + + if (!showDefaultsDialog.value) + DrawContainer( + viewModel + ) + + Menus( + imagePath, + fragmentManager, + viewModel, + launchedFromIntent, + navigateBack + ) + + if (viewModel.isSavingImage) { + SaveProgress() + } + + if (viewModel.showEyeDropperHint) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + Hint(stringResource(R.string.pick_color)) { + delayHidingHint(it) { + viewModel.showEyeDropperHint = false + } + viewModel.showEyeDropperHint + } + } + } +} + +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +@Composable +private fun Menus( + imagePath: Path?, + fragmentManager: FragmentManager, + viewModel: EditViewModel, + launchedFromIntent: Boolean, + navigateBack: () -> Unit, +) { + Box( + Modifier + .fillMaxSize() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + TopMenu( + imagePath, + fragmentManager, + viewModel, + launchedFromIntent, + navigateBack + ) + } + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .height(IntrinsicSize.Min), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (viewModel.editManager.isRotateMode.value) + Row { + Icon( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .clickable { + viewModel.editManager.apply { + rotate(-90f) + invalidatorTick.value++ + } + }, + imageVector = ImageVector + .vectorResource(R.drawable.ic_rotate_left), + tint = MaterialTheme.colors.primary, + contentDescription = null + ) + Icon( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .clickable { + viewModel.editManager.apply { + rotate(90f) + invalidatorTick.value++ + } + }, + imageVector = ImageVector + .vectorResource(R.drawable.ic_rotate_right), + tint = MaterialTheme.colors.primary, + contentDescription = null + ) + } + + EditMenuContainer(viewModel, navigateBack) + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun DrawContainer( + viewModel: EditViewModel +) { + Box( + modifier = Modifier + .padding(bottom = 32.dp) + .fillMaxSize() + .background( + if (viewModel.editManager.isCropMode.value) Color.White + else Color.Gray + ) + .pointerInteropFilter { event -> + if (event.action == MotionEvent.ACTION_DOWN) + viewModel.strokeSliderExpanded = false + false + } + .onSizeChanged { newSize -> + if (newSize == IntSize.Zero) return@onSizeChanged + if (viewModel.showSavePathDialog) return@onSizeChanged + viewModel.editManager.drawAreaSize.value = newSize + if (viewModel.isLoaded) { + viewModel.editManager.apply { + when (true) { + isCropMode.value -> { + cropWindow.updateOnDrawAreaSizeChange(newSize) + return@onSizeChanged + } + + isResizeMode.value -> { + if ( + backgroundImage.value?.width == + imageSize.width && + backgroundImage.value?.height == + imageSize.height + ) { + val editMatrixScale = scaleToFitOnEdit().scale + resizeOperation + .updateEditMatrixScale(editMatrixScale) + } + if ( + resizeOperation.isApplied() + ) { + resizeOperation.resetApply() + } + return@onSizeChanged + } + + isRotateMode.value -> { + scaleToFitOnEdit() + return@onSizeChanged + } + + isZoomMode.value -> { + return@onSizeChanged + } + + else -> { + scaleToFit() + return@onSizeChanged + } + } + } + } +// viewModel.loadImage() + }, + contentAlignment = Alignment.Center + ) { + EditCanvas(viewModel) + } +} + +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +@Composable +private fun BoxScope.TopMenu( + imagePath: Path?, + fragmentManager: FragmentManager, + viewModel: EditViewModel, + launchedFromIntent: Boolean, + navigateBack: () -> Unit +) { + val context = LocalContext.current + + if (viewModel.showSavePathDialog) + SavePathDialog( + initialImagePath = imagePath, + fragmentManager = fragmentManager, + onDismissClick = { viewModel.showSavePathDialog = false }, + onPositiveClick = { savePath -> + viewModel.saveImage(context, savePath) + } + ) + if (viewModel.showMoreOptionsPopup) + MoreOptionsPopup( + onDismissClick = { + viewModel.showMoreOptionsPopup = false + }, + onShareClick = { + viewModel.shareImage(context) + viewModel.showMoreOptionsPopup = false + }, + onSaveClick = { + if (!context.isWritePermGranted()) { + context.askWritePermissions() + return@MoreOptionsPopup + } + viewModel.showSavePathDialog = true + }, + onClearEdits = { + viewModel.showConfirmClearDialog.value = true + viewModel.showMoreOptionsPopup = false + } + ) + + ConfirmClearDialog( + viewModel.showConfirmClearDialog, + onConfirm = { + viewModel.editManager.apply { + if ( + !isRotateMode.value && + !isResizeMode.value && + !isCropMode.value && + !isEyeDropperMode.value + ) clearEdits() + } + } + ) + + if ( + !viewModel.menusVisible && + !viewModel.editManager.isRotateMode.value && + !viewModel.editManager.isResizeMode.value && + !viewModel.editManager.isCropMode.value && + !viewModel.editManager.isEyeDropperMode.value + ) + return + Icon( + modifier = Modifier + .align(Alignment.TopStart) + .padding(8.dp) + .size(36.dp) + .clip(CircleShape) + .clickable { + viewModel.editManager.apply { + if ( + isCropMode.value || isRotateMode.value || + isResizeMode.value || isEyeDropperMode.value || + isBlurMode.value + ) { + viewModel.cancelOperation() + return@clickable + } + if (isZoomMode.value) { + toggleZoomMode() + return@clickable + } + if (isPanMode.value) { + togglePanMode() + return@clickable + } + if ( + !viewModel.editManager.canUndo.value + ) { + if (isChangedForMemoIntegration) { + navigateBack() + } else { + if (launchedFromIntent) { + context + .getActivity() + ?.finish() + } else { + navigateBack() + } + } + } else { + viewModel.showExitDialog = true + } + } + }, + imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_back), + tint = MaterialTheme.colors.primary, + contentDescription = null + ) + + Row( + Modifier + .align(Alignment.TopEnd) + ) { + Icon( + modifier = Modifier + .padding(8.dp) + .size(36.dp) + .clip(CircleShape) + .clickable { + viewModel.editManager.apply { + if ( + isCropMode.value || isRotateMode.value || + isResizeMode.value || isBlurMode.value + ) { + viewModel.applyOperation() + return@clickable + } + } + viewModel.showMoreOptionsPopup = true + }, + imageVector = if ( + viewModel.editManager.isCropMode.value || + viewModel.editManager.isRotateMode.value || + viewModel.editManager.isResizeMode.value || + viewModel.editManager.isBlurMode.value + ) + ImageVector.vectorResource(R.drawable.ic_check) + else ImageVector.vectorResource(R.drawable.ic_more_vert), + tint = MaterialTheme.colors.primary, + contentDescription = null + ) + } +} + +@Composable +private fun StrokeWidthPopup( + modifier: Modifier, + viewModel: EditViewModel +) { + val editManager = viewModel.editManager + editManager.setPaintStrokeWidth(viewModel.strokeWidth.dp.toPx()) + if (viewModel.strokeSliderExpanded) { + Column( + modifier = modifier + .fillMaxWidth() + .height(150.dp) + .padding(8.dp) + ) { + Box( + modifier = Modifier + .weight(1f) + ) { + Box( + modifier = Modifier + .padding( + horizontal = 10.dp, + vertical = 5.dp + ) + .align(Alignment.Center) + .fillMaxWidth() + .height(viewModel.strokeWidth.dp) + .clip(RoundedCornerShape(30)) + .background(editManager.paintColor.value) + ) + } + + Slider( + modifier = Modifier + .fillMaxWidth(), + value = viewModel.strokeWidth, + onValueChange = { + viewModel.strokeWidth = it + }, + valueRange = 0.5f..50f, + ) + } + } +} + +@Composable +private fun EditMenuContainer(viewModel: EditViewModel, navigateBack: () -> Unit) { + Column( + Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Bottom + ) { + CropAspectRatiosMenu( + isVisible = viewModel.editManager.isCropMode.value, + viewModel.editManager.cropWindow + ) + ResizeInput( + isVisible = viewModel.editManager.isResizeMode.value, + viewModel.editManager + ) + + Box( + Modifier + .fillMaxWidth() + .wrapContentHeight() + .clip(RoundedCornerShape(topStartPercent = 30, topEndPercent = 30)) + .background(Gray) + .clickable { + viewModel.menusVisible = !viewModel.menusVisible + }, + contentAlignment = Alignment.Center + ) { + Icon( + if (viewModel.menusVisible) Icons.Filled.KeyboardArrowDown + else Icons.Filled.KeyboardArrowUp, + contentDescription = "", + modifier = Modifier.size(32.dp), + ) + } + AnimatedVisibility( + visible = viewModel.menusVisible, + enter = expandVertically(expandFrom = Alignment.Bottom), + exit = shrinkVertically(shrinkTowards = Alignment.Top) + ) { + EditMenuContent(viewModel, navigateBack) + EditMenuFlowHint( + viewModel.bottomButtonsScrollIsAtStart.value, + viewModel.bottomButtonsScrollIsAtEnd.value + ) + } + } +} + +@Composable +private fun EditMenuContent( + viewModel: EditViewModel, + navigateBack: () -> Unit +) { + val colorDialogExpanded = remember { mutableStateOf(false) } + val scrollState = rememberScrollState() + val editManager = viewModel.editManager + Column( + Modifier + .fillMaxWidth() + .background(Gray) + ) { + StrokeWidthPopup(Modifier, viewModel) + + BlurIntensityPopup(editManager) + + Row( + Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp) + .horizontalScroll(scrollState) + ) { + Icon( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .clickable { + if ( + !editManager.isRotateMode.value && + !editManager.isResizeMode.value && + !editManager.isCropMode.value && + !editManager.isEyeDropperMode.value && + !editManager.isBlurMode.value + ) { + editManager.undo() + } + }, + imageVector = ImageVector.vectorResource(R.drawable.ic_undo), + tint = if ( + editManager.canUndo.value && ( + !editManager.isRotateMode.value && + !editManager.isResizeMode.value && + !editManager.isCropMode.value && + !editManager.isEyeDropperMode.value && + !editManager.isBlurMode.value + ) + ) MaterialTheme.colors.primary else Color.Black, + contentDescription = null + ) + Icon( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .clickable { + if ( + !editManager.isRotateMode.value && + !editManager.isResizeMode.value && + !editManager.isCropMode.value && + !editManager.isEyeDropperMode.value && + !editManager.isBlurMode.value + ) editManager.redo() + }, + imageVector = ImageVector.vectorResource(R.drawable.ic_redo), + tint = if ( + editManager.canRedo.value && + ( + !editManager.isRotateMode.value && + !editManager.isResizeMode.value && + !editManager.isCropMode.value && + !editManager.isEyeDropperMode.value && + !editManager.isBlurMode.value + ) + ) MaterialTheme.colors.primary else Color.Black, + contentDescription = null + ) + Box( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .background(color = editManager.paintColor.value) + .clickable { + if (editManager.isEyeDropperMode.value) { + viewModel.toggleEyeDropper() + viewModel.cancelEyeDropper() + colorDialogExpanded.value = true + return@clickable + } + if ( + !editManager.isRotateMode.value && + !editManager.isResizeMode.value && + !editManager.isCropMode.value && + !editManager.isEraseMode.value && + !editManager.isBlurMode.value + ) + colorDialogExpanded.value = true + } + ) + ColorPickerDialog( + isVisible = colorDialogExpanded, + initialColor = editManager.paintColor.value, + usedColors = viewModel.usedColors, + enableEyeDropper = true, + onToggleEyeDropper = { + viewModel.toggleEyeDropper() + }, + onColorChanged = { + editManager.setPaintColor(it) + viewModel.trackColor(it) + } + ) + Icon( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .clickable { + if ( + !editManager.isRotateMode.value && + !editManager.isCropMode.value && + !editManager.isResizeMode.value && + !editManager.isEyeDropperMode.value && + !editManager.isBlurMode.value + ) + viewModel.strokeSliderExpanded = + !viewModel.strokeSliderExpanded + }, + imageVector = + ImageVector.vectorResource(R.drawable.ic_line_weight), + tint = if ( + !editManager.isRotateMode.value && + !editManager.isResizeMode.value && + !editManager.isCropMode.value && + !editManager.isEyeDropperMode.value && + !editManager.isBlurMode.value + ) editManager.paintColor.value + else Color.Black, + contentDescription = null + ) + Icon( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .clickable { + if ( + !editManager.isRotateMode.value && + !editManager.isResizeMode.value && + !editManager.isCropMode.value && + !editManager.isEyeDropperMode.value && + !editManager.isBlurMode.value + ) + editManager.toggleEraseMode() + }, + imageVector = ImageVector.vectorResource(R.drawable.ic_eraser), + tint = if ( + editManager.isEraseMode.value + ) + MaterialTheme.colors.primary + else + Color.Black, + contentDescription = null + ) + Icon( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .clickable { + if ( + !editManager.isRotateMode.value && + !editManager.isResizeMode.value && + !editManager.isCropMode.value && + !editManager.isEyeDropperMode.value && + !editManager.isBlurMode.value && + !editManager.isEraseMode.value + ) + editManager.toggleZoomMode() + }, + imageVector = ImageVector.vectorResource(R.drawable.ic_zoom_in), + tint = if ( + editManager.isZoomMode.value + ) + MaterialTheme.colors.primary + else + Color.Black, + contentDescription = null + ) + Icon( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .clickable { + if ( + !editManager.isRotateMode.value && + !editManager.isResizeMode.value && + !editManager.isCropMode.value && + !editManager.isEyeDropperMode.value && + !editManager.isBlurMode.value && + !editManager.isEraseMode.value + ) + editManager.togglePanMode() + }, + imageVector = ImageVector.vectorResource(R.drawable.ic_pan_tool), + tint = if ( + editManager.isPanMode.value + ) + MaterialTheme.colors.primary + else + Color.Black, + contentDescription = null + ) + Icon( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .clickable { + editManager.apply { + if ( + !isRotateMode.value && + !isResizeMode.value && + !isEyeDropperMode.value && + !isEraseMode.value && + !isBlurMode.value + ) { + toggleCropMode() + viewModel.menusVisible = + !editManager.isCropMode.value + if (isCropMode.value) { + val bitmap = viewModel.getEditedImage() + setBackgroundImage2() + backgroundImage.value = bitmap + viewModel.editManager.cropWindow.init( + bitmap.asAndroidBitmap() + ) + return@clickable + } + editManager.cancelCropMode() + editManager.scaleToFit() + editManager.cropWindow.close() + } + } + }, + imageVector = ImageVector.vectorResource(R.drawable.ic_crop), + tint = if ( + editManager.isCropMode.value + ) MaterialTheme.colors.primary + else + Color.Black, + contentDescription = null + ) + Icon( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .clickable { + editManager.apply { + if ( + !isCropMode.value && + !isResizeMode.value && + !isEyeDropperMode.value && + !isEraseMode.value && + !isBlurMode.value + ) { + toggleRotateMode() + if (isRotateMode.value) { + setBackgroundImage2() + viewModel.menusVisible = + !editManager.isRotateMode.value + scaleToFitOnEdit() + return@clickable + } + cancelRotateMode() + scaleToFit() + } + } + }, + imageVector = ImageVector + .vectorResource(R.drawable.ic_rotate_90_degrees_ccw), + tint = if (editManager.isRotateMode.value) + MaterialTheme.colors.primary + else + Color.Black, + contentDescription = null + ) + Icon( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .clickable { + editManager.apply { + if ( + !isRotateMode.value && + !isCropMode.value && + !isEyeDropperMode.value && + !isEraseMode.value && + !isBlurMode.value + ) + toggleResizeMode() + else return@clickable + viewModel.menusVisible = !isResizeMode.value + if (isResizeMode.value) { + setBackgroundImage2() + val imgBitmap = viewModel.getEditedImage() + backgroundImage.value = imgBitmap + resizeOperation.init( + imgBitmap.asAndroidBitmap() + ) + return@clickable + } + cancelResizeMode() + scaleToFit() + } + }, + imageVector = ImageVector + .vectorResource(R.drawable.ic_aspect_ratio), + tint = if (editManager.isResizeMode.value) + MaterialTheme.colors.primary + else + Color.Black, + contentDescription = null + ) + Icon( + modifier = Modifier + .padding(12.dp) + .size(40.dp) + .clip(CircleShape) + .clickable { + editManager.apply { + if ( + !isRotateMode.value && + !isCropMode.value && + !isEyeDropperMode.value && + !isResizeMode.value && + !isEraseMode.value && + !viewModel.strokeSliderExpanded + ) toggleBlurMode() + if (isBlurMode.value) { + setBackgroundImage2() + backgroundImage.value = viewModel.getEditedImage() + blurOperation.init() + return@clickable + } + blurOperation.cancel() + scaleToFit() + } + }, + imageVector = ImageVector + .vectorResource(R.drawable.ic_blur_on), + tint = if (editManager.isBlurMode.value) + MaterialTheme.colors.primary + else + Color.Black, + contentDescription = null + ) + } + } + viewModel.bottomButtonsScrollIsAtStart.value = scrollState.value == 0 + viewModel.bottomButtonsScrollIsAtEnd.value = + scrollState.value == scrollState.maxValue +} + +@Composable +fun EditMenuFlowHint( + scrollIsAtStart: Boolean = true, + scrollIsAtEnd: Boolean = false +) { + Box(Modifier.fillMaxSize()) { + AnimatedVisibility( + visible = scrollIsAtEnd || (!scrollIsAtStart && !scrollIsAtEnd), + enter = fadeIn(tween(durationMillis = 1000)), + exit = fadeOut((tween(durationMillis = 1000))), + modifier = Modifier.align(Alignment.BottomStart) + ) { + Icon( + Icons.Filled.KeyboardArrowLeft, + contentDescription = null, + Modifier + .background(Gray) + .padding(top = 16.dp, bottom = 16.dp) + .size(32.dp) + ) + } + AnimatedVisibility( + visible = scrollIsAtStart || (!scrollIsAtStart && !scrollIsAtEnd), + enter = fadeIn(tween(durationMillis = 1000)), + exit = fadeOut((tween(durationMillis = 1000))), + modifier = Modifier.align(Alignment.BottomEnd) + ) { + Icon( + Icons.Filled.KeyboardArrowRight, + contentDescription = null, + Modifier + .background(Gray) + .padding(top = 16.dp, bottom = 16.dp) + .size(32.dp) + ) + } + } +} + +@Composable +private fun HandleImageSavedEffect( + viewModel: EditViewModel, + launchedFromIntent: Boolean, + navigateBack: () -> Unit, +) { + val context = LocalContext.current + LaunchedEffect(viewModel.imageSaved) { + if (!viewModel.imageSaved) + return@LaunchedEffect + if (launchedFromIntent) + context.getActivity()?.finish() + else + navigateBack() + } +} + +@Composable +private fun ExitDialog( + viewModel: EditViewModel, + navigateBack: () -> Unit, + launchedFromIntent: Boolean, + onSaveSvg: () -> Unit = {} +) { + if (!viewModel.showExitDialog) return + + val context = LocalContext.current + + AlertDialog( + onDismissRequest = { + viewModel.showExitDialog = false + }, + title = { + Text( + modifier = Modifier.padding(top = 4.dp, bottom = 8.dp), + text = "Do you want to save the changes?", + fontSize = 16.sp + ) + }, + confirmButton = { + Button( + onClick = { + if (isChangedForMemoIntegration) { + onSaveSvg() + } + viewModel.showExitDialog = false + viewModel.showSavePathDialog = true + } + ) { + Text("Save") + } + }, + dismissButton = { + TextButton( + onClick = { + viewModel.showExitDialog = false + if (isChangedForMemoIntegration) { + navigateBack() + } else { + if (launchedFromIntent) { + context.getActivity()?.finish() + } else { + navigateBack() + } + } + } + ) { + Text("Exit") + } + } + ) +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt new file mode 100644 index 0000000..39ca4c7 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt @@ -0,0 +1,519 @@ +package dev.arkbuilders.canvas.presentation.edit + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.drawable.Drawable +import android.media.MediaScannerConnection +import android.net.Uri +import android.view.MotionEvent +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.asComposePath +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.unit.toSize +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.bumptech.glide.Glide +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dev.arkbuilders.canvas.presentation.data.Preferences +import dev.arkbuilders.canvas.presentation.data.Resolution +import dev.arkbuilders.canvas.presentation.drawing.DrawPath +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import dev.arkbuilders.canvas.presentation.graphics.SVG +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.File +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.outputStream +import kotlin.system.measureTimeMillis + +class EditViewModel( + private val primaryColor: Long, + private val launchedFromIntent: Boolean, + private val imagePath: Path?, + private val imageUri: String?, + private val maxResolution: Resolution, + private val prefs: Preferences +) : ViewModel() { + val editManager = EditManager() + + var strokeSliderExpanded by mutableStateOf(false) + var menusVisible by mutableStateOf(true) + var strokeWidth by mutableStateOf(5f) + var showSavePathDialog by mutableStateOf(false) + val showOverwriteCheckbox = mutableStateOf(imagePath != null) + var showExitDialog by mutableStateOf(false) + var showMoreOptionsPopup by mutableStateOf(false) + var imageSaved by mutableStateOf(false) + var isSavingImage by mutableStateOf(false) + var showEyeDropperHint by mutableStateOf(false) + val showConfirmClearDialog = mutableStateOf(false) + var isLoaded by mutableStateOf(false) + var exitConfirmed = false + private set + val bottomButtonsScrollIsAtStart = mutableStateOf(true) + val bottomButtonsScrollIsAtEnd = mutableStateOf(false) + + private val _usedColors = mutableListOf() + val usedColors: List = _usedColors + + fun setPaths() { + viewModelScope.launch { + editManager.setPaintColor(Color.Blue) + val svgpaths = SVG.parse(Path("/storage/emulated/0/Documents/32254-1096105931.svg")) + svgpaths.getPaths().forEach { + val draw = DrawPath( + path = it.path.asComposePath(), + paint = Paint().apply { + color = Color(it.paint.color) + } + ) + editManager.addDrawPath(draw.path) + editManager.setPaintColor(draw.paint.color) + } + } + } + + init { + if (imageUri == null && imagePath == null) { + viewModelScope.launch { + editManager.initDefaults( + prefs.readDefaults(), + maxResolution + ) + } + } + viewModelScope.launch { + _usedColors.addAll(prefs.readUsedColors()) + + val color = if (_usedColors.isNotEmpty()) { + _usedColors.last() + } else { + val defaultColor = Color(primaryColor.toULong()) + + _usedColors.add(defaultColor) + defaultColor + } + + editManager.setPaintColor(color) + } + setPaths() + } + +// fun loadImage() { +// isLoaded = true +// imagePath?.let { +// loadImageWithPath( +// DIManager.component.app(), +// imagePath, +// editManager +// ) +// return +// } +// imageUri?.let { +// loadImageWithUri( +// DIManager.component.app(), +// imageUri, +// editManager +// ) +// return +// } +// editManager.scaleToFit() +// } + + fun saveImage(context: Context, path: Path) { + viewModelScope.launch(Dispatchers.IO) { + isSavingImage = true + val combinedBitmap = getEditedImage() + path.outputStream().use { out -> + combinedBitmap.asAndroidBitmap() + .compress(Bitmap.CompressFormat.PNG, 100, out) + } + MediaScannerConnection.scanFile( + context, + arrayOf(path.toString()), + arrayOf("image/*") + ) { _, _ -> } + imageSaved = true + isSavingImage = false + showSavePathDialog = false + } + } + + fun shareImage(context: Context) = + viewModelScope.launch(Dispatchers.IO) { + val intent = Intent(Intent.ACTION_SEND) + val uri = getCachedImageUri(context) + intent.type = "image/*" + intent.putExtra(Intent.EXTRA_STREAM, uri) + context.apply { + startActivity( + Intent.createChooser( + intent, + "Share" + ) + ) + } + } + + fun getImageUri( + context: Context, + bitmap: Bitmap? = null, + name: String = "" + ) = getCachedImageUri(context, bitmap, name) + + private fun getCachedImageUri( + context: Context, + bitmap: Bitmap? = null, + name: String = "" + ): Uri { + var uri: Uri? = null + val imageCacheFolder = File(context.cacheDir, "images") + val imgBitmap = bitmap ?: getEditedImage().asAndroidBitmap() + try { + imageCacheFolder.mkdirs() + val file = File(imageCacheFolder, "image$name.png") + file.outputStream().use { out -> + imgBitmap + .compress(Bitmap.CompressFormat.PNG, 100, out) + } + Timber.tag("Cached image path").d(file.path.toString()) + uri = FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + file + ) + } catch (e: Exception) { + e.printStackTrace() + } + return uri!! + } + + fun trackColor(color: Color) { + _usedColors.remove(color) + _usedColors.add(color) + + val excess = _usedColors.size - KEEP_USED_COLORS + repeat(excess) { + _usedColors.removeFirst() + } + + viewModelScope.launch { + prefs.persistUsedColors(usedColors) + } + } + + fun toggleEyeDropper() { + editManager.toggleEyeDropper() + } + + fun cancelEyeDropper() { + editManager.setPaintColor(usedColors.last()) + } + + fun applyEyeDropper(action: Int, x: Int, y: Int) { + try { + val bitmap = getEditedImage().asAndroidBitmap() + val imageX = (x * editManager.bitmapScale.x).toInt() + val imageY = (y * editManager.bitmapScale.y).toInt() + val pixel = bitmap.getPixel(imageX, imageY) + val color = Color(pixel) + if (color == Color.Transparent) { + showEyeDropperHint = true + return + } + when (action) { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP -> { + trackColor(color) + toggleEyeDropper() + menusVisible = true + } + } + editManager.setPaintColor(color) + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun getCombinedImageBitmap(): ImageBitmap { + val size = editManager.imageSize + val drawBitmap = ImageBitmap( + size.width, + size.height, + ImageBitmapConfig.Argb8888 + ) + val combinedBitmap = + ImageBitmap(size.width, size.height, ImageBitmapConfig.Argb8888) + + val time = measureTimeMillis { + val backgroundPaint = Paint().also { + it.color = editManager.backgroundColor.value + } + val drawCanvas = Canvas(drawBitmap) + val combinedCanvas = Canvas(combinedBitmap) + val matrix = Matrix().apply { + if (editManager.rotationAngles.isNotEmpty()) { + val centerX = size.width / 2 + val centerY = size.height / 2 + setRotate( + editManager.rotationAngle.value, + centerX.toFloat(), + centerY.toFloat() + ) + } + } + combinedCanvas.drawRect( + Rect(Offset.Zero, size.toSize()), + backgroundPaint + ) + combinedCanvas.nativeCanvas.setMatrix(matrix) + editManager.backgroundImage.value?.let { + combinedCanvas.drawImage( + it, + Offset.Zero, + Paint() + ) + } + editManager.drawPaths.forEach { + drawCanvas.drawPath(it.path, it.paint) + } + combinedCanvas.drawImage(drawBitmap, Offset.Zero, Paint()) + } + Timber.tag("edit-viewmodel: getCombinedImageBitmap").d( + "processing edits took ${time / 1000} s ${time % 1000} ms" + ) + return combinedBitmap + } + + fun getEditedImage(): ImageBitmap { + val size = editManager.imageSize + var bitmap = ImageBitmap( + size.width, + size.height, + ImageBitmapConfig.Argb8888 + ) + var pathBitmap: ImageBitmap? = null + val time = measureTimeMillis { + editManager.apply { + val matrix = Matrix() + if (editManager.drawPaths.isNotEmpty()) { + pathBitmap = ImageBitmap( + size.width, + size.height, + ImageBitmapConfig.Argb8888 + ) + val pathCanvas = Canvas(pathBitmap!!) + editManager.drawPaths.forEach { + pathCanvas.drawPath(it.path, it.paint) + } + } + backgroundImage.value?.let { + val canvas = Canvas(bitmap) + if (prevRotationAngle == 0f && drawPaths.isEmpty()) { + bitmap = it + return@let + } + if (prevRotationAngle != 0f) { + val centerX = size.width / 2f + val centerY = size.height / 2f + matrix.setRotate(prevRotationAngle, centerX, centerY) + } + canvas.nativeCanvas.drawBitmap( + it.asAndroidBitmap(), + matrix, + null + ) + if (drawPaths.isNotEmpty()) { + canvas.nativeCanvas.drawBitmap( + pathBitmap?.asAndroidBitmap()!!, + matrix, + null + ) + } + } ?: run { + val canvas = Canvas(bitmap) + if (prevRotationAngle != 0f) { + val centerX = size.width / 2 + val centerY = size.height / 2 + matrix.setRotate( + prevRotationAngle, + centerX.toFloat(), + centerY.toFloat() + ) + canvas.nativeCanvas.setMatrix(matrix) + } + canvas.drawRect( + Rect(Offset.Zero, size.toSize()), + backgroundPaint + ) + if (drawPaths.isNotEmpty()) { + canvas.drawImage( + pathBitmap!!, + Offset.Zero, + Paint() + ) + } + } + } + } + Timber.tag("edit-viewmodel: getEditedImage").d( + "processing edits took ${time / 1000} s ${time % 1000} ms" + ) + return bitmap + } + + fun confirmExit() = viewModelScope.launch { + exitConfirmed = true + isLoaded = false + delay(2_000) + exitConfirmed = false + isLoaded = true + } + + fun applyOperation() { + editManager.applyOperation() + menusVisible = true + } + + fun cancelOperation() { + editManager.apply { + if (isRotateMode.value) { + toggleRotateMode() + cancelRotateMode() + menusVisible = true + } + if (isCropMode.value) { + toggleCropMode() + cancelCropMode() + menusVisible = true + } + if (isResizeMode.value) { + toggleResizeMode() + cancelResizeMode() + menusVisible = true + } + if (isEyeDropperMode.value) { + toggleEyeDropper() + cancelEyeDropper() + menusVisible = true + } + if (isBlurMode.value) { + toggleBlurMode() + blurOperation.cancel() + menusVisible = true + } + scaleToFit() + } + } + + fun persistDefaults(color: Color, resolution: Resolution) { + viewModelScope.launch { + prefs.persistDefaults(color, resolution) + } + } + + companion object { + private const val KEEP_USED_COLORS = 20 + } +} + +class EditViewModelFactory @AssistedInject constructor( + @Assisted private val primaryColor: Long, + @Assisted private val launchedFromIntent: Boolean, + @Assisted private val imagePath: Path?, + @Assisted private val imageUri: String?, + @Assisted private val maxResolution: Resolution, + private val prefs: Preferences, +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return EditViewModel( + primaryColor, + launchedFromIntent, + imagePath, + imageUri, + maxResolution, + prefs, + ) as T + } + + @AssistedFactory + interface Factory { + fun create( + @Assisted primaryColor: Long, + @Assisted launchedFromIntent: Boolean, + @Assisted imagePath: Path?, + @Assisted imageUri: String?, + @Assisted maxResolution: Resolution, + ): EditViewModelFactory + } +} + +private fun loadImageWithPath( + context: Context, + image: Path, + editManager: EditManager +) { + initGlideBuilder(context) + .load(image.toFile()) + .loadInto(editManager) +} + +private fun loadImageWithUri( + context: Context, + uri: String, + editManager: EditManager +) { + initGlideBuilder(context) + .load(uri.toUri()) + .loadInto(editManager) +} + +private fun initGlideBuilder(context: Context) = Glide + .with(context) + .asBitmap() + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + +private fun RequestBuilder.loadInto( + editManager: EditManager +) { + into(object : CustomTarget() { + override fun onResourceReady( + bitmap: Bitmap, + transition: Transition? + ) { + editManager.apply { + val image = bitmap.asImageBitmap() + backgroundImage.value = image + setOriginalBackgroundImage(image) + scaleToFit() + } + } + + override fun onLoadCleared(placeholder: Drawable?) {} + }) +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/MoreOptionsPopup.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/MoreOptionsPopup.kt new file mode 100644 index 0000000..cc74834 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/MoreOptionsPopup.kt @@ -0,0 +1,131 @@ +package dev.arkbuilders.canvas.presentation.edit + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties +import dev.arkbuilders.canvas.R +import dev.arkbuilders.canvas.presentation.picker.toPx + +@Composable +fun MoreOptionsPopup( + onClearEdits: () -> Unit, + onShareClick: () -> Unit, + onSaveClick: () -> Unit, + onDismissClick: () -> Unit +) { + Popup( + alignment = Alignment.TopEnd, + offset = IntOffset( + -8.dp.toPx().toInt(), + 8.dp.toPx().toInt() + ), + properties = PopupProperties( + focusable = true + ), + onDismissRequest = onDismissClick, + ) { + Column( + Modifier + .background( + Color.LightGray, + RoundedCornerShape(8) + ) + .padding(8.dp) + ) { + Row { + Column( + Modifier + .padding(8.dp) + .clip(RoundedCornerShape(5)) + .clickable { + onClearEdits() + } + ) { + + Icon( + modifier = Modifier + .padding(8.dp) + .size(36.dp), + imageVector = + ImageVector.vectorResource(R.drawable.ic_clear), + contentDescription = null + ) + Text( + text = stringResource(R.string.clear), + Modifier + .padding(bottom = 8.dp) + .align(Alignment.CenterHorizontally), + fontSize = 12.sp + ) + } + Column( + Modifier + .padding(8.dp) + .clip(RoundedCornerShape(5)) + .clickable { + onShareClick() + } + ) { + + Icon( + modifier = Modifier + .padding(8.dp) + .size(36.dp), + imageVector = ImageVector + .vectorResource(R.drawable.ic_share), + contentDescription = null + ) + Text( + text = stringResource(R.string.share), + Modifier + .padding(bottom = 8.dp) + .align(Alignment.CenterHorizontally), + fontSize = 12.sp + ) + } + Column( + Modifier + .padding(8.dp) + .clip(RoundedCornerShape(5)) + .clickable { + onSaveClick() + } + ) { + Icon( + modifier = Modifier + .padding(8.dp) + .size(36.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_save), + contentDescription = null + ) + Text( + text = stringResource(R.string.save), + Modifier + .padding(bottom = 8.dp) + .align(Alignment.CenterHorizontally), + fontSize = 12.sp + ) + } + } + } + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt new file mode 100644 index 0000000..179f037 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt @@ -0,0 +1,328 @@ +package dev.arkbuilders.canvas.presentation.edit + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Checkbox +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.core.text.isDigitsOnly +import dev.arkbuilders.canvas.R +import dev.arkbuilders.canvas.presentation.data.Resolution +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import dev.arkbuilders.canvas.presentation.edit.resize.Hint +import dev.arkbuilders.canvas.presentation.edit.resize.delayHidingHint +import dev.arkbuilders.canvas.presentation.theme.Gray + +//import dev.arkbuilders.canvas.presentation.theme.getGray + +@Composable +fun NewImageOptionsDialog( + defaultResolution: Resolution, + maxResolution: Resolution, + _backgroundColor: Color, + navigateBack: () -> Unit, + editManager: EditManager, + persistDefaults: (Color, Resolution) -> Unit, + onConfirm: () -> Unit +) { + var isVisible by remember { mutableStateOf(true) } + var backgroundColor by remember { + mutableStateOf(_backgroundColor) + } + val showColorDialog = remember { mutableStateOf(false) } + + ColorPickerDialog( + isVisible = showColorDialog, + initialColor = backgroundColor, + enableEyeDropper = false, + onToggleEyeDropper = {}, + onColorChanged = { + backgroundColor = it + } + ) + + if (isVisible) { + var width by remember { + mutableStateOf(defaultResolution.width.toString()) + } + var height by remember { + mutableStateOf(defaultResolution.height.toString()) + } + var widthError by remember { + mutableStateOf(0.toString()) + } + var heightError by remember { + mutableStateOf(0.toString()) + } + var rememberDefaults by remember { mutableStateOf(false) } + var showHint by remember { mutableStateOf(false) } + var hint by remember { mutableStateOf("") } + val maxHeightHint = stringResource( + R.string.height_too_large, + maxResolution.height + ) + val minHeightHint = stringResource( + R.string.height_not_accepted, + heightError + ) + val maxWidthHint = stringResource( + R.string.width_too_large, + maxResolution.width + ) + val minWidthHint = stringResource( + R.string.width_not_accepted, + widthError + ) + val digitsOnlyHint = stringResource( + R.string.digits_only + ) + val widthEmptyHint = stringResource( + R.string.width_empty + ) + val heightEmptyHint = stringResource( + R.string.height_empty + ) + + Dialog( + onDismissRequest = { + isVisible = false + navigateBack() + } + ) { + Column( + Modifier + .background(Color.White, RoundedCornerShape(5)) + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + "Customize new image", + Modifier.padding(top = 10.dp) + ) + Row( + Modifier.padding( + start = 8.dp, + end = 8.dp, + top = 20.dp, + bottom = 12.dp + ) + ) { + TextField( + modifier = Modifier + .padding(end = 6.dp) + .fillMaxWidth(0.5f), + value = width, + onValueChange = { + if (!it.isDigitsOnly()) { + hint = digitsOnlyHint + showHint = true + return@TextField + } + if ( + it.isNotEmpty() && it.isDigitsOnly() && + it.toInt() > maxResolution.width + ) { + hint = maxWidthHint + showHint = true + return@TextField + } + if ( + it.isNotEmpty() && it.isDigitsOnly() && + it.toInt() <= 0 + ) { + widthError = it + hint = minWidthHint + showHint = true + return@TextField + } + if (it.isDigitsOnly()) { + width = it + } + }, + label = { + Text( + stringResource(R.string.width), + Modifier.fillMaxWidth(), + color = MaterialTheme.colors.primary, + textAlign = TextAlign.Center + ) + }, + textStyle = TextStyle( + color = Color.DarkGray, + textAlign = TextAlign.Center + ), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number + ) + ) + TextField( + modifier = Modifier + .padding(start = 6.dp) + .fillMaxWidth(), + value = height, + onValueChange = { + if (!it.isDigitsOnly()) { + hint = digitsOnlyHint + showHint = true + return@TextField + } + if ( + it.isNotEmpty() && it.isDigitsOnly() && + it.toInt() > maxResolution.height + ) { + hint = maxHeightHint + showHint = true + return@TextField + } + if ( + it.isNotEmpty() && it.isDigitsOnly() && + it.toInt() <= 0 + ) { + heightError = it + hint = minHeightHint + showHint = true + return@TextField + } + if (it.isDigitsOnly()) { + height = it + } + }, + label = { + Text( + stringResource(R.string.height), + Modifier.fillMaxWidth(), + color = MaterialTheme.colors.primary, + textAlign = TextAlign.Center + ) + }, + textStyle = TextStyle( + color = Color.DarkGray, + textAlign = TextAlign.Center + ), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number + ) + ) + } + Row( + Modifier + .background(Gray, RoundedCornerShape(5)) + .wrapContentHeight() + .clickable { + showColorDialog.value = true + }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + stringResource(R.string.background), + Modifier.padding(8.dp) + ) + Box( + Modifier + .size(28.dp) + .padding(2.dp) + .clip(CircleShape) + .border(2.dp, Gray, CircleShape) + .background(backgroundColor) + ) + } + Row( + Modifier + .padding(start = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = rememberDefaults, + onCheckedChange = { + rememberDefaults = it + } + ) + Text("Remember") + } + Row( + Modifier.align( + Alignment.End + ) + ) { + TextButton( + modifier = Modifier + .padding(end = 8.dp), + onClick = { + isVisible = false + navigateBack() + } + ) { + Text("Close") + } + TextButton( + modifier = Modifier + .padding(end = 8.dp), + onClick = { + if (width.isEmpty()) { + hint = widthEmptyHint + showHint = true + return@TextButton + } + if (height.isEmpty()) { + hint = heightEmptyHint + showHint = true + return@TextButton + } + val resolution = Resolution( + width.toInt(), + height.toInt() + ) + editManager.setImageResolution(resolution) + editManager.setBackgroundColor(backgroundColor) + if (rememberDefaults) + persistDefaults(backgroundColor, resolution) + onConfirm() + isVisible = false + } + ) { + Text(stringResource(R.string.ok)) + } + } + } + + Hint( + hint + ) { + delayHidingHint(it) { + showHint = false + } + showHint + } + } + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/Operation.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/Operation.kt new file mode 100644 index 0000000..c22fe88 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/Operation.kt @@ -0,0 +1,9 @@ +package dev.arkbuilders.canvas.presentation.edit + +interface Operation { + fun apply() + + fun undo() + + fun redo() +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/SavePathDialog.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/SavePathDialog.kt new file mode 100644 index 0000000..1993936 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/SavePathDialog.kt @@ -0,0 +1,231 @@ +package dev.arkbuilders.canvas.presentation.edit + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.Checkbox +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.fragment.app.FragmentManager +import dev.arkbuilders.canvas.R +import dev.arkbuilders.canvas.presentation.utils.findNotExistCopyName +import dev.arkbuilders.canvas.presentation.utils.toast +import dev.arkbuilders.components.filepicker.ArkFilePickerConfig +import dev.arkbuilders.components.filepicker.ArkFilePickerFragment +import dev.arkbuilders.components.filepicker.ArkFilePickerMode +import dev.arkbuilders.components.filepicker.onArkPathPicked +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.name + +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +@Composable +fun SavePathDialog( + initialImagePath: Path?, + fragmentManager: FragmentManager, + onDismissClick: () -> Unit, + onPositiveClick: (Path) -> Unit +) { + var currentPath by remember { mutableStateOf(initialImagePath?.parent) } + var imagePath by remember { mutableStateOf(initialImagePath) } + val showOverwriteCheckbox = remember { mutableStateOf(initialImagePath != null) } + var overwriteOriginalPath by remember { mutableStateOf(false) } + var name by remember { + mutableStateOf( + initialImagePath?.let { + it.parent.findNotExistCopyName(it.fileName).name + } ?: "image.png" + ) + } + + val lifecycleOwner = LocalLifecycleOwner.current + + val context = LocalContext.current + + LaunchedEffect(overwriteOriginalPath) { + if (overwriteOriginalPath) { + imagePath?.let { + currentPath = it.parent + name = it.name + } + return@LaunchedEffect + } + imagePath?.let { + name = it.parent.findNotExistCopyName(it.fileName).name + } + } + + key(showOverwriteCheckbox.value) { + Dialog(onDismissRequest = onDismissClick) { + Column( + Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(Color.White, RoundedCornerShape(5)) + .padding(5.dp) + ) { + Text( + modifier = Modifier.padding(5.dp), + text = stringResource(R.string.location), + fontSize = 18.sp + ) + TextButton( + onClick = { + ArkFilePickerFragment + .newInstance( + folderFilePickerConfig(currentPath) + ) + .show(fragmentManager, null) + fragmentManager.onArkPathPicked(lifecycleOwner) { path -> + currentPath = path + currentPath?.let { + imagePath = it.resolve(name) + showOverwriteCheckbox.value = Files.list(it).toList() + .contains(imagePath) + if (showOverwriteCheckbox.value) { + name = it.findNotExistCopyName( + imagePath?.fileName!! + ).name + } + } + } + } + ) { + Text( + text = currentPath?.toString() + ?: stringResource(R.string.pick_folder) + ) + } + OutlinedTextField( + modifier = Modifier.padding(5.dp), + value = name, + onValueChange = { + name = it + if (name.isEmpty()) { + context.toast( + R.string.ark_retouch_notify_missing_file_name + ) + return@OutlinedTextField + } + currentPath?.let { path -> + imagePath = path.resolve(name) + showOverwriteCheckbox.value = Files.list(path).toList() + .contains(imagePath) + if (showOverwriteCheckbox.value) { + name = path.findNotExistCopyName( + imagePath?.fileName!! + ).name + } + } + }, + label = { Text(text = stringResource(R.string.name)) }, + singleLine = true + ) + if (showOverwriteCheckbox.value) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(5)) + .clickable { + overwriteOriginalPath = !overwriteOriginalPath + } + .padding(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = overwriteOriginalPath, + onCheckedChange = { + overwriteOriginalPath = !overwriteOriginalPath + } + ) + Text(text = stringResource(R.string.overwrite_original_file)) + } + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton( + modifier = Modifier.padding(5.dp), + onClick = onDismissClick + ) { + Text(text = stringResource(R.string.cancel)) + } + Button( + modifier = Modifier.padding(5.dp), + onClick = { + if (name.isEmpty()) { + context.toast( + R.string.ark_retouch_notify_missing_file_name + ) + return@Button + } + if (currentPath == null) { + context.toast( + R.string.ark_retouch_notify_choose_folder + ) + return@Button + } + onPositiveClick(currentPath?.resolve(name)!!) + } + ) { + Text(text = stringResource(R.string.ok)) + } + } + } + } + } +} + +@Composable +fun SaveProgress() { + Dialog(onDismissRequest = {}) { + Box( + Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier.size(40.dp) + ) + } + } +} + +fun folderFilePickerConfig(initialPath: Path?) = ArkFilePickerConfig( + mode = ArkFilePickerMode.FOLDER, + initialPath = initialPath, + showRoots = true, + rootsFirstPage = true +) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/TransparencyChessBoard.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/TransparencyChessBoard.kt new file mode 100644 index 0000000..f0a7b99 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/TransparencyChessBoard.kt @@ -0,0 +1,91 @@ +package dev.arkbuilders.canvas.presentation.edit + +import android.graphics.Matrix +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.unit.toSize +import dev.arkbuilders.canvas.presentation.drawing.EditManager + +private class TransparencyChessBoard { + fun create(boardSize: Size, canvas: Canvas, matrix: Matrix) { + val numberOfBoxesOnHeight = (boardSize.height / SQUARE_SIZE).toInt() + val numberOfBoxesOnWidth = (boardSize.width / SQUARE_SIZE).toInt() + var color = DARK + val paint = Paint().also { + it.color = color + } + val width = SQUARE_SIZE * (numberOfBoxesOnWidth + 1) + val height = SQUARE_SIZE * (numberOfBoxesOnHeight + 1) + val widthDelta = width - boardSize.width + val heightDelta = height - boardSize.height + canvas.nativeCanvas.setMatrix(matrix) + 0.rangeTo(numberOfBoxesOnWidth).forEach { i -> + 0.rangeTo(numberOfBoxesOnHeight).forEach { j -> + var rectWidth = SQUARE_SIZE + var rectHeight = SQUARE_SIZE + val offsetX = SQUARE_SIZE * i + val offsetY = SQUARE_SIZE * j + if (i == numberOfBoxesOnWidth && widthDelta > 0) { + rectWidth = SQUARE_SIZE - widthDelta + } + if (j == numberOfBoxesOnHeight && heightDelta > 0) { + rectHeight = SQUARE_SIZE - heightDelta + } + val offset = Offset(offsetX, offsetY) + val box = Rect(offset, Size(rectWidth, rectHeight)) + if (j == 0) { + if (color == paint.color) { + switchPaintColor(paint) + } + color = paint.color + } + switchPaintColor(paint) + canvas.drawRect(box, paint) + } + } + } + + private fun switchPaintColor(paint: Paint) { + if (paint.color == DARK) + paint.color = LIGHT + else paint.color = DARK + } + + companion object { + private const val SQUARE_SIZE = 100f + private val LIGHT = Color.White + private val DARK = Color.LightGray + } +} + +private fun transparencyChessBoard( + canvas: Canvas, + size: Size, + matrix: Matrix +) { + TransparencyChessBoard().create(size, canvas, matrix) +} + +@Composable +fun TransparencyChessBoardCanvas(modifier: Modifier, editManager: EditManager) { + Canvas(modifier.background(Color.Transparent)) { + editManager.invalidatorTick.value + drawIntoCanvas { canvas -> + transparencyChessBoard( + canvas, + editManager.imageSize.toSize(), + editManager.backgroundMatrix + ) + } + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/blur/BlurIntensityPopup.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/blur/BlurIntensityPopup.kt new file mode 100644 index 0000000..6c68eaa --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/blur/BlurIntensityPopup.kt @@ -0,0 +1,56 @@ +package dev.arkbuilders.canvas.presentation.edit.blur + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Slider +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.arkbuilders.canvas.R +import dev.arkbuilders.canvas.presentation.drawing.EditManager + +@Composable +fun BlurIntensityPopup( + editManager: EditManager +) { + if (editManager.isBlurMode.value) { + Column( + Modifier + .fillMaxWidth() + .height(150.dp) + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column { + Text(stringResource(R.string.blur_intensity)) + Slider( + modifier = Modifier + .fillMaxWidth(), + value = editManager.blurIntensity.value, + onValueChange = { + editManager.blurIntensity.value = it + }, + valueRange = 0f..25f, + ) + } + Column { + Text(stringResource(R.string.blur_size)) + Slider( + modifier = Modifier + .fillMaxWidth(), + value = editManager.blurOperation.blurSize.value, + onValueChange = { + editManager.blurOperation.blurSize.value = it + editManager.blurOperation.resize() + }, + valueRange = 100f..500f, + ) + } + } + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/blur/BlurOperation.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/blur/BlurOperation.kt new file mode 100644 index 0000000..9ba6c1a --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/blur/BlurOperation.kt @@ -0,0 +1,183 @@ +package dev.arkbuilders.canvas.presentation.edit.blur + +import android.content.Context +import android.graphics.Bitmap +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.unit.IntOffset +import com.hoko.blur.processor.HokoBlurBuild +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import dev.arkbuilders.canvas.presentation.edit.Operation +import java.util.Stack + +class BlurOperation(private val editManager: EditManager) : Operation { + private lateinit var blurredBitmap: Bitmap + private lateinit var brushBitmap: Bitmap + private lateinit var context: Context + private val blurs = Stack() + private val redoBlurs = Stack() + private var offset = Offset.Zero + private var bitmapPosition = IntOffset.Zero + + val blurSize = mutableStateOf(BRUSH_SIZE.toFloat()) + + fun init() { + editManager.apply { + backgroundImage.value?.let { + bitmapPosition = IntOffset( + (it.width / 2) - (blurSize.value.toInt() / 2), + (it.height / 2) - (blurSize.value.toInt() / 2) + ) + brushBitmap = Bitmap.createBitmap( + it.asAndroidBitmap(), + bitmapPosition.x, + bitmapPosition.y, + blurSize.value.toInt(), + blurSize.value.toInt() + ) + } + scaleToFitOnEdit() + } + } + + fun resize() { + editManager.backgroundImage.value?.let { + if (isWithinBounds(it)) { + brushBitmap = Bitmap.createBitmap( + it.asAndroidBitmap(), + bitmapPosition.x, + bitmapPosition.y, + blurSize.value.toInt(), + blurSize.value.toInt() + ) + } + } + } + + fun draw(context: Context, canvas: Canvas) { + if (blurSize.value in MIN_SIZE..MAX_SIZE) { + editManager.backgroundImage.value?.let { + this.context = context + if (isWithinBounds(it)) { + offset = Offset( + bitmapPosition.x.toFloat(), + bitmapPosition.y.toFloat() + ) + } + blur(context) + canvas.drawImage( + blurredBitmap.asImageBitmap(), + offset, + Paint() + ) + } + } + } + + fun move(blurPosition: Offset, delta: Offset) { + val position = Offset( + blurPosition.x * editManager.bitmapScale.x, + blurPosition.y * editManager.bitmapScale.y + ) + if (isBrushTouched(position)) { + editManager.apply { + bitmapPosition = IntOffset( + (offset.x + delta.x).toInt(), + (offset.y + delta.y).toInt() + ) + backgroundImage.value?.let { + if (isWithinBounds(it)) { + brushBitmap = Bitmap.createBitmap( + it.asAndroidBitmap(), + bitmapPosition.x, + bitmapPosition.y, + blurSize.value.toInt(), + blurSize.value.toInt() + ) + } + } + } + } + } + + fun clear() { + blurs.clear() + redoBlurs.clear() + editManager.updateRevised() + } + + fun cancel() { + editManager.redrawBackgroundImage2() + } + + private fun isWithinBounds(image: ImageBitmap) = bitmapPosition.x >= 0 && + (bitmapPosition.x + blurSize.value) <= image.width && + bitmapPosition.y >= 0 && (bitmapPosition.y + blurSize.value) <= image.height + + private fun isBrushTouched(position: Offset): Boolean { + return position.x >= offset.x && position.x <= (offset.x + blurSize.value) && + position.y >= offset.y && position.y <= (offset.y + blurSize.value) + } + + override fun apply() { + val image = ImageBitmap( + editManager.imageSize.width, + editManager.imageSize.height, + ImageBitmapConfig.Argb8888 + ) + editManager.backgroundImage.value?.let { + val canvas = Canvas(image) + canvas.drawImage( + it, + Offset.Zero, + Paint() + ) + canvas.drawImage( + blurredBitmap.asImageBitmap(), + offset, + Paint() + ) + blurs.add(editManager.backgroundImage2.value) + editManager.addBlur() + } + editManager.keepEditedPaths() + editManager.toggleBlurMode() + editManager.backgroundImage.value = image + } + + override fun undo() { + val bitmap = blurs.pop() + redoBlurs.push(editManager.backgroundImage.value) + editManager.backgroundImage.value = bitmap + editManager.redrawEditedPaths() + } + + override fun redo() { + val bitmap = redoBlurs.pop() + blurs.push(editManager.backgroundImage.value) + editManager.backgroundImage.value = bitmap + editManager.keepEditedPaths() + } + + private fun blur(context: Context) { + editManager.apply { + val blurProcessor = HokoBlurBuild(context) + .radius(blurIntensity.value.toInt()) + .processor() + blurredBitmap = + blurProcessor.blur(brushBitmap) + } + } + + companion object { + private const val BRUSH_SIZE = 250 + const val MAX_SIZE = 500f + const val MIN_SIZE = 100f + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/crop/CropAspectRatiosMenu.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/crop/CropAspectRatiosMenu.kt new file mode 100644 index 0000000..cc6ba13 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/crop/CropAspectRatiosMenu.kt @@ -0,0 +1,241 @@ +package dev.arkbuilders.canvas.presentation.edit.crop + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import dev.arkbuilders.canvas.R +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.aspectRatios +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.isChanged +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.isCropFree +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.isCropSquare +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.isCrop_9_16 +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.isCrop_2_3 +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.isCrop_4_5 + +@Composable +fun CropAspectRatiosMenu( + isVisible: Boolean = false, + cropWindow: CropWindow +) { + if (isVisible) { + if (isChanged.value) { + cropWindow.resize() + isChanged.value = false + } + Row( + Modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalArrangement = Arrangement.Center + ) { + Column( + Modifier + .clip(RoundedCornerShape(5.dp)) + .clickable { + switchAspectRatio(isCropFree) + } + ) { + Icon( + modifier = Modifier + .padding(start = 12.dp, end = 12.dp, bottom = 5.dp) + .align(Alignment.CenterHorizontally) + .size(30.dp), + imageVector = + ImageVector.vectorResource(id = R.drawable.ic_crop), + contentDescription = + stringResource(id = R.string.ark_retouch_crop_free), + tint = if (isCropFree.value) + MaterialTheme.colors.primary + else Color.Black + ) + Text( + text = stringResource(R.string.ark_retouch_crop_free), + modifier = Modifier + .align(Alignment.CenterHorizontally), + color = if (isCropFree.value) + MaterialTheme.colors.primary + else Color.Black + ) + } + Column( + Modifier + .clip(RoundedCornerShape(5.dp)) + .clickable { + switchAspectRatio(isCropSquare) + } + ) { + Icon( + modifier = Modifier + .padding(start = 12.dp, end = 12.dp, bottom = 5.dp) + .align(Alignment.CenterHorizontally) + .size(30.dp), + imageVector = + ImageVector.vectorResource(id = R.drawable.ic_crop_square), + contentDescription = + stringResource(id = R.string.ark_retouch_crop_square), + tint = if (isCropSquare.value) + MaterialTheme.colors.primary + else Color.Black + ) + Text( + stringResource(R.string.ark_retouch_crop_square), + modifier = Modifier.align(Alignment.CenterHorizontally), + color = if (isCropSquare.value) + MaterialTheme.colors.primary + else Color.Black + ) + } + Column( + Modifier + .clip(RoundedCornerShape(5.dp)) + .clickable { + switchAspectRatio(isCrop_4_5) + } + ) { + Icon( + modifier = Modifier + .padding( + start = 12.dp, end = 12.dp, + top = 5.dp, bottom = 5.dp + ) + .rotate(90f) + .align(Alignment.CenterHorizontally) + .size(30.dp), + imageVector = + ImageVector.vectorResource(id = R.drawable.ic_crop_5_4), + contentDescription = + stringResource(id = R.string.ark_retouch_crop_4_5), + tint = if (isCrop_4_5.value) + MaterialTheme.colors.primary + else Color.Black + ) + Text( + stringResource(R.string.ark_retouch_crop_4_5), + modifier = Modifier.align(Alignment.CenterHorizontally), + color = if (isCrop_4_5.value) + MaterialTheme.colors.primary + else Color.Black + ) + } + Column( + Modifier + .clip(RoundedCornerShape(5.dp)) + .clickable { + switchAspectRatio(isCrop_9_16) + } + ) { + Icon( + modifier = Modifier + .padding( + start = 12.dp, end = 12.dp, + top = 5.dp, bottom = 5.dp + ) + .rotate(90f) + .align(Alignment.CenterHorizontally) + .size(30.dp), + imageVector = + ImageVector.vectorResource(id = R.drawable.ic_crop_16_9), + contentDescription = + stringResource(id = R.string.ark_retouch_crop_9_16), + tint = if (isCrop_9_16.value) + MaterialTheme.colors.primary + else Color.Black + ) + Text( + stringResource(R.string.ark_retouch_crop_9_16), + modifier = Modifier.align(Alignment.CenterHorizontally), + color = if (isCrop_9_16.value) + MaterialTheme.colors.primary + else Color.Black + ) + } + Column( + Modifier + .clip(RoundedCornerShape(5.dp)) + .clickable { + switchAspectRatio(isCrop_2_3) + } + ) { + Icon( + modifier = Modifier + .padding( + start = 12.dp, end = 12.dp, + top = 5.dp, bottom = 5.dp + ) + .rotate(90f) + .align(Alignment.CenterHorizontally) + .size(30.dp), + imageVector = + ImageVector.vectorResource(id = R.drawable.ic_crop_3_2), + contentDescription = + stringResource(id = R.string.ark_retouch_crop_2_3), + tint = if (isCrop_2_3.value) + MaterialTheme.colors.primary + else Color.Black + ) + Text( + stringResource(R.string.ark_retouch_crop_2_3), + modifier = Modifier.align(Alignment.CenterHorizontally), + color = if (isCrop_2_3.value) + MaterialTheme.colors.primary + else Color.Black + ) + } + } + } else switchAspectRatio(isCropFree) +} + +internal fun switchAspectRatio(selected: MutableState) { + selected.value = true + aspectRatios.filter { + it != selected + }.forEach { + it.value = false + } + isChanged.value = true +} + +internal object AspectRatio { + val isCropFree = mutableStateOf(false) + val isCropSquare = mutableStateOf(false) + val isCrop_4_5 = mutableStateOf(false) + val isCrop_9_16 = mutableStateOf(false) + val isCrop_2_3 = mutableStateOf(false) + val isChanged = mutableStateOf(false) + + val aspectRatios = listOf( + isCropFree, + isCropSquare, + isCrop_4_5, + isCrop_9_16, + isCrop_2_3 + ) + + val CROP_FREE = Offset(0f, 0f) + val CROP_SQUARE = Offset(1f, 1f) + val CROP_4_5 = Offset(4f, 5f) + val CROP_9_16 = Offset(9f, 16f) + val CROP_2_3 = Offset(2f, 3f) +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/crop/CropOperation.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/crop/CropOperation.kt new file mode 100644 index 0000000..15c7c49 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/crop/CropOperation.kt @@ -0,0 +1,53 @@ +package dev.arkbuilders.canvas.presentation.edit.crop + +import androidx.compose.ui.graphics.asImageBitmap +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import dev.arkbuilders.canvas.presentation.edit.Operation +import dev.arkbuilders.canvas.presentation.utils.crop + +class CropOperation( + private val editManager: EditManager +) : Operation { + + override fun apply() { + editManager.apply { + cropWindow.apply { + val image = getBitmap().crop(getCropParams()).asImageBitmap() + backgroundImage.value = image + keepEditedPaths() + addCrop() + saveRotationAfterOtherOperation() + scaleToFit() + toggleCropMode() + } + } + } + + override fun undo() { + editManager.apply { + if (cropStack.isNotEmpty()) { + val image = cropStack.pop() + redoCropStack.push(backgroundImage.value) + backgroundImage.value = image + restoreRotationAfterUndoOtherOperation() + scaleToFit() + redrawEditedPaths() + updateRevised() + } + } + } + + override fun redo() { + editManager.apply { + if (redoCropStack.isNotEmpty()) { + val image = redoCropStack.pop() + cropStack.push(backgroundImage.value) + backgroundImage.value = image + saveRotationAfterOtherOperation() + scaleToFit() + keepEditedPaths() + updateRevised() + } + } + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/crop/CropWindow.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/crop/CropWindow.kt new file mode 100644 index 0000000..613f5d0 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/crop/CropWindow.kt @@ -0,0 +1,443 @@ +package dev.arkbuilders.canvas.presentation.edit.crop + +import android.graphics.Bitmap +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.PaintingStyle +import androidx.compose.ui.unit.IntSize +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.CROP_2_3 +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.CROP_4_5 +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.CROP_9_16 +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.CROP_SQUARE +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.isCropFree +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.isCropSquare +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.isCrop_2_3 +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.isCrop_4_5 +import dev.arkbuilders.canvas.presentation.edit.crop.AspectRatio.isCrop_9_16 +import dev.arkbuilders.canvas.presentation.edit.resize.ResizeOperation +import timber.log.Timber + +class CropWindow(private val editManager: EditManager) { + + private lateinit var bitmap: Bitmap + + private var offset = Offset(0f, 0f) + + private var aspectRatio = 1F + + private var width: Float = MIN_WIDTH + private var height: Float = MIN_HEIGHT + private var cropAreaWidth: Float = MIN_WIDTH + private var cropAreaHeight: Float = MIN_HEIGHT + private lateinit var matrixScale: ResizeOperation.Scale + private lateinit var cropScale: ResizeOperation.Scale + private lateinit var rectScale: ResizeOperation.Scale + + private val drawAreaSize: IntSize + get() { + return editManager.drawAreaSize.value + } + + private lateinit var rect: Rect + + private val isTouchedRight = mutableStateOf(false) + private val isTouchedLeft = mutableStateOf(false) + private val isTouchedTop = mutableStateOf(false) + private val isTouchedBottom = mutableStateOf(false) + private val isTouchedInside = mutableStateOf(false) + private val isTouched = mutableStateOf(false) + private val isTouchedTopLeft = mutableStateOf(false) + private val isTouchedBottomLeft = mutableStateOf(false) + private val isTouchedTopRight = mutableStateOf(false) + private val isTouchedBottomRight = mutableStateOf(false) + private val isTouchedOnCorner = mutableStateOf(false) + + private var delta = Offset( + 0F, + 0F + ) + + private val paint = Paint() + + private var isInitialized = false + + init { + paint.color = Color.LightGray + paint.style = PaintingStyle.Stroke + paint.strokeWidth = 5F + } + + private fun calcMaxDimens() { + width = drawAreaSize.width.toFloat() - HORIZONTAL_OFFSET + height = drawAreaSize.height.toFloat() - VERTICAL_OFFSET + } + + private fun calcOffset() { + val x = ((drawAreaSize.width - cropAreaWidth) / 2f) + .coerceAtLeast(0f) + val y = ((drawAreaSize.height - cropAreaHeight) / 2f) + .coerceAtLeast(0f) + offset = Offset(x, y) + } + + fun updateOnDrawAreaSizeChange(newSize: IntSize) { + reInit() + updateOnOffsetChange() + } + + fun close() { + isInitialized = false + } + + fun init( + bitmap: Bitmap + ) { + if (!isInitialized) { + Timber.tag("crop-window").d("Initialising") + this.bitmap = bitmap + calcMaxDimens() + val viewParams = editManager.scaleToFitOnEdit( + width.toInt(), + height.toInt() + ) + matrixScale = viewParams.scale + cropAreaWidth = viewParams.drawArea.width.toFloat() + cropAreaHeight = viewParams.drawArea.height.toFloat() + cropScale = ResizeOperation.Scale( + bitmap.width / cropAreaWidth, + bitmap.height / cropAreaHeight + ) + calcOffset() + isInitialized = true + } + } + + private fun reInit() { + calcMaxDimens() + val viewParams = editManager.scaleToFitOnEdit( + width.toInt(), + height.toInt() + ) + val prevCropAreaWidth = cropAreaWidth + val prevCropAreaHeight = cropAreaHeight + matrixScale = viewParams.scale + cropAreaWidth = viewParams.drawArea.width.toFloat() + cropAreaHeight = viewParams.drawArea.height.toFloat() + cropScale = ResizeOperation.Scale( + bitmap.width / cropAreaWidth, + bitmap.height / cropAreaHeight + ) + rectScale = ResizeOperation.Scale( + prevCropAreaWidth / cropAreaWidth, + prevCropAreaHeight / cropAreaHeight + ) + } + + private fun updateOnOffsetChange() { + val leftMove = rect.left - offset.x + val topMove = rect.top - offset.y + calcOffset() + val newLeft = offset.x + (leftMove / rectScale.x) + val newTop = offset.y + (topMove / rectScale.y) + val newRight = newLeft + (rect.width / rectScale.x) + val newBottom = newTop + (rect.height / rectScale.y) + create( + newLeft, + newTop, + newRight, + newBottom + ) + } + + fun show(canvas: Canvas) { + if (isInitialized) { + update() + } + draw(canvas) + } + + fun setDelta(delta: Offset) { + this.delta = delta + } + + private fun isAspectRatioFixed() = + isCropSquare.value || isCrop_4_5.value || + isCrop_9_16.value || isCrop_2_3.value + + private fun update() { + var left = rect.left + var right = rect.right + var top = rect.top + var bottom = rect.bottom + + if (isAspectRatioFixed()) { + if (isTouchedOnCorner.value) { + if (isTouchedTopLeft.value) { + left = rect.left + delta.x + top = rect.top + delta.y + } + if (isTouchedTopRight.value) { + right = rect.right + delta.x + top = rect.top - delta.y + } + if (isTouchedBottomLeft.value) { + left = rect.left + delta.x + bottom = rect.bottom - delta.y + } + if (isTouchedBottomRight.value) { + right = rect.right + delta.x + bottom = rect.bottom + delta.y + } + val newHeight = (right - left) * aspectRatio + if (isTouchedTopLeft.value || isTouchedTopRight.value) + top = bottom - newHeight + if (isTouchedBottomLeft.value || isTouchedBottomRight.value) + bottom = top + newHeight + } else { + if (isTouchedLeft.value) { + left = rect.left + delta.x + top = rect.top + ((delta.x * aspectRatio) / 2f) + bottom = rect.bottom - ((delta.x * aspectRatio) / 2f) + } + if (isTouchedRight.value) { + right = rect.right + delta.x + top = rect.top - ((delta.x * aspectRatio) / 2f) + bottom = rect.bottom + ((delta.x * aspectRatio) / 2f) + } + if (isTouchedTop.value) { + top = rect.top + delta.y + left = rect.left + ((delta.y * (1f / aspectRatio)) / 2f) + right = rect.right - ((delta.y * (1f / aspectRatio)) / 2f) + } + if (isTouchedBottom.value) { + bottom = rect.bottom + delta.y + left = rect.left - ((delta.y * (1f / aspectRatio)) / 2f) + right = rect.right + ((delta.y * (1f / aspectRatio)) / 2f) + } + } + } else { + left = if (isTouchedLeft.value) + rect.left + delta.x + else rect.left + right = if (isTouchedRight.value) + rect.right + delta.x + else rect.right + top = if (isTouchedTop.value) + rect.top + delta.y + else rect.top + bottom = if (isTouchedBottom.value) + rect.bottom + delta.y + else rect.bottom + } + + fun isNotMinSize() = (right - left) >= MIN_WIDTH && + (bottom - top) >= MIN_HEIGHT + + fun isNotMaxSize() = (right - left) <= cropAreaWidth && + (bottom - top) <= cropAreaHeight + + fun isWithinBounds() = left >= offset.x && + right <= offset.x + cropAreaWidth && + top >= offset.y && + bottom <= offset.y + cropAreaHeight + + if (isTouchedInside.value) { + right += delta.x + left += delta.x + top += delta.y + bottom += delta.y + if (left < offset.x) { + left = offset.x + right = left + rect.width + } + if (right > offset.x + cropAreaWidth) { + right = offset.x + cropAreaWidth + left = right - rect.width + } + if (top < offset.y) { + top = offset.y + bottom = top + rect.height + } + if (bottom > offset.y + cropAreaHeight) { + bottom = offset.y + cropAreaHeight + top = bottom - rect.height + } + } + + if (isNotMaxSize() && isNotMinSize() && isWithinBounds()) { + create( + left, + top, + right, + bottom + ) + } + } + + fun detectTouchedSide(eventPoint: Offset) { + isTouchedLeft.value = eventPoint.x >= + (rect.left - SIDE_DETECTOR_TOLERANCE) && + eventPoint.x <= (rect.left + SIDE_DETECTOR_TOLERANCE) + isTouchedRight.value = eventPoint.x >= + (rect.right - SIDE_DETECTOR_TOLERANCE) && + eventPoint.x <= (rect.right + SIDE_DETECTOR_TOLERANCE) + isTouchedTop.value = eventPoint.y >= + (rect.top - SIDE_DETECTOR_TOLERANCE) && + eventPoint.y <= (rect.top + SIDE_DETECTOR_TOLERANCE) + isTouchedBottom.value = eventPoint.y >= + (rect.bottom - SIDE_DETECTOR_TOLERANCE) && + eventPoint.y <= (rect.bottom + SIDE_DETECTOR_TOLERANCE) + isTouchedInside.value = eventPoint.x >= + rect.left + SIDE_DETECTOR_TOLERANCE && + eventPoint.x <= rect.right - SIDE_DETECTOR_TOLERANCE && + eventPoint.y >= rect.top + SIDE_DETECTOR_TOLERANCE && + eventPoint.y <= rect.bottom - SIDE_DETECTOR_TOLERANCE + isTouchedTopLeft.value = isTouchedLeft.value && isTouchedTop.value + isTouchedTopRight.value = isTouchedTop.value && isTouchedRight.value + isTouchedBottomLeft.value = isTouchedBottom.value && isTouchedLeft.value + isTouchedBottomRight.value = isTouchedBottom.value && isTouchedRight.value + isTouchedOnCorner.value = isTouchedTopLeft.value || + isTouchedTopRight.value || isTouchedBottomLeft.value || + isTouchedBottomRight.value + isTouched.value = isTouchedLeft.value || isTouchedRight.value || + isTouchedTop.value || isTouchedBottom.value || + isTouchedInside.value + } + + fun resize() { + if (isInitialized) { + resizeByAspectRatio() + } + } + + private fun resizeByBitmap() { + val newBottom = cropAreaHeight + offset.y + val newRight = cropAreaWidth + offset.x + create( + offset.x, + offset.y, + newRight, + newBottom + ) + } + + private fun resizeByAspectRatio() { + if (isCropFree.value) { + resizeByBitmap() + } else { + when { + isCropSquare.value -> + aspectRatio = CROP_SQUARE.y / CROP_SQUARE.x + isCrop_4_5.value -> + aspectRatio = CROP_4_5.y / CROP_4_5.x + isCrop_9_16.value -> + aspectRatio = CROP_9_16.y / CROP_9_16.x + isCrop_2_3.value -> + aspectRatio = CROP_2_3.y / CROP_2_3.x + } + computeSize() + } + } + + private fun create( + newLeft: Float, + newTop: Float, + newRight: Float, + newBottom: Float + ) { + rect = Rect( + newLeft, + newTop, + newRight, + newBottom + ) + } + + private fun computeSize() { + var newWidth = cropAreaWidth + var newHeight = cropAreaWidth * aspectRatio + var newLeft = offset.x + var newTop = offset.y + + (cropAreaHeight - newHeight) / 2f + var newRight = newLeft + newWidth + var newBottom = newTop + newHeight + + if (newHeight > cropAreaHeight) { + newHeight = cropAreaHeight + newWidth = newHeight / aspectRatio + newLeft = offset.x + ( + cropAreaWidth - newWidth + ) / 2f + newTop = offset.y + newRight = newLeft + newWidth + newBottom = newTop + newHeight + } + + create( + newLeft, + newTop, + newRight, + newBottom + ) + } + + private fun draw(canvas: Canvas) { + canvas.drawRect( + rect, + paint + ) + } + + fun getCropParams(): CropParams { + val x = ((rect.left - offset.x) * cropScale.x).toInt() + val y = ((rect.top - offset.y) * cropScale.y).toInt() + val width = (rect.width * cropScale.x).toInt() + val height = (rect.height * cropScale.y).toInt() + return CropParams.create( + x, + y, + width, + height + ) + } + + fun getBitmap() = bitmap + + companion object { + private const val HORIZONTAL_OFFSET = 150F + private const val VERTICAL_OFFSET = 220F + private const val SIDE_DETECTOR_TOLERANCE = 50 + private const val MIN_WIDTH = 150F + private const val MIN_HEIGHT = 150F + + fun computeDeltaX(initialX: Float, currentX: Float) = currentX - initialX + + fun computeDeltaY(initialY: Float, currentY: Float) = currentY - initialY + } + + class CropParams private constructor( + val x: Int, + val y: Int, + val width: Int, + val height: Int + ) { + companion object { + fun create( + x: Int, + y: Int, + width: Int, + height: Int + ) = CropParams( + x, + y, + width, + height + ) + } + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/draw/DrawOperation.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/draw/DrawOperation.kt new file mode 100644 index 0000000..34b7128 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/draw/DrawOperation.kt @@ -0,0 +1,37 @@ +package dev.arkbuilders.canvas.presentation.edit.draw + +import androidx.compose.ui.graphics.Path +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import dev.arkbuilders.canvas.presentation.edit.Operation + +class DrawOperation(private val editManager: EditManager) : Operation { + private var path = Path() + + override fun apply() { + editManager.addDrawPath(path) + } + + override fun undo() { + editManager.apply { + if (drawPaths.isNotEmpty()) { + redoPaths.push(drawPaths.pop()) + updateRevised() + return + } + } + } + + override fun redo() { + editManager.apply { + if (redoPaths.isNotEmpty()) { + drawPaths.push(redoPaths.pop()) + updateRevised() + return + } + } + } + + fun draw(path: Path) { + this.path = path + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/resize/ResizeInput.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/resize/ResizeInput.kt new file mode 100644 index 0000000..e7b9fe6 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/resize/ResizeInput.kt @@ -0,0 +1,201 @@ +package dev.arkbuilders.canvas.presentation.edit.resize + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.text.isDigitsOnly +import dev.arkbuilders.canvas.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import dev.arkbuilders.canvas.presentation.drawing.EditManager + +@Composable +fun ResizeInput(isVisible: Boolean, editManager: EditManager) { + if (isVisible) { + var width by rememberSaveable { + mutableStateOf( + editManager.imageSize.width.toString() + ) + } + + var height by rememberSaveable { + mutableStateOf( + editManager.imageSize.height.toString() + ) + } + + val widthHint = stringResource( + R.string.width_too_large, + editManager.imageSize.width + ) + val digitsHint = stringResource(R.string.digits_only) + val heightHint = stringResource( + R.string.height_too_large, + editManager.imageSize.height + ) + var hint by remember { + mutableStateOf("") + } + var showHint by remember { + mutableStateOf(false) + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Hint( + hint, + isVisible = { + delayHidingHint(it) { + showHint = false + } + showHint + } + ) + Row { + TextField( + modifier = Modifier.fillMaxWidth(0.5f), + value = width, + onValueChange = { + if ( + it.isNotEmpty() && + it.isDigitsOnly() && + it.toInt() > editManager.imageSize.width + ) { + hint = widthHint + showHint = true + return@TextField + } + if (it.isNotEmpty() && !it.isDigitsOnly()) { + hint = digitsHint + showHint = true + return@TextField + } + width = it + showHint = false + if (width.isEmpty()) height = width + if (width.isNotEmpty() && width.isDigitsOnly()) { + height = editManager.resizeDown(width = width.toInt()) + .height.toString() + } + }, + label = { + Text( + stringResource(R.string.width), + modifier = Modifier + .fillMaxWidth(), + color = MaterialTheme.colors.primary, + textAlign = TextAlign.Center + ) + }, + textStyle = TextStyle( + color = Color.DarkGray, + textAlign = TextAlign.Center + ), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number + ) + ) + TextField( + modifier = Modifier.fillMaxWidth(), + value = height, + onValueChange = { + if ( + it.isNotEmpty() && + it.isDigitsOnly() && + it.toInt() > editManager.imageSize.height + ) { + hint = heightHint + showHint = true + return@TextField + } + if (it.isNotEmpty() && !it.isDigitsOnly()) { + hint = digitsHint + showHint = true + return@TextField + } + height = it + showHint = false + if (height.isEmpty()) width = height + if (height.isNotEmpty() && height.isDigitsOnly()) { + width = editManager.resizeDown(height = height.toInt()) + .width.toString() + } + }, + label = { + Text( + stringResource(R.string.height), + modifier = Modifier + .fillMaxWidth(), + color = MaterialTheme.colors.primary, + textAlign = TextAlign.Center + ) + }, + textStyle = TextStyle( + color = Color.DarkGray, + textAlign = TextAlign.Center + ), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number + ) + ) + } + } + } +} + +fun delayHidingHint(scope: CoroutineScope, hide: () -> Unit) { + scope.launch { + delay(1000) + hide() + } +} + +@Composable +fun Hint(text: String, isVisible: (CoroutineScope) -> Boolean) { + val scope = rememberCoroutineScope() + AnimatedVisibility( + visible = isVisible(scope), + enter = fadeIn(), + exit = fadeOut(tween(durationMillis = 500, delayMillis = 1000)), + modifier = Modifier + .wrapContentSize() + .background(Color.LightGray, RoundedCornerShape(10)) + ) { + Text( + text, + Modifier + .padding(12.dp) + ) + } +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/resize/ResizeOperation.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/resize/ResizeOperation.kt new file mode 100644 index 0000000..8b96f67 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/resize/ResizeOperation.kt @@ -0,0 +1,114 @@ +package dev.arkbuilders.canvas.presentation.edit.resize + +import android.graphics.Bitmap +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.unit.IntSize +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import dev.arkbuilders.canvas.presentation.edit.Operation +import dev.arkbuilders.canvas.presentation.utils.resize +import java.lang.NullPointerException + +class ResizeOperation(private val editManager: EditManager) : Operation { + + private lateinit var bitmap: Bitmap + private var aspectRatio = 1f + private lateinit var editMatrixScale: Scale + private val isApplied = mutableStateOf(false) + + override fun apply() { + editManager.apply { + addResize() + saveRotationAfterOtherOperation() + scaleToFit() + toggleResizeMode() + editMatrix.reset() + isApplied.value = true + } + } + + override fun undo() { + editManager.apply { + if (resizes.isNotEmpty()) { + redoResize.push(backgroundImage.value) + backgroundImage.value = resizes.pop() + restoreRotationAfterUndoOtherOperation() + scaleToFit() + redrawEditedPaths() + } + } + } + + override fun redo() { + editManager.apply { + if (redoResize.isNotEmpty()) { + resizes.push(backgroundImage.value) + saveRotationAfterOtherOperation() + backgroundImage.value = redoResize.pop() + scaleToFit() + keepEditedPaths() + } + } + } + + fun init(bitmap: Bitmap) { + this.bitmap = bitmap + aspectRatio = bitmap.width.toFloat() / bitmap.height.toFloat() + editMatrixScale = editManager.scaleToFitOnEdit().scale + isApplied.value = false + } + + fun updateEditMatrixScale(scale: Scale) { + editMatrixScale = scale + } + + fun isApplied() = isApplied.value + + fun resetApply() { isApplied.value = false } + + fun resizeDown( + width: Int, + height: Int, + updateImage: (ImageBitmap) -> Unit + ): IntSize { + return try { + var newWidth = width + var newHeight = height + if (width > 0) newHeight = ( + newWidth / + aspectRatio + ).toInt() + if (height > 0) + newWidth = (newHeight * aspectRatio).toInt() + if (newWidth > 0 && newHeight > 0) editManager.apply { + if ( + newWidth <= bitmap.width && + newHeight <= bitmap.height + ) { + val sx = newWidth.toFloat() / bitmap.width.toFloat() + val sy = newHeight.toFloat() / bitmap.height.toFloat() + val downScale = Scale(sx, sy) + val imgBitmap = bitmap.resize(downScale).asImageBitmap() + val drawWidth = imgBitmap.width * editMatrixScale.x + val drawHeight = imgBitmap.height * editMatrixScale.y + val drawArea = IntSize(drawWidth.toInt(), drawHeight.toInt()) + updateAvailableDrawArea(drawArea) + updateImage(imgBitmap) + } + } + IntSize( + newWidth, + newHeight + ) + } catch (e: NullPointerException) { + e.printStackTrace() + IntSize.Zero + } + } + + data class Scale( + val x: Float = 1f, + val y: Float = 1f + ) +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/rotate/RotateOperation.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/rotate/RotateOperation.kt new file mode 100644 index 0000000..0bffbb3 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/rotate/RotateOperation.kt @@ -0,0 +1,47 @@ +package dev.arkbuilders.canvas.presentation.edit.rotate + +import android.graphics.Matrix +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import dev.arkbuilders.canvas.presentation.edit.Operation +import dev.arkbuilders.canvas.presentation.utils.rotate + +class RotateOperation(private val editManager: EditManager) : Operation { + + override fun apply() { + editManager.apply { + toggleRotateMode() + matrix.set(editMatrix) + editMatrix.reset() + addRotation() + } + } + + override fun undo() { + editManager.apply { + if (rotationAngles.isNotEmpty()) { + redoRotationAngles.push(prevRotationAngle) + prevRotationAngle = rotationAngles.pop() + scaleToFit() + } + } + } + + override fun redo() { + editManager.apply { + if (redoRotationAngles.isNotEmpty()) { + rotationAngles.push(prevRotationAngle) + prevRotationAngle = redoRotationAngles.pop() + scaleToFit() + } + } + } + + fun rotate(matrix: Matrix, angle: Float, px: Float, py: Float) { + matrix.rotate(angle, Center(px, py)) + } + + data class Center( + val x: Float, + val y: Float + ) +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Color.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Color.kt new file mode 100644 index 0000000..5f737ae --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Color.kt @@ -0,0 +1,22 @@ +package dev.arkbuilders.canvas.presentation.graphics + +import dev.arkbuilders.canvas.presentation.graphics.ColorCode + +enum class Color(val code: Int, val value: String) { + + BLACK(ColorCode.black, "black"), + + GRAY(ColorCode.gray, "gray"), + + RED(ColorCode.red, "red"), + + ORANGE(ColorCode.orange, "orange"), + + GREEN(ColorCode.green, "green"), + + BLUE(ColorCode.blue, "blue"), + + PURPLE(ColorCode.purple, "purple"), + + WHITE(ColorCode.white, "white") +} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/ColorCode.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/ColorCode.kt new file mode 100644 index 0000000..bc9a1a0 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/ColorCode.kt @@ -0,0 +1,13 @@ +package dev.arkbuilders.canvas.presentation.graphics + +internal object ColorCode { + val black by lazy { android.graphics.Color.parseColor("#000000") } + val gray by lazy { android.graphics.Color.parseColor("#667085") } + val red by lazy { android.graphics.Color.parseColor("#F04438") } + val orange by lazy { android.graphics.Color.parseColor("#F79009") } + val green by lazy { android.graphics.Color.parseColor("#17B26A") } + val blue by lazy { android.graphics.Color.parseColor("#0BA5EC") } + val purple by lazy { android.graphics.Color.parseColor("#7A5AF8") } + val white by lazy { android.graphics.Color.parseColor("#FFFFFF") } + val brown by lazy { android.graphics.Color.parseColor("#B54708") } +} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/SVG.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/SVG.kt new file mode 100644 index 0000000..c92b1b9 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/SVG.kt @@ -0,0 +1,303 @@ +package dev.arkbuilders.canvas.presentation.graphics + +import android.graphics.Paint +import android.util.Log +import android.graphics.Path as AndroidDrawPath +import android.util.Xml +import dev.arkbuilders.canvas.presentation.utils.DrawPath +import dev.arkbuilders.canvas.presentation.utils.getColorCode +import dev.arkbuilders.canvas.presentation.utils.getStrokeSize +import org.xmlpull.v1.XmlPullParser +import java.nio.file.Path +import kotlin.io.path.reader +import kotlin.io.path.writer + +class SVG { + private var strokeColor = Color.BLACK.value + private var strokeSize: Int = Size.TINY.id + private var fill = "none" + private var viewBox = ViewBox() + private val commands = ArrayDeque() + private val paths = ArrayDeque() + + private val paint + get() = Paint().also { + it.color = strokeColor.getColorCode() + it.style = Paint.Style.STROKE + it.strokeWidth = strokeSize.getStrokeSize() + it.strokeCap = Paint.Cap.ROUND + it.strokeJoin = Paint.Join.ROUND + it.isAntiAlias = true + } + + fun addCommand(command: SVGCommand) { + commands.addLast(command) + } + + fun addPath(path: DrawPath) { + paths.addLast(path) + } + + fun generate(path: Path) { + if (commands.isNotEmpty()) { + val xmlSerializer = Xml.newSerializer() + val pathData = commands.joinToString() + xmlSerializer.apply { + setOutput(path.writer()) + startDocument("utf-8", false) + startTag("", SVG_TAG) + attribute("", Attributes.VIEW_BOX, viewBox.toString()) + attribute("", Attributes.XML_NS_URI, XML_NS_URI) + startTag("", PATH_TAG) + attribute("", Attributes.Path.STROKE, strokeColor) + attribute("", Attributes.Path.FILL, fill) + attribute("", Attributes.Path.DATA, pathData) + endTag("", PATH_TAG) + endTag("", SVG_TAG) + endDocument() + } + } + } + + fun getPaths(): Collection = paths + + fun copy(): SVG = SVG().apply { + strokeColor = this@SVG.strokeColor + fill = this@SVG.fill + viewBox = this@SVG.viewBox + commands.addAll(this@SVG.commands) + paths.addAll(this@SVG.paths) + } + + private fun createCanvasPaths() { + if (commands.isNotEmpty()) { + if (paths.isNotEmpty()) paths.clear() + var path = AndroidDrawPath() + commands.forEach { command -> + strokeColor = command.paintColor + strokeSize = command.brushSizeId + when (command) { + is SVGCommand.MoveTo -> { + path = AndroidDrawPath() + path.moveTo(command.x, command.y) + } + + is SVGCommand.AbsQuadTo -> { + path.quadTo(command.x1, command.y1, command.x2, command.y2) + } + + is SVGCommand.AbsLineTo -> { + path.lineTo(command.x, command.y) + } + } + paths.addLast(DrawPath(path, paint.apply { + color = strokeColor.getColorCode() + strokeWidth = strokeSize.getStrokeSize() + })) + } + } + } + + companion object { + fun parse(path: Path): SVG = SVG().apply { + val xmlParser = Xml.newPullParser() + var pathData = "" + + xmlParser.apply { + setInput(path.reader()) + + var event = xmlParser.eventType + var pathCount = 0 + while (event != XmlPullParser.END_DOCUMENT) { + val tag = xmlParser.name + when (event) { + XmlPullParser.START_TAG -> { + when (tag) { + SVG_TAG -> { + viewBox = ViewBox.fromString( + getAttributeValue("", Attributes.VIEW_BOX) + ) + } + PATH_TAG -> { + pathCount += 1 + strokeColor = getAttributeValue("", Attributes.Path.STROKE) + fill = getAttributeValue("", Attributes.Path.FILL) + pathData = getAttributeValue("", Attributes.Path.DATA) + } + } + if (pathCount > 1) { + Log.d("svg", "found more than 1 path in file") + break + } + } + } + + event = next() + } + + pathData.split(COMMA).forEach { + val command = it.trim() + if (command.isEmpty()) return@forEach + val commandElements = command.split(" ") + when (command.first()) { + SVGCommand.MoveTo.CODE -> { + if (commandElements.size > 3) { + strokeColor = commandElements[3] + } + if (commandElements.size > 4) { + strokeSize = commandElements[4].toInt() + } + commands.addLast(SVGCommand.MoveTo.fromString(command).apply { + paintColor = strokeColor + brushSizeId = strokeSize + }) + } + SVGCommand.AbsLineTo.CODE -> { + if (commandElements.size > 3) { + strokeColor = commandElements[3] + } + if (commandElements.size > 4) { + strokeSize = commandElements[4].toInt() + } + commands.addLast(SVGCommand.MoveTo.fromString(command).apply { + paintColor = strokeColor + brushSizeId = strokeSize + }) + } + SVGCommand.AbsQuadTo.CODE -> { + if (commandElements.size > 5) { + strokeColor = commandElements[5] + } + if (commandElements.size > 6) { + strokeSize = commandElements[6].toInt() + } + commands.addLast(SVGCommand.AbsQuadTo.fromString(command).apply { + paintColor = strokeColor + brushSizeId = strokeSize + }) + } + else -> {} + } + } + + createCanvasPaths() + } + } + + private object Attributes { + const val VIEW_BOX = "viewBox" + const val XML_NS_URI = "xmlns" + + object Path { + const val STROKE = "stroke" + const val FILL = "fill" + const val DATA = "d" + } + } + } +} + +data class ViewBox( + val x: Float = 0f, + val y: Float = 0f, + val width: Float = 100f, + val height: Float = 100f +) { + override fun toString(): String = "$x $y $width $height" + + companion object { + fun fromString(string: String): ViewBox { + val viewBox = string.split(" ") + return ViewBox( + viewBox[0].toFloat(), + viewBox[1].toFloat(), + viewBox[2].toFloat(), + viewBox[3].toFloat() + ) + } + } +} + +sealed class SVGCommand { + + var paintColor = Color.BLACK.value + var brushSizeId = Size.TINY.id + + class MoveTo( + val x: Float, + val y: Float + ) : SVGCommand() { + override fun toString(): String = "$CODE $x $y $paintColor $brushSizeId" + + companion object { + const val CODE = 'M' + + fun fromString(string: String): SVGCommand { + val params = string.removePrefix("$CODE").trim().split(" ") + val x = params[0].toFloat() + val y = params[1].toFloat() + val colorCode = if (params.size > 2) params[2] else Color.BLACK.value + val strokeSizeId = if (params.size > 3) params[3].toInt() else Size.TINY.id + return MoveTo(x, y).apply { + paintColor = colorCode + brushSizeId = strokeSizeId + } + } + } + } + + class AbsLineTo( + val x: Float, + val y: Float + ) : SVGCommand() { + override fun toString(): String = "$CODE $x $y $paintColor $brushSizeId" + + companion object { + const val CODE = 'L' + + fun fromString(string: String): SVGCommand { + val params = string.removePrefix("$CODE").trim().split(" ") + val x = params[0].toFloat() + val y = params[1].toFloat() + val colorCode = if (params.size > 2) params[2] else Color.BLACK.value + val strokeSizeId = if (params.size > 3) params[3].toInt() else Size.TINY.id + return AbsLineTo(x, y).apply { + paintColor = colorCode + brushSizeId = strokeSizeId + } + } + } + } + + class AbsQuadTo( + val x1: Float, + val y1: Float, + val x2: Float, + val y2: Float + ) : SVGCommand() { + override fun toString(): String = "$CODE $x1 $y1 $x2 $y2 $paintColor $brushSizeId" + + companion object { + const val CODE = 'Q' + + fun fromString(string: String): SVGCommand { + val params = string.removePrefix("$CODE").trim().split(" ") + val x1 = params[0].toFloat() + val y1 = params[1].toFloat() + val x2 = params[2].toFloat() + val y2 = params[3].toFloat() + val colorCode = if (params.size > 4) params[4] else Color.BLACK.value + val strokeSizeId = if (params.size > 5) params[5].toInt() else Size.TINY.id + return AbsQuadTo(x1, y1, x2, y2).apply { + paintColor = colorCode + brushSizeId = strokeSizeId + } + } + } + } +} + +private const val COMMA = "," +private const val XML_NS_URI = "http://www.w3.org/2000/svg" +private const val SVG_TAG = "svg" +private const val PATH_TAG = "path" \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Size.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Size.kt new file mode 100644 index 0000000..43c67ce --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Size.kt @@ -0,0 +1,14 @@ +package dev.arkbuilders.canvas.presentation.graphics + +enum class Size(val id: Int, val value: Float) { + + TINY(0, 5f), + + SMALL(1, 10f), + + MEDIUM(2, 15f), + + LARGE(3, 20f), + + HUGE(4, 25f) +} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/picker/FilePickerScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/picker/FilePickerScreen.kt new file mode 100644 index 0000000..cc7b3d6 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/picker/FilePickerScreen.kt @@ -0,0 +1,155 @@ +package dev.arkbuilders.canvas.presentation.picker + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.fragment.app.FragmentManager +import dev.arkbuilders.canvas.R +import dev.arkbuilders.canvas.presentation.data.Resolution +import dev.arkbuilders.canvas.presentation.utils.askWritePermissions +import dev.arkbuilders.canvas.presentation.utils.isWritePermGranted +import dev.arkbuilders.canvas.presentation.theme.Purple500 +import dev.arkbuilders.canvas.presentation.theme.Purple700 +import dev.arkbuilders.components.filepicker.ArkFilePickerConfig +import dev.arkbuilders.components.filepicker.ArkFilePickerFragment +import dev.arkbuilders.components.filepicker.ArkFilePickerMode +import dev.arkbuilders.components.filepicker.onArkPathPicked +import java.nio.file.Path + +@Composable +fun PickerScreen( + fragmentManager: FragmentManager, + onNavigateToEdit: (Path?, Resolution) -> Unit +) { + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + var size by remember { mutableStateOf(IntSize.Zero) } + var screenSize by remember { mutableStateOf(IntSize.Zero) } + + Column( + modifier = Modifier + .fillMaxSize() + .onSizeChanged { + screenSize = it + } + ) { + Column( + modifier = Modifier + .weight(2f) + .fillMaxWidth() + .padding(20.dp) + .onSizeChanged { + size = it + } + .clip(RoundedCornerShape(10)) + .background(Purple500) + .clickable { + if (!context.isWritePermGranted()) { + context.askWritePermissions() + return@clickable + } + + ArkFilePickerFragment + .newInstance(imageFilePickerConfig()) + .show(fragmentManager, null) + fragmentManager.onArkPathPicked(lifecycleOwner) { + onNavigateToEdit(it, Resolution.fromIntSize(screenSize)) + } + } + .border(2.dp, Purple700, shape = RoundedCornerShape(10)), + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.open), + fontSize = 24.sp, + color = Color.White + ) + Icon( + modifier = Modifier.size(size.height.toDp() / 2), + imageVector = ImageVector.vectorResource(R.drawable.ic_insert_photo), + tint = Color.White, + contentDescription = null + ) + } + Text( + modifier = Modifier + .align(Alignment.CenterHorizontally), + text = stringResource(R.string.or), + fontSize = 24.sp + ) + Column( + Modifier + .weight(2f) + .fillMaxWidth() + .padding(20.dp) + .clip(RoundedCornerShape(10)) + .background(Purple500) + .fillMaxWidth() + .clickable { + onNavigateToEdit(null, Resolution.fromIntSize(screenSize)) + } + .border(2.dp, Purple700, shape = RoundedCornerShape(10)), + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally + + ) { + Text( + text = stringResource(R.string.new_), + fontSize = 24.sp, + color = Color.White + ) + Icon( + modifier = Modifier + .size(size.height.toDp() / 2), + imageVector = ImageVector.vectorResource(R.drawable.ic_add), + tint = Color.White, + contentDescription = null + ) + } + } +} + +fun imageFilePickerConfig(initPath: Path? = null) = ArkFilePickerConfig( + mode = ArkFilePickerMode.FILE, + initialPath = initPath, +) + +@Composable +fun Int.toDp() = with(LocalDensity.current) { + this@toDp.toDp() +} + +@Composable +fun Dp.toPx() = with(LocalDensity.current) { + this@toPx.toPx() +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceLoader.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceLoader.kt new file mode 100644 index 0000000..2aafabb --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceLoader.kt @@ -0,0 +1,60 @@ +package dev.arkbuilders.canvas.presentation.resourceloader + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import com.bumptech.glide.Glide +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import java.nio.file.Path + +class BitmapResourceLoader( + val context: Context, + val editManager: EditManager +) : CanvasResourceLoader { + + private val glideBuilder = Glide + .with(context) + .asBitmap() + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + + private lateinit var bitMapResource: ImageBitmap + override suspend fun loadResourceInto(path: Path, editManager: EditManager) { + loadImage(path) + } + override suspend fun getResource() { + + } + + private fun loadImage( + resourcePath: Path, + ) { + glideBuilder + .load(resourcePath.toFile()) + .loadInto() + } + + + private fun RequestBuilder.loadInto() { + into(object : CustomTarget() { + override fun onResourceReady( + bitmap: Bitmap, + transition: Transition? + ) { + editManager.apply { + backgroundImage.value = bitmap.asImageBitmap() + setOriginalBackgroundImage(backgroundImage.value) + scaleToFit() + } + } + + override fun onLoadCleared(placeholder: Drawable?) {} + }) + } +} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/CanvasResourceLoader.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/CanvasResourceLoader.kt new file mode 100644 index 0000000..e28713c --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/CanvasResourceLoader.kt @@ -0,0 +1,9 @@ +package dev.arkbuilders.canvas.presentation.resourceloader + +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import java.nio.file.Path + +interface CanvasResourceLoader { + suspend fun loadResourceInto(path: Path, editManager: EditManager) + suspend fun getResource() +} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceLoader.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceLoader.kt new file mode 100644 index 0000000..d7b6911 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceLoader.kt @@ -0,0 +1,15 @@ +package dev.arkbuilders.canvas.presentation.resourceloader + +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import dev.arkbuilders.canvas.presentation.resourceloader.CanvasResourceLoader +import java.nio.file.Path + +class SvgResourceLoader: CanvasResourceLoader { + + override suspend fun loadResourceInto(path: Path, editManager: EditManager) { + + } + override suspend fun getResource() { + + } +} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Color.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Color.kt new file mode 100644 index 0000000..2ea42c9 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Color.kt @@ -0,0 +1,9 @@ +package dev.arkbuilders.canvas.presentation.theme + +import androidx.compose.ui.graphics.Color + +val Purple200 = Color(0xFFBB86FC) +val Purple500 = Color(0xFF6200EE) +val Purple700 = Color(0xFF3700B3) +val Teal200 = Color(0xFF03DAC5) +val Gray = Color(0xFFD3D3D3) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Shape.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Shape.kt new file mode 100644 index 0000000..42b3876 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Shape.kt @@ -0,0 +1,11 @@ +package dev.arkbuilders.canvas.presentation.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) +) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Theme.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Theme.kt new file mode 100644 index 0000000..0ea4d42 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Theme.kt @@ -0,0 +1,47 @@ +package dev.arkbuilders.canvas.presentation.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable + +private val DarkColorPalette = darkColors( + primary = Purple200, + primaryVariant = Purple700, + secondary = Teal200 +) + +private val LightColorPalette = lightColors( + primary = Purple500, + primaryVariant = Purple700, + secondary = Teal200 + + /* Other default colors to override + background = Color.White, + surface = Color.White, + onPrimary = Color.White, + onSecondary = Color.Black, + onBackground = Color.Black, + onSurface = Color.Black, + */ +) + +@Composable +fun ARKRetouchTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colors = colors, + typography = Typography, + shapes = Shapes, + content = content + ) +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Type.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Type.kt new file mode 100644 index 0000000..e7056de --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/theme/Type.kt @@ -0,0 +1,28 @@ +package dev.arkbuilders.canvas.presentation.theme + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + body1 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ) + /* Other default text styles to override + button = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W500, + fontSize = 14.sp + ), + caption = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp + ) + */ +) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/GraphicBrushExt.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/GraphicBrushExt.kt new file mode 100644 index 0000000..7c2599a --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/GraphicBrushExt.kt @@ -0,0 +1,90 @@ +package dev.arkbuilders.canvas.presentation.utils + +import dev.arkbuilders.canvas.presentation.graphics.Color +import dev.arkbuilders.canvas.presentation.graphics.Size +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColor +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorBlack +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorBlue +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorGreen +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorGrey +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorOrange +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorPurple +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorRed +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushSize +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushSizeHuge +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushSizeLarge +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushSizeMedium +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushSizeSmall +import dev.arkbuilders.canvas.presentation.utils.adapters.BrushSizeTiny + +fun Int.getStrokeSize(): Float { + return when(this) { + Size.TINY.id -> Size.TINY.value + Size.SMALL.id -> Size.SMALL.value + Size.MEDIUM.id -> Size.MEDIUM.value + Size.LARGE.id -> Size.LARGE.value + Size.HUGE.id -> Size.HUGE.value + else -> Size.TINY.value + } +} + +fun Float.getBrushSizeId(): Int { + return when(this) { + Size.TINY.value -> Size.TINY.id + Size.SMALL.value -> Size.SMALL.id + Size.MEDIUM.value -> Size.MEDIUM.id + Size.LARGE.value -> Size.LARGE.id + Size.HUGE.value -> Size.HUGE.id + else -> { Size.TINY.id } + } +} + +fun Int.getStrokeColor(): String { + return when (this) { + Color.BLACK.code -> Color.BLACK.value + Color.GRAY.code -> Color.GRAY.value + Color.RED.code -> Color.RED.value + Color.GREEN.code -> Color.GREEN.value + Color.BLUE.code -> Color.BLUE.value + Color.PURPLE.code -> Color.PURPLE.value + Color.ORANGE.code -> Color.ORANGE.value + Color.WHITE.code -> Color.WHITE.value + else -> Color.BLACK.value + } +} + +fun String.getColorCode(): Int { + return when (this) { + Color.BLACK.value -> Color.BLACK.code + Color.GRAY.value -> Color.GRAY.code + Color.RED.value -> Color.RED.code + Color.GREEN.value -> Color.GREEN.code + Color.BLUE.value -> Color.BLUE.code + Color.PURPLE.value -> Color.PURPLE.code + Color.ORANGE.value -> Color.ORANGE.code + Color.WHITE.value -> Color.WHITE.code + else -> Color.BLACK.code + } +} + +fun BrushColor.getColorCode(): Int { + return when (this) { + is BrushColorBlack -> Color.BLACK.code + is BrushColorGrey -> Color.GRAY.code + is BrushColorRed -> Color.RED.code + is BrushColorOrange -> Color.ORANGE.code + is BrushColorGreen -> Color.GREEN.code + is BrushColorBlue -> Color.BLUE.code + is BrushColorPurple -> Color.PURPLE.code + } +} + +fun BrushSize.getBrushSize(): Float { + return when(this) { + is BrushSizeTiny -> Size.TINY.value + is BrushSizeSmall -> Size.SMALL.value + is BrushSizeMedium -> Size.MEDIUM.value + is BrushSizeLarge -> Size.LARGE.value + is BrushSizeHuge -> Size.HUGE.value + } +} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/ImageHelper.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/ImageHelper.kt new file mode 100644 index 0000000..cdeaca5 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/ImageHelper.kt @@ -0,0 +1,33 @@ +package dev.arkbuilders.canvas.presentation.utils + +import android.graphics.Bitmap +import android.graphics.Matrix +import dev.arkbuilders.canvas.presentation.edit.crop.CropWindow +import dev.arkbuilders.canvas.presentation.edit.resize.ResizeOperation +import dev.arkbuilders.canvas.presentation.edit.rotate.RotateOperation + +fun Bitmap.crop(cropParams: CropWindow.CropParams): Bitmap = Bitmap.createBitmap( + this, + cropParams.x, + cropParams.y, + cropParams.width, + cropParams.height +) + +fun Bitmap.resize(scale: ResizeOperation.Scale): Bitmap { + val matrix = Matrix() + matrix.postScale(scale.x, scale.y) + return Bitmap.createBitmap( + this, + 0, + 0, + width, + height, + matrix, + true + ) +} + +fun Matrix.rotate(angle: Float, center: RotateOperation.Center) { + this.postRotate(angle, center.x, center.y) +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/PermissionsHelper.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/PermissionsHelper.kt new file mode 100644 index 0000000..5fe2ff8 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/PermissionsHelper.kt @@ -0,0 +1,45 @@ +package dev.arkbuilders.canvas.presentation.utils + +import android.Manifest +import android.annotation.TargetApi +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.contract.ActivityResultContracts + +object PermissionsHelper { + fun writePermContract(): ActivityResultContract { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + AllFilesAccessContract() + } else { + ActivityResultContracts.RequestPermission() + } + } + + fun launchWritePerm(launcher: ActivityResultLauncher) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val packageUri = "package:" +" BuildConfig.APPLICATION_ID" + launcher.launch(packageUri) + } else { + launcher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } +} + +@TargetApi(30) +private class AllFilesAccessContract : ActivityResultContract() { + override fun createIntent(context: Context, input: String): Intent { + return Intent( + Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, + Uri.parse(input) + ) + } + + override fun parseResult(resultCode: Int, intent: Intent?) = + Environment.isExternalStorageManager() +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt new file mode 100644 index 0000000..67ac3a7 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt @@ -0,0 +1,307 @@ +package dev.arkbuilders.canvas.presentation.utils + +import android.graphics.Paint +import android.util.Log +import android.util.Xml +import dev.arkbuilders.canvas.presentation.graphics.Color +import org.xmlpull.v1.XmlPullParser +import java.nio.file.Path +import kotlin.io.path.reader +import kotlin.io.path.writer +import android.graphics.Path as AndroidDrawPath + + +data class DrawPath( + val path: android.graphics.Path, + val paint: Paint +) + +class SVG { + private var strokeColor = Color.BLACK.value + private var strokeSize: Int = dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + private var fill = "none" + private var viewBox = ViewBox() + private val commands = ArrayDeque() + private val paths = ArrayDeque() + + private val paint + get() = Paint().also { + it.color = strokeColor.getColorCode() + it.style = Paint.Style.STROKE + it.strokeWidth = strokeSize.getStrokeSize() + it.strokeCap = Paint.Cap.ROUND + it.strokeJoin = Paint.Join.ROUND + it.isAntiAlias = true + } + + fun addCommand(command: SVGCommand) { + commands.addLast(command) + } + + fun addPath(path: DrawPath) { + paths.addLast(path) + } + + fun generate(path: Path) { + if (commands.isNotEmpty()) { + val xmlSerializer = Xml.newSerializer() + val pathData = commands.joinToString() + xmlSerializer.apply { + setOutput(path.writer()) + startDocument("utf-8", false) + startTag("", SVG_TAG) + attribute("", Attributes.VIEW_BOX, viewBox.toString()) + attribute("", Attributes.XML_NS_URI, XML_NS_URI) + startTag("", PATH_TAG) + attribute("", Attributes.Path.STROKE, strokeColor) + attribute("", Attributes.Path.FILL, fill) + attribute("", Attributes.Path.DATA, pathData) + endTag("", PATH_TAG) + endTag("", SVG_TAG) + endDocument() + } + } + } + + fun getPaths(): Collection = paths + + fun copy(): SVG = SVG().apply { + strokeColor = this@SVG.strokeColor + fill = this@SVG.fill + viewBox = this@SVG.viewBox + commands.addAll(this@SVG.commands) + paths.addAll(this@SVG.paths) + } + + private fun createCanvasPaths() { + if (commands.isNotEmpty()) { + if (paths.isNotEmpty()) paths.clear() + var path = AndroidDrawPath() + commands.forEach { command -> + strokeColor = command.paintColor + strokeSize = command.brushSizeId + when (command) { + is SVGCommand.MoveTo -> { + path = AndroidDrawPath() + path.moveTo(command.x, command.y) + } + + is SVGCommand.AbsQuadTo -> { + path.quadTo(command.x1, command.y1, command.x2, command.y2) + } + + is SVGCommand.AbsLineTo -> { + path.lineTo(command.x, command.y) + } + } + paths.addLast(DrawPath(path, paint.apply { + color = strokeColor.getColorCode() + strokeWidth = strokeSize.getStrokeSize() + })) + } + } + } + + companion object { + fun parse(path: Path): SVG = SVG().apply { + val xmlParser = Xml.newPullParser() + var pathData = "" + + xmlParser.apply { + setInput(path.reader()) + + var event = xmlParser.eventType + var pathCount = 0 + while (event != XmlPullParser.END_DOCUMENT) { + val tag = xmlParser.name + when (event) { + XmlPullParser.START_TAG -> { + when (tag) { + SVG_TAG -> { + viewBox = ViewBox.fromString( + getAttributeValue("", Attributes.VIEW_BOX) + ) + } + PATH_TAG -> { + pathCount += 1 + strokeColor = getAttributeValue("", Attributes.Path.STROKE) + fill = getAttributeValue("", Attributes.Path.FILL) + pathData = getAttributeValue("", Attributes.Path.DATA) + } + } + if (pathCount > 1) { + Log.d("svg", "found more than 1 path in file") + break + } + } + } + + event = next() + } + + pathData.split(COMMA).forEach { + val command = it.trim() + if (command.isEmpty()) return@forEach + val commandElements = command.split(" ") + when (command.first()) { + SVGCommand.MoveTo.CODE -> { + if (commandElements.size > 3) { + strokeColor = commandElements[3] + } + if (commandElements.size > 4) { + strokeSize = commandElements[4].toInt() + } + commands.addLast(SVGCommand.MoveTo.fromString(command).apply { + paintColor = strokeColor + brushSizeId = strokeSize + }) + } + SVGCommand.AbsLineTo.CODE -> { + if (commandElements.size > 3) { + strokeColor = commandElements[3] + } + if (commandElements.size > 4) { + strokeSize = commandElements[4].toInt() + } + commands.addLast(SVGCommand.MoveTo.fromString(command).apply { + paintColor = strokeColor + brushSizeId = strokeSize + }) + } + SVGCommand.AbsQuadTo.CODE -> { + if (commandElements.size > 5) { + strokeColor = commandElements[5] + } + if (commandElements.size > 6) { + strokeSize = commandElements[6].toInt() + } + commands.addLast(SVGCommand.AbsQuadTo.fromString(command).apply { + paintColor = strokeColor + brushSizeId = strokeSize + }) + } + else -> {} + } + } + + createCanvasPaths() + } + } + + private object Attributes { + const val VIEW_BOX = "viewBox" + const val XML_NS_URI = "xmlns" + + object Path { + const val STROKE = "stroke" + const val FILL = "fill" + const val DATA = "d" + } + } + } +} + +data class ViewBox( + val x: Float = 0f, + val y: Float = 0f, + val width: Float = 100f, + val height: Float = 100f +) { + override fun toString(): String = "$x $y $width $height" + + companion object { + fun fromString(string: String): ViewBox { + val viewBox = string.split(" ") + return ViewBox( + viewBox[0].toFloat(), + viewBox[1].toFloat(), + viewBox[2].toFloat(), + viewBox[3].toFloat() + ) + } + } +} + +sealed class SVGCommand { + + var paintColor = Color.BLACK.value + var brushSizeId = dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + + class MoveTo( + val x: Float, + val y: Float + ) : SVGCommand() { + override fun toString(): String = "$CODE $x $y $paintColor $brushSizeId" + + companion object { + const val CODE = 'M' + + fun fromString(string: String): SVGCommand { + val params = string.removePrefix("$CODE").trim().split(" ") + val x = params[0].toFloat() + val y = params[1].toFloat() + val colorCode = if (params.size > 2) params[2] else Color.BLACK.value + val strokeSizeId = if (params.size > 3) params[3].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + return MoveTo(x, y).apply { + paintColor = colorCode + brushSizeId = strokeSizeId + } + } + } + } + + class AbsLineTo( + val x: Float, + val y: Float + ) : SVGCommand() { + override fun toString(): String = "$CODE $x $y $paintColor $brushSizeId" + + companion object { + const val CODE = 'L' + + fun fromString(string: String): SVGCommand { + val params = string.removePrefix("$CODE").trim().split(" ") + val x = params[0].toFloat() + val y = params[1].toFloat() + val colorCode = if (params.size > 2) params[2] else Color.BLACK.value + val strokeSizeId = if (params.size > 3) params[3].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + return AbsLineTo(x, y).apply { + paintColor = colorCode + brushSizeId = strokeSizeId + } + } + } + } + + class AbsQuadTo( + val x1: Float, + val y1: Float, + val x2: Float, + val y2: Float + ) : SVGCommand() { + override fun toString(): String = "$CODE $x1 $y1 $x2 $y2 $paintColor $brushSizeId" + + companion object { + const val CODE = 'Q' + + fun fromString(string: String): SVGCommand { + val params = string.removePrefix("$CODE").trim().split(" ") + val x1 = params[0].toFloat() + val y1 = params[1].toFloat() + val x2 = params[2].toFloat() + val y2 = params[3].toFloat() + val colorCode = if (params.size > 4) params[4] else Color.BLACK.value + val strokeSizeId = if (params.size > 5) params[5].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + return AbsQuadTo(x1, y1, x2, y2).apply { + paintColor = colorCode + brushSizeId = strokeSizeId + } + } + } + } +} + +private const val COMMA = "," +private const val XML_NS_URI = "http://www.w3.org/2000/svg" +private const val SVG_TAG = "svg" +private const val PATH_TAG = "path" \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/Utils.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/Utils.kt new file mode 100644 index 0000000..c183b41 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/Utils.kt @@ -0,0 +1,105 @@ +package dev.arkbuilders.canvas.presentation.utils + +import android.Manifest +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.extension +import kotlin.io.path.nameWithoutExtension +import kotlin.math.atan2 + +fun Path.findNotExistCopyName(name: Path): Path { + val parent = this + var filesCounter = 1 + + fun formatNameWithCounter() = + "${name.nameWithoutExtension}_$filesCounter.${name.extension}" + + var newPath = parent.resolve(formatNameWithCounter()) + + while (newPath.exists()) { + newPath = parent.resolve(formatNameWithCounter()) + filesCounter++ + } + return newPath +} + +fun Context.askWritePermissions() = getActivity()?.apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val packageUri = + Uri.parse("package:") + val intent = + Intent( + Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, + packageUri + ) + startActivityForResult(intent, 1) + } else { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + 2 + ) + } +} + +fun Context.isWritePermGranted(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Environment.isExternalStorageManager() + } else { + ContextCompat.checkSelfPermission( + this, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED + } +} + +fun Context.getActivity(): AppCompatActivity? = when (this) { + is AppCompatActivity -> this + is ContextWrapper -> baseContext.getActivity() + else -> null +} + +typealias Degrees = Float + +fun PointerEvent.calculateRotationFromOneFingerGesture( + center: Offset +): Degrees { + var angleDelta = 0.0 + changes.forEach { change -> + if (change.pressed) { + val currentPosition = change.position + val prevPosition = change.previousPosition + val prevOffset = prevPosition - center + val currentOffset = currentPosition - center + val prevAngle = atan2( + prevOffset.y.toDouble(), + prevOffset.x.toDouble() + ) + val currentAngle = atan2( + currentOffset.y.toDouble(), + currentOffset.x.toDouble() + ) + angleDelta = Math.toDegrees(currentAngle - prevAngle) + } + } + return angleDelta.toFloat() +} + +fun Context.toast(@StringRes stringId: Int) { + Toast.makeText(this, getString(stringId), Toast.LENGTH_SHORT).show() +} diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/adapters/BrushAttribute.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/adapters/BrushAttribute.kt new file mode 100644 index 0000000..12bfa22 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/adapters/BrushAttribute.kt @@ -0,0 +1,27 @@ +package dev.arkbuilders.canvas.presentation.utils.adapters + +sealed interface BrushAttribute { + var isSelected: Boolean +} + +sealed class BrushSize : BrushAttribute { + override var isSelected = false +} + +sealed class BrushColor : BrushAttribute { + override var isSelected = false +} + +data object BrushSizeTiny : BrushSize() +data object BrushSizeSmall : BrushSize() +data object BrushSizeMedium : BrushSize() +data object BrushSizeLarge : BrushSize() +data object BrushSizeHuge : BrushSize() + +data object BrushColorBlack : BrushColor() +data object BrushColorGrey : BrushColor() +data object BrushColorRed : BrushColor() +data object BrushColorOrange : BrushColor() +data object BrushColorGreen : BrushColor() +data object BrushColorBlue : BrushColor() +data object BrushColorPurple : BrushColor() \ No newline at end of file diff --git a/canvas/src/main/res/drawable/ic_add.xml b/canvas/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..e40b9ce --- /dev/null +++ b/canvas/src/main/res/drawable/ic_add.xml @@ -0,0 +1,13 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_arrow_back.xml b/canvas/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..6bd5650 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_aspect_ratio.xml b/canvas/src/main/res/drawable/ic_aspect_ratio.xml new file mode 100644 index 0000000..77f9080 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_aspect_ratio.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_blur_on.xml b/canvas/src/main/res/drawable/ic_blur_on.xml new file mode 100644 index 0000000..92390c9 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_blur_on.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_check.xml b/canvas/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..cf143d4 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_check.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_clear.xml b/canvas/src/main/res/drawable/ic_clear.xml new file mode 100644 index 0000000..844b6b6 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_clear.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_crop.xml b/canvas/src/main/res/drawable/ic_crop.xml new file mode 100644 index 0000000..8e91f26 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_crop.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_crop_16_9.xml b/canvas/src/main/res/drawable/ic_crop_16_9.xml new file mode 100644 index 0000000..d7bdc55 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_crop_16_9.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_crop_3_2.xml b/canvas/src/main/res/drawable/ic_crop_3_2.xml new file mode 100644 index 0000000..466797f --- /dev/null +++ b/canvas/src/main/res/drawable/ic_crop_3_2.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_crop_5_4.xml b/canvas/src/main/res/drawable/ic_crop_5_4.xml new file mode 100644 index 0000000..8993425 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_crop_5_4.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_crop_square.xml b/canvas/src/main/res/drawable/ic_crop_square.xml new file mode 100644 index 0000000..7d6b01b --- /dev/null +++ b/canvas/src/main/res/drawable/ic_crop_square.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_eraser.xml b/canvas/src/main/res/drawable/ic_eraser.xml new file mode 100644 index 0000000..ab1f850 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_eraser.xml @@ -0,0 +1,13 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_eyedropper.xml b/canvas/src/main/res/drawable/ic_eyedropper.xml new file mode 100644 index 0000000..e897caa --- /dev/null +++ b/canvas/src/main/res/drawable/ic_eyedropper.xml @@ -0,0 +1,4 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_insert_photo.xml b/canvas/src/main/res/drawable/ic_insert_photo.xml new file mode 100644 index 0000000..35960a0 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_insert_photo.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_line_weight.xml b/canvas/src/main/res/drawable/ic_line_weight.xml new file mode 100644 index 0000000..11f94cd --- /dev/null +++ b/canvas/src/main/res/drawable/ic_line_weight.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_more_vert.xml b/canvas/src/main/res/drawable/ic_more_vert.xml new file mode 100644 index 0000000..39fbab5 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_more_vert.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_pan_tool.xml b/canvas/src/main/res/drawable/ic_pan_tool.xml new file mode 100644 index 0000000..5a9158f --- /dev/null +++ b/canvas/src/main/res/drawable/ic_pan_tool.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_redo.xml b/canvas/src/main/res/drawable/ic_redo.xml new file mode 100644 index 0000000..b486fdb --- /dev/null +++ b/canvas/src/main/res/drawable/ic_redo.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_rotate_90_degrees_ccw.xml b/canvas/src/main/res/drawable/ic_rotate_90_degrees_ccw.xml new file mode 100644 index 0000000..255da67 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_rotate_90_degrees_ccw.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_rotate_left.xml b/canvas/src/main/res/drawable/ic_rotate_left.xml new file mode 100644 index 0000000..b644196 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_rotate_left.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_rotate_right.xml b/canvas/src/main/res/drawable/ic_rotate_right.xml new file mode 100644 index 0000000..9fa1482 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_rotate_right.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_save.xml b/canvas/src/main/res/drawable/ic_save.xml new file mode 100644 index 0000000..dbc4c4e --- /dev/null +++ b/canvas/src/main/res/drawable/ic_save.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_share.xml b/canvas/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000..87cea78 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_share.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_undo.xml b/canvas/src/main/res/drawable/ic_undo.xml new file mode 100644 index 0000000..5fd6184 --- /dev/null +++ b/canvas/src/main/res/drawable/ic_undo.xml @@ -0,0 +1,5 @@ + + + diff --git a/canvas/src/main/res/drawable/ic_zoom_in.xml b/canvas/src/main/res/drawable/ic_zoom_in.xml new file mode 100644 index 0000000..670484f --- /dev/null +++ b/canvas/src/main/res/drawable/ic_zoom_in.xml @@ -0,0 +1,6 @@ + + + + diff --git a/canvas/src/main/res/layout/fragment_ark_canvas.xml b/canvas/src/main/res/layout/fragment_ark_canvas.xml index f224669..5d694b3 100644 --- a/canvas/src/main/res/layout/fragment_ark_canvas.xml +++ b/canvas/src/main/res/layout/fragment_ark_canvas.xml @@ -1,14 +1,12 @@ + android:layout_height="match_parent"> - - + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/canvas/src/main/res/values/strings.xml b/canvas/src/main/res/values/strings.xml index 6048840..2b47406 100644 --- a/canvas/src/main/res/values/strings.xml +++ b/canvas/src/main/res/values/strings.xml @@ -1,4 +1,42 @@ Hello blank fragment + Intensity + Size + Free\n + + + Name + Overwrite original file + Cancel + OK + Location + Pick folder + Open + Or + New + Crop + Square + 9:16 + 2:3 + 4:5 + Options + Share + Clear + Height + Width + Input digits only + Width should be less than %d + Height should be less than %d + Sorry, the application crashed. Please send a report to the developers. + Pick a color please + Background + Height cannot be %s + Width cannot be %s + Please enter width + Please enter height + Please pick folder to save image + Please provide file name + Save + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ebf516..8e75449 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,4 +57,4 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "uiAndroid" } [plugins] jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -androidLibrary = { id = "com.android.library", version.ref = "agp" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } \ No newline at end of file diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 5579f68..e9f85e2 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -9,7 +9,7 @@ android { defaultConfig { applicationId = "dev.arkbuilders.sample" - minSdk = 26 + minSdk = 29 targetSdk = 34 versionCode = 1 versionName = "1.0" @@ -46,6 +46,11 @@ android { buildFeatures { buildConfig = true viewBinding = true + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.10" } splits { @@ -78,6 +83,7 @@ dependencies { implementation("androidx.core:core-ktx:1.12.0") implementation(libraries.androidx.appcompat) implementation(libraries.android.material) + implementation(libs.androidx.ui.android) testImplementation(libraries.junit) androidTestImplementation(libraries.androidx.test.junit) androidTestImplementation(libraries.androidx.test.espresso) diff --git a/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasFragment.kt b/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasFragment.kt index f1092ca..3cb8aa3 100644 --- a/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasFragment.kt +++ b/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasFragment.kt @@ -5,34 +5,10 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment +import dev.arkbuilders.components.about.presentation.ArkAbout import dev.arkbuilders.sample.R private const val imagePath = "image_path_param" -class CanvasFragment : Fragment() { - private var imagePathParam: String? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - arguments?.let { - imagePathParam = it.getString(imagePath) - } - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_canvas, container, false) - } - - companion object { - @JvmStatic - fun newInstance(param1: String) = - CanvasFragment().apply { - arguments = Bundle().apply { - putString(imagePath, param1) - } - } - } -} \ No newline at end of file From cbba929bca67d6339cf2a9874ee267e8dcff898f Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Wed, 13 Nov 2024 22:52:32 +0700 Subject: [PATCH 03/29] Load drawing & remove unused files --- .../canvas/presentation/ArkCanvasFragment.kt | 24 ++++++++++--------- .../dev/arkbuilders/sample/MainActivity.kt | 2 +- .../sample/canvas/CanvasActivity.kt | 1 - .../sample/canvas/CanvasFragment.kt | 14 ----------- 4 files changed, 14 insertions(+), 27 deletions(-) delete mode 100644 sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasFragment.kt diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt index 416821d..fde4137 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import dev.arkbuilders.canvas.R import dev.arkbuilders.canvas.presentation.data.Preferences import dev.arkbuilders.canvas.presentation.data.Resolution +import dev.arkbuilders.canvas.presentation.drawing.EditCanvas import dev.arkbuilders.canvas.presentation.edit.EditViewModel private const val imagePath = "image_path_param" @@ -17,22 +18,24 @@ private const val imagePath = "image_path_param" class ArkCanvasFragment : Fragment() { private var imagePathParam: String? = null - private val prefs = Preferences(appCtx = requireActivity().applicationContext) + lateinit var prefs: Preferences - val viewModel = EditViewModel( - primaryColor = 0, - launchedFromIntent = false, - imagePath = null, - imageUri = null, - maxResolution = Resolution(720, 350), - prefs = prefs, - ) + lateinit var viewModel: EditViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { imagePathParam = it.getString(imagePath) } + prefs = Preferences(appCtx = requireActivity().applicationContext) + viewModel = EditViewModel( + primaryColor = 0, + launchedFromIntent = false, + imagePath = null, + imageUri = null, + maxResolution = Resolution(350, 720), + prefs = prefs, + ) } override fun onCreateView( @@ -43,7 +46,6 @@ class ArkCanvasFragment : Fragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState) val composeView = view.findViewById(R.id.compose_view) @@ -53,7 +55,7 @@ class ArkCanvasFragment : Fragment() { ) setContent { // Set Content here - + EditCanvas(viewModel = viewModel) } } } diff --git a/sample/src/main/java/dev/arkbuilders/sample/MainActivity.kt b/sample/src/main/java/dev/arkbuilders/sample/MainActivity.kt index b5cc7e9..a7f2ae5 100644 --- a/sample/src/main/java/dev/arkbuilders/sample/MainActivity.kt +++ b/sample/src/main/java/dev/arkbuilders/sample/MainActivity.kt @@ -59,7 +59,7 @@ class MainActivity : AppCompatActivity() { val intent = Intent(this, AboutActivity::class.java) startActivity(intent) } - findViewById(R.id.btn_open_file_mode).setOnClickListener { + findViewById(R.id.btn_canvas).setOnClickListener { val intent = Intent(this, CanvasActivity::class.java) startActivity(intent) } diff --git a/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasActivity.kt b/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasActivity.kt index 365cba7..8703a42 100644 --- a/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasActivity.kt +++ b/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasActivity.kt @@ -7,7 +7,6 @@ import dev.arkbuilders.canvas.presentation.ArkCanvasFragment import dev.arkbuilders.sample.R class CanvasActivity : AppCompatActivity() { - @SuppressLint("CommitTransaction") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_canvas) diff --git a/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasFragment.kt b/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasFragment.kt deleted file mode 100644 index 3cb8aa3..0000000 --- a/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasFragment.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.arkbuilders.sample.canvas - - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import dev.arkbuilders.components.about.presentation.ArkAbout -import dev.arkbuilders.sample.R - -private const val imagePath = "image_path_param" - From c70f229be115d22de524fe586c805309cde8848e Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 16 Nov 2024 16:16:59 +0700 Subject: [PATCH 04/29] Load edit screen --- .../canvas/presentation/ArkCanvasFragment.kt | 19 ++++- .../presentation/drawing/EditManager.kt | 1 + .../canvas/presentation/edit/EditScreen.kt | 70 +++++++------------ .../canvas/presentation/edit/EditViewModel.kt | 42 +++++------ 4 files changed, 64 insertions(+), 68 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt index fde4137..9ff78d1 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt @@ -1,16 +1,18 @@ package dev.arkbuilders.canvas.presentation +import android.os.Build import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.RequiresApi import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment import dev.arkbuilders.canvas.R import dev.arkbuilders.canvas.presentation.data.Preferences import dev.arkbuilders.canvas.presentation.data.Resolution -import dev.arkbuilders.canvas.presentation.drawing.EditCanvas +import dev.arkbuilders.canvas.presentation.edit.EditScreen import dev.arkbuilders.canvas.presentation.edit.EditViewModel private const val imagePath = "image_path_param" @@ -45,6 +47,7 @@ class ArkCanvasFragment : Fragment() { return inflater.inflate(R.layout.fragment_ark_canvas, container, false) } + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val composeView = view.findViewById(R.id.compose_view) @@ -55,7 +58,17 @@ class ArkCanvasFragment : Fragment() { ) setContent { // Set Content here - EditCanvas(viewModel = viewModel) + EditScreen( + imagePath = null, + imageUri = null, + fragmentManager = childFragmentManager, + navigateBack = { /*TODO*/ }, + launchedFromIntent = false, + maxResolution = Resolution(350, 720), + onSaveSvg = { /*TODO*/ }, + viewModel = viewModel + ) +// EditCanvas(viewModel = viewModel) } } } diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt index f991865..08388d1 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt @@ -430,6 +430,7 @@ class EditManager { path, currentPaint.copy().apply { strokeWidth = drawPaint.value.strokeWidth + color = Color.Black } ) ) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt index 38884dd..e5298bf 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -63,7 +63,6 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.fragment.app.FragmentManager -import androidx.lifecycle.viewmodel.compose.viewModel import dev.arkbuilders.canvas.R import dev.arkbuilders.canvas.presentation.data.Resolution import dev.arkbuilders.canvas.presentation.drawing.EditCanvas @@ -79,7 +78,6 @@ import dev.arkbuilders.canvas.presentation.utils.getActivity import dev.arkbuilders.canvas.presentation.utils.isWritePermGranted import java.nio.file.Path -private const val isChangedForMemoIntegration = true @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @Composable fun EditScreen( @@ -92,8 +90,6 @@ fun EditScreen( onSaveSvg: () -> Unit, viewModel: EditViewModel, ) { - val primaryColor = MaterialTheme.colors.primary.value.toLong() - val context = LocalContext.current val showDefaultsDialog = remember { mutableStateOf( @@ -127,7 +123,6 @@ fun EditScreen( viewModel.isLoaded = false }, launchedFromIntent = launchedFromIntent, - onSaveSvg = onSaveSvg ) BackHandler { @@ -279,6 +274,8 @@ private fun Menus( private fun DrawContainer( viewModel: EditViewModel ) { + + val context = LocalContext.current Box( modifier = Modifier .padding(bottom = 32.dp) @@ -328,9 +325,7 @@ private fun DrawContainer( return@onSizeChanged } - isZoomMode.value -> { - return@onSizeChanged - } + isZoomMode.value -> { return@onSizeChanged } else -> { scaleToFit() @@ -339,7 +334,7 @@ private fun DrawContainer( } } } -// viewModel.loadImage() + viewModel.loadImage(context) }, contentAlignment = Alignment.Center ) { @@ -438,16 +433,12 @@ private fun BoxScope.TopMenu( if ( !viewModel.editManager.canUndo.value ) { - if (isChangedForMemoIntegration) { - navigateBack() + if (launchedFromIntent) { + context + .getActivity() + ?.finish() } else { - if (launchedFromIntent) { - context - .getActivity() - ?.finish() - } else { - navigateBack() - } + navigateBack() } } else { viewModel.showExitDialog = true @@ -630,12 +621,12 @@ private fun EditMenuContent( imageVector = ImageVector.vectorResource(R.drawable.ic_undo), tint = if ( editManager.canUndo.value && ( - !editManager.isRotateMode.value && - !editManager.isResizeMode.value && - !editManager.isCropMode.value && - !editManager.isEyeDropperMode.value && - !editManager.isBlurMode.value - ) + !editManager.isRotateMode.value && + !editManager.isResizeMode.value && + !editManager.isCropMode.value && + !editManager.isEyeDropperMode.value && + !editManager.isBlurMode.value + ) ) MaterialTheme.colors.primary else Color.Black, contentDescription = null ) @@ -657,12 +648,12 @@ private fun EditMenuContent( tint = if ( editManager.canRedo.value && ( - !editManager.isRotateMode.value && - !editManager.isResizeMode.value && - !editManager.isCropMode.value && - !editManager.isEyeDropperMode.value && - !editManager.isBlurMode.value - ) + !editManager.isRotateMode.value && + !editManager.isResizeMode.value && + !editManager.isCropMode.value && + !editManager.isEyeDropperMode.value && + !editManager.isBlurMode.value + ) ) MaterialTheme.colors.primary else Color.Black, contentDescription = null ) @@ -718,8 +709,7 @@ private fun EditMenuContent( viewModel.strokeSliderExpanded = !viewModel.strokeSliderExpanded }, - imageVector = - ImageVector.vectorResource(R.drawable.ic_line_weight), + imageVector = ImageVector.vectorResource(R.drawable.ic_line_weight), tint = if ( !editManager.isRotateMode.value && !editManager.isResizeMode.value && @@ -1017,8 +1007,7 @@ private fun HandleImageSavedEffect( private fun ExitDialog( viewModel: EditViewModel, navigateBack: () -> Unit, - launchedFromIntent: Boolean, - onSaveSvg: () -> Unit = {} + launchedFromIntent: Boolean ) { if (!viewModel.showExitDialog) return @@ -1038,9 +1027,6 @@ private fun ExitDialog( confirmButton = { Button( onClick = { - if (isChangedForMemoIntegration) { - onSaveSvg() - } viewModel.showExitDialog = false viewModel.showSavePathDialog = true } @@ -1052,14 +1038,10 @@ private fun ExitDialog( TextButton( onClick = { viewModel.showExitDialog = false - if (isChangedForMemoIntegration) { - navigateBack() + if (launchedFromIntent) { + context.getActivity()?.finish() } else { - if (launchedFromIntent) { - context.getActivity()?.finish() - } else { - navigateBack() - } + navigateBack() } } ) { diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt index 39ca4c7..dc8bb3c 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt @@ -99,6 +99,7 @@ class EditViewModel( } init { +// setPaths() if (imageUri == null && imagePath == null) { viewModelScope.launch { editManager.initDefaults( @@ -121,29 +122,28 @@ class EditViewModel( editManager.setPaintColor(color) } - setPaths() } -// fun loadImage() { -// isLoaded = true -// imagePath?.let { -// loadImageWithPath( -// DIManager.component.app(), -// imagePath, -// editManager -// ) -// return -// } -// imageUri?.let { -// loadImageWithUri( -// DIManager.component.app(), -// imageUri, -// editManager -// ) -// return -// } -// editManager.scaleToFit() -// } + fun loadImage(context: Context) { + isLoaded = true + imagePath?.let { + loadImageWithPath( + context, + imagePath, + editManager + ) + return + } + imageUri?.let { + loadImageWithUri( + context, + imageUri, + editManager + ) + return + } + editManager.scaleToFit() + } fun saveImage(context: Context, path: Path) { viewModelScope.launch(Dispatchers.IO) { From 7577b3098b34b1bb92842428f0f184f4b53c0a2f Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 17 Nov 2024 15:01:06 +0700 Subject: [PATCH 05/29] Remove unused --- .../dev/arkbuilders/canvas/presentation/drawing/EditManager.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt index 08388d1..f991865 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt @@ -430,7 +430,6 @@ class EditManager { path, currentPaint.copy().apply { strokeWidth = drawPaint.value.strokeWidth - color = Color.Black } ) ) From befade159e431f7a71e6150baa18edc2d9a00da5 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 17 Nov 2024 15:42:10 +0700 Subject: [PATCH 06/29] Extract dependencies --- .../canvas/presentation/edit/EditViewModel.kt | 57 +------ .../resourceloader/BitmapResourceLoader.kt | 60 ------- .../resourceloader/BitmapResourceManager.kt | 160 ++++++++++++++++++ .../resourceloader/CanvasResourceLoader.kt | 9 - .../resourceloader/CanvasResourceManager.kt | 8 + .../resourceloader/SvgResourceLoader.kt | 15 -- .../resourceloader/SvgResourceManager.kt | 32 ++++ 7 files changed, 206 insertions(+), 135 deletions(-) delete mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceLoader.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceManager.kt delete mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/CanvasResourceLoader.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/CanvasResourceManager.kt delete mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceLoader.kt create mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt index dc8bb3c..b5b2b7e 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt @@ -5,7 +5,6 @@ import android.content.Intent import android.graphics.Bitmap import android.graphics.Matrix import android.graphics.drawable.Drawable -import android.media.MediaScannerConnection import android.net.Uri import android.view.MotionEvent import androidx.compose.runtime.getValue @@ -26,21 +25,18 @@ import androidx.compose.ui.unit.toSize import androidx.core.content.FileProvider import androidx.core.net.toUri import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject import dev.arkbuilders.canvas.presentation.data.Preferences import dev.arkbuilders.canvas.presentation.data.Resolution import dev.arkbuilders.canvas.presentation.drawing.DrawPath import dev.arkbuilders.canvas.presentation.drawing.EditManager import dev.arkbuilders.canvas.presentation.graphics.SVG +import dev.arkbuilders.canvas.presentation.resourceloader.CanvasResourceManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -48,7 +44,6 @@ import timber.log.Timber import java.io.File import java.nio.file.Path import kotlin.io.path.Path -import kotlin.io.path.outputStream import kotlin.system.measureTimeMillis class EditViewModel( @@ -57,10 +52,10 @@ class EditViewModel( private val imagePath: Path?, private val imageUri: String?, private val maxResolution: Resolution, - private val prefs: Preferences + private val prefs: Preferences, + val editManager: EditManager, + private val canvasResourceManager: CanvasResourceManager, ) : ViewModel() { - val editManager = EditManager() - var strokeSliderExpanded by mutableStateOf(false) var menusVisible by mutableStateOf(true) var strokeWidth by mutableStateOf(5f) @@ -114,7 +109,7 @@ class EditViewModel( val color = if (_usedColors.isNotEmpty()) { _usedColors.last() } else { - val defaultColor = Color(primaryColor.toULong()) + val defaultColor = Color.Blue _usedColors.add(defaultColor) defaultColor @@ -148,16 +143,7 @@ class EditViewModel( fun saveImage(context: Context, path: Path) { viewModelScope.launch(Dispatchers.IO) { isSavingImage = true - val combinedBitmap = getEditedImage() - path.outputStream().use { out -> - combinedBitmap.asAndroidBitmap() - .compress(Bitmap.CompressFormat.PNG, 100, out) - } - MediaScannerConnection.scanFile( - context, - arrayOf(path.toString()), - arrayOf("image/*") - ) { _, _ -> } + canvasResourceManager.saveResource(path) imageSaved = true isSavingImage = false showSavePathDialog = false @@ -441,37 +427,6 @@ class EditViewModel( } } -class EditViewModelFactory @AssistedInject constructor( - @Assisted private val primaryColor: Long, - @Assisted private val launchedFromIntent: Boolean, - @Assisted private val imagePath: Path?, - @Assisted private val imageUri: String?, - @Assisted private val maxResolution: Resolution, - private val prefs: Preferences, -) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return EditViewModel( - primaryColor, - launchedFromIntent, - imagePath, - imageUri, - maxResolution, - prefs, - ) as T - } - - @AssistedFactory - interface Factory { - fun create( - @Assisted primaryColor: Long, - @Assisted launchedFromIntent: Boolean, - @Assisted imagePath: Path?, - @Assisted imageUri: String?, - @Assisted maxResolution: Resolution, - ): EditViewModelFactory - } -} - private fun loadImageWithPath( context: Context, image: Path, diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceLoader.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceLoader.kt deleted file mode 100644 index 2aafabb..0000000 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceLoader.kt +++ /dev/null @@ -1,60 +0,0 @@ -package dev.arkbuilders.canvas.presentation.resourceloader - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.drawable.Drawable -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestBuilder -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.transition.Transition -import dev.arkbuilders.canvas.presentation.drawing.EditManager -import java.nio.file.Path - -class BitmapResourceLoader( - val context: Context, - val editManager: EditManager -) : CanvasResourceLoader { - - private val glideBuilder = Glide - .with(context) - .asBitmap() - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - - private lateinit var bitMapResource: ImageBitmap - override suspend fun loadResourceInto(path: Path, editManager: EditManager) { - loadImage(path) - } - override suspend fun getResource() { - - } - - private fun loadImage( - resourcePath: Path, - ) { - glideBuilder - .load(resourcePath.toFile()) - .loadInto() - } - - - private fun RequestBuilder.loadInto() { - into(object : CustomTarget() { - override fun onResourceReady( - bitmap: Bitmap, - transition: Transition? - ) { - editManager.apply { - backgroundImage.value = bitmap.asImageBitmap() - setOriginalBackgroundImage(backgroundImage.value) - scaleToFit() - } - } - - override fun onLoadCleared(placeholder: Drawable?) {} - }) - } -} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceManager.kt new file mode 100644 index 0000000..71bc45f --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceManager.kt @@ -0,0 +1,160 @@ +package dev.arkbuilders.canvas.presentation.resourceloader + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.drawable.Drawable +import android.media.MediaScannerConnection +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.unit.toSize +import com.bumptech.glide.Glide +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import timber.log.Timber +import java.nio.file.Path +import kotlin.io.path.outputStream +import kotlin.system.measureTimeMillis + +class BitmapResourceManager( + val context: Context, + val editManager: EditManager +) : CanvasResourceManager { + + private val glideBuilder = Glide + .with(context) + .asBitmap() + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + + private lateinit var bitMapResource: ImageBitmap + override suspend fun loadResource(path: Path) { + loadImage(path) + } + + private fun loadImage( + resourcePath: Path, + ) { + glideBuilder + .load(resourcePath.toFile()) + .loadInto() + } + + override suspend fun saveResource(path: Path) { + val combinedBitmap = getEditedImage() + path.outputStream().use { out -> + combinedBitmap.asAndroidBitmap() + .compress(Bitmap.CompressFormat.PNG, 100, out) + } + MediaScannerConnection.scanFile( + context, + arrayOf(path.toString()), + arrayOf("image/*") + ) { _, _ -> } + } + + fun getEditedImage(): ImageBitmap { + val size = editManager.imageSize + var bitmap = ImageBitmap( + size.width, + size.height, + ImageBitmapConfig.Argb8888 + ) + var pathBitmap: ImageBitmap? = null + val time = measureTimeMillis { + editManager.apply { + val matrix = Matrix() + if (editManager.drawPaths.isNotEmpty()) { + pathBitmap = ImageBitmap( + size.width, + size.height, + ImageBitmapConfig.Argb8888 + ) + val pathCanvas = Canvas(pathBitmap!!) + editManager.drawPaths.forEach { + pathCanvas.drawPath(it.path, it.paint) + } + } + backgroundImage.value?.let { + val canvas = Canvas(bitmap) + if (prevRotationAngle == 0f && drawPaths.isEmpty()) { + bitmap = it + return@let + } + if (prevRotationAngle != 0f) { + val centerX = size.width / 2f + val centerY = size.height / 2f + matrix.setRotate(prevRotationAngle, centerX, centerY) + } + canvas.nativeCanvas.drawBitmap( + it.asAndroidBitmap(), + matrix, + null + ) + if (drawPaths.isNotEmpty()) { + canvas.nativeCanvas.drawBitmap( + pathBitmap?.asAndroidBitmap()!!, + matrix, + null + ) + } + } ?: run { + val canvas = Canvas(bitmap) + if (prevRotationAngle != 0f) { + val centerX = size.width / 2 + val centerY = size.height / 2 + matrix.setRotate( + prevRotationAngle, + centerX.toFloat(), + centerY.toFloat() + ) + canvas.nativeCanvas.setMatrix(matrix) + } + canvas.drawRect( + Rect(Offset.Zero, size.toSize()), + backgroundPaint + ) + if (drawPaths.isNotEmpty()) { + canvas.drawImage( + pathBitmap!!, + Offset.Zero, + Paint() + ) + } + } + } + } + Timber.tag("edit-viewmodel: getEditedImage").d( + "processing edits took ${time / 1000} s ${time % 1000} ms" + ) + return bitmap + } + + + private fun RequestBuilder.loadInto() { + into(object : CustomTarget() { + override fun onResourceReady( + bitmap: Bitmap, + transition: Transition? + ) { + editManager.apply { + backgroundImage.value = bitmap.asImageBitmap() + setOriginalBackgroundImage(backgroundImage.value) + scaleToFit() + } + } + + override fun onLoadCleared(placeholder: Drawable?) {} + }) + } +} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/CanvasResourceLoader.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/CanvasResourceLoader.kt deleted file mode 100644 index e28713c..0000000 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/CanvasResourceLoader.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.arkbuilders.canvas.presentation.resourceloader - -import dev.arkbuilders.canvas.presentation.drawing.EditManager -import java.nio.file.Path - -interface CanvasResourceLoader { - suspend fun loadResourceInto(path: Path, editManager: EditManager) - suspend fun getResource() -} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/CanvasResourceManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/CanvasResourceManager.kt new file mode 100644 index 0000000..a015336 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/CanvasResourceManager.kt @@ -0,0 +1,8 @@ +package dev.arkbuilders.canvas.presentation.resourceloader + +import java.nio.file.Path + +interface CanvasResourceManager { + suspend fun loadResource(path: Path) + suspend fun saveResource(path: Path) +} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceLoader.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceLoader.kt deleted file mode 100644 index d7b6911..0000000 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceLoader.kt +++ /dev/null @@ -1,15 +0,0 @@ -package dev.arkbuilders.canvas.presentation.resourceloader - -import dev.arkbuilders.canvas.presentation.drawing.EditManager -import dev.arkbuilders.canvas.presentation.resourceloader.CanvasResourceLoader -import java.nio.file.Path - -class SvgResourceLoader: CanvasResourceLoader { - - override suspend fun loadResourceInto(path: Path, editManager: EditManager) { - - } - override suspend fun getResource() { - - } -} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt new file mode 100644 index 0000000..7e74f83 --- /dev/null +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt @@ -0,0 +1,32 @@ +package dev.arkbuilders.canvas.presentation.resourceloader + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.asComposePath +import dev.arkbuilders.canvas.presentation.drawing.DrawPath +import dev.arkbuilders.canvas.presentation.drawing.EditManager +import dev.arkbuilders.canvas.presentation.graphics.SVG +import java.nio.file.Path + +class SvgResourceManager( + private val editManager: EditManager, +): CanvasResourceManager { + + override suspend fun loadResource(path: Path) { + val svgpaths = SVG.parse(path) + svgpaths.getPaths().forEach { + val draw = DrawPath( + path = it.path.asComposePath(), + paint = Paint().apply { + color = Color(it.paint.color) + } + ) + editManager.addDrawPath(draw.path) + editManager.setPaintColor(draw.paint.color) + } + } + + override suspend fun saveResource(path: Path) { + TODO("Not yet implemented") + } +} \ No newline at end of file From 2bac253138e7fcdbbabc95a470bb76762906f7da Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sun, 17 Nov 2024 15:42:32 +0700 Subject: [PATCH 07/29] Extract dependencies --- .../canvas/presentation/ArkCanvasFragment.kt | 14 ++++++++++++-- .../canvas/presentation/graphics/Color.kt | 2 -- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt index 9ff78d1..a7825de 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt @@ -12,8 +12,11 @@ import androidx.fragment.app.Fragment import dev.arkbuilders.canvas.R import dev.arkbuilders.canvas.presentation.data.Preferences import dev.arkbuilders.canvas.presentation.data.Resolution +import dev.arkbuilders.canvas.presentation.drawing.EditManager import dev.arkbuilders.canvas.presentation.edit.EditScreen import dev.arkbuilders.canvas.presentation.edit.EditViewModel +import dev.arkbuilders.canvas.presentation.resourceloader.BitmapResourceManager +import dev.arkbuilders.canvas.presentation.resourceloader.CanvasResourceManager private const val imagePath = "image_path_param" @@ -23,20 +26,27 @@ class ArkCanvasFragment : Fragment() { lateinit var prefs: Preferences lateinit var viewModel: EditViewModel + lateinit var canvasResourceManager: CanvasResourceManager + lateinit var editManager: EditManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { imagePathParam = it.getString(imagePath) } - prefs = Preferences(appCtx = requireActivity().applicationContext) + val context = requireActivity().applicationContext + prefs = Preferences(appCtx = context) + editManager = EditManager() + canvasResourceManager = BitmapResourceManager(context = context, editManager = editManager) viewModel = EditViewModel( - primaryColor = 0, + primaryColor = 0xFF101828, launchedFromIntent = false, imagePath = null, imageUri = null, maxResolution = Resolution(350, 720), prefs = prefs, + editManager = editManager, + canvasResourceManager = canvasResourceManager, ) } diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Color.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Color.kt index 5f737ae..70124a5 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Color.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Color.kt @@ -1,7 +1,5 @@ package dev.arkbuilders.canvas.presentation.graphics -import dev.arkbuilders.canvas.presentation.graphics.ColorCode - enum class Color(val code: Int, val value: String) { BLACK(ColorCode.black, "black"), From c752e78c92eaa3d0635a4c24226d2b12532a64f9 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Wed, 20 Nov 2024 22:35:33 +0700 Subject: [PATCH 08/29] Extract logic of applying mode --- .../presentation/drawing/EditManager.kt | 150 ++++++++++++++++++ .../canvas/presentation/edit/EditScreen.kt | 124 +++------------ .../canvas/presentation/edit/EditViewModel.kt | 77 +-------- 3 files changed, 169 insertions(+), 182 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt index f991865..2f5acc7 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt @@ -6,9 +6,12 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.Path @@ -16,7 +19,9 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.toSize import dev.arkbuilders.canvas.presentation.data.ImageDefaults import dev.arkbuilders.canvas.presentation.data.Resolution import dev.arkbuilders.canvas.presentation.edit.Operation @@ -26,7 +31,9 @@ import dev.arkbuilders.canvas.presentation.edit.crop.CropWindow import dev.arkbuilders.canvas.presentation.edit.draw.DrawOperation import dev.arkbuilders.canvas.presentation.edit.resize.ResizeOperation import dev.arkbuilders.canvas.presentation.edit.rotate.RotateOperation +import timber.log.Timber import java.util.Stack +import kotlin.system.measureTimeMillis class EditManager { private val drawPaint: MutableState = mutableStateOf(defaultPaint()) @@ -165,6 +172,7 @@ class EditManager { operation.apply() } + private fun undoOperation(operation: Operation) { operation.undo() } @@ -255,6 +263,109 @@ class EditManager { _backgroundColor.value = Color(defaults.colorValue) } + fun getEditedImage(): ImageBitmap { + val size = this.imageSize + var bitmap = ImageBitmap( + size.width, + size.height, + ImageBitmapConfig.Argb8888 + ) + var pathBitmap: ImageBitmap? = null + val time = measureTimeMillis { + val matrix = Matrix() + if (this.drawPaths.isNotEmpty()) { + pathBitmap = ImageBitmap( + size.width, + size.height, + ImageBitmapConfig.Argb8888 + ) + val pathCanvas = Canvas(pathBitmap!!) + this.drawPaths.forEach { + pathCanvas.drawPath(it.path, it.paint) + } + } + backgroundImage.value?.let { + val canvas = Canvas(bitmap) + if (prevRotationAngle == 0f && drawPaths.isEmpty()) { + bitmap = it + return@let + } + if (prevRotationAngle != 0f) { + val centerX = size.width / 2f + val centerY = size.height / 2f + matrix.setRotate(prevRotationAngle, centerX, centerY) + } + canvas.nativeCanvas.drawBitmap( + it.asAndroidBitmap(), + matrix, + null + ) + if (drawPaths.isNotEmpty()) { + canvas.nativeCanvas.drawBitmap( + pathBitmap?.asAndroidBitmap()!!, + matrix, + null + ) + } + } ?: run { + val canvas = Canvas(bitmap) + if (prevRotationAngle != 0f) { + val centerX = size.width / 2 + val centerY = size.height / 2 + matrix.setRotate( + prevRotationAngle, + centerX.toFloat(), + centerY.toFloat() + ) + canvas.nativeCanvas.setMatrix(matrix) + } + canvas.drawRect( + Rect(Offset.Zero, size.toSize()), + backgroundPaint + ) + if (drawPaths.isNotEmpty()) { + canvas.drawImage( + pathBitmap!!, + Offset.Zero, + Paint() + ) + } + } + + } + Timber.tag("edit-viewmodel: getEditedImage").d( + "processing edits took ${time / 1000} s ${time % 1000} ms" + ) + return bitmap + } + + fun enterCropMode() { + toggleCropMode() + if (_isCropMode.value) { + val bitmap = getEditedImage() + setBackgroundImage2() + backgroundImage.value = bitmap + this.cropWindow.init( + bitmap.asAndroidBitmap() + ) + return + } + cancelCropMode() + scaleToFit() + cropWindow.close() + } + + fun enterRotateMode() { + toggleRotateMode() + if (isRotateMode.value) { + setBackgroundImage2() + scaleToFitOnEdit() + return + } + cancelRotateMode() + scaleToFit() + } + fun updateAvailableDrawAreaByMatrix() { val drawArea = backgroundImage.value?.let { val drawWidth = it.width * matrixScale.value @@ -273,6 +384,7 @@ class EditManager { } updateAvailableDrawArea(drawArea) } + fun updateAvailableDrawArea(bitmap: ImageBitmap? = backgroundImage.value) { if (bitmap == null) { resolution.value?.let { @@ -285,6 +397,7 @@ class EditManager { bitmap.height ) } + fun updateAvailableDrawArea(area: IntSize) { availableDrawAreaSize.value = area } @@ -392,6 +505,41 @@ class EditManager { else -> drawOperation } + fun isEligibleForUndoOrRedo(): Boolean = ( + !_isRotateMode.value && + !_isResizeMode.value && + !_isCropMode.value && + !_isEyeDropperMode.value && + !_isBlurMode.value + ) + + fun isEligibleForCropOrRotate(): Boolean { + return ( + !_isCropMode.value && + !_isResizeMode.value && + !_isEyeDropperMode.value && + !_isEraseMode.value && + !_isBlurMode.value + ) + } + + fun isEligibleForStrokeExpandOrErase(): Boolean = ( + !_isRotateMode.value && + !_isCropMode.value && + !_isResizeMode.value && + !_isEyeDropperMode.value && + !_isBlurMode.value + ) + + fun isEligibleForPanOrZoomMode(): Boolean = ( + !_isRotateMode.value && + !_isResizeMode.value && + !_isCropMode.value && + !_isEyeDropperMode.value && + !_isBlurMode.value && + !_isEraseMode.value + ) + fun undo() { if (canUndo.value) { val undoTask = undoStack.pop() @@ -555,6 +703,7 @@ class EditManager { fun toggleBlurMode() { _isBlurMode.value = !isBlurMode.value } + fun setPaintStrokeWidth(strokeWidth: Float) { drawPaint.value.strokeWidth = strokeWidth } @@ -663,6 +812,7 @@ class EditManager { ) ) } + class ImageViewParams( val drawArea: IntSize, val scale: ResizeOperation.Scale diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt index e5298bf..3ecd71b 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -325,7 +325,9 @@ private fun DrawContainer( return@onSizeChanged } - isZoomMode.value -> { return@onSizeChanged } + isZoomMode.value -> { + return@onSizeChanged + } else -> { scaleToFit() @@ -609,24 +611,14 @@ private fun EditMenuContent( .clip(CircleShape) .clickable { if ( - !editManager.isRotateMode.value && - !editManager.isResizeMode.value && - !editManager.isCropMode.value && - !editManager.isEyeDropperMode.value && - !editManager.isBlurMode.value + editManager.isEligibleForUndoOrRedo() ) { editManager.undo() } }, imageVector = ImageVector.vectorResource(R.drawable.ic_undo), tint = if ( - editManager.canUndo.value && ( - !editManager.isRotateMode.value && - !editManager.isResizeMode.value && - !editManager.isCropMode.value && - !editManager.isEyeDropperMode.value && - !editManager.isBlurMode.value - ) + editManager.canUndo.value && (editManager.isEligibleForUndoOrRedo()) ) MaterialTheme.colors.primary else Color.Black, contentDescription = null ) @@ -636,24 +628,12 @@ private fun EditMenuContent( .size(40.dp) .clip(CircleShape) .clickable { - if ( - !editManager.isRotateMode.value && - !editManager.isResizeMode.value && - !editManager.isCropMode.value && - !editManager.isEyeDropperMode.value && - !editManager.isBlurMode.value - ) editManager.redo() + if (editManager.isEligibleForUndoOrRedo()) editManager.redo() }, imageVector = ImageVector.vectorResource(R.drawable.ic_redo), tint = if ( editManager.canRedo.value && - ( - !editManager.isRotateMode.value && - !editManager.isResizeMode.value && - !editManager.isCropMode.value && - !editManager.isEyeDropperMode.value && - !editManager.isBlurMode.value - ) + (editManager.isEligibleForUndoOrRedo()) ) MaterialTheme.colors.primary else Color.Black, contentDescription = null ) @@ -699,15 +679,8 @@ private fun EditMenuContent( .size(40.dp) .clip(CircleShape) .clickable { - if ( - !editManager.isRotateMode.value && - !editManager.isCropMode.value && - !editManager.isResizeMode.value && - !editManager.isEyeDropperMode.value && - !editManager.isBlurMode.value - ) - viewModel.strokeSliderExpanded = - !viewModel.strokeSliderExpanded + if (editManager.isEligibleForStrokeExpandOrErase()) + viewModel.strokeSliderExpanded = !viewModel.strokeSliderExpanded }, imageVector = ImageVector.vectorResource(R.drawable.ic_line_weight), tint = if ( @@ -726,13 +699,7 @@ private fun EditMenuContent( .size(40.dp) .clip(CircleShape) .clickable { - if ( - !editManager.isRotateMode.value && - !editManager.isResizeMode.value && - !editManager.isCropMode.value && - !editManager.isEyeDropperMode.value && - !editManager.isBlurMode.value - ) + if (editManager.isEligibleForStrokeExpandOrErase()) editManager.toggleEraseMode() }, imageVector = ImageVector.vectorResource(R.drawable.ic_eraser), @@ -750,15 +717,7 @@ private fun EditMenuContent( .size(40.dp) .clip(CircleShape) .clickable { - if ( - !editManager.isRotateMode.value && - !editManager.isResizeMode.value && - !editManager.isCropMode.value && - !editManager.isEyeDropperMode.value && - !editManager.isBlurMode.value && - !editManager.isEraseMode.value - ) - editManager.toggleZoomMode() + if (editManager.isEligibleForPanOrZoomMode()) editManager.toggleZoomMode() }, imageVector = ImageVector.vectorResource(R.drawable.ic_zoom_in), tint = if ( @@ -775,15 +734,7 @@ private fun EditMenuContent( .size(40.dp) .clip(CircleShape) .clickable { - if ( - !editManager.isRotateMode.value && - !editManager.isResizeMode.value && - !editManager.isCropMode.value && - !editManager.isEyeDropperMode.value && - !editManager.isBlurMode.value && - !editManager.isEraseMode.value - ) - editManager.togglePanMode() + if (editManager.isEligibleForPanOrZoomMode()) editManager.togglePanMode() }, imageVector = ImageVector.vectorResource(R.drawable.ic_pan_tool), tint = if ( @@ -800,31 +751,9 @@ private fun EditMenuContent( .size(40.dp) .clip(CircleShape) .clickable { - editManager.apply { - if ( - !isRotateMode.value && - !isResizeMode.value && - !isEyeDropperMode.value && - !isEraseMode.value && - !isBlurMode.value - ) { - toggleCropMode() - viewModel.menusVisible = - !editManager.isCropMode.value - if (isCropMode.value) { - val bitmap = viewModel.getEditedImage() - setBackgroundImage2() - backgroundImage.value = bitmap - viewModel.editManager.cropWindow.init( - bitmap.asAndroidBitmap() - ) - return@clickable - } - editManager.cancelCropMode() - editManager.scaleToFit() - editManager.cropWindow.close() - } - } + if (!editManager.isEligibleForCropOrRotate()) return@clickable + editManager.enterCropMode() + viewModel.menusVisible = !editManager.isCropMode.value }, imageVector = ImageVector.vectorResource(R.drawable.ic_crop), tint = if ( @@ -840,26 +769,9 @@ private fun EditMenuContent( .size(40.dp) .clip(CircleShape) .clickable { - editManager.apply { - if ( - !isCropMode.value && - !isResizeMode.value && - !isEyeDropperMode.value && - !isEraseMode.value && - !isBlurMode.value - ) { - toggleRotateMode() - if (isRotateMode.value) { - setBackgroundImage2() - viewModel.menusVisible = - !editManager.isRotateMode.value - scaleToFitOnEdit() - return@clickable - } - cancelRotateMode() - scaleToFit() - } - } + if (!editManager.isEligibleForCropOrRotate()) return@clickable + editManager.enterRotateMode() + viewModel.menusVisible = !editManager.isRotateMode.value }, imageVector = ImageVector .vectorResource(R.drawable.ic_rotate_90_degrees_ccw), diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt index b5b2b7e..d00670d 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt @@ -295,82 +295,7 @@ class EditViewModel( return combinedBitmap } - fun getEditedImage(): ImageBitmap { - val size = editManager.imageSize - var bitmap = ImageBitmap( - size.width, - size.height, - ImageBitmapConfig.Argb8888 - ) - var pathBitmap: ImageBitmap? = null - val time = measureTimeMillis { - editManager.apply { - val matrix = Matrix() - if (editManager.drawPaths.isNotEmpty()) { - pathBitmap = ImageBitmap( - size.width, - size.height, - ImageBitmapConfig.Argb8888 - ) - val pathCanvas = Canvas(pathBitmap!!) - editManager.drawPaths.forEach { - pathCanvas.drawPath(it.path, it.paint) - } - } - backgroundImage.value?.let { - val canvas = Canvas(bitmap) - if (prevRotationAngle == 0f && drawPaths.isEmpty()) { - bitmap = it - return@let - } - if (prevRotationAngle != 0f) { - val centerX = size.width / 2f - val centerY = size.height / 2f - matrix.setRotate(prevRotationAngle, centerX, centerY) - } - canvas.nativeCanvas.drawBitmap( - it.asAndroidBitmap(), - matrix, - null - ) - if (drawPaths.isNotEmpty()) { - canvas.nativeCanvas.drawBitmap( - pathBitmap?.asAndroidBitmap()!!, - matrix, - null - ) - } - } ?: run { - val canvas = Canvas(bitmap) - if (prevRotationAngle != 0f) { - val centerX = size.width / 2 - val centerY = size.height / 2 - matrix.setRotate( - prevRotationAngle, - centerX.toFloat(), - centerY.toFloat() - ) - canvas.nativeCanvas.setMatrix(matrix) - } - canvas.drawRect( - Rect(Offset.Zero, size.toSize()), - backgroundPaint - ) - if (drawPaths.isNotEmpty()) { - canvas.drawImage( - pathBitmap!!, - Offset.Zero, - Paint() - ) - } - } - } - } - Timber.tag("edit-viewmodel: getEditedImage").d( - "processing edits took ${time / 1000} s ${time % 1000} ms" - ) - return bitmap - } + fun getEditedImage(): ImageBitmap = editManager.getEditedImage() fun confirmExit() = viewModelScope.launch { exitConfirmed = true From 49496c904f30886ff508e4dbe23b067242928832 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Thu, 21 Nov 2024 22:15:06 +0700 Subject: [PATCH 09/29] Save and load svg --- .../canvas/presentation/ArkCanvasFragment.kt | 11 +- .../canvas/presentation/drawing/EditCanvas.kt | 75 +++-- .../presentation/drawing/EditManager.kt | 4 +- .../canvas/presentation/edit/EditScreen.kt | 2 +- .../canvas/presentation/edit/EditViewModel.kt | 27 +- .../canvas/presentation/graphics/SVG.kt | 303 ------------------ .../resourceloader/BitmapResourceManager.kt | 2 - .../resourceloader/SvgResourceManager.kt | 16 +- .../presentation/utils/GraphicBrushExt.kt | 16 +- .../canvas/presentation/utils/SVG.kt | 46 ++- 10 files changed, 118 insertions(+), 384 deletions(-) delete mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/SVG.kt diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt index a7825de..5937e4d 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt @@ -17,6 +17,7 @@ import dev.arkbuilders.canvas.presentation.edit.EditScreen import dev.arkbuilders.canvas.presentation.edit.EditViewModel import dev.arkbuilders.canvas.presentation.resourceloader.BitmapResourceManager import dev.arkbuilders.canvas.presentation.resourceloader.CanvasResourceManager +import dev.arkbuilders.canvas.presentation.resourceloader.SvgResourceManager private const val imagePath = "image_path_param" @@ -26,7 +27,9 @@ class ArkCanvasFragment : Fragment() { lateinit var prefs: Preferences lateinit var viewModel: EditViewModel - lateinit var canvasResourceManager: CanvasResourceManager + lateinit var bitmapResourceManager: CanvasResourceManager + lateinit var svgResourceManager: CanvasResourceManager + lateinit var editManager: EditManager override fun onCreate(savedInstanceState: Bundle?) { @@ -37,7 +40,8 @@ class ArkCanvasFragment : Fragment() { val context = requireActivity().applicationContext prefs = Preferences(appCtx = context) editManager = EditManager() - canvasResourceManager = BitmapResourceManager(context = context, editManager = editManager) + bitmapResourceManager = BitmapResourceManager(context = context, editManager = editManager) + svgResourceManager = SvgResourceManager(editManager = editManager) viewModel = EditViewModel( primaryColor = 0xFF101828, launchedFromIntent = false, @@ -46,7 +50,8 @@ class ArkCanvasFragment : Fragment() { maxResolution = Resolution(350, 720), prefs = prefs, editManager = editManager, - canvasResourceManager = canvasResourceManager, + bitMapResourceManager = bitmapResourceManager, + svgResourceManager = svgResourceManager, ) } diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt index d8ae454..1d114b2 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt @@ -37,7 +37,9 @@ import dev.arkbuilders.canvas.presentation.edit.EditViewModel import dev.arkbuilders.canvas.presentation.edit.TransparencyChessBoardCanvas import dev.arkbuilders.canvas.presentation.edit.crop.CropWindow.Companion.computeDeltaX import dev.arkbuilders.canvas.presentation.edit.crop.CropWindow.Companion.computeDeltaY +import dev.arkbuilders.canvas.presentation.graphics.Size import dev.arkbuilders.canvas.presentation.picker.toDp +import dev.arkbuilders.canvas.presentation.utils.SVGCommand import dev.arkbuilders.canvas.presentation.utils.calculateRotationFromOneFingerGesture @Composable @@ -58,24 +60,26 @@ fun EditCanvas(viewModel: EditViewModel) { } Box(contentAlignment = Alignment.Center) { - val modifier = Modifier.size( - editManager.availableDrawAreaSize.value.width.toDp(), - editManager.availableDrawAreaSize.value.height.toDp() - ).graphicsLayer { - resetScaleAndTranslate() + val modifier = Modifier + .size( + editManager.availableDrawAreaSize.value.width.toDp(), + editManager.availableDrawAreaSize.value.height.toDp() + ) + .graphicsLayer { + resetScaleAndTranslate() - // Eraser leaves black line instead of erasing without this hack, it uses BlendMode.SrcOut - // https://stackoverflow.com/questions/65653560/jetpack-compose-applying-porterduffmode-to-image - // Provide a slight opacity to for compositing into an - // offscreen buffer to ensure blend modes are applied to empty pixel information - // By default any alpha != 1.0f will use a compositing layer by default - alpha = 0.99f + // Eraser leaves black line instead of erasing without this hack, it uses BlendMode.SrcOut + // https://stackoverflow.com/questions/65653560/jetpack-compose-applying-porterduffmode-to-image + // Provide a slight opacity to for compositing into an + // offscreen buffer to ensure blend modes are applied to empty pixel information + // By default any alpha != 1.0f will use a compositing layer by default + alpha = 0.99f - scaleX = scale - scaleY = scale - translationX = offset.x - translationY = offset.y - } + scaleX = scale + scaleY = scale + translationX = offset.x + translationY = offset.y + } TransparencyChessBoardCanvas(modifier, editManager) BackgroundCanvas(modifier, editManager) DrawCanvas(modifier, viewModel) @@ -85,7 +89,8 @@ fun EditCanvas(viewModel: EditViewModel) { editManager.isPanMode.value ) { Canvas( - Modifier.fillMaxSize() + Modifier + .fillMaxSize() .pointerInput(Any()) { forEachGesture { awaitPointerEventScope { @@ -101,6 +106,7 @@ fun EditCanvas(viewModel: EditViewModel) { editManager.rotate(angle) editManager.invalidatorTick.value++ } + else -> { if (editManager.isZoomMode.value) { scale *= event.calculateZoom() @@ -175,15 +181,40 @@ fun DrawCanvas(modifier: Modifier, viewModel: EditViewModel) { editManager.apply { drawOperation.draw(path) applyOperation() + + svg.apply { + val svgCommand = SVGCommand.MoveTo(eventX, eventY).apply { + //TODO Add color for paint + brushSizeId = Size.MEDIUM.id + } + addCommand(svgCommand) + } } + } MotionEvent.ACTION_MOVE -> { - path.quadraticBezierTo( + path.quadraticTo( currentPoint.x, currentPoint.y, (eventX + currentPoint.x) / 2, (eventY + currentPoint.y) / 2 ) + editManager.apply { + svg.apply { + addCommand( + SVGCommand.AbsQuadTo( + currentPoint.x, + currentPoint.y, + (eventX + currentPoint.x) / 2, + (eventY + currentPoint.y) / 2 + ).apply { + //TODO Add color for paint + brushSizeId = Size.MEDIUM.id + + }) + } + } + currentPoint.x = eventX currentPoint.y = eventY } @@ -194,13 +225,19 @@ fun DrawCanvas(modifier: Modifier, viewModel: EditViewModel) { ) { path.lineTo(currentPoint.x, currentPoint.y) } - editManager.clearRedoPath() editManager.updateRevised() path = Path() } + else -> {} } + editManager.svg.addPath( + DrawPath( + path = path, + paint = editManager.drawPaint.value + ) + ) } fun handleCropEvent(action: Int, eventX: Float, eventY: Float) { diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt index 2f5acc7..c1153ce 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt @@ -31,15 +31,17 @@ import dev.arkbuilders.canvas.presentation.edit.crop.CropWindow import dev.arkbuilders.canvas.presentation.edit.draw.DrawOperation import dev.arkbuilders.canvas.presentation.edit.resize.ResizeOperation import dev.arkbuilders.canvas.presentation.edit.rotate.RotateOperation +import dev.arkbuilders.canvas.presentation.utils.SVG import timber.log.Timber import java.util.Stack import kotlin.system.measureTimeMillis class EditManager { - private val drawPaint: MutableState = mutableStateOf(defaultPaint()) + val drawPaint: MutableState = mutableStateOf(defaultPaint()) private val _paintColor: MutableState = mutableStateOf(drawPaint.value.color) + val svg = SVG() val paintColor: State = _paintColor private val _backgroundColor = mutableStateOf(Color.Transparent) val backgroundColor: State = _backgroundColor diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt index 3ecd71b..6f6337c 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -361,7 +361,7 @@ private fun BoxScope.TopMenu( fragmentManager = fragmentManager, onDismissClick = { viewModel.showSavePathDialog = false }, onPositiveClick = { savePath -> - viewModel.saveImage(context, savePath) + viewModel.saveImage(savePath) } ) if (viewModel.showMoreOptionsPopup) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt index d00670d..75c5421 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmapConfig import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.asAndroidBitmap -import androidx.compose.ui.graphics.asComposePath import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.unit.toSize @@ -33,9 +32,7 @@ import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition import dev.arkbuilders.canvas.presentation.data.Preferences import dev.arkbuilders.canvas.presentation.data.Resolution -import dev.arkbuilders.canvas.presentation.drawing.DrawPath import dev.arkbuilders.canvas.presentation.drawing.EditManager -import dev.arkbuilders.canvas.presentation.graphics.SVG import dev.arkbuilders.canvas.presentation.resourceloader.CanvasResourceManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -53,8 +50,9 @@ class EditViewModel( private val imageUri: String?, private val maxResolution: Resolution, private val prefs: Preferences, - val editManager: EditManager, - private val canvasResourceManager: CanvasResourceManager, + val editManager: EditManager, + private val bitMapResourceManager: CanvasResourceManager, + private val svgResourceManager: CanvasResourceManager, ) : ViewModel() { var strokeSliderExpanded by mutableStateOf(false) var menusVisible by mutableStateOf(true) @@ -79,22 +77,13 @@ class EditViewModel( fun setPaths() { viewModelScope.launch { editManager.setPaintColor(Color.Blue) - val svgpaths = SVG.parse(Path("/storage/emulated/0/Documents/32254-1096105931.svg")) - svgpaths.getPaths().forEach { - val draw = DrawPath( - path = it.path.asComposePath(), - paint = Paint().apply { - color = Color(it.paint.color) - } - ) - editManager.addDrawPath(draw.path) - editManager.setPaintColor(draw.paint.color) - } + val svgPath = Path("/storage/emulated/0/Documents/love.svg") + svgResourceManager.loadResource(svgPath) } } init { -// setPaths() + setPaths() if (imageUri == null && imagePath == null) { viewModelScope.launch { editManager.initDefaults( @@ -140,10 +129,10 @@ class EditViewModel( editManager.scaleToFit() } - fun saveImage(context: Context, path: Path) { + fun saveImage(path: Path) { viewModelScope.launch(Dispatchers.IO) { isSavingImage = true - canvasResourceManager.saveResource(path) + svgResourceManager.saveResource(path) imageSaved = true isSavingImage = false showSavePathDialog = false diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/SVG.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/SVG.kt deleted file mode 100644 index c92b1b9..0000000 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/SVG.kt +++ /dev/null @@ -1,303 +0,0 @@ -package dev.arkbuilders.canvas.presentation.graphics - -import android.graphics.Paint -import android.util.Log -import android.graphics.Path as AndroidDrawPath -import android.util.Xml -import dev.arkbuilders.canvas.presentation.utils.DrawPath -import dev.arkbuilders.canvas.presentation.utils.getColorCode -import dev.arkbuilders.canvas.presentation.utils.getStrokeSize -import org.xmlpull.v1.XmlPullParser -import java.nio.file.Path -import kotlin.io.path.reader -import kotlin.io.path.writer - -class SVG { - private var strokeColor = Color.BLACK.value - private var strokeSize: Int = Size.TINY.id - private var fill = "none" - private var viewBox = ViewBox() - private val commands = ArrayDeque() - private val paths = ArrayDeque() - - private val paint - get() = Paint().also { - it.color = strokeColor.getColorCode() - it.style = Paint.Style.STROKE - it.strokeWidth = strokeSize.getStrokeSize() - it.strokeCap = Paint.Cap.ROUND - it.strokeJoin = Paint.Join.ROUND - it.isAntiAlias = true - } - - fun addCommand(command: SVGCommand) { - commands.addLast(command) - } - - fun addPath(path: DrawPath) { - paths.addLast(path) - } - - fun generate(path: Path) { - if (commands.isNotEmpty()) { - val xmlSerializer = Xml.newSerializer() - val pathData = commands.joinToString() - xmlSerializer.apply { - setOutput(path.writer()) - startDocument("utf-8", false) - startTag("", SVG_TAG) - attribute("", Attributes.VIEW_BOX, viewBox.toString()) - attribute("", Attributes.XML_NS_URI, XML_NS_URI) - startTag("", PATH_TAG) - attribute("", Attributes.Path.STROKE, strokeColor) - attribute("", Attributes.Path.FILL, fill) - attribute("", Attributes.Path.DATA, pathData) - endTag("", PATH_TAG) - endTag("", SVG_TAG) - endDocument() - } - } - } - - fun getPaths(): Collection = paths - - fun copy(): SVG = SVG().apply { - strokeColor = this@SVG.strokeColor - fill = this@SVG.fill - viewBox = this@SVG.viewBox - commands.addAll(this@SVG.commands) - paths.addAll(this@SVG.paths) - } - - private fun createCanvasPaths() { - if (commands.isNotEmpty()) { - if (paths.isNotEmpty()) paths.clear() - var path = AndroidDrawPath() - commands.forEach { command -> - strokeColor = command.paintColor - strokeSize = command.brushSizeId - when (command) { - is SVGCommand.MoveTo -> { - path = AndroidDrawPath() - path.moveTo(command.x, command.y) - } - - is SVGCommand.AbsQuadTo -> { - path.quadTo(command.x1, command.y1, command.x2, command.y2) - } - - is SVGCommand.AbsLineTo -> { - path.lineTo(command.x, command.y) - } - } - paths.addLast(DrawPath(path, paint.apply { - color = strokeColor.getColorCode() - strokeWidth = strokeSize.getStrokeSize() - })) - } - } - } - - companion object { - fun parse(path: Path): SVG = SVG().apply { - val xmlParser = Xml.newPullParser() - var pathData = "" - - xmlParser.apply { - setInput(path.reader()) - - var event = xmlParser.eventType - var pathCount = 0 - while (event != XmlPullParser.END_DOCUMENT) { - val tag = xmlParser.name - when (event) { - XmlPullParser.START_TAG -> { - when (tag) { - SVG_TAG -> { - viewBox = ViewBox.fromString( - getAttributeValue("", Attributes.VIEW_BOX) - ) - } - PATH_TAG -> { - pathCount += 1 - strokeColor = getAttributeValue("", Attributes.Path.STROKE) - fill = getAttributeValue("", Attributes.Path.FILL) - pathData = getAttributeValue("", Attributes.Path.DATA) - } - } - if (pathCount > 1) { - Log.d("svg", "found more than 1 path in file") - break - } - } - } - - event = next() - } - - pathData.split(COMMA).forEach { - val command = it.trim() - if (command.isEmpty()) return@forEach - val commandElements = command.split(" ") - when (command.first()) { - SVGCommand.MoveTo.CODE -> { - if (commandElements.size > 3) { - strokeColor = commandElements[3] - } - if (commandElements.size > 4) { - strokeSize = commandElements[4].toInt() - } - commands.addLast(SVGCommand.MoveTo.fromString(command).apply { - paintColor = strokeColor - brushSizeId = strokeSize - }) - } - SVGCommand.AbsLineTo.CODE -> { - if (commandElements.size > 3) { - strokeColor = commandElements[3] - } - if (commandElements.size > 4) { - strokeSize = commandElements[4].toInt() - } - commands.addLast(SVGCommand.MoveTo.fromString(command).apply { - paintColor = strokeColor - brushSizeId = strokeSize - }) - } - SVGCommand.AbsQuadTo.CODE -> { - if (commandElements.size > 5) { - strokeColor = commandElements[5] - } - if (commandElements.size > 6) { - strokeSize = commandElements[6].toInt() - } - commands.addLast(SVGCommand.AbsQuadTo.fromString(command).apply { - paintColor = strokeColor - brushSizeId = strokeSize - }) - } - else -> {} - } - } - - createCanvasPaths() - } - } - - private object Attributes { - const val VIEW_BOX = "viewBox" - const val XML_NS_URI = "xmlns" - - object Path { - const val STROKE = "stroke" - const val FILL = "fill" - const val DATA = "d" - } - } - } -} - -data class ViewBox( - val x: Float = 0f, - val y: Float = 0f, - val width: Float = 100f, - val height: Float = 100f -) { - override fun toString(): String = "$x $y $width $height" - - companion object { - fun fromString(string: String): ViewBox { - val viewBox = string.split(" ") - return ViewBox( - viewBox[0].toFloat(), - viewBox[1].toFloat(), - viewBox[2].toFloat(), - viewBox[3].toFloat() - ) - } - } -} - -sealed class SVGCommand { - - var paintColor = Color.BLACK.value - var brushSizeId = Size.TINY.id - - class MoveTo( - val x: Float, - val y: Float - ) : SVGCommand() { - override fun toString(): String = "$CODE $x $y $paintColor $brushSizeId" - - companion object { - const val CODE = 'M' - - fun fromString(string: String): SVGCommand { - val params = string.removePrefix("$CODE").trim().split(" ") - val x = params[0].toFloat() - val y = params[1].toFloat() - val colorCode = if (params.size > 2) params[2] else Color.BLACK.value - val strokeSizeId = if (params.size > 3) params[3].toInt() else Size.TINY.id - return MoveTo(x, y).apply { - paintColor = colorCode - brushSizeId = strokeSizeId - } - } - } - } - - class AbsLineTo( - val x: Float, - val y: Float - ) : SVGCommand() { - override fun toString(): String = "$CODE $x $y $paintColor $brushSizeId" - - companion object { - const val CODE = 'L' - - fun fromString(string: String): SVGCommand { - val params = string.removePrefix("$CODE").trim().split(" ") - val x = params[0].toFloat() - val y = params[1].toFloat() - val colorCode = if (params.size > 2) params[2] else Color.BLACK.value - val strokeSizeId = if (params.size > 3) params[3].toInt() else Size.TINY.id - return AbsLineTo(x, y).apply { - paintColor = colorCode - brushSizeId = strokeSizeId - } - } - } - } - - class AbsQuadTo( - val x1: Float, - val y1: Float, - val x2: Float, - val y2: Float - ) : SVGCommand() { - override fun toString(): String = "$CODE $x1 $y1 $x2 $y2 $paintColor $brushSizeId" - - companion object { - const val CODE = 'Q' - - fun fromString(string: String): SVGCommand { - val params = string.removePrefix("$CODE").trim().split(" ") - val x1 = params[0].toFloat() - val y1 = params[1].toFloat() - val x2 = params[2].toFloat() - val y2 = params[3].toFloat() - val colorCode = if (params.size > 4) params[4] else Color.BLACK.value - val strokeSizeId = if (params.size > 5) params[5].toInt() else Size.TINY.id - return AbsQuadTo(x1, y1, x2, y2).apply { - paintColor = colorCode - brushSizeId = strokeSizeId - } - } - } - } -} - -private const val COMMA = "," -private const val XML_NS_URI = "http://www.w3.org/2000/svg" -private const val SVG_TAG = "svg" -private const val PATH_TAG = "path" \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceManager.kt index 71bc45f..da39e82 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/BitmapResourceManager.kt @@ -36,8 +36,6 @@ class BitmapResourceManager( .asBitmap() .skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.NONE) - - private lateinit var bitMapResource: ImageBitmap override suspend fun loadResource(path: Path) { loadImage(path) } diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt index 7e74f83..5ffcd46 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt @@ -1,11 +1,7 @@ package dev.arkbuilders.canvas.presentation.resourceloader -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Paint -import androidx.compose.ui.graphics.asComposePath -import dev.arkbuilders.canvas.presentation.drawing.DrawPath import dev.arkbuilders.canvas.presentation.drawing.EditManager -import dev.arkbuilders.canvas.presentation.graphics.SVG +import dev.arkbuilders.canvas.presentation.utils.SVG import java.nio.file.Path class SvgResourceManager( @@ -14,19 +10,13 @@ class SvgResourceManager( override suspend fun loadResource(path: Path) { val svgpaths = SVG.parse(path) - svgpaths.getPaths().forEach { - val draw = DrawPath( - path = it.path.asComposePath(), - paint = Paint().apply { - color = Color(it.paint.color) - } - ) + svgpaths.getPaths().forEach {draw -> editManager.addDrawPath(draw.path) editManager.setPaintColor(draw.paint.color) } } override suspend fun saveResource(path: Path) { - TODO("Not yet implemented") + editManager.svg.generate(path) } } \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/GraphicBrushExt.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/GraphicBrushExt.kt index 7c2599a..cbdde21 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/GraphicBrushExt.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/GraphicBrushExt.kt @@ -29,13 +29,15 @@ fun Int.getStrokeSize(): Float { } fun Float.getBrushSizeId(): Int { - return when(this) { - Size.TINY.value -> Size.TINY.id - Size.SMALL.value -> Size.SMALL.id - Size.MEDIUM.value -> Size.MEDIUM.id - Size.LARGE.value -> Size.LARGE.id - Size.HUGE.value -> Size.HUGE.id - else -> { Size.TINY.id } + return when (this) { + in 0f..Size.TINY.value -> Size.TINY.id + in Size.TINY.value..Size.SMALL.value -> Size.SMALL.id + in Size.SMALL.value..Size.MEDIUM.value -> Size.MEDIUM.id + in Size.MEDIUM.value..Size.LARGE.value -> Size.LARGE.id + in Size.LARGE.value..Size.HUGE.value -> Size.HUGE.id + else -> { + Size.TINY.id + } } } diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt index 67ac3a7..412254e 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt @@ -3,19 +3,18 @@ package dev.arkbuilders.canvas.presentation.utils import android.graphics.Paint import android.util.Log import android.util.Xml +import androidx.compose.ui.graphics.Path as ComposePath +import androidx.compose.ui.graphics.asComposePaint +import dev.arkbuilders.canvas.presentation.drawing.DrawPath import dev.arkbuilders.canvas.presentation.graphics.Color import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlSerializer import java.nio.file.Path import kotlin.io.path.reader import kotlin.io.path.writer import android.graphics.Path as AndroidDrawPath -data class DrawPath( - val path: android.graphics.Path, - val paint: Paint -) - class SVG { private var strokeColor = Color.BLACK.value private var strokeSize: Int = dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id @@ -42,7 +41,7 @@ class SVG { paths.addLast(path) } - fun generate(path: Path) { + fun generate(path: Path): XmlSerializer? { if (commands.isNotEmpty()) { val xmlSerializer = Xml.newSerializer() val pathData = commands.joinToString() @@ -60,7 +59,9 @@ class SVG { endTag("", SVG_TAG) endDocument() } + return xmlSerializer } + return null } fun getPaths(): Collection = paths @@ -76,28 +77,34 @@ class SVG { private fun createCanvasPaths() { if (commands.isNotEmpty()) { if (paths.isNotEmpty()) paths.clear() - var path = AndroidDrawPath() + var path = ComposePath() commands.forEach { command -> strokeColor = command.paintColor strokeSize = command.brushSizeId when (command) { is SVGCommand.MoveTo -> { - path = AndroidDrawPath() + path = ComposePath() path.moveTo(command.x, command.y) } is SVGCommand.AbsQuadTo -> { - path.quadTo(command.x1, command.y1, command.x2, command.y2) + path.quadraticTo(command.x1, command.y1, command.x2, command.y2) } is SVGCommand.AbsLineTo -> { path.lineTo(command.x, command.y) } } - paths.addLast(DrawPath(path, paint.apply { - color = strokeColor.getColorCode() - strokeWidth = strokeSize.getStrokeSize() - })) + + paths.addLast( + DrawPath( + path = path, + paint = paint.apply { + color = Color.BLACK.code + strokeWidth = 3f + }.asComposePaint() + ) + ) } } } @@ -122,6 +129,7 @@ class SVG { getAttributeValue("", Attributes.VIEW_BOX) ) } + PATH_TAG -> { pathCount += 1 strokeColor = getAttributeValue("", Attributes.Path.STROKE) @@ -156,6 +164,7 @@ class SVG { brushSizeId = strokeSize }) } + SVGCommand.AbsLineTo.CODE -> { if (commandElements.size > 3) { strokeColor = commandElements[3] @@ -168,6 +177,7 @@ class SVG { brushSizeId = strokeSize }) } + SVGCommand.AbsQuadTo.CODE -> { if (commandElements.size > 5) { strokeColor = commandElements[5] @@ -180,6 +190,7 @@ class SVG { brushSizeId = strokeSize }) } + else -> {} } } @@ -241,7 +252,8 @@ sealed class SVGCommand { val x = params[0].toFloat() val y = params[1].toFloat() val colorCode = if (params.size > 2) params[2] else Color.BLACK.value - val strokeSizeId = if (params.size > 3) params[3].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + val strokeSizeId = + if (params.size > 3) params[3].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id return MoveTo(x, y).apply { paintColor = colorCode brushSizeId = strokeSizeId @@ -264,7 +276,8 @@ sealed class SVGCommand { val x = params[0].toFloat() val y = params[1].toFloat() val colorCode = if (params.size > 2) params[2] else Color.BLACK.value - val strokeSizeId = if (params.size > 3) params[3].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + val strokeSizeId = + if (params.size > 3) params[3].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id return AbsLineTo(x, y).apply { paintColor = colorCode brushSizeId = strokeSizeId @@ -291,7 +304,8 @@ sealed class SVGCommand { val x2 = params[2].toFloat() val y2 = params[3].toFloat() val colorCode = if (params.size > 4) params[4] else Color.BLACK.value - val strokeSizeId = if (params.size > 5) params[5].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + val strokeSizeId = + if (params.size > 5) params[5].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id return AbsQuadTo(x1, y1, x2, y2).apply { paintColor = colorCode brushSizeId = strokeSizeId From 28cd02b9cd162f99014ad1ae4095b02c53e13b18 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Fri, 22 Nov 2024 21:57:54 +0700 Subject: [PATCH 10/29] Rename variables --- .../canvas/presentation/resourceloader/SvgResourceManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt index 5ffcd46..00428b1 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt @@ -9,8 +9,8 @@ class SvgResourceManager( ): CanvasResourceManager { override suspend fun loadResource(path: Path) { - val svgpaths = SVG.parse(path) - svgpaths.getPaths().forEach {draw -> + val svgPaths = SVG.parse(path) + svgPaths.getPaths().forEach { draw -> editManager.addDrawPath(draw.path) editManager.setPaintColor(draw.paint.color) } From 780cc954e688d63bc8bd5bc9fdceab5ce931b233 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 23 Nov 2024 16:20:49 +0700 Subject: [PATCH 11/29] Switch based on file extension --- .../canvas/presentation/edit/EditViewModel.kt | 75 +++---------------- 1 file changed, 9 insertions(+), 66 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt index 75c5421..f8e7f39 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt @@ -41,6 +41,7 @@ import timber.log.Timber import java.io.File import java.nio.file.Path import kotlin.io.path.Path +import kotlin.io.path.name import kotlin.system.measureTimeMillis class EditViewModel( @@ -74,10 +75,10 @@ class EditViewModel( private val _usedColors = mutableListOf() val usedColors: List = _usedColors - fun setPaths() { + private fun setPaths() { viewModelScope.launch { editManager.setPaintColor(Color.Blue) - val svgPath = Path("/storage/emulated/0/Documents/love.svg") + val svgPath = Path("/storage/emulated/0/Documents/newcolor.svg") svgResourceManager.loadResource(svgPath) } } @@ -110,29 +111,17 @@ class EditViewModel( fun loadImage(context: Context) { isLoaded = true - imagePath?.let { - loadImageWithPath( - context, - imagePath, - editManager - ) - return - } - imageUri?.let { - loadImageWithUri( - context, - imageUri, - editManager - ) - return - } editManager.scaleToFit() } fun saveImage(path: Path) { viewModelScope.launch(Dispatchers.IO) { isSavingImage = true - svgResourceManager.saveResource(path) + if (path.name.endsWith("svg")) { + svgResourceManager.saveResource(path) + } else { + bitMapResourceManager.saveResource(path) + } imageSaved = true isSavingImage = false showSavePathDialog = false @@ -339,50 +328,4 @@ class EditViewModel( companion object { private const val KEEP_USED_COLORS = 20 } -} - -private fun loadImageWithPath( - context: Context, - image: Path, - editManager: EditManager -) { - initGlideBuilder(context) - .load(image.toFile()) - .loadInto(editManager) -} - -private fun loadImageWithUri( - context: Context, - uri: String, - editManager: EditManager -) { - initGlideBuilder(context) - .load(uri.toUri()) - .loadInto(editManager) -} - -private fun initGlideBuilder(context: Context) = Glide - .with(context) - .asBitmap() - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - -private fun RequestBuilder.loadInto( - editManager: EditManager -) { - into(object : CustomTarget() { - override fun onResourceReady( - bitmap: Bitmap, - transition: Transition? - ) { - editManager.apply { - val image = bitmap.asImageBitmap() - backgroundImage.value = image - setOriginalBackgroundImage(image) - scaleToFit() - } - } - - override fun onLoadCleared(placeholder: Drawable?) {} - }) -} +} \ No newline at end of file From 14d3c5767fbfa3e6088b7af6f06450a6638f0222 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Mon, 25 Nov 2024 22:40:49 +0700 Subject: [PATCH 12/29] Use Ulong color --- .../canvas/presentation/drawing/EditCanvas.kt | 10 ++--- .../presentation/drawing/EditManager.kt | 4 +- .../canvas/presentation/edit/EditViewModel.kt | 30 +++++++------- .../canvas/presentation/utils/SVG.kt | 41 ++++++++++--------- 4 files changed, 43 insertions(+), 42 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt index 1d114b2..78bf699 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt @@ -185,7 +185,8 @@ fun DrawCanvas(modifier: Modifier, viewModel: EditViewModel) { svg.apply { val svgCommand = SVGCommand.MoveTo(eventX, eventY).apply { //TODO Add color for paint - brushSizeId = Size.MEDIUM.id + paintColor = editManager.currentPaint.color.value + brushSizeId = Size.MEDIUM.id } addCommand(svgCommand) } @@ -208,9 +209,8 @@ fun DrawCanvas(modifier: Modifier, viewModel: EditViewModel) { (eventX + currentPoint.x) / 2, (eventY + currentPoint.y) / 2 ).apply { - //TODO Add color for paint - brushSizeId = Size.MEDIUM.id - + paintColor = editManager.currentPaint.color.value + brushSizeId = Size.MEDIUM.id }) } } @@ -235,7 +235,7 @@ fun DrawCanvas(modifier: Modifier, viewModel: EditViewModel) { editManager.svg.addPath( DrawPath( path = path, - paint = editManager.drawPaint.value + paint = editManager.currentPaint ) ) } diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt index c1153ce..f81fcc5 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt @@ -37,7 +37,7 @@ import java.util.Stack import kotlin.system.measureTimeMillis class EditManager { - val drawPaint: MutableState = mutableStateOf(defaultPaint()) + private val drawPaint: MutableState = mutableStateOf(defaultPaint()) private val _paintColor: MutableState = mutableStateOf(drawPaint.value.color) @@ -72,7 +72,7 @@ class EditManager { val cropOperation = CropOperation(this) val blurOperation = BlurOperation(this) - private val currentPaint: Paint + val currentPaint: Paint get() = when (true) { isEraseMode.value -> erasePaint else -> drawPaint.value diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt index f8e7f39..c66f183 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt @@ -78,7 +78,7 @@ class EditViewModel( private fun setPaths() { viewModelScope.launch { editManager.setPaintColor(Color.Blue) - val svgPath = Path("/storage/emulated/0/Documents/newcolor.svg") + val svgPath = Path("/storage/emulated/0/Documents/color.svg") svgResourceManager.loadResource(svgPath) } } @@ -93,20 +93,20 @@ class EditViewModel( ) } } - viewModelScope.launch { - _usedColors.addAll(prefs.readUsedColors()) - - val color = if (_usedColors.isNotEmpty()) { - _usedColors.last() - } else { - val defaultColor = Color.Blue - - _usedColors.add(defaultColor) - defaultColor - } - - editManager.setPaintColor(color) - } +// viewModelScope.launch { +// _usedColors.addAll(prefs.readUsedColors()) +// +// val color = if (_usedColors.isNotEmpty()) { +// _usedColors.last() +// } else { +// val defaultColor = Color.Blue +// +// _usedColors.add(defaultColor) +// defaultColor +// } +// +// editManager.setPaintColor(color) +// } } fun loadImage(context: Context) { diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt index 412254e..9e2f1d8 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt @@ -1,22 +1,24 @@ package dev.arkbuilders.canvas.presentation.utils -import android.graphics.Paint import android.util.Log import android.util.Xml +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.PaintingStyle +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.Path as ComposePath import androidx.compose.ui.graphics.asComposePaint import dev.arkbuilders.canvas.presentation.drawing.DrawPath -import dev.arkbuilders.canvas.presentation.graphics.Color import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlSerializer import java.nio.file.Path import kotlin.io.path.reader import kotlin.io.path.writer -import android.graphics.Path as AndroidDrawPath class SVG { - private var strokeColor = Color.BLACK.value + private var strokeColor: ULong = Color.Black.value private var strokeSize: Int = dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id private var fill = "none" private var viewBox = ViewBox() @@ -25,11 +27,10 @@ class SVG { private val paint get() = Paint().also { - it.color = strokeColor.getColorCode() - it.style = Paint.Style.STROKE + it.style = PaintingStyle.Stroke it.strokeWidth = strokeSize.getStrokeSize() - it.strokeCap = Paint.Cap.ROUND - it.strokeJoin = Paint.Join.ROUND + it.strokeCap = StrokeCap.Round + it.strokeJoin = StrokeJoin.Round it.isAntiAlias = true } @@ -52,14 +53,14 @@ class SVG { attribute("", Attributes.VIEW_BOX, viewBox.toString()) attribute("", Attributes.XML_NS_URI, XML_NS_URI) startTag("", PATH_TAG) - attribute("", Attributes.Path.STROKE, strokeColor) + attribute("", Attributes.Path.STROKE, strokeColor.toString()) attribute("", Attributes.Path.FILL, fill) attribute("", Attributes.Path.DATA, pathData) endTag("", PATH_TAG) endTag("", SVG_TAG) endDocument() } - return xmlSerializer + return xmlSerializer } return null } @@ -100,9 +101,9 @@ class SVG { DrawPath( path = path, paint = paint.apply { - color = Color.BLACK.code + color = Color(command.paintColor) strokeWidth = 3f - }.asComposePaint() + } ) ) } @@ -132,7 +133,7 @@ class SVG { PATH_TAG -> { pathCount += 1 - strokeColor = getAttributeValue("", Attributes.Path.STROKE) + strokeColor = getAttributeValue("", Attributes.Path.STROKE).toULong() fill = getAttributeValue("", Attributes.Path.FILL) pathData = getAttributeValue("", Attributes.Path.DATA) } @@ -154,7 +155,7 @@ class SVG { when (command.first()) { SVGCommand.MoveTo.CODE -> { if (commandElements.size > 3) { - strokeColor = commandElements[3] + strokeColor = commandElements[3].toULong() } if (commandElements.size > 4) { strokeSize = commandElements[4].toInt() @@ -167,7 +168,7 @@ class SVG { SVGCommand.AbsLineTo.CODE -> { if (commandElements.size > 3) { - strokeColor = commandElements[3] + strokeColor = commandElements[3].toULong() } if (commandElements.size > 4) { strokeSize = commandElements[4].toInt() @@ -180,7 +181,7 @@ class SVG { SVGCommand.AbsQuadTo.CODE -> { if (commandElements.size > 5) { - strokeColor = commandElements[5] + strokeColor = commandElements[5].toULong() } if (commandElements.size > 6) { strokeSize = commandElements[6].toInt() @@ -235,7 +236,7 @@ data class ViewBox( sealed class SVGCommand { - var paintColor = Color.BLACK.value + var paintColor: ULong = Color.Black.value var brushSizeId = dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id class MoveTo( @@ -251,7 +252,7 @@ sealed class SVGCommand { val params = string.removePrefix("$CODE").trim().split(" ") val x = params[0].toFloat() val y = params[1].toFloat() - val colorCode = if (params.size > 2) params[2] else Color.BLACK.value + val colorCode = if (params.size > 2) params[2].toULong() else Color.Black.value val strokeSizeId = if (params.size > 3) params[3].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id return MoveTo(x, y).apply { @@ -275,7 +276,7 @@ sealed class SVGCommand { val params = string.removePrefix("$CODE").trim().split(" ") val x = params[0].toFloat() val y = params[1].toFloat() - val colorCode = if (params.size > 2) params[2] else Color.BLACK.value + val colorCode = if (params.size > 2) params[2].toULong() else Color.Black.value val strokeSizeId = if (params.size > 3) params[3].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id return AbsLineTo(x, y).apply { @@ -303,7 +304,7 @@ sealed class SVGCommand { val y1 = params[1].toFloat() val x2 = params[2].toFloat() val y2 = params[3].toFloat() - val colorCode = if (params.size > 4) params[4] else Color.BLACK.value + val colorCode = if (params.size > 4) params[4].toULong() else Color.Black.value val strokeSizeId = if (params.size > 5) params[5].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id return AbsQuadTo(x1, y1, x2, y2).apply { From 49b522a76cab7d700ac388b4889e39420adf3ac3 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Tue, 26 Nov 2024 22:41:33 +0700 Subject: [PATCH 13/29] Load resource based on extension --- .../canvas/presentation/ArkCanvasFragment.kt | 3 +- .../canvas/presentation/edit/EditViewModel.kt | 72 ++++++++++--------- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt index 5937e4d..f33707f 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt @@ -18,6 +18,7 @@ import dev.arkbuilders.canvas.presentation.edit.EditViewModel import dev.arkbuilders.canvas.presentation.resourceloader.BitmapResourceManager import dev.arkbuilders.canvas.presentation.resourceloader.CanvasResourceManager import dev.arkbuilders.canvas.presentation.resourceloader.SvgResourceManager +import kotlin.io.path.Path private const val imagePath = "image_path_param" @@ -45,7 +46,7 @@ class ArkCanvasFragment : Fragment() { viewModel = EditViewModel( primaryColor = 0xFF101828, launchedFromIntent = false, - imagePath = null, + imagePath = Path("/storage/emulated/0/Documents/improvement.png"), imageUri = null, maxResolution = Resolution(350, 720), prefs = prefs, diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt index c66f183..2f01b18 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt @@ -4,7 +4,6 @@ import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.Matrix -import android.graphics.drawable.Drawable import android.net.Uri import android.view.MotionEvent import androidx.compose.runtime.getValue @@ -18,18 +17,11 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmapConfig import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.asAndroidBitmap -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.unit.toSize import androidx.core.content.FileProvider -import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestBuilder -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.transition.Transition import dev.arkbuilders.canvas.presentation.data.Preferences import dev.arkbuilders.canvas.presentation.data.Resolution import dev.arkbuilders.canvas.presentation.drawing.EditManager @@ -40,7 +32,6 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.io.File import java.nio.file.Path -import kotlin.io.path.Path import kotlin.io.path.name import kotlin.system.measureTimeMillis @@ -75,38 +66,51 @@ class EditViewModel( private val _usedColors = mutableListOf() val usedColors: List = _usedColors - private fun setPaths() { + private fun loadResource(path: Path) { viewModelScope.launch { - editManager.setPaintColor(Color.Blue) - val svgPath = Path("/storage/emulated/0/Documents/color.svg") - svgResourceManager.loadResource(svgPath) + if (path.name.endsWith(".png")) { + bitMapResourceManager.loadResource(path) + } else { + svgResourceManager.loadResource(path) + } + } + } + + private fun loadUsedColors() { + viewModelScope.launch { + _usedColors.addAll(prefs.readUsedColors()) + + val color = if (_usedColors.isNotEmpty()) { + _usedColors.last() + } else { + val defaultColor = Color.Blue + + _usedColors.add(defaultColor) + defaultColor + } + + editManager.setPaintColor(color) + } + } + + private fun initDefaults() { + viewModelScope.launch { + editManager.initDefaults( + prefs.readDefaults(), + maxResolution + ) } } init { - setPaths() + imagePath?.let { + loadResource(it) + } if (imageUri == null && imagePath == null) { - viewModelScope.launch { - editManager.initDefaults( - prefs.readDefaults(), - maxResolution - ) - } + initDefaults() } -// viewModelScope.launch { -// _usedColors.addAll(prefs.readUsedColors()) -// -// val color = if (_usedColors.isNotEmpty()) { -// _usedColors.last() -// } else { -// val defaultColor = Color.Blue -// -// _usedColors.add(defaultColor) -// defaultColor -// } -// -// editManager.setPaintColor(color) -// } + initDefaults() + loadUsedColors() } fun loadImage(context: Context) { From b836a955b456b4eb2e1103e23b48a68d30b5008f Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Wed, 27 Nov 2024 22:27:22 +0700 Subject: [PATCH 14/29] Add file picker screen to test canvas --- .../canvas/presentation/ArkCanvasFragment.kt | 24 +++++--- .../canvas/presentation/edit/EditViewModel.kt | 3 - .../sample/canvas/CanvasActivity.kt | 8 +-- .../sample/canvas/FilePickerFragment.kt | 61 +++++++++++++++++++ .../main/res/layout/fragment_file_picker.xml | 12 ++++ 5 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 sample/src/main/java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt create mode 100644 sample/src/main/res/layout/fragment_file_picker.xml diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt index f33707f..0bbb9db 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt @@ -18,25 +18,26 @@ import dev.arkbuilders.canvas.presentation.edit.EditViewModel import dev.arkbuilders.canvas.presentation.resourceloader.BitmapResourceManager import dev.arkbuilders.canvas.presentation.resourceloader.CanvasResourceManager import dev.arkbuilders.canvas.presentation.resourceloader.SvgResourceManager +import java.nio.file.Path import kotlin.io.path.Path private const val imagePath = "image_path_param" class ArkCanvasFragment : Fragment() { - private var imagePathParam: String? = null + private lateinit var imagePathParam: String - lateinit var prefs: Preferences + private lateinit var prefs: Preferences - lateinit var viewModel: EditViewModel - lateinit var bitmapResourceManager: CanvasResourceManager - lateinit var svgResourceManager: CanvasResourceManager + private lateinit var viewModel: EditViewModel + private lateinit var bitmapResourceManager: CanvasResourceManager + private lateinit var svgResourceManager: CanvasResourceManager lateinit var editManager: EditManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { - imagePathParam = it.getString(imagePath) + imagePathParam = it.getString(imagePath) ?: "" } val context = requireActivity().applicationContext prefs = Preferences(appCtx = context) @@ -46,7 +47,7 @@ class ArkCanvasFragment : Fragment() { viewModel = EditViewModel( primaryColor = 0xFF101828, launchedFromIntent = false, - imagePath = Path("/storage/emulated/0/Documents/improvement.png"), + imagePath = pathFromString(), imageUri = null, maxResolution = Resolution(350, 720), prefs = prefs, @@ -56,6 +57,14 @@ class ArkCanvasFragment : Fragment() { ) } + private fun pathFromString(): Path?{ + return if (imagePathParam.isEmpty()) { + null + } else { + Path(imagePathParam) + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -84,7 +93,6 @@ class ArkCanvasFragment : Fragment() { onSaveSvg = { /*TODO*/ }, viewModel = viewModel ) -// EditCanvas(viewModel = viewModel) } } } diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt index 2f01b18..e073c94 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt @@ -106,9 +106,6 @@ class EditViewModel( imagePath?.let { loadResource(it) } - if (imageUri == null && imagePath == null) { - initDefaults() - } initDefaults() loadUsedColors() } diff --git a/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasActivity.kt b/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasActivity.kt index 8703a42..bc767c3 100644 --- a/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasActivity.kt +++ b/sample/src/main/java/dev/arkbuilders/sample/canvas/CanvasActivity.kt @@ -1,22 +1,18 @@ package dev.arkbuilders.sample.canvas -import android.annotation.SuppressLint import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import dev.arkbuilders.canvas.presentation.ArkCanvasFragment import dev.arkbuilders.sample.R class CanvasActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_canvas) - val canvasFragment = ArkCanvasFragment.newInstance( - param1 = "imagePath" - ) + val filePickerFragment = FilePickerFragment.newInstance() supportFragmentManager .beginTransaction() - .replace(R.id.canvas_content, canvasFragment) + .replace(R.id.canvas_content, filePickerFragment) .commit() } } \ No newline at end of file diff --git a/sample/src/main/java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt b/sample/src/main/java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt new file mode 100644 index 0000000..788924e --- /dev/null +++ b/sample/src/main/java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt @@ -0,0 +1,61 @@ +package dev.arkbuilders.sample.canvas + +import android.os.Build +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.RequiresApi +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.FragmentManager +import dev.arkbuilders.canvas.presentation.ArkCanvasFragment +import dev.arkbuilders.canvas.presentation.picker.PickerScreen +import dev.arkbuilders.sample.R +import java.nio.file.Path + +class FilePickerFragment : Fragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_file_picker, container, false) + } + + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val composeView = view.findViewById(dev.arkbuilders.canvas.R.id.compose_view) + + composeView.apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + setContent { + // Set Content here + PickerScreen(fragmentManager = childFragmentManager, + onNavigateToEdit = { path, resolution -> + onNavigateToEdit(path, parentFragmentManager) + }) + } + } + } + + private fun onNavigateToEdit(path: Path?, fragmentManager: FragmentManager) { + val canvasFragment = ArkCanvasFragment.newInstance( + param1 = path?.toString() ?: "" + ) + + fragmentManager + .beginTransaction() + .replace(R.id.canvas_content, canvasFragment) + .commit() + } + + companion object { + @JvmStatic + fun newInstance() = FilePickerFragment() + } +} \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_file_picker.xml b/sample/src/main/res/layout/fragment_file_picker.xml new file mode 100644 index 0000000..5d694b3 --- /dev/null +++ b/sample/src/main/res/layout/fragment_file_picker.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file From 2d63e781c6580c3009e43f3e01f6a984db25cbf4 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Wed, 27 Nov 2024 23:00:22 +0700 Subject: [PATCH 15/29] Improve edit current svg --- .../arkbuilders/canvas/presentation/drawing/EditCanvas.kt | 1 - .../presentation/resourceloader/SvgResourceManager.kt | 1 + .../java/dev/arkbuilders/canvas/presentation/utils/SVG.kt | 8 ++++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt index 78bf699..a886649 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt @@ -184,7 +184,6 @@ fun DrawCanvas(modifier: Modifier, viewModel: EditViewModel) { svg.apply { val svgCommand = SVGCommand.MoveTo(eventX, eventY).apply { - //TODO Add color for paint paintColor = editManager.currentPaint.color.value brushSizeId = Size.MEDIUM.id } diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt index 00428b1..5b11448 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/resourceloader/SvgResourceManager.kt @@ -14,6 +14,7 @@ class SvgResourceManager( editManager.addDrawPath(draw.path) editManager.setPaintColor(draw.paint.color) } + editManager.svg.addAll(svgPaths.getCommands()) } override suspend fun saveResource(path: Path) { diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt index 9e2f1d8..6b4e670 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt @@ -38,6 +38,10 @@ class SVG { commands.addLast(command) } + fun getCommands(): ArrayDeque { + return commands + } + fun addPath(path: DrawPath) { paths.addLast(path) } @@ -110,6 +114,10 @@ class SVG { } } + fun addAll(commands: ArrayDeque) { + commands.addAll(commands) + } + companion object { fun parse(path: Path): SVG = SVG().apply { val xmlParser = Xml.newPullParser() From 4d01477f4c6040978b70763575a61969dee07580 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Fri, 29 Nov 2024 21:11:10 +0700 Subject: [PATCH 16/29] Remove unneeded require api annotation --- .../java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sample/src/main/java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt b/sample/src/main/java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt index 788924e..aecf458 100644 --- a/sample/src/main/java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt +++ b/sample/src/main/java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt @@ -1,14 +1,12 @@ package dev.arkbuilders.sample.canvas -import android.os.Build import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.annotation.RequiresApi import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import dev.arkbuilders.canvas.presentation.ArkCanvasFragment import dev.arkbuilders.canvas.presentation.picker.PickerScreen @@ -23,7 +21,6 @@ class FilePickerFragment : Fragment() { return inflater.inflate(R.layout.fragment_file_picker, container, false) } - @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) From 1fa95b6e1728eec292f5dd5d1eee2f7864fe83f8 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Wed, 4 Dec 2024 21:21:19 +0700 Subject: [PATCH 17/29] Handle navigate back --- .../canvas/presentation/ArkCanvasFragment.kt | 4 +- .../canvas/presentation/edit/EditScreen.kt | 71 +++++-------------- .../sample/canvas/FilePickerFragment.kt | 7 +- 3 files changed, 22 insertions(+), 60 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt index 0bbb9db..5ff31c1 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/ArkCanvasFragment.kt @@ -86,8 +86,8 @@ class ArkCanvasFragment : Fragment() { EditScreen( imagePath = null, imageUri = null, - fragmentManager = childFragmentManager, - navigateBack = { /*TODO*/ }, + fragmentManager = requireActivity().supportFragmentManager, + navigateBack = { requireActivity().supportFragmentManager.popBackStackImmediate() }, launchedFromIntent = false, maxResolution = Resolution(350, 720), onSaveSvg = { /*TODO*/ }, diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt index 6f6337c..58e109b 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -4,8 +4,6 @@ package dev.arkbuilders.canvas.presentation.edit import android.os.Build import android.view.MotionEvent -import android.widget.Toast -import androidx.activity.compose.BackHandler import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween @@ -13,40 +11,40 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.clickable import androidx.compose.foundation.background -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.TextButton import androidx.compose.material.AlertDialog import androidx.compose.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme import androidx.compose.material.Slider +import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowLeft import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -120,48 +118,11 @@ fun EditScreen( viewModel = viewModel, navigateBack = { navigateBack() - viewModel.isLoaded = false +// viewModel.isLoaded = false }, launchedFromIntent = launchedFromIntent, ) - BackHandler { - val editManager = viewModel.editManager - if ( - editManager.isCropMode.value || editManager.isRotateMode.value || - editManager.isResizeMode.value || editManager.isEyeDropperMode.value || - editManager.isBlurMode.value - ) { - viewModel.cancelOperation() - return@BackHandler - } - if (editManager.isZoomMode.value) { - editManager.toggleZoomMode() - return@BackHandler - } - if (editManager.isPanMode.value) { - editManager.togglePanMode() - return@BackHandler - } - if (editManager.canUndo.value) { - editManager.undo() - return@BackHandler - } - if (viewModel.exitConfirmed) { - if (launchedFromIntent) - context.getActivity()?.finish() - else - navigateBack() - return@BackHandler - } - if (!viewModel.exitConfirmed) { - Toast.makeText(context, "Tap back again to exit", Toast.LENGTH_SHORT) - .show() - viewModel.confirmExit() - return@BackHandler - } - } - HandleImageSavedEffect(viewModel, launchedFromIntent, navigateBack) if (!showDefaultsDialog.value) diff --git a/sample/src/main/java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt b/sample/src/main/java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt index aecf458..ba3ed12 100644 --- a/sample/src/main/java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt +++ b/sample/src/main/java/dev/arkbuilders/sample/canvas/FilePickerFragment.kt @@ -32,9 +32,9 @@ class FilePickerFragment : Fragment() { ) setContent { // Set Content here - PickerScreen(fragmentManager = childFragmentManager, + PickerScreen(fragmentManager = requireActivity().supportFragmentManager, onNavigateToEdit = { path, resolution -> - onNavigateToEdit(path, parentFragmentManager) + onNavigateToEdit(path, requireActivity().supportFragmentManager) }) } } @@ -47,7 +47,8 @@ class FilePickerFragment : Fragment() { fragmentManager .beginTransaction() - .replace(R.id.canvas_content, canvasFragment) + .add(R.id.canvas_content, canvasFragment) + .addToBackStack(null) .commit() } From 12435bc6a68f17803ec18c7622f6aaef4b8c022c Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Wed, 4 Dec 2024 22:16:52 +0700 Subject: [PATCH 18/29] Encapsulate logic of edit manager --- .../presentation/drawing/EditManager.kt | 47 ++++++++++ .../canvas/presentation/edit/EditScreen.kt | 94 +++++-------------- 2 files changed, 73 insertions(+), 68 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt index f81fcc5..4f72aaa 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt @@ -542,6 +542,52 @@ class EditManager { !_isEraseMode.value ) + fun shouldApplyOperation(): Boolean = ( + _isCropMode.value || + _isRotateMode.value || + _isResizeMode.value || + _isBlurMode.value + ) + + fun isControlsDisabled(): Boolean = ( + !_isRotateMode.value && + !_isResizeMode.value && + !_isCropMode.value && + !_isEyeDropperMode.value + ) + + fun shouldCancelOperation(): Boolean = ( + _isCropMode.value || + _isRotateMode.value || + _isResizeMode.value || + _isEyeDropperMode.value || + _isBlurMode.value + ) + + fun isEligibleForBlurMode() = ( + !_isRotateMode.value && + !_isCropMode.value && + !_isEyeDropperMode.value && + !_isResizeMode.value && + !_isEraseMode.value + ) + + fun isEligibleForResizeMode() = ( + !_isRotateMode.value && + !_isCropMode.value && + !_isEyeDropperMode.value && + !_isEraseMode.value && + !_isBlurMode.value + ) + + fun shouldExpandColorDialog() = ( + !_isRotateMode.value && + !_isResizeMode.value && + !_isCropMode.value && + !_isEraseMode.value && + !_isBlurMode.value + ) + fun undo() { if (canUndo.value) { val undoTask = undoStack.pop() @@ -617,6 +663,7 @@ class EditManager { } fun clearEdits() { + if (_isRotateMode.value || _isResizeMode.value || _isCropMode.value || _isEyeDropperMode.value) return clearPaths() clearResizes() clearRotations() diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt index 58e109b..ad0562e 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -96,29 +96,27 @@ fun EditScreen( } if (showDefaultsDialog.value) { - viewModel.editManager.apply { - resolution.value?.let { - NewImageOptionsDialog( - it, - maxResolution, - this.backgroundColor.value, - navigateBack, - this, - persistDefaults = { color, resolution -> - viewModel.persistDefaults(color, resolution) - }, - onConfirm = { - showDefaultsDialog.value = false - } - ) - } + viewModel.editManager.resolution.value?.let { + NewImageOptionsDialog( + it, + maxResolution, + viewModel.editManager.backgroundColor.value, + navigateBack, + viewModel.editManager, + persistDefaults = { color, resolution -> + viewModel.persistDefaults(color, resolution) + }, + onConfirm = { + showDefaultsDialog.value = false + } + ) } } ExitDialog( viewModel = viewModel, navigateBack = { navigateBack() -// viewModel.isLoaded = false + viewModel.isLoaded = false }, launchedFromIntent = launchedFromIntent, ) @@ -350,23 +348,13 @@ private fun BoxScope.TopMenu( ConfirmClearDialog( viewModel.showConfirmClearDialog, onConfirm = { - viewModel.editManager.apply { - if ( - !isRotateMode.value && - !isResizeMode.value && - !isCropMode.value && - !isEyeDropperMode.value - ) clearEdits() - } + viewModel.editManager.clearEdits() } ) if ( !viewModel.menusVisible && - !viewModel.editManager.isRotateMode.value && - !viewModel.editManager.isResizeMode.value && - !viewModel.editManager.isCropMode.value && - !viewModel.editManager.isEyeDropperMode.value + viewModel.editManager.isControlsDisabled() ) return Icon( @@ -377,11 +365,7 @@ private fun BoxScope.TopMenu( .clip(CircleShape) .clickable { viewModel.editManager.apply { - if ( - isCropMode.value || isRotateMode.value || - isResizeMode.value || isEyeDropperMode.value || - isBlurMode.value - ) { + if (shouldCancelOperation()) { viewModel.cancelOperation() return@clickable } @@ -394,7 +378,7 @@ private fun BoxScope.TopMenu( return@clickable } if ( - !viewModel.editManager.canUndo.value + !canUndo.value ) { if (launchedFromIntent) { context @@ -423,23 +407,13 @@ private fun BoxScope.TopMenu( .size(36.dp) .clip(CircleShape) .clickable { - viewModel.editManager.apply { - if ( - isCropMode.value || isRotateMode.value || - isResizeMode.value || isBlurMode.value - ) { - viewModel.applyOperation() - return@clickable - } + if (viewModel.editManager.shouldApplyOperation()) { + viewModel.applyOperation() + return@clickable } viewModel.showMoreOptionsPopup = true }, - imageVector = if ( - viewModel.editManager.isCropMode.value || - viewModel.editManager.isRotateMode.value || - viewModel.editManager.isResizeMode.value || - viewModel.editManager.isBlurMode.value - ) + imageVector = if (viewModel.editManager.shouldApplyOperation()) ImageVector.vectorResource(R.drawable.ic_check) else ImageVector.vectorResource(R.drawable.ic_more_vert), tint = MaterialTheme.colors.primary, @@ -611,13 +585,7 @@ private fun EditMenuContent( colorDialogExpanded.value = true return@clickable } - if ( - !editManager.isRotateMode.value && - !editManager.isResizeMode.value && - !editManager.isCropMode.value && - !editManager.isEraseMode.value && - !editManager.isBlurMode.value - ) + if (editManager.shouldExpandColorDialog()) colorDialogExpanded.value = true } ) @@ -749,12 +717,7 @@ private fun EditMenuContent( .clip(CircleShape) .clickable { editManager.apply { - if ( - !isRotateMode.value && - !isCropMode.value && - !isEyeDropperMode.value && - !isEraseMode.value && - !isBlurMode.value + if (isEligibleForResizeMode() ) toggleResizeMode() else return@clickable @@ -787,12 +750,7 @@ private fun EditMenuContent( .clip(CircleShape) .clickable { editManager.apply { - if ( - !isRotateMode.value && - !isCropMode.value && - !isEyeDropperMode.value && - !isResizeMode.value && - !isEraseMode.value && + if (isEligibleForBlurMode() && !viewModel.strokeSliderExpanded ) toggleBlurMode() if (isBlurMode.value) { From 626cb2fee964377decd493406761e241158040aa Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Wed, 4 Dec 2024 22:19:45 +0700 Subject: [PATCH 19/29] Remove unused image --- .../canvas/presentation/edit/NewImageOptionsDialog.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt index 179f037..1c21d22 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt @@ -41,8 +41,6 @@ import dev.arkbuilders.canvas.presentation.edit.resize.Hint import dev.arkbuilders.canvas.presentation.edit.resize.delayHidingHint import dev.arkbuilders.canvas.presentation.theme.Gray -//import dev.arkbuilders.canvas.presentation.theme.getGray - @Composable fun NewImageOptionsDialog( defaultResolution: Resolution, From 40dbb24b609ca698e609c0cab0f910a28134b2ec Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Fri, 6 Dec 2024 20:50:18 +0700 Subject: [PATCH 20/29] Extract logic --- .../presentation/drawing/EditManager.kt | 43 +++++++++++++++++ .../canvas/presentation/edit/EditScreen.kt | 46 +------------------ 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt index 4f72aaa..27ee73a 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt @@ -798,6 +798,49 @@ class EditManager { .asImageBitmap() } + fun onResizeChanged(newSize: IntSize) { + drawAreaSize.value = newSize + when (true) { + isCropMode.value -> { + cropWindow.updateOnDrawAreaSizeChange(newSize) + return + } + + isResizeMode.value -> { + if ( + backgroundImage.value?.width == + imageSize.width && + backgroundImage.value?.height == + imageSize.height + ) { + val editMatrixScale = scaleToFitOnEdit().scale + resizeOperation + .updateEditMatrixScale(editMatrixScale) + } + if ( + resizeOperation.isApplied() + ) { + resizeOperation.resetApply() + } + return + } + + isRotateMode.value -> { + scaleToFitOnEdit() + return + } + + isZoomMode.value -> { + return + } + + else -> { + scaleToFit() + return + } + } + } + fun fitImage( imageBitmap: ImageBitmap, maxWidth: Int, diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt index ad0562e..9a2fb9b 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -249,51 +249,9 @@ private fun DrawContainer( false } .onSizeChanged { newSize -> - if (newSize == IntSize.Zero) return@onSizeChanged - if (viewModel.showSavePathDialog) return@onSizeChanged - viewModel.editManager.drawAreaSize.value = newSize + if (newSize == IntSize.Zero || viewModel.showSavePathDialog) return@onSizeChanged if (viewModel.isLoaded) { - viewModel.editManager.apply { - when (true) { - isCropMode.value -> { - cropWindow.updateOnDrawAreaSizeChange(newSize) - return@onSizeChanged - } - - isResizeMode.value -> { - if ( - backgroundImage.value?.width == - imageSize.width && - backgroundImage.value?.height == - imageSize.height - ) { - val editMatrixScale = scaleToFitOnEdit().scale - resizeOperation - .updateEditMatrixScale(editMatrixScale) - } - if ( - resizeOperation.isApplied() - ) { - resizeOperation.resetApply() - } - return@onSizeChanged - } - - isRotateMode.value -> { - scaleToFitOnEdit() - return@onSizeChanged - } - - isZoomMode.value -> { - return@onSizeChanged - } - - else -> { - scaleToFit() - return@onSizeChanged - } - } - } + viewModel.editManager.onResizeChanged(newSize) } viewModel.loadImage(context) }, From 4c7dcc23589844fde1b5df90e555abc2852b2331 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Fri, 6 Dec 2024 21:28:54 +0700 Subject: [PATCH 21/29] Extract logic to view model --- .../presentation/drawing/EditManager.kt | 29 +++++++++++ .../canvas/presentation/edit/EditScreen.kt | 48 ++----------------- .../canvas/presentation/edit/EditViewModel.kt | 5 ++ 3 files changed, 39 insertions(+), 43 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt index 27ee73a..52d4ed8 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt @@ -744,6 +744,23 @@ class EditManager { _isResizeMode.value = !isResizeMode.value } + fun enterResizeMode() { + if (!isEligibleForResizeMode()) + return + toggleResizeMode() + if (isResizeMode.value) { + setBackgroundImage2() + val imgBitmap = getEditedImage() + backgroundImage.value = imgBitmap + resizeOperation.init( + imgBitmap.asAndroidBitmap() + ) + return + } + cancelResizeMode() + scaleToFit() + } + fun cancelResizeMode() { backgroundImage.value = backgroundImage2.value editMatrix.reset() @@ -753,6 +770,18 @@ class EditManager { _isBlurMode.value = !isBlurMode.value } + fun enterBlurMode(strokeSliderExpanded: Boolean) { + if (isEligibleForBlurMode() && !strokeSliderExpanded) toggleBlurMode() + if (isBlurMode.value) { + setBackgroundImage2() + backgroundImage.value = getEditedImage() + blurOperation.init() + return + } + blurOperation.cancel() + scaleToFit() + } + fun setPaintStrokeWidth(strokeWidth: Float) { drawPaint.value.strokeWidth = strokeWidth } diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt index 9a2fb9b..c51149e 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.layout.onSizeChanged @@ -570,14 +569,7 @@ private fun EditMenuContent( viewModel.strokeSliderExpanded = !viewModel.strokeSliderExpanded }, imageVector = ImageVector.vectorResource(R.drawable.ic_line_weight), - tint = if ( - !editManager.isRotateMode.value && - !editManager.isResizeMode.value && - !editManager.isCropMode.value && - !editManager.isEyeDropperMode.value && - !editManager.isBlurMode.value - ) editManager.paintColor.value - else Color.Black, + tint = if (editManager.isEligibleForUndoOrRedo()) editManager.paintColor.value else Color.Black, contentDescription = null ) Icon( @@ -674,24 +666,8 @@ private fun EditMenuContent( .size(40.dp) .clip(CircleShape) .clickable { - editManager.apply { - if (isEligibleForResizeMode() - ) - toggleResizeMode() - else return@clickable - viewModel.menusVisible = !isResizeMode.value - if (isResizeMode.value) { - setBackgroundImage2() - val imgBitmap = viewModel.getEditedImage() - backgroundImage.value = imgBitmap - resizeOperation.init( - imgBitmap.asAndroidBitmap() - ) - return@clickable - } - cancelResizeMode() - scaleToFit() - } + editManager.enterResizeMode() + viewModel.menusVisible = !editManager.isResizeMode.value }, imageVector = ImageVector .vectorResource(R.drawable.ic_aspect_ratio), @@ -707,19 +683,7 @@ private fun EditMenuContent( .size(40.dp) .clip(CircleShape) .clickable { - editManager.apply { - if (isEligibleForBlurMode() && - !viewModel.strokeSliderExpanded - ) toggleBlurMode() - if (isBlurMode.value) { - setBackgroundImage2() - backgroundImage.value = viewModel.getEditedImage() - blurOperation.init() - return@clickable - } - blurOperation.cancel() - scaleToFit() - } + editManager.enterBlurMode(viewModel.strokeSliderExpanded) }, imageVector = ImageVector .vectorResource(R.drawable.ic_blur_on), @@ -731,9 +695,7 @@ private fun EditMenuContent( ) } } - viewModel.bottomButtonsScrollIsAtStart.value = scrollState.value == 0 - viewModel.bottomButtonsScrollIsAtEnd.value = - scrollState.value == scrollState.maxValue + viewModel.onBottomButtonStateChange(scrollState.value, 0, scrollState.maxValue) } @Composable diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt index e073c94..f3c262a 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditViewModel.kt @@ -66,6 +66,11 @@ class EditViewModel( private val _usedColors = mutableListOf() val usedColors: List = _usedColors + fun onBottomButtonStateChange(scrollStateValue: Int, minStateValue: Int = 0, maxStateValue: Int) { + bottomButtonsScrollIsAtStart.value = scrollStateValue == minStateValue + bottomButtonsScrollIsAtEnd.value = scrollStateValue == maxStateValue + } + private fun loadResource(path: Path) { viewModelScope.launch { if (path.name.endsWith(".png")) { From 55b4206be579db270b766f93bb22f9d23c160384 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 7 Dec 2024 19:32:15 +0700 Subject: [PATCH 22/29] Simplify code to get stroke size --- .../canvas/presentation/drawing/EditCanvas.kt | 5 +- .../canvas/presentation/utils/SVG.kt | 52 ++++++++++--------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt index a886649..b793025 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditCanvas.kt @@ -37,7 +37,6 @@ import dev.arkbuilders.canvas.presentation.edit.EditViewModel import dev.arkbuilders.canvas.presentation.edit.TransparencyChessBoardCanvas import dev.arkbuilders.canvas.presentation.edit.crop.CropWindow.Companion.computeDeltaX import dev.arkbuilders.canvas.presentation.edit.crop.CropWindow.Companion.computeDeltaY -import dev.arkbuilders.canvas.presentation.graphics.Size import dev.arkbuilders.canvas.presentation.picker.toDp import dev.arkbuilders.canvas.presentation.utils.SVGCommand import dev.arkbuilders.canvas.presentation.utils.calculateRotationFromOneFingerGesture @@ -185,7 +184,7 @@ fun DrawCanvas(modifier: Modifier, viewModel: EditViewModel) { svg.apply { val svgCommand = SVGCommand.MoveTo(eventX, eventY).apply { paintColor = editManager.currentPaint.color.value - brushSizeId = Size.MEDIUM.id + brushSize = editManager.currentPaint.strokeWidth } addCommand(svgCommand) } @@ -209,7 +208,7 @@ fun DrawCanvas(modifier: Modifier, viewModel: EditViewModel) { (eventY + currentPoint.y) / 2 ).apply { paintColor = editManager.currentPaint.color.value - brushSizeId = Size.MEDIUM.id + brushSize = editManager.currentPaint.strokeWidth }) } } diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt index 6b4e670..31645a7 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.graphics.PaintingStyle import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.Path as ComposePath -import androidx.compose.ui.graphics.asComposePaint import dev.arkbuilders.canvas.presentation.drawing.DrawPath import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlSerializer @@ -19,7 +18,7 @@ import kotlin.io.path.writer class SVG { private var strokeColor: ULong = Color.Black.value - private var strokeSize: Int = dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + private var strokeSize: Float = SVGCommand.DEFAULT_BRUSH_SIZE private var fill = "none" private var viewBox = ViewBox() private val commands = ArrayDeque() @@ -28,7 +27,7 @@ class SVG { private val paint get() = Paint().also { it.style = PaintingStyle.Stroke - it.strokeWidth = strokeSize.getStrokeSize() + it.strokeWidth = strokeSize it.strokeCap = StrokeCap.Round it.strokeJoin = StrokeJoin.Round it.isAntiAlias = true @@ -85,7 +84,7 @@ class SVG { var path = ComposePath() commands.forEach { command -> strokeColor = command.paintColor - strokeSize = command.brushSizeId + strokeSize = command.brushSize when (command) { is SVGCommand.MoveTo -> { path = ComposePath() @@ -141,7 +140,8 @@ class SVG { PATH_TAG -> { pathCount += 1 - strokeColor = getAttributeValue("", Attributes.Path.STROKE).toULong() + strokeColor = + getAttributeValue("", Attributes.Path.STROKE).toULong() fill = getAttributeValue("", Attributes.Path.FILL) pathData = getAttributeValue("", Attributes.Path.DATA) } @@ -166,11 +166,11 @@ class SVG { strokeColor = commandElements[3].toULong() } if (commandElements.size > 4) { - strokeSize = commandElements[4].toInt() + strokeSize = commandElements[4].toFloat() } commands.addLast(SVGCommand.MoveTo.fromString(command).apply { paintColor = strokeColor - brushSizeId = strokeSize + brushSize = strokeSize }) } @@ -179,11 +179,11 @@ class SVG { strokeColor = commandElements[3].toULong() } if (commandElements.size > 4) { - strokeSize = commandElements[4].toInt() + strokeSize = commandElements[4].toFloat() } commands.addLast(SVGCommand.MoveTo.fromString(command).apply { paintColor = strokeColor - brushSizeId = strokeSize + brushSize = strokeSize }) } @@ -192,11 +192,11 @@ class SVG { strokeColor = commandElements[5].toULong() } if (commandElements.size > 6) { - strokeSize = commandElements[6].toInt() + strokeSize = commandElements[6].toFloat() } commands.addLast(SVGCommand.AbsQuadTo.fromString(command).apply { paintColor = strokeColor - brushSizeId = strokeSize + brushSize = strokeSize }) } @@ -244,14 +244,18 @@ data class ViewBox( sealed class SVGCommand { + companion object { + const val DEFAULT_BRUSH_SIZE = 3.0f + } + var paintColor: ULong = Color.Black.value - var brushSizeId = dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + var brushSize: Float = 3.0f class MoveTo( val x: Float, val y: Float ) : SVGCommand() { - override fun toString(): String = "$CODE $x $y $paintColor $brushSizeId" + override fun toString(): String = "$CODE $x $y $paintColor $brushSize" companion object { const val CODE = 'M' @@ -261,11 +265,11 @@ sealed class SVGCommand { val x = params[0].toFloat() val y = params[1].toFloat() val colorCode = if (params.size > 2) params[2].toULong() else Color.Black.value - val strokeSizeId = - if (params.size > 3) params[3].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + val strokeSize = + if (params.size > 3) params[3].toFloat() else DEFAULT_BRUSH_SIZE return MoveTo(x, y).apply { paintColor = colorCode - brushSizeId = strokeSizeId + brushSize = strokeSize } } } @@ -275,7 +279,7 @@ sealed class SVGCommand { val x: Float, val y: Float ) : SVGCommand() { - override fun toString(): String = "$CODE $x $y $paintColor $brushSizeId" + override fun toString(): String = "$CODE $x $y $paintColor $brushSize" companion object { const val CODE = 'L' @@ -285,11 +289,11 @@ sealed class SVGCommand { val x = params[0].toFloat() val y = params[1].toFloat() val colorCode = if (params.size > 2) params[2].toULong() else Color.Black.value - val strokeSizeId = - if (params.size > 3) params[3].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + val strokeSize = + if (params.size > 3) params[3].toFloat() else DEFAULT_BRUSH_SIZE return AbsLineTo(x, y).apply { paintColor = colorCode - brushSizeId = strokeSizeId + brushSize = strokeSize } } } @@ -301,7 +305,7 @@ sealed class SVGCommand { val x2: Float, val y2: Float ) : SVGCommand() { - override fun toString(): String = "$CODE $x1 $y1 $x2 $y2 $paintColor $brushSizeId" + override fun toString(): String = "$CODE $x1 $y1 $x2 $y2 $paintColor $brushSize" companion object { const val CODE = 'Q' @@ -313,11 +317,11 @@ sealed class SVGCommand { val x2 = params[2].toFloat() val y2 = params[3].toFloat() val colorCode = if (params.size > 4) params[4].toULong() else Color.Black.value - val strokeSizeId = - if (params.size > 5) params[5].toInt() else dev.arkbuilders.canvas.presentation.graphics.Size.TINY.id + val strokeSize = + if (params.size > 5) params[5].toFloat() else DEFAULT_BRUSH_SIZE return AbsQuadTo(x1, y1, x2, y2).apply { paintColor = colorCode - brushSizeId = strokeSizeId + brushSize = strokeSize } } } From 4c3ecaa0da4f5bbbcc8d13b57263998d4019925c Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 7 Dec 2024 19:32:26 +0700 Subject: [PATCH 23/29] Remove unused files --- .../canvas/presentation/graphics/Color.kt | 20 ---- .../presentation/utils/GraphicBrushExt.kt | 92 ------------------- 2 files changed, 112 deletions(-) delete mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Color.kt delete mode 100644 canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/GraphicBrushExt.kt diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Color.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Color.kt deleted file mode 100644 index 70124a5..0000000 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/graphics/Color.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dev.arkbuilders.canvas.presentation.graphics - -enum class Color(val code: Int, val value: String) { - - BLACK(ColorCode.black, "black"), - - GRAY(ColorCode.gray, "gray"), - - RED(ColorCode.red, "red"), - - ORANGE(ColorCode.orange, "orange"), - - GREEN(ColorCode.green, "green"), - - BLUE(ColorCode.blue, "blue"), - - PURPLE(ColorCode.purple, "purple"), - - WHITE(ColorCode.white, "white") -} \ No newline at end of file diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/GraphicBrushExt.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/GraphicBrushExt.kt deleted file mode 100644 index cbdde21..0000000 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/GraphicBrushExt.kt +++ /dev/null @@ -1,92 +0,0 @@ -package dev.arkbuilders.canvas.presentation.utils - -import dev.arkbuilders.canvas.presentation.graphics.Color -import dev.arkbuilders.canvas.presentation.graphics.Size -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColor -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorBlack -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorBlue -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorGreen -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorGrey -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorOrange -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorPurple -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushColorRed -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushSize -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushSizeHuge -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushSizeLarge -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushSizeMedium -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushSizeSmall -import dev.arkbuilders.canvas.presentation.utils.adapters.BrushSizeTiny - -fun Int.getStrokeSize(): Float { - return when(this) { - Size.TINY.id -> Size.TINY.value - Size.SMALL.id -> Size.SMALL.value - Size.MEDIUM.id -> Size.MEDIUM.value - Size.LARGE.id -> Size.LARGE.value - Size.HUGE.id -> Size.HUGE.value - else -> Size.TINY.value - } -} - -fun Float.getBrushSizeId(): Int { - return when (this) { - in 0f..Size.TINY.value -> Size.TINY.id - in Size.TINY.value..Size.SMALL.value -> Size.SMALL.id - in Size.SMALL.value..Size.MEDIUM.value -> Size.MEDIUM.id - in Size.MEDIUM.value..Size.LARGE.value -> Size.LARGE.id - in Size.LARGE.value..Size.HUGE.value -> Size.HUGE.id - else -> { - Size.TINY.id - } - } -} - -fun Int.getStrokeColor(): String { - return when (this) { - Color.BLACK.code -> Color.BLACK.value - Color.GRAY.code -> Color.GRAY.value - Color.RED.code -> Color.RED.value - Color.GREEN.code -> Color.GREEN.value - Color.BLUE.code -> Color.BLUE.value - Color.PURPLE.code -> Color.PURPLE.value - Color.ORANGE.code -> Color.ORANGE.value - Color.WHITE.code -> Color.WHITE.value - else -> Color.BLACK.value - } -} - -fun String.getColorCode(): Int { - return when (this) { - Color.BLACK.value -> Color.BLACK.code - Color.GRAY.value -> Color.GRAY.code - Color.RED.value -> Color.RED.code - Color.GREEN.value -> Color.GREEN.code - Color.BLUE.value -> Color.BLUE.code - Color.PURPLE.value -> Color.PURPLE.code - Color.ORANGE.value -> Color.ORANGE.code - Color.WHITE.value -> Color.WHITE.code - else -> Color.BLACK.code - } -} - -fun BrushColor.getColorCode(): Int { - return when (this) { - is BrushColorBlack -> Color.BLACK.code - is BrushColorGrey -> Color.GRAY.code - is BrushColorRed -> Color.RED.code - is BrushColorOrange -> Color.ORANGE.code - is BrushColorGreen -> Color.GREEN.code - is BrushColorBlue -> Color.BLUE.code - is BrushColorPurple -> Color.PURPLE.code - } -} - -fun BrushSize.getBrushSize(): Float { - return when(this) { - is BrushSizeTiny -> Size.TINY.value - is BrushSizeSmall -> Size.SMALL.value - is BrushSizeMedium -> Size.MEDIUM.value - is BrushSizeLarge -> Size.LARGE.value - is BrushSizeHuge -> Size.HUGE.value - } -} \ No newline at end of file From 55f59ff4ebb669cc5db3c49307772a87e029662f Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 7 Dec 2024 19:37:05 +0700 Subject: [PATCH 24/29] Reduce duplicate code --- .../canvas/presentation/utils/SVG.kt | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt index 31645a7..2d87d24 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/utils/SVG.kt @@ -155,38 +155,30 @@ class SVG { event = next() } - + fun extractStrokeFromCommand(originalCommand: String, commandElements: List) { + if (commandElements.size > 3) { + strokeColor = commandElements[3].toULong() + } + if (commandElements.size > 4) { + strokeSize = commandElements[4].toFloat() + } + commands.addLast(SVGCommand.MoveTo.fromString(originalCommand).apply { + paintColor = strokeColor + brushSize = strokeSize + }) + } pathData.split(COMMA).forEach { val command = it.trim() if (command.isEmpty()) return@forEach val commandElements = command.split(" ") + when (command.first()) { SVGCommand.MoveTo.CODE -> { - if (commandElements.size > 3) { - strokeColor = commandElements[3].toULong() - } - if (commandElements.size > 4) { - strokeSize = commandElements[4].toFloat() - } - commands.addLast(SVGCommand.MoveTo.fromString(command).apply { - paintColor = strokeColor - brushSize = strokeSize - }) + extractStrokeFromCommand(originalCommand = command, commandElements = commandElements) } - SVGCommand.AbsLineTo.CODE -> { - if (commandElements.size > 3) { - strokeColor = commandElements[3].toULong() - } - if (commandElements.size > 4) { - strokeSize = commandElements[4].toFloat() - } - commands.addLast(SVGCommand.MoveTo.fromString(command).apply { - paintColor = strokeColor - brushSize = strokeSize - }) + extractStrokeFromCommand(originalCommand = command, commandElements = commandElements) } - SVGCommand.AbsQuadTo.CODE -> { if (commandElements.size > 5) { strokeColor = commandElements[5].toULong() From fd9337173d7e08195cd060bd86e81a4172c7e3ae Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 7 Dec 2024 20:02:14 +0700 Subject: [PATCH 25/29] Add color property --- .../presentation/drawing/EditManager.kt | 7 ++++- .../canvas/presentation/edit/EditScreen.kt | 26 +++++++++---------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt index 52d4ed8..19d118a 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt @@ -36,7 +36,12 @@ import timber.log.Timber import java.util.Stack import kotlin.system.measureTimeMillis -class EditManager { +class ArkColorPalette( + val primary: Color +) +class EditManager( + val colorProperties: ArkColorPalette = ArkColorPalette(primary = Color.Green) +) { private val drawPaint: MutableState = mutableStateOf(defaultPaint()) private val _paintColor: MutableState = diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt index c51149e..97ba782 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -201,7 +201,7 @@ private fun Menus( }, imageVector = ImageVector .vectorResource(R.drawable.ic_rotate_left), - tint = MaterialTheme.colors.primary, + tint = viewModel.editManager.colorProperties.primary, contentDescription = null ) Icon( @@ -217,7 +217,7 @@ private fun Menus( }, imageVector = ImageVector .vectorResource(R.drawable.ic_rotate_right), - tint = MaterialTheme.colors.primary, + tint = viewModel.editManager.colorProperties.primary, contentDescription = null ) } @@ -350,7 +350,7 @@ private fun BoxScope.TopMenu( } }, imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_back), - tint = MaterialTheme.colors.primary, + tint = viewModel.editManager.colorProperties.primary, contentDescription = null ) @@ -373,7 +373,7 @@ private fun BoxScope.TopMenu( imageVector = if (viewModel.editManager.shouldApplyOperation()) ImageVector.vectorResource(R.drawable.ic_check) else ImageVector.vectorResource(R.drawable.ic_more_vert), - tint = MaterialTheme.colors.primary, + tint = viewModel.editManager.colorProperties.primary, contentDescription = null ) } @@ -511,7 +511,7 @@ private fun EditMenuContent( imageVector = ImageVector.vectorResource(R.drawable.ic_undo), tint = if ( editManager.canUndo.value && (editManager.isEligibleForUndoOrRedo()) - ) MaterialTheme.colors.primary else Color.Black, + ) viewModel.editManager.colorProperties.primary else Color.Black, contentDescription = null ) Icon( @@ -526,7 +526,7 @@ private fun EditMenuContent( tint = if ( editManager.canRedo.value && (editManager.isEligibleForUndoOrRedo()) - ) MaterialTheme.colors.primary else Color.Black, + ) viewModel.editManager.colorProperties.primary else Color.Black, contentDescription = null ) Box( @@ -585,7 +585,7 @@ private fun EditMenuContent( tint = if ( editManager.isEraseMode.value ) - MaterialTheme.colors.primary + viewModel.editManager.colorProperties.primary else Color.Black, contentDescription = null @@ -602,7 +602,7 @@ private fun EditMenuContent( tint = if ( editManager.isZoomMode.value ) - MaterialTheme.colors.primary + viewModel.editManager.colorProperties.primary else Color.Black, contentDescription = null @@ -619,7 +619,7 @@ private fun EditMenuContent( tint = if ( editManager.isPanMode.value ) - MaterialTheme.colors.primary + viewModel.editManager.colorProperties.primary else Color.Black, contentDescription = null @@ -637,7 +637,7 @@ private fun EditMenuContent( imageVector = ImageVector.vectorResource(R.drawable.ic_crop), tint = if ( editManager.isCropMode.value - ) MaterialTheme.colors.primary + ) viewModel.editManager.colorProperties.primary else Color.Black, contentDescription = null @@ -655,7 +655,7 @@ private fun EditMenuContent( imageVector = ImageVector .vectorResource(R.drawable.ic_rotate_90_degrees_ccw), tint = if (editManager.isRotateMode.value) - MaterialTheme.colors.primary + viewModel.editManager.colorProperties.primary else Color.Black, contentDescription = null @@ -672,7 +672,7 @@ private fun EditMenuContent( imageVector = ImageVector .vectorResource(R.drawable.ic_aspect_ratio), tint = if (editManager.isResizeMode.value) - MaterialTheme.colors.primary + viewModel.editManager.colorProperties.primary else Color.Black, contentDescription = null @@ -688,7 +688,7 @@ private fun EditMenuContent( imageVector = ImageVector .vectorResource(R.drawable.ic_blur_on), tint = if (editManager.isBlurMode.value) - MaterialTheme.colors.primary + editManager.colorProperties.primary else Color.Black, contentDescription = null From 1b0c9cc261e26a0d84f6ef2529d064c684318a71 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 7 Dec 2024 20:36:28 +0700 Subject: [PATCH 26/29] Fix wrong background setting --- .../dev/arkbuilders/canvas/presentation/drawing/EditManager.kt | 1 - .../java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt index 19d118a..87969bf 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt @@ -833,7 +833,6 @@ class EditManager( } fun onResizeChanged(newSize: IntSize) { - drawAreaSize.value = newSize when (true) { isCropMode.value -> { cropWindow.updateOnDrawAreaSizeChange(newSize) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt index 97ba782..3bb2bf6 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -249,6 +249,7 @@ private fun DrawContainer( } .onSizeChanged { newSize -> if (newSize == IntSize.Zero || viewModel.showSavePathDialog) return@onSizeChanged + viewModel.editManager.drawAreaSize.value = newSize if (viewModel.isLoaded) { viewModel.editManager.onResizeChanged(newSize) } From eae99c35b510cca20f21eb53558d87ea44edf905 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 7 Dec 2024 20:36:39 +0700 Subject: [PATCH 27/29] Remove unused import --- .../java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt index 3bb2bf6..0f97790 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -32,7 +32,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme import androidx.compose.material.Slider import androidx.compose.material.Text import androidx.compose.material.TextButton From 5e043c843ac4ba17ab23417ccec4edf8c60c5ea3 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 7 Dec 2024 20:53:27 +0700 Subject: [PATCH 28/29] Add theme color for manager --- .../canvas/presentation/edit/EditScreen.kt | 12 +++++++++--- .../presentation/edit/NewImageOptionsDialog.kt | 14 ++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt index 0f97790..182a857 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.AlertDialog import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.material.Slider import androidx.compose.material.Text @@ -780,7 +781,8 @@ private fun ExitDialog( onClick = { viewModel.showExitDialog = false viewModel.showSavePathDialog = true - } + }, + colors = ButtonDefaults.buttonColors(backgroundColor = viewModel.editManager.colorProperties.primary) ) { Text("Save") } @@ -794,9 +796,13 @@ private fun ExitDialog( } else { navigateBack() } - } + }, + colors = ButtonDefaults.buttonColors(backgroundColor = viewModel.editManager.colorProperties.primary) ) { - Text("Exit") + Text( + text = "Exit", + color = viewModel.editManager.colorProperties.primary, + ) } } ) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt index 1c21d22..7c09e82 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt @@ -13,8 +13,8 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Checkbox -import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.TextField @@ -169,7 +169,7 @@ fun NewImageOptionsDialog( Text( stringResource(R.string.width), Modifier.fillMaxWidth(), - color = MaterialTheme.colors.primary, + color = editManager.colorProperties.primary, textAlign = TextAlign.Center ) }, @@ -216,9 +216,9 @@ fun NewImageOptionsDialog( }, label = { Text( - stringResource(R.string.height), - Modifier.fillMaxWidth(), - color = MaterialTheme.colors.primary, + text = stringResource(R.string.height), + modifier = Modifier.fillMaxWidth(), + color = editManager.colorProperties.primary, textAlign = TextAlign.Center ) }, @@ -278,11 +278,13 @@ fun NewImageOptionsDialog( onClick = { isVisible = false navigateBack() - } + }, + colors = ButtonDefaults.buttonColors(backgroundColor = editManager.colorProperties.primary) ) { Text("Close") } TextButton( + colors = ButtonDefaults.buttonColors(backgroundColor = editManager.colorProperties.primary), modifier = Modifier .padding(end = 8.dp), onClick = { From 8f61433861ca73daa0ed28bf1c6e806cf901bbeb Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Sat, 7 Dec 2024 21:05:20 +0700 Subject: [PATCH 29/29] Add colors --- .../presentation/drawing/EditManager.kt | 11 +++---- .../canvas/presentation/edit/EditScreen.kt | 33 ++++++++++--------- .../edit/NewImageOptionsDialog.kt | 9 ++--- .../presentation/edit/SavePathDialog.kt | 16 +++++++-- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt index 87969bf..f926ec3 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/drawing/EditManager.kt @@ -36,12 +36,11 @@ import timber.log.Timber import java.util.Stack import kotlin.system.measureTimeMillis -class ArkColorPalette( - val primary: Color -) -class EditManager( - val colorProperties: ArkColorPalette = ArkColorPalette(primary = Color.Green) -) { +object ArkColorPalette { + val primary: Color = Color.Green + +} +class EditManager{ private val drawPaint: MutableState = mutableStateOf(defaultPaint()) private val _paintColor: MutableState = diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt index 182a857..db7b513 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/EditScreen.kt @@ -62,6 +62,7 @@ import androidx.compose.ui.unit.sp import androidx.fragment.app.FragmentManager import dev.arkbuilders.canvas.R import dev.arkbuilders.canvas.presentation.data.Resolution +import dev.arkbuilders.canvas.presentation.drawing.ArkColorPalette import dev.arkbuilders.canvas.presentation.drawing.EditCanvas import dev.arkbuilders.canvas.presentation.edit.blur.BlurIntensityPopup import dev.arkbuilders.canvas.presentation.edit.crop.CropAspectRatiosMenu @@ -201,7 +202,7 @@ private fun Menus( }, imageVector = ImageVector .vectorResource(R.drawable.ic_rotate_left), - tint = viewModel.editManager.colorProperties.primary, + tint = ArkColorPalette.primary, contentDescription = null ) Icon( @@ -217,7 +218,7 @@ private fun Menus( }, imageVector = ImageVector .vectorResource(R.drawable.ic_rotate_right), - tint = viewModel.editManager.colorProperties.primary, + tint = ArkColorPalette.primary, contentDescription = null ) } @@ -351,7 +352,7 @@ private fun BoxScope.TopMenu( } }, imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_back), - tint = viewModel.editManager.colorProperties.primary, + tint = ArkColorPalette.primary, contentDescription = null ) @@ -374,7 +375,7 @@ private fun BoxScope.TopMenu( imageVector = if (viewModel.editManager.shouldApplyOperation()) ImageVector.vectorResource(R.drawable.ic_check) else ImageVector.vectorResource(R.drawable.ic_more_vert), - tint = viewModel.editManager.colorProperties.primary, + tint = ArkColorPalette.primary, contentDescription = null ) } @@ -512,7 +513,7 @@ private fun EditMenuContent( imageVector = ImageVector.vectorResource(R.drawable.ic_undo), tint = if ( editManager.canUndo.value && (editManager.isEligibleForUndoOrRedo()) - ) viewModel.editManager.colorProperties.primary else Color.Black, + ) ArkColorPalette.primary else Color.Black, contentDescription = null ) Icon( @@ -527,7 +528,7 @@ private fun EditMenuContent( tint = if ( editManager.canRedo.value && (editManager.isEligibleForUndoOrRedo()) - ) viewModel.editManager.colorProperties.primary else Color.Black, + ) ArkColorPalette.primary else Color.Black, contentDescription = null ) Box( @@ -586,7 +587,7 @@ private fun EditMenuContent( tint = if ( editManager.isEraseMode.value ) - viewModel.editManager.colorProperties.primary + ArkColorPalette.primary else Color.Black, contentDescription = null @@ -603,7 +604,7 @@ private fun EditMenuContent( tint = if ( editManager.isZoomMode.value ) - viewModel.editManager.colorProperties.primary + ArkColorPalette.primary else Color.Black, contentDescription = null @@ -620,7 +621,7 @@ private fun EditMenuContent( tint = if ( editManager.isPanMode.value ) - viewModel.editManager.colorProperties.primary + ArkColorPalette.primary else Color.Black, contentDescription = null @@ -638,7 +639,7 @@ private fun EditMenuContent( imageVector = ImageVector.vectorResource(R.drawable.ic_crop), tint = if ( editManager.isCropMode.value - ) viewModel.editManager.colorProperties.primary + ) ArkColorPalette.primary else Color.Black, contentDescription = null @@ -656,7 +657,7 @@ private fun EditMenuContent( imageVector = ImageVector .vectorResource(R.drawable.ic_rotate_90_degrees_ccw), tint = if (editManager.isRotateMode.value) - viewModel.editManager.colorProperties.primary + ArkColorPalette.primary else Color.Black, contentDescription = null @@ -673,7 +674,7 @@ private fun EditMenuContent( imageVector = ImageVector .vectorResource(R.drawable.ic_aspect_ratio), tint = if (editManager.isResizeMode.value) - viewModel.editManager.colorProperties.primary + ArkColorPalette.primary else Color.Black, contentDescription = null @@ -689,7 +690,7 @@ private fun EditMenuContent( imageVector = ImageVector .vectorResource(R.drawable.ic_blur_on), tint = if (editManager.isBlurMode.value) - editManager.colorProperties.primary + ArkColorPalette.primary else Color.Black, contentDescription = null @@ -782,7 +783,7 @@ private fun ExitDialog( viewModel.showExitDialog = false viewModel.showSavePathDialog = true }, - colors = ButtonDefaults.buttonColors(backgroundColor = viewModel.editManager.colorProperties.primary) + colors = ButtonDefaults.buttonColors(backgroundColor = ArkColorPalette.primary) ) { Text("Save") } @@ -797,11 +798,11 @@ private fun ExitDialog( navigateBack() } }, - colors = ButtonDefaults.buttonColors(backgroundColor = viewModel.editManager.colorProperties.primary) + colors = ButtonDefaults.textButtonColors(contentColor = ArkColorPalette.primary) ) { Text( text = "Exit", - color = viewModel.editManager.colorProperties.primary, + color = ArkColorPalette.primary, ) } } diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt index 7c09e82..92dcc1c 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/NewImageOptionsDialog.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.window.Dialog import androidx.core.text.isDigitsOnly import dev.arkbuilders.canvas.R import dev.arkbuilders.canvas.presentation.data.Resolution +import dev.arkbuilders.canvas.presentation.drawing.ArkColorPalette import dev.arkbuilders.canvas.presentation.drawing.EditManager import dev.arkbuilders.canvas.presentation.edit.resize.Hint import dev.arkbuilders.canvas.presentation.edit.resize.delayHidingHint @@ -169,7 +170,7 @@ fun NewImageOptionsDialog( Text( stringResource(R.string.width), Modifier.fillMaxWidth(), - color = editManager.colorProperties.primary, + color = ArkColorPalette.primary, textAlign = TextAlign.Center ) }, @@ -218,7 +219,7 @@ fun NewImageOptionsDialog( Text( text = stringResource(R.string.height), modifier = Modifier.fillMaxWidth(), - color = editManager.colorProperties.primary, + color = ArkColorPalette.primary, textAlign = TextAlign.Center ) }, @@ -279,12 +280,12 @@ fun NewImageOptionsDialog( isVisible = false navigateBack() }, - colors = ButtonDefaults.buttonColors(backgroundColor = editManager.colorProperties.primary) + colors = ButtonDefaults.textButtonColors(contentColor = ArkColorPalette.primary) ) { Text("Close") } TextButton( - colors = ButtonDefaults.buttonColors(backgroundColor = editManager.colorProperties.primary), + colors = ButtonDefaults.buttonColors(backgroundColor = ArkColorPalette.primary), modifier = Modifier .padding(end = 8.dp), onClick = { diff --git a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/SavePathDialog.kt b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/SavePathDialog.kt index 1993936..3181be5 100644 --- a/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/SavePathDialog.kt +++ b/canvas/src/main/java/dev/arkbuilders/canvas/presentation/edit/SavePathDialog.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Checkbox import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.OutlinedTextField @@ -39,6 +40,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.fragment.app.FragmentManager import dev.arkbuilders.canvas.R +import dev.arkbuilders.canvas.presentation.drawing.ArkColorPalette import dev.arkbuilders.canvas.presentation.utils.findNotExistCopyName import dev.arkbuilders.canvas.presentation.utils.toast import dev.arkbuilders.components.filepicker.ArkFilePickerConfig @@ -101,6 +103,10 @@ fun SavePathDialog( fontSize = 18.sp ) TextButton( + colors = ButtonDefaults.textButtonColors( + contentColor = ArkColorPalette.primary + ) + , onClick = { ArkFilePickerFragment .newInstance( @@ -178,7 +184,10 @@ fun SavePathDialog( ) { TextButton( modifier = Modifier.padding(5.dp), - onClick = onDismissClick + onClick = onDismissClick, + colors = ButtonDefaults.textButtonColors( + contentColor = ArkColorPalette.primary + ) ) { Text(text = stringResource(R.string.cancel)) } @@ -198,7 +207,10 @@ fun SavePathDialog( return@Button } onPositiveClick(currentPath?.resolve(name)!!) - } + }, + colors = ButtonDefaults.buttonColors( + backgroundColor = ArkColorPalette.primary + ) ) { Text(text = stringResource(R.string.ok)) }