From f6d755b4e3ebc8856995a0f2bef7cded77b7a444 Mon Sep 17 00:00:00 2001 From: BohunD Date: Thu, 17 Jul 2025 12:24:23 +0300 Subject: [PATCH 1/2] implemented camera container --- design/api/current.api | 272 +++++++++++++++++- design/build.gradle | 11 + .../android/design/resources/dimens/Dimens.kt | 3 + .../ui/camera/delegate/CameraDelegate.kt | 32 +++ .../ui/camera/delegate/CameraDelegateImpl.kt | 257 +++++++++++++++++ .../ui/camera/delegate/CameraDelegateState.kt | 12 + .../design/ui/camera/delegate/CameraType.kt | 10 + .../design/ui/camera/model/AppMediaType.kt | 25 ++ .../model/CameraModeTypePresentationModel.kt | 6 + .../model/RecordingStatePresentationModel.kt | 11 + .../design/ui/camera/ui/CameraContainer.kt | 162 +++++++++++ .../design/ui/camera/ui/CameraPreview.kt | 37 +++ .../ui/camera/ui/CameraRecorderControls.kt | 130 +++++++++ .../ui/camera/ui/CameraRecorderDimens.kt | 10 + .../design/ui/camera/ui/CircleButton.kt | 40 +++ .../design/ui/camera/ui/RecordMediaButton.kt | 154 ++++++++++ .../ui/camera/util/CacheFoldersConstants.kt | 6 + .../ui/camera/util/MediaTypeExtensions.kt | 17 ++ .../ui/camera/util/PhotoTempFileProvider.kt | 17 ++ .../util/VideoRecordingTempFileProvider.kt | 17 ++ gradle/libs.versions.toml | 14 + 21 files changed, 1237 insertions(+), 6 deletions(-) create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraDelegate.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraDelegateImpl.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraDelegateState.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraType.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/model/AppMediaType.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/model/CameraModeTypePresentationModel.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/model/RecordingStatePresentationModel.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraContainer.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraPreview.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraRecorderControls.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraRecorderDimens.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CircleButton.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/ui/RecordMediaButton.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/util/CacheFoldersConstants.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/util/MediaTypeExtensions.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/util/PhotoTempFileProvider.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/camera/util/VideoRecordingTempFileProvider.kt diff --git a/design/api/current.api b/design/api/current.api index ccba476d..f91bfed5 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -30,6 +30,8 @@ package com.urlaunched.android.design.resources.dimens { method public float getCornerRadiusNormalSpecial(); method public float getCornerRadiusSmall(); method public float getCornerRadiusTiny(); + method public float getIconSizeNormal(); + method public float getIconSizeNormalSpecial(); method public float getSpacingBig(); method public float getSpacingBigSpecial(); method public float getSpacingExtraLarge(); @@ -50,6 +52,8 @@ package com.urlaunched.android.design.resources.dimens { property public final float cornerRadiusNormalSpecial; property public final float cornerRadiusSmall; property public final float cornerRadiusTiny; + property public final float iconSizeNormal; + property public final float iconSizeNormalSpecial; property public final float spacingBig; property public final float spacingBigSpecial; property public final float spacingExtraLarge; @@ -83,6 +87,264 @@ package com.urlaunched.android.design.ui.bottomsheet { } +package com.urlaunched.android.design.ui.camera.delegate { + + public interface CameraDelegate { + method public void bindLifecycle(androidx.lifecycle.LifecycleOwner lifecycleOwner); + method public void bindPreviewView(androidx.camera.view.PreviewView previewView); + method public kotlinx.coroutines.flow.Flow getCameraSideEffect(); + method public kotlinx.coroutines.flow.StateFlow getUiCameraState(); + method public void onGalleryClick(); + method public void onMediaPick(java.util.List uris, kotlin.jvm.functions.Function1,kotlin.Unit> onHandleUris); + method public void onPause(); + method public void onPhotoCameraClick(); + method public void onShutterClick(kotlin.jvm.functions.Function1 onCaptureMedia); + method public void onVideoCameraClick(); + method public void toggleCameraType(); + method public void toggleFlash(); + method public void unbindLifecycle(); + property public abstract kotlinx.coroutines.flow.Flow cameraSideEffect; + property public abstract kotlinx.coroutines.flow.StateFlow uiCameraState; + } + + public final class CameraDelegateImpl extends com.urlaunched.android.design.ui.textfield.hashtags.Delegate implements com.urlaunched.android.design.ui.camera.delegate.CameraDelegate { + ctor public CameraDelegateImpl(kotlinx.coroutines.CoroutineDispatcher ioDispatcher, kotlinx.coroutines.CoroutineDispatcher mainDispatcher, androidx.camera.view.LifecycleCameraController lifecycleCameraController, com.urlaunched.android.design.ui.camera.util.VideoRecordingTempFileProvider videoRecordingTempFileProvider, com.urlaunched.android.design.ui.camera.util.PhotoTempFileProvider photoTempFileProvider, kotlinx.coroutines.CoroutineScope viewModelScope); + method public void bindLifecycle(androidx.lifecycle.LifecycleOwner lifecycleOwner); + method public void bindPreviewView(androidx.camera.view.PreviewView previewView); + method public kotlinx.coroutines.flow.Flow getCameraSideEffect(); + method public kotlinx.coroutines.flow.StateFlow getUiCameraState(); + method public void onGalleryClick(); + method public void onMediaPick(java.util.List uris, kotlin.jvm.functions.Function1,kotlin.Unit> onHandleUris); + method public void onPause(); + method public void onPhotoCameraClick(); + method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public void onShutterClick(kotlin.jvm.functions.Function1 onCaptureMedia); + method public void onVideoCameraClick(); + method public void toggleCameraType(); + method public void toggleFlash(); + method public void unbindLifecycle(); + property public kotlinx.coroutines.flow.Flow cameraSideEffect; + property public kotlinx.coroutines.flow.StateFlow uiCameraState; + } + + public abstract static sealed class CameraDelegateImpl.CameraSideEffect { + } + + public static final class CameraDelegateImpl.CameraSideEffect.PickGalleryMedia extends com.urlaunched.android.design.ui.camera.delegate.CameraDelegateImpl.CameraSideEffect { + field public static final com.urlaunched.android.design.ui.camera.delegate.CameraDelegateImpl.CameraSideEffect.PickGalleryMedia INSTANCE; + } + + public static final class CameraDelegateImpl.CameraSideEffect.ShowCameraInitErrorSnackbar extends com.urlaunched.android.design.ui.camera.delegate.CameraDelegateImpl.CameraSideEffect { + field public static final com.urlaunched.android.design.ui.camera.delegate.CameraDelegateImpl.CameraSideEffect.ShowCameraInitErrorSnackbar INSTANCE; + } + + public static final class CameraDelegateImpl.CameraSideEffect.ShowSnackbar extends com.urlaunched.android.design.ui.camera.delegate.CameraDelegateImpl.CameraSideEffect { + ctor public CameraDelegateImpl.CameraSideEffect.ShowSnackbar(String message); + method public String component1(); + method public com.urlaunched.android.design.ui.camera.delegate.CameraDelegateImpl.CameraSideEffect.ShowSnackbar copy(String message); + method public String getMessage(); + property public final String message; + } + + public final class CameraDelegateState { + ctor public CameraDelegateState(com.urlaunched.android.design.ui.camera.delegate.CameraType cameraType, com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel recordingState, com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel cameraMode, boolean isFlashEnabled, boolean shouldRestoreStatusBarColors); + method public com.urlaunched.android.design.ui.camera.delegate.CameraType component1(); + method public com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel component2(); + method public com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel component3(); + method public boolean component4(); + method public boolean component5(); + method public com.urlaunched.android.design.ui.camera.delegate.CameraDelegateState copy(com.urlaunched.android.design.ui.camera.delegate.CameraType cameraType, com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel recordingState, com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel cameraMode, boolean isFlashEnabled, boolean shouldRestoreStatusBarColors); + method public com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel getCameraMode(); + method public com.urlaunched.android.design.ui.camera.delegate.CameraType getCameraType(); + method public com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel getRecordingState(); + method public boolean getShouldRestoreStatusBarColors(); + method public boolean isFlashEnabled(); + property public final com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel cameraMode; + property public final com.urlaunched.android.design.ui.camera.delegate.CameraType cameraType; + property public final boolean isFlashEnabled; + property public final com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel recordingState; + property public final boolean shouldRestoreStatusBarColors; + } + + public enum CameraType { + method public final androidx.camera.core.CameraSelector! getSelector(); + method public static com.urlaunched.android.design.ui.camera.delegate.CameraType valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException; + method public static com.urlaunched.android.design.ui.camera.delegate.CameraType[] values(); + property public final androidx.camera.core.CameraSelector! selector; + enum_constant public static final com.urlaunched.android.design.ui.camera.delegate.CameraType BACK; + enum_constant public static final com.urlaunched.android.design.ui.camera.delegate.CameraType FRONT; + } + +} + +package com.urlaunched.android.design.ui.camera.model { + + public enum AppMediaType implements com.urlaunched.android.common.files.MediaType { + method public final java.util.List! getExtensions(); + method public java.util.List! getExtraMimeTypes(); + method public String! getMimeType(); + method public static com.urlaunched.android.design.ui.camera.model.AppMediaType valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException; + method public static com.urlaunched.android.design.ui.camera.model.AppMediaType[] values(); + property public final java.util.List! extensions; + property public java.util.List! extraMimeTypes; + property public String! mimeType; + enum_constant public static final com.urlaunched.android.design.ui.camera.model.AppMediaType IMAGE_PREVIEW; + enum_constant public static final com.urlaunched.android.design.ui.camera.model.AppMediaType VIDEO; + } + + public final class AppMediaTypeKt { + method public static com.urlaunched.android.design.ui.camera.model.AppMediaType? getMediaTypeForUri(android.net.Uri, android.content.Context context); + } + + public enum CameraModeTypePresentationModel { + method public static com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException; + method public static com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel[] values(); + enum_constant public static final com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel PHOTO; + enum_constant public static final com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel VIDEO; + } + + public abstract sealed class RecordingStatePresentationModel { + } + + public static final class RecordingStatePresentationModel.Recording extends com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel { + ctor public RecordingStatePresentationModel.Recording(long duration, long durationLimit); + method public long component1-UwyO8pc(); + method public long component2-UwyO8pc(); + method public com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel.Recording copy-QTBD994(long duration, long durationLimit); + method public long getDuration(); + method public long getDurationLimit(); + method public float getProgress(); + property public final long duration; + property public final long durationLimit; + property public final float progress; + } + + public static final class RecordingStatePresentationModel.Stopped extends com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel { + field public static final com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel.Stopped INSTANCE; + } + +} + +package com.urlaunched.android.design.ui.camera.ui { + + public final class CameraContainerConfig { + ctor public CameraContainerConfig(optional long backgroundColor, optional long surfaceColor, optional float surfaceCornerRadius, optional float surfacePaddingBottom, optional long closeButtonBackground, optional long closeButtonIconTint, optional kotlin.jvm.functions.Function0 closeButtonIcon, optional long flashButtonBackground, optional long flashButtonIconTint, optional float flashIconSize, optional float flashIconInnerPadding, optional kotlin.jvm.functions.Function0 flashOnIcon, optional kotlin.jvm.functions.Function0 flashOffIcon, optional float topButtonsSpacing, optional float controlPaddingHorizontal, optional float controlPaddingBottom, optional androidx.compose.ui.graphics.Brush progressBrush, optional long selectedOptionBackground, optional long selectedOptionTextColor, optional long unselectedOptionBackground, optional long unselectedOptionTextColor, optional long controlButtonBackground, optional long controlButtonBorderColor, optional long controlIconTint); + method public long component1-0d7_KjU(); + method public float component10-D9Ej5fM(); + method public float component11-D9Ej5fM(); + method public kotlin.jvm.functions.Function0 component12(); + method public kotlin.jvm.functions.Function0 component13(); + method public float component14-D9Ej5fM(); + method public float component15-D9Ej5fM(); + method public float component16-D9Ej5fM(); + method public androidx.compose.ui.graphics.Brush component17(); + method public long component18-0d7_KjU(); + method public long component19-0d7_KjU(); + method public long component2-0d7_KjU(); + method public long component20-0d7_KjU(); + method public long component21-0d7_KjU(); + method public long component22-0d7_KjU(); + method public long component23-0d7_KjU(); + method public long component24-0d7_KjU(); + method public float component3-D9Ej5fM(); + method public float component4-D9Ej5fM(); + method public long component5-0d7_KjU(); + method public long component6-0d7_KjU(); + method public kotlin.jvm.functions.Function0 component7(); + method public long component8-0d7_KjU(); + method public long component9-0d7_KjU(); + method public com.urlaunched.android.design.ui.camera.ui.CameraContainerConfig copy-ZR-Z7PU(long backgroundColor, long surfaceColor, float surfaceCornerRadius, float surfacePaddingBottom, long closeButtonBackground, long closeButtonIconTint, kotlin.jvm.functions.Function0 closeButtonIcon, long flashButtonBackground, long flashButtonIconTint, float flashIconSize, float flashIconInnerPadding, kotlin.jvm.functions.Function0 flashOnIcon, kotlin.jvm.functions.Function0 flashOffIcon, float topButtonsSpacing, float controlPaddingHorizontal, float controlPaddingBottom, androidx.compose.ui.graphics.Brush progressBrush, long selectedOptionBackground, long selectedOptionTextColor, long unselectedOptionBackground, long unselectedOptionTextColor, long controlButtonBackground, long controlButtonBorderColor, long controlIconTint); + method public long getBackgroundColor(); + method public long getCloseButtonBackground(); + method public kotlin.jvm.functions.Function0 getCloseButtonIcon(); + method public long getCloseButtonIconTint(); + method public long getControlButtonBackground(); + method public long getControlButtonBorderColor(); + method public long getControlIconTint(); + method public float getControlPaddingBottom(); + method public float getControlPaddingHorizontal(); + method public long getFlashButtonBackground(); + method public long getFlashButtonIconTint(); + method public float getFlashIconInnerPadding(); + method public float getFlashIconSize(); + method public kotlin.jvm.functions.Function0 getFlashOffIcon(); + method public kotlin.jvm.functions.Function0 getFlashOnIcon(); + method public androidx.compose.ui.graphics.Brush getProgressBrush(); + method public long getSelectedOptionBackground(); + method public long getSelectedOptionTextColor(); + method public long getSurfaceColor(); + method public float getSurfaceCornerRadius(); + method public float getSurfacePaddingBottom(); + method public float getTopButtonsSpacing(); + method public long getUnselectedOptionBackground(); + method public long getUnselectedOptionTextColor(); + property public final long backgroundColor; + property public final long closeButtonBackground; + property public final kotlin.jvm.functions.Function0 closeButtonIcon; + property public final long closeButtonIconTint; + property public final long controlButtonBackground; + property public final long controlButtonBorderColor; + property public final long controlIconTint; + property public final float controlPaddingBottom; + property public final float controlPaddingHorizontal; + property public final long flashButtonBackground; + property public final long flashButtonIconTint; + property public final float flashIconInnerPadding; + property public final float flashIconSize; + property public final kotlin.jvm.functions.Function0 flashOffIcon; + property public final kotlin.jvm.functions.Function0 flashOnIcon; + property public final androidx.compose.ui.graphics.Brush progressBrush; + property public final long selectedOptionBackground; + property public final long selectedOptionTextColor; + property public final long surfaceColor; + property public final float surfaceCornerRadius; + property public final float surfacePaddingBottom; + property public final float topButtonsSpacing; + property public final long unselectedOptionBackground; + property public final long unselectedOptionTextColor; + } + + public final class CameraContainerKt { + method @androidx.compose.runtime.Composable public static void CameraContainer(optional androidx.compose.ui.Modifier modifier, optional com.urlaunched.android.design.ui.camera.ui.CameraContainerConfig config, com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel recordingState, com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel cameraMode, boolean isFlashEnabled, boolean isGalleryEnabled, kotlin.jvm.functions.Function0 selectedCameraOption, kotlin.jvm.functions.Function1,kotlin.Unit> unselectedCameraOption, kotlin.jvm.functions.Function1,kotlin.Unit> galleryButton, kotlin.jvm.functions.Function1,kotlin.Unit> cameraToggleButton, kotlin.jvm.functions.Function0 onShutterClick, optional kotlin.jvm.functions.Function1 recordMediaButton, kotlin.jvm.functions.Function1 onCameraPreviewViewAvailable, kotlin.jvm.functions.Function0 onVideoCameraClick, kotlin.jvm.functions.Function0 onPhotoCameraClick, kotlin.jvm.functions.Function0 onToggleCameraClick, kotlin.jvm.functions.Function0 onToggleFlashClick, kotlin.jvm.functions.Function0 onGalleryClick, kotlin.jvm.functions.Function0 goBack); + } + + public final class CircleButtonKt { + method @androidx.compose.runtime.Composable public static void CircleButton(optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional long backgroundColor, optional float iconSize, optional float innerPadding, kotlin.jvm.functions.Function0 onClick, kotlin.jvm.functions.Function0 icon); + } + +} + +package com.urlaunched.android.design.ui.camera.util { + + public final class CacheFoldersConstants { + field public static final com.urlaunched.android.design.ui.camera.util.CacheFoldersConstants INSTANCE; + field public static final String PHOTOS = "photos"; + field public static final String VIDEO_RECORDINGS = "video_recordings"; + } + + public final class MediaTypeExtensionsKt { + method public static com.urlaunched.android.design.ui.camera.model.AppMediaType? mimeToMediaType(String); + } + + public final class PhotoTempFileProvider { + ctor public PhotoTempFileProvider(android.content.Context context); + method public operator java.io.File invoke(); + field public static final com.urlaunched.android.design.ui.camera.util.PhotoTempFileProvider.Companion Companion; + } + + public static final class PhotoTempFileProvider.Companion { + } + + public final class VideoRecordingTempFileProvider { + ctor public VideoRecordingTempFileProvider(android.content.Context context); + method public operator java.io.File invoke(); + field public static final com.urlaunched.android.design.ui.camera.util.VideoRecordingTempFileProvider.Companion Companion; + } + + public static final class VideoRecordingTempFileProvider.Companion { + } + +} + package com.urlaunched.android.design.ui.clickable { public final class DebouncedClickableKt { @@ -456,12 +718,6 @@ package com.urlaunched.android.design.ui.textfield.hashtags { public abstract class Delegate implements java.io.Closeable { ctor public Delegate(kotlinx.coroutines.CoroutineDispatcher coroutineDispatcher); method public void close(); - method public final error.NonExistentClass! getDelegateScope(); - property public final error.NonExistentClass! delegateScope; - } - - public final class HashtagsContainerKt { - method @androidx.compose.runtime.Composable public static void HashtagsContainer(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Modifier innerFieldModifier, optional androidx.compose.ui.text.input.TextFieldValue value, optional String? label, String? placeHolder, optional String? error, optional boolean enabled, optional boolean readOnly, optional com.urlaunched.android.design.ui.textfield.models.TextFieldBorderConfig borderConfig, optional com.urlaunched.android.design.ui.textfield.models.TextFieldBackgroundConfig backgroundConfig, optional com.urlaunched.android.design.ui.textfield.models.TextFieldInputTextConfig inputTextConfig, optional com.urlaunched.android.design.ui.textfield.models.TextFieldInputPlaceholderTextConfig inputPlaceholderTextConfig, optional com.urlaunched.android.design.ui.textfield.models.TextFieldTopLabelConfig topLabelConfig, optional com.urlaunched.android.design.ui.textfield.models.TextFieldsSpacerConfig textFieldsSpacerConfig, optional long selectionHandleColor, optional long selectionBackgroundColor, optional androidx.compose.ui.graphics.Brush cursorBrush, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional boolean collapseLabel, optional androidx.compose.ui.unit.Dp? textFieldHeight, optional androidx.compose.foundation.layout.PaddingValues innerPadding, optional kotlin.jvm.functions.Function0? trailingIcon, optional kotlin.jvm.functions.Function0? leadingIcon, optional kotlin.jvm.functions.Function0? labelIcon, optional boolean trailingIconAlwaysShown, kotlin.jvm.functions.Function1 onValueChange); } public interface HashtagsDelegate { @@ -485,6 +741,10 @@ package com.urlaunched.android.design.ui.textfield.hashtags { property public final androidx.compose.ui.text.input.TextFieldValue currentHashtagsText; } + public final class HashtagsTextFieldKt { + method @androidx.compose.runtime.Composable public static void HashtagsTextField(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Modifier innerFieldModifier, optional androidx.compose.ui.text.input.TextFieldValue value, optional String? label, String? placeHolder, optional String? error, optional boolean enabled, optional boolean readOnly, optional com.urlaunched.android.design.ui.textfield.models.TextFieldBorderConfig borderConfig, optional com.urlaunched.android.design.ui.textfield.models.TextFieldBackgroundConfig backgroundConfig, optional com.urlaunched.android.design.ui.textfield.models.TextFieldInputTextConfig inputTextConfig, optional com.urlaunched.android.design.ui.textfield.models.TextFieldInputPlaceholderTextConfig inputPlaceholderTextConfig, optional com.urlaunched.android.design.ui.textfield.models.TextFieldTopLabelConfig topLabelConfig, optional com.urlaunched.android.design.ui.textfield.models.TextFieldsSpacerConfig textFieldsSpacerConfig, optional long selectionHandleColor, optional long selectionBackgroundColor, optional androidx.compose.ui.graphics.Brush cursorBrush, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional boolean collapseLabel, optional androidx.compose.ui.unit.Dp? textFieldHeight, optional androidx.compose.foundation.layout.PaddingValues innerPadding, optional kotlin.jvm.functions.Function0? trailingIcon, optional kotlin.jvm.functions.Function0? leadingIcon, optional kotlin.jvm.functions.Function0? labelIcon, optional boolean trailingIconAlwaysShown, kotlin.jvm.functions.Function1 onValueChange); + } + } package com.urlaunched.android.design.ui.textfield.models { diff --git a/design/build.gradle b/design/build.gradle index 1b31aea9..235fb3fe 100644 --- a/design/build.gradle +++ b/design/build.gradle @@ -87,7 +87,18 @@ dependencies { // Local modules implementation project(":cdn:models:presentation") + implementation project(":common") // Bottom sheet implementation libs.bottomSheet + + // CameraX + implementation libs.camerax.camera2 + implementation libs.camerax.lifecycle + implementation libs.camerax.video + implementation libs.camerax.view + + // Guava + implementation libs.guava.core + implementation libs.guava.androidExt } \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/resources/dimens/Dimens.kt b/design/src/main/java/com/urlaunched/android/design/resources/dimens/Dimens.kt index 334d4062..ba5d5f56 100644 --- a/design/src/main/java/com/urlaunched/android/design/resources/dimens/Dimens.kt +++ b/design/src/main/java/com/urlaunched/android/design/resources/dimens/Dimens.kt @@ -25,4 +25,7 @@ object Dimens { val cornerRadiusBigSpecial = 20.dp val cornerRadiusBig = 24.dp val cornerRadiusLarge = 32.dp + + val iconSizeNormalSpecial = 20.dp + val iconSizeNormal = 16.dp } \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraDelegate.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraDelegate.kt new file mode 100644 index 00000000..79620cf6 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraDelegate.kt @@ -0,0 +1,32 @@ +package com.urlaunched.android.design.ui.camera.delegate + +import android.net.Uri +import androidx.camera.core.CameraSelector +import androidx.camera.view.PreviewView +import androidx.lifecycle.LifecycleOwner +import com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel +import com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel +import com.urlaunched.android.design.ui.camera.delegate.CameraDelegateImpl.CameraSideEffect +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +interface CameraDelegate { + val uiCameraState: StateFlow + val cameraSideEffect: Flow + + fun bindPreviewView(previewView: PreviewView) + fun bindLifecycle(lifecycleOwner: LifecycleOwner) + fun unbindLifecycle() + + fun toggleCameraType() + fun toggleFlash() + + fun onPhotoCameraClick() + fun onVideoCameraClick() + fun onGalleryClick() + + fun onShutterClick(onCaptureMedia: (uri: Uri) -> Unit) + fun onPause() + + fun onMediaPick(uris: List, onHandleUris: (uris: List) -> Unit) +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraDelegateImpl.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraDelegateImpl.kt new file mode 100644 index 00000000..2a969546 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraDelegateImpl.kt @@ -0,0 +1,257 @@ +package com.urlaunched.android.design.ui.camera.delegate + +import android.Manifest +import android.net.Uri +import androidx.annotation.RequiresPermission +import androidx.camera.core.CameraUnavailableException +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO +import androidx.camera.core.ImageCapture.FLASH_MODE_OFF +import androidx.camera.core.ImageCaptureException +import androidx.camera.video.FileOutputOptions +import androidx.camera.video.VideoRecordEvent +import androidx.camera.view.CameraController.IMAGE_CAPTURE +import androidx.camera.view.CameraController.VIDEO_CAPTURE +import androidx.camera.view.LifecycleCameraController +import androidx.camera.view.PreviewView +import androidx.camera.view.video.AudioConfig +import androidx.concurrent.futures.await +import androidx.core.util.Consumer +import androidx.lifecycle.LifecycleOwner +import com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel +import com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel +import com.urlaunched.android.design.ui.camera.util.VideoRecordingTempFileProvider +import com.urlaunched.android.design.ui.camera.util.PhotoTempFileProvider +import com.urlaunched.android.design.ui.textfield.hashtags.Delegate +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.time.Duration +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.Duration.Companion.seconds + +class CameraDelegateImpl( + private val ioDispatcher: CoroutineDispatcher, + private val mainDispatcher: CoroutineDispatcher, + private val lifecycleCameraController: LifecycleCameraController, + private val videoRecordingTempFileProvider: VideoRecordingTempFileProvider, + private val photoTempFileProvider: PhotoTempFileProvider, + private val viewModelScope: CoroutineScope, +) : Delegate(coroutineDispatcher = ioDispatcher), CameraDelegate { + private val _uiState = MutableStateFlow( + CameraDelegateState( + cameraType = CameraType.BACK, + recordingState = RecordingStatePresentationModel.Stopped, + cameraMode = CameraModeTypePresentationModel.VIDEO, + isFlashEnabled = true, + shouldRestoreStatusBarColors = false, + ) + ) + private val _cameraSideEffect = Channel() + override val cameraSideEffect: Flow get() = _cameraSideEffect.receiveAsFlow() + + override val uiCameraState: StateFlow = _uiState + + private var isCameraInitialized = false + private var recordController: androidx.camera.video.Recording? = null + + init { + lifecycleCameraController.setEnabledUseCases(VIDEO_CAPTURE or IMAGE_CAPTURE) + initCameraController() + } + + private fun initCameraController() { + viewModelScope.launch(mainDispatcher) { + try { + lifecycleCameraController.initializationFuture.await() + isCameraInitialized = true + } catch (e: CameraUnavailableException) { + withContext(ioDispatcher) { + _cameraSideEffect.send(CameraSideEffect.ShowCameraInitErrorSnackbar) + } + } + } + } + + override fun bindPreviewView(previewView: PreviewView) { + previewView.controller = lifecycleCameraController + } + + override fun bindLifecycle(lifecycleOwner: LifecycleOwner) { + lifecycleCameraController.bindToLifecycle(lifecycleOwner) + } + + override fun unbindLifecycle() { + lifecycleCameraController.unbind() + } + + override fun toggleCameraType() { + val newType = if (_uiState.value.cameraType == CameraType.BACK) CameraType.FRONT else CameraType.BACK + setCameraType(newType) + } + + private fun setCameraType(cameraType: CameraType) { + if (_uiState.value.cameraType == cameraType) return + if (hasCamera(cameraType) && !lifecycleCameraController.isRecording) { + if (lifecycleCameraController.cameraSelector != cameraType.selector) { + lifecycleCameraController.cameraSelector = cameraType.selector + _uiState.update { it.copy(cameraType = cameraType) } + } + } + } + + private fun hasCamera(cameraType: CameraType): Boolean = + isCameraInitialized && lifecycleCameraController.hasCamera(cameraType.selector) + + override fun toggleFlash() { + val newFlash = !_uiState.value.isFlashEnabled + lifecycleCameraController.imageCaptureFlashMode = if (newFlash) FLASH_MODE_AUTO else FLASH_MODE_OFF + _uiState.update { it.copy(isFlashEnabled = newFlash) } + } + + override fun onPhotoCameraClick() { + _uiState.update { it.copy(cameraMode = CameraModeTypePresentationModel.PHOTO) } + } + + override fun onVideoCameraClick() { + _uiState.update { it.copy(cameraMode = CameraModeTypePresentationModel.VIDEO) } + } + + override fun onGalleryClick() { + viewModelScope.launch { _cameraSideEffect.send(CameraSideEffect.PickGalleryMedia) } + } + + @RequiresPermission(Manifest.permission.RECORD_AUDIO) + override fun onShutterClick(onCaptureMedia: (uri: Uri) -> Unit) { + when (_uiState.value.cameraMode) { + CameraModeTypePresentationModel.PHOTO -> takePicture(onCaptureMedia) + CameraModeTypePresentationModel.VIDEO -> toggleRecording(onCaptureMedia) + } + } + + @RequiresPermission(Manifest.permission.RECORD_AUDIO) + private fun toggleRecording(onCaptureMedia: (uri: Uri) -> Unit) { + when (_uiState.value.recordingState) { + is RecordingStatePresentationModel.Recording -> stopRecording() + is RecordingStatePresentationModel.Stopped -> startRecording(90.seconds, onCaptureMedia) + } + } + + private fun takePicture(onCaptureMedia: (uri: Uri) -> Unit) { + lifecycleCameraController.takePicture( + ImageCapture.OutputFileOptions.Builder(photoTempFileProvider()).build(), + Runnable::run, + object : ImageCapture.OnImageSavedCallback { + override fun onError(error: ImageCaptureException) { + showSnackbar(error.message.orEmpty()) + } + + override fun onImageSaved(result: ImageCapture.OutputFileResults) { + result.savedUri?.let { uri -> + onCaptureMedia(uri) + + } + } + } + ) + } + + @RequiresPermission(Manifest.permission.RECORD_AUDIO) + private fun startRecording(limit: Duration, onCaptureMedia: (uri: Uri) -> Unit) { + viewModelScope.launch(ioDispatcher) { + val file = videoRecordingTempFileProvider() + withContext(mainDispatcher) { + try { + recordController = lifecycleCameraController.startRecording( + FileOutputOptions.Builder(file) + .setDurationLimitMillis(limit.inWholeMilliseconds) + .build(), + AudioConfig.create(true), + Runnable::run, + withContext(mainDispatcher) { + recordingStateListener(limit, onCaptureMedia) + } + ) + } catch (e: Exception) { + _uiState.update { it.copy(recordingState = RecordingStatePresentationModel.Stopped) } + showSnackbar(e.message.orEmpty()) + } + } + } + } + + private fun stopRecording() { + recordController?.stop() + } + + private fun recordingStateListener( + recordingDurationLimit: Duration, + onCaptureMedia: (uri: Uri) -> Unit + ): Consumer = + Consumer { event -> + viewModelScope.launch(ioDispatcher) { + when (event) { + is VideoRecordEvent.Finalize -> { + _uiState.update { uiState -> + uiState.copy( + recordingState = RecordingStatePresentationModel.Stopped + ) + } + + when { + !event.hasError() || event.error == VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED -> { + withContext(mainDispatcher) { + onCaptureMedia(event.outputResults.outputUri) + } + } + + else -> showSnackbar(event.cause?.message.toString()) + } + + recordController = null + } + + else -> { + _uiState.update { uiState -> + uiState.copy( + recordingState = RecordingStatePresentationModel.Recording( + event.recordingStats.recordedDurationNanos.nanoseconds, + recordingDurationLimit + ) + ) + } + } + } + } + } + + override fun onPause() { + recordController?.stop() + } + + override fun onMediaPick(uris: List, onHandleUris: (uris: List) -> Unit) { + viewModelScope.launch(ioDispatcher) { + _uiState.update { it.copy(shouldRestoreStatusBarColors = true) } + onHandleUris(uris) + } + } + + private fun showSnackbar(msg: String) { + viewModelScope.launch(ioDispatcher) { + _cameraSideEffect.send(CameraSideEffect.ShowSnackbar(msg)) + } + } + + sealed class CameraSideEffect { + data class ShowSnackbar(val message: String) : CameraSideEffect() + data object ShowCameraInitErrorSnackbar : CameraSideEffect() + data object PickGalleryMedia : CameraSideEffect() + } +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraDelegateState.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraDelegateState.kt new file mode 100644 index 00000000..989f600c --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraDelegateState.kt @@ -0,0 +1,12 @@ +package com.urlaunched.android.design.ui.camera.delegate + +import com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel +import com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel + +data class CameraDelegateState( + val cameraType: CameraType, + val recordingState: RecordingStatePresentationModel, + val cameraMode: CameraModeTypePresentationModel, + val isFlashEnabled: Boolean, + val shouldRestoreStatusBarColors: Boolean, +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraType.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraType.kt new file mode 100644 index 00000000..a2f92107 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/delegate/CameraType.kt @@ -0,0 +1,10 @@ +package com.urlaunched.android.design.ui.camera.delegate + +import androidx.camera.core.CameraSelector + +enum class CameraType( + val selector: CameraSelector +) { + FRONT(CameraSelector.DEFAULT_FRONT_CAMERA), + BACK(CameraSelector.DEFAULT_BACK_CAMERA) +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/model/AppMediaType.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/model/AppMediaType.kt new file mode 100644 index 00000000..77cbe9fd --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/model/AppMediaType.kt @@ -0,0 +1,25 @@ +package com.urlaunched.android.design.ui.camera.model + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.webkit.MimeTypeMap +import com.urlaunched.android.common.files.MediaType +import com.urlaunched.android.design.ui.camera.util.mimeToMediaType + +enum class AppMediaType( + override val mimeType: String, + override val extraMimeTypes: List = listOf(), + val extensions: List = listOf() +) : MediaType { + IMAGE_PREVIEW(mimeType = "image/*", extensions = listOf(".jpg")), + VIDEO(mimeType = "video/mp4", extensions = listOf(".mp4")) +} + +fun Uri.getMediaTypeForUri(context: Context) = if (ContentResolver.SCHEME_CONTENT == scheme) { + context.contentResolver.getType(this)?.mimeToMediaType() +} else { + MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(toString())) + ?.mimeToMediaType() +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/model/CameraModeTypePresentationModel.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/model/CameraModeTypePresentationModel.kt new file mode 100644 index 00000000..a12c38aa --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/model/CameraModeTypePresentationModel.kt @@ -0,0 +1,6 @@ +package com.urlaunched.android.design.ui.camera.model + +enum class CameraModeTypePresentationModel { + PHOTO, + VIDEO +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/model/RecordingStatePresentationModel.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/model/RecordingStatePresentationModel.kt new file mode 100644 index 00000000..039489c9 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/model/RecordingStatePresentationModel.kt @@ -0,0 +1,11 @@ +package com.urlaunched.android.design.ui.camera.model + +import kotlin.time.Duration + +sealed class RecordingStatePresentationModel { + data class Recording(val duration: Duration, val durationLimit: Duration) : RecordingStatePresentationModel() { + val progress = (duration / durationLimit).toFloat() + } + + data object Stopped : RecordingStatePresentationModel() +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraContainer.kt new file mode 100644 index 00000000..da9d3096 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraContainer.kt @@ -0,0 +1,162 @@ +package com.urlaunched.android.design.ui.camera.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.unit.Dp +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel +import com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel + +data class CameraContainerColors( + val backgroundColor: Color = Color.Black, + val surfaceColor: Color = Color.White, + val closeButtonBackground: Color = Color.Black, + val closeButtonIconTint: Color = Color.White, + val flashButtonBackground: Color = Color.Black, + val flashButtonIconTint: Color = Color.White, + val progressBrush: Brush = SolidColor(Color.Black), + val selectedOptionBackground: Color = Color.White, + val selectedOptionTextColor: Color = Color.Black, + val unselectedOptionBackground: Color = Color.DarkGray, + val unselectedOptionTextColor: Color = Color.White, + val controlButtonBackground: Color = Color.Black, + val controlButtonBorderColor: Color = Color.DarkGray, + val controlIconTint: Color = Color.White, +) + +data class CameraContainerConfig( + val colors: CameraContainerColors = CameraContainerColors(), + val surfaceCornerRadius: Dp = Dimens.cornerRadiusNormalSpecial, + val surfacePaddingBottom: Dp = Dimens.spacingLarge, + val flashIconSize: Dp = Dimens.iconSizeNormalSpecial, + val flashIconInnerPadding: Dp = CameraRecorderDimens.flashIconPadding, + val topButtonsSpacing: Dp = Dimens.spacingNormalSpecial, + val controlPaddingHorizontal: Dp = Dimens.spacingNormalSpecial, + val controlPaddingBottom: Dp = Dimens.spacingNormalSpecial, +) + +@Composable +fun CameraContainer( + modifier: Modifier = Modifier, + config: CameraContainerConfig = CameraContainerConfig(), + recordingState: RecordingStatePresentationModel, + cameraMode: CameraModeTypePresentationModel, + isFlashEnabled: Boolean, + isGalleryEnabled: Boolean, + onShutterClick: () -> Unit, + onCameraPreviewViewAvailable: (androidx.camera.view.PreviewView) -> Unit, + onVideoCameraClick: () -> Unit, + onPhotoCameraClick: () -> Unit, + onToggleCameraClick: () -> Unit, + onToggleFlashClick: () -> Unit, + onGalleryClick: () -> Unit, + goBack: () -> Unit, + selectedCameraOption: @Composable () -> Unit, + unselectedCameraOption: @Composable (onClick: () -> Unit) -> Unit, + galleryButton: @Composable (onClick: () -> Unit) -> Unit, + cameraToggleButton: @Composable (onClick: () -> Unit) -> Unit, + closeButton: @Composable () -> Unit = {}, + flashOnButton: @Composable () -> Unit = {}, + flashOffButton: @Composable () -> Unit = {}, + recordMediaButton : @Composable BoxScope.() -> Unit = { + RecordMediaButton( + modifier = Modifier + .size(CameraRecorderDimens.recordMediaButtonSize) + .align(Alignment.Center), + isRecording = recordingState is RecordingStatePresentationModel.Recording, + progress = (recordingState as? RecordingStatePresentationModel.Recording)?.progress ?: 0f, + progressBrush = config.colors.progressBrush, + onClick = onShutterClick + ) + } +) { + Box( + modifier = modifier + .imePadding() + .fillMaxSize() + .background(config.colors.backgroundColor) + .statusBarsPadding() + .padding(bottom = config.surfacePaddingBottom) + .background( + color = config.colors.surfaceColor, + shape = RoundedCornerShape(config.surfaceCornerRadius) + ) + .clip(RoundedCornerShape(config.surfaceCornerRadius)) + ) { + CameraPreview( + modifier = Modifier.fillMaxSize(), + onCameraPreviewViewAvailable = onCameraPreviewViewAvailable + ) + + AnimatedVisibility( + modifier = Modifier.fillMaxWidth(), + enter = fadeIn(), + exit = fadeOut(), + visible = recordingState !is RecordingStatePresentationModel.Recording + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(config.topButtonsSpacing), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Box(modifier = Modifier.clickable(onClick = goBack)) { + closeButton() + } + + AnimatedContent(isFlashEnabled) { enabled -> + Box(modifier = Modifier.clickable(onClick = onToggleFlashClick)) { + if (enabled) { + flashOnButton() + } else { + flashOffButton() + } + } + } + } + } + + CameraRecorderControls( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(horizontal = config.controlPaddingHorizontal) + .padding(bottom = config.controlPaddingBottom), + cameraModeType = cameraMode, + recordingState = recordingState, + isGalleryEnabled = isGalleryEnabled, + onVideoCameraClick = onVideoCameraClick, + onToggleCameraClick = onToggleCameraClick, + onPhotoCameraClick = onPhotoCameraClick, + onGalleryClick = onGalleryClick, + selectedCameraOption = selectedCameraOption, + unselectedCameraOption = unselectedCameraOption, + galleryButton = galleryButton, + cameraToggleButton = cameraToggleButton, + recordMediaButton = recordMediaButton + ) + } +} + diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraPreview.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraPreview.kt new file mode 100644 index 00000000..7e26ea8c --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraPreview.kt @@ -0,0 +1,37 @@ +package com.urlaunched.android.design.ui.camera.ui + +import android.view.ViewGroup +import androidx.camera.view.PreviewView +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.viewinterop.AndroidView + +@Composable +internal fun CameraPreview(modifier: Modifier = Modifier, onCameraPreviewViewAvailable: (view: PreviewView) -> Unit) { + if (!LocalInspectionMode.current) { + AndroidView( + modifier = modifier, + factory = { context -> + PreviewView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + ) + + onCameraPreviewViewAvailable(this) + + implementationMode = PreviewView.ImplementationMode.COMPATIBLE + scaleType = PreviewView.ScaleType.FILL_CENTER + } + }, + onRelease = { view -> + view.controller = null + } + ) + } else { + Box(modifier.background(color = Color(0xFF4CAF50))) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraRecorderControls.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraRecorderControls.kt new file mode 100644 index 00000000..dae571d7 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraRecorderControls.kt @@ -0,0 +1,130 @@ +package com.urlaunched.android.design.ui.camera.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.Layout +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel +import com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel + +@Composable +internal fun CameraRecorderControls( + modifier: Modifier = Modifier, + recordingState: RecordingStatePresentationModel, + cameraModeType: CameraModeTypePresentationModel, + isGalleryEnabled: Boolean, + onVideoCameraClick: () -> Unit, + onPhotoCameraClick: () -> Unit, + onGalleryClick: () -> Unit, + onToggleCameraClick: () -> Unit, + selectedCameraOption: @Composable () -> Unit, + unselectedCameraOption: @Composable (onClick: () -> Unit) -> Unit, + galleryButton: @Composable (onClick: () -> Unit) -> Unit, + cameraToggleButton: @Composable (onClick: () -> Unit) -> Unit, + recordMediaButton: @Composable BoxScope.() -> Unit +) { + Column(modifier = modifier) { + AnimatedContent( + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth(), + contentAlignment = Alignment.Center, + targetState = recordingState is RecordingStatePresentationModel.Recording + ) { isRecording -> + if (isRecording) { + selectedCameraOption() + } else { + AnimatedContent(targetState = cameraModeType) { type -> + CameraOptionLayout( + firstOption = { selectedCameraOption() }, + secondOption = { + unselectedCameraOption( + if (type == CameraModeTypePresentationModel.PHOTO) { + onVideoCameraClick + } else { + onPhotoCameraClick + } + ) + } + ) + } + } + } + + Spacer(Modifier.height(Dimens.spacingSmall)) + + Box(modifier = Modifier.height(IntrinsicSize.Max)) { + recordMediaButton() + + Row( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + AnimatedVisibility(visible = recordingState !is RecordingStatePresentationModel.Recording && isGalleryEnabled) { + galleryButton(onGalleryClick) + } + + AnimatedVisibility(visible = recordingState !is RecordingStatePresentationModel.Recording) { + cameraToggleButton(onToggleCameraClick) + } + } + } + } +} + +@Composable +private fun CameraOptionLayout( + modifier: Modifier = Modifier, + firstOption: @Composable () -> Unit, + secondOption: @Composable () -> Unit +) { + Layout( + modifier = modifier, + content = { + firstOption() + secondOption() + } + ) { measurables, constraints -> + if (measurables.size < 2) { + return@Layout layout(0, 0) { } + } + + val firstButton = measurables[0].measure(constraints) + val secondButton = measurables[1].measure(constraints) + + val width = firstButton.width + secondButton.width * 2 + Dimens.spacingBig.roundToPx() * 2 + val height = maxOf(secondButton.height, firstButton.height) + + layout(width, height) { + val firstButtonX = secondButton.width + Dimens.spacingBig.roundToPx() + + firstButton.placeRelative(firstButtonX, 0) + secondButton.placeRelative( + x = firstButtonX + firstButton.width + Dimens.spacingBig.roundToPx(), + 0 + ) + } + } +} + diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraRecorderDimens.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraRecorderDimens.kt new file mode 100644 index 00000000..839b58ca --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraRecorderDimens.kt @@ -0,0 +1,10 @@ +package com.urlaunched.android.design.ui.camera.ui + +import androidx.compose.ui.unit.dp + +internal object CameraRecorderDimens { + val recordMediaButtonProgressWidth = 4.dp + val recordMediaButtonBorderWidth = 2.dp + val recordMediaButtonSize = 72.dp + val flashIconPadding = 10.dp +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CircleButton.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CircleButton.kt new file mode 100644 index 00000000..a78a7f9a --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CircleButton.kt @@ -0,0 +1,40 @@ +package com.urlaunched.android.design.ui.camera.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import com.urlaunched.android.design.resources.dimens.Dimens + +@Composable +fun CircleButton( + modifier: Modifier = Modifier, + enabled: Boolean = true, + backgroundColor: Color = Color.LightGray, + iconSize: Dp = Dimens.iconSizeNormal, + innerPadding: Dp = Dimens.spacingNormalSpecial, + onClick: () -> Unit, + icon: @Composable () -> Unit +) { + Box( + modifier = modifier + .background( + color = backgroundColor, + shape = CircleShape + ) + .clip(CircleShape) + .clickable(enabled = enabled, onClick = onClick) + .padding(innerPadding) + ) { + Box(modifier = Modifier.size(iconSize), propagateMinConstraints = true) { + icon() + } + } +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/RecordMediaButton.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/RecordMediaButton.kt new file mode 100644 index 00000000..e68318f8 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/RecordMediaButton.kt @@ -0,0 +1,154 @@ +package com.urlaunched.android.design.ui.camera.ui + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.animateInt +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.urlaunched.android.design.resources.dimens.Dimens + +private const val recordMediaButtonRecordingCornersPercent = 20 +private const val recordMediaButtonDefaultCornersPercent = 100 +private const val recordLabel = "recordingTransition" +private const val backgroundColorAlpha = 0.6f +private const val progressStartAngle = 270f + +@Composable +internal fun RecordMediaButton( + modifier: Modifier = Modifier, + isRecording: Boolean, + progress: Float, + progressBrush: Brush, + recordingButtonColor: Color = Color.Black, + defaultButtonColor: Color = Color.Red, + backgroundWhite: Color = Color.White, + backgroundWhiteAlpha: Color = backgroundWhite.copy(alpha = backgroundColorAlpha), + transparentWhite: Color = backgroundWhite.copy(alpha = 0f), + onClick: () -> Unit +) { + val recordingTransition = updateTransition(isRecording, label = recordLabel) + + val cornerRadius by recordingTransition.animateInt { recordingState -> + if (recordingState) recordMediaButtonRecordingCornersPercent + else recordMediaButtonDefaultCornersPercent + } + + val innerPadding by recordingTransition.animateDp { recordingState -> + if (recordingState) Dimens.spacingBig + else Dimens.spacingSmall + } + + val buttonColor by recordingTransition.animateColor { + if (it) recordingButtonColor else defaultButtonColor + } + + val backgroundColor by recordingTransition.animateColor { + if (it) backgroundWhiteAlpha else transparentWhite + } + + val borderColor by recordingTransition.animateColor { + if (it) transparentWhite else backgroundWhite + } + + val coercedProgress = progress.coerceIn(0f, 1f) + val stroke = with(LocalDensity.current) { + Stroke( + width = CameraRecorderDimens.recordMediaButtonProgressWidth.toPx(), + cap = StrokeCap.Round + ) + } + + Box( + modifier = modifier + .drawWithCache { + onDrawWithContent { + drawContent() + if (isRecording) { + val sweep = coercedProgress * 360f + drawCircularIndicator(progressStartAngle, sweep, progressBrush, stroke) + } + } + } + .clip(CircleShape) + .clickable(onClick = onClick) + .border( + width = CameraRecorderDimens.recordMediaButtonBorderWidth, + color = borderColor, + shape = CircleShape + ) + .background( + color = backgroundColor, + shape = CircleShape + ) + ) { + Box( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .background( + color = buttonColor, + shape = RoundedCornerShape(cornerRadius) + ) + ) + } +} + + +private fun DrawScope.drawCircularIndicator(startAngle: Float, sweep: Float, brush: Brush, stroke: Stroke) { + // To draw this circle we need a rect with edges that line up with the midpoint of the stroke. + // To do this we need to remove half the stroke width from the total diameter for both sides. + val diameterOffset = stroke.width / 2 + val arcDimen = size.width - 2 * diameterOffset + drawArc( + brush = brush, + startAngle = startAngle, + sweepAngle = sweep, + useCenter = false, + topLeft = Offset(diameterOffset, diameterOffset), + size = Size(arcDimen, arcDimen), + style = stroke + ) +} + +@Preview(showBackground = true, backgroundColor = 0xD8FFFFFF) +@Composable +private fun RecordMediaButtonPreview() { + var isRecording by remember { mutableStateOf(false) } + + RecordMediaButton( + modifier = Modifier.size(80.dp), + isRecording = isRecording, + progress = 1f, + progressBrush = SolidColor(Color.Black), + onClick = { + isRecording = !isRecording + } + ) +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/util/CacheFoldersConstants.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/util/CacheFoldersConstants.kt new file mode 100644 index 00000000..19e0a5bf --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/util/CacheFoldersConstants.kt @@ -0,0 +1,6 @@ +package com.urlaunched.android.design.ui.camera.util + +object CacheFoldersConstants { + const val VIDEO_RECORDINGS = "video_recordings" + const val PHOTOS = "photos" +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/util/MediaTypeExtensions.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/util/MediaTypeExtensions.kt new file mode 100644 index 00000000..2cc8ab77 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/util/MediaTypeExtensions.kt @@ -0,0 +1,17 @@ +package com.urlaunched.android.design.ui.camera.util + +import com.urlaunched.android.design.ui.camera.model.AppMediaType + +fun String.mimeToMediaType() = when { + startsWith("image/") -> { + AppMediaType.IMAGE_PREVIEW + } + + startsWith("video/") -> { + AppMediaType.VIDEO + } + + else -> { + null + } +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/util/PhotoTempFileProvider.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/util/PhotoTempFileProvider.kt new file mode 100644 index 00000000..7b5f49ae --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/util/PhotoTempFileProvider.kt @@ -0,0 +1,17 @@ +package com.urlaunched.android.design.ui.camera.util + +import android.content.Context +import com.urlaunched.android.design.ui.camera.model.AppMediaType +import java.io.File + +class PhotoTempFileProvider ( private val context: Context) { + operator fun invoke(): File = File(context.cacheDir, CacheFoldersConstants.PHOTOS).apply { + mkdir() + }.let { dir -> + File.createTempFile(TEMP_FILE_PREFIX, AppMediaType.IMAGE_PREVIEW.extensions.first(), dir) + } + + companion object { + private const val TEMP_FILE_PREFIX = "photo" + } +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/util/VideoRecordingTempFileProvider.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/util/VideoRecordingTempFileProvider.kt new file mode 100644 index 00000000..e0aacca5 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/util/VideoRecordingTempFileProvider.kt @@ -0,0 +1,17 @@ +package com.urlaunched.android.design.ui.camera.util + +import android.content.Context +import com.urlaunched.android.design.ui.camera.model.AppMediaType +import java.io.File + +class VideoRecordingTempFileProvider (private val context: Context) { + operator fun invoke(): File = File(context.cacheDir, CacheFoldersConstants.VIDEO_RECORDINGS).apply { + mkdir() + }.let { dir -> + File.createTempFile(TEMP_FILE_PREFIX, AppMediaType.VIDEO.extensions.first(), dir) + } + + companion object { + private const val TEMP_FILE_PREFIX = "video" + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 31fa6a59..4bc4b105 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -82,6 +82,13 @@ aspectjweaverVersion = "1.9.22.1" # bottomSheet bottomSheetVersion = "1.33.0" +# cameraxDependencies +cameraxVersion = "1.4.2" + +# guavaDependencies +guavaVersion = "33.4.0-android" +guavaAndroidxVersion = "1.2.0" + # plugins # dependencies androidApplicationPluginVersion = "8.4.2" @@ -185,6 +192,13 @@ junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" } espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCoreVersion" } material = { group = "com.google.android.material", name = "material", version.ref = "materialVersion" } +camerax-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraxVersion" } +camerax-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraxVersion" } +camerax-video = { module = "androidx.camera:camera-video", version.ref = "cameraxVersion" } +camerax-view = { module = "androidx.camera:camera-view", version.ref = "cameraxVersion" } + +guava-core = { module = "com.google.guava:guava", version.ref = "guavaVersion" } +guava-androidExt = { module = "androidx.concurrent:concurrent-futures-ktx", version.ref = "guavaAndroidxVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "androidApplicationPluginVersion" } From ff4ac12bcb6c8d295c85ddd7f5cf733b7b7766e7 Mon Sep 17 00:00:00 2001 From: BohunD Date: Thu, 17 Jul 2025 17:23:39 +0300 Subject: [PATCH 2/2] removed useless parameters --- design/api/current.api | 88 +++++++------------ .../design/ui/camera/ui/CameraContainer.kt | 20 ++--- .../design/ui/camera/ui/RecordMediaButton.kt | 6 +- 3 files changed, 40 insertions(+), 74 deletions(-) diff --git a/design/api/current.api b/design/api/current.api index f91bfed5..e432bca3 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -226,85 +226,57 @@ package com.urlaunched.android.design.ui.camera.model { package com.urlaunched.android.design.ui.camera.ui { - public final class CameraContainerConfig { - ctor public CameraContainerConfig(optional long backgroundColor, optional long surfaceColor, optional float surfaceCornerRadius, optional float surfacePaddingBottom, optional long closeButtonBackground, optional long closeButtonIconTint, optional kotlin.jvm.functions.Function0 closeButtonIcon, optional long flashButtonBackground, optional long flashButtonIconTint, optional float flashIconSize, optional float flashIconInnerPadding, optional kotlin.jvm.functions.Function0 flashOnIcon, optional kotlin.jvm.functions.Function0 flashOffIcon, optional float topButtonsSpacing, optional float controlPaddingHorizontal, optional float controlPaddingBottom, optional androidx.compose.ui.graphics.Brush progressBrush, optional long selectedOptionBackground, optional long selectedOptionTextColor, optional long unselectedOptionBackground, optional long unselectedOptionTextColor, optional long controlButtonBackground, optional long controlButtonBorderColor, optional long controlIconTint); + public final class CameraContainerColors { + ctor public CameraContainerColors(optional long backgroundColor, optional long surfaceColor, optional androidx.compose.ui.graphics.Brush progressBrush, optional long startRecordButtonColor, optional long recordingButtonColor); method public long component1-0d7_KjU(); - method public float component10-D9Ej5fM(); - method public float component11-D9Ej5fM(); - method public kotlin.jvm.functions.Function0 component12(); - method public kotlin.jvm.functions.Function0 component13(); - method public float component14-D9Ej5fM(); - method public float component15-D9Ej5fM(); - method public float component16-D9Ej5fM(); - method public androidx.compose.ui.graphics.Brush component17(); - method public long component18-0d7_KjU(); - method public long component19-0d7_KjU(); method public long component2-0d7_KjU(); - method public long component20-0d7_KjU(); - method public long component21-0d7_KjU(); - method public long component22-0d7_KjU(); - method public long component23-0d7_KjU(); - method public long component24-0d7_KjU(); - method public float component3-D9Ej5fM(); - method public float component4-D9Ej5fM(); + method public androidx.compose.ui.graphics.Brush component3(); + method public long component4-0d7_KjU(); method public long component5-0d7_KjU(); - method public long component6-0d7_KjU(); - method public kotlin.jvm.functions.Function0 component7(); - method public long component8-0d7_KjU(); - method public long component9-0d7_KjU(); - method public com.urlaunched.android.design.ui.camera.ui.CameraContainerConfig copy-ZR-Z7PU(long backgroundColor, long surfaceColor, float surfaceCornerRadius, float surfacePaddingBottom, long closeButtonBackground, long closeButtonIconTint, kotlin.jvm.functions.Function0 closeButtonIcon, long flashButtonBackground, long flashButtonIconTint, float flashIconSize, float flashIconInnerPadding, kotlin.jvm.functions.Function0 flashOnIcon, kotlin.jvm.functions.Function0 flashOffIcon, float topButtonsSpacing, float controlPaddingHorizontal, float controlPaddingBottom, androidx.compose.ui.graphics.Brush progressBrush, long selectedOptionBackground, long selectedOptionTextColor, long unselectedOptionBackground, long unselectedOptionTextColor, long controlButtonBackground, long controlButtonBorderColor, long controlIconTint); + method public com.urlaunched.android.design.ui.camera.ui.CameraContainerColors copy-w8f9n5g(long backgroundColor, long surfaceColor, androidx.compose.ui.graphics.Brush progressBrush, long startRecordButtonColor, long recordingButtonColor); method public long getBackgroundColor(); - method public long getCloseButtonBackground(); - method public kotlin.jvm.functions.Function0 getCloseButtonIcon(); - method public long getCloseButtonIconTint(); - method public long getControlButtonBackground(); - method public long getControlButtonBorderColor(); - method public long getControlIconTint(); + method public androidx.compose.ui.graphics.Brush getProgressBrush(); + method public long getRecordingButtonColor(); + method public long getStartRecordButtonColor(); + method public long getSurfaceColor(); + property public final long backgroundColor; + property public final androidx.compose.ui.graphics.Brush progressBrush; + property public final long recordingButtonColor; + property public final long startRecordButtonColor; + property public final long surfaceColor; + } + + public final class CameraContainerConfig { + ctor public CameraContainerConfig(optional com.urlaunched.android.design.ui.camera.ui.CameraContainerColors colors, optional float surfaceCornerRadius, optional float surfacePaddingBottom, optional float flashIconSize, optional float flashIconInnerPadding, optional float topButtonsSpacing, optional float controlPaddingHorizontal, optional float controlPaddingBottom); + method public com.urlaunched.android.design.ui.camera.ui.CameraContainerColors component1(); + method public float component2-D9Ej5fM(); + method public float component3-D9Ej5fM(); + method public float component4-D9Ej5fM(); + method public float component5-D9Ej5fM(); + method public float component6-D9Ej5fM(); + method public float component7-D9Ej5fM(); + method public float component8-D9Ej5fM(); + method public com.urlaunched.android.design.ui.camera.ui.CameraContainerConfig copy-a145CXI(com.urlaunched.android.design.ui.camera.ui.CameraContainerColors colors, float surfaceCornerRadius, float surfacePaddingBottom, float flashIconSize, float flashIconInnerPadding, float topButtonsSpacing, float controlPaddingHorizontal, float controlPaddingBottom); + method public com.urlaunched.android.design.ui.camera.ui.CameraContainerColors getColors(); method public float getControlPaddingBottom(); method public float getControlPaddingHorizontal(); - method public long getFlashButtonBackground(); - method public long getFlashButtonIconTint(); method public float getFlashIconInnerPadding(); method public float getFlashIconSize(); - method public kotlin.jvm.functions.Function0 getFlashOffIcon(); - method public kotlin.jvm.functions.Function0 getFlashOnIcon(); - method public androidx.compose.ui.graphics.Brush getProgressBrush(); - method public long getSelectedOptionBackground(); - method public long getSelectedOptionTextColor(); - method public long getSurfaceColor(); method public float getSurfaceCornerRadius(); method public float getSurfacePaddingBottom(); method public float getTopButtonsSpacing(); - method public long getUnselectedOptionBackground(); - method public long getUnselectedOptionTextColor(); - property public final long backgroundColor; - property public final long closeButtonBackground; - property public final kotlin.jvm.functions.Function0 closeButtonIcon; - property public final long closeButtonIconTint; - property public final long controlButtonBackground; - property public final long controlButtonBorderColor; - property public final long controlIconTint; + property public final com.urlaunched.android.design.ui.camera.ui.CameraContainerColors colors; property public final float controlPaddingBottom; property public final float controlPaddingHorizontal; - property public final long flashButtonBackground; - property public final long flashButtonIconTint; property public final float flashIconInnerPadding; property public final float flashIconSize; - property public final kotlin.jvm.functions.Function0 flashOffIcon; - property public final kotlin.jvm.functions.Function0 flashOnIcon; - property public final androidx.compose.ui.graphics.Brush progressBrush; - property public final long selectedOptionBackground; - property public final long selectedOptionTextColor; - property public final long surfaceColor; property public final float surfaceCornerRadius; property public final float surfacePaddingBottom; property public final float topButtonsSpacing; - property public final long unselectedOptionBackground; - property public final long unselectedOptionTextColor; } public final class CameraContainerKt { - method @androidx.compose.runtime.Composable public static void CameraContainer(optional androidx.compose.ui.Modifier modifier, optional com.urlaunched.android.design.ui.camera.ui.CameraContainerConfig config, com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel recordingState, com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel cameraMode, boolean isFlashEnabled, boolean isGalleryEnabled, kotlin.jvm.functions.Function0 selectedCameraOption, kotlin.jvm.functions.Function1,kotlin.Unit> unselectedCameraOption, kotlin.jvm.functions.Function1,kotlin.Unit> galleryButton, kotlin.jvm.functions.Function1,kotlin.Unit> cameraToggleButton, kotlin.jvm.functions.Function0 onShutterClick, optional kotlin.jvm.functions.Function1 recordMediaButton, kotlin.jvm.functions.Function1 onCameraPreviewViewAvailable, kotlin.jvm.functions.Function0 onVideoCameraClick, kotlin.jvm.functions.Function0 onPhotoCameraClick, kotlin.jvm.functions.Function0 onToggleCameraClick, kotlin.jvm.functions.Function0 onToggleFlashClick, kotlin.jvm.functions.Function0 onGalleryClick, kotlin.jvm.functions.Function0 goBack); + method @androidx.compose.runtime.Composable public static void CameraContainer(optional androidx.compose.ui.Modifier modifier, optional com.urlaunched.android.design.ui.camera.ui.CameraContainerConfig config, com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationModel recordingState, com.urlaunched.android.design.ui.camera.model.CameraModeTypePresentationModel cameraMode, boolean isFlashEnabled, boolean isGalleryEnabled, kotlin.jvm.functions.Function0 onShutterClick, kotlin.jvm.functions.Function1 onCameraPreviewViewAvailable, kotlin.jvm.functions.Function0 onVideoCameraClick, kotlin.jvm.functions.Function0 onPhotoCameraClick, kotlin.jvm.functions.Function0 onToggleCameraClick, kotlin.jvm.functions.Function0 onToggleFlashClick, kotlin.jvm.functions.Function0 onGalleryClick, kotlin.jvm.functions.Function0 goBack, kotlin.jvm.functions.Function0 selectedCameraOption, kotlin.jvm.functions.Function1,kotlin.Unit> unselectedCameraOption, kotlin.jvm.functions.Function1,kotlin.Unit> galleryButton, kotlin.jvm.functions.Function1,kotlin.Unit> cameraToggleButton, optional kotlin.jvm.functions.Function0 closeButton, optional kotlin.jvm.functions.Function0 flashOnButton, optional kotlin.jvm.functions.Function0 flashOffButton, optional kotlin.jvm.functions.Function1 recordMediaButton); } public final class CircleButtonKt { diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraContainer.kt index da9d3096..62f4d48b 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/CameraContainer.kt @@ -1,5 +1,6 @@ package com.urlaunched.android.design.ui.camera.ui +import androidx.camera.view.PreviewView import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -32,18 +33,9 @@ import com.urlaunched.android.design.ui.camera.model.RecordingStatePresentationM data class CameraContainerColors( val backgroundColor: Color = Color.Black, val surfaceColor: Color = Color.White, - val closeButtonBackground: Color = Color.Black, - val closeButtonIconTint: Color = Color.White, - val flashButtonBackground: Color = Color.Black, - val flashButtonIconTint: Color = Color.White, val progressBrush: Brush = SolidColor(Color.Black), - val selectedOptionBackground: Color = Color.White, - val selectedOptionTextColor: Color = Color.Black, - val unselectedOptionBackground: Color = Color.DarkGray, - val unselectedOptionTextColor: Color = Color.White, - val controlButtonBackground: Color = Color.Black, - val controlButtonBorderColor: Color = Color.DarkGray, - val controlIconTint: Color = Color.White, + val startRecordButtonColor: Color = Color.Red, + val recordingButtonColor: Color = Color.Red, ) data class CameraContainerConfig( @@ -66,7 +58,7 @@ fun CameraContainer( isFlashEnabled: Boolean, isGalleryEnabled: Boolean, onShutterClick: () -> Unit, - onCameraPreviewViewAvailable: (androidx.camera.view.PreviewView) -> Unit, + onCameraPreviewViewAvailable: (PreviewView) -> Unit, onVideoCameraClick: () -> Unit, onPhotoCameraClick: () -> Unit, onToggleCameraClick: () -> Unit, @@ -88,7 +80,9 @@ fun CameraContainer( isRecording = recordingState is RecordingStatePresentationModel.Recording, progress = (recordingState as? RecordingStatePresentationModel.Recording)?.progress ?: 0f, progressBrush = config.colors.progressBrush, - onClick = onShutterClick + defaultButtonColor = config.colors.startRecordButtonColor, + onClick = onShutterClick, + recordingButtonColor = config.colors.recordingButtonColor, ) } ) { diff --git a/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/RecordMediaButton.kt b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/RecordMediaButton.kt index e68318f8..8dd75dae 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/RecordMediaButton.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/camera/ui/RecordMediaButton.kt @@ -48,11 +48,11 @@ internal fun RecordMediaButton( progressBrush: Brush, recordingButtonColor: Color = Color.Black, defaultButtonColor: Color = Color.Red, - backgroundWhite: Color = Color.White, - backgroundWhiteAlpha: Color = backgroundWhite.copy(alpha = backgroundColorAlpha), - transparentWhite: Color = backgroundWhite.copy(alpha = 0f), onClick: () -> Unit ) { + val backgroundWhite: Color = Color.White + val backgroundWhiteAlpha: Color = backgroundWhite.copy(alpha = backgroundColorAlpha) + val transparentWhite: Color = backgroundWhite.copy(alpha = 0f) val recordingTransition = updateTransition(isRecording, label = recordLabel) val cornerRadius by recordingTransition.animateInt { recordingState ->