Repository: robertlevonyan/camerax-demo Branch: master Commit: 5792be7ffdb2 Files: 79 Total size: 155.6 KB Directory structure: gitextract_1un5hc0l/ ├── .gitignore ├── LICENSE ├── README.md └── camerax-demo/ ├── .gitignore ├── .idea/ │ ├── .gitignore │ └── codeStyles/ │ ├── Project.xml │ └── codeStyleConfig.xml ├── app/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── kotlin/ │ │ └── com/ │ │ └── robertlevonyan/ │ │ └── demo/ │ │ └── camerax/ │ │ ├── CameraXApplication.kt │ │ ├── MainActivity.kt │ │ ├── adapter/ │ │ │ ├── Media.kt │ │ │ ├── MediaAdapter.kt │ │ │ └── MediaDiffCallback.kt │ │ ├── analyzer/ │ │ │ └── LuminosityAnalyzer.kt │ │ ├── enums/ │ │ │ └── CameraTimer.kt │ │ ├── fragments/ │ │ │ ├── BaseFragment.kt │ │ │ ├── CameraFragment.kt │ │ │ ├── PreviewFragment.kt │ │ │ └── VideoFragment.kt │ │ └── utils/ │ │ ├── Extensions.kt │ │ ├── MainExecutor.kt │ │ ├── SharedPrefsManager.kt │ │ ├── SwipeGestureDetector.kt │ │ └── ThreadExecutor.kt │ └── res/ │ ├── anim/ │ │ ├── slide_in.xml │ │ ├── slide_in_pop.xml │ │ ├── slide_out.xml │ │ └── slide_out_pop.xml │ ├── drawable/ │ │ ├── bg_button_round.xml │ │ ├── bg_options.xml │ │ ├── ic_arrow_back.xml │ │ ├── ic_delete.xml │ │ ├── ic_edit.xml │ │ ├── ic_exposure.xml │ │ ├── ic_flash_auto.xml │ │ ├── ic_flash_off.xml │ │ ├── ic_flash_on.xml │ │ ├── ic_grid_off.xml │ │ ├── ic_grid_on.xml │ │ ├── ic_hdr_off.xml │ │ ├── ic_hdr_on.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_no_picture.xml │ │ ├── ic_outline_camera_enhance.xml │ │ ├── ic_outline_camera_front.xml │ │ ├── ic_outline_camera_rear.xml │ │ ├── ic_play.xml │ │ ├── ic_share.xml │ │ ├── ic_take_picture.xml │ │ ├── ic_take_video.xml │ │ ├── ic_timer_10.xml │ │ ├── ic_timer_3.xml │ │ └── ic_timer_off.xml │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── fragment_camera.xml │ │ ├── fragment_preview.xml │ │ ├── fragment_video.xml │ │ └── item_picture.xml │ ├── layout-land/ │ │ ├── fragment_camera.xml │ │ └── fragment_video.xml │ ├── menu/ │ │ └── menu_main.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── navigation/ │ │ └── nav_graph.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── xml/ │ └── provider_paths.xml ├── build.gradle.kts ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.ap_ # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # IntelliJ *.iml .idea/workspace.xml .idea/tasks.xml .idea/gradle.xml .idea/assetWizardSettings.xml .idea/dictionaries .idea/libraries .idea/caches # Keystore files # Uncomment the following line if you do not want to check your keystore files in. #*.jks # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild # Google Services (e.g. APIs or Firebase) google-services.json # Freeline freeline.py freeline/ freeline_project_description.json # fastlane fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Robert Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ #     Camera X Demo |A demo camera application created with Android Jetpack CameraX API|| |----------------------------------------------------------------------------------------------|-----------| [![API](https://img.shields.io/badge/API-21%2B-yellow.svg?style=flat-square)](https://android-arsenal.com/api?level=21) ### UI Structure and features |

1. Take photo

2. Switch camera

3. Open gallery

4. Select timer

5. Toggle grid

6. Select flashlight mode

7. Toggle HDR (if device supports)

8. Record video

||| |-----------------------------------|---------------------------------------------|---------------------------------------------------| ||

Some Screenshots

