├── .gitignore ├── .idea ├── .gitignore ├── .name ├── appInsightsSettings.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── sonarlint │ ├── issuestore │ │ ├── 0 │ │ │ └── 5 │ │ │ │ └── 05efc8b1657769a27696d478ded1e95f38737233 │ │ ├── 8 │ │ │ ├── c │ │ │ │ └── 8c55c3ccc257e5907959013f99656e4c8ec3903e │ │ │ └── e │ │ │ │ └── 8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d │ │ ├── 9 │ │ │ └── 9 │ │ │ │ └── 998f1e5ce0e58f4ed6899eb4dccf4a10c44bbaaa │ │ ├── f │ │ │ ├── 0 │ │ │ │ └── f07866736216be0ee2aba49e392191aeae700a35 │ │ │ ├── 4 │ │ │ │ └── f4a01d6a4fcb971362ec00a83903fd3902f52164 │ │ │ └── b │ │ │ │ └── fbe448ebfc3eb2d4e308f6b8b043666f5b57235e │ │ └── index.pb │ └── securityhotspotstore │ │ ├── 0 │ │ └── 5 │ │ │ └── 05efc8b1657769a27696d478ded1e95f38737233 │ │ ├── 8 │ │ ├── c │ │ │ └── 8c55c3ccc257e5907959013f99656e4c8ec3903e │ │ └── e │ │ │ └── 8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d │ │ ├── 9 │ │ └── 9 │ │ │ └── 998f1e5ce0e58f4ed6899eb4dccf4a10c44bbaaa │ │ ├── f │ │ ├── 0 │ │ │ └── f07866736216be0ee2aba49e392191aeae700a35 │ │ ├── 4 │ │ │ └── f4a01d6a4fcb971362ec00a83903fd3902f52164 │ │ └── b │ │ │ └── fbe448ebfc3eb2d4e308f6b8b043666f5b57235e │ │ └── index.pb └── vcs.xml ├── KeyPath ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ └── app-release.aab └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── armutyus │ │ └── cameraxproject │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── armutyus │ │ │ └── cameraxproject │ │ │ ├── CameraXComposeApplication.kt │ │ │ ├── MainActivity.kt │ │ │ ├── ui │ │ │ ├── gallery │ │ │ │ ├── GalleryScreen.kt │ │ │ │ ├── GalleryViewModel.kt │ │ │ │ ├── models │ │ │ │ │ ├── BottomNavItem.kt │ │ │ │ │ ├── GalleryEvent.kt │ │ │ │ │ ├── GalleryState.kt │ │ │ │ │ └── MediaItem.kt │ │ │ │ └── preview │ │ │ │ │ ├── PreviewScreen.kt │ │ │ │ │ ├── PreviewViewModel.kt │ │ │ │ │ ├── editmedia │ │ │ │ │ ├── EditImageContent.kt │ │ │ │ │ ├── ImageCropMode.kt │ │ │ │ │ ├── cropproperties │ │ │ │ │ │ ├── AnimatedAspectRatioSelection.kt │ │ │ │ │ │ ├── BaseSheet.kt │ │ │ │ │ │ ├── ContentScaleDialogSelection.kt │ │ │ │ │ │ ├── CropFrameEditDialog.kt │ │ │ │ │ │ ├── CropFrameListDialog.kt │ │ │ │ │ │ ├── CropFrameSelection.kt │ │ │ │ │ │ ├── CropPropertySelection.kt │ │ │ │ │ │ ├── CropShapeAddDialog.kt │ │ │ │ │ │ ├── CropStyleSelectionMenu.kt │ │ │ │ │ │ ├── CropTypeDialogSelection.kt │ │ │ │ │ │ ├── CustomPathEdit.kt │ │ │ │ │ │ ├── CutCornerCropShapeEdit.kt │ │ │ │ │ │ ├── ImageMaskEdit.kt │ │ │ │ │ │ ├── OvalCropShapeEdit.kt │ │ │ │ │ │ ├── PolygonCropShapeEdit.kt │ │ │ │ │ │ ├── PropertySelectionSheet.kt │ │ │ │ │ │ ├── RoundedCornerShapeEdit.kt │ │ │ │ │ │ └── SelectionWidgets.kt │ │ │ │ │ ├── models │ │ │ │ │ │ ├── EditModesItem.kt │ │ │ │ │ │ └── ImageFilter.kt │ │ │ │ │ └── repo │ │ │ │ │ │ ├── EditMediaRepository.kt │ │ │ │ │ │ └── EditMediaRepositoryImpl.kt │ │ │ │ │ ├── models │ │ │ │ │ ├── PreviewScreenEvent.kt │ │ │ │ │ └── PreviewScreenState.kt │ │ │ │ │ └── videoplayback │ │ │ │ │ ├── CustomMediaController.kt │ │ │ │ │ ├── CustomPlayerView.kt │ │ │ │ │ └── VideoPlaybackContent.kt │ │ │ ├── photo │ │ │ │ ├── PhotoCaptureManager.kt │ │ │ │ ├── PhotoScreen.kt │ │ │ │ ├── PhotoViewModel.kt │ │ │ │ └── models │ │ │ │ │ ├── CameraModesItem.kt │ │ │ │ │ ├── PhotoEvent.kt │ │ │ │ │ ├── PhotoState.kt │ │ │ │ │ └── PreviewPhotoState.kt │ │ │ ├── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── video │ │ │ │ ├── VideoCaptureManager.kt │ │ │ │ ├── VideoScreen.kt │ │ │ │ ├── VideoViewModel.kt │ │ │ │ └── models │ │ │ │ ├── PreviewVideoState.kt │ │ │ │ ├── RecordingStatus.kt │ │ │ │ ├── VideoEvent.kt │ │ │ │ └── VideoState.kt │ │ │ └── util │ │ │ ├── BaseViewModel.kt │ │ │ ├── Extensions.kt │ │ │ ├── FileManager.kt │ │ │ ├── HelperFunctions.kt │ │ │ ├── Permissions.kt │ │ │ ├── UIComposables.kt │ │ │ └── Util.kt │ └── res │ │ ├── drawable-anydpi-v26 │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── cloud.png │ │ ├── squircle.png │ │ └── sun.png │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── splash.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── file_paths.xml │ └── test │ └── java │ └── com │ └── armutyus │ └── cameraxproject │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | CameraX Project -------------------------------------------------------------------------------- /.idea/appInsightsSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 27 | 28 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/0/5/05efc8b1657769a27696d478ded1e95f38737233: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/issuestore/0/5/05efc8b1657769a27696d478ded1e95f38737233 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/8/c/8c55c3ccc257e5907959013f99656e4c8ec3903e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/issuestore/8/c/8c55c3ccc257e5907959013f99656e4c8ec3903e -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/issuestore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/9/9/998f1e5ce0e58f4ed6899eb4dccf4a10c44bbaaa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/issuestore/9/9/998f1e5ce0e58f4ed6899eb4dccf4a10c44bbaaa -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/f/0/f07866736216be0ee2aba49e392191aeae700a35: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/issuestore/f/0/f07866736216be0ee2aba49e392191aeae700a35 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/f/4/f4a01d6a4fcb971362ec00a83903fd3902f52164: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/issuestore/f/4/f4a01d6a4fcb971362ec00a83903fd3902f52164 -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/f/b/fbe448ebfc3eb2d4e308f6b8b043666f5b57235e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/issuestore/f/b/fbe448ebfc3eb2d4e308f6b8b043666f5b57235e -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/index.pb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/issuestore/index.pb -------------------------------------------------------------------------------- /.idea/sonarlint/securityhotspotstore/0/5/05efc8b1657769a27696d478ded1e95f38737233: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/securityhotspotstore/0/5/05efc8b1657769a27696d478ded1e95f38737233 -------------------------------------------------------------------------------- /.idea/sonarlint/securityhotspotstore/8/c/8c55c3ccc257e5907959013f99656e4c8ec3903e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/securityhotspotstore/8/c/8c55c3ccc257e5907959013f99656e4c8ec3903e -------------------------------------------------------------------------------- /.idea/sonarlint/securityhotspotstore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/securityhotspotstore/8/e/8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d -------------------------------------------------------------------------------- /.idea/sonarlint/securityhotspotstore/9/9/998f1e5ce0e58f4ed6899eb4dccf4a10c44bbaaa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/securityhotspotstore/9/9/998f1e5ce0e58f4ed6899eb4dccf4a10c44bbaaa -------------------------------------------------------------------------------- /.idea/sonarlint/securityhotspotstore/f/0/f07866736216be0ee2aba49e392191aeae700a35: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/securityhotspotstore/f/0/f07866736216be0ee2aba49e392191aeae700a35 -------------------------------------------------------------------------------- /.idea/sonarlint/securityhotspotstore/f/4/f4a01d6a4fcb971362ec00a83903fd3902f52164: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/securityhotspotstore/f/4/f4a01d6a4fcb971362ec00a83903fd3902f52164 -------------------------------------------------------------------------------- /.idea/sonarlint/securityhotspotstore/f/b/fbe448ebfc3eb2d4e308f6b8b043666f5b57235e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/securityhotspotstore/f/b/fbe448ebfc3eb2d4e308f6b8b043666f5b57235e -------------------------------------------------------------------------------- /.idea/sonarlint/securityhotspotstore/index.pb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/.idea/sonarlint/securityhotspotstore/index.pb -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /KeyPath: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/KeyPath -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ömer Faruk Delibaş 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | readme header 3 |

4 | 5 | # cXc 6 | cXc is an application created with Jetpack Compose using the Camera X library. 7 | 8 | ## Features 9 | 10 | Taking photos and videos with some basic features: 11 | * Tap-to-focus, pinch-to-zoom 12 | * Capture delay 13 | * Choose video quaility 14 | 15 | Basic filters and crop mode for photo editing 16 | 17 | Photo, video sharing 18 | 19 | Some cool features like sticky header, horizontal pager and transitions for better design 20 | 21 | Exoplayer for video player 22 | 23 | ## Built With 24 | 25 | I tried to use many up-to-date approaches and technologies while developing this application: 26 | 27 | * Jetpack Compose 28 | * Camera X 29 | * Exoplayer 30 | * Material 3 31 | * Coil 32 | 33 | ## Screenshots 34 | 35 |

36 | 37 | 38 | 39 |

40 | 41 |

42 | 43 | 44 | 45 |

46 | 47 |

48 | 49 |

