Create initial markup layers boilerplate

This commit is contained in:
T8RIN
2024-11-24 04:21:24 +03:00
parent c8e9e55ceb
commit 3fbd216a7f
14 changed files with 451 additions and 7 deletions

View File

@ -1497,4 +1497,6 @@
<string name="enable_tonemapping">Enable Tonemapping</string>
<string name="enter_percent">Enter %</string>
<string name="unknown_host">Cannot access the site, try using VPN or check if the url correct</string>
<string name="markup_layers">Markup Layers</string>
<string name="markup_layers_sub">Layers mode with ability to freely place images, text and more</string>
</resources>

View File

@ -36,6 +36,7 @@ import androidx.compose.material.icons.outlined.FolderZip
import androidx.compose.material.icons.outlined.GifBox
import androidx.compose.material.icons.outlined.Gradient
import androidx.compose.material.icons.outlined.Grain
import androidx.compose.material.icons.outlined.Layers
import androidx.compose.material.icons.outlined.Photo
import androidx.compose.material.icons.outlined.PictureAsPdf
import androidx.compose.material.icons.outlined.QrCode
@ -125,6 +126,7 @@ sealed class Screen(
is NoiseGeneration -> "Noise_Generation"
is CollageMaker -> "Collage_Maker"
is LibrariesInfo -> "Libraries_Info"
is MarkupLayers -> "Markup Layers"
}
val icon: ImageVector?
@ -168,8 +170,16 @@ sealed class Screen(
is WebpTools -> Icons.Rounded.WebpBox
NoiseGeneration -> Icons.Outlined.Grain
is CollageMaker -> Icons.Outlined.AutoAwesomeMosaic
is MarkupLayers -> Icons.Outlined.Layers //TODO: Icons for this and stacking
}
@Serializable
data object LibrariesInfo : Screen(
id = -4,
title = 0,
subtitle = 0
)
@Serializable
data object Settings : Screen(
id = -3,
@ -780,10 +790,12 @@ sealed class Screen(
)
@Serializable
data object LibrariesInfo : Screen(
id = -4,
title = 0,
subtitle = 0
data class MarkupLayers(
val uri: KUri? = null
) : Screen(
id = 34,
title = R.string.markup_layers,
subtitle = R.string.markup_layers_sub
)
companion object {
@ -806,6 +818,7 @@ sealed class Screen(
Filter(),
Draw(),
EraseBackground(),
MarkupLayers(),
CollageMaker(),
ImageStitching(),
ImageStacking(),

1
feature/markup-layers/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,25 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
plugins {
alias(libs.plugins.image.toolbox.library)
alias(libs.plugins.image.toolbox.feature)
alias(libs.plugins.image.toolbox.hilt)
alias(libs.plugins.image.toolbox.compose)
}
android.namespace = "ru.tech.imageresizershrinker.feature.markup_layers"

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
</manifest>

View File

@ -0,0 +1,28 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.feature.markup_layers.presentation
import androidx.compose.runtime.Composable
import ru.tech.imageresizershrinker.feature.markup_layers.presentation.screenLogic.MarkupLayersComponent
@Composable
fun MarkupLayersContent(
component: MarkupLayersComponent
) {
}

View File

@ -0,0 +1,206 @@
package ru.tech.imageresizershrinker.feature.markup_layers.presentation.components
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.InfiniteTransition
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.rememberTransformableState
import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
@Composable
fun BoxWithConstraintsScope.EditBox(
onTap: () -> Unit,
modifier: Modifier = Modifier,
state: EditBoxState = remember { EditBoxState() },
shape: Shape = RoundedCornerShape(4.dp),
content: @Composable BoxScope.() -> Unit
) {
val parentSize by remember(constraints) {
derivedStateOf {
IntSize(
constraints.maxWidth,
constraints.maxHeight
)
}
}
EditBox(
modifier = modifier,
onTap = onTap,
state = state,
parentSize = parentSize,
shape = shape,
content = content
)
}
@Composable
fun EditBox(
onTap: () -> Unit,
parentSize: IntSize,
modifier: Modifier = Modifier,
state: EditBoxState = remember { EditBoxState() },
shape: Shape = RoundedCornerShape(4.dp),
content: @Composable BoxScope.() -> Unit
) {
var contentSize by remember {
mutableStateOf(IntSize.Zero)
}
val parentMaxWidth = parentSize.width
val parentMaxHeight = parentSize.height
SideEffect {
state.canvasSize = parentSize
}
val transformState = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
state.applyChanges(
parentMaxWidth = parentMaxWidth,
parentMaxHeight = parentMaxHeight,
contentSize = contentSize,
zoomChange = zoomChange,
offsetChange = offsetChange,
rotationChange = rotationChange
)
}
val tapScale = remember { Animatable(1f) }
LaunchedEffect(state.isActive) {
if (state.isActive) {
tapScale.animateTo(0.95f)
tapScale.animateTo(1.02f)
tapScale.animateTo(1f)
}
}
val borderAlpha by animateFloatAsState(if (state.isActive) 1f else 0f)
Box(
modifier = modifier
.onSizeChanged {
contentSize = it
}
.graphicsLayer(
scaleX = state.scale,
scaleY = state.scale,
rotationZ = state.rotation,
translationX = state.offset.x,
translationY = state.offset.y
)
.scale(tapScale.value)
.clip(shape)
.background(MaterialTheme.colorScheme.primary.copy(0.2f * borderAlpha))
.pointerInput(onTap) {
detectTapGestures {
onTap()
}
}
.transformable(
state = transformState,
enabled = state.isActive
),
contentAlignment = Alignment.Center
) {
content()
AnimatedBorder(
modifier = Modifier.matchParentSize(),
alpha = borderAlpha,
scale = state.scale,
shape = shape
)
Surface(
color = Color.Transparent,
modifier = Modifier.matchParentSize()
) { }
}
}
@Composable
internal fun AnimatedBorder(
modifier: Modifier,
alpha: Float,
scale: Float,
shape: Shape
) {
val transition: InfiniteTransition = rememberInfiniteTransition()
// Infinite phase animation for PathEffect
val phase by transition.animateFloat(
initialValue = 0f,
targetValue = 80f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
)
)
val pathEffect = PathEffect.dashPathEffect(
intervals = floatArrayOf(20f, 20f),
phase = phase
)
val density = LocalDensity.current
val colorScheme = MaterialTheme.colorScheme
Canvas(modifier = modifier) {
val outline = shape.createOutline(
size = size,
layoutDirection = layoutDirection,
density = density
)
drawOutline(
outline = outline,
color = colorScheme.primary.copy(alpha),
style = Stroke(
width = 3.dp.toPx() * (1f / scale)
)
)
drawOutline(
outline = outline,
color = colorScheme.primaryContainer.copy(alpha),
style = Stroke(
width = 3.dp.toPx() * (1f / scale),
pathEffect = pathEffect
)
)
}
}

View File

@ -0,0 +1,93 @@
package ru.tech.imageresizershrinker.feature.markup_layers.presentation.components
import androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastCoerceIn
import kotlin.math.absoluteValue
import kotlin.math.cos
import kotlin.math.sin
class EditBoxState(
scale: Float = 1f,
rotation: Float = 0f,
offset: Offset = Offset.Companion.Zero,
isActive: Boolean = false
) {
var isActive by mutableStateOf(isActive)
internal set
fun activate() {
isActive = true
}
fun deactivate() {
isActive = false
}
internal fun applyChanges(
parentMaxWidth: Int,
parentMaxHeight: Int,
contentSize: IntSize,
zoomChange: Float,
offsetChange: Offset,
rotationChange: Float
) {
rotation += rotationChange
scale = (scale * zoomChange).fastCoerceIn(0.5f, 10f)
val panChange = (offsetChange * scale).rotateBy(rotation)
val extraWidth = (parentMaxWidth - contentSize.width * scale).absoluteValue
val extraHeight = (parentMaxHeight - contentSize.height * scale).absoluteValue
val maxX = extraWidth / 2
val maxY = extraHeight / 2
offset = Offset(
x = (offset.x + panChange.x).coerceIn(-maxX, maxX),
y = (offset.y + panChange.y).coerceIn(-maxY, maxY),
)
}
var scale by mutableFloatStateOf(scale)
internal set
var rotation by mutableFloatStateOf(rotation)
internal set
var offset by mutableStateOf(offset)
internal set
private val IntSize.aspect: Float get() = width / height.toFloat()
private val _canvasSize = mutableStateOf(IntSize.Companion.Zero)
var canvasSize: IntSize
get() = _canvasSize.value
set(value) {
if (_canvasSize.value != IntSize.Companion.Zero && _canvasSize.value != value) {
val sx = value.width.toFloat() / _canvasSize.value.width
val sy = value.height.toFloat() / _canvasSize.value.height
if (_canvasSize.value.aspect < value.aspect) {
scale *= minOf(sx, sy)
offset *= minOf(sx, sy)
} else {
scale /= minOf(sx, sy)
offset /= minOf(sx, sy)
}
}
_canvasSize.value = value
}
}
private fun Offset.rotateBy(
angle: Float
): Offset {
val angleInRadians = ROTATION_CONST * angle
val newX = x * cos(angleInRadians) - y * sin(angleInRadians)
val newY = x * sin(angleInRadians) + y * cos(angleInRadians)
return Offset(newX, newY)
}
private const val ROTATION_CONST = (Math.PI / 180f).toFloat()

View File

@ -0,0 +1,48 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package ru.tech.imageresizershrinker.feature.markup_layers.presentation.screenLogic
import android.net.Uri
import com.arkivanov.decompose.ComponentContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import ru.tech.imageresizershrinker.core.domain.dispatchers.DispatchersHolder
import ru.tech.imageresizershrinker.core.ui.utils.BaseComponent
import ru.tech.imageresizershrinker.core.ui.utils.navigation.Screen
class MarkupLayersComponent @AssistedInject internal constructor(
@Assisted componentContext: ComponentContext,
@Assisted initialUri: Uri?,
@Assisted val onGoBack: () -> Unit,
@Assisted val onNavigate: (Screen) -> Unit,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {
@AssistedFactory
fun interface Factory {
operator fun invoke(
componentContext: ComponentContext,
initialUri: Uri?,
onGoBack: () -> Unit,
onNavigate: (Screen) -> Unit,
): MarkupLayersComponent
}
}

View File

@ -63,4 +63,5 @@ dependencies {
implementation(projects.feature.noiseGeneration)
implementation(projects.feature.colllageMaker)
implementation(projects.feature.librariesInfo)
implementation(projects.feature.markupLayers)
}

View File

@ -43,6 +43,7 @@ import ru.tech.imageresizershrinker.feature.libraries_info.presentation.screenLo
import ru.tech.imageresizershrinker.feature.limits_resize.presentation.screenLogic.LimitsResizeComponent
import ru.tech.imageresizershrinker.feature.load_net_image.presentation.screenLogic.LoadNetImageComponent
import ru.tech.imageresizershrinker.feature.main.presentation.screenLogic.MainComponent
import ru.tech.imageresizershrinker.feature.markup_layers.presentation.screenLogic.MarkupLayersComponent
import ru.tech.imageresizershrinker.feature.pdf_tools.presentation.screenLogic.PdfToolsComponent
import ru.tech.imageresizershrinker.feature.pick_color.presentation.screenLogic.PickColorFromImageComponent
import ru.tech.imageresizershrinker.feature.recognize.text.presentation.screenLogic.RecognizeTextComponent
@ -98,7 +99,8 @@ internal class ChildProvider @Inject constructor(
private val easterEggComponentFactory: EasterEggComponent.Factory,
private val colorToolsComponentFactory: ColorToolsComponent.Factory,
private val librariesInfoComponentFactory: LibrariesInfoComponent.Factory,
private val mainComponentFactory: MainComponent.Factory
private val mainComponentFactory: MainComponent.Factory,
private val markupLayersComponentFactory: MarkupLayersComponent.Factory
) {
fun RootComponent.createChild(
config: Screen,
@ -430,5 +432,14 @@ internal class ChildProvider @Inject constructor(
onGoBack = ::navigateBack
)
)
is Screen.MarkupLayers -> NavigationChild.MarkupLayers(
markupLayersComponentFactory(
componentContext = componentContext,
initialUri = config.uri,
onGoBack = ::navigateBack,
onNavigate = ::navigateTo
)
)
}
}

View File

@ -66,6 +66,8 @@ import ru.tech.imageresizershrinker.feature.load_net_image.presentation.LoadNetI
import ru.tech.imageresizershrinker.feature.load_net_image.presentation.screenLogic.LoadNetImageComponent
import ru.tech.imageresizershrinker.feature.main.presentation.MainContent
import ru.tech.imageresizershrinker.feature.main.presentation.screenLogic.MainComponent
import ru.tech.imageresizershrinker.feature.markup_layers.presentation.MarkupLayersContent
import ru.tech.imageresizershrinker.feature.markup_layers.presentation.screenLogic.MarkupLayersComponent
import ru.tech.imageresizershrinker.feature.pdf_tools.presentation.PdfToolsContent
import ru.tech.imageresizershrinker.feature.pdf_tools.presentation.screenLogic.PdfToolsComponent
import ru.tech.imageresizershrinker.feature.pick_color.presentation.PickColorFromImageContent
@ -292,4 +294,9 @@ internal sealed class NavigationChild {
override fun Content() = LibrariesInfoContent(component)
}
class MarkupLayers(val component: MarkupLayersComponent) : NavigationChild() {
@Composable
override fun Content() = MarkupLayersContent(component)
}
}

View File

@ -3,8 +3,8 @@ androidMinSdk = "21"
androidTargetSdk = "35"
androidCompileSdk = "35"
versionName = "3.1.0"
versionCode = "159"
versionName = "3.2.0-alpha01"
versionCode = "160"
jvmTarget = "17"
@ -75,6 +75,7 @@ zxingAndroidEmbedded = "4.3.0"
capturable = "3.0.0"
moshi = "1.15.1"
aboutlibraries = "11.2.3"
junit = "4.13.2"
[libraries]
aboutlibraries-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" }
@ -193,6 +194,7 @@ androidx-material = { module = "androidx.compose.material:material", version.ref
androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugaring" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
[plugins]
image-toolbox-library = { id = "image.toolbox.library", version = "unspecified" }

View File

@ -17,6 +17,9 @@
@file:Suppress("UnstableApiUsage")
include(":feature:markup-layers")
include(":feature:libraries-info")