Merge pull request #318 from KryptKode/feat/camera-x
Rewrite to use CameraX
This commit is contained in:
commit
749c86da14
|
@ -34,7 +34,7 @@ android {
|
|||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
applicationIdSuffix ".debugcamerax"
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
|
@ -65,4 +65,14 @@ dependencies {
|
|||
implementation 'com.github.SimpleMobileTools:Simple-Commons:d1d5402388'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
|
||||
implementation 'androidx.window:window:1.1.0-alpha02'
|
||||
|
||||
def camerax_version = '1.1.0-rc02'
|
||||
implementation "androidx.camera:camera-core:$camerax_version"
|
||||
implementation "androidx.camera:camera-camera2:$camerax_version"
|
||||
implementation "androidx.camera:camera-video:$camerax_version"
|
||||
implementation "androidx.camera:camera-extensions:$camerax_version"
|
||||
implementation "androidx.camera:camera-lifecycle:$camerax_version"
|
||||
implementation "androidx.camera:camera-view:$camerax_version"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_launcher_name">Camera_debug</string>
|
||||
<!--TODO Revert to Camera_debug -->
|
||||
<string name="app_launcher_name">CameraX_debug</string>
|
||||
</resources>
|
||||
|
|
|
@ -7,7 +7,12 @@ import android.net.Uri
|
|||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.provider.MediaStore
|
||||
import android.view.*
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.widget.RelativeLayout
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
|
@ -16,17 +21,40 @@ import com.bumptech.glide.request.RequestOptions
|
|||
import com.simplemobiletools.camera.BuildConfig
|
||||
import com.simplemobiletools.camera.R
|
||||
import com.simplemobiletools.camera.extensions.config
|
||||
import com.simplemobiletools.camera.helpers.*
|
||||
import com.simplemobiletools.camera.helpers.FLASH_OFF
|
||||
import com.simplemobiletools.camera.helpers.FLASH_ON
|
||||
import com.simplemobiletools.camera.helpers.ORIENT_LANDSCAPE_LEFT
|
||||
import com.simplemobiletools.camera.helpers.ORIENT_LANDSCAPE_RIGHT
|
||||
import com.simplemobiletools.camera.helpers.ORIENT_PORTRAIT
|
||||
import com.simplemobiletools.camera.helpers.PhotoProcessor
|
||||
import com.simplemobiletools.camera.implementations.CameraXPreview
|
||||
import com.simplemobiletools.camera.implementations.CameraXPreviewListener
|
||||
import com.simplemobiletools.camera.implementations.MyCameraImpl
|
||||
import com.simplemobiletools.camera.interfaces.MyPreview
|
||||
import com.simplemobiletools.camera.views.CameraPreview
|
||||
import com.simplemobiletools.camera.views.FocusCircleView
|
||||
import com.simplemobiletools.commons.extensions.*
|
||||
import com.simplemobiletools.commons.helpers.*
|
||||
import com.simplemobiletools.commons.helpers.BROADCAST_REFRESH_MEDIA
|
||||
import com.simplemobiletools.commons.helpers.PERMISSION_CAMERA
|
||||
import com.simplemobiletools.commons.helpers.PERMISSION_RECORD_AUDIO
|
||||
import com.simplemobiletools.commons.helpers.PERMISSION_WRITE_STORAGE
|
||||
import com.simplemobiletools.commons.helpers.REFRESH_PATH
|
||||
import com.simplemobiletools.commons.models.Release
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.android.synthetic.main.activity_main.btn_holder
|
||||
import kotlinx.android.synthetic.main.activity_main.capture_black_screen
|
||||
import kotlinx.android.synthetic.main.activity_main.change_resolution
|
||||
import kotlinx.android.synthetic.main.activity_main.last_photo_video_preview
|
||||
import kotlinx.android.synthetic.main.activity_main.settings
|
||||
import kotlinx.android.synthetic.main.activity_main.shutter
|
||||
import kotlinx.android.synthetic.main.activity_main.toggle_camera
|
||||
import kotlinx.android.synthetic.main.activity_main.toggle_flash
|
||||
import kotlinx.android.synthetic.main.activity_main.toggle_photo_video
|
||||
import kotlinx.android.synthetic.main.activity_main.video_rec_curr_timer
|
||||
import kotlinx.android.synthetic.main.activity_main.view_finder
|
||||
import kotlinx.android.synthetic.main.activity_main.view_holder
|
||||
|
||||
class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
||||
class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, CameraXPreviewListener {
|
||||
private val TAG = "MainActivity"
|
||||
private val FADE_DELAY = 5000L
|
||||
private val CAPTURE_ANIMATION_DURATION = 100L
|
||||
|
||||
|
@ -68,7 +96,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (hasStorageAndCameraPermissions()) {
|
||||
mPreview?.onResumed()
|
||||
resumeCameraItems()
|
||||
setupPreviewImage(mIsInPhotoMode)
|
||||
scheduleFadeOut()
|
||||
|
@ -97,14 +124,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
|
||||
hideTimer()
|
||||
mOrientationEventListener.disable()
|
||||
|
||||
if (mPreview?.getCameraState() == STATE_PICTURE_TAKEN) {
|
||||
toast(R.string.photo_not_saved)
|
||||
}
|
||||
|
||||
ensureBackgroundThread {
|
||||
mPreview?.onPaused()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -201,8 +220,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
)
|
||||
|
||||
checkVideoCaptureIntent()
|
||||
mPreview = CameraPreview(this, camera_texture_view, mIsInPhotoMode)
|
||||
view_holder.addView(mPreview as ViewGroup)
|
||||
mPreview = CameraXPreview(this, view_finder, this)
|
||||
checkImageCaptureIntent()
|
||||
mPreview?.setIsImageCaptureIntent(isImageCaptureIntent())
|
||||
|
||||
|
@ -217,7 +235,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
mFadeHandler = Handler()
|
||||
setupPreviewImage(true)
|
||||
|
||||
val initialFlashlightState = FLASH_OFF
|
||||
val initialFlashlightState = config.flashlightState
|
||||
mPreview!!.setFlashlightState(initialFlashlightState)
|
||||
updateFlashlightState(initialFlashlightState)
|
||||
}
|
||||
|
@ -261,10 +279,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
toggle_flash.setImageResource(flashDrawable)
|
||||
}
|
||||
|
||||
fun updateCameraIcon(isUsingFrontCamera: Boolean) {
|
||||
toggle_camera.setImageResource(if (isUsingFrontCamera) R.drawable.ic_camera_rear_vector else R.drawable.ic_camera_front_vector)
|
||||
}
|
||||
|
||||
private fun shutterPressed() {
|
||||
if (checkCameraAvailable()) {
|
||||
handleShutter()
|
||||
|
@ -283,19 +297,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
}
|
||||
}
|
||||
|
||||
fun toggleBottomButtons(hide: Boolean) {
|
||||
runOnUiThread {
|
||||
val alpha = if (hide) 0f else 1f
|
||||
shutter.animate().alpha(alpha).start()
|
||||
toggle_camera.animate().alpha(alpha).start()
|
||||
toggle_flash.animate().alpha(alpha).start()
|
||||
|
||||
shutter.isClickable = !hide
|
||||
toggle_camera.isClickable = !hide
|
||||
toggle_flash.isClickable = !hide
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSettings() {
|
||||
if (settings.alpha == 1f) {
|
||||
val intent = Intent(applicationContext, SettingsActivity::class.java)
|
||||
|
@ -324,14 +325,13 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
}
|
||||
|
||||
if (mIsVideoCaptureIntent) {
|
||||
mPreview?.tryInitVideoMode()
|
||||
mPreview?.initVideoMode()
|
||||
}
|
||||
|
||||
mPreview?.setFlashlightState(FLASH_OFF)
|
||||
hideTimer()
|
||||
mIsInPhotoMode = !mIsInPhotoMode
|
||||
config.initPhotoMode = mIsInPhotoMode
|
||||
showToggleCameraIfNeeded()
|
||||
checkButtons()
|
||||
toggleBottomButtons(false)
|
||||
}
|
||||
|
@ -352,9 +352,10 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
}
|
||||
|
||||
private fun tryInitVideoMode() {
|
||||
if (mPreview?.initVideoMode() == true) {
|
||||
try {
|
||||
mPreview?.initVideoMode()
|
||||
initVideoButtons()
|
||||
} else {
|
||||
} catch (e: Exception) {
|
||||
if (!mIsVideoCaptureIntent) {
|
||||
toast(R.string.video_mode_error)
|
||||
}
|
||||
|
@ -363,7 +364,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
|
||||
private fun initVideoButtons() {
|
||||
toggle_photo_video.setImageResource(R.drawable.ic_camera_vector)
|
||||
showToggleCameraIfNeeded()
|
||||
shutter.setImageResource(R.drawable.ic_video_rec)
|
||||
setupPreviewImage(false)
|
||||
mPreview?.checkFlashlight()
|
||||
|
@ -378,6 +378,13 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
|
||||
mPreviewUri = Uri.withAppendedPath(uri, lastMediaId.toString())
|
||||
|
||||
Log.e(TAG, "mPreviewUri= $mPreviewUri")
|
||||
|
||||
loadLastTakenMedia(mPreviewUri)
|
||||
}
|
||||
|
||||
private fun loadLastTakenMedia(uri: Uri?) {
|
||||
mPreviewUri = uri
|
||||
runOnUiThread {
|
||||
if (!isDestroyed) {
|
||||
val options = RequestOptions()
|
||||
|
@ -385,7 +392,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
|
||||
Glide.with(this)
|
||||
.load(mPreviewUri)
|
||||
.load(uri)
|
||||
.apply(options)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(last_photo_video_preview)
|
||||
|
@ -447,7 +454,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
}
|
||||
|
||||
private fun resumeCameraItems() {
|
||||
showToggleCameraIfNeeded()
|
||||
hideNavigationBarIcons()
|
||||
|
||||
if (!mIsInPhotoMode) {
|
||||
|
@ -455,10 +461,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
}
|
||||
}
|
||||
|
||||
private fun showToggleCameraIfNeeded() {
|
||||
toggle_camera?.beInvisibleIf(mCameraImpl.getCountOfCameras() ?: 1 <= 1)
|
||||
}
|
||||
|
||||
private fun hasStorageAndCameraPermissions() = hasPermission(PERMISSION_WRITE_STORAGE) && hasPermission(PERMISSION_CAMERA)
|
||||
|
||||
private fun setupOrientationEventListener() {
|
||||
|
@ -505,7 +507,15 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
return mIsCameraAvailable
|
||||
}
|
||||
|
||||
fun setFlashAvailable(available: Boolean) {
|
||||
override fun setCameraAvailable(available: Boolean) {
|
||||
mIsCameraAvailable = available
|
||||
}
|
||||
|
||||
override fun setHasFrontAndBackCamera(hasFrontAndBack: Boolean) {
|
||||
toggle_camera?.beVisibleIf(hasFrontAndBack)
|
||||
}
|
||||
|
||||
override fun setFlashAvailable(available: Boolean) {
|
||||
if (available) {
|
||||
toggle_flash.beVisible()
|
||||
} else {
|
||||
|
@ -515,8 +525,51 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
}
|
||||
}
|
||||
|
||||
fun setIsCameraAvailable(available: Boolean) {
|
||||
mIsCameraAvailable = available
|
||||
override fun onChangeCamera(frontCamera: Boolean) {
|
||||
toggle_camera.setImageResource(if (frontCamera) R.drawable.ic_camera_rear_vector else R.drawable.ic_camera_front_vector)
|
||||
}
|
||||
|
||||
override fun toggleBottomButtons(hide: Boolean) {
|
||||
runOnUiThread {
|
||||
val alpha = if (hide) 0f else 1f
|
||||
shutter.animate().alpha(alpha).start()
|
||||
toggle_camera.animate().alpha(alpha).start()
|
||||
toggle_flash.animate().alpha(alpha).start()
|
||||
|
||||
shutter.isClickable = !hide
|
||||
toggle_camera.isClickable = !hide
|
||||
toggle_flash.isClickable = !hide
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMediaCaptured(uri: Uri) {
|
||||
loadLastTakenMedia(uri)
|
||||
}
|
||||
|
||||
override fun onChangeFlashMode(flashMode: Int) {
|
||||
updateFlashlightState(flashMode)
|
||||
}
|
||||
|
||||
override fun onVideoRecordingStarted() {
|
||||
shutter.setImageResource(R.drawable.ic_video_stop)
|
||||
toggle_camera.beInvisible()
|
||||
video_rec_curr_timer.beVisible()
|
||||
}
|
||||
|
||||
override fun onVideoRecordingStopped() {
|
||||
shutter.setImageResource(R.drawable.ic_video_rec)
|
||||
video_rec_curr_timer.text = 0.getFormattedDuration()
|
||||
video_rec_curr_timer.beGone()
|
||||
toggle_camera.beVisible()
|
||||
}
|
||||
|
||||
override fun onVideoDurationChanged(durationNanos: Long) {
|
||||
val seconds = TimeUnit.NANOSECONDS.toSeconds(durationNanos).toInt()
|
||||
video_rec_curr_timer.text = seconds.getFormattedDuration()
|
||||
}
|
||||
|
||||
override fun onFocusCamera(xPos: Float, yPos: Float) {
|
||||
mFocusCircleView.drawFocusCircle(xPos, yPos)
|
||||
}
|
||||
|
||||
fun setRecordingState(isRecording: Boolean) {
|
||||
|
@ -527,7 +580,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
showTimer()
|
||||
} else {
|
||||
shutter.setImageResource(R.drawable.ic_video_rec)
|
||||
showToggleCameraIfNeeded()
|
||||
hideTimer()
|
||||
}
|
||||
}
|
||||
|
@ -548,6 +600,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener {
|
|||
fun drawFocusCircle(x: Float, y: Float) = mFocusCircleView.drawFocusCircle(x, y)
|
||||
|
||||
override fun mediaSaved(path: String) {
|
||||
Log.e(TAG, "mediaSaved: $path")
|
||||
rescanPaths(arrayListOf(path)) {
|
||||
setupPreviewImage(true)
|
||||
Intent(BROADCAST_REFRESH_MEDIA).apply {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package com.simplemobiletools.camera.extensions
|
||||
|
||||
import androidx.camera.core.CameraSelector
|
||||
|
||||
fun CameraSelector.toLensFacing(): Int {
|
||||
return if (this == CameraSelector.DEFAULT_FRONT_CAMERA) {
|
||||
CameraSelector.LENS_FACING_FRONT
|
||||
} else {
|
||||
CameraSelector.LENS_FACING_BACK
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package com.simplemobiletools.camera.extensions
|
||||
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageCapture
|
||||
import com.simplemobiletools.camera.helpers.FLASH_AUTO
|
||||
import com.simplemobiletools.camera.helpers.FLASH_OFF
|
||||
import com.simplemobiletools.camera.helpers.FLASH_ON
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
fun Int.toCameraXFlashMode(): Int {
|
||||
return when (this) {
|
||||
FLASH_ON -> ImageCapture.FLASH_MODE_ON
|
||||
FLASH_OFF -> ImageCapture.FLASH_MODE_OFF
|
||||
FLASH_AUTO -> ImageCapture.FLASH_MODE_AUTO
|
||||
else -> throw IllegalArgumentException("Unknown mode: $this")
|
||||
}
|
||||
}
|
||||
|
||||
fun Int.toAppFlashMode(): Int {
|
||||
return when (this) {
|
||||
ImageCapture.FLASH_MODE_ON -> FLASH_ON
|
||||
ImageCapture.FLASH_MODE_OFF -> FLASH_OFF
|
||||
ImageCapture.FLASH_MODE_AUTO -> FLASH_AUTO
|
||||
else -> throw IllegalArgumentException("Unknown mode: $this")
|
||||
}
|
||||
}
|
||||
|
||||
fun Int.toCameraSelector(): CameraSelector {
|
||||
return if (this == CameraSelector.LENS_FACING_FRONT) {
|
||||
CameraSelector.DEFAULT_FRONT_CAMERA
|
||||
} else {
|
||||
CameraSelector.DEFAULT_BACK_CAMERA
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.simplemobiletools.camera.helpers
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import androidx.camera.core.CameraSelector
|
||||
import com.simplemobiletools.commons.helpers.BaseConfig
|
||||
import java.io.File
|
||||
|
||||
|
@ -37,6 +38,10 @@ class Config(context: Context) : BaseConfig(context) {
|
|||
get() = prefs.getString(LAST_USED_CAMERA, "0")!!
|
||||
set(cameraId) = prefs.edit().putString(LAST_USED_CAMERA, cameraId).apply()
|
||||
|
||||
var lastUsedCameraLens: Int
|
||||
get() = prefs.getInt(LAST_USED_CAMERA_LENS, CameraSelector.LENS_FACING_BACK)
|
||||
set(lens) = prefs.edit().putInt(LAST_USED_CAMERA_LENS, lens).apply()
|
||||
|
||||
var initPhotoMode: Boolean
|
||||
get() = prefs.getBoolean(INIT_PHOTO_MODE, true)
|
||||
set(initPhotoMode) = prefs.edit().putBoolean(INIT_PHOTO_MODE, initPhotoMode).apply()
|
||||
|
|
|
@ -10,6 +10,7 @@ const val SOUND = "sound"
|
|||
const val VOLUME_BUTTONS_AS_SHUTTER = "volume_buttons_as_shutter"
|
||||
const val FLIP_PHOTOS = "flip_photos"
|
||||
const val LAST_USED_CAMERA = "last_used_camera_2"
|
||||
const val LAST_USED_CAMERA_LENS = "last_used_camera_lens"
|
||||
const val FLASHLIGHT_STATE = "flashlight_state"
|
||||
const val INIT_PHOTO_MODE = "init_photo_mode"
|
||||
const val BACK_PHOTO_RESOLUTION_INDEX = "back_photo_resolution_index_2"
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package com.simplemobiletools.camera.helpers
|
||||
|
||||
import android.media.MediaActionSound
|
||||
|
||||
class MediaSoundHelper {
|
||||
private val mediaActionSound = MediaActionSound()
|
||||
|
||||
fun loadSounds() {
|
||||
mediaActionSound.load(MediaActionSound.START_VIDEO_RECORDING)
|
||||
mediaActionSound.load(MediaActionSound.STOP_VIDEO_RECORDING)
|
||||
mediaActionSound.load(MediaActionSound.SHUTTER_CLICK)
|
||||
}
|
||||
|
||||
fun playShutterSound() {
|
||||
mediaActionSound.play(MediaActionSound.SHUTTER_CLICK)
|
||||
}
|
||||
|
||||
fun playStartVideoRecordingSound() {
|
||||
mediaActionSound.play(MediaActionSound.START_VIDEO_RECORDING)
|
||||
}
|
||||
|
||||
fun playStopVideoRecordingSound() {
|
||||
mediaActionSound.play(MediaActionSound.STOP_VIDEO_RECORDING)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.simplemobiletools.camera.helpers
|
||||
|
||||
import android.view.ScaleGestureDetector
|
||||
import androidx.camera.core.CameraControl
|
||||
import androidx.camera.core.CameraInfo
|
||||
|
||||
class PinchToZoomOnScaleGestureListener(
|
||||
private val cameraInfo: CameraInfo,
|
||||
private val cameraControl: CameraControl,
|
||||
) : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
private val zoomCalculator = ZoomCalculator()
|
||||
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
val zoomState = cameraInfo.zoomState.value ?: return false
|
||||
val zoomRatio = zoomCalculator.calculateZoomRatio(zoomState, detector.scaleFactor)
|
||||
cameraControl.setZoomRatio(zoomRatio)
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.simplemobiletools.camera.helpers
|
||||
|
||||
import androidx.camera.core.ZoomState
|
||||
|
||||
class ZoomCalculator {
|
||||
|
||||
fun calculateZoomRatio(zoomState: ZoomState, pinchToZoomScale: Float): Float {
|
||||
val clampedRatio = zoomState.zoomRatio * speedUpZoomBy2X(pinchToZoomScale)
|
||||
// Clamp the ratio with the zoom range.
|
||||
return clampedRatio.coerceAtLeast(zoomState.minZoomRatio).coerceAtMost(zoomState.maxZoomRatio)
|
||||
}
|
||||
|
||||
private fun speedUpZoomBy2X(scaleFactor: Float): Float {
|
||||
return if (scaleFactor > 1f) {
|
||||
1.0f + (scaleFactor - 1.0f) * 2
|
||||
} else {
|
||||
1.0f - (1.0f - scaleFactor) * 2
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,485 @@
|
|||
package com.simplemobiletools.camera.implementations
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.hardware.SensorManager
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.view.Display
|
||||
import android.view.GestureDetector
|
||||
import android.view.GestureDetector.SimpleOnGestureListener
|
||||
import android.view.MotionEvent
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.Surface
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.camera.core.*
|
||||
import androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
|
||||
import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO
|
||||
import androidx.camera.core.ImageCapture.FLASH_MODE_OFF
|
||||
import androidx.camera.core.ImageCapture.FLASH_MODE_ON
|
||||
import androidx.camera.core.ImageCapture.Metadata
|
||||
import androidx.camera.core.ImageCapture.OnImageSavedCallback
|
||||
import androidx.camera.core.ImageCapture.OutputFileOptions
|
||||
import androidx.camera.core.ImageCapture.OutputFileResults
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.video.MediaStoreOutputOptions
|
||||
import androidx.camera.video.Quality
|
||||
import androidx.camera.video.QualitySelector
|
||||
import androidx.camera.video.Recorder
|
||||
import androidx.camera.video.Recording
|
||||
import androidx.camera.video.VideoCapture
|
||||
import androidx.camera.video.VideoRecordEvent
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.window.layout.WindowMetricsCalculator
|
||||
import com.bumptech.glide.load.ImageHeaderParser.UNKNOWN_ORIENTATION
|
||||
import com.simplemobiletools.camera.R
|
||||
import com.simplemobiletools.camera.extensions.config
|
||||
import com.simplemobiletools.camera.extensions.toAppFlashMode
|
||||
import com.simplemobiletools.camera.extensions.toCameraSelector
|
||||
import com.simplemobiletools.camera.extensions.toCameraXFlashMode
|
||||
import com.simplemobiletools.camera.extensions.toLensFacing
|
||||
import com.simplemobiletools.camera.helpers.MediaSoundHelper
|
||||
import com.simplemobiletools.camera.helpers.PinchToZoomOnScaleGestureListener
|
||||
import com.simplemobiletools.camera.interfaces.MyPreview
|
||||
import com.simplemobiletools.commons.extensions.showErrorToast
|
||||
import com.simplemobiletools.commons.extensions.toast
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class CameraXPreview(
|
||||
private val activity: AppCompatActivity,
|
||||
private val previewView: PreviewView,
|
||||
private val listener: CameraXPreviewListener,
|
||||
) : MyPreview, DefaultLifecycleObserver {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CameraXPreview"
|
||||
private const val RATIO_4_3_VALUE = 4.0 / 3.0
|
||||
private const val RATIO_16_9_VALUE = 16.0 / 9.0
|
||||
|
||||
// Auto focus is 1/6 of the area.
|
||||
private const val AF_SIZE = 1.0f / 6.0f
|
||||
private const val AE_SIZE = AF_SIZE * 1.5f
|
||||
}
|
||||
|
||||
private val config = activity.config
|
||||
private val contentResolver = activity.contentResolver
|
||||
private val mainExecutor = activity.mainExecutor
|
||||
private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
private val mediaSoundHelper = MediaSoundHelper()
|
||||
private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
|
||||
|
||||
private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) {
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
if (orientation == UNKNOWN_ORIENTATION) {
|
||||
return
|
||||
}
|
||||
|
||||
val rotation = when (orientation) {
|
||||
in 45 until 135 -> Surface.ROTATION_270
|
||||
in 135 until 225 -> Surface.ROTATION_180
|
||||
in 225 until 315 -> Surface.ROTATION_90
|
||||
else -> Surface.ROTATION_0
|
||||
}
|
||||
|
||||
preview?.targetRotation = rotation
|
||||
imageCapture?.targetRotation = rotation
|
||||
videoCapture?.targetRotation = rotation
|
||||
}
|
||||
}
|
||||
|
||||
private var preview: Preview? = null
|
||||
private var cameraProvider: ProcessCameraProvider? = null
|
||||
private var imageCapture: ImageCapture? = null
|
||||
private var videoCapture: VideoCapture<Recorder>? = null
|
||||
private var camera: Camera? = null
|
||||
private var currentRecording: Recording? = null
|
||||
private var recordingState: VideoRecordEvent? = null
|
||||
private var cameraSelector = config.lastUsedCameraLens.toCameraSelector()
|
||||
private var flashMode = config.flashlightState.toCameraXFlashMode()
|
||||
private var isPhotoCapture = config.initPhotoMode
|
||||
|
||||
init {
|
||||
bindToLifeCycle()
|
||||
mediaSoundHelper.loadSounds()
|
||||
previewView.doOnLayout {
|
||||
startCamera()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindToLifeCycle() {
|
||||
activity.lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
private fun startCamera() {
|
||||
Log.i(TAG, "startCamera: ")
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
|
||||
cameraProviderFuture.addListener({
|
||||
try {
|
||||
cameraProvider = cameraProviderFuture.get()
|
||||
bindCameraUseCases()
|
||||
setupCameraObservers()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "startCamera: ", e)
|
||||
activity.showErrorToast(activity.getString(R.string.camera_open_error))
|
||||
}
|
||||
}, mainExecutor)
|
||||
}
|
||||
|
||||
private fun bindCameraUseCases() {
|
||||
val cameraProvider = cameraProvider ?: throw IllegalStateException("Camera initialization failed.")
|
||||
val metrics = windowMetricsCalculator.computeCurrentWindowMetrics(activity).bounds
|
||||
val aspectRatio = aspectRatio(metrics.width(), metrics.height())
|
||||
val rotation = previewView.display.rotation
|
||||
|
||||
preview = buildPreview(aspectRatio, rotation)
|
||||
val captureUseCase = getCaptureUseCase(aspectRatio, rotation)
|
||||
cameraProvider.unbindAll()
|
||||
camera = cameraProvider.bindToLifecycle(
|
||||
activity,
|
||||
cameraSelector,
|
||||
preview,
|
||||
captureUseCase,
|
||||
)
|
||||
preview?.setSurfaceProvider(previewView.surfaceProvider)
|
||||
setupZoomAndFocus()
|
||||
}
|
||||
|
||||
private fun setupCameraObservers() {
|
||||
listener.setFlashAvailable(camera?.cameraInfo?.hasFlashUnit() ?: false)
|
||||
listener.onChangeCamera(isFrontCameraInUse())
|
||||
|
||||
camera?.cameraInfo?.cameraState?.observe(activity) { cameraState ->
|
||||
when (cameraState.type) {
|
||||
CameraState.Type.OPEN,
|
||||
CameraState.Type.OPENING -> {
|
||||
listener.setHasFrontAndBackCamera(hasFrontCamera() && hasBackCamera())
|
||||
listener.setCameraAvailable(true)
|
||||
}
|
||||
CameraState.Type.PENDING_OPEN,
|
||||
CameraState.Type.CLOSING,
|
||||
CameraState.Type.CLOSED -> {
|
||||
listener.setCameraAvailable(false)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle errors
|
||||
cameraState.error?.let { error ->
|
||||
listener.setCameraAvailable(false)
|
||||
when (error.code) {
|
||||
CameraState.ERROR_STREAM_CONFIG -> {
|
||||
Log.e(TAG, "ERROR_STREAM_CONFIG")
|
||||
// Make sure to setup the use cases properly
|
||||
activity.toast(R.string.camera_unavailable)
|
||||
}
|
||||
CameraState.ERROR_CAMERA_IN_USE -> {
|
||||
Log.e(TAG, "ERROR_CAMERA_IN_USE")
|
||||
// Close the camera or ask user to close another camera app that's using the
|
||||
// camera
|
||||
activity.showErrorToast("Camera is in use by another app, please close")
|
||||
}
|
||||
CameraState.ERROR_MAX_CAMERAS_IN_USE -> {
|
||||
Log.e(TAG, "ERROR_MAX_CAMERAS_IN_USE")
|
||||
// Close another open camera in the app, or ask the user to close another
|
||||
// camera app that's using the camera
|
||||
activity.showErrorToast("Camera is in use by another app, please close")
|
||||
}
|
||||
CameraState.ERROR_OTHER_RECOVERABLE_ERROR -> {
|
||||
Log.e(TAG, "ERROR_OTHER_RECOVERABLE_ERROR")
|
||||
activity.toast(R.string.camera_open_error)
|
||||
}
|
||||
CameraState.ERROR_CAMERA_DISABLED -> {
|
||||
Log.e(TAG, "ERROR_CAMERA_DISABLED")
|
||||
// Ask the user to enable the device's cameras
|
||||
activity.toast(R.string.camera_open_error)
|
||||
}
|
||||
CameraState.ERROR_CAMERA_FATAL_ERROR -> {
|
||||
Log.e(TAG, "ERROR_CAMERA_FATAL_ERROR")
|
||||
// Ask the user to reboot the device to restore camera function
|
||||
activity.toast(R.string.camera_open_error)
|
||||
}
|
||||
CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED -> {
|
||||
// Ask the user to disable the "Do Not Disturb" mode, then reopen the camera
|
||||
Log.e(TAG, "ERROR_DO_NOT_DISTURB_MODE_ENABLED")
|
||||
activity.toast(R.string.camera_open_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCaptureUseCase(aspectRatio: Int, rotation: Int): UseCase {
|
||||
return if (isPhotoCapture) {
|
||||
cameraProvider?.unbind(videoCapture)
|
||||
buildImageCapture(aspectRatio, rotation).also {
|
||||
imageCapture = it
|
||||
}
|
||||
} else {
|
||||
cameraProvider?.unbind(imageCapture)
|
||||
buildVideoCapture().also {
|
||||
videoCapture = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildImageCapture(aspectRatio: Int, rotation: Int): ImageCapture {
|
||||
return ImageCapture.Builder()
|
||||
.setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY)
|
||||
.setFlashMode(flashMode)
|
||||
.setJpegQuality(config.photoQuality)
|
||||
.setTargetAspectRatio(aspectRatio)
|
||||
.setTargetRotation(rotation)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildPreview(aspectRatio: Int, rotation: Int): Preview {
|
||||
return Preview.Builder()
|
||||
.setTargetAspectRatio(aspectRatio)
|
||||
.setTargetRotation(rotation)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildVideoCapture(): VideoCapture<Recorder> {
|
||||
val recorder = Recorder.Builder()
|
||||
//TODO: user control for quality
|
||||
.setQualitySelector(QualitySelector.from(Quality.FHD))
|
||||
.build()
|
||||
return VideoCapture.withOutput(recorder)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private fun hasBackCamera(): Boolean {
|
||||
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
|
||||
}
|
||||
|
||||
private fun hasFrontCamera(): Boolean {
|
||||
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
|
||||
}
|
||||
|
||||
private fun isFrontCameraInUse(): Boolean {
|
||||
return cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
// source: https://stackoverflow.com/a/60095886/10552591
|
||||
private fun setupZoomAndFocus() {
|
||||
Log.i(TAG, "camera controller: ${previewView.controller}")
|
||||
val scaleGesture = camera?.let { ScaleGestureDetector(activity, PinchToZoomOnScaleGestureListener(it.cameraInfo, it.cameraControl)) }
|
||||
val gestureDetector = GestureDetector(activity, object : SimpleOnGestureListener() {
|
||||
override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
|
||||
return camera?.cameraInfo?.let {
|
||||
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
|
||||
val width = previewView.width.toFloat()
|
||||
val height = previewView.height.toFloat()
|
||||
Log.i(TAG, "onSingleTapConfirmed: width=$width,height=$height")
|
||||
val factory = DisplayOrientedMeteringPointFactory(display, it, width, height)
|
||||
val xPos = event.x
|
||||
val yPos = event.y
|
||||
val autoFocusPoint = factory.createPoint(xPos, yPos, AF_SIZE)
|
||||
val autoExposurePoint = factory.createPoint(xPos, yPos, AE_SIZE)
|
||||
val focusMeteringAction = FocusMeteringAction.Builder(autoFocusPoint, FocusMeteringAction.FLAG_AF)
|
||||
.addPoint(autoExposurePoint, FocusMeteringAction.FLAG_AE)
|
||||
.disableAutoCancel()
|
||||
.build()
|
||||
camera?.cameraControl?.startFocusAndMetering(focusMeteringAction)
|
||||
listener.onFocusCamera(xPos, yPos)
|
||||
Log.i(TAG, "start focus")
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
})
|
||||
previewView.setOnTouchListener { _, event ->
|
||||
Log.i(TAG, "setOnTouchListener: x=${event.x}, y=${event.y}")
|
||||
gestureDetector.onTouchEvent(event)
|
||||
scaleGesture?.onTouchEvent(event)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
orientationEventListener.enable()
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
orientationEventListener.disable()
|
||||
}
|
||||
|
||||
override fun setTargetUri(uri: Uri) {
|
||||
|
||||
}
|
||||
|
||||
override fun showChangeResolutionDialog() {
|
||||
|
||||
}
|
||||
|
||||
override fun toggleFrontBackCamera() {
|
||||
val newCameraSelector = if (isFrontCameraInUse()) {
|
||||
CameraSelector.DEFAULT_BACK_CAMERA
|
||||
} else {
|
||||
CameraSelector.DEFAULT_FRONT_CAMERA
|
||||
}
|
||||
cameraSelector = newCameraSelector
|
||||
config.lastUsedCameraLens = newCameraSelector.toLensFacing()
|
||||
startCamera()
|
||||
}
|
||||
|
||||
override fun toggleFlashlight() {
|
||||
val newFlashMode = when (flashMode) {
|
||||
FLASH_MODE_OFF -> FLASH_MODE_ON
|
||||
FLASH_MODE_ON -> FLASH_MODE_AUTO
|
||||
FLASH_MODE_AUTO -> FLASH_MODE_OFF
|
||||
else -> throw IllegalArgumentException("Unknown mode: $flashMode")
|
||||
}
|
||||
|
||||
flashMode = newFlashMode
|
||||
imageCapture?.flashMode = newFlashMode
|
||||
val appFlashMode = flashMode.toAppFlashMode()
|
||||
config.flashlightState = appFlashMode
|
||||
listener.onChangeFlashMode(appFlashMode)
|
||||
}
|
||||
|
||||
override fun tryTakePicture() {
|
||||
Log.i(TAG, "captureImage: ")
|
||||
val imageCapture = imageCapture ?: throw IllegalStateException("Camera initialization failed.")
|
||||
|
||||
val metadata = Metadata().apply {
|
||||
isReversedHorizontal = config.flipPhotos
|
||||
}
|
||||
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, getRandomMediaName(true))
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
|
||||
}
|
||||
val contentUri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
val outputOptions = OutputFileOptions.Builder(contentResolver, contentUri, contentValues)
|
||||
.setMetadata(metadata)
|
||||
.build()
|
||||
|
||||
|
||||
imageCapture.takePicture(outputOptions, mainExecutor, object : OnImageSavedCallback {
|
||||
override fun onImageSaved(outputFileResults: OutputFileResults) {
|
||||
listener.toggleBottomButtons(false)
|
||||
listener.onMediaCaptured(outputFileResults.savedUri!!)
|
||||
}
|
||||
|
||||
override fun onError(exception: ImageCaptureException) {
|
||||
listener.toggleBottomButtons(false)
|
||||
activity.showErrorToast("Capture picture $exception")
|
||||
Log.e(TAG, "Error", exception)
|
||||
}
|
||||
})
|
||||
playShutterSoundIfEnabled()
|
||||
}
|
||||
|
||||
override fun initPhotoMode() {
|
||||
isPhotoCapture = true
|
||||
startCamera()
|
||||
}
|
||||
|
||||
override fun initVideoMode() {
|
||||
isPhotoCapture = false
|
||||
startCamera()
|
||||
}
|
||||
|
||||
override fun toggleRecording() {
|
||||
Log.d(TAG, "toggleRecording: currentRecording=$currentRecording, recordingState=$recordingState")
|
||||
if (currentRecording == null || recordingState is VideoRecordEvent.Finalize) {
|
||||
startRecording()
|
||||
} else {
|
||||
currentRecording?.stop()
|
||||
currentRecording = null
|
||||
Log.d(TAG, "Recording stopped")
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun startRecording() {
|
||||
val videoCapture = videoCapture ?: throw IllegalStateException("Camera initialization failed.")
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, getRandomMediaName(false))
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
|
||||
}
|
||||
val contentUri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
val outputOptions = MediaStoreOutputOptions.Builder(contentResolver, contentUri)
|
||||
.setContentValues(contentValues)
|
||||
.build()
|
||||
|
||||
currentRecording = videoCapture.output
|
||||
.prepareRecording(activity, outputOptions)
|
||||
.withAudioEnabled()
|
||||
.start(mainExecutor) { recordEvent ->
|
||||
Log.d(TAG, "recordEvent=$recordEvent ")
|
||||
recordingState = recordEvent
|
||||
when (recordEvent) {
|
||||
is VideoRecordEvent.Start -> {
|
||||
playStartVideoRecordingSoundIfEnabled()
|
||||
listener.onVideoRecordingStarted()
|
||||
}
|
||||
|
||||
is VideoRecordEvent.Status -> {
|
||||
listener.onVideoDurationChanged(recordEvent.recordingStats.recordedDurationNanos)
|
||||
}
|
||||
|
||||
is VideoRecordEvent.Finalize -> {
|
||||
playStopVideoRecordingSoundIfEnabled()
|
||||
listener.onVideoRecordingStopped()
|
||||
if (recordEvent.hasError()) {
|
||||
// TODO: Handle errors
|
||||
} else {
|
||||
listener.onMediaCaptured(recordEvent.outputResults.outputUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Recording started")
|
||||
}
|
||||
|
||||
private fun getRandomMediaName(isPhoto: Boolean): String {
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||
return if (isPhoto) {
|
||||
"IMG_$timestamp"
|
||||
} else {
|
||||
"VID_$timestamp"
|
||||
}
|
||||
}
|
||||
|
||||
private fun playShutterSoundIfEnabled() {
|
||||
if (config.isSoundEnabled) {
|
||||
mediaSoundHelper.playShutterSound()
|
||||
}
|
||||
}
|
||||
|
||||
private fun playStartVideoRecordingSoundIfEnabled() {
|
||||
if (config.isSoundEnabled) {
|
||||
mediaSoundHelper.playStartVideoRecordingSound()
|
||||
}
|
||||
}
|
||||
|
||||
private fun playStopVideoRecordingSoundIfEnabled() {
|
||||
if (config.isSoundEnabled) {
|
||||
mediaSoundHelper.playStopVideoRecordingSound()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.simplemobiletools.camera.implementations
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
interface CameraXPreviewListener {
|
||||
fun setCameraAvailable(available: Boolean)
|
||||
fun setHasFrontAndBackCamera(hasFrontAndBack:Boolean)
|
||||
fun setFlashAvailable(available: Boolean)
|
||||
fun onChangeCamera(frontCamera: Boolean)
|
||||
fun toggleBottomButtons(hide:Boolean)
|
||||
fun onMediaCaptured(uri: Uri)
|
||||
fun onChangeFlashMode(flashMode: Int)
|
||||
fun onVideoRecordingStarted()
|
||||
fun onVideoRecordingStopped()
|
||||
fun onVideoDurationChanged(durationNanos: Long)
|
||||
fun onFocusCamera(xPos: Float, yPos: Float)
|
||||
}
|
|
@ -3,17 +3,18 @@ package com.simplemobiletools.camera.interfaces
|
|||
import android.net.Uri
|
||||
|
||||
interface MyPreview {
|
||||
fun onResumed()
|
||||
|
||||
fun onPaused()
|
||||
fun onResumed() = Unit
|
||||
|
||||
fun onPaused() = Unit
|
||||
|
||||
fun setTargetUri(uri: Uri)
|
||||
|
||||
fun setIsImageCaptureIntent(isImageCaptureIntent: Boolean)
|
||||
fun setIsImageCaptureIntent(isImageCaptureIntent: Boolean) = Unit
|
||||
|
||||
fun setFlashlightState(state: Int)
|
||||
fun setFlashlightState(state: Int) = Unit
|
||||
|
||||
fun getCameraState(): Int
|
||||
fun getCameraState(): Int = 0
|
||||
|
||||
fun showChangeResolutionDialog()
|
||||
|
||||
|
@ -25,11 +26,9 @@ interface MyPreview {
|
|||
|
||||
fun toggleRecording()
|
||||
|
||||
fun tryInitVideoMode()
|
||||
|
||||
fun initPhotoMode()
|
||||
|
||||
fun initVideoMode(): Boolean
|
||||
fun initVideoMode()
|
||||
|
||||
fun checkFlashlight()
|
||||
fun checkFlashlight() = Unit
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview {
|
|||
private val mCameraToPreviewMatrix = Matrix()
|
||||
private val mPreviewToCameraMatrix = Matrix()
|
||||
private val mCameraOpenCloseLock = Semaphore(1)
|
||||
private val mMediaActionSound = MediaActionSound()
|
||||
private val mediaSoundHelper = MediaSoundHelper()
|
||||
private var mZoomRect: Rect? = null
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
@ -114,7 +114,7 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview {
|
|||
|
||||
mUseFrontCamera = false
|
||||
mIsInVideoMode = !initPhotoMode
|
||||
loadSounds()
|
||||
mediaSoundHelper.loadSounds()
|
||||
|
||||
val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
||||
|
@ -182,12 +182,6 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview {
|
|||
}
|
||||
}
|
||||
|
||||
private fun loadSounds() {
|
||||
mMediaActionSound.load(MediaActionSound.START_VIDEO_RECORDING)
|
||||
mMediaActionSound.load(MediaActionSound.STOP_VIDEO_RECORDING)
|
||||
mMediaActionSound.load(MediaActionSound.SHUTTER_CLICK)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun openCamera(width: Int, height: Int) {
|
||||
try {
|
||||
|
@ -369,7 +363,7 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview {
|
|||
mIsFocusSupported = get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)!!.size > 1
|
||||
}
|
||||
mActivity.setFlashAvailable(mIsFlashSupported)
|
||||
mActivity.updateCameraIcon(mUseFrontCamera)
|
||||
mActivity.onChangeCamera(mUseFrontCamera)
|
||||
return
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -429,21 +423,21 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview {
|
|||
mCameraOpenCloseLock.release()
|
||||
mCameraDevice = cameraDevice
|
||||
createCameraPreviewSession()
|
||||
mActivity.setIsCameraAvailable(true)
|
||||
mActivity.setCameraAvailable(true)
|
||||
}
|
||||
|
||||
override fun onDisconnected(cameraDevice: CameraDevice) {
|
||||
mCameraOpenCloseLock.release()
|
||||
cameraDevice.close()
|
||||
mCameraDevice = null
|
||||
mActivity.setIsCameraAvailable(false)
|
||||
mActivity.setCameraAvailable(false)
|
||||
}
|
||||
|
||||
override fun onError(cameraDevice: CameraDevice, error: Int) {
|
||||
mCameraOpenCloseLock.release()
|
||||
cameraDevice.close()
|
||||
mCameraDevice = null
|
||||
mActivity.setIsCameraAvailable(false)
|
||||
mActivity.setCameraAvailable(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -576,7 +570,7 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview {
|
|||
}
|
||||
|
||||
if (mActivity.config.isSoundEnabled) {
|
||||
mMediaActionSound.play(MediaActionSound.SHUTTER_CLICK)
|
||||
mediaSoundHelper.playShutterSound()
|
||||
}
|
||||
|
||||
mCameraState = STATE_PICTURE_TAKEN
|
||||
|
@ -824,7 +818,7 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview {
|
|||
closeCaptureSession()
|
||||
setupMediaRecorder()
|
||||
if (mActivity.config.isSoundEnabled) {
|
||||
mMediaActionSound.play(MediaActionSound.START_VIDEO_RECORDING)
|
||||
mediaSoundHelper.playStartVideoRecordingSound()
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -853,7 +847,7 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview {
|
|||
private fun stopRecording() {
|
||||
mCameraState = STATE_STOPING_RECORDING
|
||||
if (mActivity.config.isSoundEnabled) {
|
||||
mMediaActionSound.play(MediaActionSound.STOP_VIDEO_RECORDING)
|
||||
mediaSoundHelper.playStopVideoRecordingSound()
|
||||
}
|
||||
|
||||
mIsRecording = false
|
||||
|
@ -981,23 +975,18 @@ class CameraPreview : ViewGroup, TextureView.SurfaceTextureListener, MyPreview {
|
|||
}
|
||||
}
|
||||
|
||||
override fun tryInitVideoMode() {
|
||||
initVideoMode()
|
||||
}
|
||||
|
||||
override fun initPhotoMode() {
|
||||
mIsInVideoMode = false
|
||||
closeCamera()
|
||||
openCamera(mTextureView.width, mTextureView.height)
|
||||
}
|
||||
|
||||
override fun initVideoMode(): Boolean {
|
||||
override fun initVideoMode() {
|
||||
mLastFocusX = 0f
|
||||
mLastFocusY = 0f
|
||||
mIsInVideoMode = true
|
||||
closeCamera()
|
||||
openCamera(mTextureView.width, mTextureView.height)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun checkFlashlight() {
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black">
|
||||
|
||||
<com.simplemobiletools.camera.views.AutoFitTextureView
|
||||
android:id="@+id/camera_texture_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
<androidx.camera.view.PreviewView
|
||||
android:id="@+id/view_finder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/capture_black_screen"
|
||||
|
|
Loading…
Reference in New Issue