50 | 51 | ## TO-DO 52 | 53 | * Now we use a library for crop feature(it's very cool and easy to use though), maybe we could change it for our needs and then remove the library. 54 | * Adding more filters for editing photos 55 | * Live filters when capturing photo or video 56 | * Landscape mode 57 | * and more.. 58 | 59 | ## Credits 60 | 61 | *[Mayowa Egbewunmi](https://github.com/mayowa-egbewunmi/camerexandcompose) 62 | *[Compose Cropper](https://github.com/SmartToolFactory/Compose-Cropper) 63 | *[Accompanist](https://github.com/google/accompanist) 64 | *[GPUImage](https://github.com/cats-oss/android-gpuimage) 65 | *[Coil](https://coil-kt.github.io/coil/) 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | 4 | plugins { 5 | id 'com.android.application' 6 | id 'kotlin-android' 7 | id 'kotlin-kapt' 8 | id 'org.jetbrains.kotlin.android' 9 | } 10 | 11 | android { 12 | namespace 'com.armutyus.cameraxproject' 13 | compileSdk 34 14 | 15 | defaultConfig { 16 | applicationId "com.armutyus.cameraxproject" 17 | minSdk 23 18 | targetSdk 34 19 | versionCode 1 20 | versionName "0.0.1-alpha" 21 | 22 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 23 | vectorDrawables { 24 | useSupportLibrary true 25 | } 26 | } 27 | 28 | buildTypes { 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | targetCompatibility JavaVersion.VERSION_1_8 37 | } 38 | tasks.withType(KotlinCompile).configureEach { 39 | compilerOptions.jvmTarget.set(JvmTarget.JVM_1_8) 40 | } 41 | buildFeatures { 42 | compose true 43 | } 44 | composeOptions { 45 | kotlinCompilerExtensionVersion '1.5.4' 46 | } 47 | packagingOptions { 48 | resources { 49 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 50 | } 51 | } 52 | 53 | configurations.configureEach { 54 | resolutionStrategy.cacheChangingModulesFor 0, 'seconds' 55 | resolutionStrategy.force 'com.github.SmartToolFactory:Compose-Colorful-Sliders:master-SNAPSHOT' 56 | } 57 | } 58 | 59 | dependencies { 60 | 61 | def composeVer = "1.6.0-alpha08" 62 | implementation("androidx.core:core-ktx:1.12.0") 63 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") 64 | implementation("androidx.activity:activity-compose:1.8.0") 65 | implementation("androidx.compose.material:material:$composeVer") 66 | implementation("androidx.compose.material3:material3:1.2.0-alpha10") 67 | implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") 68 | implementation("androidx.compose.runtime:runtime-livedata:$composeVer") 69 | implementation("androidx.compose.ui:ui:$composeVer") 70 | implementation("androidx.compose.ui:ui-tooling-preview:$composeVer") 71 | 72 | implementation("androidx.compose.material:material-icons-core:$composeVer") 73 | implementation("androidx.compose.material:material-icons-extended:$composeVer") 74 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") 75 | implementation("androidx.navigation:navigation-compose:2.7.5") 76 | implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha13") 77 | implementation("androidx.core:core-splashscreen:1.0.1") 78 | 79 | // Coroutines 80 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") 81 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") 82 | 83 | // Coroutine Lifecycle Scopes 84 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") 85 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") 86 | 87 | // Coil 88 | implementation("io.coil-kt:coil-compose:2.3.0") 89 | implementation("io.coil-kt:coil-video:2.3.0") 90 | implementation("com.google.accompanist:accompanist-coil:0.15.0") 91 | 92 | // CameraX 93 | def cameraxVersion = "1.3.0" 94 | implementation("androidx.camera:camera-camera2:$cameraxVersion") 95 | implementation("androidx.camera:camera-core:$cameraxVersion") 96 | implementation("androidx.camera:camera-extensions:$cameraxVersion") 97 | implementation("androidx.camera:camera-lifecycle:$cameraxVersion") 98 | implementation("androidx.camera:camera-video:$cameraxVersion") 99 | implementation("androidx.camera:camera-view:$cameraxVersion") 100 | 101 | // ExoPlayer 102 | implementation("androidx.media3:media3-exoplayer:1.1.1") 103 | implementation("androidx.media3:media3-ui:1.1.1") 104 | 105 | // GPUImage 106 | implementation("jp.co.cyberagent.android:gpuimage:2.1.0") 107 | 108 | // Accompanist 109 | def accompanistVersion = '0.31.3-beta' 110 | implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion") 111 | implementation("com.google.accompanist:accompanist-pager:$accompanistVersion") 112 | implementation("com.google.accompanist:accompanist-navigation-animation:$accompanistVersion") 113 | implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion") 114 | 115 | // ExifInterface 116 | implementation("androidx.exifinterface:exifinterface:1.3.6") 117 | 118 | // SmartTool Factory 119 | implementation("com.github.smarttoolfactory:compose-cropper:0.4.0") 120 | implementation("com.github.smarttoolfactory:compose-zoom:master-SNAPSHOT") 121 | implementation("com.github.SmartToolFactory:Compose-Colorful-Sliders:1.2.2") 122 | implementation("com.github.SmartToolFactory:Compose-Color-Picker-Bundle:1.0.1") 123 | implementation("com.github.SmartToolFactory:Compose-Extended-Gestures:3.0.0") 124 | implementation("com.github.SmartToolFactory:Compose-AnimatedList:0.5.1") 125 | 126 | // Photo Picker 127 | implementation("com.google.modernstorage:modernstorage-photopicker:1.0.0-alpha06") 128 | 129 | // Test 130 | testImplementation("junit:junit:4.13.2") 131 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 132 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 133 | androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeVer") 134 | debugImplementation("androidx.compose.ui:ui-tooling:$composeVer") 135 | debugImplementation("androidx.compose.ui:ui-test-manifest:$composeVer") 136 | 137 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/release/app-release.aab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/release/app-release.aab -------------------------------------------------------------------------------- /app/src/androidTest/java/com/armutyus/cameraxproject/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.armutyus.cameraxproject", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | 47 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/CameraXComposeApplication.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import androidx.camera.camera2.Camera2Config 6 | import androidx.camera.core.CameraXConfig 7 | import coil.ImageLoader 8 | import coil.ImageLoaderFactory 9 | import coil.decode.VideoFrameDecoder 10 | import coil.memory.MemoryCache 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.asExecutor 13 | 14 | class CameraXComposeApplication : Application(), CameraXConfig.Provider, ImageLoaderFactory { 15 | override fun getCameraXConfig(): CameraXConfig = 16 | CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig()) 17 | .setCameraExecutor(Dispatchers.IO.asExecutor()) 18 | .setMinimumLoggingLevel(Log.ERROR) 19 | .build() 20 | 21 | override fun newImageLoader(): ImageLoader = 22 | ImageLoader.Builder(applicationContext) 23 | .components { 24 | add(VideoFrameDecoder.Factory()) 25 | }.crossfade(true) 26 | .memoryCache { 27 | MemoryCache.Builder(applicationContext) 28 | .maxSizePercent(0.25) 29 | .build() 30 | } 31 | .build() 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.widget.Toast 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.compose.animation.AnimatedVisibilityScope 9 | import androidx.compose.animation.ExperimentalAnimationApi 10 | import androidx.compose.animation.core.tween 11 | import androidx.compose.animation.fadeIn 12 | import androidx.compose.animation.fadeOut 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.remember 15 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 16 | import androidx.lifecycle.ViewModel 17 | import androidx.lifecycle.ViewModelProvider 18 | import androidx.media3.common.util.UnstableApi 19 | import androidx.navigation.* 20 | import com.armutyus.cameraxproject.ui.gallery.GalleryScreen 21 | import com.armutyus.cameraxproject.ui.gallery.GalleryViewModel 22 | import com.armutyus.cameraxproject.ui.gallery.preview.PreviewScreen 23 | import com.armutyus.cameraxproject.ui.gallery.preview.PreviewViewModel 24 | import com.armutyus.cameraxproject.ui.gallery.preview.editmedia.repo.EditMediaRepositoryImpl 25 | import com.armutyus.cameraxproject.ui.photo.PhotoScreen 26 | import com.armutyus.cameraxproject.ui.photo.PhotoViewModel 27 | import com.armutyus.cameraxproject.ui.theme.CameraXProjectTheme 28 | import com.armutyus.cameraxproject.ui.video.VideoScreen 29 | import com.armutyus.cameraxproject.ui.video.VideoViewModel 30 | import com.armutyus.cameraxproject.util.FileManager 31 | import com.armutyus.cameraxproject.util.Permissions 32 | import com.armutyus.cameraxproject.util.Util.Companion.ALL_CONTENT 33 | import com.armutyus.cameraxproject.util.Util.Companion.GALLERY_ROUTE 34 | import com.armutyus.cameraxproject.util.Util.Companion.PHOTO_ROUTE 35 | import com.armutyus.cameraxproject.util.Util.Companion.VIDEO_ROUTE 36 | import com.google.accompanist.navigation.animation.AnimatedNavHost 37 | import com.google.accompanist.navigation.animation.composable 38 | import com.google.accompanist.navigation.animation.rememberAnimatedNavController 39 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 40 | 41 | @OptIn(ExperimentalPermissionsApi::class) 42 | class MainActivity : ComponentActivity() { 43 | 44 | private val fileManager = FileManager(this) 45 | private val editMediaRepository = EditMediaRepositoryImpl(this) 46 | 47 | @UnstableApi 48 | @OptIn(ExperimentalAnimationApi::class) 49 | override fun onCreate(savedInstanceState: Bundle?) { 50 | super.onCreate(savedInstanceState) 51 | installSplashScreen() 52 | setContent { 53 | CameraXProjectTheme { 54 | val navController = rememberAnimatedNavController() 55 | 56 | @Suppress("UNCHECKED_CAST") 57 | val viewModelFactory = object : ViewModelProvider.Factory { 58 | override fun create(modelClass: Class): T { 59 | if (modelClass.isAssignableFrom(PhotoViewModel::class.java)) 60 | return PhotoViewModel(fileManager, navController) as T 61 | if (modelClass.isAssignableFrom(PreviewViewModel::class.java)) 62 | return PreviewViewModel( 63 | editMediaRepository, 64 | fileManager, 65 | navController 66 | ) as T 67 | if (modelClass.isAssignableFrom(VideoViewModel::class.java)) 68 | return VideoViewModel(fileManager, navController) as T 69 | if (modelClass.isAssignableFrom(GalleryViewModel::class.java)) 70 | return GalleryViewModel(fileManager, navController) as T 71 | throw IllegalArgumentException(getString(R.string.unknown_viewmodel)) 72 | } 73 | } 74 | 75 | Permissions( 76 | permissionGrantedContent = { 77 | AnimatedNavHost( 78 | navController = navController, 79 | startDestination = GALLERY_ROUTE 80 | ) { 81 | composableWithDefaultAnimation(GALLERY_ROUTE) { 82 | GalleryScreen(factory = viewModelFactory) 83 | } 84 | composableWithDefaultAnimation(route = PHOTO_ROUTE) { 85 | PhotoScreen(factory = viewModelFactory) { 86 | showMessage(this@MainActivity, it) 87 | } 88 | } 89 | composableWithDefaultAnimation(route = VIDEO_ROUTE) { 90 | VideoScreen(factory = viewModelFactory) { 91 | showMessage(this@MainActivity, it) 92 | } 93 | } 94 | composableWithDefaultAnimation( 95 | route = "preview_screen/?filePath={filePath}/?contentFilter={contentFilter}", 96 | arguments = listOf( 97 | navArgument("filePath") { 98 | type = NavType.StringType 99 | defaultValue = "" 100 | }, 101 | navArgument("contentFilter") { 102 | type = NavType.StringType 103 | defaultValue = ALL_CONTENT 104 | } 105 | ), 106 | ) { 107 | val filePath = remember { it.arguments?.getString("filePath") } 108 | val contentFilter = 109 | remember { it.arguments?.getString("contentFilter") } 110 | PreviewScreen( 111 | contentFilter = contentFilter ?: ALL_CONTENT, 112 | filePath = filePath ?: "", 113 | factory = viewModelFactory 114 | ) 115 | } 116 | } 117 | }) 118 | } 119 | } 120 | } 121 | } 122 | 123 | private fun showMessage(context: Context, message: String) { 124 | Toast.makeText(context, message, Toast.LENGTH_LONG).show() 125 | } 126 | 127 | @OptIn(ExperimentalAnimationApi::class) 128 | private fun NavGraphBuilder.composableWithDefaultAnimation( 129 | route: String, 130 | arguments: List = emptyList(), 131 | content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit 132 | ) { 133 | composable( 134 | route = route, 135 | arguments = arguments, 136 | enterTransition = { 137 | fadeIn(tween(700)) 138 | }, 139 | exitTransition = { 140 | fadeOut(tween(700)) 141 | }, 142 | popEnterTransition = { 143 | fadeIn(tween(700)) 144 | }, 145 | popExitTransition = { 146 | fadeOut(tween(700)) 147 | }, 148 | content = content 149 | ) 150 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/GalleryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.widget.Toast 8 | import androidx.core.content.FileProvider 9 | import androidx.core.net.toFile 10 | import androidx.core.net.toUri 11 | import androidx.lifecycle.LiveData 12 | import androidx.lifecycle.MutableLiveData 13 | import androidx.lifecycle.viewModelScope 14 | import androidx.navigation.NavController 15 | import com.armutyus.cameraxproject.R 16 | import com.armutyus.cameraxproject.ui.gallery.models.GalleryEvent 17 | import com.armutyus.cameraxproject.ui.gallery.models.GalleryState 18 | import com.armutyus.cameraxproject.ui.gallery.models.MediaItem 19 | import com.armutyus.cameraxproject.util.BaseViewModel 20 | import com.armutyus.cameraxproject.util.FileManager 21 | import com.armutyus.cameraxproject.util.Util.Companion.EDIT_DIR 22 | import com.armutyus.cameraxproject.util.Util.Companion.PHOTO_DIR 23 | import com.armutyus.cameraxproject.util.Util.Companion.PHOTO_ROUTE 24 | import com.armutyus.cameraxproject.util.Util.Companion.VIDEO_DIR 25 | import kotlinx.coroutines.launch 26 | 27 | class GalleryViewModel constructor( 28 | private val fileManager: FileManager, 29 | navController: NavController 30 | ) : BaseViewModel(navController) { 31 | 32 | private val _galleryState: MutableLiveData = MutableLiveData(GalleryState()) 33 | val galleryState: LiveData = _galleryState 34 | 35 | private val _mediaItems: MutableLiveData>> = 36 | MutableLiveData(mapOf()) 37 | val mediaItems: LiveData>> = _mediaItems 38 | 39 | fun onEvent(galleryEvent: GalleryEvent) { 40 | when (galleryEvent) { 41 | is GalleryEvent.ItemClicked -> onItemClicked( 42 | galleryEvent.item, 43 | galleryEvent.contentFilter 44 | ) 45 | 46 | is GalleryEvent.ShareTapped -> onShareTapped(galleryEvent.context) 47 | GalleryEvent.FabClicked -> onFabClicked() 48 | GalleryEvent.SelectAllClicked -> changeSelectAllState() 49 | GalleryEvent.ItemLongClicked -> onItemLongClicked() 50 | GalleryEvent.CancelSelectableMode -> cancelSelectableMode() 51 | GalleryEvent.CancelDelete -> cancelDeleteAction() 52 | GalleryEvent.DeleteTapped -> onDeleteTapped() 53 | GalleryEvent.DeleteSelectedItems -> deleteSelectedItems() 54 | } 55 | } 56 | 57 | fun loadMedia() = viewModelScope.launch { 58 | val media = mutableSetOf() 59 | 60 | val photoDir = fileManager.getPrivateFileDirectory(PHOTO_DIR) 61 | val photos = photoDir?.listFiles()?.mapIndexed { _, file -> 62 | val takenTime = file.name.substring(0, 10).replace("-", "/") 63 | MediaItem( 64 | takenTime, 65 | name = file.name, 66 | uri = file.toUri(), 67 | type = MediaItem.Type.PHOTO 68 | ) 69 | } as List 70 | 71 | val videoDir = fileManager.getPrivateFileDirectory(VIDEO_DIR) 72 | val videos = videoDir?.listFiles()?.mapIndexed { _, file -> 73 | val takenTime = file.name.substring(0, 10).replace("-", "/") 74 | MediaItem( 75 | takenTime, 76 | name = file.name, 77 | uri = file.toUri(), 78 | type = MediaItem.Type.VIDEO 79 | ) 80 | } as List 81 | 82 | val editedMediaDir = fileManager.getPrivateFileDirectory(EDIT_DIR) 83 | val editedMedia = editedMediaDir?.listFiles()?.mapIndexed { _, file -> 84 | val editTime = file.name.substring(4, 14).replace("-", "/") 85 | MediaItem( 86 | editTime, 87 | name = file.name, 88 | uri = file.toUri(), 89 | type = if (file.extension == "jpg") MediaItem.Type.PHOTO else MediaItem.Type.VIDEO 90 | ) 91 | } as List 92 | 93 | media.addAll(photos + videos + editedMedia) 94 | 95 | val groupedMedia = media.sortedByDescending { it.takenTime }.groupBy { it.takenTime } 96 | _mediaItems.value = groupedMedia 97 | } 98 | 99 | fun onItemCheckedChange(checked: Boolean, item: MediaItem) = viewModelScope.launch { 100 | val itemList = _mediaItems.value?.values?.flatten() ?: emptyList() 101 | val findItem = itemList.firstOrNull { it.uri == item.uri } 102 | findItem?.selected = checked 103 | } 104 | 105 | private fun onFabClicked() = viewModelScope.launch { 106 | cancelSelectableMode() 107 | navigateTo(PHOTO_ROUTE) 108 | } 109 | 110 | private fun changeSelectAllState() = viewModelScope.launch { 111 | val newValue: Boolean = !_galleryState.value!!.selectAllClicked 112 | _mediaItems.value?.forEach { 113 | it.value.forEach { mediaItem -> 114 | mediaItem.selected = newValue 115 | } 116 | } 117 | _galleryState.value = _galleryState.value!!.copy(selectAllClicked = newValue) 118 | } 119 | 120 | private fun onItemClicked(item: MediaItem?, contentFilter: String) = viewModelScope.launch { 121 | val uri = item?.uri 122 | navigateTo("preview_screen/?filePath=${uri?.toString()}/?contentFilter=${contentFilter}") 123 | } 124 | 125 | private fun onItemLongClicked() = viewModelScope.launch { 126 | _galleryState.value = _galleryState.value!!.copy(selectableMode = true) 127 | } 128 | 129 | private fun cancelSelectableMode() = viewModelScope.launch { 130 | _mediaItems.value?.forEach { 131 | it.value.forEach { mediaItem -> 132 | mediaItem.selected = false 133 | } 134 | } 135 | _galleryState.value = 136 | _galleryState.value!!.copy(selectableMode = false, selectAllClicked = false) 137 | } 138 | 139 | private fun onShareTapped(context: Context) = viewModelScope.launch { 140 | val itemList = _mediaItems.value?.values?.flatten() ?: emptyList() 141 | val selectedItems = itemList.filter { it.selected } 142 | if (selectedItems.isNotEmpty()) { 143 | val uriList = ArrayList() 144 | selectedItems.forEach { 145 | val contentUri = FileProvider.getUriForFile( 146 | context, 147 | "com.armutyus.cameraxproject.fileprovider", 148 | it.uri?.toFile()!! 149 | ) 150 | uriList.add(contentUri) 151 | } 152 | val shareIntent: Intent = Intent().apply { 153 | action = Intent.ACTION_SEND_MULTIPLE 154 | type = "*/*" 155 | putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList) 156 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) 157 | } 158 | try { 159 | context.startActivity( 160 | Intent.createChooser( 161 | shareIntent, 162 | context.getString(R.string.share) 163 | ) 164 | ) 165 | uriList.clear() 166 | } catch (e: ActivityNotFoundException) { 167 | Toast.makeText(context, R.string.no_app_available, Toast.LENGTH_SHORT).show() 168 | } 169 | } else { 170 | Toast.makeText(context, R.string.choose_media, Toast.LENGTH_SHORT).show() 171 | } 172 | } 173 | 174 | private fun cancelDeleteAction() = viewModelScope.launch { 175 | _galleryState.value = _galleryState.value!!.copy(deleteTapped = false) 176 | } 177 | 178 | private fun onDeleteTapped() = viewModelScope.launch { 179 | _galleryState.value = _galleryState.value!!.copy(deleteTapped = true) 180 | } 181 | 182 | private fun deleteSelectedItems() = viewModelScope.launch { 183 | val itemList = _mediaItems.value?.values?.flatten() ?: emptyList() 184 | val selectedItems = itemList.filter { it.selected } 185 | selectedItems.forEach { 186 | it.uri?.toFile()?.delete() 187 | } 188 | cancelDeleteAction() 189 | cancelSelectableMode() 190 | loadMedia() 191 | } 192 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/models/BottomNavItem.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.models 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.sharp.Cancel 5 | import androidx.compose.material.icons.sharp.Compare 6 | import androidx.compose.material.icons.sharp.Delete 7 | import androidx.compose.material.icons.sharp.Edit 8 | import androidx.compose.material.icons.sharp.LibraryBooks 9 | import androidx.compose.material.icons.sharp.PhotoLibrary 10 | import androidx.compose.material.icons.sharp.Share 11 | import androidx.compose.material.icons.sharp.VideoLibrary 12 | import androidx.compose.ui.graphics.vector.ImageVector 13 | import com.armutyus.cameraxproject.R 14 | 15 | sealed class BottomNavItem(var filter: MediaItem.Filter?, var icon: ImageVector, var label: Int) { 16 | 17 | object Gallery : BottomNavItem(MediaItem.Filter.ALL, Icons.Sharp.LibraryBooks, R.string.gallery) 18 | object Photos : 19 | BottomNavItem(MediaItem.Filter.PHOTOS, Icons.Sharp.PhotoLibrary, R.string.photos) 20 | 21 | object Videos : 22 | BottomNavItem(MediaItem.Filter.VIDEOS, Icons.Sharp.VideoLibrary, R.string.videos) 23 | 24 | object Edits : BottomNavItem(MediaItem.Filter.EDITS, Icons.Sharp.Compare, R.string.edits) 25 | 26 | object EditItem : BottomNavItem(null, Icons.Sharp.Edit, R.string.edit) 27 | object Cancel : BottomNavItem(null, Icons.Sharp.Cancel, R.string.cancel) 28 | object Delete : BottomNavItem(null, Icons.Sharp.Delete, R.string.delete) 29 | object Share : BottomNavItem(null, Icons.Sharp.Share, R.string.share) 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/models/GalleryEvent.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.models 2 | 3 | import android.content.Context 4 | 5 | sealed class GalleryEvent { 6 | 7 | data class ItemClicked(val item: MediaItem, val contentFilter: String) : GalleryEvent() 8 | data class ShareTapped(val context: Context) : GalleryEvent() 9 | 10 | object CancelSelectableMode : GalleryEvent() 11 | object CancelDelete : GalleryEvent() 12 | object DeleteSelectedItems : GalleryEvent() 13 | object DeleteTapped : GalleryEvent() 14 | object ItemLongClicked : GalleryEvent() 15 | object FabClicked : GalleryEvent() 16 | object SelectAllClicked : GalleryEvent() 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/models/GalleryState.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.models 2 | 3 | data class GalleryState( 4 | val selectableMode: Boolean = false, 5 | val selectAllClicked: Boolean = false, 6 | val deleteTapped: Boolean = false 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/models/MediaItem.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.models 2 | 3 | import android.net.Uri 4 | 5 | data class MediaItem( 6 | val takenTime: String = "", 7 | val name: String = "", 8 | var selected: Boolean = false, 9 | val uri: Uri? = Uri.EMPTY, 10 | val type: Type? = Type.UNKNOWN 11 | ) { 12 | enum class Type { UNKNOWN, PHOTO, VIDEO } 13 | enum class Filter { ALL, PHOTOS, VIDEOS, EDITS } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/PreviewViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.graphics.Bitmap 7 | import android.util.Log 8 | import android.widget.Toast 9 | import androidx.core.content.FileProvider 10 | import androidx.lifecycle.LiveData 11 | import androidx.lifecycle.MutableLiveData 12 | import androidx.lifecycle.viewModelScope 13 | import androidx.navigation.NavController 14 | import com.armutyus.cameraxproject.R 15 | import com.armutyus.cameraxproject.ui.gallery.preview.editmedia.models.ImageFilter 16 | import com.armutyus.cameraxproject.ui.gallery.preview.editmedia.repo.EditMediaRepository 17 | import com.armutyus.cameraxproject.ui.gallery.preview.models.PreviewScreenEvent 18 | import com.armutyus.cameraxproject.ui.gallery.preview.models.PreviewScreenState 19 | import com.armutyus.cameraxproject.util.BaseViewModel 20 | import com.armutyus.cameraxproject.util.FileManager 21 | import com.armutyus.cameraxproject.util.Util.Companion.EDIT_CONTENT 22 | import com.armutyus.cameraxproject.util.Util.Companion.EDIT_DIR 23 | import com.armutyus.cameraxproject.util.Util.Companion.GALLERY_ROUTE 24 | import com.armutyus.cameraxproject.util.Util.Companion.PHOTO_EXTENSION 25 | import com.armutyus.cameraxproject.util.Util.Companion.TAG 26 | import kotlinx.coroutines.launch 27 | import java.io.File 28 | 29 | class PreviewViewModel constructor( 30 | private val editMediaRepository: EditMediaRepository, 31 | private val fileManager: FileManager, 32 | navController: NavController 33 | ) : BaseViewModel(navController) { 34 | 35 | private val _previewScreenState: MutableLiveData = 36 | MutableLiveData(PreviewScreenState()) 37 | val previewScreenState: LiveData = _previewScreenState 38 | 39 | fun onEvent(previewScreenEvent: PreviewScreenEvent) { 40 | when (previewScreenEvent) { 41 | is PreviewScreenEvent.ShareTapped -> onShareTapped( 42 | previewScreenEvent.context, 43 | previewScreenEvent.file 44 | ) 45 | 46 | is PreviewScreenEvent.DeleteTapped -> onDeleteTapped(previewScreenEvent.file) 47 | is PreviewScreenEvent.FullScreenToggleTapped -> onFullScreenToggleTapped( 48 | previewScreenEvent.isFullScreen 49 | ) 50 | 51 | is PreviewScreenEvent.ChangeBarState -> onChangeBarState(previewScreenEvent.zoomState) 52 | is PreviewScreenEvent.HideController -> hideController(previewScreenEvent.isPlaying) 53 | is PreviewScreenEvent.SaveTapped -> saveEditedImage(previewScreenEvent.context) 54 | PreviewScreenEvent.EditTapped -> onEditTapped() 55 | PreviewScreenEvent.CancelEditTapped -> onCancelEditTapped() 56 | PreviewScreenEvent.PlayerViewTapped -> onPlayerViewTapped() 57 | PreviewScreenEvent.NavigateBack -> onNavigateBack() 58 | } 59 | } 60 | 61 | private fun onEditTapped() = viewModelScope.launch { 62 | _previewScreenState.value = _previewScreenState.value!!.copy( 63 | isInEditMode = true, 64 | showBars = false 65 | ) 66 | } 67 | 68 | private fun onCancelEditTapped() = viewModelScope.launch { 69 | _previewScreenState.value = _previewScreenState.value!!.copy( 70 | isInEditMode = false, 71 | showBars = true 72 | ) 73 | _imageHasFilter.value = false 74 | _isImageCropped.value = false 75 | _editedBitmap.value = null 76 | _croppedBitmap.value = null 77 | } 78 | 79 | private fun onDeleteTapped(file: File) = viewModelScope.launch { 80 | file.delete() 81 | navigateTo(GALLERY_ROUTE) 82 | } 83 | 84 | private fun onShareTapped(context: Context, file: File) = viewModelScope.launch { 85 | if (file.exists()) { 86 | val contentUri = FileProvider.getUriForFile( 87 | context, 88 | "com.armutyus.cameraxproject.fileprovider", 89 | file 90 | ) 91 | val shareIntent: Intent = Intent().apply { 92 | action = Intent.ACTION_SEND 93 | type = "*/*" 94 | putExtra(Intent.EXTRA_STREAM, contentUri) 95 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) 96 | } 97 | try { 98 | context.startActivity( 99 | Intent.createChooser( 100 | shareIntent, 101 | context.getString(R.string.share) 102 | ) 103 | ) 104 | } catch (e: ActivityNotFoundException) { 105 | Toast.makeText(context, R.string.no_app_available, Toast.LENGTH_SHORT).show() 106 | } 107 | } else { 108 | Toast.makeText(context, R.string.file_error, Toast.LENGTH_SHORT).show() 109 | } 110 | } 111 | 112 | private fun onFullScreenToggleTapped(isFullScreen: Boolean) = viewModelScope.launch { 113 | _previewScreenState.value = _previewScreenState.value!! 114 | .copy( 115 | isFullScreen = !isFullScreen, 116 | showBars = isFullScreen, 117 | showMediaController = isFullScreen 118 | ) 119 | } 120 | 121 | private fun onPlayerViewTapped() = viewModelScope.launch { 122 | val newValue = 123 | !(_previewScreenState.value!!.showBars && _previewScreenState.value!!.showMediaController) 124 | _previewScreenState.value = 125 | _previewScreenState.value!!.copy(showBars = newValue, showMediaController = newValue) 126 | } 127 | 128 | private fun onChangeBarState(zoomState: Boolean) = viewModelScope.launch { 129 | if (zoomState) { 130 | _previewScreenState.value = _previewScreenState.value?.copy(showBars = false) 131 | } else { 132 | val newValue = !_previewScreenState.value!!.showBars 133 | _previewScreenState.value = _previewScreenState.value?.copy(showBars = newValue) 134 | } 135 | } 136 | 137 | private fun hideController(isPlaying: Boolean) = viewModelScope.launch { 138 | _previewScreenState.value = 139 | _previewScreenState.value?.copy(showMediaController = !isPlaying, showBars = !isPlaying) 140 | } 141 | 142 | //region:: EditMedia Works 143 | 144 | private val _imageFilterList: MutableLiveData> = MutableLiveData(emptyList()) 145 | val imageFilterList: LiveData> = _imageFilterList 146 | 147 | private val _editedBitmap: MutableLiveData = MutableLiveData() 148 | val editedBitmap: LiveData = _editedBitmap 149 | 150 | private val _croppedBitmap: MutableLiveData = MutableLiveData() 151 | val croppedBitmap: LiveData = _croppedBitmap 152 | 153 | private val _imageHasFilter: MutableLiveData = MutableLiveData(false) 154 | val imageHasFilter: LiveData = _imageHasFilter 155 | 156 | private val _isImageCropped: MutableLiveData = MutableLiveData(false) 157 | val isImageCropped: LiveData = _isImageCropped 158 | 159 | fun loadImageFilters(bitmap: Bitmap?) = viewModelScope.launch { 160 | kotlin.runCatching { 161 | val image = getPreviewImage(originalImage = bitmap!!) 162 | editMediaRepository.getImageFiltersList(image) 163 | }.onSuccess { 164 | setImageFilterList(it) 165 | }.onFailure { 166 | Log.e(TAG, it.localizedMessage ?: "Bitmaps with filter load failed.") 167 | } 168 | } 169 | 170 | private fun getPreviewImage(originalImage: Bitmap): Bitmap { 171 | return kotlin.runCatching { 172 | val previewWidth = 90 173 | val previewHeight = originalImage.height * previewWidth / originalImage.width 174 | Bitmap.createScaledBitmap(originalImage, previewWidth, previewHeight, true) 175 | }.getOrDefault(originalImage) 176 | } 177 | 178 | private fun saveEditedImage(context: Context) = viewModelScope.launch { 179 | if (_editedBitmap.value != null || _imageHasFilter.value == true) { 180 | fileManager.saveEditedImageToFile(_editedBitmap.value!!, EDIT_DIR, PHOTO_EXTENSION) 181 | .also { 182 | navigateTo("preview_screen/?filePath=${it}/?contentFilter=${EDIT_CONTENT}") 183 | onCancelEditTapped() 184 | } 185 | Toast.makeText(context, R.string.edited_image_saved, Toast.LENGTH_SHORT).show() 186 | } else { 187 | Toast.makeText(context, R.string.no_changes_on_image, Toast.LENGTH_SHORT).show() 188 | } 189 | } 190 | 191 | private fun setImageFilterList(imageFilterList: List) = viewModelScope.launch { 192 | _imageFilterList.value = imageFilterList 193 | } 194 | 195 | fun setCroppedImage(croppedImage: Bitmap?) = viewModelScope.launch { 196 | _isImageCropped.value = croppedImage != null 197 | _croppedBitmap.value = croppedImage 198 | _editedBitmap.value = croppedImage 199 | } 200 | 201 | fun selectedFilter(filterName: String) = viewModelScope.launch { 202 | _imageHasFilter.value = filterName != "Normal" 203 | } 204 | 205 | fun setEditedBitmap(imageBitmap: Bitmap) = viewModelScope.launch { 206 | _editedBitmap.value = imageBitmap 207 | } 208 | 209 | fun switchEditMode(editModeName: String) = viewModelScope.launch { 210 | _previewScreenState.value = _previewScreenState.value?.copy(switchEditMode = editModeName) 211 | } 212 | 213 | //endregion 214 | 215 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/AnimatedAspectRatioSelection.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.layout.width 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableIntStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.setValue 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.graphicsLayer 13 | import androidx.compose.ui.unit.Dp 14 | import androidx.compose.ui.unit.dp 15 | import com.smarttoolfactory.animatedlist.AnimatedInfiniteLazyRow 16 | import com.smarttoolfactory.animatedlist.model.AnimationProgress 17 | import com.smarttoolfactory.cropper.model.CropAspectRatio 18 | import com.smarttoolfactory.cropper.model.aspectRatios 19 | import com.smarttoolfactory.cropper.widget.AspectRatioSelectionCard 20 | 21 | @Composable 22 | internal fun AnimatedAspectRatioSelection( 23 | modifier: Modifier = Modifier, 24 | initialSelectedIndex: Int = 2, 25 | onAspectRatioChange: (CropAspectRatio) -> Unit 26 | ) { 27 | 28 | var currentIndex by remember { mutableIntStateOf(initialSelectedIndex) } 29 | 30 | AnimatedInfiniteLazyRow( 31 | modifier = modifier.padding(horizontal = 10.dp), 32 | items = aspectRatios, 33 | inactiveItemPercent = 80, 34 | initialFirstVisibleIndex = initialSelectedIndex - 2 35 | ) { animationProgress: AnimationProgress, _: Int, item: CropAspectRatio, width: Dp -> 36 | 37 | val scale = animationProgress.scale 38 | val color = animationProgress.color 39 | val selectedLocalIndex = animationProgress.itemIndex 40 | 41 | AspectRatioSelectionCard(modifier = Modifier 42 | .graphicsLayer { 43 | scaleX = scale 44 | scaleY = scale 45 | } 46 | .width(width), 47 | contentColor = MaterialTheme.colorScheme.surface, 48 | color = color, 49 | cropAspectRatio = item 50 | ) 51 | 52 | if (currentIndex != selectedLocalIndex) { 53 | currentIndex = selectedLocalIndex 54 | onAspectRatioChange(aspectRatios[selectedLocalIndex]) 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/BaseSheet.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.foundation.rememberScrollState 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.foundation.verticalScroll 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Surface 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.unit.dp 19 | 20 | @Composable 21 | internal fun BaseSheet(content: @Composable () -> Unit) { 22 | Surface { 23 | Column { 24 | Box( 25 | modifier = Modifier 26 | .padding(vertical = 16.dp) 27 | .fillMaxWidth(), 28 | contentAlignment = Alignment.Center 29 | ) { 30 | Box( 31 | modifier = Modifier 32 | .width(32.dp) 33 | .height(4.dp) 34 | .background( 35 | MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = .4f), 36 | RoundedCornerShape(50) 37 | ) 38 | ) 39 | } 40 | 41 | Column( 42 | modifier = Modifier 43 | .fillMaxWidth() 44 | .height(400.dp) 45 | .padding(horizontal = 16.dp, vertical = 8.dp) 46 | .verticalScroll(rememberScrollState()) 47 | ) { 48 | content() 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/ContentScaleDialogSelection.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.layout.ContentScale 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | 17 | val contentScaleOptions = 18 | listOf("None", "Fit", "Crop", "FillBounds", "FillWidth", "FillHeight", "Inside") 19 | 20 | @Composable 21 | internal fun ContentScaleDialogSelection( 22 | contentScale: ContentScale, 23 | onContentScaleChanged: (ContentScale) -> Unit 24 | ) { 25 | 26 | var showDialog by remember { mutableStateOf(false) } 27 | 28 | val index = when (contentScale) { 29 | ContentScale.None -> 0 30 | ContentScale.Fit -> 1 31 | ContentScale.Crop -> 2 32 | ContentScale.FillBounds -> 3 33 | ContentScale.FillWidth -> 4 34 | ContentScale.FillHeight -> 5 35 | else -> 6 36 | } 37 | 38 | Text( 39 | text = contentScaleOptions[index], 40 | fontSize = 18.sp, 41 | modifier = Modifier 42 | .fillMaxWidth() 43 | .clickable { 44 | showDialog = true 45 | } 46 | .padding(8.dp) 47 | ) 48 | 49 | if (showDialog) { 50 | DialogWithMultipleSelection( 51 | title = "Content Scale", 52 | options = contentScaleOptions, 53 | value = index, 54 | onDismiss = { showDialog = false }, 55 | onConfirm = { 56 | 57 | val scale = when (it) { 58 | 0 -> ContentScale.None 59 | 1 -> ContentScale.Fit 60 | 2 -> ContentScale.Crop 61 | 3 -> ContentScale.FillBounds 62 | 4 -> ContentScale.FillWidth 63 | 5 -> ContentScale.FillHeight 64 | else -> ContentScale.Inside 65 | } 66 | onContentScaleChanged(scale) 67 | showDialog = false 68 | } 69 | ) 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/CropFrameEditDialog.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.Text 5 | import androidx.compose.material3.TextButton 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.setValue 11 | import androidx.compose.ui.graphics.ImageBitmap 12 | import androidx.compose.ui.res.imageResource 13 | import com.smarttoolfactory.cropper.R 14 | import com.smarttoolfactory.cropper.model.AspectRatio 15 | import com.smarttoolfactory.cropper.model.CropFrame 16 | import com.smarttoolfactory.cropper.model.CropOutline 17 | import com.smarttoolfactory.cropper.model.CustomPathOutline 18 | import com.smarttoolfactory.cropper.model.CutCornerCropShape 19 | import com.smarttoolfactory.cropper.model.ImageMaskOutline 20 | import com.smarttoolfactory.cropper.model.OutlineType 21 | import com.smarttoolfactory.cropper.model.OvalCropShape 22 | import com.smarttoolfactory.cropper.model.PolygonCropShape 23 | import com.smarttoolfactory.cropper.model.RoundedCornerCropShape 24 | import com.smarttoolfactory.cropper.model.getOutlineContainer 25 | 26 | @Composable 27 | fun CropFrameEditDialog( 28 | aspectRatio: AspectRatio, 29 | index: Int, 30 | cropFrame: CropFrame, 31 | onConfirm: (CropFrame) -> Unit, 32 | onDismiss: () -> Unit 33 | ) { 34 | 35 | val dstBitmap = ImageBitmap.imageResource(id = R.drawable.landscape2) 36 | 37 | val outlineType = cropFrame.outlineType 38 | 39 | var outline: CropOutline by remember { 40 | mutableStateOf(cropFrame.outlines[index]) 41 | } 42 | 43 | AlertDialog( 44 | onDismissRequest = onDismiss, 45 | text = { 46 | when (outlineType) { 47 | OutlineType.RoundedRect -> { 48 | 49 | val shape = outline as RoundedCornerCropShape 50 | 51 | RoundedCornerCropShapeEdit( 52 | aspectRatio = aspectRatio, 53 | dstBitmap = dstBitmap, 54 | title = outline.title, 55 | roundedCornerCropShape = shape 56 | ) { 57 | outline = it 58 | } 59 | } 60 | 61 | OutlineType.CutCorner -> { 62 | val shape = outline as CutCornerCropShape 63 | 64 | CutCornerCropShapeEdit( 65 | aspectRatio = aspectRatio, 66 | dstBitmap = dstBitmap, 67 | title = outline.title, 68 | cutCornerCropShape = shape 69 | ) { 70 | outline = it 71 | } 72 | } 73 | 74 | OutlineType.Oval -> { 75 | 76 | val shape = outline as OvalCropShape 77 | 78 | OvalCropShapeEdit( 79 | aspectRatio = aspectRatio, 80 | dstBitmap = dstBitmap, 81 | title = outline.title, 82 | ovalCropShape = shape 83 | ) { 84 | outline = it 85 | } 86 | } 87 | 88 | OutlineType.Polygon -> { 89 | 90 | val shape = outline as PolygonCropShape 91 | 92 | PolygonCropShapeEdit( 93 | aspectRatio = aspectRatio, 94 | dstBitmap = dstBitmap, 95 | title = outline.title, 96 | polygonCropShape = shape 97 | ) { 98 | outline = it 99 | } 100 | } 101 | 102 | OutlineType.ImageMask -> { 103 | val imageMaskOutline = outline as ImageMaskOutline 104 | ImageMaskEdit(imageMaskOutline) { 105 | outline = it 106 | } 107 | } 108 | 109 | OutlineType.Custom -> { 110 | val customPathOutline = outline as CustomPathOutline 111 | CustomPathEdit( 112 | aspectRatio = aspectRatio, 113 | dstBitmap = dstBitmap, 114 | customPathOutline = customPathOutline, 115 | ) { 116 | outline = it 117 | } 118 | } 119 | 120 | else -> Unit 121 | } 122 | }, 123 | confirmButton = { 124 | TextButton( 125 | onClick = { 126 | val newOutlines: List = cropFrame.outlines 127 | .toMutableList() 128 | .apply { 129 | set(index, outline) 130 | } 131 | .toList() 132 | 133 | val newCropFrame = cropFrame.copy( 134 | cropOutlineContainer = getOutlineContainer(outlineType, index, newOutlines) 135 | ) 136 | 137 | onConfirm(newCropFrame) 138 | }) { 139 | Text("Accept") 140 | } 141 | }, 142 | dismissButton = { 143 | TextButton( 144 | onClick = { onDismiss() } 145 | ) { 146 | Text(text = "Cancel") 147 | } 148 | } 149 | ) 150 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/CropFrameSelection.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.width 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.setValue 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.Dp 13 | import androidx.compose.ui.unit.dp 14 | import com.smarttoolfactory.animatedlist.AnimatedInfiniteLazyRow 15 | import com.smarttoolfactory.animatedlist.model.AnimationProgress 16 | import com.smarttoolfactory.cropper.model.AspectRatio 17 | import com.smarttoolfactory.cropper.model.CropFrame 18 | import com.smarttoolfactory.cropper.model.OutlineType 19 | import com.smarttoolfactory.cropper.settings.CropFrameFactory 20 | import com.smarttoolfactory.cropper.settings.CropOutlineProperty 21 | import com.smarttoolfactory.cropper.widget.CropFrameDisplayCard 22 | 23 | /** 24 | * Crop frame selection 25 | */ 26 | @Composable 27 | fun CropFrameSelection( 28 | aspectRatio: AspectRatio, 29 | cropFrameFactory: CropFrameFactory, 30 | cropOutlineProperty: CropOutlineProperty, 31 | conCropOutlinePropertyChange: (CropOutlineProperty) -> Unit 32 | ) { 33 | 34 | var showEditDialog by remember { mutableStateOf(false) } 35 | 36 | var cropFrame by remember { 37 | mutableStateOf( 38 | cropFrameFactory.getCropFrame(cropOutlineProperty.outlineType) 39 | ) 40 | } 41 | 42 | if (showEditDialog) { 43 | CropFrameListDialog( 44 | aspectRatio = aspectRatio, 45 | cropFrame = cropFrame, 46 | onConfirm = { 47 | cropFrame = it 48 | cropFrameFactory.editCropFrame(cropFrame) 49 | 50 | conCropOutlinePropertyChange( 51 | CropOutlineProperty( 52 | it.outlineType, 53 | it.cropOutlineContainer.selectedItem 54 | ) 55 | ) 56 | showEditDialog = false 57 | }, 58 | onDismiss = { 59 | showEditDialog = false 60 | } 61 | ) 62 | } 63 | 64 | val initialIndex = remember { 65 | OutlineType.values().indexOfFirst { 66 | it == cropOutlineProperty.outlineType 67 | } 68 | } 69 | 70 | CropFrameSelectionList( 71 | modifier = Modifier.fillMaxWidth(), 72 | cropFrames = cropFrameFactory.getCropFrames(), 73 | initialSelectedIndex = initialIndex, 74 | onClick = { 75 | cropFrame = it 76 | showEditDialog = true 77 | }, 78 | onCropFrameChange = { 79 | conCropOutlinePropertyChange( 80 | CropOutlineProperty( 81 | it.outlineType, 82 | it.cropOutlineContainer.selectedItem 83 | ) 84 | ) 85 | } 86 | ) 87 | } 88 | 89 | /** 90 | * Animated list for selecting [CropFrame] 91 | */ 92 | @Composable 93 | private fun CropFrameSelectionList( 94 | modifier: Modifier = Modifier, 95 | initialSelectedIndex: Int = 0, 96 | cropFrames: List, 97 | onClick: (CropFrame) -> Unit, 98 | onCropFrameChange: (CropFrame) -> Unit 99 | ) { 100 | 101 | var currentIndex by remember { mutableStateOf(initialSelectedIndex) } 102 | 103 | AnimatedInfiniteLazyRow( 104 | modifier = modifier.padding(horizontal = 10.dp), 105 | items = cropFrames, 106 | inactiveItemPercent = 80, 107 | initialFirstVisibleIndex = initialSelectedIndex - 2, 108 | ) { animationProgress: AnimationProgress, _: Int, item: CropFrame, width: Dp -> 109 | 110 | val scale = animationProgress.scale 111 | val color = animationProgress.color 112 | 113 | val selectedLocalIndex = animationProgress.itemIndex 114 | val cropOutline = item.cropOutlineContainer.selectedItem 115 | 116 | val editable = item.editable 117 | 118 | CropFrameDisplayCard( 119 | modifier = Modifier.width(width), 120 | editable = editable, 121 | scale = scale, 122 | outlineColor = color, 123 | title = cropOutline.title, 124 | cropOutline = cropOutline 125 | ) { 126 | onClick(item) 127 | } 128 | 129 | if (currentIndex != selectedLocalIndex) { 130 | currentIndex = selectedLocalIndex 131 | onCropFrameChange(cropFrames[selectedLocalIndex]) 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/CropPropertySelection.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.LocalDensity 10 | import androidx.compose.ui.unit.dp 11 | import androidx.compose.ui.unit.sp 12 | import com.smarttoolfactory.cropper.model.AspectRatio 13 | import com.smarttoolfactory.cropper.model.CropAspectRatio 14 | import com.smarttoolfactory.cropper.model.aspectRatios 15 | import com.smarttoolfactory.cropper.settings.CropFrameFactory 16 | import com.smarttoolfactory.cropper.settings.CropProperties 17 | import com.smarttoolfactory.cropper.settings.CropType 18 | import kotlin.math.roundToInt 19 | 20 | 21 | @Composable 22 | internal fun CropPropertySelectionMenu( 23 | cropFrameFactory: CropFrameFactory, 24 | cropProperties: CropProperties, 25 | onCropPropertiesChange: (CropProperties) -> Unit 26 | ) { 27 | 28 | val density = LocalDensity.current 29 | 30 | // Crop properties 31 | val cropType = cropProperties.cropType 32 | val aspectRatio = cropProperties.aspectRatio 33 | 34 | val handleSize = density.run { cropProperties.handleSize.toDp() } 35 | val contentScale = cropProperties.contentScale 36 | val cropOutlineProperty = cropProperties.cropOutlineProperty 37 | val overlayRatio = (cropProperties.overlayRatio * 100) 38 | 39 | Title("Crop Type") 40 | CropTypeDialogSelection( 41 | cropType = cropType, 42 | onCropTypeChange = { cropTypeChange -> 43 | onCropPropertiesChange( 44 | cropProperties.copy(cropType = cropTypeChange) 45 | ) 46 | } 47 | ) 48 | 49 | Title("Content Scale") 50 | ContentScaleDialogSelection(contentScale) { 51 | onCropPropertiesChange( 52 | cropProperties.copy(contentScale = it) 53 | ) 54 | } 55 | 56 | Title("Aspect Ratio") 57 | AspectRatioSelection( 58 | aspectRatio = aspectRatio, 59 | onAspectRatioChange = { 60 | onCropPropertiesChange( 61 | cropProperties.copy(aspectRatio = it.aspectRatio) 62 | ) 63 | } 64 | ) 65 | 66 | Title("Frame") 67 | CropFrameSelection( 68 | aspectRatio = aspectRatio, 69 | cropFrameFactory = cropFrameFactory, 70 | cropOutlineProperty = cropOutlineProperty, 71 | conCropOutlinePropertyChange = { 72 | onCropPropertiesChange( 73 | cropProperties.copy(cropOutlineProperty = it) 74 | ) 75 | } 76 | ) 77 | 78 | Title("Overlay Ratio ${overlayRatio.toInt()}%") 79 | SliderSelection( 80 | value = overlayRatio, valueRange = 50f..100f 81 | ) { 82 | onCropPropertiesChange( 83 | cropProperties.copy(overlayRatio = (it.toInt() / 100f)) 84 | ) 85 | } 86 | 87 | // Handle size and overlay size applies only to Dynamic crop 88 | if (cropType == CropType.Dynamic) { 89 | Title("Handle Size") 90 | DpSliderSelection( 91 | value = handleSize, 92 | onValueChange = { 93 | onCropPropertiesChange( 94 | cropProperties.copy(handleSize = density.run { it.toPx() }) 95 | ) 96 | }, 97 | lowerBound = 10.dp, 98 | upperBound = 40.dp 99 | ) 100 | } 101 | } 102 | 103 | @Composable 104 | internal fun CropGestureSelectionMenu( 105 | cropProperties: CropProperties, 106 | onCropPropertiesChange: (CropProperties) -> Unit 107 | ) { 108 | // Gestures 109 | val flingEnabled = cropProperties.fling 110 | val pannable = cropProperties.pannable 111 | val zoomable = cropProperties.zoomable 112 | val maxZoom = cropProperties.maxZoom 113 | 114 | Title("Pan Enabled") 115 | PanEnableSelection( 116 | panEnabled = pannable, 117 | onPanEnabledChange = { 118 | onCropPropertiesChange( 119 | cropProperties.copy(pannable = it) 120 | ) 121 | } 122 | ) 123 | 124 | Title("Fling") 125 | FlingEnableSelection( 126 | flingEnabled = flingEnabled, 127 | onFlingEnabledChange = { 128 | onCropPropertiesChange( 129 | cropProperties.copy(fling = it) 130 | ) 131 | } 132 | ) 133 | 134 | Title("Zoom Enabled") 135 | ZoomEnableSelection( 136 | zoomEnabled = zoomable, 137 | onZoomEnabledChange = { 138 | onCropPropertiesChange( 139 | cropProperties.copy(zoomable = it) 140 | ) 141 | } 142 | ) 143 | 144 | AnimatedVisibility(visible = zoomable) { 145 | Column { 146 | 147 | Title("Max Zoom ${maxZoom}x", fontSize = 16.sp) 148 | MaxZoomSelection( 149 | maxZoom = maxZoom, 150 | onMaxZoomChange = { 151 | 152 | val max = (it * 100f).roundToInt() / 100f 153 | onCropPropertiesChange( 154 | cropProperties.copy(maxZoom = max) 155 | ) 156 | }, 157 | valueRange = 1f..10f 158 | ) 159 | } 160 | } 161 | } 162 | 163 | @Composable 164 | internal fun AspectRatioSelection( 165 | aspectRatio: AspectRatio, 166 | onAspectRatioChange: (CropAspectRatio) -> Unit 167 | ) { 168 | 169 | val initialSelectedIndex = remember { 170 | val aspectRatios = aspectRatios 171 | val aspectRatioModel = aspectRatios.first { it.aspectRatio.value == aspectRatio.value } 172 | aspectRatios.indexOf(aspectRatioModel) 173 | } 174 | 175 | AnimatedAspectRatioSelection( 176 | modifier = Modifier.fillMaxWidth(), 177 | initialSelectedIndex = initialSelectedIndex 178 | ) { 179 | onAspectRatioChange(it) 180 | } 181 | } 182 | 183 | @Composable 184 | internal fun FlingEnableSelection( 185 | flingEnabled: Boolean, 186 | onFlingEnabledChange: (Boolean) -> Unit 187 | ) { 188 | FullRowSwitch( 189 | label = "Enable fling gesture", 190 | state = flingEnabled, 191 | onStateChange = onFlingEnabledChange 192 | ) 193 | 194 | } 195 | 196 | @Composable 197 | internal fun PanEnableSelection( 198 | panEnabled: Boolean, 199 | onPanEnabledChange: (Boolean) -> Unit 200 | ) { 201 | FullRowSwitch( 202 | label = "Enable pan gesture", 203 | state = panEnabled, 204 | onStateChange = onPanEnabledChange 205 | ) 206 | } 207 | 208 | @Composable 209 | internal fun ZoomEnableSelection( 210 | zoomEnabled: Boolean, 211 | onZoomEnabledChange: (Boolean) -> Unit 212 | ) { 213 | FullRowSwitch( 214 | label = "Enable zoom gesture", 215 | state = zoomEnabled, 216 | onStateChange = onZoomEnabledChange 217 | ) 218 | } 219 | 220 | @Composable 221 | internal fun MaxZoomSelection( 222 | maxZoom: Float, 223 | onMaxZoomChange: (Float) -> Unit, 224 | valueRange: ClosedFloatingPointRange 225 | ) { 226 | SliderSelection( 227 | value = maxZoom, 228 | onValueChange = onMaxZoomChange, 229 | valueRange = valueRange 230 | ) 231 | } 232 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/CropShapeAddDialog.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.Text 5 | import androidx.compose.material3.TextButton 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.setValue 11 | import androidx.compose.ui.graphics.ImageBitmap 12 | import androidx.compose.ui.res.imageResource 13 | import com.smarttoolfactory.cropper.R 14 | import com.smarttoolfactory.cropper.model.AspectRatio 15 | import com.smarttoolfactory.cropper.model.CropFrame 16 | import com.smarttoolfactory.cropper.model.CropOutline 17 | import com.smarttoolfactory.cropper.model.CutCornerCropShape 18 | import com.smarttoolfactory.cropper.model.OutlineType 19 | import com.smarttoolfactory.cropper.model.OvalCropShape 20 | import com.smarttoolfactory.cropper.model.PolygonCropShape 21 | import com.smarttoolfactory.cropper.model.RoundedCornerCropShape 22 | import com.smarttoolfactory.cropper.model.getOutlineContainer 23 | 24 | @Composable 25 | fun CropShapeAddDialog( 26 | aspectRatio: AspectRatio, 27 | cropFrame: CropFrame, 28 | onConfirm: (CropFrame) -> Unit, 29 | onDismiss: () -> Unit 30 | ) { 31 | 32 | val dstBitmap = ImageBitmap.imageResource(id = R.drawable.landscape2) 33 | 34 | val outlineType = cropFrame.outlineType 35 | 36 | var outline: CropOutline by remember { 37 | mutableStateOf(cropFrame.copy().outlines[0]) 38 | } 39 | 40 | AlertDialog( 41 | onDismissRequest = onDismiss, 42 | text = { 43 | when (outlineType) { 44 | OutlineType.RoundedRect -> { 45 | 46 | val shape = outline as RoundedCornerCropShape 47 | 48 | RoundedCornerCropShapeEdit( 49 | aspectRatio = aspectRatio, 50 | dstBitmap = dstBitmap, 51 | title = outline.title, 52 | roundedCornerCropShape = shape 53 | ) { 54 | outline = it 55 | } 56 | } 57 | 58 | OutlineType.CutCorner -> { 59 | val shape = outline as CutCornerCropShape 60 | 61 | CutCornerCropShapeEdit( 62 | aspectRatio = aspectRatio, 63 | dstBitmap = dstBitmap, 64 | title = outline.title, 65 | cutCornerCropShape = shape 66 | ) { 67 | outline = it 68 | } 69 | } 70 | 71 | OutlineType.Oval -> { 72 | 73 | val shape = outline as OvalCropShape 74 | 75 | OvalCropShapeEdit( 76 | aspectRatio = aspectRatio, 77 | dstBitmap = dstBitmap, 78 | title = outline.title, 79 | ovalCropShape = shape 80 | ) { 81 | outline = it 82 | } 83 | } 84 | 85 | OutlineType.Polygon -> { 86 | 87 | val shape = outline as PolygonCropShape 88 | 89 | PolygonCropShapeEdit( 90 | aspectRatio = aspectRatio, 91 | dstBitmap = dstBitmap, 92 | title = outline.title, 93 | polygonCropShape = shape 94 | ) { 95 | outline = it 96 | } 97 | } 98 | 99 | else -> Unit 100 | } 101 | }, 102 | confirmButton = { 103 | TextButton(onClick = { 104 | 105 | val newOutlines: List = cropFrame.outlines 106 | .toMutableList() 107 | .apply { 108 | add(outline) 109 | } 110 | .toList() 111 | 112 | val newCropFrame = cropFrame.copy( 113 | cropOutlineContainer = getOutlineContainer( 114 | outlineType = outlineType, 115 | index = newOutlines.size - 1, 116 | outlines = newOutlines 117 | ) 118 | ) 119 | 120 | onConfirm(newCropFrame) 121 | }) { 122 | Text("Accept") 123 | } 124 | }, 125 | dismissButton = { 126 | TextButton(onClick = { onDismiss() }) { 127 | Text(text = "Cancel") 128 | } 129 | } 130 | ) 131 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/CropStyleSelectionMenu.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.border 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.foundation.shape.CircleShape 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.clip 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import com.smarttoolfactory.colorpicker.dialog.ColorPickerRingDiamondHSLDialog 28 | import com.smarttoolfactory.cropper.settings.CropStyle 29 | import com.smarttoolfactory.cropper.settings.CropType 30 | 31 | /** 32 | * Crop style selection menu 33 | */ 34 | @Composable 35 | internal fun CropStyleSelectionMenu( 36 | cropType: CropType, 37 | cropStyle: CropStyle, 38 | onCropStyleChange: (CropStyle) -> Unit 39 | ) { 40 | 41 | BaseSheet { 42 | val drawOverlayEnabled = cropStyle.drawOverlay 43 | val overlayStrokeWidth = cropStyle.strokeWidth 44 | val overlayColor = cropStyle.overlayColor 45 | val handleColor = cropStyle.handleColor 46 | val backgroundColor = cropStyle.backgroundColor 47 | val drawGridEnabled = cropStyle.drawGrid 48 | 49 | 50 | Title("Overlay") 51 | FullRowSwitch( 52 | label = "Draw overlay", 53 | state = drawOverlayEnabled, 54 | onStateChange = { 55 | onCropStyleChange( 56 | cropStyle.copy(drawOverlay = it) 57 | ) 58 | } 59 | ) 60 | 61 | AnimatedVisibility( 62 | visible = drawOverlayEnabled 63 | ) { 64 | 65 | Column { 66 | Title("StrokeWidth", fontSize = 16.sp) 67 | DpSliderSelection( 68 | value = overlayStrokeWidth, 69 | onValueChange = { 70 | onCropStyleChange( 71 | cropStyle.copy(strokeWidth = it) 72 | ) 73 | }, 74 | lowerBound = .5.dp, 75 | upperBound = 3.dp 76 | ) 77 | 78 | 79 | ColorSelection( 80 | title = "Overlay Color", 81 | color = overlayColor, 82 | onColorChange = { color: Color -> 83 | onCropStyleChange( 84 | cropStyle.copy(overlayColor = color) 85 | ) 86 | } 87 | ) 88 | 89 | if (cropType == CropType.Dynamic) { 90 | Spacer(modifier = Modifier.height(20.dp)) 91 | ColorSelection( 92 | title = "Handle Color", 93 | color = handleColor, 94 | onColorChange = { color: Color -> 95 | onCropStyleChange( 96 | cropStyle.copy(handleColor = color) 97 | ) 98 | } 99 | ) 100 | } 101 | 102 | Spacer(modifier = Modifier.height(20.dp)) 103 | ColorSelection( 104 | title = "Background Color", 105 | color = backgroundColor, 106 | onColorChange = { color: Color -> 107 | onCropStyleChange( 108 | cropStyle.copy(backgroundColor = color) 109 | ) 110 | } 111 | ) 112 | 113 | Title("Grid") 114 | FullRowSwitch( 115 | label = "Draw grid", 116 | state = drawGridEnabled, 117 | onStateChange = { 118 | onCropStyleChange( 119 | cropStyle.copy(drawGrid = it) 120 | ) 121 | } 122 | ) 123 | } 124 | } 125 | } 126 | } 127 | 128 | @Composable 129 | internal fun ColorSelection( 130 | title: String, 131 | color: Color, 132 | onColorChange: (Color) -> Unit 133 | ) { 134 | 135 | var showColorDialog by remember { mutableStateOf(false) } 136 | Row( 137 | modifier = Modifier.fillMaxWidth(), 138 | verticalAlignment = Alignment.CenterVertically 139 | ) { 140 | Title(title, fontSize = 16.sp) 141 | Spacer(modifier = Modifier.weight(1f)) 142 | Box( 143 | modifier = Modifier 144 | .clip(CircleShape) 145 | .size(40.dp) 146 | .border( 147 | 1.dp, 148 | MaterialTheme.colorScheme.primary, 149 | CircleShape 150 | ) 151 | .background(color = color) 152 | .clickable { 153 | showColorDialog = true 154 | } 155 | ) 156 | } 157 | 158 | if (showColorDialog) { 159 | ColorPickerRingDiamondHSLDialog( 160 | initialColor = color, 161 | onDismiss = { colorChange: Color, _: String -> 162 | showColorDialog = false 163 | onColorChange(colorChange) 164 | } 165 | ) 166 | } 167 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/CropTypeDialogSelection.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.unit.sp 15 | import com.smarttoolfactory.cropper.settings.CropType 16 | 17 | @Composable 18 | internal fun CropTypeDialogSelection( 19 | cropType: CropType, 20 | onCropTypeChange: (CropType) -> Unit 21 | ) { 22 | 23 | val cropTypeOptions = 24 | remember { listOf(CropType.Dynamic.toString(), CropType.Static.toString()) } 25 | 26 | var showDialog by remember { mutableStateOf(false) } 27 | 28 | val index = when (cropType) { 29 | CropType.Dynamic -> 0 30 | else -> 1 31 | } 32 | 33 | Text( 34 | text = cropTypeOptions[index], 35 | fontSize = 18.sp, 36 | modifier = Modifier 37 | .fillMaxWidth() 38 | .clickable { 39 | showDialog = true 40 | } 41 | .padding(8.dp) 42 | 43 | ) 44 | 45 | if (showDialog) { 46 | DialogWithMultipleSelection( 47 | title = "Crop Type", 48 | options = cropTypeOptions, 49 | value = index, 50 | onDismiss = { showDialog = false }, 51 | onConfirm = { 52 | 53 | val cropTypeChange = when (it) { 54 | 0 -> CropType.Dynamic 55 | else -> CropType.Static 56 | } 57 | onCropTypeChange(cropTypeChange) 58 | showDialog = false 59 | } 60 | ) 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/CustomPathEdit.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.aspectRatio 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clipToBounds 14 | import androidx.compose.ui.draw.drawWithCache 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.graphics.ImageBitmap 17 | import androidx.compose.ui.graphics.Path 18 | import com.smarttoolfactory.cropper.model.AspectRatio 19 | import com.smarttoolfactory.cropper.model.CustomPathOutline 20 | import com.smarttoolfactory.cropper.util.calculateSizeAndOffsetFromAspectRatio 21 | import com.smarttoolfactory.cropper.util.drawBlockWithCheckerAndLayer 22 | import com.smarttoolfactory.cropper.util.scaleAndTranslatePath 23 | 24 | @Composable 25 | internal fun CustomPathEdit( 26 | aspectRatio: AspectRatio, 27 | dstBitmap: ImageBitmap, 28 | customPathOutline: CustomPathOutline, 29 | onChange: (CustomPathOutline) -> Unit 30 | ) { 31 | var newTitle by remember { 32 | mutableStateOf(customPathOutline.title) 33 | } 34 | 35 | Column { 36 | 37 | Box( 38 | modifier = Modifier 39 | .fillMaxWidth() 40 | .clipToBounds() 41 | .aspectRatio(4 / 3f) 42 | .drawWithCache { 43 | 44 | val path = Path().apply { 45 | addPath(customPathOutline.path) 46 | 47 | val (newSize, offset) = calculateSizeAndOffsetFromAspectRatio( 48 | aspectRatio = aspectRatio, 49 | coefficient = 1f, 50 | size = size 51 | ) 52 | scaleAndTranslatePath(newSize.width, newSize.height) 53 | translate(offset) 54 | } 55 | 56 | onDrawWithContent { 57 | drawBlockWithCheckerAndLayer(dstBitmap) { 58 | drawPath(path, Color.Red) 59 | 60 | } 61 | } 62 | } 63 | 64 | ) 65 | 66 | CropTextField( 67 | value = newTitle, 68 | onValueChange = { 69 | newTitle = it 70 | onChange( 71 | customPathOutline.copy( 72 | title = newTitle 73 | ) 74 | ) 75 | 76 | } 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/CutCornerCropShapeEdit.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.aspectRatio 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.shape.CutCornerShape 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.derivedStateOf 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableFloatStateOf 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.clipToBounds 19 | import androidx.compose.ui.graphics.ImageBitmap 20 | import androidx.compose.ui.platform.LocalDensity 21 | import androidx.compose.ui.unit.dp 22 | import com.smarttoolfactory.cropper.model.AspectRatio 23 | import com.smarttoolfactory.cropper.model.CornerRadiusProperties 24 | import com.smarttoolfactory.cropper.model.CutCornerCropShape 25 | import com.smarttoolfactory.cropper.util.drawOutlineWithBlendModeAndChecker 26 | import kotlin.math.roundToInt 27 | 28 | @Composable 29 | internal fun CutCornerCropShapeEdit( 30 | aspectRatio: AspectRatio, 31 | dstBitmap: ImageBitmap, 32 | title: String, 33 | cutCornerCropShape: CutCornerCropShape, 34 | onChange: (CutCornerCropShape) -> Unit 35 | ) { 36 | 37 | var newTitle by remember { 38 | mutableStateOf(title) 39 | } 40 | 41 | val cornerRadius = remember { 42 | cutCornerCropShape.cornerRadius 43 | } 44 | 45 | var topStartPercent by remember { 46 | mutableFloatStateOf( 47 | cornerRadius.topStartPercent.toFloat() 48 | ) 49 | } 50 | 51 | var topEndPercent by remember { 52 | mutableFloatStateOf( 53 | cornerRadius.topEndPercent.toFloat() 54 | ) 55 | } 56 | 57 | var bottomStartPercent by remember { 58 | mutableFloatStateOf( 59 | cornerRadius.bottomStartPercent.toFloat() 60 | ) 61 | } 62 | 63 | var bottomEndPercent by remember { 64 | mutableFloatStateOf( 65 | cornerRadius.bottomEndPercent.toFloat() 66 | ) 67 | } 68 | 69 | val shape by remember { 70 | derivedStateOf { 71 | CutCornerShape( 72 | topStartPercent = topStartPercent.toInt(), 73 | topEndPercent = topEndPercent.toInt(), 74 | bottomStartPercent = bottomStartPercent.toInt(), 75 | bottomEndPercent = bottomEndPercent.toInt() 76 | ) 77 | } 78 | } 79 | 80 | onChange( 81 | cutCornerCropShape.copy( 82 | cornerRadius = CornerRadiusProperties( 83 | topStartPercent = topStartPercent.toInt(), 84 | topEndPercent = topEndPercent.toInt(), 85 | bottomStartPercent = bottomStartPercent.toInt(), 86 | bottomEndPercent = bottomEndPercent.toInt() 87 | ), 88 | title = newTitle, 89 | shape = shape 90 | ) 91 | ) 92 | 93 | Column { 94 | 95 | val density = LocalDensity.current 96 | Box( 97 | modifier = Modifier 98 | .fillMaxWidth() 99 | .aspectRatio(4 / 3f) 100 | .clipToBounds() 101 | .drawOutlineWithBlendModeAndChecker( 102 | aspectRatio, 103 | shape, 104 | density, 105 | dstBitmap 106 | ) 107 | ) 108 | 109 | CropTextField( 110 | value = newTitle, 111 | onValueChange = { newTitle = it } 112 | ) 113 | 114 | Spacer(modifier = Modifier.height(10.dp)) 115 | 116 | SliderWithValueSelection( 117 | value = topStartPercent, 118 | title = "Top Start", 119 | text = "${(topStartPercent * 10f).roundToInt() / 10f}%", 120 | onValueChange = { topStartPercent = it }, 121 | valueRange = 0f..100f 122 | ) 123 | SliderWithValueSelection( 124 | value = topEndPercent, 125 | title = "Top End", 126 | text = "${(topEndPercent * 10f).roundToInt() / 10f}%", 127 | onValueChange = { topEndPercent = it }, 128 | valueRange = 0f..100f 129 | ) 130 | SliderWithValueSelection( 131 | value = bottomStartPercent, 132 | title = "Bottom Start", 133 | text = "${(bottomStartPercent * 10f).roundToInt() / 10f}%", 134 | onValueChange = { bottomStartPercent = it }, 135 | valueRange = 0f..100f 136 | ) 137 | SliderWithValueSelection( 138 | value = bottomEndPercent, 139 | title = "Bottom End", 140 | text = "${(bottomEndPercent * 10f).roundToInt() / 10f}%", 141 | onValueChange = { bottomEndPercent = it }, 142 | valueRange = 0f..100f 143 | ) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/ImageMaskEdit.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import com.smarttoolfactory.cropper.model.ImageMaskOutline 15 | 16 | @Composable 17 | internal fun ImageMaskEdit( 18 | imageMaskOutline: ImageMaskOutline, 19 | onChange: (ImageMaskOutline) -> Unit 20 | ) { 21 | 22 | var newTitle by remember { 23 | mutableStateOf(imageMaskOutline.title) 24 | } 25 | 26 | Column { 27 | 28 | Box( 29 | modifier = Modifier.fillMaxWidth(), 30 | contentAlignment = Alignment.Center 31 | ) { 32 | Image(bitmap = imageMaskOutline.image, contentDescription = "ImageMask") 33 | 34 | } 35 | CropTextField( 36 | value = newTitle, 37 | onValueChange = { 38 | newTitle = it 39 | onChange( 40 | imageMaskOutline.copy( 41 | title = newTitle 42 | ) 43 | ) 44 | 45 | } 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/OvalCropShapeEdit.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.aspectRatio 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.shape.GenericShape 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.derivedStateOf 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableFloatStateOf 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.clipToBounds 19 | import androidx.compose.ui.geometry.Offset 20 | import androidx.compose.ui.geometry.Rect 21 | import androidx.compose.ui.geometry.Size 22 | import androidx.compose.ui.graphics.ImageBitmap 23 | import androidx.compose.ui.platform.LocalDensity 24 | import androidx.compose.ui.unit.LayoutDirection 25 | import androidx.compose.ui.unit.dp 26 | import com.smarttoolfactory.cropper.model.AspectRatio 27 | import com.smarttoolfactory.cropper.model.OvalCropShape 28 | import com.smarttoolfactory.cropper.util.drawOutlineWithBlendModeAndChecker 29 | 30 | 31 | @Composable 32 | internal fun OvalCropShapeEdit( 33 | aspectRatio: AspectRatio, 34 | dstBitmap: ImageBitmap, 35 | title: String, 36 | ovalCropShape: OvalCropShape, 37 | onChange: (OvalCropShape) -> Unit 38 | ) { 39 | 40 | var newTitle by remember { 41 | mutableStateOf(title) 42 | } 43 | 44 | val ovalProperties = remember { 45 | ovalCropShape.ovalProperties 46 | } 47 | 48 | var startAngle by remember { 49 | mutableFloatStateOf( 50 | ovalProperties.startAngle 51 | ) 52 | } 53 | 54 | var sweepAngle by remember { 55 | mutableFloatStateOf( 56 | ovalProperties.sweepAngle 57 | ) 58 | } 59 | 60 | var offsetX by remember { 61 | mutableFloatStateOf( 62 | ovalProperties.offset.x 63 | ) 64 | } 65 | 66 | var offsetY by remember { 67 | mutableFloatStateOf( 68 | ovalProperties.offset.y 69 | ) 70 | } 71 | 72 | val shape by remember(startAngle, sweepAngle) { 73 | derivedStateOf { 74 | GenericShape { size: Size, _: LayoutDirection -> 75 | val width = size.width 76 | val height = size.height 77 | val diameter = width.coerceAtMost(height) 78 | val left = (width - diameter) / 2 79 | val top = (height - diameter) / 2 80 | 81 | val rect = Rect(offset = Offset(left, top), size = Size(diameter, diameter)) 82 | 83 | if (sweepAngle == 360f) { 84 | addOval(rect) 85 | } else { 86 | moveTo(size.width / 2, size.height / 2) 87 | arcTo(rect, startAngle, sweepAngle, false) 88 | 89 | } 90 | 91 | close() 92 | } 93 | } 94 | } 95 | 96 | onChange( 97 | ovalCropShape.copy( 98 | ovalProperties = ovalProperties.copy( 99 | startAngle = startAngle, 100 | sweepAngle = sweepAngle 101 | ), 102 | title = newTitle, 103 | shape = shape 104 | ) 105 | ) 106 | 107 | Column { 108 | 109 | val density = LocalDensity.current 110 | Box( 111 | modifier = Modifier 112 | .fillMaxWidth() 113 | .aspectRatio(4 / 3f) 114 | .clipToBounds() 115 | .drawOutlineWithBlendModeAndChecker( 116 | aspectRatio, 117 | shape, 118 | density, 119 | dstBitmap 120 | ) 121 | ) 122 | 123 | CropTextField( 124 | value = newTitle, 125 | onValueChange = { newTitle = it } 126 | ) 127 | 128 | Spacer(modifier = Modifier.height(10.dp)) 129 | 130 | SliderWithValueSelection( 131 | value = startAngle, 132 | title = "Start Angle", 133 | text = "${startAngle.toInt()}°", 134 | onValueChange = { startAngle = it }, 135 | valueRange = 0f..360f 136 | ) 137 | SliderWithValueSelection( 138 | value = sweepAngle, 139 | title = "Sweep Angle", 140 | text = "${sweepAngle.toInt()}°", 141 | onValueChange = { sweepAngle = it }, 142 | valueRange = 0f..360f 143 | ) 144 | 145 | // TODO Add offset 146 | // Slider( 147 | // value = offsetX, 148 | // onValueChange = { offsetX = it }, 149 | // valueRange = 0f..100f 150 | // ) 151 | // Slider( 152 | // value = offsetY, 153 | // onValueChange = { offsetY = it }, 154 | // valueRange = 0f..100f 155 | // ) 156 | 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/PolygonCropShapeEdit.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.aspectRatio 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableFloatStateOf 12 | import androidx.compose.runtime.mutableIntStateOf 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.clipToBounds 18 | import androidx.compose.ui.graphics.ImageBitmap 19 | import androidx.compose.ui.platform.LocalDensity 20 | import androidx.compose.ui.unit.dp 21 | import com.smarttoolfactory.cropper.model.AspectRatio 22 | import com.smarttoolfactory.cropper.model.PolygonCropShape 23 | import com.smarttoolfactory.cropper.util.createPolygonShape 24 | import com.smarttoolfactory.cropper.util.drawOutlineWithBlendModeAndChecker 25 | 26 | @Composable 27 | internal fun PolygonCropShapeEdit( 28 | aspectRatio: AspectRatio, 29 | dstBitmap: ImageBitmap, 30 | title: String, 31 | polygonCropShape: PolygonCropShape, 32 | onChange: (PolygonCropShape) -> Unit 33 | ) { 34 | 35 | var newTitle by remember { 36 | mutableStateOf(title) 37 | } 38 | 39 | val polygonProperties = remember { 40 | polygonCropShape.polygonProperties 41 | } 42 | 43 | var sides by remember { 44 | mutableIntStateOf( 45 | polygonProperties.sides 46 | ) 47 | } 48 | 49 | var angle by remember { 50 | mutableFloatStateOf( 51 | polygonProperties.angle 52 | ) 53 | } 54 | 55 | var shape by remember { 56 | mutableStateOf( 57 | polygonCropShape.shape 58 | ) 59 | } 60 | 61 | onChange( 62 | polygonCropShape.copy( 63 | polygonProperties = polygonProperties.copy( 64 | sides = sides, 65 | angle = angle 66 | ), 67 | title = newTitle, 68 | shape = shape 69 | ) 70 | ) 71 | 72 | Column { 73 | 74 | val density = LocalDensity.current 75 | Box( 76 | modifier = Modifier 77 | .fillMaxWidth() 78 | .aspectRatio(4 / 3f) 79 | .clipToBounds() 80 | .drawOutlineWithBlendModeAndChecker( 81 | aspectRatio, 82 | shape, 83 | density, 84 | dstBitmap 85 | ) 86 | ) 87 | 88 | CropTextField( 89 | value = newTitle, 90 | onValueChange = { newTitle = it } 91 | ) 92 | 93 | Spacer(modifier = Modifier.height(10.dp)) 94 | 95 | SliderWithValueSelection( 96 | value = sides.toFloat(), 97 | title = "Sides", 98 | text = "$sides", 99 | onValueChange = { 100 | sides = it.toInt() 101 | shape = createPolygonShape(sides = sides, angle) 102 | }, 103 | valueRange = 3f..15f 104 | ) 105 | 106 | SliderWithValueSelection( 107 | value = angle, 108 | title = "Angle", 109 | text = "${angle.toInt()}°", 110 | onValueChange = { 111 | angle = it 112 | shape = createPolygonShape(sides = sides, degrees = angle) 113 | }, 114 | valueRange = 0f..360f 115 | ) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/PropertySelectionSheet.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.smarttoolfactory.cropper.settings.CropFrameFactory 5 | import com.smarttoolfactory.cropper.settings.CropProperties 6 | 7 | @Composable 8 | internal fun PropertySelectionSheet( 9 | cropFrameFactory: CropFrameFactory, 10 | cropProperties: CropProperties, 11 | onCropPropertiesChange: (CropProperties) -> Unit 12 | ) { 13 | BaseSheet { 14 | CropPropertySelectionMenu( 15 | cropFrameFactory = cropFrameFactory, 16 | cropProperties = cropProperties, 17 | onCropPropertiesChange = onCropPropertiesChange 18 | ) 19 | 20 | CropGestureSelectionMenu( 21 | cropProperties = cropProperties, 22 | onCropPropertiesChange = onCropPropertiesChange 23 | ) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/RoundedCornerShapeEdit.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.aspectRatio 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.derivedStateOf 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableFloatStateOf 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.clipToBounds 19 | import androidx.compose.ui.graphics.ImageBitmap 20 | import androidx.compose.ui.platform.LocalDensity 21 | import androidx.compose.ui.unit.dp 22 | import com.smarttoolfactory.cropper.model.AspectRatio 23 | import com.smarttoolfactory.cropper.model.CornerRadiusProperties 24 | import com.smarttoolfactory.cropper.model.RoundedCornerCropShape 25 | import com.smarttoolfactory.cropper.util.drawOutlineWithBlendModeAndChecker 26 | import kotlin.math.roundToInt 27 | 28 | @Composable 29 | internal fun RoundedCornerCropShapeEdit( 30 | aspectRatio: AspectRatio, 31 | dstBitmap: ImageBitmap, 32 | title: String, 33 | roundedCornerCropShape: RoundedCornerCropShape, 34 | onChange: (RoundedCornerCropShape) -> Unit 35 | ) { 36 | 37 | var newTitle by remember { 38 | mutableStateOf(title) 39 | } 40 | 41 | val cornerRadius = remember { 42 | roundedCornerCropShape.cornerRadius 43 | } 44 | 45 | var topStartPercent by remember { 46 | mutableFloatStateOf( 47 | cornerRadius.topStartPercent.toFloat() 48 | ) 49 | } 50 | 51 | var topEndPercent by remember { 52 | mutableFloatStateOf( 53 | cornerRadius.topEndPercent.toFloat() 54 | ) 55 | } 56 | 57 | var bottomStartPercent by remember { 58 | mutableFloatStateOf( 59 | cornerRadius.bottomStartPercent.toFloat() 60 | ) 61 | } 62 | 63 | var bottomEndPercent by remember { 64 | mutableFloatStateOf( 65 | cornerRadius.bottomEndPercent.toFloat() 66 | ) 67 | } 68 | 69 | val shape by remember { 70 | derivedStateOf { 71 | RoundedCornerShape( 72 | topStartPercent = topStartPercent.toInt(), 73 | topEndPercent = topEndPercent.toInt(), 74 | bottomStartPercent = bottomStartPercent.toInt(), 75 | bottomEndPercent = bottomEndPercent.toInt() 76 | ) 77 | } 78 | } 79 | 80 | onChange( 81 | roundedCornerCropShape.copy( 82 | cornerRadius = CornerRadiusProperties( 83 | topStartPercent = topStartPercent.toInt(), 84 | topEndPercent = topEndPercent.toInt(), 85 | bottomStartPercent = bottomStartPercent.toInt(), 86 | bottomEndPercent = bottomEndPercent.toInt() 87 | ), 88 | title = newTitle, 89 | shape = shape 90 | ) 91 | ) 92 | 93 | Column { 94 | 95 | val density = LocalDensity.current 96 | Box( 97 | modifier = Modifier 98 | .fillMaxWidth() 99 | .aspectRatio(4 / 3f) 100 | .clipToBounds() 101 | .drawOutlineWithBlendModeAndChecker( 102 | aspectRatio, 103 | shape, 104 | density, 105 | dstBitmap 106 | ) 107 | ) 108 | 109 | CropTextField( 110 | value = newTitle, 111 | onValueChange = { newTitle = it } 112 | ) 113 | 114 | Spacer(modifier = Modifier.height(10.dp)) 115 | 116 | SliderWithValueSelection( 117 | value = topStartPercent, 118 | title = "Top Start", 119 | text = "${(topStartPercent * 10f).roundToInt() / 10f}%", 120 | onValueChange = { topStartPercent = it }, 121 | valueRange = 0f..100f 122 | ) 123 | SliderWithValueSelection( 124 | value = topEndPercent, 125 | title = "Top End", 126 | text = "${(topEndPercent * 10f).roundToInt() / 10f}%", 127 | onValueChange = { topEndPercent = it }, 128 | valueRange = 0f..100f 129 | ) 130 | SliderWithValueSelection( 131 | value = bottomStartPercent, 132 | title = "Bottom Start", 133 | text = "${(bottomStartPercent * 10f).roundToInt() / 10f}%", 134 | onValueChange = { bottomStartPercent = it }, 135 | valueRange = 0f..100f 136 | ) 137 | SliderWithValueSelection( 138 | value = bottomEndPercent, 139 | title = "Bottom End", 140 | text = "${(bottomEndPercent * 10f).roundToInt() / 10f}%", 141 | onValueChange = { bottomEndPercent = it }, 142 | valueRange = 0f..100f 143 | ) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/cropproperties/SelectionWidgets.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.cropproperties 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.width 12 | import androidx.compose.foundation.selection.selectable 13 | import androidx.compose.foundation.selection.selectableGroup 14 | import androidx.compose.material.RadioButton 15 | import androidx.compose.material3.AlertDialog 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Switch 18 | import androidx.compose.material3.Text 19 | import androidx.compose.material3.TextButton 20 | import androidx.compose.material3.TextField 21 | import androidx.compose.material3.TextFieldDefaults 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.mutableIntStateOf 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.ui.Alignment 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.graphics.Color 28 | import androidx.compose.ui.platform.LocalDensity 29 | import androidx.compose.ui.semantics.Role 30 | import androidx.compose.ui.text.font.FontWeight 31 | import androidx.compose.ui.unit.Dp 32 | import androidx.compose.ui.unit.TextUnit 33 | import androidx.compose.ui.unit.dp 34 | import androidx.compose.ui.unit.sp 35 | import com.smarttoolfactory.slider.ColorfulSlider 36 | import com.smarttoolfactory.slider.MaterialSliderColors 37 | import com.smarttoolfactory.slider.MaterialSliderDefaults 38 | import com.smarttoolfactory.slider.SliderBrushColor 39 | 40 | @Composable 41 | internal fun DpSliderSelection( 42 | value: Dp, 43 | onValueChange: (Dp) -> Unit, 44 | lowerBound: Dp, 45 | upperBound: Dp 46 | ) { 47 | 48 | val density = LocalDensity.current 49 | val strokeWidthPx = density.run { value.toPx() } 50 | val lowerBoundPx = density.run { lowerBound.toPx() } 51 | val upperBoundPx = density.run { upperBound.toPx() } 52 | 53 | SliderSelection( 54 | value = strokeWidthPx, 55 | onValueChange = { 56 | onValueChange( 57 | density.run { it.toDp() } 58 | ) 59 | }, 60 | valueRange = lowerBoundPx..upperBoundPx 61 | ) 62 | } 63 | 64 | @Composable 65 | internal fun SliderWithValueSelection( 66 | modifier: Modifier = Modifier, 67 | value: Float, 68 | title: String = "", 69 | text: String, 70 | onValueChange: (Float) -> Unit, 71 | valueRange: ClosedFloatingPointRange, 72 | colors: MaterialSliderColors = MaterialSliderDefaults.materialColors( 73 | activeTrackColor = SliderBrushColor(MaterialTheme.colorScheme.primary), 74 | inactiveTrackColor = SliderBrushColor(Color.Transparent), 75 | thumbColor = SliderBrushColor(MaterialTheme.colorScheme.inversePrimary) 76 | ) 77 | ) { 78 | Column { 79 | 80 | Text( 81 | text = if (title.isNotEmpty()) "$title $text" else text, 82 | fontSize = 12.sp, 83 | color = MaterialTheme.colorScheme.tertiary, 84 | fontWeight = FontWeight.Bold 85 | ) 86 | 87 | Row( 88 | modifier = modifier, 89 | verticalAlignment = Alignment.CenterVertically 90 | ) { 91 | ColorfulSlider( 92 | modifier = Modifier.weight(1f), 93 | value = value, 94 | onValueChange = onValueChange, 95 | valueRange = valueRange, 96 | colors = colors, 97 | trackHeight = 10.dp, 98 | thumbRadius = 12.dp 99 | ) 100 | } 101 | } 102 | } 103 | 104 | @Composable 105 | internal fun SliderSelection( 106 | modifier: Modifier = Modifier, 107 | value: Float, 108 | valueRange: ClosedFloatingPointRange, 109 | colors: MaterialSliderColors = MaterialSliderDefaults.materialColors( 110 | activeTrackColor = SliderBrushColor(MaterialTheme.colorScheme.primary), 111 | inactiveTrackColor = SliderBrushColor(Color.Transparent) 112 | ), 113 | onValueChangeFinished: (() -> Unit)? = null, 114 | onValueChange: (Float) -> Unit, 115 | ) { 116 | ColorfulSlider( 117 | modifier = modifier, 118 | value = value, 119 | onValueChange = onValueChange, 120 | valueRange = valueRange, 121 | colors = colors, 122 | borderStroke = BorderStroke(2.dp, MaterialTheme.colorScheme.primary), 123 | trackHeight = 10.dp, 124 | thumbRadius = 12.dp, 125 | onValueChangeFinished = onValueChangeFinished 126 | ) 127 | } 128 | 129 | @Composable 130 | internal fun Title( 131 | text: String, 132 | fontSize: TextUnit = 20.sp 133 | ) { 134 | Text( 135 | modifier = Modifier.padding(vertical = 1.dp), 136 | text = text, 137 | color = MaterialTheme.colorScheme.primary, 138 | fontSize = fontSize, 139 | fontWeight = FontWeight.Bold 140 | ) 141 | } 142 | 143 | 144 | @Composable 145 | internal fun FullRowSwitch( 146 | label: String, 147 | state: Boolean, 148 | onStateChange: (Boolean) -> Unit 149 | ) { 150 | 151 | // Switch with text on right side 152 | Row(modifier = Modifier 153 | .fillMaxWidth() 154 | .clickable( 155 | interactionSource = remember { MutableInteractionSource() }, 156 | indication = null, 157 | role = Role.Switch, 158 | onClick = { 159 | onStateChange(!state) 160 | } 161 | ) 162 | .padding(8.dp), 163 | verticalAlignment = Alignment.CenterVertically 164 | ) { 165 | 166 | Text(text = label, modifier = Modifier.weight(1f)) 167 | 168 | Switch( 169 | checked = state, 170 | onCheckedChange = null 171 | ) 172 | } 173 | } 174 | 175 | @Composable 176 | internal fun CropTextField(value: String, onValueChange: (String) -> Unit) { 177 | TextField( 178 | value = value, 179 | onValueChange = onValueChange, 180 | colors = TextFieldDefaults.colors( 181 | focusedContainerColor = Color.Transparent, 182 | unfocusedContainerColor = Color.Transparent, 183 | disabledContainerColor = Color.Transparent, 184 | focusedIndicatorColor = Color.Transparent, 185 | unfocusedIndicatorColor = Color.Transparent, 186 | disabledIndicatorColor = Color.Transparent, 187 | ) 188 | ) 189 | } 190 | 191 | @Composable 192 | internal fun DialogWithMultipleSelection( 193 | title: String = "", 194 | options: List, 195 | value: Int, 196 | onDismiss: () -> Unit, 197 | onConfirm: (Int) -> Unit 198 | ) { 199 | 200 | val (selectedOption: Int, onOptionSelected: (Int) -> Unit) = remember { 201 | mutableIntStateOf(value) 202 | } 203 | 204 | AlertDialog( 205 | onDismissRequest = { onDismiss() }, 206 | title = { 207 | Text( 208 | title, 209 | fontSize = 18.sp, 210 | fontWeight = FontWeight.Bold, 211 | color = MaterialTheme.colorScheme.primary 212 | ) 213 | }, 214 | text = { 215 | 216 | // Note that Modifier.selectableGroup() 217 | // is essential to ensure correct accessibility behavior 218 | Column(Modifier.selectableGroup()) { 219 | options.forEachIndexed { index, text -> 220 | Row( 221 | Modifier 222 | .fillMaxWidth() 223 | .selectable( 224 | selected = (index == selectedOption), 225 | onClick = { onOptionSelected(index) }, 226 | role = Role.RadioButton 227 | ) 228 | .padding(horizontal = 4.dp, vertical = 8.dp), 229 | verticalAlignment = Alignment.CenterVertically 230 | ) { 231 | RadioButton( 232 | selected = (index == selectedOption), 233 | onClick = null 234 | ) 235 | Spacer(modifier = Modifier.width(8.dp)) 236 | Text( 237 | text = text, 238 | fontSize = 18.sp, 239 | ) 240 | } 241 | } 242 | } 243 | }, 244 | confirmButton = { 245 | TextButton( 246 | onClick = { 247 | onConfirm(selectedOption) 248 | } 249 | ) { 250 | Text(text = "Accept") 251 | } 252 | }, 253 | dismissButton = { 254 | TextButton( 255 | onClick = { 256 | onDismiss() 257 | } 258 | ) { 259 | Text(text = "Cancel") 260 | } 261 | } 262 | ) 263 | } 264 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/models/EditModesItem.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.models 2 | 3 | data class EditModesItem( 4 | val editMode: Int, 5 | val name: String, 6 | val selected: Boolean = false 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/models/ImageFilter.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.models 2 | 3 | import android.graphics.Bitmap 4 | import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter 5 | 6 | data class ImageFilter( 7 | val name: String = "", 8 | val filter: GPUImageFilter, 9 | val filterPreview: Bitmap 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/editmedia/repo/EditMediaRepository.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.editmedia.repo 2 | 3 | import android.graphics.Bitmap 4 | import com.armutyus.cameraxproject.ui.gallery.preview.editmedia.models.ImageFilter 5 | 6 | interface EditMediaRepository { 7 | suspend fun getImageFiltersList(image: Bitmap): List 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/models/PreviewScreenEvent.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.models 2 | 3 | import android.content.Context 4 | import java.io.File 5 | 6 | sealed class PreviewScreenEvent { 7 | 8 | data class ShareTapped(val context: Context, val file: File) : PreviewScreenEvent() 9 | data class DeleteTapped(val file: File) : PreviewScreenEvent() 10 | data class FullScreenToggleTapped(val isFullScreen: Boolean) : PreviewScreenEvent() 11 | data class ChangeBarState(val zoomState: Boolean) : PreviewScreenEvent() 12 | data class HideController(val isPlaying: Boolean) : PreviewScreenEvent() 13 | data class SaveTapped(val context: Context) : PreviewScreenEvent() 14 | object EditTapped : PreviewScreenEvent() 15 | object CancelEditTapped : PreviewScreenEvent() 16 | object PlayerViewTapped : PreviewScreenEvent() 17 | object NavigateBack : PreviewScreenEvent() 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/models/PreviewScreenState.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.models 2 | 3 | import com.armutyus.cameraxproject.util.Util.Companion.FILTER_NAME 4 | 5 | data class PreviewScreenState( 6 | val isFullScreen: Boolean = false, 7 | val isInEditMode: Boolean = false, 8 | val switchEditMode: String = FILTER_NAME, 9 | val showBars: Boolean = false, 10 | val showMediaController: Boolean = false 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/videoplayback/CustomMediaController.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.videoplayback 2 | 3 | import androidx.compose.animation.* 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.unit.dp 12 | import androidx.media3.common.Player.STATE_ENDED 13 | import com.armutyus.cameraxproject.util.* 14 | 15 | @OptIn(ExperimentalAnimationApi::class) 16 | @Composable 17 | fun CustomMediaController( 18 | modifier: Modifier = Modifier, 19 | isVisible: Boolean, 20 | isPlaying: Boolean, 21 | videoTimer: Float, 22 | bufferedPercentage: Int, 23 | playbackState: Int, 24 | totalDuration: Long, 25 | isFullScreen: Boolean, 26 | onPauseToggle: () -> Unit, 27 | onReplay: () -> Unit, 28 | onForward: () -> Unit, 29 | onSeekChanged: (newValue: Float) -> Unit, 30 | onFullScreenToggle: (isFullScreen: Boolean) -> Unit 31 | ) { 32 | 33 | val visible = remember(isVisible) { isVisible } 34 | 35 | val playing = remember(isPlaying) { isPlaying } 36 | 37 | val duration = remember(totalDuration) { totalDuration.coerceAtLeast(0) } 38 | 39 | val timer = remember(videoTimer) { videoTimer } 40 | 41 | val buffer = remember(bufferedPercentage) { bufferedPercentage } 42 | 43 | val playerState = remember(playbackState) { playbackState } 44 | 45 | AnimatedVisibility( 46 | modifier = modifier, 47 | visible = visible, 48 | enter = fadeIn(), 49 | exit = fadeOut() 50 | ) { 51 | Box( 52 | modifier = Modifier 53 | .background(MaterialTheme.colorScheme.background.copy(alpha = 0.6f)) 54 | ) { 55 | 56 | Row( 57 | modifier = Modifier 58 | .align(Alignment.Center) 59 | .fillMaxWidth(), 60 | horizontalArrangement = if (isFullScreen) { 61 | Arrangement.Center 62 | } else { 63 | Arrangement.SpaceEvenly 64 | } 65 | ) { 66 | 67 | VideoReplayIcon { 68 | onReplay() 69 | } 70 | 71 | when { 72 | playing -> { 73 | VideoPauseIcon { 74 | onPauseToggle() 75 | } 76 | } 77 | 78 | playing.not() && playerState == STATE_ENDED -> { 79 | VideoPlayIcon { 80 | onPauseToggle() 81 | } 82 | } 83 | 84 | else -> { 85 | VideoPlayIcon { 86 | onPauseToggle() 87 | } 88 | } 89 | } 90 | 91 | VideoForwardIcon { 92 | onForward() 93 | } 94 | 95 | } 96 | 97 | Column( 98 | modifier = Modifier 99 | .align(Alignment.BottomCenter) 100 | .fillMaxWidth() 101 | .padding(bottom = if (isFullScreen) 32.dp else 16.dp) 102 | .animateEnterExit( 103 | enter = slideInVertically( 104 | initialOffsetY = { fullHeight: Int -> fullHeight } 105 | ), 106 | exit = slideOutVertically( 107 | targetOffsetY = { fullHeight: Int -> fullHeight } 108 | ) 109 | ) 110 | ) { 111 | Box(modifier = Modifier.fillMaxWidth()) { 112 | Slider( 113 | value = buffer.toFloat(), 114 | enabled = false, 115 | onValueChange = { /*do nothing*/ }, 116 | valueRange = 0f..100f, 117 | colors = 118 | SliderDefaults.colors( 119 | disabledThumbColor = Color.Transparent, 120 | disabledActiveTrackColor = MaterialTheme.colorScheme.outline 121 | ) 122 | ) 123 | 124 | Slider( 125 | value = timer, 126 | onValueChange = { 127 | onSeekChanged.invoke(it) 128 | }, 129 | valueRange = 0f..duration.toFloat(), 130 | colors = SliderDefaults.colors( 131 | thumbColor = MaterialTheme.colorScheme.onBackground, 132 | activeTrackColor = MaterialTheme.colorScheme.onBackground 133 | ) 134 | ) 135 | } 136 | 137 | Row( 138 | modifier = Modifier 139 | .fillMaxWidth() 140 | .padding(top = 8.dp), 141 | horizontalArrangement = Arrangement.SpaceBetween 142 | ) { 143 | Text( 144 | modifier = Modifier 145 | .padding(start = 16.dp) 146 | .animateEnterExit( 147 | enter = slideInVertically( 148 | initialOffsetY = { fullHeight: Int -> fullHeight } 149 | ), 150 | exit = slideOutVertically( 151 | targetOffsetY = { fullHeight: Int -> fullHeight } 152 | ) 153 | ), 154 | text = duration.formatMinSec(), 155 | color = MaterialTheme.colorScheme.onBackground 156 | ) 157 | 158 | FullScreenToggleIcon( 159 | modifier = Modifier 160 | .padding(end = 16.dp) 161 | .size(24.dp) 162 | .animateEnterExit( 163 | enter = slideInVertically( 164 | initialOffsetY = { fullHeight: Int -> fullHeight } 165 | ), 166 | exit = slideOutVertically( 167 | targetOffsetY = { fullHeight: Int -> fullHeight } 168 | ) 169 | ), 170 | isFullScreen = isFullScreen 171 | ) { 172 | onFullScreenToggle(isFullScreen.not()) 173 | } 174 | } 175 | } 176 | } 177 | } 178 | 179 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/videoplayback/CustomPlayerView.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.videoplayback 2 | 3 | import android.net.Uri 4 | import android.view.ViewGroup 5 | import android.widget.FrameLayout 6 | import androidx.activity.compose.BackHandler 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.DisposableEffect 14 | import androidx.compose.runtime.LaunchedEffect 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableFloatStateOf 17 | import androidx.compose.runtime.mutableIntStateOf 18 | import androidx.compose.runtime.mutableLongStateOf 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.runtime.saveable.rememberSaveable 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.platform.LocalLifecycleOwner 25 | import androidx.compose.ui.viewinterop.AndroidView 26 | import androidx.lifecycle.Lifecycle 27 | import androidx.lifecycle.LifecycleEventObserver 28 | import androidx.media3.common.Player 29 | import androidx.media3.common.Player.STATE_ENDED 30 | import androidx.media3.exoplayer.ExoPlayer 31 | import androidx.media3.ui.PlayerView 32 | import com.armutyus.cameraxproject.util.Util.Companion.VIDEO_CONTROLS_VISIBILITY 33 | import kotlinx.coroutines.delay 34 | import kotlinx.coroutines.isActive 35 | 36 | @Composable 37 | fun CustomPlayerView( 38 | filePath: Uri?, 39 | modifier: Modifier = Modifier, 40 | videoPlayer: ExoPlayer, 41 | isFullScreen: Boolean, 42 | shouldShowController: Boolean, 43 | onFullScreenToggle: (isFullScreen: Boolean) -> Unit, 44 | hideController: (isPlaying: Boolean) -> Unit, 45 | onPlayerClick: () -> Unit, 46 | navigateBack: (() -> Unit)? = null 47 | ) { 48 | var isPlaying by rememberSaveable(filePath) { mutableStateOf(videoPlayer.isPlaying) } 49 | var playbackState by rememberSaveable(filePath) { mutableIntStateOf(videoPlayer.playbackState) } 50 | var videoTimer by rememberSaveable(filePath) { mutableFloatStateOf(0f) } 51 | var totalDuration by rememberSaveable(filePath) { mutableLongStateOf(0L) } 52 | var bufferedPercentage by rememberSaveable(filePath) { mutableIntStateOf(0) } 53 | 54 | BackHandler { 55 | if (isFullScreen) { 56 | onFullScreenToggle.invoke(true) 57 | } else { 58 | navigateBack?.invoke() 59 | } 60 | } 61 | 62 | Box(modifier = modifier) { 63 | DisposableEffect(videoPlayer) { 64 | val listener = object : Player.Listener { 65 | override fun onEvents(player: Player, events: Player.Events) { 66 | super.onEvents(player, events) 67 | isPlaying = player.isPlaying 68 | totalDuration = player.duration 69 | videoTimer = player.contentPosition.toFloat() 70 | bufferedPercentage = player.bufferedPercentage 71 | playbackState = player.playbackState 72 | } 73 | } 74 | 75 | videoPlayer.addListener(listener) 76 | 77 | onDispose { 78 | videoPlayer.removeListener(listener) 79 | } 80 | } 81 | 82 | LaunchedEffect(true) { 83 | while (isActive) { 84 | videoTimer = videoPlayer.contentPosition.toFloat() 85 | delay(50) 86 | } 87 | } 88 | 89 | LaunchedEffect(shouldShowController) { 90 | delay(VIDEO_CONTROLS_VISIBILITY) 91 | if (shouldShowController) hideController(true) 92 | } 93 | 94 | VideoPlayer( 95 | filePath = filePath, 96 | modifier = Modifier.fillMaxSize(), 97 | videoPlayer = videoPlayer 98 | ) { 99 | onPlayerClick() 100 | } 101 | 102 | CustomMediaController( 103 | modifier = Modifier.fillMaxSize(), 104 | isVisible = shouldShowController, 105 | isPlaying = isPlaying, 106 | playbackState = playbackState, 107 | totalDuration = totalDuration, 108 | bufferedPercentage = bufferedPercentage, 109 | isFullScreen = isFullScreen, 110 | onReplay = { videoPlayer.seekBack() }, 111 | onForward = { videoPlayer.seekForward() }, 112 | onPauseToggle = { 113 | when { 114 | videoPlayer.isPlaying -> { 115 | videoPlayer.pause() 116 | } 117 | 118 | videoPlayer.isPlaying.not() && playbackState == STATE_ENDED -> { 119 | videoPlayer.seekTo(0, 0) 120 | videoPlayer.playWhenReady = true 121 | } 122 | 123 | else -> { 124 | videoPlayer.play() 125 | } 126 | } 127 | isPlaying = isPlaying.not() 128 | }, 129 | onSeekChanged = { position -> videoPlayer.seekTo(position.toLong()) }, 130 | videoTimer = videoTimer, 131 | onFullScreenToggle = onFullScreenToggle 132 | ) 133 | } 134 | } 135 | 136 | @Composable 137 | private fun VideoPlayer( 138 | filePath: Uri?, 139 | modifier: Modifier = Modifier, 140 | videoPlayer: ExoPlayer, 141 | onPlayerClick: () -> Unit 142 | ) { 143 | var lifecycle by remember { 144 | mutableStateOf(Lifecycle.Event.ON_CREATE) 145 | } 146 | val lifecycleOwner = LocalLifecycleOwner.current 147 | 148 | DisposableEffect(lifecycleOwner) { 149 | val observer = LifecycleEventObserver { _, event -> 150 | lifecycle = event 151 | } 152 | lifecycleOwner.lifecycle.addObserver(observer) 153 | 154 | onDispose { 155 | lifecycleOwner.lifecycle.removeObserver(observer) 156 | } 157 | } 158 | 159 | Box( 160 | modifier = modifier 161 | .background(MaterialTheme.colorScheme.background) 162 | .clickable { onPlayerClick() } 163 | ) { 164 | DisposableEffect( 165 | AndroidView( 166 | modifier = modifier, 167 | factory = { 168 | PlayerView(it).apply { 169 | player = videoPlayer 170 | useController = false 171 | layoutParams = FrameLayout.LayoutParams( 172 | ViewGroup.LayoutParams.MATCH_PARENT, 173 | ViewGroup.LayoutParams.MATCH_PARENT 174 | ) 175 | player?.setMediaItem(androidx.media3.common.MediaItem.fromUri(filePath!!)) 176 | player?.prepare() 177 | 178 | when (lifecycle) { 179 | Lifecycle.Event.ON_PAUSE -> { 180 | onPause() 181 | player?.pause() 182 | } 183 | 184 | Lifecycle.Event.ON_RESUME -> { 185 | onResume() 186 | } 187 | 188 | else -> Unit 189 | } 190 | } 191 | } 192 | ) 193 | ) { 194 | onDispose { 195 | videoPlayer.release() 196 | } 197 | } 198 | } 199 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/gallery/preview/videoplayback/VideoPlaybackContent.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.gallery.preview.videoplayback 2 | 3 | import android.net.Uri 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.LaunchedEffect 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.platform.LocalContext 8 | import androidx.media3.common.util.UnstableApi 9 | import androidx.media3.exoplayer.ExoPlayer 10 | import com.armutyus.cameraxproject.util.Util 11 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 12 | 13 | 14 | @UnstableApi 15 | @Composable 16 | fun VideoPlaybackContent( 17 | filePath: Uri?, 18 | isFullScreen: Boolean, 19 | shouldShowController: Boolean, 20 | onFullScreenToggle: (isFullScreen: Boolean) -> Unit, 21 | hideController: (isPlaying: Boolean) -> Unit, 22 | onPlayerClick: () -> Unit, 23 | navigateBack: () -> Unit, 24 | ) { 25 | val systemUiController = rememberSystemUiController() 26 | LaunchedEffect(isFullScreen) { 27 | systemUiController.isSystemBarsVisible = !isFullScreen 28 | } 29 | val context = LocalContext.current 30 | val exoPlayer = remember(filePath) { 31 | ExoPlayer.Builder(context) 32 | .setSeekBackIncrementMs(Util.VIDEO_REPLAY_5) 33 | .setSeekForwardIncrementMs(Util.VIDEO_FORWARD_5) 34 | .build() 35 | } 36 | 37 | CustomPlayerView( 38 | filePath = filePath, 39 | videoPlayer = exoPlayer, 40 | isFullScreen = isFullScreen, 41 | shouldShowController = shouldShowController, 42 | onFullScreenToggle = onFullScreenToggle, 43 | hideController = hideController, 44 | onPlayerClick = onPlayerClick, 45 | navigateBack = navigateBack, 46 | ) 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/photo/PhotoViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.photo 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.util.Log 6 | import android.widget.Toast 7 | import androidx.camera.core.CameraInfo 8 | import androidx.camera.core.CameraSelector 9 | import androidx.camera.core.ImageCapture 10 | import androidx.core.net.toUri 11 | import androidx.lifecycle.LiveData 12 | import androidx.lifecycle.MutableLiveData 13 | import androidx.lifecycle.viewModelScope 14 | import androidx.navigation.NavController 15 | import com.armutyus.cameraxproject.R 16 | import com.armutyus.cameraxproject.ui.photo.models.PhotoEvent 17 | import com.armutyus.cameraxproject.ui.photo.models.PhotoState 18 | import com.armutyus.cameraxproject.util.BaseViewModel 19 | import com.armutyus.cameraxproject.util.FileManager 20 | import com.armutyus.cameraxproject.util.Util.Companion.CAPTURE_FAIL 21 | import com.armutyus.cameraxproject.util.Util.Companion.PHOTO_CONTENT 22 | import com.armutyus.cameraxproject.util.Util.Companion.PHOTO_DIR 23 | import com.armutyus.cameraxproject.util.Util.Companion.PHOTO_EXTENSION 24 | import com.armutyus.cameraxproject.util.Util.Companion.TAG 25 | import com.armutyus.cameraxproject.util.Util.Companion.TIMER_10S 26 | import com.armutyus.cameraxproject.util.Util.Companion.TIMER_3S 27 | import com.armutyus.cameraxproject.util.Util.Companion.TIMER_OFF 28 | import com.armutyus.cameraxproject.util.Util.Companion.VIDEO_ROUTE 29 | import kotlinx.coroutines.delay 30 | import kotlinx.coroutines.launch 31 | 32 | class PhotoViewModel constructor( 33 | private val fileManager: FileManager, 34 | navController: NavController 35 | ) : BaseViewModel(navController) { 36 | 37 | private val _photoState: MutableLiveData = MutableLiveData(PhotoState()) 38 | val photoState: LiveData = _photoState 39 | 40 | fun onEvent(photoEvent: PhotoEvent) { 41 | when (photoEvent) { 42 | PhotoEvent.DelayTimerTapped -> onDelayTimerTapped() 43 | PhotoEvent.FlashTapped -> onFlashTapped() 44 | PhotoEvent.FlipTapped -> onFlipTapped() 45 | 46 | is PhotoEvent.EditIconTapped -> onEditIconTapped(photoEvent.context) 47 | is PhotoEvent.ThumbnailTapped -> onThumbnailTapped(photoEvent.uri) 48 | is PhotoEvent.CaptureTapped -> onCaptureTapped( 49 | photoEvent.timeMillis, 50 | photoEvent.photoCaptureManager 51 | ) 52 | 53 | is PhotoEvent.CameraInitialized -> onCameraInitialized(photoEvent.cameraLensInfo) 54 | is PhotoEvent.ImageCaptured -> onImageCaptured(photoEvent.imageResult.savedUri) 55 | is PhotoEvent.SwitchToVideo -> switchCameraMode() 56 | } 57 | } 58 | 59 | private fun onCaptureTapped(timeMillis: Long, photoCaptureManager: PhotoCaptureManager) = 60 | viewModelScope.launch { 61 | delay(timeMillis) 62 | try { 63 | val filePath = fileManager.createFile(PHOTO_DIR, PHOTO_EXTENSION) 64 | photoCaptureManager.takePhoto( 65 | filePath, _photoState.value!!.lens 66 | ?: CameraSelector.LENS_FACING_BACK 67 | ) 68 | } catch (exception: IllegalArgumentException) { 69 | Log.e(TAG, exception.localizedMessage ?: CAPTURE_FAIL) 70 | } 71 | } 72 | 73 | private fun onDelayTimerTapped() = viewModelScope.launch { 74 | _photoState.value = when (_photoState.value!!.delayTimer) { 75 | TIMER_OFF -> _photoState.value!!.copy(delayTimer = TIMER_3S) 76 | TIMER_3S -> _photoState.value!!.copy(delayTimer = TIMER_10S) 77 | TIMER_10S -> _photoState.value!!.copy(delayTimer = TIMER_OFF) 78 | else -> _photoState.value!!.copy(delayTimer = TIMER_OFF) 79 | } 80 | } 81 | 82 | private fun onEditIconTapped(context: Context) = viewModelScope.launch { 83 | Toast.makeText(context, R.string.feature_not_available, Toast.LENGTH_SHORT).show() 84 | } 85 | 86 | private fun onFlashTapped() = viewModelScope.launch { 87 | _photoState.value = when (_photoState.value!!.flashMode) { 88 | ImageCapture.FLASH_MODE_OFF -> _photoState.value!!.copy(flashMode = ImageCapture.FLASH_MODE_AUTO) 89 | ImageCapture.FLASH_MODE_AUTO -> _photoState.value!!.copy(flashMode = ImageCapture.FLASH_MODE_ON) 90 | ImageCapture.FLASH_MODE_ON -> _photoState.value!!.copy(flashMode = ImageCapture.FLASH_MODE_OFF) 91 | else -> _photoState.value!!.copy(flashMode = ImageCapture.FLASH_MODE_OFF) 92 | } 93 | } 94 | 95 | private fun onFlipTapped() = viewModelScope.launch { 96 | val lens = if (_photoState.value!!.lens == CameraSelector.LENS_FACING_FRONT) { 97 | CameraSelector.LENS_FACING_BACK 98 | } else { 99 | CameraSelector.LENS_FACING_FRONT 100 | } 101 | //Check if the lens has flash unit 102 | val flashMode = if (_photoState.value!!.lensInfo[lens]?.hasFlashUnit() == true) { 103 | _photoState.value!!.flashMode 104 | } else { 105 | ImageCapture.FLASH_MODE_OFF 106 | } 107 | if (_photoState.value!!.lensInfo[lens] != null) { 108 | _photoState.value = _photoState.value!!.copy(lens = lens, flashMode = flashMode) 109 | } 110 | } 111 | 112 | private fun switchCameraMode() = viewModelScope.launch { 113 | navigateTo(VIDEO_ROUTE) 114 | } 115 | 116 | private fun onThumbnailTapped(uri: Uri?) = viewModelScope.launch { 117 | navigateTo("preview_screen/?filePath=${uri?.toString()}/?contentFilter=${PHOTO_CONTENT}") 118 | } 119 | 120 | private fun onImageCaptured(uri: Uri?) = viewModelScope.launch { 121 | if (uri != null && uri.path != null) { 122 | _photoState.value = _photoState.value!!.copy(latestImageUri = uri) 123 | } else { 124 | val mediaDir = fileManager.getPrivateFileDirectory(PHOTO_DIR) 125 | val latestImageUri = mediaDir?.listFiles()?.lastOrNull()?.toUri() ?: Uri.EMPTY 126 | _photoState.value = _photoState.value!!.copy(latestImageUri = latestImageUri) 127 | } 128 | } 129 | 130 | private fun onCameraInitialized(cameraLensInfo: HashMap) = 131 | viewModelScope.launch { 132 | if (cameraLensInfo.isNotEmpty()) { 133 | val defaultLens = if (cameraLensInfo[CameraSelector.LENS_FACING_BACK] != null) { 134 | CameraSelector.LENS_FACING_BACK 135 | } else if (cameraLensInfo[CameraSelector.LENS_FACING_BACK] != null) { 136 | CameraSelector.LENS_FACING_FRONT 137 | } else { 138 | null 139 | } 140 | _photoState.value = _photoState.value!! 141 | .copy(lens = _photoState.value!!.lens ?: defaultLens, lensInfo = cameraLensInfo) 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/photo/models/CameraModesItem.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.photo.models 2 | 3 | data class CameraModesItem( 4 | val cameraMode: Int, 5 | val name: String, 6 | val selected: Boolean = false 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/photo/models/PhotoEvent.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.photo.models 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import androidx.camera.core.CameraInfo 6 | import androidx.camera.core.ImageCapture 7 | import com.armutyus.cameraxproject.ui.photo.PhotoCaptureManager 8 | 9 | sealed class PhotoEvent { 10 | data class CameraInitialized(val cameraLensInfo: HashMap) : PhotoEvent() 11 | data class ImageCaptured(val imageResult: ImageCapture.OutputFileResults) : PhotoEvent() 12 | data class CaptureTapped( 13 | val timeMillis: Long = 0L, 14 | val photoCaptureManager: PhotoCaptureManager 15 | ) : PhotoEvent() 16 | 17 | data class EditIconTapped(val context: Context) : PhotoEvent() 18 | 19 | data class ThumbnailTapped(val uri: Uri) : PhotoEvent() 20 | 21 | object SwitchToVideo : PhotoEvent() 22 | object DelayTimerTapped : PhotoEvent() 23 | object FlashTapped : PhotoEvent() 24 | object FlipTapped : PhotoEvent() 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/photo/models/PhotoState.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.photo.models 2 | 3 | import android.net.Uri 4 | import androidx.camera.core.CameraInfo 5 | import androidx.camera.core.ImageCapture 6 | import com.armutyus.cameraxproject.util.Util 7 | 8 | /** 9 | * Defines the current UI state of the camera during pre-capture. 10 | * The state encapsulates the available camera extensions, the available camera lenses to toggle, 11 | * the current camera lens, the current extension mode, and the state of the camera. 12 | */ 13 | data class PhotoState( 14 | val cameraState: CameraState = CameraState.NOT_READY, 15 | val captureWithDelay: Int = 0, 16 | val delayTimer: Int = Util.TIMER_OFF, 17 | @ImageCapture.FlashMode val flashMode: Int = ImageCapture.FLASH_MODE_OFF, 18 | val latestImageUri: Uri? = null, 19 | val lens: Int? = null, 20 | val lensInfo: MutableMap = mutableMapOf() 21 | ) 22 | 23 | /** 24 | * Defines the current state of the camera. 25 | */ 26 | enum class CameraState { 27 | /** 28 | * Camera hasn't been initialized. 29 | */ 30 | NOT_READY, 31 | 32 | /** 33 | * Camera is open and presenting a preview stream. 34 | */ 35 | READY, 36 | 37 | /** 38 | * Some values changed on camera state. 39 | */ 40 | CHANGED 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/photo/models/PreviewPhotoState.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.photo.models 2 | 3 | import androidx.camera.core.CameraSelector 4 | import androidx.camera.core.ImageCapture 5 | import androidx.camera.core.TorchState 6 | 7 | data class PreviewPhotoState( 8 | val cameraState: CameraState = CameraState.NOT_READY, 9 | @ImageCapture.FlashMode val flashMode: Int = ImageCapture.FLASH_MODE_OFF, 10 | @TorchState.State val torchState: Int = TorchState.OFF, 11 | val cameraLens: Int = CameraSelector.LENS_FACING_BACK 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val md_theme_light_primary = Color(0xFFB32631) 6 | val md_theme_light_onPrimary = Color(0xFFFFFFFF) 7 | val md_theme_light_primaryContainer = Color(0xFFFFDAD8) 8 | val md_theme_light_onPrimaryContainer = Color(0xFF410007) 9 | val md_theme_light_secondary = Color(0xFF9C4140) 10 | val md_theme_light_onSecondary = Color(0xFFFFFFFF) 11 | val md_theme_light_secondaryContainer = Color(0xFFFFDAD7) 12 | val md_theme_light_onSecondaryContainer = Color(0xFF410005) 13 | val md_theme_light_tertiary = Color(0xFF9B4428) 14 | val md_theme_light_onTertiary = Color(0xFFFFFFFF) 15 | val md_theme_light_tertiaryContainer = Color(0xFFFFDBD0) 16 | val md_theme_light_onTertiaryContainer = Color(0xFF3A0A00) 17 | val md_theme_light_error = Color(0xFFBA1A1A) 18 | val md_theme_light_errorContainer = Color(0xFFFFDAD6) 19 | val md_theme_light_onError = Color(0xFFFFFFFF) 20 | val md_theme_light_onErrorContainer = Color(0xFF410002) 21 | val md_theme_light_background = Color(0xFFFFFBFF) 22 | val md_theme_light_onBackground = Color(0xFF3E0500) 23 | val md_theme_light_surface = Color(0xFFFFFBFF) 24 | val md_theme_light_onSurface = Color(0xFF3E0500) 25 | val md_theme_light_surfaceVariant = Color(0xFFF4DDDC) 26 | val md_theme_light_onSurfaceVariant = Color(0xFF524342) 27 | val md_theme_light_outline = Color(0xFF857372) 28 | val md_theme_light_inverseOnSurface = Color(0xFFFFEDE9) 29 | val md_theme_light_inverseSurface = Color(0xFF5E1608) 30 | val md_theme_light_inversePrimary = Color(0xFFFFB3B0) 31 | val md_theme_light_shadow = Color(0xFF000000) 32 | val md_theme_light_surfaceTint = Color(0xFFB32631) 33 | 34 | val md_theme_dark_primary = Color(0xFFFFB3B0) 35 | val md_theme_dark_onPrimary = Color(0xFF680010) 36 | val md_theme_dark_primaryContainer = Color(0xFF91061D) 37 | val md_theme_dark_onPrimaryContainer = Color(0xFFFFDAD8) 38 | val md_theme_dark_secondary = Color(0xFFFFB3AF) 39 | val md_theme_dark_onSecondary = Color(0xFF5F1316) 40 | val md_theme_dark_secondaryContainer = Color(0xFF7E2A2A) 41 | val md_theme_dark_onSecondaryContainer = Color(0xFFFFDAD7) 42 | val md_theme_dark_tertiary = Color(0xFFFFB59F) 43 | val md_theme_dark_onTertiary = Color(0xFF5E1701) 44 | val md_theme_dark_tertiaryContainer = Color(0xFF7C2D14) 45 | val md_theme_dark_onTertiaryContainer = Color(0xFFFFDBD0) 46 | val md_theme_dark_error = Color(0xFFFFB4AB) 47 | val md_theme_dark_errorContainer = Color(0xFF93000A) 48 | val md_theme_dark_onError = Color(0xFF690005) 49 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 50 | val md_theme_dark_background = Color(0xFF3E0500) 51 | val md_theme_dark_onBackground = Color(0xFFFFDAD3) 52 | val md_theme_dark_surface = Color(0xFF3E0500) 53 | val md_theme_dark_onSurface = Color(0xFFFFDAD3) 54 | val md_theme_dark_surfaceVariant = Color(0xFF524342) 55 | val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C0) 56 | val md_theme_dark_outline = Color(0xFFA08C8B) 57 | val md_theme_dark_inverseOnSurface = Color(0xFF3E0500) 58 | val md_theme_dark_inverseSurface = Color(0xFFFFDAD3) 59 | val md_theme_dark_inversePrimary = Color(0xFFB32631) 60 | val md_theme_dark_shadow = Color(0xFF000000) 61 | val md_theme_dark_surfaceTint = Color(0xFFFFB3B0) 62 | 63 | 64 | val seed = Color(0xFFA91E2B) -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.theme 2 | 3 | import android.app.Activity 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.darkColorScheme 7 | import androidx.compose.material3.lightColorScheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.SideEffect 10 | import androidx.compose.ui.graphics.toArgb 11 | import androidx.compose.ui.platform.LocalView 12 | import androidx.core.view.ViewCompat 13 | 14 | private val LightColorScheme = lightColorScheme( 15 | primary = md_theme_light_primary, 16 | onPrimary = md_theme_light_onPrimary, 17 | primaryContainer = md_theme_light_primaryContainer, 18 | onPrimaryContainer = md_theme_light_onPrimaryContainer, 19 | secondary = md_theme_light_secondary, 20 | onSecondary = md_theme_light_onSecondary, 21 | secondaryContainer = md_theme_light_secondaryContainer, 22 | onSecondaryContainer = md_theme_light_onSecondaryContainer, 23 | tertiary = md_theme_light_tertiary, 24 | onTertiary = md_theme_light_onTertiary, 25 | tertiaryContainer = md_theme_light_tertiaryContainer, 26 | onTertiaryContainer = md_theme_light_onTertiaryContainer, 27 | error = md_theme_light_error, 28 | errorContainer = md_theme_light_errorContainer, 29 | onError = md_theme_light_onError, 30 | onErrorContainer = md_theme_light_onErrorContainer, 31 | background = md_theme_light_background, 32 | onBackground = md_theme_light_onBackground, 33 | surface = md_theme_light_surface, 34 | onSurface = md_theme_light_onSurface, 35 | surfaceVariant = md_theme_light_surfaceVariant, 36 | onSurfaceVariant = md_theme_light_onSurfaceVariant, 37 | outline = md_theme_light_outline, 38 | inverseOnSurface = md_theme_light_inverseOnSurface, 39 | inverseSurface = md_theme_light_inverseSurface, 40 | inversePrimary = md_theme_light_inversePrimary, 41 | surfaceTint = md_theme_light_surfaceTint, 42 | ) 43 | 44 | 45 | private val DarkColorScheme = darkColorScheme( 46 | primary = md_theme_dark_primary, 47 | onPrimary = md_theme_dark_onPrimary, 48 | primaryContainer = md_theme_dark_primaryContainer, 49 | onPrimaryContainer = md_theme_dark_onPrimaryContainer, 50 | secondary = md_theme_dark_secondary, 51 | onSecondary = md_theme_dark_onSecondary, 52 | secondaryContainer = md_theme_dark_secondaryContainer, 53 | onSecondaryContainer = md_theme_dark_onSecondaryContainer, 54 | tertiary = md_theme_dark_tertiary, 55 | onTertiary = md_theme_dark_onTertiary, 56 | tertiaryContainer = md_theme_dark_tertiaryContainer, 57 | onTertiaryContainer = md_theme_dark_onTertiaryContainer, 58 | error = md_theme_dark_error, 59 | errorContainer = md_theme_dark_errorContainer, 60 | onError = md_theme_dark_onError, 61 | onErrorContainer = md_theme_dark_onErrorContainer, 62 | background = md_theme_dark_background, 63 | onBackground = md_theme_dark_onBackground, 64 | surface = md_theme_dark_surface, 65 | onSurface = md_theme_dark_onSurface, 66 | surfaceVariant = md_theme_dark_surfaceVariant, 67 | onSurfaceVariant = md_theme_dark_onSurfaceVariant, 68 | outline = md_theme_dark_outline, 69 | inverseOnSurface = md_theme_dark_inverseOnSurface, 70 | inverseSurface = md_theme_dark_inverseSurface, 71 | inversePrimary = md_theme_dark_inversePrimary, 72 | surfaceTint = md_theme_dark_surfaceTint, 73 | ) 74 | 75 | @Composable 76 | fun CameraXProjectTheme( 77 | darkTheme: Boolean = isSystemInDarkTheme(), 78 | // Dynamic color is available on Android 12+ 79 | dynamicColor: Boolean = true, 80 | content: @Composable () -> Unit 81 | ) { 82 | val colorScheme = when { 83 | /*dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 84 | val context = LocalContext.current 85 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 86 | }*/ 87 | darkTheme -> DarkColorScheme 88 | else -> LightColorScheme 89 | } 90 | val view = LocalView.current 91 | if (!view.isInEditMode) { 92 | SideEffect { 93 | (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() 94 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme 95 | } 96 | } 97 | 98 | MaterialTheme( 99 | colorScheme = colorScheme, 100 | typography = Typography, 101 | content = content 102 | ) 103 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/video/VideoViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.video 2 | 3 | import android.net.Uri 4 | import android.util.Log 5 | import androidx.camera.core.CameraInfo 6 | import androidx.camera.core.CameraSelector 7 | import androidx.camera.core.ImageCapture 8 | import androidx.camera.core.TorchState 9 | import androidx.camera.video.Quality 10 | import androidx.core.net.toUri 11 | import androidx.lifecycle.LiveData 12 | import androidx.lifecycle.MutableLiveData 13 | import androidx.lifecycle.viewModelScope 14 | import androidx.navigation.NavController 15 | import com.armutyus.cameraxproject.ui.photo.models.CameraState 16 | import com.armutyus.cameraxproject.ui.video.models.RecordingStatus 17 | import com.armutyus.cameraxproject.ui.video.models.VideoEvent 18 | import com.armutyus.cameraxproject.ui.video.models.VideoState 19 | import com.armutyus.cameraxproject.util.BaseViewModel 20 | import com.armutyus.cameraxproject.util.FileManager 21 | import com.armutyus.cameraxproject.util.Util 22 | import com.armutyus.cameraxproject.util.Util.Companion.CAPTURE_FAIL 23 | import com.armutyus.cameraxproject.util.Util.Companion.PHOTO_ROUTE 24 | import com.armutyus.cameraxproject.util.Util.Companion.TAG 25 | import com.armutyus.cameraxproject.util.Util.Companion.VIDEO_CONTENT 26 | import com.armutyus.cameraxproject.util.Util.Companion.VIDEO_DIR 27 | import com.armutyus.cameraxproject.util.Util.Companion.VIDEO_EXTENSION 28 | import kotlinx.coroutines.delay 29 | import kotlinx.coroutines.launch 30 | 31 | class VideoViewModel constructor( 32 | private val fileManager: FileManager, 33 | navController: NavController 34 | ) : BaseViewModel(navController) { 35 | 36 | private val _videoState: MutableLiveData = MutableLiveData(VideoState()) 37 | val videoState: LiveData = _videoState 38 | 39 | fun onEvent(videoEvent: VideoEvent) { 40 | when (videoEvent) { 41 | VideoEvent.FlashTapped -> onFlashTapped() 42 | VideoEvent.FlipTapped -> onFlipTapped() 43 | VideoEvent.DelayTimerTapped -> onDelayTimerTapped() 44 | VideoEvent.SetVideoQuality -> onSetVideoQuality() 45 | VideoEvent.SwitchToPhoto -> switchCameraMode() 46 | is VideoEvent.ThumbnailTapped -> onThumbnailTapped(videoEvent.uri) 47 | 48 | is VideoEvent.PauseTapped -> onPauseTapped(videoEvent.videoCaptureManager) 49 | is VideoEvent.ResumeTapped -> onResumeTapped(videoEvent.videoCaptureManager) 50 | is VideoEvent.StopTapped -> onStopTapped(videoEvent.videoCaptureManager) 51 | 52 | is VideoEvent.RecordTapped -> onRecordTapped( 53 | videoEvent.timeMillis, 54 | videoEvent.videoCaptureManager 55 | ) 56 | 57 | is VideoEvent.CameraInitialized -> onCameraInitialized(videoEvent.cameraLensInfo) 58 | is VideoEvent.StateChanged -> onStateChanged(videoEvent.cameraState) 59 | is VideoEvent.OnProgress -> onProgress(videoEvent.progress) 60 | is VideoEvent.RecordingPaused -> onPaused() 61 | is VideoEvent.RecordingEnded -> onRecordingEnded(videoEvent.outputUri) 62 | VideoEvent.Error -> onError() 63 | } 64 | } 65 | 66 | private fun onFlashTapped() = viewModelScope.launch { 67 | _videoState.value = when (_videoState.value!!.torchState) { 68 | TorchState.OFF -> _videoState.value!!.copy(torchState = TorchState.ON) 69 | TorchState.ON -> _videoState.value!!.copy(torchState = TorchState.OFF) 70 | else -> _videoState.value!!.copy(torchState = TorchState.OFF) 71 | } 72 | } 73 | 74 | private fun onFlipTapped() = viewModelScope.launch { 75 | val lens = if (_videoState.value!!.lens == CameraSelector.LENS_FACING_FRONT) { 76 | CameraSelector.LENS_FACING_BACK 77 | } else { 78 | CameraSelector.LENS_FACING_FRONT 79 | } 80 | //Check if the lens has flash unit 81 | val flashMode = if (_videoState.value!!.lensInfo[lens]?.hasFlashUnit() == true) { 82 | _videoState.value!!.flashMode 83 | } else { 84 | ImageCapture.FLASH_MODE_OFF 85 | } 86 | if (_videoState.value!!.lensInfo[lens] != null) { 87 | _videoState.value = _videoState.value!!.copy( 88 | lens = lens, 89 | flashMode = flashMode, 90 | quality = Quality.HIGHEST 91 | ) 92 | } 93 | } 94 | 95 | private fun onSetVideoQuality() = viewModelScope.launch { 96 | _videoState.value = when (_videoState.value!!.quality) { 97 | Quality.HIGHEST -> _videoState.value!!.copy( 98 | quality = Quality.SD, 99 | cameraState = CameraState.CHANGED 100 | ) 101 | 102 | Quality.SD -> _videoState.value!!.copy( 103 | quality = Quality.HD, 104 | cameraState = CameraState.CHANGED 105 | ) 106 | 107 | Quality.HD -> _videoState.value!!.copy( 108 | quality = Quality.FHD, 109 | cameraState = CameraState.CHANGED 110 | ) 111 | 112 | Quality.FHD -> _videoState.value!!.copy( 113 | quality = Quality.UHD, 114 | cameraState = CameraState.CHANGED 115 | ) 116 | 117 | Quality.UHD -> _videoState.value!!.copy( 118 | quality = Quality.SD, 119 | cameraState = CameraState.CHANGED 120 | ) 121 | 122 | else -> _videoState.value!!.copy(quality = Quality.HIGHEST) 123 | } 124 | } 125 | 126 | private fun onThumbnailTapped(uri: Uri?) = viewModelScope.launch { 127 | navigateTo("preview_screen/?filePath=${uri?.toString()}/?contentFilter=${VIDEO_CONTENT}") 128 | } 129 | 130 | private fun onPauseTapped(videoCaptureManager: VideoCaptureManager) = viewModelScope.launch { 131 | videoCaptureManager.pauseRecording() 132 | } 133 | 134 | private fun onResumeTapped(videoCaptureManager: VideoCaptureManager) = viewModelScope.launch { 135 | videoCaptureManager.resumeRecording() 136 | } 137 | 138 | private fun onStopTapped(videoCaptureManager: VideoCaptureManager) = viewModelScope.launch { 139 | videoCaptureManager.stopRecording() 140 | } 141 | 142 | private fun onRecordTapped(timeMillis: Long, videoCaptureManager: VideoCaptureManager) = 143 | viewModelScope.launch { 144 | _videoState.value = _videoState.value!!.copy(cameraState = CameraState.NOT_READY) 145 | delay(timeMillis) 146 | try { 147 | val filePath = fileManager.createFile(VIDEO_DIR, VIDEO_EXTENSION) 148 | videoCaptureManager.startRecording(filePath) 149 | } catch (exception: IllegalArgumentException) { 150 | Log.e(TAG, exception.localizedMessage ?: CAPTURE_FAIL) 151 | } 152 | } 153 | 154 | private fun onRecordingEnded(uri: Uri?) = viewModelScope.launch { 155 | if (uri != null && uri.path != null) { 156 | _videoState.value = _videoState.value!!.copy( 157 | cameraState = CameraState.READY, 158 | recordingStatus = RecordingStatus.Idle, 159 | recordedLength = 0, 160 | latestVideoUri = uri 161 | ) 162 | } else { 163 | val mediaDir = fileManager.getPrivateFileDirectory(VIDEO_DIR) 164 | val latestVideoUri = mediaDir?.listFiles()?.lastOrNull()?.toUri() ?: Uri.EMPTY 165 | _videoState.value = _videoState.value!!.copy( 166 | cameraState = CameraState.READY, 167 | recordingStatus = RecordingStatus.Idle, 168 | recordedLength = 0, 169 | latestVideoUri = latestVideoUri 170 | ) 171 | } 172 | } 173 | 174 | private fun onError() = viewModelScope.launch { 175 | _videoState.value = 176 | _videoState.value!!.copy(recordedLength = 0, recordingStatus = RecordingStatus.Idle) 177 | } 178 | 179 | private fun onPaused() = viewModelScope.launch { 180 | _videoState.value = _videoState.value!!.copy(recordingStatus = RecordingStatus.Paused) 181 | } 182 | 183 | private fun onProgress(progress: Int) = viewModelScope.launch { 184 | _videoState.value = _videoState.value!!.copy( 185 | recordedLength = progress, 186 | recordingStatus = RecordingStatus.InProgress 187 | ) 188 | } 189 | 190 | private fun switchCameraMode() = viewModelScope.launch { 191 | navigateTo(PHOTO_ROUTE) 192 | } 193 | 194 | private fun onStateChanged(cameraState: CameraState) = viewModelScope.launch { 195 | _videoState.value = _videoState.value!!.copy(cameraState = cameraState) 196 | } 197 | 198 | private fun onDelayTimerTapped() = viewModelScope.launch { 199 | _videoState.value = when (_videoState.value!!.delayTimer) { 200 | Util.TIMER_OFF -> _videoState.value!!.copy(delayTimer = Util.TIMER_3S) 201 | Util.TIMER_3S -> _videoState.value!!.copy(delayTimer = Util.TIMER_10S) 202 | Util.TIMER_10S -> _videoState.value!!.copy(delayTimer = Util.TIMER_OFF) 203 | else -> _videoState.value!!.copy(delayTimer = Util.TIMER_OFF) 204 | } 205 | } 206 | 207 | private fun onCameraInitialized(cameraLensInfo: HashMap) { 208 | if (cameraLensInfo.isNotEmpty()) { 209 | val defaultLens = if (cameraLensInfo[CameraSelector.LENS_FACING_BACK] != null) { 210 | CameraSelector.LENS_FACING_BACK 211 | } else if (cameraLensInfo[CameraSelector.LENS_FACING_FRONT] != null) { 212 | CameraSelector.LENS_FACING_FRONT 213 | } else { 214 | null 215 | } 216 | _videoState.value = _videoState.value!!.copy( 217 | cameraState = CameraState.NOT_READY, 218 | lens = _videoState.value!!.lens ?: defaultLens, 219 | lensInfo = cameraLensInfo 220 | ) 221 | } 222 | } 223 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/video/models/PreviewVideoState.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.video.models 2 | 3 | import androidx.camera.core.CameraSelector 4 | import androidx.camera.core.ImageCapture 5 | import androidx.camera.core.TorchState 6 | import androidx.camera.video.Quality 7 | import com.armutyus.cameraxproject.ui.photo.models.CameraState 8 | 9 | data class PreviewVideoState( 10 | val cameraState: CameraState = CameraState.READY, 11 | @ImageCapture.FlashMode val flashMode: Int = ImageCapture.FLASH_MODE_OFF, 12 | @TorchState.State val torchState: Int = TorchState.OFF, 13 | val quality: Quality = Quality.HIGHEST, 14 | val cameraLens: Int = CameraSelector.LENS_FACING_BACK 15 | ) 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/video/models/RecordingStatus.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.video.models 2 | 3 | sealed class RecordingStatus { 4 | object Idle : RecordingStatus() 5 | object InProgress : RecordingStatus() 6 | object Paused : RecordingStatus() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/video/models/VideoEvent.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.video.models 2 | 3 | import android.net.Uri 4 | import androidx.camera.core.CameraInfo 5 | import com.armutyus.cameraxproject.ui.photo.models.CameraState 6 | import com.armutyus.cameraxproject.ui.video.VideoCaptureManager 7 | 8 | sealed class VideoEvent { 9 | data class CameraInitialized(val cameraLensInfo: HashMap) : VideoEvent() 10 | 11 | data class OnProgress(val progress: Int) : VideoEvent() 12 | object RecordingPaused : VideoEvent() 13 | data class RecordingEnded(val outputUri: Uri) : VideoEvent() 14 | object Error : VideoEvent() 15 | object SwitchToPhoto : VideoEvent() 16 | data class StateChanged(val cameraState: CameraState) : VideoEvent() 17 | 18 | object SetVideoQuality : VideoEvent() 19 | object FlashTapped : VideoEvent() 20 | object FlipTapped : VideoEvent() 21 | data class ThumbnailTapped(val uri: Uri) : VideoEvent() 22 | object DelayTimerTapped : VideoEvent() 23 | 24 | data class RecordTapped( 25 | val timeMillis: Long = 0L, 26 | val videoCaptureManager: VideoCaptureManager 27 | ) : VideoEvent() 28 | 29 | data class PauseTapped(val videoCaptureManager: VideoCaptureManager) : VideoEvent() 30 | data class ResumeTapped(val videoCaptureManager: VideoCaptureManager) : VideoEvent() 31 | data class StopTapped(val videoCaptureManager: VideoCaptureManager) : VideoEvent() 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/ui/video/models/VideoState.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.ui.video.models 2 | 3 | import android.net.Uri 4 | import androidx.camera.core.CameraInfo 5 | import androidx.camera.core.ImageCapture 6 | import androidx.camera.core.TorchState 7 | import androidx.camera.video.Quality 8 | import com.armutyus.cameraxproject.ui.photo.models.CameraState 9 | import com.armutyus.cameraxproject.util.Util 10 | 11 | data class VideoState( 12 | val cameraState: CameraState = CameraState.READY, 13 | val lens: Int? = null, 14 | val delayTimer: Int = Util.TIMER_OFF, 15 | @TorchState.State val torchState: Int = TorchState.OFF, 16 | @ImageCapture.FlashMode val flashMode: Int = ImageCapture.FLASH_MODE_OFF, 17 | val supportedQualities: List = mutableListOf(), 18 | val quality: Quality = Quality.HIGHEST, 19 | val latestVideoUri: Uri? = null, 20 | val lensInfo: MutableMap = mutableMapOf(), 21 | val recordedLength: Int = 0, 22 | val recordingStatus: RecordingStatus = RecordingStatus.Idle 23 | ) 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/util/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.util 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.navigation.NavController 5 | 6 | open class BaseViewModel(private val navController: NavController) : ViewModel() { 7 | 8 | fun navigateTo(route: String) { 9 | navController.navigate(route) { 10 | popUpTo(navController.graph.startDestinationId) { 11 | saveState = true 12 | } 13 | launchSingleTop = true 14 | restoreState = true 15 | } 16 | } 17 | 18 | fun onNavigateBack() { 19 | navController.popBackStack() 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/util/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.util 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.ContextWrapper 6 | import android.graphics.Bitmap 7 | import android.graphics.ImageDecoder 8 | import android.net.Uri 9 | import android.os.Build 10 | import android.provider.MediaStore 11 | import android.view.HapticFeedbackConstants 12 | import android.view.View 13 | import java.util.concurrent.TimeUnit 14 | 15 | fun View.vibrate(feedbackConstant: Int) { 16 | // Either this needs to be set to true, or android:hapticFeedbackEnabled="true" needs to be set in XML 17 | isHapticFeedbackEnabled = true 18 | // Most of the constants are off by default: for example, clicking on a button doesn't cause the phone to vibrate anymore 19 | // if we still want to access this vibration, we'll have to ignore the global settings on that. 20 | performHapticFeedback(feedbackConstant, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) 21 | } 22 | 23 | fun Context.findActivity(): Activity? = when (this) { 24 | is Activity -> this 25 | is ContextWrapper -> baseContext.findActivity() 26 | else -> null 27 | } 28 | 29 | fun Long.formatMinSec(): String { 30 | return if (this == 0L) { 31 | "..." 32 | } else { 33 | String.format( 34 | "%02d : %02d", 35 | TimeUnit.MILLISECONDS.toMinutes(this), 36 | TimeUnit.MILLISECONDS.toSeconds(this) - TimeUnit.MINUTES.toSeconds( 37 | TimeUnit.MILLISECONDS.toMinutes(this) 38 | ) 39 | ) 40 | } 41 | } 42 | 43 | @Suppress("DEPRECATION") 44 | fun Uri.toBitmap(context: Context): Bitmap { 45 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 46 | val source = ImageDecoder.createSource(context.contentResolver, this) 47 | ImageDecoder.decodeBitmap(source).copy(Bitmap.Config.ARGB_8888, false) 48 | } else { 49 | MediaStore.Images.Media.getBitmap(context.contentResolver, this) 50 | .copy(Bitmap.Config.ARGB_8888, false) 51 | } 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/util/FileManager.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.util 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.net.Uri 6 | import androidx.core.net.toUri 7 | import com.armutyus.cameraxproject.util.Util.Companion.APP_NAME 8 | import com.armutyus.cameraxproject.util.Util.Companion.FILENAME 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.withContext 11 | import java.io.File 12 | import java.io.FileOutputStream 13 | import java.text.SimpleDateFormat 14 | import java.util.Locale 15 | 16 | class FileManager(private val context: Context) { 17 | 18 | fun getPrivateFileDirectory(dir: String): File? { 19 | val directory = File(context.getExternalFilesDir(APP_NAME), dir) 20 | return if (directory.exists() || directory.mkdirs()) { 21 | directory 22 | } else context.filesDir 23 | } 24 | 25 | suspend fun createFile(directory: String, ext: String): String { 26 | return withContext(Dispatchers.IO) { 27 | val timestamp = SimpleDateFormat( 28 | FILENAME, 29 | Locale.getDefault() 30 | ).format(System.currentTimeMillis()) 31 | return@withContext File( 32 | getPrivateFileDirectory(directory), 33 | "$timestamp.$ext" 34 | ).canonicalPath 35 | } 36 | } 37 | 38 | suspend fun saveEditedImageToFile(bitmap: Bitmap, directory: String, ext: String): Uri { 39 | return withContext(Dispatchers.IO) { 40 | val timestamp = SimpleDateFormat( 41 | FILENAME, 42 | Locale.getDefault() 43 | ).format(System.currentTimeMillis()) 44 | val file = File(getPrivateFileDirectory(directory), "cXc_$timestamp.$ext") 45 | val fileOutputStream = FileOutputStream(file) 46 | fileOutputStream.use { 47 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) 48 | it.close() 49 | } 50 | return@withContext file.toUri() 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/util/HelperFunctions.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.util 2 | 3 | import androidx.camera.core.AspectRatio 4 | import androidx.camera.video.Quality 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.DisposableEffect 7 | import androidx.compose.ui.platform.LocalContext 8 | import kotlin.math.abs 9 | import kotlin.math.max 10 | import kotlin.math.min 11 | 12 | fun aspectRatio(width: Int, height: Int): Int { 13 | val previewRatio = max(width, height).toDouble() / min(width, height) 14 | if (abs(previewRatio - Util.RATIO_4_3_VALUE) <= abs(previewRatio - Util.RATIO_16_9_VALUE)) { 15 | return AspectRatio.RATIO_4_3 16 | } 17 | return AspectRatio.RATIO_16_9 18 | } 19 | 20 | /** 21 | * a helper function to retrieve the aspect ratio from a QualitySelector enum. 22 | */ 23 | fun getAspectRatio(quality: Quality): Int { 24 | return when { 25 | arrayOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.HIGHEST) 26 | .contains(quality) -> AspectRatio.RATIO_16_9 27 | 28 | (quality == Quality.SD) -> AspectRatio.RATIO_4_3 29 | else -> throw UnsupportedOperationException() 30 | } 31 | } 32 | 33 | @Composable 34 | fun LockScreenOrientation(orientation: Int) { 35 | val context = LocalContext.current 36 | DisposableEffect(Unit) { 37 | val activity = context.findActivity() ?: return@DisposableEffect onDispose {} 38 | val originalOrientation = activity.requestedOrientation 39 | activity.requestedOrientation = orientation 40 | onDispose { 41 | // restore original orientation when view disappears 42 | activity.requestedOrientation = originalOrientation 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/util/Permissions.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.util 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import android.widget.Toast 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material3.AlertDialog 10 | import androidx.compose.material3.Button 11 | import androidx.compose.material3.Snackbar 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.unit.dp 19 | import com.armutyus.cameraxproject.R 20 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 21 | import com.google.accompanist.permissions.MultiplePermissionsState 22 | import com.google.accompanist.permissions.rememberMultiplePermissionsState 23 | 24 | @ExperimentalPermissionsApi 25 | @Composable 26 | fun Permissions( 27 | permissions: List = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 28 | listOf( 29 | android.Manifest.permission.CAMERA, 30 | android.Manifest.permission.READ_MEDIA_IMAGES, 31 | android.Manifest.permission.READ_MEDIA_VIDEO, 32 | android.Manifest.permission.RECORD_AUDIO 33 | ) 34 | } else { 35 | listOf( 36 | android.Manifest.permission.CAMERA, 37 | android.Manifest.permission.READ_EXTERNAL_STORAGE, 38 | android.Manifest.permission.RECORD_AUDIO 39 | ) 40 | }, 41 | permissionGrantedContent: @Composable () -> Unit = { } 42 | ) { 43 | val permissionsState = rememberMultiplePermissionsState(permissions = permissions) 44 | RequestPermissions( 45 | multiplePermissionsState = permissionsState, 46 | permissionGrantedContent = permissionGrantedContent 47 | ) 48 | } 49 | 50 | @ExperimentalPermissionsApi 51 | @Composable 52 | private fun RequestPermissions( 53 | multiplePermissionsState: MultiplePermissionsState, 54 | permissionGrantedContent: @Composable (() -> Unit) 55 | ) { 56 | val context = LocalContext.current 57 | val activity = LocalContext.current as Activity 58 | if (multiplePermissionsState.allPermissionsGranted) { 59 | // If all permissions are granted, then show screen with the feature enabled 60 | permissionGrantedContent() 61 | } else { 62 | if (multiplePermissionsState.shouldShowRationale) { 63 | Box( 64 | modifier = Modifier 65 | .fillMaxSize() 66 | .padding(8.dp), 67 | contentAlignment = Alignment.BottomCenter 68 | ) { 69 | Snackbar(modifier = Modifier.align(Alignment.BottomCenter), action = { 70 | Button(onClick = { multiplePermissionsState.launchMultiplePermissionRequest() }) { 71 | Text(text = stringResource(id = R.string.give_permissions)) 72 | } 73 | }) { 74 | Text(text = stringResource(id = R.string.permissions_required)) 75 | } 76 | } 77 | } else { 78 | AlertDialog(onDismissRequest = { /* */ }, 79 | title = { Text(text = stringResource(id = R.string.permissions)) }, 80 | text = { Text(text = stringResource(id = R.string.permissions_important)) }, 81 | confirmButton = { 82 | Button(onClick = { multiplePermissionsState.launchMultiplePermissionRequest() }) { 83 | Text(text = stringResource(id = R.string.give_permissions)) 84 | } 85 | }, 86 | dismissButton = { 87 | Button(onClick = { 88 | activity.finish() 89 | Toast.makeText(context, R.string.permissions_needed, Toast.LENGTH_SHORT) 90 | .show() 91 | } 92 | ) { 93 | Text(text = stringResource(id = R.string.deny)) 94 | } 95 | } 96 | ) 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/java/com/armutyus/cameraxproject/util/Util.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject.util 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.os.Build 6 | import android.util.DisplayMetrics 7 | import android.util.Size 8 | import android.view.WindowManager 9 | import android.view.WindowMetrics 10 | import androidx.annotation.RequiresApi 11 | 12 | class Util { 13 | companion object { 14 | const val TAG = "CameraXProject" 15 | const val APP_NAME = "cXc" 16 | const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS" 17 | const val PHOTO_DIR = "Photos" 18 | const val VIDEO_DIR = "Videos" 19 | const val EDIT_DIR = "Edited" 20 | const val PHOTO_EXTENSION = "jpg" 21 | const val VIDEO_EXTENSION = "mp4" 22 | const val RATIO_4_3_VALUE = 4.0 / 3.0 23 | const val RATIO_16_9_VALUE = 16.0 / 9.0 24 | const val CAPTURE_FAIL = "Image capture failed." 25 | const val GENERAL_ERROR_MESSAGE = "Something went wrong." 26 | const val PHOTO_MODE = 0 27 | const val VIDEO_MODE = 1 28 | const val FILTER_MODE = 2 29 | const val CROP_MODE = 3 30 | const val FILTER_NAME = "Filter" 31 | const val CROP_NAME = "Crop" 32 | const val UNKNOWN_ORIENTATION = -1 33 | const val ALL_CONTENT = "ALL" 34 | const val PHOTO_CONTENT = "PHOTOS" 35 | const val VIDEO_CONTENT = "VIDEOS" 36 | const val EDIT_CONTENT = "EDITS" 37 | 38 | const val GALLERY_ROUTE = "gallery_screen" 39 | const val PHOTO_ROUTE = "photo_screen" 40 | const val VIDEO_ROUTE = "video_screen" 41 | 42 | const val TIMER_OFF = 0 43 | const val TIMER_3S = 1 44 | const val TIMER_10S = 2 45 | const val DELAY_3S = 3000L 46 | const val DELAY_10S = 10000L 47 | 48 | const val VIDEO_CONTROLS_VISIBILITY = 3000L 49 | const val VIDEO_REPLAY_5 = 5000L 50 | const val VIDEO_FORWARD_5 = 5000L 51 | 52 | } 53 | 54 | object ScreenSizeCompat { 55 | private val api: Api = 56 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) ApiLevel30() 57 | else Api() 58 | 59 | /** 60 | * Returns screen size in pixels. 61 | */ 62 | fun getScreenSize(context: Context): Size = api.getScreenSize(context) 63 | 64 | @Suppress("DEPRECATION") 65 | private open class Api { 66 | open fun getScreenSize(context: Context): Size { 67 | val display = context.getSystemService(WindowManager::class.java).defaultDisplay 68 | val metrics = if (display != null) { 69 | DisplayMetrics().also { display.getRealMetrics(it) } 70 | } else { 71 | Resources.getSystem().displayMetrics 72 | } 73 | return Size(metrics.widthPixels, metrics.heightPixels) 74 | } 75 | } 76 | 77 | @RequiresApi(Build.VERSION_CODES.R) 78 | private class ApiLevel30 : Api() { 79 | override fun getScreenSize(context: Context): Size { 80 | val metrics: WindowMetrics = 81 | context.getSystemService(WindowManager::class.java).currentWindowMetrics 82 | return Size(metrics.bounds.width(), metrics.bounds.height()) 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi-v26/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/drawable/cloud.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/squircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/drawable/squircle.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/drawable/sun.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #B24A3B 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | cXc 3 | Give Permissions 4 | Permissions 5 | Permissions are required to use the app. 6 | These permissions are important for the app. Please grant all of them for the app to function properly. 7 | Permissions needed! 8 | Are you sure to delete the selected item? 9 | Are you sure to delete the selected items? 10 | Do you want to save your changes before exit? 11 | No item selected. 12 | Deny 13 | Open Camera 14 | Gallery 15 | Photos 16 | Videos 17 | Edits 18 | Cancel 19 | Delete 20 | Share 21 | Edit 22 | Save 23 | Save Changes 24 | SELECT 25 | Select All 26 | Photos and Videos 27 | No App Available 28 | Image saved. 29 | No changes made. 30 | This feature is currently under development. 31 | Choose some image or video to share 32 | Seems like file does not exist. 33 | Unknown ViewModel class 34 | Change flash settings 35 | Flip camera 36 | Stop recording 37 | Start recording 38 | Resume recording 39 | Pause recording 40 | Play video 41 | Pause video 42 | Replay five seconds 43 | Forward five seconds 44 | Fullscreen toggle 45 | Capture image 46 | Latest captured image 47 | Add filter 48 | Go to settings 49 | Capture delay settings 50 | Change video quality 51 | Auto 52 | Night 53 | HDR 54 | Face Retouch 55 | Bokeh 56 | Photo 57 | Video 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/test/java/com/armutyus/cameraxproject/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.armutyus.cameraxproject 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | }// Top-level build file where you can add configuration options common to all sub-projects/modules. 7 | plugins { 8 | id("com.android.application") version "8.1.2" apply false 9 | id("com.android.library") version "8.1.2" apply false 10 | id("org.jetbrains.kotlin.android") version "1.9.20" apply false 11 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.defaults.buildfeatures.buildconfig=true 25 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/cXc-/0ee6eb7598011bd273f00e981c6eff47460bec56/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Oct 06 22:32:25 TRT 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | //noinspection JcenterRepositoryObsolete 15 | jcenter() // Warning: this repository is going to shut down soon 16 | maven { url "https://jitpack.io" } 17 | } 18 | } 19 | 20 | rootProject.name = "CameraX Project" 21 | include(":app") 22 | --------------------------------------------------------------------------------