|| || | ## How to switch between photo and video ## Contact - **Email**: me@robertlevonyan.com - **Website**: https://robertlevonyan.com/ - **Medium**: https://medium.com/@RobertLevonyan - **Twitter**: https://twitter.com/@RobertLevonyan - **Facebook**: https://facebook.com/robert.levonyan - **Google Play**: https://play.google.com/store/apps/dev?id=5477562049350283357 ## Licence ``` MIT License Copyright (c) 2019 Robert Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ================================================ FILE: camerax-demo/.gitignore ================================================ *.iml .gradle /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild /gradlew /gradlew.bat /.idea/compiler.xml /.idea/gradle.xml /.idea/jarRepositories.xml /.idea/misc.xml /.idea/vcs.xml /.idea/ /.idea/codeStyles/ /.idea/.gitignore ================================================ FILE: camerax-demo/.idea/.gitignore ================================================ # Default ignored files /shelf/ /workspace.xml ================================================ FILE: camerax-demo/.idea/codeStyles/Project.xml ================================================ ================================================ FILE: camerax-demo/.idea/codeStyles/codeStyleConfig.xml ================================================ ================================================ FILE: camerax-demo/app/.gitignore ================================================ /build ================================================ FILE: camerax-demo/app/build.gradle.kts ================================================ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.android.nav.safeargs) } android { compileSdk = 35 defaultConfig { applicationId = "com.robertlevonyan.demo.camerax" minSdk = 21 targetSdk = 35 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { getByName("release") { isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" } buildFeatures { viewBinding = true } namespace = "com.robertlevonyan.demo.camerax" } dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.android) implementation(libs.material) implementation(libs.androidx.appcompat) implementation(libs.core.ktx) implementation(libs.androidx.constraintlayout) implementation(libs.androidx.camera.core) implementation(libs.androidx.camera.camera2) implementation(libs.androidx.camera.lifecycle) implementation(libs.androidx.camera.extensions) implementation(libs.androidx.camera.view) implementation(libs.androidx.fragment.ktx) implementation(libs.androidx.navigation.fragment.ktx) implementation(libs.androidx.navigation.ui.ktx) implementation(libs.lifecycle.runtime.ktx) implementation(libs.androidx.viewpager2) implementation(libs.coil) implementation(libs.coil.video) } ================================================ FILE: camerax-demo/app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: camerax-demo/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/CameraXApplication.kt ================================================ package com.robertlevonyan.demo.camerax import android.app.Application import coil.ImageLoader import coil.ImageLoaderFactory import coil.decode.VideoFrameDecoder class CameraXApplication: Application(), ImageLoaderFactory { override fun newImageLoader(): ImageLoader = ImageLoader.Builder(this) .components { add(VideoFrameDecoder.Factory()) } .build() } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/MainActivity.kt ================================================ package com.robertlevonyan.demo.camerax import android.os.Bundle import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/adapter/Media.kt ================================================ package com.robertlevonyan.demo.camerax.adapter import android.net.Uri import java.util.* data class Media( val uri: Uri, val isVideo: Boolean, val date: Long, ) ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/adapter/MediaAdapter.kt ================================================ package com.robertlevonyan.demo.camerax.adapter import android.net.Uri import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import coil.load import com.robertlevonyan.demo.camerax.R import com.robertlevonyan.demo.camerax.utils.layoutInflater /** * This is an adapter to preview taken photos or videos * */ class MediaAdapter( private val onItemClick: (Boolean, Uri) -> Unit, private val onDeleteClick: (Boolean, Uri) -> Unit, ) : ListAdapter(MediaDiffCallback()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = PicturesViewHolder(parent.context.layoutInflater.inflate(R.layout.item_picture, parent, false)) override fun onBindViewHolder(holder: PicturesViewHolder, position: Int) { holder.bind(getItem(position)) } fun shareImage(currentPage: Int, action: (Media) -> Unit) { if (currentPage < itemCount) { action(getItem(currentPage)) } } fun deleteImage(currentPage: Int) { if (currentPage < itemCount) { val media = getItem(currentPage) val allMedia = currentList.toMutableList() allMedia.removeAt(currentPage) submitList(allMedia) onDeleteClick(allMedia.size == 0, media.uri) } } inner class PicturesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val imagePreview: ImageView = itemView.findViewById(R.id.imagePreview) private val imagePlay: ImageView = itemView.findViewById(R.id.imagePlay) fun bind(item: Media) { imagePlay.visibility = if (item.isVideo) View.VISIBLE else View.GONE imagePreview.load(item.uri) imagePreview.setOnClickListener { onItemClick(item.isVideo, item.uri) } } } } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/adapter/MediaDiffCallback.kt ================================================ package com.robertlevonyan.demo.camerax.adapter import androidx.recyclerview.widget.DiffUtil class MediaDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Media, newItem: Media): Boolean = oldItem.uri == newItem.uri override fun areContentsTheSame(oldItem: Media, newItem: Media): Boolean = oldItem == newItem } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/analyzer/LuminosityAnalyzer.kt ================================================ package com.robertlevonyan.demo.camerax.analyzer import android.util.Log import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy import java.nio.ByteBuffer import java.util.concurrent.TimeUnit class LuminosityAnalyzer: ImageAnalysis.Analyzer { private var lastAnalyzedTimestamp = 0L private fun ByteBuffer.toByteArray(): ByteArray { rewind() // Rewind the buffer to zero val data = ByteArray(remaining()) get(data) // Copy the buffer into a byte array return data // Return the byte array } override fun analyze(image: ImageProxy) { val currentTimestamp = System.currentTimeMillis() // Calculate the average luma no more often than every second if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) { // Since format in ImageAnalysis is YUV, image.planes[0] // contains the Y (luminance) plane val buffer = image.planes[0].buffer // Extract image data from callback object val data = buffer.toByteArray() // Convert the data into an array of pixel values val pixels = data.map { it.toInt() and 0xFF } // Compute average luminance for the image val luma = pixels.average() // Log the new luma value Log.e("CameraXDemo", "Average luminosity: $luma") // Update timestamp of last analyzed frame lastAnalyzedTimestamp = currentTimestamp } } } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/enums/CameraTimer.kt ================================================ package com.robertlevonyan.demo.camerax.enums enum class CameraTimer { OFF, S3, S10 } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/fragments/BaseFragment.kt ================================================ package com.robertlevonyan.demo.camerax.fragments import android.Manifest import android.content.ContentUris import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Environment import android.provider.MediaStore import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding import com.google.android.material.snackbar.Snackbar import com.robertlevonyan.demo.camerax.R import com.robertlevonyan.demo.camerax.adapter.Media import java.io.File /**Parent class of all the fragments in this project*/ abstract class BaseFragment(private val fragmentLayout: Int) : Fragment() { /** * Generic ViewBinding of the subclasses * */ abstract val binding: B // The Folder location where all the files will be stored protected val outputDirectory: String by lazy { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { "${Environment.DIRECTORY_DCIM}/CameraXDemo/" } else { "${requireContext().getExternalFilesDir(Environment.DIRECTORY_DCIM)}/CameraXDemo/" } } // The permissions we need for the app to work properly private val permissions = mutableListOf( Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, // Manifest.permission.READ_EXTERNAL_STORAGE, ).apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { add(Manifest.permission.ACCESS_MEDIA_LOCATION) } } private val permissionRequest = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> if (permissions.all { it.value }) { onPermissionGranted() } else { view?.let { v -> Snackbar.make(v, R.string.message_no_permissions, Snackbar.LENGTH_INDEFINITE) .setAction(R.string.label_ok) { ActivityCompat.finishAffinity(requireActivity()) } .show() } } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { // Adding an option to handle the back press in fragment requireActivity().onBackPressedDispatcher.addCallback( viewLifecycleOwner, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { onBackPressed() } }) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (allPermissionsGranted()) { onPermissionGranted() } else { permissionRequest.launch(permissions.toTypedArray()) } } protected fun getMedia(): List = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { getMediaQPlus() } else { getMediaQMinus() }.reversed() private fun getMediaQPlus(): List { val items = mutableListOf() val contentResolver = requireContext().applicationContext.contentResolver contentResolver.query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, arrayOf( MediaStore.Video.Media._ID, MediaStore.Video.Media.RELATIVE_PATH, MediaStore.Video.Media.DATE_TAKEN, ), null, null, "${MediaStore.Video.Media.DISPLAY_NAME} ASC" )?.use { cursor -> val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID) val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.RELATIVE_PATH) val dateColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_TAKEN) while (cursor.moveToNext()) { val id = cursor.getLong(idColumn) val path = cursor.getString(pathColumn) val date = cursor.getLong(dateColumn) val contentUri: Uri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id) if (path == outputDirectory) { items.add(Media(contentUri, true, date)) } } } contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.RELATIVE_PATH, MediaStore.Images.Media.DATE_TAKEN, ), null, null, "${MediaStore.Images.Media.DISPLAY_NAME} ASC" )?.use { cursor -> val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.RELATIVE_PATH) val dateColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_TAKEN) while (cursor.moveToNext()) { val id = cursor.getLong(idColumn) val path = cursor.getString(pathColumn) val date = cursor.getLong(dateColumn) val contentUri: Uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) if (path == outputDirectory) { items.add(Media(contentUri, false, date)) } } } return items } private fun getMediaQMinus(): List { val items = mutableListOf() File(outputDirectory).listFiles()?.forEach { val authority = requireContext().applicationContext.packageName + ".provider" val mediaUri = FileProvider.getUriForFile(requireContext(), authority, it) items.add(Media(mediaUri, it.extension == "mp4", it.lastModified())) } return items } /** * Check for the permissions */ protected fun allPermissionsGranted() = permissions.all { ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED } /** * A function which will be called after the permission check * */ open fun onPermissionGranted() = Unit /** * An abstract function which will be called on the Back button press * */ abstract fun onBackPressed() } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/fragments/CameraFragment.kt ================================================ package com.robertlevonyan.demo.camerax.fragments import android.annotation.SuppressLint import android.content.ContentValues import android.content.Context import android.content.res.Configuration import android.hardware.display.DisplayManager import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler import android.os.HandlerThread import android.provider.MediaStore import android.util.DisplayMetrics import android.util.Log import android.view.GestureDetector import android.view.View import android.widget.Toast import androidx.camera.core.* import androidx.camera.core.ImageCapture.* import androidx.camera.extensions.ExtensionMode import androidx.camera.extensions.ExtensionsManager import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView import androidx.core.content.ContextCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.lifecycleScope import androidx.navigation.Navigation import coil.load import coil.request.ErrorResult import coil.request.ImageRequest import coil.transform.CircleCropTransformation import com.robertlevonyan.demo.camerax.R import com.robertlevonyan.demo.camerax.analyzer.LuminosityAnalyzer import com.robertlevonyan.demo.camerax.databinding.FragmentCameraBinding import com.robertlevonyan.demo.camerax.enums.CameraTimer import com.robertlevonyan.demo.camerax.utils.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.io.File import java.util.concurrent.ExecutionException import kotlin.math.abs import kotlin.math.max import kotlin.math.min import kotlin.properties.Delegates class CameraFragment : BaseFragment(R.layout.fragment_camera) { // An instance for display manager to get display change callbacks private val displayManager by lazy { requireContext().getSystemService(Context.DISPLAY_SERVICE) as DisplayManager } // An instance of a helper function to work with Shared Preferences private val prefs by lazy { SharedPrefsManager.newInstance(requireContext()) } private var preview: Preview? = null private var cameraProvider: ProcessCameraProvider? = null private var imageCapture: ImageCapture? = null private var imageAnalyzer: ImageAnalysis? = null // A lazy instance of the current fragment's view binding override val binding: FragmentCameraBinding by lazy { FragmentCameraBinding.inflate(layoutInflater) } private var displayId = -1 // Selector showing which camera is selected (front or back) private var lensFacing = CameraSelector.DEFAULT_BACK_CAMERA private var hdrCameraSelector: CameraSelector? = null // Selector showing which flash mode is selected (on, off or auto) private var flashMode by Delegates.observable(FLASH_MODE_OFF) { _, _, new -> binding.btnFlash.setImageResource( when (new) { FLASH_MODE_ON -> R.drawable.ic_flash_on FLASH_MODE_AUTO -> R.drawable.ic_flash_auto else -> R.drawable.ic_flash_off } ) } // Selector showing is grid enabled or not private var hasGrid = false // Selector showing is hdr enabled or not (will work, only if device's camera supports hdr on hardware level) private var hasHdr = false // Selector showing is there any selected timer and it's value (3s or 10s) private var selectedTimer = CameraTimer.OFF /** * A display listener for orientation changes that do not trigger a configuration * change, for example if we choose to override config change in manifest or for 180-degree * orientation changes. */ private val displayListener = object : DisplayManager.DisplayListener { override fun onDisplayAdded(displayId: Int) = Unit override fun onDisplayRemoved(displayId: Int) = Unit @SuppressLint("UnsafeExperimentalUsageError", "UnsafeOptInUsageError") override fun onDisplayChanged(displayId: Int) = view?.let { view -> if (displayId == this@CameraFragment.displayId) { preview?.targetRotation = view.display.rotation imageCapture?.targetRotation = view.display.rotation imageAnalyzer?.targetRotation = view.display.rotation } } ?: Unit } @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) flashMode = prefs.getInt(KEY_FLASH, FLASH_MODE_OFF) hasGrid = prefs.getBoolean(KEY_GRID, false) hasHdr = prefs.getBoolean(KEY_HDR, false) initViews() displayManager.registerDisplayListener(displayListener, null) binding.run { viewFinder.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewDetachedFromWindow(v: View) = displayManager.registerDisplayListener(displayListener, null) override fun onViewAttachedToWindow(v: View) = displayManager.unregisterDisplayListener(displayListener) }) btnTakePicture.setOnClickListener { takePicture() } btnGallery.setOnClickListener { openPreview() } btnSwitchCamera.setOnClickListener { toggleCamera() } btnTimer.setOnClickListener { selectTimer() } btnGrid.setOnClickListener { toggleGrid() } btnFlash.setOnClickListener { selectFlash() } btnHdr.setOnClickListener { toggleHdr() } btnTimerOff.setOnClickListener { closeTimerAndSelect(CameraTimer.OFF) } btnTimer3.setOnClickListener { closeTimerAndSelect(CameraTimer.S3) } btnTimer10.setOnClickListener { closeTimerAndSelect(CameraTimer.S10) } btnFlashOff.setOnClickListener { closeFlashAndSelect(FLASH_MODE_OFF) } btnFlashOn.setOnClickListener { closeFlashAndSelect(FLASH_MODE_ON) } btnFlashAuto.setOnClickListener { closeFlashAndSelect(FLASH_MODE_AUTO) } btnExposure.setOnClickListener { flExposure.visibility = View.VISIBLE } flExposure.setOnClickListener { flExposure.visibility = View.GONE } // This swipe gesture adds a fun gesture to switch between video and photo val swipeGestures = SwipeGestureDetector().apply { setSwipeCallback(right = { Navigation.findNavController(view).navigate(R.id.action_camera_to_video) }) } val gestureDetectorCompat = GestureDetector(requireContext(), swipeGestures) viewFinder.setOnTouchListener { _, motionEvent -> if (gestureDetectorCompat.onTouchEvent(motionEvent)) return@setOnTouchListener false return@setOnTouchListener true } } } /** * Create some initial states * */ private fun initViews() { binding.btnGrid.setImageResource(if (hasGrid) R.drawable.ic_grid_on else R.drawable.ic_grid_off) binding.groupGridLines.visibility = if (hasGrid) View.VISIBLE else View.GONE adjustInsets() } /** * This methods adds all necessary margins to some views based on window insets and screen orientation * */ private fun adjustInsets() { activity?.window?.fitSystemWindows() binding.btnTakePicture.onWindowInsets { view, windowInsets -> if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { view.bottomMargin = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom } else { view.endMargin = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).right } } binding.btnTimer.onWindowInsets { view, windowInsets -> view.topMargin = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).top } binding.llTimerOptions.onWindowInsets { view, windowInsets -> if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { view.topPadding = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).top } else { view.startPadding = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).left } } binding.llFlashOptions.onWindowInsets { view, windowInsets -> if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { view.topPadding = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).top } else { view.startPadding = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).left } } } /** * Change the facing of camera * toggleButton() function is an Extension function made to animate button rotation * */ @SuppressLint("RestrictedApi") fun toggleCamera() = binding.btnSwitchCamera.toggleButton( flag = lensFacing == CameraSelector.DEFAULT_BACK_CAMERA, rotationAngle = 180f, firstIcon = R.drawable.ic_outline_camera_rear, secondIcon = R.drawable.ic_outline_camera_front, ) { lensFacing = if (it) { CameraSelector.DEFAULT_BACK_CAMERA } else { CameraSelector.DEFAULT_FRONT_CAMERA } startCamera() } /** * Navigate to PreviewFragment * */ private fun openPreview() { if (getMedia().isEmpty()) return view?.let { Navigation.findNavController(it).navigate(R.id.action_camera_to_preview) } } /** * Show timer selection menu by circular reveal animation. * circularReveal() function is an Extension function which is adding the circular reveal * */ private fun selectTimer() = binding.llTimerOptions.circularReveal(binding.btnTimer) /** * This function is called from XML view via Data Binding to select a timer * possible values are OFF, S3 or S10 * circularClose() function is an Extension function which is adding circular close * */ private fun closeTimerAndSelect(timer: CameraTimer) = binding.llTimerOptions.circularClose(binding.btnTimer) { selectedTimer = timer binding.btnTimer.setImageResource( when (timer) { CameraTimer.S3 -> R.drawable.ic_timer_3 CameraTimer.S10 -> R.drawable.ic_timer_10 CameraTimer.OFF -> R.drawable.ic_timer_off } ) } /** * Show flashlight selection menu by circular reveal animation. * circularReveal() function is an Extension function which is adding the circular reveal * */ private fun selectFlash() = binding.llFlashOptions.circularReveal(binding.btnFlash) /** * This function is called from XML view via Data Binding to select a FlashMode * possible values are ON, OFF or AUTO * circularClose() function is an Extension function which is adding circular close * */ private fun closeFlashAndSelect(@FlashMode flash: Int) = binding.llFlashOptions.circularClose(binding.btnFlash) { flashMode = flash binding.btnFlash.setImageResource( when (flash) { FLASH_MODE_ON -> R.drawable.ic_flash_on FLASH_MODE_OFF -> R.drawable.ic_flash_off else -> R.drawable.ic_flash_auto } ) imageCapture?.flashMode = flashMode prefs.putInt(KEY_FLASH, flashMode) } /** * Turns on or off the grid on the screen * */ private fun toggleGrid() { binding.btnGrid.toggleButton( flag = hasGrid, rotationAngle = 180f, firstIcon = R.drawable.ic_grid_off, secondIcon = R.drawable.ic_grid_on, ) { flag -> hasGrid = flag prefs.putBoolean(KEY_GRID, flag) binding.groupGridLines.visibility = if (flag) View.VISIBLE else View.GONE } } /** * Turns on or off the HDR if available * */ private fun toggleHdr() { binding.btnHdr.toggleButton( flag = hasHdr, rotationAngle = 360f, firstIcon = R.drawable.ic_hdr_off, secondIcon = R.drawable.ic_hdr_on, ) { flag -> hasHdr = flag prefs.putBoolean(KEY_HDR, flag) startCamera() } } override fun onPermissionGranted() { // Each time apps is coming to foreground the need permission check is being processed binding.viewFinder.let { vf -> vf.post { // Setting current display ID displayId = vf.display.displayId startCamera() lifecycleScope.launch(Dispatchers.IO) { // Do on IO Dispatcher setLastPictureThumbnail() } } } } private fun setLastPictureThumbnail() = binding.btnGallery.post { getMedia().firstOrNull() // check if there are any photos or videos in the app directory ?.let { setGalleryThumbnail(it.uri) } // preview the last one ?: binding.btnGallery.setImageResource(R.drawable.ic_no_picture) // or the default placeholder } /** * Unbinds all the lifecycles from CameraX, then creates new with new parameters * */ private fun startCamera() { // This is the CameraX PreviewView where the camera will be rendered val viewFinder = binding.viewFinder val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) cameraProviderFuture.addListener({ try { cameraProvider = cameraProviderFuture.get() } catch (e: InterruptedException) { Toast.makeText(requireContext(), "Error starting camera", Toast.LENGTH_SHORT).show() return@addListener } catch (e: ExecutionException) { Toast.makeText(requireContext(), "Error starting camera", Toast.LENGTH_SHORT).show() return@addListener } // The display information val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) } // The ratio for the output image and preview val aspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels) // The display rotation val rotation = viewFinder.display.rotation val localCameraProvider = cameraProvider ?: throw IllegalStateException("Camera initialization failed.") // The Configuration of camera preview preview = Preview.Builder() .setTargetAspectRatio(aspectRatio) // set the camera aspect ratio .setTargetRotation(rotation) // set the camera rotation .build() // The Configuration of image capture imageCapture = Builder() .setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY) // setting to have pictures with highest quality possible (may be slow) .setFlashMode(flashMode) // set capture flash .setTargetAspectRatio(aspectRatio) // set the capture aspect ratio .setTargetRotation(rotation) // set the capture rotation .build() checkForHdrExtensionAvailability() // The Configuration of image analyzing imageAnalyzer = ImageAnalysis.Builder() .setTargetAspectRatio(aspectRatio) // set the analyzer aspect ratio .setTargetRotation(rotation) // set the analyzer rotation .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) // in our analysis, we care about the latest image .build() .also { setLuminosityAnalyzer(it) } // Unbind the use-cases before rebinding them localCameraProvider.unbindAll() // Bind all use cases to the camera with lifecycle bindToLifecycle(localCameraProvider, viewFinder) }, ContextCompat.getMainExecutor(requireContext())) } private fun checkForHdrExtensionAvailability() { // Create a Vendor Extension for HDR val extensionsManagerFuture = ExtensionsManager.getInstanceAsync( requireContext(), cameraProvider ?: return, ) extensionsManagerFuture.addListener( { val extensionsManager = extensionsManagerFuture.get() ?: return@addListener val cameraProvider = cameraProvider ?: return@addListener val isAvailable = extensionsManager.isExtensionAvailable(lensFacing, ExtensionMode.HDR) // check for any extension availability println("AUTO " + extensionsManager.isExtensionAvailable(lensFacing, ExtensionMode.AUTO)) println("HDR " + extensionsManager.isExtensionAvailable(lensFacing, ExtensionMode.HDR)) println("FACE RETOUCH " + extensionsManager.isExtensionAvailable(lensFacing, ExtensionMode.FACE_RETOUCH)) println("BOKEH " + extensionsManager.isExtensionAvailable(lensFacing, ExtensionMode.BOKEH)) println("NIGHT " + extensionsManager.isExtensionAvailable(lensFacing, ExtensionMode.NIGHT)) println("NONE " + extensionsManager.isExtensionAvailable(lensFacing, ExtensionMode.NONE)) // Check if the extension is available on the device if (!isAvailable) { // If not, hide the HDR button binding.btnHdr.visibility = View.GONE } else if (hasHdr) { // If yes, turn on if the HDR is turned on by the user binding.btnHdr.visibility = View.VISIBLE hdrCameraSelector = extensionsManager.getExtensionEnabledCameraSelector(lensFacing, ExtensionMode.HDR) } }, ContextCompat.getMainExecutor(requireContext()) ) } private fun setLuminosityAnalyzer(imageAnalysis: ImageAnalysis) { // Use a worker thread for image analysis to prevent glitches val analyzerThread = HandlerThread("LuminosityAnalysis").apply { start() } imageAnalysis.setAnalyzer( ThreadExecutor(Handler(analyzerThread.looper)), LuminosityAnalyzer() ) } private fun bindToLifecycle(localCameraProvider: ProcessCameraProvider, viewFinder: PreviewView) { try { localCameraProvider.bindToLifecycle( viewLifecycleOwner, // current lifecycle owner hdrCameraSelector ?: lensFacing, // either front or back facing preview, // camera preview use case imageCapture, // image capture use case imageAnalyzer, // image analyzer use case ).run { // Init camera exposure control cameraInfo.exposureState.run { val lower = exposureCompensationRange.lower val upper = exposureCompensationRange.upper binding.sliderExposure.run { valueFrom = lower.toFloat() valueTo = upper.toFloat() stepSize = 1f value = exposureCompensationIndex.toFloat() addOnChangeListener { _, value, _ -> cameraControl.setExposureCompensationIndex(value.toInt()) } } } } // Attach the viewfinder's surface provider to preview use case preview?.setSurfaceProvider(viewFinder.surfaceProvider) } catch (e: Exception) { Log.e(TAG, "Failed to bind use cases", e) } } /** * Detecting the most suitable aspect ratio for current dimensions * * @param width - preview width * @param height - preview height * @return suitable aspect ratio */ private fun aspectRatio(width: Int, height: Int): Int { val previewRatio = max(width, height).toDouble() / min(width, height) if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) { return AspectRatio.RATIO_4_3 } return AspectRatio.RATIO_16_9 } @Suppress("NON_EXHAUSTIVE_WHEN") private fun takePicture() = lifecycleScope.launch(Dispatchers.Main) { // Show a timer based on user selection when (selectedTimer) { CameraTimer.S3 -> for (i in 3 downTo 1) { binding.tvCountDown.text = i.toString() delay(1000) } CameraTimer.S10 -> for (i in 10 downTo 1) { binding.tvCountDown.text = i.toString() delay(1000) } CameraTimer.OFF -> {} } binding.tvCountDown.text = "" captureImage() } private fun captureImage() { val localImageCapture = imageCapture ?: throw IllegalStateException("Camera initialization failed.") // Setup image capture metadata val metadata = Metadata().apply { // Mirror image when using the front camera isReversedHorizontal = lensFacing == CameraSelector.DEFAULT_FRONT_CAMERA } MediaStore.Images.Media.EXTERNAL_CONTENT_URI // Options fot the output image file val outputOptions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis()) put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") put(MediaStore.MediaColumns.RELATIVE_PATH, outputDirectory) } val contentResolver = requireContext().contentResolver // Create the output uri val contentUri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) OutputFileOptions.Builder(contentResolver, contentUri, contentValues) } else { File(outputDirectory).mkdirs() val file = File(outputDirectory, "${System.currentTimeMillis()}.jpg") OutputFileOptions.Builder(file) }.setMetadata(metadata).build() localImageCapture.takePicture( outputOptions, // the options needed for the final image requireContext().mainExecutor(), // the executor, on which the task will run object : OnImageSavedCallback { // the callback, about the result of capture process override fun onImageSaved(outputFileResults: OutputFileResults) { // This function is called if capture is successfully completed outputFileResults.savedUri ?.let { uri -> setGalleryThumbnail(uri) Log.d(TAG, "Photo saved in $uri") } ?: setLastPictureThumbnail() } override fun onError(exception: ImageCaptureException) { // This function is called if there is an errors during capture process val msg = "Photo capture failed: ${exception.message}" Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() Log.e(TAG, msg) exception.printStackTrace() } } ) } private fun setGalleryThumbnail(savedUri: Uri?) = binding.btnGallery.load(savedUri) { placeholder(R.drawable.ic_no_picture) transformations(CircleCropTransformation()) listener(object : ImageRequest.Listener { override fun onError(request: ImageRequest, result: ErrorResult) { super.onError(request, result) binding.btnGallery.load(savedUri) { placeholder(R.drawable.ic_no_picture) transformations(CircleCropTransformation()) // fetcher(VideoFrameUriFetcher(requireContext())) } } }) } override fun onDestroyView() { super.onDestroyView() displayManager.unregisterDisplayListener(displayListener) } override fun onBackPressed() = when { binding.llTimerOptions.visibility == View.VISIBLE -> binding.llTimerOptions.circularClose(binding.btnTimer) binding.llFlashOptions.visibility == View.VISIBLE -> binding.llFlashOptions.circularClose(binding.btnFlash) else -> requireActivity().finish() } companion object { private const val TAG = "CameraXDemo" const val KEY_FLASH = "sPrefFlashCamera" const val KEY_GRID = "sPrefGridCamera" const val KEY_HDR = "sPrefHDR" private const val RATIO_4_3_VALUE = 4.0 / 3.0 // aspect ratio 4x3 private const val RATIO_16_9_VALUE = 16.0 / 9.0 // aspect ratio 16x9 } } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/fragments/PreviewFragment.kt ================================================ package com.robertlevonyan.demo.camerax.fragments import android.content.Intent import android.os.Bundle import android.view.View import androidx.core.view.WindowInsetsCompat import androidx.navigation.Navigation import com.robertlevonyan.demo.camerax.R import com.robertlevonyan.demo.camerax.adapter.MediaAdapter import com.robertlevonyan.demo.camerax.databinding.FragmentPreviewBinding import com.robertlevonyan.demo.camerax.utils.* class PreviewFragment : BaseFragment(R.layout.fragment_preview) { private val mediaAdapter = MediaAdapter( onItemClick = { isVideo, uri -> if (!isVideo) { val visibility = if (binding.groupPreviewActions.visibility == View.VISIBLE) View.GONE else View.VISIBLE binding.groupPreviewActions.visibility = visibility } else { val play = Intent(Intent.ACTION_VIEW, uri).apply { setDataAndType(uri, "video/mp4") } startActivity(play) } }, onDeleteClick = { isEmpty, uri -> if (isEmpty) onBackPressed() val resolver = requireContext().applicationContext.contentResolver resolver.delete(uri, null, null) }, ) private var currentPage = 0 override val binding: FragmentPreviewBinding by lazy { FragmentPreviewBinding.inflate(layoutInflater) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) adjustInsets() // Check for the permissions and show files if (allPermissionsGranted()) { binding.pagerPhotos.apply { adapter = mediaAdapter.apply { submitList(getMedia()) } onPageSelected { page -> currentPage = page } } } binding.btnBack.setOnClickListener { onBackPressed() } binding.btnShare.setOnClickListener { shareImage() } binding.btnDelete.setOnClickListener { deleteImage() } } /** * This methods adds all necessary margins to some views based on window insets and screen orientation * */ private fun adjustInsets() { activity?.window?.fitSystemWindows() binding.btnBack.onWindowInsets { view, windowInsets -> view.topMargin = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).top } binding.btnShare.onWindowInsets { view, windowInsets -> view.bottomMargin = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom } } private fun shareImage() { mediaAdapter.shareImage(currentPage) { share(it) } } private fun deleteImage() { mediaAdapter.deleteImage(currentPage) } override fun onBackPressed() { view?.let { Navigation.findNavController(it).popBackStack() } } } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/fragments/VideoFragment.kt ================================================ package com.robertlevonyan.demo.camerax.fragments import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.ContentValues import android.content.Context import android.content.res.Configuration import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraMetadata import android.hardware.display.DisplayManager import android.net.Uri import android.os.Bundle import android.provider.MediaStore import android.util.DisplayMetrics import android.util.Log import android.view.GestureDetector import android.view.View import android.widget.Toast import androidx.camera.camera2.interop.Camera2CameraInfo import androidx.camera.camera2.interop.ExperimentalCamera2Interop import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.video.* import androidx.core.animation.doOnCancel import androidx.core.content.ContextCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.lifecycleScope import androidx.navigation.Navigation import coil.load import coil.request.ErrorResult import coil.request.ImageRequest import coil.transform.CircleCropTransformation import com.robertlevonyan.demo.camerax.R import com.robertlevonyan.demo.camerax.databinding.FragmentVideoBinding import com.robertlevonyan.demo.camerax.utils.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.math.abs import kotlin.math.max import kotlin.math.min import kotlin.properties.Delegates @ExperimentalCamera2Interop @SuppressLint("RestrictedApi") class VideoFragment : BaseFragment(R.layout.fragment_video) { // An instance for display manager to get display change callbacks private val displayManager by lazy { requireContext().getSystemService(Context.DISPLAY_SERVICE) as DisplayManager } // An instance of a helper function to work with Shared Preferences private val prefs by lazy { SharedPrefsManager.newInstance(requireContext()) } private var camera: Camera? = null private var cameraProvider: ProcessCameraProvider? = null private var preview: Preview? = null private var videoCapture: VideoCapture? = null private var displayId = -1 // Selector showing which camera is selected (front or back) private var lensFacing = CameraSelector.DEFAULT_BACK_CAMERA // Selector showing which flash mode is selected (on, off or auto) private var flashMode by Delegates.observable(ImageCapture.FLASH_MODE_OFF) { _, _, new -> binding.btnFlash.setImageResource( when (new) { ImageCapture.FLASH_MODE_ON -> R.drawable.ic_flash_on ImageCapture.FLASH_MODE_AUTO -> R.drawable.ic_flash_auto else -> R.drawable.ic_flash_off } ) } // Selector showing is grid enabled or not private var hasGrid = false // Selector showing is flash enabled or not private var isTorchOn = false // Selector showing is recording currently active private var isRecording = false private val animateRecord by lazy { ObjectAnimator.ofFloat(binding.btnRecordVideo, View.ALPHA, 1f, 0.5f).apply { repeatMode = ObjectAnimator.REVERSE repeatCount = ObjectAnimator.INFINITE doOnCancel { binding.btnRecordVideo.alpha = 1f } } } // A lazy instance of the current fragment's view binding override val binding: FragmentVideoBinding by lazy { FragmentVideoBinding.inflate(layoutInflater) } /** * A display listener for orientation changes that do not trigger a configuration * change, for example if we choose to override config change in manifest or for 180-degree * orientation changes. */ private val displayListener = object : DisplayManager.DisplayListener { override fun onDisplayAdded(displayId: Int) = Unit override fun onDisplayRemoved(displayId: Int) = Unit @SuppressLint("UnsafeExperimentalUsageError", "UnsafeOptInUsageError") override fun onDisplayChanged(displayId: Int) = view?.let { view -> if (displayId == this@VideoFragment.displayId) { preview?.targetRotation = view.display.rotation videoCapture?.targetRotation = view.display.rotation } } ?: Unit } @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) hasGrid = prefs.getBoolean(KEY_GRID, false) initViews() displayManager.registerDisplayListener(displayListener, null) binding.run { viewFinder.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewDetachedFromWindow(v: View) = displayManager.registerDisplayListener(displayListener, null) override fun onViewAttachedToWindow(v: View) = displayManager.unregisterDisplayListener(displayListener) }) binding.btnRecordVideo.setOnClickListener { recordVideo() } btnGallery.setOnClickListener { openPreview() } btnSwitchCamera.setOnClickListener { toggleCamera() } btnGrid.setOnClickListener { toggleGrid() } btnFlash.setOnClickListener { toggleFlash() } // This swipe gesture adds a fun gesture to switch between video and photo val swipeGestures = SwipeGestureDetector().apply { setSwipeCallback(left = { Navigation.findNavController(view).navigate(R.id.action_video_to_camera) }) } val gestureDetectorCompat = GestureDetector(requireContext(), swipeGestures) viewFinder.setOnTouchListener { _, motionEvent -> if (gestureDetectorCompat.onTouchEvent(motionEvent)) return@setOnTouchListener false return@setOnTouchListener true } } } /** * Create some initial states * */ private fun initViews() { binding.btnGrid.setImageResource(if (hasGrid) R.drawable.ic_grid_on else R.drawable.ic_grid_off) binding.groupGridLines.visibility = if (hasGrid) View.VISIBLE else View.GONE adjustInsets() } /** * This methods adds all necessary margins to some views based on window insets and screen orientation * */ private fun adjustInsets() { activity?.window?.fitSystemWindows() binding.btnRecordVideo.onWindowInsets { view, windowInsets -> if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { view.bottomMargin = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom } else { view.endMargin = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).right } } binding.btnFlash.onWindowInsets { view, windowInsets -> view.topMargin = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).top } } /** * Change the facing of camera * toggleButton() function is an Extension function made to animate button rotation * */ private fun toggleCamera() = binding.btnSwitchCamera.toggleButton( flag = lensFacing == CameraSelector.DEFAULT_BACK_CAMERA, rotationAngle = 180f, firstIcon = R.drawable.ic_outline_camera_rear, secondIcon = R.drawable.ic_outline_camera_front, ) { lensFacing = if (it) { CameraSelector.DEFAULT_BACK_CAMERA } else { CameraSelector.DEFAULT_FRONT_CAMERA } startCamera() } /** * Unbinds all the lifecycles from CameraX, then creates new with new parameters * */ private fun startCamera() { // This is the Texture View where the camera will be rendered val viewFinder = binding.viewFinder val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) cameraProviderFuture.addListener({ cameraProvider = cameraProviderFuture.get() // The display information val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) } // The ratio for the output image and preview val aspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels) // The display rotation val rotation = viewFinder.display.rotation val localCameraProvider = cameraProvider ?: throw IllegalStateException("Camera initialization failed.") // The Configuration of camera preview preview = Preview.Builder() .setTargetAspectRatio(aspectRatio) // set the camera aspect ratio .setTargetRotation(rotation) // set the camera rotation .build() val cameraInfo = localCameraProvider.availableCameraInfos.filter { Camera2CameraInfo .from(it) .getCameraCharacteristic(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_BACK } val supportedQualities = QualitySelector.getSupportedQualities(cameraInfo[0]) val qualitySelector = QualitySelector.fromOrderedList( listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD), FallbackStrategy.lowerQualityOrHigherThan(Quality.SD) ) val recorder = Recorder.Builder() .setExecutor(ContextCompat.getMainExecutor(requireContext())).setQualitySelector(qualitySelector) .build() videoCapture = VideoCapture.withOutput(recorder) localCameraProvider.unbindAll() // unbind the use-cases before rebinding them try { // Bind all use cases to the camera with lifecycle camera = localCameraProvider.bindToLifecycle( viewLifecycleOwner, // current lifecycle owner lensFacing, // either front or back facing preview, // camera preview use case videoCapture, // video capture use case ) // Attach the viewfinder's surface provider to preview use case preview?.setSurfaceProvider(viewFinder.surfaceProvider) } catch (e: Exception) { Log.e(TAG, "Failed to bind use cases", e) } }, ContextCompat.getMainExecutor(requireContext())) } /** * Detecting the most suitable aspect ratio for current dimensions * * @param width - preview width * @param height - preview height * @return suitable aspect ratio */ private fun aspectRatio(width: Int, height: Int): Int { val previewRatio = max(width, height).toDouble() / min(width, height) if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) { return AspectRatio.RATIO_4_3 } return AspectRatio.RATIO_16_9 } /** * Navigate to PreviewFragment * */ private fun openPreview() { view?.let { Navigation.findNavController(it).navigate(R.id.action_video_to_preview) } } var recording: Recording? = null @SuppressLint("MissingPermission") private fun recordVideo() { if (recording != null) { animateRecord.cancel() recording?.stop() } val name = "CameraX-recording-${System.currentTimeMillis()}.mp4" val contentValues = ContentValues().apply { put(MediaStore.Video.Media.DISPLAY_NAME, name) } val mediaStoreOutput = MediaStoreOutputOptions.Builder( requireContext().contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI ) .setContentValues(contentValues) .build() recording = videoCapture?.output ?.prepareRecording(requireContext(), mediaStoreOutput) ?.withAudioEnabled() ?.start(ContextCompat.getMainExecutor(requireContext())) { event -> when (event) { is VideoRecordEvent.Start -> { animateRecord.start() } is VideoRecordEvent.Finalize -> { if (!event.hasError()) { val msg = "Video capture succeeded: " + "${event.outputResults.outputUri}" Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT) .show() Log.d(TAG, msg) } else { recording?.close() recording = null Log.e(TAG, "Video capture ends with error: " + "${event.error}") } } } } isRecording = !isRecording } /** * Turns on or off the grid on the screen * */ private fun toggleGrid() = binding.btnGrid.toggleButton( flag = hasGrid, rotationAngle = 180f, firstIcon = R.drawable.ic_grid_off, secondIcon = R.drawable.ic_grid_on ) { flag -> hasGrid = flag prefs.putBoolean(KEY_GRID, flag) binding.groupGridLines.visibility = if (flag) View.VISIBLE else View.GONE } /** * Turns on or off the flashlight * */ private fun toggleFlash() = binding.btnFlash.toggleButton( flag = flashMode == ImageCapture.FLASH_MODE_ON, rotationAngle = 360f, firstIcon = R.drawable.ic_flash_off, secondIcon = R.drawable.ic_flash_on ) { flag -> isTorchOn = flag flashMode = if (flag) ImageCapture.FLASH_MODE_ON else ImageCapture.FLASH_MODE_OFF camera?.cameraControl?.enableTorch(flag) } override fun onPermissionGranted() { // Each time apps is coming to foreground the need permission check is being processed binding.viewFinder.let { vf -> vf.post { // Setting current display ID displayId = vf.display.displayId startCamera() lifecycleScope.launch(Dispatchers.IO) { // Do on IO Dispatcher setLastPictureThumbnail() } camera?.cameraControl?.enableTorch(isTorchOn) } } } private fun setLastPictureThumbnail() = binding.btnGallery.post { getMedia().firstOrNull() // check if there are any photos or videos in the app directory ?.let { setGalleryThumbnail(it.uri) } // preview the last one ?: binding.btnGallery.setImageResource(R.drawable.ic_no_picture) // or the default placeholder } private fun setGalleryThumbnail(savedUri: Uri?) = binding.btnGallery.let { btnGallery -> // Do the work on view's thread, this is needed, because the function is called in a Coroutine Scope's IO Dispatcher btnGallery.post { btnGallery.load(savedUri) { placeholder(R.drawable.ic_no_picture) transformations(CircleCropTransformation()) listener(object : ImageRequest.Listener { override fun onError(request: ImageRequest, result: ErrorResult) { super.onError(request, result) binding.btnGallery.load(savedUri) { placeholder(R.drawable.ic_no_picture) transformations(CircleCropTransformation()) // fetcher(VideoFrameUriFetcher(requireContext())) } } }) } } } override fun onBackPressed() = requireActivity().finish() override fun onStop() { super.onStop() camera?.cameraControl?.enableTorch(false) } companion object { private const val TAG = "CameraXDemo" const val KEY_GRID = "sPrefGridVideo" private const val RATIO_4_3_VALUE = 4.0 / 3.0 // aspect ratio 4x3 private const val RATIO_16_9_VALUE = 16.0 / 9.0 // aspect ratio 16x9 } } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/utils/Extensions.kt ================================================ package com.robertlevonyan.demo.camerax.utils import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.content.Context import android.content.Intent import android.content.res.Configuration import android.os.Build import android.view.* import android.view.View.* import android.widget.ImageButton import androidx.annotation.DrawableRes import androidx.core.animation.doOnEnd import androidx.core.animation.doOnStart import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.viewpager2.widget.ViewPager2 import com.robertlevonyan.demo.camerax.adapter.Media import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.concurrent.Executor fun ImageButton.toggleButton( flag: Boolean, rotationAngle: Float, @DrawableRes firstIcon: Int, @DrawableRes secondIcon: Int, action: (Boolean) -> Unit ) { if (flag) { if (rotationY == 0f) rotationY = rotationAngle animate().rotationY(0f).apply { setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) action(!flag) } }) }.duration = 200 GlobalScope.launch(Dispatchers.Main) { delay(100) setImageResource(firstIcon) } } else { if (rotationY == rotationAngle) rotationY = 0f animate().rotationY(rotationAngle).apply { setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) action(!flag) } }) }.duration = 200 GlobalScope.launch(Dispatchers.Main) { delay(100) setImageResource(secondIcon) } } } fun ViewGroup.circularReveal(button: ImageButton) { ViewAnimationUtils.createCircularReveal( this, button.x.toInt() + button.width / 2, button.y.toInt() + button.height / 2, 0f, if (button.context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) this.width.toFloat() else this.height.toFloat() ).apply { duration = 500 doOnStart { visibility = VISIBLE } }.start() } fun ViewGroup.circularClose(button: ImageButton, action: () -> Unit = {}) { ViewAnimationUtils.createCircularReveal( this, button.x.toInt() + button.width / 2, button.y.toInt() + button.height / 2, if (button.context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) this.width.toFloat() else this.height.toFloat(), 0f ).apply { duration = 500 doOnStart { action() } doOnEnd { visibility = GONE } }.start() } fun View.onWindowInsets(action: (View, WindowInsetsCompat) -> Unit) { ViewCompat.requestApplyInsets(this) ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets -> action(v, insets) insets } } fun Window.fitSystemWindows() { WindowCompat.setDecorFitsSystemWindows(this, false) } fun Fragment.share(media: Media, title: String = "Share with...") { val share = Intent(Intent.ACTION_SEND) share.type = "image/*" share.putExtra(Intent.EXTRA_STREAM, media.uri) startActivity(Intent.createChooser(share, title)) } fun ViewPager2.onPageSelected(action: (Int) -> Unit) { registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { super.onPageSelected(position) action(position) } }) } fun Context.mainExecutor(): Executor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { mainExecutor } else { MainExecutor() } val Context.layoutInflater: LayoutInflater get() = LayoutInflater.from(this) var View.topMargin: Int get() = (this.layoutParams as ViewGroup.MarginLayoutParams).topMargin set(value) { updateLayoutParams { topMargin = value } } var View.topPadding: Int get() = paddingTop set(value) { updateLayoutParams { setPaddingRelative(paddingStart, value, paddingEnd, paddingBottom) } } var View.bottomMargin: Int get() = (this.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin set(value) { updateLayoutParams { bottomMargin = value } } var View.endMargin: Int get() = (this.layoutParams as ViewGroup.MarginLayoutParams).marginEnd set(value) { updateLayoutParams { marginEnd = value } } var View.startMargin: Int get() = (this.layoutParams as ViewGroup.MarginLayoutParams).marginStart set(value) { updateLayoutParams { marginStart = value } } var View.startPadding: Int get() = paddingStart set(value) { updateLayoutParams { setPaddingRelative(value, paddingTop, paddingEnd, paddingBottom) } } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/utils/MainExecutor.kt ================================================ package com.robertlevonyan.demo.camerax.utils import android.os.Handler import android.os.Looper class MainExecutor : ThreadExecutor(Handler(Looper.getMainLooper())) { override fun execute(runnable: Runnable) { handler.post(runnable) } } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/utils/SharedPrefsManager.kt ================================================ package com.robertlevonyan.demo.camerax.utils import android.content.Context class SharedPrefsManager private constructor(private val context: Context) { private val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) companion object { private const val PREFERENCES = "sPrefs" @Synchronized fun newInstance(context: Context) = SharedPrefsManager(context) } fun putBoolean(key: String, value: Boolean) = preferences.edit().putBoolean(key, value).apply() fun putInt(key: String, value: Int) = preferences.edit().putInt(key, value).apply() fun getBoolean(key: String, defValue: Boolean) = preferences.getBoolean(key, defValue) fun getInt(key: String, defValue: Int) = preferences.getInt(key, defValue) } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/utils/SwipeGestureDetector.kt ================================================ package com.robertlevonyan.demo.camerax.utils import android.view.GestureDetector import android.view.MotionEvent import kotlin.math.abs class SwipeGestureDetector : GestureDetector.SimpleOnGestureListener() { companion object { private const val MIN_SWIPE_DISTANCE_X = 100 } var swipeCallback: SwipeCallback? = null override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { e1 ?: return super.onFling(e1, e2, velocityX, velocityY) val deltaX = e1.x - e2.x val deltaXAbs = abs(deltaX) if (deltaXAbs >= MIN_SWIPE_DISTANCE_X) { if (deltaX > 0) { swipeCallback?.onLeftSwipe() } else { swipeCallback?.onRightSwipe() } } return true } interface SwipeCallback { fun onLeftSwipe() fun onRightSwipe() } fun setSwipeCallback(left: ()-> Unit = {}, right: ()-> Unit = {}) { swipeCallback = object : SwipeCallback { override fun onLeftSwipe() { left() } override fun onRightSwipe() { right() } } } } ================================================ FILE: camerax-demo/app/src/main/kotlin/com/robertlevonyan/demo/camerax/utils/ThreadExecutor.kt ================================================ package com.robertlevonyan.demo.camerax.utils import android.os.Handler import java.util.concurrent.Executor open class ThreadExecutor(protected val handler: Handler) : Executor { override fun execute(runnable: Runnable) { handler.post(runnable) } } ================================================ FILE: camerax-demo/app/src/main/res/anim/slide_in.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/anim/slide_in_pop.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/anim/slide_out.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/anim/slide_out_pop.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/bg_button_round.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/bg_options.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_arrow_back.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_delete.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_edit.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_exposure.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_flash_auto.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_flash_off.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_flash_on.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_grid_off.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_grid_on.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_hdr_off.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_hdr_on.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_no_picture.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_outline_camera_enhance.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_outline_camera_front.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_outline_camera_rear.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_play.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_share.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_take_picture.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_take_video.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_timer_10.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_timer_3.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/drawable/ic_timer_off.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/layout/fragment_camera.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/layout/fragment_preview.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/layout/fragment_video.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/layout/item_picture.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/layout-land/fragment_camera.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/layout-land/fragment_video.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/menu/menu_main.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/navigation/nav_graph.xml ================================================ ================================================ FILE: camerax-demo/app/src/main/res/values/colors.xml ================================================ #1a1a18 #80000000 #e7a942 #444444 ================================================ FILE: camerax-demo/app/src/main/res/values/dimens.xml ================================================ 8dp 16dp 24dp 32dp 100dp 56dp 36dp 72sp ================================================ FILE: camerax-demo/app/src/main/res/values/ic_launcher_background.xml ================================================ #F1F1F1 ================================================ FILE: camerax-demo/app/src/main/res/values/strings.xml ================================================ CameraX Demo Settings The app cannot work without permissions OK Hello blank fragment Take picture Navigate to Gallery Switch between front and back cameras Select a timer ================================================ FILE: camerax-demo/app/src/main/res/values/styles.xml ================================================