├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
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 |
--------------------------------------------------------------------------------