diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt index a85f5d43..13cc1416 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/activities/MainActivity.kt @@ -2,6 +2,7 @@ package com.simplemobiletools.camera.activities import android.app.Activity import android.content.Intent +import android.content.res.ColorStateList import android.graphics.Bitmap import android.hardware.SensorManager import android.net.Uri @@ -10,38 +11,59 @@ import android.os.Handler import android.os.Looper import android.provider.MediaStore import android.view.* -import android.widget.RelativeLayout +import android.view.animation.OvershootInterpolator +import android.widget.LinearLayout +import androidx.appcompat.content.res.AppCompatResources +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.view.* +import androidx.transition.* import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestOptions +import com.google.android.material.button.MaterialButton +import com.google.android.material.tabs.TabLayout import com.simplemobiletools.camera.BuildConfig import com.simplemobiletools.camera.R import com.simplemobiletools.camera.extensions.config +import com.simplemobiletools.camera.extensions.toFlashModeId import com.simplemobiletools.camera.helpers.* import com.simplemobiletools.camera.implementations.CameraXInitializer import com.simplemobiletools.camera.implementations.CameraXPreviewListener import com.simplemobiletools.camera.implementations.MyCameraImpl import com.simplemobiletools.camera.interfaces.MyPreview +import com.simplemobiletools.camera.models.ResolutionOption import com.simplemobiletools.camera.views.FocusCircleView import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.models.Release import java.util.concurrent.TimeUnit import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.layout_flash.flash_auto +import kotlinx.android.synthetic.main.layout_flash.flash_off +import kotlinx.android.synthetic.main.layout_flash.flash_on +import kotlinx.android.synthetic.main.layout_flash.flash_toggle_group +import kotlinx.android.synthetic.main.layout_media_size.media_size_toggle_group +import kotlinx.android.synthetic.main.layout_top.change_resolution +import kotlinx.android.synthetic.main.layout_top.default_icons +import kotlinx.android.synthetic.main.layout_top.settings +import kotlinx.android.synthetic.main.layout_top.toggle_flash class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, CameraXPreviewListener { - companion object { - private const val FADE_DELAY = 5000L - private const val CAPTURE_ANIMATION_DURATION = 100L + private companion object { + const val CAPTURE_ANIMATION_DURATION = 500L + const val PHOTO_MODE_INDEX = 1 + const val VIDEO_MODE_INDEX = 0 } lateinit var mTimerHandler: Handler + private lateinit var defaultScene: Scene + private lateinit var mediaSizeScene: Scene + private lateinit var flashModeScene: Scene private lateinit var mOrientationEventListener: OrientationEventListener private lateinit var mFocusCircleView: FocusCircleView - private lateinit var mFadeHandler: Handler private lateinit var mCameraImpl: MyCameraImpl - private var mPreview: MyPreview? = null private var mPreviewUri: Uri? = null private var mIsInPhotoMode = true @@ -50,23 +72,65 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera private var mCurrVideoRecTimer = 0 var mLastHandledOrientation = 0 + private val tabSelectedListener = object : TabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + handleTogglePhotoVideo() + } + } + override fun onCreate(savedInstanceState: Bundle?) { - window.addFlags( - WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or - WindowManager.LayoutParams.FLAG_FULLSCREEN - ) useDynamicTheme = false super.onCreate(savedInstanceState) appLaunched(BuildConfig.APPLICATION_ID) requestWindowFeature(Window.FEATURE_NO_TITLE) + WindowCompat.setDecorFitsSystemWindows(window, false) initVariables() tryInitCamera() supportActionBar?.hide() checkWhatsNewDialog() setupOrientationEventListener() + + val windowInsetsController = ViewCompat.getWindowInsetsController(window.decorView) + windowInsetsController?.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + windowInsetsController?.hide(WindowInsetsCompat.Type.statusBars()) + + if (isOreoMr1Plus()) { + setShowWhenLocked(true) + setTurnScreenOn(true) + } else { + window.addFlags( + WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or + WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or + WindowManager.LayoutParams.FLAG_FULLSCREEN + ) + } + + } + + private fun selectPhotoTab(triggerListener: Boolean = false) { + if (!triggerListener) { + removeTabListener() + } + camera_mode_tab.getTabAt(PHOTO_MODE_INDEX)?.select() + setTabListener() + } + + private fun selectVideoTab(triggerListener: Boolean = false) { + if (!triggerListener) { + removeTabListener() + } + camera_mode_tab.getTabAt(VIDEO_MODE_INDEX)?.select() + setTabListener() + } + + private fun setTabListener() { + camera_mode_tab.addOnTabSelectedListener(tabSelectedListener) + } + + private fun removeTabListener() { + camera_mode_tab.removeOnTabSelectedListener(tabSelectedListener) } override fun onResume() { @@ -74,7 +138,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera if (hasStorageAndCameraPermissions()) { resumeCameraItems() setupPreviewImage(mIsInPhotoMode) - scheduleFadeOut() mFocusCircleView.setStrokeColor(getProperPrimaryColor()) if (isVideoCaptureIntent() && mIsInPhotoMode) { @@ -82,11 +145,14 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera checkButtons() } toggleBottomButtons(false) - } - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - if (hasStorageAndCameraPermissions()) { mOrientationEventListener.enable() } + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + ensureTransparentNavigationBar() + } + + private fun ensureTransparentNavigationBar() { + window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent) } override fun onPause() { @@ -96,8 +162,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera return } - mFadeHandler.removeCallbacksAndMessages(null) - hideTimer() mOrientationEventListener.disable() } @@ -107,6 +171,12 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera mPreview = null } + override fun onBackPressed() { + if (!closeOptions()) { + super.onBackPressed() + } + } + private fun initVariables() { mIsInPhotoMode = if (isVideoCaptureIntent()) { false @@ -145,9 +215,9 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } private fun hideIntentButtons() { - toggle_photo_video.beGone() + camera_mode_tab.beGone() settings.beGone() - last_photo_video_preview.beGone() + last_photo_video_preview.beInvisible() } private fun tryInitCamera() { @@ -180,6 +250,8 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } } + private fun is3rdPartyIntent() = isVideoCaptureIntent() || isImageCaptureIntent() + private fun isImageCaptureIntent(): Boolean = intent?.action == MediaStore.ACTION_IMAGE_CAPTURE || intent?.action == MediaStore.ACTION_IMAGE_CAPTURE_SECURE private fun isVideoCaptureIntent(): Boolean = intent?.action == MediaStore.ACTION_VIDEO_CAPTURE @@ -195,7 +267,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } private fun checkVideoCaptureIntent() { - if (intent?.action == MediaStore.ACTION_VIDEO_CAPTURE) { + if (isVideoCaptureIntent()) { mIsInPhotoMode = false hideIntentButtons() shutter.setImageResource(R.drawable.ic_video_rec) @@ -206,16 +278,37 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera setContentView(R.layout.activity_main) initButtons() - (btn_holder.layoutParams as RelativeLayout.LayoutParams).setMargins( - 0, - 0, - 0, - (navigationBarHeight + resources.getDimension(R.dimen.activity_margin)).toInt() - ) + defaultScene = Scene(top_options, default_icons) + mediaSizeScene = Scene(top_options, media_size_toggle_group) + flashModeScene = Scene(top_options, flash_toggle_group) + + ViewCompat.setOnApplyWindowInsetsListener(view_holder) { _, windowInsets -> + val safeInsetBottom = windowInsets.displayCutout?.safeInsetBottom ?: 0 + val safeInsetTop = windowInsets.displayCutout?.safeInsetTop ?: 0 + + top_options.updateLayoutParams { + topMargin = safeInsetTop + } + + val marginBottom = safeInsetBottom + navigationBarHeight + resources.getDimensionPixelSize(R.dimen.bigger_margin) + (shutter.layoutParams as ConstraintLayout.LayoutParams).goneBottomMargin = marginBottom + + video_rec_curr_timer.updateLayoutParams { + bottomMargin = marginBottom + } + + WindowInsetsCompat.CONSUMED + } checkVideoCaptureIntent() + if (mIsInPhotoMode) { + selectPhotoTab() + } else { + selectVideoTab() + } + val outputUri = intent.extras?.get(MediaStore.EXTRA_OUTPUT) as? Uri - val is3rdPartyIntent = isVideoCaptureIntent() || isImageCaptureIntent() + val is3rdPartyIntent = is3rdPartyIntent() mPreview = CameraXInitializer(this).createCameraXPreview( preview_view, listener = this, @@ -234,12 +327,19 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera view_holder.addView(mFocusCircleView) mTimerHandler = Handler(Looper.getMainLooper()) - mFadeHandler = Handler(Looper.getMainLooper()) setupPreviewImage(true) val initialFlashlightState = FLASH_OFF mPreview!!.setFlashlightState(initialFlashlightState) updateFlashlightState(initialFlashlightState) + initFlashModeTransitionNames() + } + + private fun initFlashModeTransitionNames() { + val baseName = getString(R.string.toggle_flash) + flash_auto.transitionName = "$baseName$FLASH_AUTO" + flash_off.transitionName = "$baseName$FLASH_OFF" + flash_on.transitionName = "$baseName$FLASH_ON" } private fun initButtons() { @@ -248,8 +348,15 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera toggle_flash.setOnClickListener { toggleFlash() } shutter.setOnClickListener { shutterPressed() } settings.setOnClickListener { launchSettings() } - toggle_photo_video.setOnClickListener { handleTogglePhotoVideo() } - change_resolution.setOnClickListener { mPreview?.showChangeResolutionDialog() } + change_resolution.setOnClickListener { mPreview?.showChangeResolution() } + flash_on.setOnClickListener { selectFlashMode(FLASH_ON) } + flash_off.setOnClickListener { selectFlashMode(FLASH_OFF) } + flash_auto.setOnClickListener { selectFlashMode(FLASH_AUTO) } + } + + private fun selectFlashMode(flashMode: Int) { + closeOptions() + mPreview?.setFlashlightState(flashMode) } private fun toggleCamera() { @@ -267,7 +374,11 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera private fun toggleFlash() { if (checkCameraAvailable()) { - mPreview?.toggleFlashlight() + if (mIsInPhotoMode) { + showFlashOptions(mIsInPhotoMode) + } else { + mPreview?.toggleFlashlight() + } } } @@ -279,6 +390,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera else -> R.drawable.ic_flash_auto_vector } toggle_flash.setImageResource(flashDrawable) + toggle_flash.transitionName = "${getString(R.string.toggle_flash)}$state" } private fun shutterPressed() { @@ -290,22 +402,18 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera private fun handleShutter() { if (mIsInPhotoMode) { toggleBottomButtons(true) + change_resolution.isEnabled = true mPreview?.tryTakePicture() - capture_black_screen.animate().alpha(0.8f).setDuration(CAPTURE_ANIMATION_DURATION).withEndAction { - capture_black_screen.animate().alpha(0f).setDuration(CAPTURE_ANIMATION_DURATION).start() - }.start() + shutter_animation.alpha = 1.0f + shutter_animation.animate().alpha(0f).setDuration(CAPTURE_ANIMATION_DURATION).start() } else { mPreview?.toggleRecording() } } private fun launchSettings() { - if (settings.alpha == 1f) { - val intent = Intent(applicationContext, SettingsActivity::class.java) - startActivity(intent) - } else { - fadeInButtons() - } + val intent = Intent(applicationContext, SettingsActivity::class.java) + startActivity(intent) } private fun handleTogglePhotoVideo() { @@ -314,6 +422,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera togglePhotoVideo() } else { toast(R.string.no_audio_permissions) + selectPhotoTab() if (isVideoCaptureIntent()) { finish() } @@ -351,10 +460,10 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } private fun initPhotoMode() { - toggle_photo_video.setImageResource(R.drawable.ic_video_vector) - shutter.setImageResource(R.drawable.ic_shutter_vector) + shutter.setImageResource(R.drawable.ic_shutter_animated) mPreview?.initPhotoMode() setupPreviewImage(true) + selectPhotoTab() } private fun tryInitVideoMode() { @@ -369,10 +478,10 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } private fun initVideoButtons() { - toggle_photo_video.setImageResource(R.drawable.ic_camera_vector) - shutter.setImageResource(R.drawable.ic_video_rec) + shutter.setImageResource(R.drawable.ic_video_rec_animated) setupPreviewImage(false) mPreview?.checkFlashlight() + selectVideoTab() } private fun setupPreviewImage(isPhoto: Boolean) { @@ -405,43 +514,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } } - private fun scheduleFadeOut() { - if (!config.keepSettingsVisible) { - mFadeHandler.postDelayed({ - fadeOutButtons() - }, FADE_DELAY) - } - } - - private fun fadeOutButtons() { - fadeAnim(settings, .5f) - fadeAnim(toggle_photo_video, .0f) - fadeAnim(change_resolution, .0f) - fadeAnim(last_photo_video_preview, .0f) - } - - private fun fadeInButtons() { - fadeAnim(settings, 1f) - fadeAnim(toggle_photo_video, 1f) - fadeAnim(change_resolution, 1f) - fadeAnim(last_photo_video_preview, 1f) - scheduleFadeOut() - } - - private fun fadeAnim(view: View, value: Float) { - view.animate().alpha(value).start() - view.isClickable = value != .0f - } - - @Suppress("DEPRECATION") - private fun hideNavigationBarIcons() { - if (isRPlus()) { - window.insetsController?.hide(WindowInsets.Type.systemBars()) - } else { - window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE - } - } - private fun showTimer() { video_rec_curr_timer.beVisible() setupTimer() @@ -464,8 +536,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } private fun resumeCameraItems() { - hideNavigationBarIcons() - if (!mIsInPhotoMode) { initVideoButtons() } @@ -508,7 +578,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } private fun animateViews(degrees: Int) { - val views = arrayOf(toggle_camera, toggle_flash, toggle_photo_video, change_resolution, shutter, settings, last_photo_video_preview) + val views = arrayOf(toggle_camera, toggle_flash, change_resolution, shutter, settings, last_photo_video_preview) for (view in views) { rotate(view, degrees) } @@ -547,11 +617,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera 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 @@ -559,6 +624,7 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } override fun onMediaSaved(uri: Uri) { + change_resolution.isEnabled = true loadLastTakenMedia(uri) if (isImageCaptureIntent()) { Intent().apply { @@ -592,15 +658,19 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera } override fun onVideoRecordingStarted() { - shutter.setImageResource(R.drawable.ic_video_stop) + camera_mode_tab.beInvisible() + shutter.isSelected = true toggle_camera.beInvisible() + change_resolution.isEnabled = false video_rec_curr_timer.beVisible() } override fun onVideoRecordingStopped() { - shutter.setImageResource(R.drawable.ic_video_rec) + camera_mode_tab.beVisible() + shutter.isSelected = false video_rec_curr_timer.text = 0.getFormattedDuration() video_rec_curr_timer.beGone() + change_resolution.isEnabled = true toggle_camera.beVisible() } @@ -613,14 +683,127 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera mFocusCircleView.drawFocusCircle(xPos, yPos) } + override fun onSwipeLeft() { + if (!is3rdPartyIntent() && camera_mode_tab.isVisible()) { + selectPhotoTab(triggerListener = true) + } + } + + override fun onSwipeRight() { + if (!is3rdPartyIntent() && camera_mode_tab.isVisible()) { + selectVideoTab(triggerListener = true) + } + } + + override fun onTouchPreview() { + closeOptions() + } + + private fun closeOptions(): Boolean { + if (media_size_toggle_group.isVisible() || + flash_toggle_group.isVisible() + ) { + val transitionSet = createTransition(isClosing = true) + TransitionManager.go(defaultScene, transitionSet) + return true + } + + return false + } + + override fun displaySelectedResolution(resolutionOption: ResolutionOption) { + val imageRes = resolutionOption.imageDrawableResId + change_resolution.setImageResource(imageRes) + change_resolution.transitionName = "${resolutionOption.buttonViewId}" + } + + override fun showImageSizes( + selectedResolution: ResolutionOption, + resolutions: List, + isPhotoCapture: Boolean, + isFrontCamera: Boolean, + onSelect: (index: Int, changed: Boolean) -> Unit + ) { + + media_size_toggle_group.removeAllViews() + media_size_toggle_group.clearChecked() + + val onItemClick = { clickedViewId: Int -> + closeOptions() + val index = resolutions.indexOfFirst { it.buttonViewId == clickedViewId } + onSelect.invoke(index, selectedResolution.buttonViewId != clickedViewId) + } + + resolutions.map { + createButton(it, onItemClick) + }.forEach { button -> + media_size_toggle_group.addView(button) + } + + media_size_toggle_group.check(selectedResolution.buttonViewId) + showResolutionOptions() + } + + private fun createButton(resolutionOption: ResolutionOption, onClick: (clickedViewId: Int) -> Unit): MaterialButton { + val params = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT).apply { + weight = 1f + } + return (layoutInflater.inflate(R.layout.layout_button, null) as MaterialButton).apply { + layoutParams = params + icon = AppCompatResources.getDrawable(context, resolutionOption.imageDrawableResId) + id = resolutionOption.buttonViewId + transitionName = "${resolutionOption.buttonViewId}" + setOnClickListener { + onClick.invoke(id) + } + } + } + + private fun showResolutionOptions() { + val transitionSet = createTransition() + TransitionManager.go(mediaSizeScene, transitionSet) + media_size_toggle_group.children.map { it as MaterialButton }.forEach(::setButtonColors) + } + + private fun createTransition(isClosing: Boolean = false): Transition { + val fadeTransition = Fade() + val changeBounds = ChangeBounds().apply { + interpolator = OvershootInterpolator() + } + return TransitionSet().apply { + if (!isClosing) { + addTransition(changeBounds) + } + addTransition(fadeTransition) + this.duration = resources.getInteger(R.integer.icon_anim_duration).toLong() + } + } + + override fun showFlashOptions(photoCapture: Boolean) { + val transitionSet = createTransition() + TransitionManager.go(flashModeScene, transitionSet) + flash_auto.beVisibleIf(photoCapture) + flash_toggle_group.check(config.flashlightState.toFlashModeId()) + + flash_toggle_group.beVisible() + flash_toggle_group.children.map { it as MaterialButton }.forEach(::setButtonColors) + } + + private fun setButtonColors(button: MaterialButton) { + val primaryColor = getProperPrimaryColor() + val states = arrayOf(intArrayOf(-android.R.attr.state_checked), intArrayOf(android.R.attr.state_checked)) + val iconColors = intArrayOf(ContextCompat.getColor(this, R.color.md_grey_white), primaryColor) + button.iconTint = ColorStateList(states, iconColors) + } + fun setRecordingState(isRecording: Boolean) { runOnUiThread { if (isRecording) { - shutter.setImageResource(R.drawable.ic_video_stop) + shutter.isSelected = true toggle_camera.beInvisible() showTimer() } else { - shutter.setImageResource(R.drawable.ic_video_rec) + shutter.isSelected = false hideTimer() } } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/activities/SettingsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/camera/activities/SettingsActivity.kt index 4dcd33c2..9b55c767 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/activities/SettingsActivity.kt @@ -34,7 +34,6 @@ class SettingsActivity : SimpleActivity() { setupSound() setupVolumeButtonsAsShutter() setupFlipPhotos() - setupKeepSettingsVisible() setupSavePhotoMetadata() setupSavePhotosFolder() setupPhotoQuality() @@ -94,9 +93,8 @@ class SettingsActivity : SimpleActivity() { settings_use_english_holder.beVisibleIf(config.wasUseEnglishToggled || Locale.getDefault().language != "en") settings_use_english.isChecked = config.useEnglish - if (settings_use_english_holder.isGone() && settings_purchase_thank_you_holder.isGone()) { - settings_keep_settings_visible_holder.background = ResourcesCompat.getDrawable(resources, R.drawable.ripple_all_corners, theme) - } + settings_general_settings_holder.beGoneIf(settings_use_english_holder.isGone() && settings_purchase_thank_you_holder.isGone()) + settings_general_settings_label.beGoneIf(settings_use_english_holder.isGone() && settings_purchase_thank_you_holder.isGone()) settings_use_english_holder.setOnClickListener { settings_use_english.toggle() @@ -149,14 +147,6 @@ class SettingsActivity : SimpleActivity() { } } - private fun setupKeepSettingsVisible() { - settings_keep_settings_visible.isChecked = config.keepSettingsVisible - settings_keep_settings_visible_holder.setOnClickListener { - settings_keep_settings_visible.toggle() - config.keepSettingsVisible = settings_keep_settings_visible.isChecked - } - } - private fun setupSavePhotoMetadata() { settings_save_photo_metadata.isChecked = config.savePhotoMetadata settings_save_photo_metadata_holder.setOnClickListener { diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/dialogs/ChangeResolutionDialogX.kt b/app/src/main/kotlin/com/simplemobiletools/camera/dialogs/ChangeResolutionDialogX.kt index 92e83085..cc04517e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/dialogs/ChangeResolutionDialogX.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/dialogs/ChangeResolutionDialogX.kt @@ -47,7 +47,12 @@ class ChangeResolutionDialogX( val items = photoResolutions.mapIndexed { index, resolution -> val megapixels = resolution.megaPixels val aspectRatio = resolution.getAspectRatio(activity) - RadioItem(index, "${resolution.width} x ${resolution.height} ($megapixels MP, $aspectRatio)") + if (resolution.isFullScreen) { + //TODO: Extract to string resource + RadioItem(index, "Full") + } else { + RadioItem(index, "${resolution.width} x ${resolution.height} ($megapixels MP, $aspectRatio)") + } } var selectionIndex = if (isFrontCamera) config.frontPhotoResIndex else config.backPhotoResIndex selectionIndex = selectionIndex.coerceAtLeast(0) diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/extensions/Int.kt b/app/src/main/kotlin/com/simplemobiletools/camera/extensions/Int.kt index 4cf4caf7..6f13a1ff 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/extensions/Int.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/extensions/Int.kt @@ -2,10 +2,10 @@ package com.simplemobiletools.camera.extensions import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture +import com.simplemobiletools.camera.R 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) { @@ -25,6 +25,24 @@ fun Int.toAppFlashMode(): Int { } } +fun Int.toFlashModeId(): Int { + return when (this) { + FLASH_ON -> R.id.flash_on + FLASH_OFF -> R.id.flash_off + FLASH_AUTO -> R.id.flash_auto + else -> throw IllegalArgumentException("Unknown mode: $this") + } +} + +fun Int.idToAppFlashMode(): Int { + return when (this) { + R.id.flash_on -> FLASH_ON + R.id.flash_off -> FLASH_OFF + R.id.flash_auto -> FLASH_AUTO + else -> throw IllegalArgumentException("Unknown mode: $this") + } +} + fun Int.toCameraSelector(): CameraSelector { return if (this == CameraSelector.LENS_FACING_FRONT) { CameraSelector.DEFAULT_FRONT_CAMERA diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Config.kt index 5ede2c12..ec229691 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/Config.kt @@ -66,10 +66,6 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getInt(FRONT_VIDEO_RESOLUTION_INDEX, 0) set(frontVideoResIndex) = prefs.edit().putInt(FRONT_VIDEO_RESOLUTION_INDEX, frontVideoResIndex).apply() - var keepSettingsVisible: Boolean - get() = prefs.getBoolean(KEEP_SETTINGS_VISIBLE, false) - set(keepSettingsVisible) = prefs.edit().putBoolean(KEEP_SETTINGS_VISIBLE, keepSettingsVisible).apply() - var savePhotoMetadata: Boolean get() = prefs.getBoolean(SAVE_PHOTO_METADATA, true) set(savePhotoMetadata) = prefs.edit().putBoolean(SAVE_PHOTO_METADATA, savePhotoMetadata).apply() diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/ImageQualityManager.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/ImageQualityManager.kt index b92e5867..9177c781 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/ImageQualityManager.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/ImageQualityManager.kt @@ -20,21 +20,20 @@ class ImageQualityManager( } private val cameraManager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager - private val config = activity.config private val imageQualities = mutableListOf() + private val mediaSizeStore = MediaSizeStore(activity.config) fun initSupportedQualities() { if (imageQualities.isEmpty()) { for (cameraId in cameraManager.cameraIdList) { try { val characteristics = cameraManager.getCameraCharacteristics(cameraId) - for (lens in CAMERA_LENS) { - if (characteristics.get(CameraCharacteristics.LENS_FACING) == lens) { - val configMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: continue - val imageSizes = configMap.getOutputSizes(ImageFormat.JPEG).map { MySize(it.width, it.height) } - val cameraSelector = lens.toCameraSelector() - imageQualities.add(CameraSelectorImageQualities(cameraSelector, imageSizes)) - } + val lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING) ?: continue + if (lensFacing in CAMERA_LENS) { + val configMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: continue + val imageSizes = configMap.getOutputSizes(ImageFormat.JPEG).map { MySize(it.width, it.height) } + val cameraSelector = lensFacing.toCameraSelector() + imageQualities.add(CameraSelectorImageQualities(cameraSelector, imageSizes)) } } catch (e: Exception) { activity.showErrorToast(e) @@ -52,19 +51,27 @@ class ImageQualityManager( } fun getUserSelectedResolution(cameraSelector: CameraSelector): MySize { - val index = if (cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) config.frontPhotoResIndex else config.backPhotoResIndex - return imageQualities.filter { it.camSelector == cameraSelector } - .flatMap { it.qualities } - .sortedWith(compareByDescending { it.ratio }.thenByDescending { it.pixels }) - .distinctBy { it.pixels } - .filter { it.megaPixels != "0.0" }[index] + val resolutions = getSupportedResolutions(cameraSelector) + val isFrontCamera = cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA + var index = mediaSizeStore.getCurrentSizeIndex(isPhotoCapture = true, isFrontCamera = isFrontCamera) + index = index.coerceAtMost(resolutions.lastIndex).coerceAtLeast(0) + return resolutions[index] } fun getSupportedResolutions(cameraSelector: CameraSelector): List { + val fullScreenSize = getFullScreenResolution(cameraSelector) + return listOf(fullScreenSize) + imageQualities.filter { it.camSelector == cameraSelector } + .flatMap { it.qualities } + .sortedByDescending { it.pixels } + .distinctBy { it.getAspectRatio(activity) } + .filter { it.isSupported(fullScreenSize.isSixteenToNine()) } + } + + private fun getFullScreenResolution(cameraSelector: CameraSelector): MySize { return imageQualities.filter { it.camSelector == cameraSelector } .flatMap { it.qualities } - .sortedWith(compareByDescending { it.ratio }.thenByDescending { it.pixels }) - .distinctBy { it.pixels } - .filter { it.megaPixels != "0.0" } + .sortedByDescending { it.width } + .first { it.isSupported(false) } + .copy(isFullScreen = true) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSizeStore.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSizeStore.kt new file mode 100644 index 00000000..8702eb66 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/MediaSizeStore.kt @@ -0,0 +1,38 @@ +package com.simplemobiletools.camera.helpers + +class MediaSizeStore( + private val config: Config, +) { + + fun storeSize(isPhotoCapture: Boolean, isFrontCamera: Boolean, currentIndex: Int) { + if (isPhotoCapture) { + if (isFrontCamera) { + config.frontPhotoResIndex = currentIndex + } else { + config.backPhotoResIndex = currentIndex + } + } else { + if (isFrontCamera) { + config.frontVideoResIndex = currentIndex + } else { + config.backVideoResIndex = currentIndex + } + } + } + + fun getCurrentSizeIndex(isPhotoCapture: Boolean, isFrontCamera: Boolean): Int { + return if (isPhotoCapture) { + if (isFrontCamera) { + config.frontPhotoResIndex + } else { + config.backPhotoResIndex + } + } else { + if (isFrontCamera) { + config.frontVideoResIndex + } else { + config.backVideoResIndex + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/TabSelectedListener.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/TabSelectedListener.kt new file mode 100644 index 00000000..361729ec --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/TabSelectedListener.kt @@ -0,0 +1,9 @@ +package com.simplemobiletools.camera.helpers + +import com.google.android.material.tabs.TabLayout + +interface TabSelectedListener : TabLayout.OnTabSelectedListener { + override fun onTabReselected(tab: TabLayout.Tab?){} + + override fun onTabUnselected(tab: TabLayout.Tab?) {} +} diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/VideoQualityManager.kt b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/VideoQualityManager.kt index 0a3d784a..d768deec 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/helpers/VideoQualityManager.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/helpers/VideoQualityManager.kt @@ -20,8 +20,8 @@ class VideoQualityManager( private val CAMERA_SELECTORS = arrayOf(CameraSelector.DEFAULT_BACK_CAMERA, CameraSelector.DEFAULT_FRONT_CAMERA) } - private val config = activity.config private val videoQualities = mutableListOf() + private val mediaSizeStore = MediaSizeStore(activity.config) fun initSupportedQualities(cameraProvider: ProcessCameraProvider) { if (videoQualities.isEmpty()) { @@ -45,8 +45,8 @@ class VideoQualityManager( } fun getUserSelectedQuality(cameraSelector: CameraSelector): VideoQuality { - var selectionIndex = if (cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) config.frontVideoResIndex else config.backVideoResIndex - selectionIndex = selectionIndex.coerceAtLeast(0) + val isFrontCamera = cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA + val selectionIndex = mediaSizeStore.getCurrentSizeIndex(isPhotoCapture = false, isFrontCamera = isFrontCamera).coerceAtLeast(0) return getSupportedQualities(cameraSelector).getOrElse(selectionIndex) { VideoQuality.HD } } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt index 42a6927f..471dfb51 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreview.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import android.hardware.SensorManager import android.hardware.display.DisplayManager +import android.util.Rational import android.util.Size import android.view.* import android.view.GestureDetector.SimpleOnGestureListener @@ -14,10 +15,12 @@ import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.video.* import androidx.camera.video.VideoCapture import androidx.camera.view.PreviewView +import androidx.camera.view.PreviewView.ScaleType import androidx.core.content.ContextCompat 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.dialogs.ChangeResolutionDialogX @@ -26,8 +29,10 @@ import com.simplemobiletools.camera.helpers.* import com.simplemobiletools.camera.interfaces.MyPreview import com.simplemobiletools.camera.models.MediaOutput import com.simplemobiletools.camera.models.MySize +import com.simplemobiletools.camera.models.ResolutionOption import com.simplemobiletools.commons.extensions.toast import com.simplemobiletools.commons.helpers.ensureBackgroundThread +import kotlin.math.abs class CameraXPreview( private val activity: AppCompatActivity, @@ -42,6 +47,7 @@ class CameraXPreview( // 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 const val MIN_SWIPE_DISTANCE_X = 100 } private val config = activity.config @@ -49,9 +55,11 @@ class CameraXPreview( private val mainExecutor = ContextCompat.getMainExecutor(activity) private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager private val mediaSoundHelper = MediaSoundHelper() + private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate() private val videoQualityManager = VideoQualityManager(activity) private val imageQualityManager = ImageQualityManager(activity) private val exifRemover = ExifRemover(contentResolver) + private val mediaSizeStore = MediaSizeStore(config) private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { @SuppressLint("RestrictedApi") @@ -120,30 +128,110 @@ class CameraXPreview( private fun bindCameraUseCases() { val cameraProvider = cameraProvider ?: throw IllegalStateException("Camera initialization failed.") - val rotation = previewView.display.rotation + val resolution = if (isPhotoCapture) { - imageQualityManager.getUserSelectedResolution(cameraSelector) + imageQualityManager.getUserSelectedResolution(cameraSelector).also { + displaySelectedResolution(it.toResolutionOption()) + } } else { - val selectedQuality = videoQualityManager.getUserSelectedQuality(cameraSelector) + val selectedQuality = videoQualityManager.getUserSelectedQuality(cameraSelector).also { + displaySelectedResolution(it.toResolutionOption()) + } MySize(selectedQuality.width, selectedQuality.height) } + val isFullSize = resolution.isFullScreen + previewView.scaleType = if (isFullSize) ScaleType.FILL_CENTER else ScaleType.FIT_CENTER + val rotation = previewView.display.rotation val rotatedResolution = getRotatedResolution(resolution, rotation) - preview = buildPreview(rotatedResolution, rotation) + val previewUseCase = buildPreview(rotatedResolution, rotation) val captureUseCase = getCaptureUseCase(rotatedResolution, rotation) - cameraProvider.unbindAll() - camera = cameraProvider.bindToLifecycle( - activity, - cameraSelector, - preview, - captureUseCase, - ) - preview?.setSurfaceProvider(previewView.surfaceProvider) + cameraProvider.unbindAll() + camera = if (isFullSize) { + val metrics = windowMetricsCalculator.computeCurrentWindowMetrics(activity).bounds + val screenWidth = metrics.width() + val screenHeight = metrics.height() + val viewPort = ViewPort.Builder(Rational(screenWidth, screenHeight), rotation).build() + + val useCaseGroup = UseCaseGroup.Builder() + .addUseCase(previewUseCase) + .addUseCase(captureUseCase) + .setViewPort(viewPort) + .build() + + cameraProvider.bindToLifecycle( + activity, + cameraSelector, + useCaseGroup, + ) + } else { + cameraProvider.bindToLifecycle( + activity, + cameraSelector, + previewUseCase, + captureUseCase, + ) + } + + previewUseCase.setSurfaceProvider(previewView.surfaceProvider) + preview = previewUseCase setupZoomAndFocus() } + private fun displaySelectedResolution(resolutionOption: ResolutionOption) { + listener.displaySelectedResolution(resolutionOption) + } + + private fun getRotatedResolution(resolution: MySize, rotationDegrees: Int): Size { + return if (rotationDegrees == Surface.ROTATION_0 || rotationDegrees == Surface.ROTATION_180) { + Size(resolution.height, resolution.width) + } else { + Size(resolution.width, resolution.height) + } + } + + private fun buildPreview(resolution: Size, rotation: Int): Preview { + return Preview.Builder() + .setTargetRotation(rotation) + .setTargetResolution(resolution) + .build() + } + + private fun getCaptureUseCase(resolution: Size, rotation: Int): UseCase { + return if (isPhotoCapture) { + buildImageCapture(resolution, rotation).also { + imageCapture = it + } + } else { + buildVideoCapture().also { + videoCapture = it + } + } + } + + private fun buildImageCapture(resolution: Size, rotation: Int): ImageCapture { + return Builder() + .setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY) + .setFlashMode(flashMode) + .setJpegQuality(config.photoQuality) + .setTargetRotation(rotation) + .setTargetResolution(resolution) + .build() + } + + private fun buildVideoCapture(): VideoCapture { + val qualitySelector = QualitySelector.from( + videoQualityManager.getUserSelectedQuality(cameraSelector).toCameraXQuality(), + FallbackStrategy.higherQualityOrLowerThan(Quality.SD), + ) + val recorder = Recorder.Builder() + .setQualitySelector(qualitySelector) + .build() + return VideoCapture.withOutput(recorder) + } + private fun setupCameraObservers() { listener.setFlashAvailable(camera?.cameraInfo?.hasFlashUnit() ?: false) listener.onChangeCamera(isFrontCameraInUse()) @@ -166,56 +254,6 @@ class CameraXPreview( } } - private fun getCaptureUseCase(resolution: Size, rotation: Int): UseCase { - return if (isPhotoCapture) { - cameraProvider?.unbind(videoCapture) - buildImageCapture(resolution, rotation).also { - imageCapture = it - } - } else { - cameraProvider?.unbind(imageCapture) - buildVideoCapture().also { - videoCapture = it - } - } - } - - private fun buildImageCapture(resolution: Size, rotation: Int): ImageCapture { - return Builder() - .setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY) - .setFlashMode(flashMode) - .setJpegQuality(config.photoQuality) - .setTargetRotation(rotation) - .setTargetResolution(resolution) - .build() - } - - private fun getRotatedResolution(resolution: MySize, rotationDegrees: Int): Size { - return if (rotationDegrees == Surface.ROTATION_0 || rotationDegrees == Surface.ROTATION_180) { - Size(resolution.height, resolution.width) - } else { - Size(resolution.width, resolution.height) - } - } - - private fun buildPreview(resolution: Size, rotation: Int): Preview { - return Preview.Builder() - .setTargetRotation(rotation) - .setTargetResolution(resolution) - .build() - } - - private fun buildVideoCapture(): VideoCapture { - val qualitySelector = QualitySelector.from( - videoQualityManager.getUserSelectedQuality(cameraSelector).toCameraXQuality(), - FallbackStrategy.higherQualityOrLowerThan(Quality.SD), - ) - val recorder = Recorder.Builder() - .setQualitySelector(qualitySelector) - .build() - return VideoCapture.withOutput(recorder) - } - private fun hasBackCamera(): Boolean { return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false } @@ -233,6 +271,11 @@ class CameraXPreview( private fun setupZoomAndFocus() { val scaleGesture = camera?.let { ScaleGestureDetector(activity, PinchToZoomOnScaleGestureListener(it.cameraInfo, it.cameraControl)) } val gestureDetector = GestureDetector(activity, object : SimpleOnGestureListener() { + override fun onDown(e: MotionEvent?): Boolean { + listener.onTouchPreview() + return super.onDown(e) + } + override fun onSingleTapConfirmed(event: MotionEvent): Boolean { return camera?.cameraInfo?.let { val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) @@ -252,11 +295,26 @@ class CameraXPreview( true } ?: false } + + override fun onFling(event1: MotionEvent, event2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { + val deltaX = event1.x - event2.x + val deltaXAbs = abs(deltaX) + + if (deltaXAbs >= MIN_SWIPE_DISTANCE_X) { + if (deltaX > 0) { + listener.onSwipeLeft() + } else { + listener.onSwipeRight() + } + } + + return true + } }) previewView.setOnTouchListener { _, event -> - gestureDetector.onTouchEvent(event) - scaleGesture?.onTouchEvent(event) - true + val handledGesture = gestureDetector.onTouchEvent(event) + val handledScaleGesture = scaleGesture?.onTouchEvent(event) + handledGesture || handledScaleGesture ?: false } } @@ -283,6 +341,52 @@ class CameraXPreview( } } + override fun showChangeResolution() { + + val selectedResolution = if (isPhotoCapture) { + imageQualityManager.getUserSelectedResolution(cameraSelector).toResolutionOption() + } else { + videoQualityManager.getUserSelectedQuality(cameraSelector).toResolutionOption() + } + val resolutions = if (isPhotoCapture) { + imageQualityManager.getSupportedResolutions(cameraSelector).map { it.toResolutionOption() } + } else { + videoQualityManager.getSupportedQualities(cameraSelector).map { it.toResolutionOption() } + } + + if (resolutions.size > 2) { + listener.showImageSizes( + selectedResolution = selectedResolution, + resolutions = resolutions, + isPhotoCapture = isPhotoCapture, + isFrontCamera = isFrontCameraInUse() + ) { index, changed -> + mediaSizeStore.storeSize(isPhotoCapture, isFrontCameraInUse(), index) + if (changed) { + currentRecording?.stop() + startCamera() + } + } + } else { + toggleResolutions(resolutions) + } + } + + private fun toggleResolutions(resolutions: List) { + val currentIndex = mediaSizeStore.getCurrentSizeIndex(isPhotoCapture, isFrontCameraInUse()) + + val nextIndex = if (currentIndex >= resolutions.lastIndex) { + 0 + } else { + currentIndex + 1 + } + + mediaSizeStore.storeSize(isPhotoCapture, isFrontCameraInUse(), nextIndex) + currentRecording?.stop() + startCamera() + + } + override fun toggleFrontBackCamera() { val newCameraSelector = if (isFrontCameraInUse()) { CameraSelector.DEFAULT_BACK_CAMERA @@ -307,10 +411,16 @@ class CameraXPreview( FLASH_MODE_OFF -> FLASH_MODE_ON FLASH_MODE_ON -> FLASH_MODE_OFF else -> throw IllegalArgumentException("Unknown mode: $flashMode") - }.also { - camera?.cameraControl?.enableTorch(it == FLASH_MODE_ON) } } + setFlashlightState(newFlashMode.toAppFlashMode()) + } + + override fun setFlashlightState(state: Int) { + val newFlashMode = state.toCameraXFlashMode() + if (!isPhotoCapture) { + camera?.cameraControl?.enableTorch(newFlashMode == FLASH_MODE_ON) + } flashMode = newFlashMode imageCapture?.flashMode = newFlashMode val appFlashMode = flashMode.toAppFlashMode() diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt index 73bf5db5..2e5fe335 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/implementations/CameraXPreviewListener.kt @@ -2,13 +2,14 @@ package com.simplemobiletools.camera.implementations import android.graphics.Bitmap import android.net.Uri +import com.simplemobiletools.camera.models.ResolutionOption interface CameraXPreviewListener { fun setCameraAvailable(available: Boolean) - fun setHasFrontAndBackCamera(hasFrontAndBack:Boolean) + fun setHasFrontAndBackCamera(hasFrontAndBack: Boolean) fun setFlashAvailable(available: Boolean) fun onChangeCamera(frontCamera: Boolean) - fun toggleBottomButtons(hide:Boolean) + fun toggleBottomButtons(hide: Boolean) fun onMediaSaved(uri: Uri) fun onImageCaptured(bitmap: Bitmap) fun onChangeFlashMode(flashMode: Int) @@ -16,4 +17,17 @@ interface CameraXPreviewListener { fun onVideoRecordingStopped() fun onVideoDurationChanged(durationNanos: Long) fun onFocusCamera(xPos: Float, yPos: Float) + fun onSwipeLeft() + fun onSwipeRight() + fun onTouchPreview() + fun displaySelectedResolution(resolutionOption: ResolutionOption) + fun showImageSizes( + selectedResolution: ResolutionOption, + resolutions: List, + isPhotoCapture: Boolean, + isFrontCamera: Boolean, + onSelect: (index: Int, changed: Boolean) -> Unit, + ) + + fun showFlashOptions(photoCapture: Boolean) } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/interfaces/MyPreview.kt b/app/src/main/kotlin/com/simplemobiletools/camera/interfaces/MyPreview.kt index 81125acb..cc670165 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/interfaces/MyPreview.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/interfaces/MyPreview.kt @@ -20,7 +20,7 @@ interface MyPreview { fun toggleFrontBackCamera() - fun toggleFlashlight() + fun toggleFlashlight() = Unit fun tryTakePicture() @@ -31,4 +31,6 @@ interface MyPreview { fun initVideoMode() fun checkFlashlight() = Unit + + fun showChangeResolution() = Unit } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/models/MySize.kt b/app/src/main/kotlin/com/simplemobiletools/camera/models/MySize.kt index bd9702ea..56e587d3 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/models/MySize.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/models/MySize.kt @@ -2,18 +2,21 @@ package com.simplemobiletools.camera.models import android.content.Context import android.util.Size +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes import com.simplemobiletools.camera.R -data class MySize(val width: Int, val height: Int) { +data class MySize(val width: Int, val height: Int, val isFullScreen: Boolean = false) { companion object { private const val ONE_MEGA_PIXEL = 1000000 + private const val ZERO_MEGA_PIXEL = "0.0" } - val ratio = width / height.toFloat() + private val ratio = width / height.toFloat() val pixels: Int = width * height - val megaPixels: String = String.format("%.1f", (width * height.toFloat()) / ONE_MEGA_PIXEL) + val megaPixels: String = String.format("%.1f", (width * height.toFloat()) / ONE_MEGA_PIXEL) fun isSixteenToNine() = ratio == 16 / 9f @@ -37,6 +40,14 @@ data class MySize(val width: Int, val height: Int) { private fun isSquare() = width == height + fun isSupported(isFullScreenSize16x9: Boolean): Boolean { + return if (isFullScreenSize16x9) { + isFourToThree() || isSquare() + } else { + isFourToThree() || isSixteenToNine() || isSquare() + } && megaPixels != ZERO_MEGA_PIXEL + } + fun getAspectRatio(context: Context) = when { isSixteenToNine() -> "16:9" isFiveToThree() -> "5:3" @@ -52,5 +63,27 @@ data class MySize(val width: Int, val height: Int) { else -> context.resources.getString(R.string.other) } + @DrawableRes + fun getImageResId(): Int = when { + isFullScreen -> R.drawable.ic_photo_full + isSixteenToNine() -> R.drawable.ic_photo_16x9 + isFourToThree() -> R.drawable.ic_photo_4x3 + isSquare() -> R.drawable.ic_photo_1x1 + else -> throw UnsupportedOperationException("This size $this is not supported") + } + + @IdRes + fun getButtonId(): Int = when { + isFullScreen -> R.id.photo_full + isSixteenToNine() -> R.id.photo_16x9 + isFourToThree() -> R.id.photo_4x3 + isSquare() -> R.id.photo_1x1 + else -> throw UnsupportedOperationException("This size $this is not supported") + } + + fun toResolutionOption(): ResolutionOption { + return ResolutionOption(buttonViewId = getButtonId(), imageDrawableResId = getImageResId()) + } + fun toSize() = Size(width, height) } diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/models/ResolutionOption.kt b/app/src/main/kotlin/com/simplemobiletools/camera/models/ResolutionOption.kt new file mode 100644 index 00000000..39b9eac6 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/camera/models/ResolutionOption.kt @@ -0,0 +1,9 @@ +package com.simplemobiletools.camera.models + +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes + +data class ResolutionOption( + @IdRes val buttonViewId: Int, + @DrawableRes val imageDrawableResId: Int, +) diff --git a/app/src/main/kotlin/com/simplemobiletools/camera/models/VideoQuality.kt b/app/src/main/kotlin/com/simplemobiletools/camera/models/VideoQuality.kt index ec2a536b..adcfefbd 100644 --- a/app/src/main/kotlin/com/simplemobiletools/camera/models/VideoQuality.kt +++ b/app/src/main/kotlin/com/simplemobiletools/camera/models/VideoQuality.kt @@ -1,6 +1,8 @@ package com.simplemobiletools.camera.models import android.content.Context +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes import com.simplemobiletools.camera.R enum class VideoQuality(val width: Int, val height: Int) { @@ -11,7 +13,7 @@ enum class VideoQuality(val width: Int, val height: Int) { val pixels: Int = width * height - val megaPixels: String = String.format("%.1f", (width * height.toFloat()) / VideoQuality.ONE_MEGA_PIXEL) + val megaPixels: String = String.format("%.1f", (width * height.toFloat()) / VideoQuality.ONE_MEGA_PIXEL) val ratio = width / height.toFloat() @@ -52,6 +54,26 @@ enum class VideoQuality(val width: Int, val height: Int) { else -> context.resources.getString(R.string.other) } + @DrawableRes + fun getImageResId(): Int = when (this) { + UHD -> R.drawable.ic_video_uhd + FHD -> R.drawable.ic_video_fhd + HD -> R.drawable.ic_video_hd + SD -> R.drawable.ic_video_sd + } + + @IdRes + fun getButtonId(): Int = when (this) { + UHD -> R.id.video_uhd + FHD -> R.id.video_fhd + HD -> R.id.video_hd + SD -> R.id.video_sd + } + + fun toResolutionOption(): ResolutionOption { + return ResolutionOption(buttonViewId = getButtonId(), imageDrawableResId = getImageResId()) + } + companion object { private const val ONE_MEGA_PIXEL = 1000000 } diff --git a/app/src/main/res/color/camera_option_color.xml b/app/src/main/res/color/camera_option_color.xml new file mode 100644 index 00000000..1c13eef5 --- /dev/null +++ b/app/src/main/res/color/camera_option_color.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/color/tab_color.xml b/app/src/main/res/color/tab_color.xml new file mode 100644 index 00000000..02947e5a --- /dev/null +++ b/app/src/main/res/color/tab_color.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable-hdpi/ic_video_rec.png b/app/src/main/res/drawable-hdpi/ic_video_rec.png deleted file mode 100644 index 3555237b..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_video_rec.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_video_stop.png b/app/src/main/res/drawable-hdpi/ic_video_stop.png deleted file mode 100644 index 72847bb3..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_video_stop.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_video_rec.png b/app/src/main/res/drawable-xhdpi/ic_video_rec.png deleted file mode 100644 index 6cebd9be..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_video_rec.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_video_stop.png b/app/src/main/res/drawable-xhdpi/ic_video_stop.png deleted file mode 100644 index 4726160d..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_video_stop.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_video_rec.png b/app/src/main/res/drawable-xxhdpi/ic_video_rec.png deleted file mode 100644 index b833e67c..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_video_rec.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_video_stop.png b/app/src/main/res/drawable-xxhdpi/ic_video_stop.png deleted file mode 100644 index c6896893..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_video_stop.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_video_rec.png b/app/src/main/res/drawable-xxxhdpi/ic_video_rec.png deleted file mode 100644 index e53f62fa..00000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_video_rec.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_video_stop.png b/app/src/main/res/drawable-xxxhdpi/ic_video_stop.png deleted file mode 100644 index c3670ced..00000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_video_stop.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_flash_auto_vector.xml b/app/src/main/res/drawable/ic_flash_auto_vector.xml index 308f724c..bdd3aab7 100644 --- a/app/src/main/res/drawable/ic_flash_auto_vector.xml +++ b/app/src/main/res/drawable/ic_flash_auto_vector.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_flash_off_vector.xml b/app/src/main/res/drawable/ic_flash_off_vector.xml index 9d4b374c..1e019ab3 100644 --- a/app/src/main/res/drawable/ic_flash_off_vector.xml +++ b/app/src/main/res/drawable/ic_flash_off_vector.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_flash_on_vector.xml b/app/src/main/res/drawable/ic_flash_on_vector.xml index 57cd3a63..6947cbab 100644 --- a/app/src/main/res/drawable/ic_flash_on_vector.xml +++ b/app/src/main/res/drawable/ic_flash_on_vector.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_photo_16x9.xml b/app/src/main/res/drawable/ic_photo_16x9.xml new file mode 100644 index 00000000..bc210589 --- /dev/null +++ b/app/src/main/res/drawable/ic_photo_16x9.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_photo_1x1.xml b/app/src/main/res/drawable/ic_photo_1x1.xml new file mode 100644 index 00000000..7acddfcd --- /dev/null +++ b/app/src/main/res/drawable/ic_photo_1x1.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_photo_4x3.xml b/app/src/main/res/drawable/ic_photo_4x3.xml new file mode 100644 index 00000000..2907e907 --- /dev/null +++ b/app/src/main/res/drawable/ic_photo_4x3.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_photo_full.xml b/app/src/main/res/drawable/ic_photo_full.xml new file mode 100644 index 00000000..0656868c --- /dev/null +++ b/app/src/main/res/drawable/ic_photo_full.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_shutter.xml b/app/src/main/res/drawable/ic_shutter.xml new file mode 100644 index 00000000..7c8a609d --- /dev/null +++ b/app/src/main/res/drawable/ic_shutter.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_shutter_animated.xml b/app/src/main/res/drawable/ic_shutter_animated.xml new file mode 100644 index 00000000..b1baee68 --- /dev/null +++ b/app/src/main/res/drawable/ic_shutter_animated.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_shutter_vector.xml b/app/src/main/res/drawable/ic_shutter_vector.xml deleted file mode 100644 index 465884e0..00000000 --- a/app/src/main/res/drawable/ic_shutter_vector.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_video_fhd.xml b/app/src/main/res/drawable/ic_video_fhd.xml new file mode 100644 index 00000000..16b240a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_video_fhd.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_video_hd.xml b/app/src/main/res/drawable/ic_video_hd.xml new file mode 100644 index 00000000..efc8d803 --- /dev/null +++ b/app/src/main/res/drawable/ic_video_hd.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_video_rec.xml b/app/src/main/res/drawable/ic_video_rec.xml new file mode 100644 index 00000000..63c9f4c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_video_rec.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_video_rec_animated.xml b/app/src/main/res/drawable/ic_video_rec_animated.xml new file mode 100644 index 00000000..52574767 --- /dev/null +++ b/app/src/main/res/drawable/ic_video_rec_animated.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_video_sd.xml b/app/src/main/res/drawable/ic_video_sd.xml new file mode 100644 index 00000000..1db2f721 --- /dev/null +++ b/app/src/main/res/drawable/ic_video_sd.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_video_uhd.xml b/app/src/main/res/drawable/ic_video_uhd.xml new file mode 100644 index 00000000..3aa0d9d4 --- /dev/null +++ b/app/src/main/res/drawable/ic_video_uhd.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/app/src/main/res/drawable/shutter_pressed_to_unpressed.xml b/app/src/main/res/drawable/shutter_pressed_to_unpressed.xml new file mode 100644 index 00000000..eb8bdd91 --- /dev/null +++ b/app/src/main/res/drawable/shutter_pressed_to_unpressed.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/shutter_unpressed_to_pressed.xml b/app/src/main/res/drawable/shutter_unpressed_to_pressed.xml new file mode 100644 index 00000000..7ebc744a --- /dev/null +++ b/app/src/main/res/drawable/shutter_unpressed_to_pressed.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/tab_indicator.xml b/app/src/main/res/drawable/tab_indicator.xml new file mode 100644 index 00000000..afaa1853 --- /dev/null +++ b/app/src/main/res/drawable/tab_indicator.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/video_rec_idle_to_record.xml b/app/src/main/res/drawable/video_rec_idle_to_record.xml new file mode 100644 index 00000000..4f26e17f --- /dev/null +++ b/app/src/main/res/drawable/video_rec_idle_to_record.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/video_rec_pressed_to_unpressed.xml b/app/src/main/res/drawable/video_rec_pressed_to_unpressed.xml new file mode 100644 index 00000000..4c894dba --- /dev/null +++ b/app/src/main/res/drawable/video_rec_pressed_to_unpressed.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/video_rec_record_to_idle.xml b/app/src/main/res/drawable/video_rec_record_to_idle.xml new file mode 100644 index 00000000..2a3fe687 --- /dev/null +++ b/app/src/main/res/drawable/video_rec_record_to_idle.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/video_rec_unpressed_to_pressed.xml b/app/src/main/res/drawable/video_rec_unpressed_to_pressed.xml new file mode 100644 index 00000000..37754633 --- /dev/null +++ b/app/src/main/res/drawable/video_rec_unpressed_to_pressed.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 7ed73b50..72a572f7 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,107 +1,138 @@ - + android:animateLayoutChanges="true" + android:background="@android:color/black" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" /> + + android:background="@android:color/black" /> + + + + + + + + + + + + + + + + + + + + + + android:src="@drawable/ic_camera_front_vector" + app:layout_constraintBottom_toBottomOf="@id/shutter" + app:layout_constraintEnd_toStartOf="@id/shutter" + app:layout_constraintHorizontal_chainStyle="spread" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/shutter" /> - - + android:id="@+id/shutter" + android:layout_width="@dimen/large_icon_size" + android:layout_height="@dimen/large_icon_size" + android:layout_marginBottom="@dimen/small_margin" + android:contentDescription="@string/shutter" + android:src="@drawable/ic_shutter_animated" + app:layout_constraintBottom_toTopOf="@id/video_rec_curr_timer" + app:layout_constraintEnd_toStartOf="@id/last_photo_video_preview" + app:layout_constraintStart_toEndOf="@id/toggle_camera" + app:layout_constraintVertical_bias="1" + app:layout_goneMarginBottom="@dimen/big_margin" /> + android:padding="@dimen/medium_margin" + app:layout_constraintBottom_toBottomOf="@id/shutter" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/shutter" + app:layout_constraintTop_toTopOf="@id/shutter" + tools:src="@tools:sample/backgrounds/scenic" /> - - - - - - - - - + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + tools:text="00:00" + tools:visibility="gone" /> - + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index fa470786..c6316f4c 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -116,21 +116,6 @@ - - - - - +