From 4cae25f34dab08b279ac9527f953df4ec95ddd59 Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Tue, 20 Aug 2024 20:58:22 +0300 Subject: [PATCH 01/16] use interop filter to get access to the motion event for drawing --- .../composables/ArtMakerDrawScreen.kt | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt b/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt index 7240ab54..41285328 100644 --- a/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt +++ b/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt @@ -17,10 +17,9 @@ package io.artmaker.composables import android.Manifest import android.os.Build +import android.view.MotionEvent import androidx.compose.foundation.Canvas import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable @@ -31,6 +30,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset @@ -44,7 +44,7 @@ import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.graphics.rememberGraphicsLayer -import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext @@ -64,7 +64,7 @@ import kotlinx.coroutines.launch /** * [ArtMakerDrawScreen] is the composable that handles the drawing. */ -@OptIn(ExperimentalPermissionsApi::class) +@OptIn(ExperimentalPermissionsApi::class, ExperimentalComposeUiApi::class) @Composable internal fun ArtMakerDrawScreen( modifier: Modifier = Modifier, @@ -142,27 +142,24 @@ internal fun ArtMakerDrawScreen( drawLayer(graphicsLayer) } } - .pointerInput(Unit) { - detectTapGestures( - onTap = { offset -> + .pointerInteropFilter { event -> + val offset = Offset(event.x, event.y) + when (event.action) { + MotionEvent.ACTION_DOWN -> { onDrawEvent(DrawEvent.AddNewShape(offset)) - }, - ) - } - .pointerInput(Unit) { - detectDragGestures( - onDragStart = { offset -> - onDrawEvent(DrawEvent.AddNewShape(offset)) - }, - onDragCancel = { + } + + MotionEvent.ACTION_MOVE -> { + val clampedOffset = + Offset(x = offset.x, y = clamp(offset.y, 0f, clippedScreenHeight)) + onDrawEvent(DrawEvent.UpdateCurrentShape(clampedOffset)) + } + + MotionEvent.ACTION_CANCEL -> { onDrawEvent(DrawEvent.UndoLastShapePoint) - }, - ) { change, _ -> - val offset = change.position - val clampedOffset = - Offset(x = offset.x, y = clamp(offset.y, 0f, clippedScreenHeight)) - onDrawEvent(DrawEvent.UpdateCurrentShape(clampedOffset)) + } } + true }, onDraw = { drawIntoCanvas { canvas -> From 8d252fa5f5e34f12cf91c1ce386be2a447903db3 Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Wed, 21 Aug 2024 19:52:31 +0300 Subject: [PATCH 02/16] housekeeping --- artmaker/src/main/java/io/artmaker/ArtMaker.kt | 1 - .../main/java/io/artmaker/{state => }/ArtMakerUIState.kt | 2 +- .../io/artmaker/{viewmodels => }/ArtMakerViewModel.kt | 9 ++++----- .../java/io/artmaker/composables/ArtMakerControlMenu.kt | 2 +- .../java/io/artmaker/composables/StrokeWidthSlider.kt | 2 +- .../ArtMakerSharedPreferences.kt | 2 +- .../{sharedpreferences => data}/PreferenceKeys.kt | 2 +- 7 files changed, 9 insertions(+), 11 deletions(-) rename artmaker/src/main/java/io/artmaker/{state => }/ArtMakerUIState.kt (97%) rename artmaker/src/main/java/io/artmaker/{viewmodels => }/ArtMakerViewModel.kt (96%) rename artmaker/src/main/java/io/artmaker/{sharedpreferences => data}/ArtMakerSharedPreferences.kt (98%) rename artmaker/src/main/java/io/artmaker/{sharedpreferences => data}/PreferenceKeys.kt (95%) diff --git a/artmaker/src/main/java/io/artmaker/ArtMaker.kt b/artmaker/src/main/java/io/artmaker/ArtMaker.kt index e54ec82f..bde9472b 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMaker.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMaker.kt @@ -49,7 +49,6 @@ import io.artmaker.composables.ArtMakerControlMenu import io.artmaker.composables.ArtMakerDrawScreen import io.artmaker.composables.StrokeWidthSlider import io.artmaker.models.ArtMakerConfiguration -import io.artmaker.viewmodels.ArtMakerViewModel import io.fbada006.artmaker.R /** diff --git a/artmaker/src/main/java/io/artmaker/state/ArtMakerUIState.kt b/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt similarity index 97% rename from artmaker/src/main/java/io/artmaker/state/ArtMakerUIState.kt rename to artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt index 80fe5ee9..0bb6f4e3 100644 --- a/artmaker/src/main/java/io/artmaker/state/ArtMakerUIState.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.artmaker.state +package io.artmaker import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb diff --git a/artmaker/src/main/java/io/artmaker/viewmodels/ArtMakerViewModel.kt b/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt similarity index 96% rename from artmaker/src/main/java/io/artmaker/viewmodels/ArtMakerViewModel.kt rename to artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt index 476e70e4..c5e3f2da 100644 --- a/artmaker/src/main/java/io/artmaker/viewmodels/ArtMakerViewModel.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.artmaker.viewmodels +package io.artmaker import android.app.Application import android.content.Context @@ -35,10 +35,9 @@ import io.artmaker.actions.ArtMakerAction import io.artmaker.actions.DrawEvent import io.artmaker.actions.ExportType import io.artmaker.models.PointsData -import io.artmaker.sharedpreferences.ArtMakerSharedPreferences -import io.artmaker.sharedpreferences.PreferenceKeys -import io.artmaker.sharedpreferences.PreferenceKeys.SELECTED_STROKE_WIDTH -import io.artmaker.state.ArtMakerUIState +import io.artmaker.data.ArtMakerSharedPreferences +import io.artmaker.data.PreferenceKeys +import io.artmaker.data.PreferenceKeys.SELECTED_STROKE_WIDTH import io.artmaker.utils.saveToDisk import io.artmaker.utils.shareBitmap import kotlinx.coroutines.flow.MutableStateFlow diff --git a/artmaker/src/main/java/io/artmaker/composables/ArtMakerControlMenu.kt b/artmaker/src/main/java/io/artmaker/composables/ArtMakerControlMenu.kt index 1ebaa163..0ffc4027 100644 --- a/artmaker/src/main/java/io/artmaker/composables/ArtMakerControlMenu.kt +++ b/artmaker/src/main/java/io/artmaker/composables/ArtMakerControlMenu.kt @@ -62,7 +62,7 @@ import androidx.core.os.BuildCompat import com.google.modernstorage.photopicker.PhotoPicker import io.artmaker.actions.ArtMakerAction import io.artmaker.models.ArtMakerConfiguration -import io.artmaker.state.ArtMakerUIState +import io.artmaker.ArtMakerUIState import io.artmaker.utils.ColorUtils import io.fbada006.artmaker.R diff --git a/artmaker/src/main/java/io/artmaker/composables/StrokeWidthSlider.kt b/artmaker/src/main/java/io/artmaker/composables/StrokeWidthSlider.kt index 85e4c8b8..1b6807e6 100644 --- a/artmaker/src/main/java/io/artmaker/composables/StrokeWidthSlider.kt +++ b/artmaker/src/main/java/io/artmaker/composables/StrokeWidthSlider.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import io.artmaker.actions.ArtMakerAction import io.artmaker.models.ArtMakerConfiguration -import io.artmaker.state.ArtMakerUIState +import io.artmaker.ArtMakerUIState import io.fbada006.artmaker.R /** diff --git a/artmaker/src/main/java/io/artmaker/sharedpreferences/ArtMakerSharedPreferences.kt b/artmaker/src/main/java/io/artmaker/data/ArtMakerSharedPreferences.kt similarity index 98% rename from artmaker/src/main/java/io/artmaker/sharedpreferences/ArtMakerSharedPreferences.kt rename to artmaker/src/main/java/io/artmaker/data/ArtMakerSharedPreferences.kt index fde46235..d6a6a374 100644 --- a/artmaker/src/main/java/io/artmaker/sharedpreferences/ArtMakerSharedPreferences.kt +++ b/artmaker/src/main/java/io/artmaker/data/ArtMakerSharedPreferences.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.artmaker.sharedpreferences +package io.artmaker.data import android.content.Context import android.content.SharedPreferences diff --git a/artmaker/src/main/java/io/artmaker/sharedpreferences/PreferenceKeys.kt b/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt similarity index 95% rename from artmaker/src/main/java/io/artmaker/sharedpreferences/PreferenceKeys.kt rename to artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt index 0369f4b1..e35c7583 100644 --- a/artmaker/src/main/java/io/artmaker/sharedpreferences/PreferenceKeys.kt +++ b/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.artmaker.sharedpreferences +package io.artmaker.data object PreferenceKeys { const val SELECTED_BACKGROUND_COLOUR = "com.artmaker.sharedpreferences.selectedBackgroundColour" From d02721a2c07437a90b4933e6171db09a82dab4ba Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Wed, 21 Aug 2024 20:51:04 +0300 Subject: [PATCH 03/16] add a stylus config composable --- .../src/main/java/io/artmaker/ArtMaker.kt | 27 ++++--- .../java/io/artmaker/ArtMakerViewModel.kt | 4 +- .../io/artmaker/actions/ArtMakerAction.kt | 2 +- .../composables/ArtMakerControlMenu.kt | 2 +- .../java/io/artmaker/composables/Slider.kt | 20 +++-- .../io/artmaker/composables/StrokeSettings.kt | 61 +++++++++++++++ .../artmaker/composables/StrokeWidthSlider.kt | 75 ------------------- artmaker/src/main/res/values/strings.xml | 2 +- 8 files changed, 90 insertions(+), 103 deletions(-) create mode 100644 artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt delete mode 100644 artmaker/src/main/java/io/artmaker/composables/StrokeWidthSlider.kt diff --git a/artmaker/src/main/java/io/artmaker/ArtMaker.kt b/artmaker/src/main/java/io/artmaker/ArtMaker.kt index bde9472b..aa649ae5 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMaker.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMaker.kt @@ -47,7 +47,7 @@ import io.artmaker.actions.ArtMakerAction import io.artmaker.actions.ExportType import io.artmaker.composables.ArtMakerControlMenu import io.artmaker.composables.ArtMakerDrawScreen -import io.artmaker.composables.StrokeWidthSlider +import io.artmaker.composables.StrokeSettings import io.artmaker.models.ArtMakerConfiguration import io.fbada006.artmaker.R @@ -70,8 +70,8 @@ fun ArtMaker( val viewModel: ArtMakerViewModel = viewModel( factory = ArtMakerViewModel.provideFactory(application = context.applicationContext as Application), ) - var showStrokeWidth by remember { mutableStateOf(value = false) } - val artMakerUIState by viewModel.artMakerUIState.collectAsStateWithLifecycle() + var showStrokeSettings by remember { mutableStateOf(value = false) } + val state by viewModel.artMakerUIState.collectAsStateWithLifecycle() val shouldTriggerArtExport by viewModel.shouldTriggerArtExport.collectAsStateWithLifecycle() val finishedImage by viewModel.finishedImage.collectAsStateWithLifecycle() var isFullScreenEnabled by remember { mutableStateOf(false) } @@ -115,7 +115,7 @@ fun ArtMaker( .weight(1f), artMakerConfiguration = artMakerConfiguration, onDrawEvent = { - showStrokeWidth = false + showStrokeSettings = false viewModel.onDrawEvent(it) }, onAction = viewModel::onAction, @@ -124,19 +124,22 @@ fun ArtMaker( imageBitmap = viewModel.backgroundImage.value, isFullScreenMode = isFullScreenEnabled, ) - StrokeWidthSlider( - state = artMakerUIState, - onAction = viewModel::onAction, - isVisible = showStrokeWidth, - artMakerConfiguration = artMakerConfiguration, - ) + AnimatedVisibility(visible = showStrokeSettings) { + StrokeSettings( + strokeWidth = state.strokeWidth, + onAction = viewModel::onAction, + configuration = artMakerConfiguration, + modifier = Modifier + .padding(top = dimensionResource(id = R.dimen.Padding8), end = dimensionResource(id = R.dimen.Padding8), start = dimensionResource(id = R.dimen.Padding8)), + ) + } AnimatedVisibility(visible = !isFullScreenEnabled) { ArtMakerControlMenu( - state = artMakerUIState, + state = state, onAction = viewModel::onAction, modifier = Modifier.height(dimensionResource(id = R.dimen.Padding60)), onShowStrokeWidthPopup = { - showStrokeWidth = !showStrokeWidth + showStrokeSettings = !showStrokeSettings }, setBackgroundImage = viewModel::setImage, imageBitmap = viewModel.backgroundImage.value, diff --git a/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt b/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt index c5e3f2da..7aa78d7d 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt @@ -34,10 +34,10 @@ import androidx.lifecycle.viewModelScope import io.artmaker.actions.ArtMakerAction import io.artmaker.actions.DrawEvent import io.artmaker.actions.ExportType -import io.artmaker.models.PointsData import io.artmaker.data.ArtMakerSharedPreferences import io.artmaker.data.PreferenceKeys import io.artmaker.data.PreferenceKeys.SELECTED_STROKE_WIDTH +import io.artmaker.models.PointsData import io.artmaker.utils.saveToDisk import io.artmaker.utils.shareBitmap import kotlinx.coroutines.flow.MutableStateFlow @@ -91,7 +91,7 @@ internal class ArtMakerViewModel( ArtMakerAction.Clear -> clear() ArtMakerAction.UpdateBackground -> updateBackgroundColour() is ArtMakerAction.SelectStrokeColour -> updateStrokeColor(colour = action.color) - is ArtMakerAction.SelectStrokeWidth -> selectStrokeWidth(strokeWidth = action.strokeWidth) + is ArtMakerAction.SetStrokeWidth -> selectStrokeWidth(strokeWidth = action.strokeWidth) } } diff --git a/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt b/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt index 41ec65bf..d302ec6d 100644 --- a/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt +++ b/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt @@ -29,7 +29,7 @@ sealed interface ArtMakerAction { data object Clear : ArtMakerAction data object UpdateBackground : ArtMakerAction data class SelectStrokeColour(val color: Color) : ArtMakerAction - data class SelectStrokeWidth(val strokeWidth: Int) : ArtMakerAction + data class SetStrokeWidth(val strokeWidth: Int) : ArtMakerAction } sealed interface ExportType { diff --git a/artmaker/src/main/java/io/artmaker/composables/ArtMakerControlMenu.kt b/artmaker/src/main/java/io/artmaker/composables/ArtMakerControlMenu.kt index 0ffc4027..7ccb174b 100644 --- a/artmaker/src/main/java/io/artmaker/composables/ArtMakerControlMenu.kt +++ b/artmaker/src/main/java/io/artmaker/composables/ArtMakerControlMenu.kt @@ -60,9 +60,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.core.os.BuildCompat import com.google.modernstorage.photopicker.PhotoPicker +import io.artmaker.ArtMakerUIState import io.artmaker.actions.ArtMakerAction import io.artmaker.models.ArtMakerConfiguration -import io.artmaker.ArtMakerUIState import io.artmaker.utils.ColorUtils import io.fbada006.artmaker.R diff --git a/artmaker/src/main/java/io/artmaker/composables/Slider.kt b/artmaker/src/main/java/io/artmaker/composables/Slider.kt index 9a7e9434..160a02b9 100644 --- a/artmaker/src/main/java/io/artmaker/composables/Slider.kt +++ b/artmaker/src/main/java/io/artmaker/composables/Slider.kt @@ -17,7 +17,6 @@ package io.artmaker.composables import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults @@ -25,7 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource import io.artmaker.models.ArtMakerConfiguration import io.fbada006.artmaker.R @@ -37,29 +36,28 @@ private const val MAX_WIDTH = 50f */ @Composable internal fun Slider( - modifier: Modifier = Modifier, sliderPosition: Float, onValueChange: (Float) -> Unit, - artMakerConfiguration: ArtMakerConfiguration, + configuration: ArtMakerConfiguration, + modifier: Modifier = Modifier, ) { Column( - modifier = modifier - .padding(all = dimensionResource(id = R.dimen.Padding7)), + modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { Text( - text = sliderPosition.toInt().toString(), + text = stringResource(id = R.string.set_width, sliderPosition.toInt()), style = MaterialTheme.typography.titleLarge, - color = artMakerConfiguration.strokeSliderTextColor, + color = configuration.strokeSliderTextColor, ) Slider( value = sliderPosition, onValueChange = onValueChange, colors = SliderDefaults.colors( - thumbColor = artMakerConfiguration.strokeSliderThumbColor, - activeTrackColor = artMakerConfiguration.strokeSliderActiveTrackColor, - inactiveTickColor = artMakerConfiguration.strokeSliderInactiveTickColor, + thumbColor = configuration.strokeSliderThumbColor, + activeTrackColor = configuration.strokeSliderActiveTrackColor, + inactiveTickColor = configuration.strokeSliderInactiveTickColor, ), valueRange = MIN_WIDTH..MAX_WIDTH, ) diff --git a/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt b/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt new file mode 100644 index 00000000..9e6c86d9 --- /dev/null +++ b/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024 ArtMaker + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package io.artmaker.composables + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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 io.artmaker.actions.ArtMakerAction +import io.artmaker.models.ArtMakerConfiguration + +@Composable +fun StrokeSettings(strokeWidth: Int, onAction: (ArtMakerAction) -> Unit, configuration: ArtMakerConfiguration, modifier: Modifier = Modifier) { + var sliderPosition by remember { mutableIntStateOf(strokeWidth) } + var useStylusOnly by remember { mutableStateOf(false) } + + Column(modifier = modifier, verticalArrangement = Arrangement.SpaceEvenly) { + Slider( + sliderPosition = sliderPosition.toFloat(), + onValueChange = { + sliderPosition = it.toInt() + onAction(ArtMakerAction.SetStrokeWidth(strokeWidth = sliderPosition)) + }, + configuration = configuration, + modifier = Modifier.fillMaxWidth(), + ) + HorizontalDivider() + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + Text( + text = "Use stylus only", + style = MaterialTheme.typography.bodyLarge, + ) + Switch(checked = useStylusOnly, onCheckedChange = { useStylusOnly = !useStylusOnly }) + } + } +} diff --git a/artmaker/src/main/java/io/artmaker/composables/StrokeWidthSlider.kt b/artmaker/src/main/java/io/artmaker/composables/StrokeWidthSlider.kt deleted file mode 100644 index 1b6807e6..00000000 --- a/artmaker/src/main/java/io/artmaker/composables/StrokeWidthSlider.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2024 ArtMaker - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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. - */ -package io.artmaker.composables - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.stringResource -import io.artmaker.actions.ArtMakerAction -import io.artmaker.models.ArtMakerConfiguration -import io.artmaker.ArtMakerUIState -import io.fbada006.artmaker.R - -/** - * This is a popup that displays the ArtMakerStrokeWidthSlider and accompanying title. - */ -@Composable -internal fun StrokeWidthSlider( - state: ArtMakerUIState, - onAction: (ArtMakerAction) -> Unit, - isVisible: Boolean, - artMakerConfiguration: ArtMakerConfiguration, -) { - var sliderPosition by remember { mutableIntStateOf(value = state.strokeWidth) } - AnimatedVisibility(visible = isVisible) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(color = artMakerConfiguration.strokeSliderBackgroundColor) - .padding(top = dimensionResource(id = R.dimen.Padding4)), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceEvenly, - ) { - Text( - text = stringResource(id = R.string.set_width), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.primary, - ) - Slider( - sliderPosition = sliderPosition.toFloat(), - onValueChange = { - sliderPosition = it.toInt() - onAction(ArtMakerAction.SelectStrokeWidth(strokeWidth = sliderPosition)) - }, - artMakerConfiguration = artMakerConfiguration, - ) - } - } -} diff --git a/artmaker/src/main/res/values/strings.xml b/artmaker/src/main/res/values/strings.xml index 27e431f5..1aa330b1 100644 --- a/artmaker/src/main/res/values/strings.xml +++ b/artmaker/src/main/res/values/strings.xml @@ -17,7 +17,7 @@ The storage permission is needed to save the image. Grant Access - Set width + Set width: %d Share your image Change Image Clear Image From c0c395e7a67990ef653ade782ecee5eeaa12308c Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Thu, 22 Aug 2024 14:49:05 +0300 Subject: [PATCH 04/16] adds palm rejection validation and persistence --- .../src/main/java/io/artmaker/ArtMaker.kt | 6 ++- .../main/java/io/artmaker/ArtMakerUIState.kt | 1 + .../java/io/artmaker/ArtMakerViewModel.kt | 17 +++++++ .../io/artmaker/actions/ArtMakerAction.kt | 1 + .../composables/ArtMakerDrawScreen.kt | 7 ++- .../io/artmaker/composables/StrokeSettings.kt | 12 +++-- .../java/io/artmaker/data/PreferenceKeys.kt | 1 + .../java/io/artmaker/utils/MotionEventUtil.kt | 48 +++++++++++++++++++ 8 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt diff --git a/artmaker/src/main/java/io/artmaker/ArtMaker.kt b/artmaker/src/main/java/io/artmaker/ArtMaker.kt index aa649ae5..73b72169 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMaker.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMaker.kt @@ -82,7 +82,7 @@ fun ArtMaker( Scaffold( floatingActionButton = { AnimatedVisibility( - visible = viewModel.pathList.isNotEmpty(), + visible = viewModel.pathList.isNotEmpty() && !showStrokeSettings, modifier = Modifier.padding(bottom = if (isFullScreenEnabled) dimensionResource(id = R.dimen.Padding0) else dimensionResource(id = R.dimen.Padding60)), ) { Column { @@ -123,6 +123,7 @@ fun ArtMaker( shouldTriggerArtExport = shouldTriggerArtExport, imageBitmap = viewModel.backgroundImage.value, isFullScreenMode = isFullScreenEnabled, + useStylusOnly = state.useStylusOnly, ) AnimatedVisibility(visible = showStrokeSettings) { StrokeSettings( @@ -130,7 +131,8 @@ fun ArtMaker( onAction = viewModel::onAction, configuration = artMakerConfiguration, modifier = Modifier - .padding(top = dimensionResource(id = R.dimen.Padding8), end = dimensionResource(id = R.dimen.Padding8), start = dimensionResource(id = R.dimen.Padding8)), + .padding(top = dimensionResource(id = R.dimen.Padding8), end = dimensionResource(id = R.dimen.Padding12), start = dimensionResource(id = R.dimen.Padding12)), + isStylusOnly = state.useStylusOnly, ) } AnimatedVisibility(visible = !isFullScreenEnabled) { diff --git a/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt b/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt index 0bb6f4e3..1e8fee28 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt @@ -28,4 +28,5 @@ data class ArtMakerUIState( val canRedo: Boolean = false, val canUndo: Boolean = false, val canClear: Boolean = false, + val useStylusOnly: Boolean = false, ) diff --git a/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt b/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt index 7aa78d7d..b8f4c6b6 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt @@ -62,6 +62,10 @@ internal class ArtMakerViewModel( key = SELECTED_STROKE_WIDTH, defaultValue = 5, ), + useStylusOnly = preferences.get( + key = PreferenceKeys.USE_STYLUS_ONLY, + false, + ), ), ) val artMakerUIState = _artMakerUIState.asStateFlow() @@ -92,6 +96,7 @@ internal class ArtMakerViewModel( ArtMakerAction.UpdateBackground -> updateBackgroundColour() is ArtMakerAction.SelectStrokeColour -> updateStrokeColor(colour = action.color) is ArtMakerAction.SetStrokeWidth -> selectStrokeWidth(strokeWidth = action.strokeWidth) + is ArtMakerAction.UpdateSetStylusOnly -> updateStylusSetting(useStylusOnly = action.useStylusOnly) } } @@ -198,6 +203,18 @@ internal class ArtMakerViewModel( } } + private fun updateStylusSetting(useStylusOnly: Boolean) { + preferences.set(PreferenceKeys.USE_STYLUS_ONLY, useStylusOnly) + _artMakerUIState.update { + it.copy( + useStylusOnly = preferences.get( + key = PreferenceKeys.USE_STYLUS_ONLY, + false, + ), + ) + } + } + companion object { fun provideFactory( application: Application, diff --git a/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt b/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt index d302ec6d..02472f89 100644 --- a/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt +++ b/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt @@ -30,6 +30,7 @@ sealed interface ArtMakerAction { data object UpdateBackground : ArtMakerAction data class SelectStrokeColour(val color: Color) : ArtMakerAction data class SetStrokeWidth(val strokeWidth: Int) : ArtMakerAction + data class UpdateSetStylusOnly(val useStylusOnly: Boolean) : ArtMakerAction } sealed interface ExportType { diff --git a/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt b/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt index 0a22f77e..efb2d378 100644 --- a/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt +++ b/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt @@ -58,6 +58,7 @@ import io.artmaker.actions.ArtMakerAction import io.artmaker.actions.DrawEvent import io.artmaker.models.ArtMakerConfiguration import io.artmaker.models.PointsData +import io.artmaker.utils.validateEvent import io.fbada006.artmaker.R import kotlinx.coroutines.launch @@ -75,7 +76,9 @@ internal fun ArtMakerDrawScreen( imageBitmap: ImageBitmap?, shouldTriggerArtExport: Boolean, isFullScreenMode: Boolean, + useStylusOnly: Boolean, ) { + val context = LocalContext.current val density = LocalDensity.current val configuration = LocalConfiguration.current @@ -106,8 +109,6 @@ internal fun ArtMakerDrawScreen( }, ) - val context = LocalContext.current - LaunchedEffect(key1 = shouldTriggerArtExport) { if (shouldTriggerArtExport) { if (writeStorageAccessState.allPermissionsGranted) { @@ -148,6 +149,8 @@ internal fun ArtMakerDrawScreen( } } .pointerInteropFilter { event -> + if (!event.validateEvent(context, useStylusOnly)) return@pointerInteropFilter false + val offset = Offset(event.x, event.y) when (event.action) { MotionEvent.ACTION_DOWN -> { diff --git a/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt b/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt index 9e6c86d9..e2882334 100644 --- a/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt +++ b/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt @@ -35,9 +35,9 @@ import io.artmaker.actions.ArtMakerAction import io.artmaker.models.ArtMakerConfiguration @Composable -fun StrokeSettings(strokeWidth: Int, onAction: (ArtMakerAction) -> Unit, configuration: ArtMakerConfiguration, modifier: Modifier = Modifier) { +fun StrokeSettings(strokeWidth: Int, isStylusOnly: Boolean, onAction: (ArtMakerAction) -> Unit, configuration: ArtMakerConfiguration, modifier: Modifier = Modifier) { var sliderPosition by remember { mutableIntStateOf(strokeWidth) } - var useStylusOnly by remember { mutableStateOf(false) } + var stylusOnly by remember { mutableStateOf(isStylusOnly) } Column(modifier = modifier, verticalArrangement = Arrangement.SpaceEvenly) { Slider( @@ -55,7 +55,13 @@ fun StrokeSettings(strokeWidth: Int, onAction: (ArtMakerAction) -> Unit, configu text = "Use stylus only", style = MaterialTheme.typography.bodyLarge, ) - Switch(checked = useStylusOnly, onCheckedChange = { useStylusOnly = !useStylusOnly }) + Switch( + checked = stylusOnly, + onCheckedChange = { + stylusOnly = !stylusOnly + onAction(ArtMakerAction.UpdateSetStylusOnly(useStylusOnly = stylusOnly)) + }, + ) } } } diff --git a/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt b/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt index e35c7583..b04e318d 100644 --- a/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt +++ b/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt @@ -19,4 +19,5 @@ object PreferenceKeys { const val SELECTED_BACKGROUND_COLOUR = "com.artmaker.sharedpreferences.selectedBackgroundColour" const val SELECTED_STROKE_COLOUR = "com.artmaker.sharedpreferences.selectedStrokeColour" const val SELECTED_STROKE_WIDTH = "com.artmaker.sharedpreferences.selectedStrokeWidth" + const val USE_STYLUS_ONLY = "com.artmaker.sharedpreferences.useStylusOnly" } diff --git a/artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt b/artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt new file mode 100644 index 00000000..e27545e5 --- /dev/null +++ b/artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024 ArtMaker + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@file: JvmName("MotionEventUtil") + +package io.artmaker.utils + +import android.content.Context +import android.hardware.input.InputManager +import android.view.MotionEvent +import java.util.Locale + +/** + * Check if the input is from a stylus or not during drawing + */ +internal fun MotionEvent.isStylusInput(): Boolean = this.getToolType(this.actionIndex) == MotionEvent.TOOL_TYPE_STYLUS + +/** + * Validate the logic for the auto detection of the stylus to determine whether to only use stylus input for palm rejection + */ +internal fun MotionEvent.validateEvent(context: Context, useStylusOnly: Boolean): Boolean { + if (!isStylusConnected(context)) return true // No stylus + val isStylusDrawing = this.isStylusInput() + + // Check if stylus drawing is required but not detected + return !useStylusOnly || isStylusDrawing +} + +private fun isStylusConnected(context: Context): Boolean { + val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager + val inputDeviceIds: IntArray = inputManager.inputDeviceIds + return inputDeviceIds.any { + val device = inputManager.getInputDevice(it) + device?.name?.lowercase(Locale.ROOT)?.contains("pen") == true + } +} From ea32934190354561595c32047f546d3b1c2266fb Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Thu, 22 Aug 2024 16:39:57 +0300 Subject: [PATCH 05/16] add dialog for input types --- .../composables/ArtMakerDrawScreen.kt | 61 ++++++++++++++++--- artmaker/src/main/res/values/strings.xml | 4 ++ 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt b/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt index efb2d378..1a9b69d2 100644 --- a/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt +++ b/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt @@ -16,16 +16,24 @@ package io.artmaker.composables import android.Manifest +import android.content.Context import android.os.Build import android.view.MotionEvent import androidx.compose.foundation.Canvas import androidx.compose.foundation.background +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Icon import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -50,6 +58,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.math.MathUtils.clamp import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -58,6 +67,7 @@ import io.artmaker.actions.ArtMakerAction import io.artmaker.actions.DrawEvent import io.artmaker.models.ArtMakerConfiguration import io.artmaker.models.PointsData +import io.artmaker.utils.isStylusInput import io.artmaker.utils.validateEvent import io.fbada006.artmaker.R import kotlinx.coroutines.launch @@ -85,17 +95,15 @@ internal fun ArtMakerDrawScreen( val screenHeight = configuration.screenHeightDp.dp // Used to clip the y value from the Offset during drawing so that the canvas cannot draw into the control menu // Add an extra 2dp for line visibility - val yOffset = if (isFullScreenMode) { - 0f - } else { - with(density) { - (dimensionResource(id = R.dimen.Padding60) + dimensionResource(id = R.dimen.Padding2)).toPx() - } + val yOffset = if (isFullScreenMode) 0f else with(density) { + (dimensionResource(id = R.dimen.Padding60) + dimensionResource(id = R.dimen.Padding2)).toPx() } val screenHeightPx = with(density) { screenHeight.toPx() } val maxDrawingHeight = screenHeightPx - yOffset var bitmapHeight by rememberSaveable { mutableIntStateOf(0) } var bitmapWidth by rememberSaveable { mutableIntStateOf(0) } + var shouldShowStylusDialog by rememberSaveable { mutableStateOf(false) } + var stylusDialogType by rememberSaveable { mutableStateOf("") } val graphicsLayer = rememberGraphicsLayer() val snackbarHostState = remember { SnackbarHostState() } @@ -149,11 +157,16 @@ internal fun ArtMakerDrawScreen( } } .pointerInteropFilter { event -> - if (!event.validateEvent(context, useStylusOnly)) return@pointerInteropFilter false - val offset = Offset(event.x, event.y) when (event.action) { MotionEvent.ACTION_DOWN -> { + getDialogType(context, event, useStylusOnly)?.let { type -> + shouldShowStylusDialog = true + stylusDialogType = type + } + + if (!event.validateEvent(context, useStylusOnly)) return@pointerInteropFilter false + onDrawEvent(DrawEvent.AddNewShape(offset)) } @@ -191,4 +204,36 @@ internal fun ArtMakerDrawScreen( } }, ) + + if (shouldShowStylusDialog) { + if (stylusDialogType.isEmpty()) return + val message: String = when (StylusDialogType.valueOf(stylusDialogType)) { + StylusDialogType.ENABLE_STYLUS_ONLY -> stringResource(R.string.styluse_input_detected_message) + StylusDialogType.DISABLE_STYLUS_ONLY -> stringResource(R.string.non_stylus_input_detected_message) + } + val title: String = when (StylusDialogType.valueOf(stylusDialogType)) { + StylusDialogType.ENABLE_STYLUS_ONLY -> stringResource(R.string.stylus_input_detected_title) + StylusDialogType.DISABLE_STYLUS_ONLY -> stringResource(R.string.non_stylus_input_detected_title) + } + + AlertDialog( + icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = Icons.Filled.Edit.name) }, + title = { Text(text = title) }, + text = { Text(text = message) }, + onDismissRequest = { shouldShowStylusDialog = false }, + confirmButton = { + Button(onClick = { shouldShowStylusDialog = false }) { + Text(text = stringResource(id = android.R.string.ok)) + } + }, + ) + } } + +private fun getDialogType(context: Context, event: MotionEvent, useStylusOnly: Boolean) = when { + event.isStylusInput() && !useStylusOnly -> StylusDialogType.ENABLE_STYLUS_ONLY.name + !event.validateEvent(context, useStylusOnly) -> StylusDialogType.DISABLE_STYLUS_ONLY.name + else -> null +} + +internal enum class StylusDialogType { ENABLE_STYLUS_ONLY, DISABLE_STYLUS_ONLY } \ No newline at end of file diff --git a/artmaker/src/main/res/values/strings.xml b/artmaker/src/main/res/values/strings.xml index 1aa330b1..a78583c4 100644 --- a/artmaker/src/main/res/values/strings.xml +++ b/artmaker/src/main/res/values/strings.xml @@ -21,4 +21,8 @@ Share your image Change Image Clear Image + Stylus input detected + Non-Stylus input detected + We have detected that you are using a stylus to draw. For a better experience, please enable stylus only input using the pencil icon at the bottom of the screen. + We have detected that you are not using your stylus to draw but you have enabled the stylus only input setting. Please disable this using the pencil icon at the bottom of the screen if you wish to use other input types to draw. \ No newline at end of file From 180f4f594eed440d5442c670d27ead6aaebfae63 Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Thu, 22 Aug 2024 16:47:08 +0300 Subject: [PATCH 06/16] extract draw state to its own class --- .../src/main/java/io/artmaker/ArtMaker.kt | 12 +++--- .../src/main/java/io/artmaker/DrawState.kt | 31 +++++++++++++++ .../composables/ArtMakerDrawScreen.kt | 39 +++++++++---------- 3 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 artmaker/src/main/java/io/artmaker/DrawState.kt diff --git a/artmaker/src/main/java/io/artmaker/ArtMaker.kt b/artmaker/src/main/java/io/artmaker/ArtMaker.kt index 73b72169..1ea0c963 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMaker.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMaker.kt @@ -119,11 +119,13 @@ fun ArtMaker( viewModel.onDrawEvent(it) }, onAction = viewModel::onAction, - pathList = viewModel.pathList, - shouldTriggerArtExport = shouldTriggerArtExport, - imageBitmap = viewModel.backgroundImage.value, - isFullScreenMode = isFullScreenEnabled, - useStylusOnly = state.useStylusOnly, + state = DrawState( + pathList = viewModel.pathList, + shouldTriggerArtExport = shouldTriggerArtExport, + backgroundImage = viewModel.backgroundImage.value, + isFullScreenMode = isFullScreenEnabled, + useStylusOnly = state.useStylusOnly, + ), ) AnimatedVisibility(visible = showStrokeSettings) { StrokeSettings( diff --git a/artmaker/src/main/java/io/artmaker/DrawState.kt b/artmaker/src/main/java/io/artmaker/DrawState.kt new file mode 100644 index 00000000..680b6ce4 --- /dev/null +++ b/artmaker/src/main/java/io/artmaker/DrawState.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024 ArtMaker + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package io.artmaker + +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.graphics.ImageBitmap +import io.artmaker.models.PointsData + +/** + * Provides state from the [io.artmaker.composables.ArtMakerDrawScreen] screen + */ +internal data class DrawState( + val pathList: SnapshotStateList, + val backgroundImage: ImageBitmap?, + val shouldTriggerArtExport: Boolean, + val isFullScreenMode: Boolean, + val useStylusOnly: Boolean, +) diff --git a/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt b/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt index 1a9b69d2..6193791a 100644 --- a/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt +++ b/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt @@ -37,14 +37,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageShader import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.graphics.ShaderBrush @@ -63,10 +61,10 @@ import androidx.compose.ui.unit.dp import androidx.core.math.MathUtils.clamp import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState +import io.artmaker.DrawState import io.artmaker.actions.ArtMakerAction import io.artmaker.actions.DrawEvent import io.artmaker.models.ArtMakerConfiguration -import io.artmaker.models.PointsData import io.artmaker.utils.isStylusInput import io.artmaker.utils.validateEvent import io.fbada006.artmaker.R @@ -82,11 +80,7 @@ internal fun ArtMakerDrawScreen( artMakerConfiguration: ArtMakerConfiguration, onDrawEvent: (DrawEvent) -> Unit, onAction: (ArtMakerAction) -> Unit, - pathList: SnapshotStateList, - imageBitmap: ImageBitmap?, - shouldTriggerArtExport: Boolean, - isFullScreenMode: Boolean, - useStylusOnly: Boolean, + state: DrawState, ) { val context = LocalContext.current val density = LocalDensity.current @@ -95,8 +89,12 @@ internal fun ArtMakerDrawScreen( val screenHeight = configuration.screenHeightDp.dp // Used to clip the y value from the Offset during drawing so that the canvas cannot draw into the control menu // Add an extra 2dp for line visibility - val yOffset = if (isFullScreenMode) 0f else with(density) { - (dimensionResource(id = R.dimen.Padding60) + dimensionResource(id = R.dimen.Padding2)).toPx() + val yOffset = if (state.isFullScreenMode) { + 0f + } else { + with(density) { + (dimensionResource(id = R.dimen.Padding60) + dimensionResource(id = R.dimen.Padding2)).toPx() + } } val screenHeightPx = with(density) { screenHeight.toPx() } val maxDrawingHeight = screenHeightPx - yOffset @@ -117,8 +115,8 @@ internal fun ArtMakerDrawScreen( }, ) - LaunchedEffect(key1 = shouldTriggerArtExport) { - if (shouldTriggerArtExport) { + LaunchedEffect(key1 = state.shouldTriggerArtExport) { + if (state.shouldTriggerArtExport) { if (writeStorageAccessState.allPermissionsGranted) { val bitmap = graphicsLayer.toImageBitmap() onAction(ArtMakerAction.ExportArt(bitmap)) @@ -160,12 +158,12 @@ internal fun ArtMakerDrawScreen( val offset = Offset(event.x, event.y) when (event.action) { MotionEvent.ACTION_DOWN -> { - getDialogType(context, event, useStylusOnly)?.let { type -> + getDialogType(context, event, state.useStylusOnly)?.let { type -> shouldShowStylusDialog = true stylusDialogType = type } - if (!event.validateEvent(context, useStylusOnly)) return@pointerInteropFilter false + if (!event.validateEvent(context, state.useStylusOnly)) return@pointerInteropFilter false onDrawEvent(DrawEvent.AddNewShape(offset)) } @@ -184,15 +182,15 @@ internal fun ArtMakerDrawScreen( }, onDraw = { drawIntoCanvas { - imageBitmap?.let { imageBitmap -> - val shader = ImageShader(imageBitmap, TileMode.Clamp) + state.backgroundImage?.let { bitmap -> + val shader = ImageShader(bitmap, TileMode.Clamp) val brush = ShaderBrush(shader) drawRect( brush = brush, size = Size(bitmapWidth.toFloat(), bitmapHeight.toFloat()), ) } - pathList.forEach { data -> + state.pathList.forEach { data -> drawPoints( points = data.points, pointMode = if (data.points.size == 1) PointMode.Points else PointMode.Polygon, // Draw a point if the shape has only one item otherwise a free flowing shape @@ -207,11 +205,12 @@ internal fun ArtMakerDrawScreen( if (shouldShowStylusDialog) { if (stylusDialogType.isEmpty()) return - val message: String = when (StylusDialogType.valueOf(stylusDialogType)) { + val type = StylusDialogType.valueOf(stylusDialogType) + val message: String = when (type) { StylusDialogType.ENABLE_STYLUS_ONLY -> stringResource(R.string.styluse_input_detected_message) StylusDialogType.DISABLE_STYLUS_ONLY -> stringResource(R.string.non_stylus_input_detected_message) } - val title: String = when (StylusDialogType.valueOf(stylusDialogType)) { + val title: String = when (type) { StylusDialogType.ENABLE_STYLUS_ONLY -> stringResource(R.string.stylus_input_detected_title) StylusDialogType.DISABLE_STYLUS_ONLY -> stringResource(R.string.non_stylus_input_detected_title) } @@ -236,4 +235,4 @@ private fun getDialogType(context: Context, event: MotionEvent, useStylusOnly: B else -> null } -internal enum class StylusDialogType { ENABLE_STYLUS_ONLY, DISABLE_STYLUS_ONLY } \ No newline at end of file +internal enum class StylusDialogType { ENABLE_STYLUS_ONLY, DISABLE_STYLUS_ONLY } From 4d7441eb8827f5602afed277ac69786da2933cc1 Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Fri, 23 Aug 2024 12:37:12 +0300 Subject: [PATCH 07/16] add palm rejection --- .../src/main/java/io/artmaker/ArtMaker.kt | 9 ++-- .../main/java/io/artmaker/ArtMakerUIState.kt | 3 +- .../java/io/artmaker/ArtMakerViewModel.kt | 23 ++++++++-- .../src/main/java/io/artmaker/DrawState.kt | 3 +- .../io/artmaker/actions/ArtMakerAction.kt | 3 +- .../composables/ArtMakerDrawScreen.kt | 46 +++++++++++++------ .../io/artmaker/composables/StrokeSettings.kt | 2 +- .../java/io/artmaker/data/PreferenceKeys.kt | 1 + artmaker/src/main/res/values/strings.xml | 1 + 9 files changed, 64 insertions(+), 27 deletions(-) diff --git a/artmaker/src/main/java/io/artmaker/ArtMaker.kt b/artmaker/src/main/java/io/artmaker/ArtMaker.kt index 1ea0c963..65756682 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMaker.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMaker.kt @@ -124,7 +124,8 @@ fun ArtMaker( shouldTriggerArtExport = shouldTriggerArtExport, backgroundImage = viewModel.backgroundImage.value, isFullScreenMode = isFullScreenEnabled, - useStylusOnly = state.useStylusOnly, + shouldUseStylusOnly = state.shouldUseStylusOnly, + canShowStylusDialog = state.canShowStylusDialog, ), ) AnimatedVisibility(visible = showStrokeSettings) { @@ -134,7 +135,7 @@ fun ArtMaker( configuration = artMakerConfiguration, modifier = Modifier .padding(top = dimensionResource(id = R.dimen.Padding8), end = dimensionResource(id = R.dimen.Padding12), start = dimensionResource(id = R.dimen.Padding12)), - isStylusOnly = state.useStylusOnly, + isStylusOnly = state.shouldUseStylusOnly, ) } AnimatedVisibility(visible = !isFullScreenEnabled) { @@ -142,9 +143,7 @@ fun ArtMaker( state = state, onAction = viewModel::onAction, modifier = Modifier.height(dimensionResource(id = R.dimen.Padding60)), - onShowStrokeWidthPopup = { - showStrokeSettings = !showStrokeSettings - }, + onShowStrokeWidthPopup = { showStrokeSettings = !showStrokeSettings }, setBackgroundImage = viewModel::setImage, imageBitmap = viewModel.backgroundImage.value, artMakerConfiguration = artMakerConfiguration, diff --git a/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt b/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt index 1e8fee28..2c04d737 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt @@ -28,5 +28,6 @@ data class ArtMakerUIState( val canRedo: Boolean = false, val canUndo: Boolean = false, val canClear: Boolean = false, - val useStylusOnly: Boolean = false, + val shouldUseStylusOnly: Boolean = false, + val canShowStylusDialog: Boolean = true, ) diff --git a/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt b/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt index b8f4c6b6..1e0ffba0 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt @@ -62,10 +62,14 @@ internal class ArtMakerViewModel( key = SELECTED_STROKE_WIDTH, defaultValue = 5, ), - useStylusOnly = preferences.get( + shouldUseStylusOnly = preferences.get( key = PreferenceKeys.USE_STYLUS_ONLY, false, ), + canShowStylusDialog = preferences.get( + key = PreferenceKeys.SHOW_STYLUS_DIALOG, + true, + ), ), ) val artMakerUIState = _artMakerUIState.asStateFlow() @@ -96,7 +100,8 @@ internal class ArtMakerViewModel( ArtMakerAction.UpdateBackground -> updateBackgroundColour() is ArtMakerAction.SelectStrokeColour -> updateStrokeColor(colour = action.color) is ArtMakerAction.SetStrokeWidth -> selectStrokeWidth(strokeWidth = action.strokeWidth) - is ArtMakerAction.UpdateSetStylusOnly -> updateStylusSetting(useStylusOnly = action.useStylusOnly) + is ArtMakerAction.UpdateSetStylusOnly -> updateStylusSetting(useStylusOnly = action.shouldUseStylusOnly) + is ArtMakerAction.UpdateStylusDialogNeverShow -> updateStylusDialogShowSetting(canShowStylusDialog = action.canShowStylusDialog) } } @@ -207,7 +212,7 @@ internal class ArtMakerViewModel( preferences.set(PreferenceKeys.USE_STYLUS_ONLY, useStylusOnly) _artMakerUIState.update { it.copy( - useStylusOnly = preferences.get( + shouldUseStylusOnly = preferences.get( key = PreferenceKeys.USE_STYLUS_ONLY, false, ), @@ -215,6 +220,18 @@ internal class ArtMakerViewModel( } } + private fun updateStylusDialogShowSetting(canShowStylusDialog: Boolean) { + preferences.set(PreferenceKeys.SHOW_STYLUS_DIALOG, canShowStylusDialog) + _artMakerUIState.update { + it.copy( + canShowStylusDialog = preferences.get( + key = PreferenceKeys.SHOW_STYLUS_DIALOG, + true, + ), + ) + } + } + companion object { fun provideFactory( application: Application, diff --git a/artmaker/src/main/java/io/artmaker/DrawState.kt b/artmaker/src/main/java/io/artmaker/DrawState.kt index 680b6ce4..b4a89e22 100644 --- a/artmaker/src/main/java/io/artmaker/DrawState.kt +++ b/artmaker/src/main/java/io/artmaker/DrawState.kt @@ -27,5 +27,6 @@ internal data class DrawState( val backgroundImage: ImageBitmap?, val shouldTriggerArtExport: Boolean, val isFullScreenMode: Boolean, - val useStylusOnly: Boolean, + val shouldUseStylusOnly: Boolean, + val canShowStylusDialog: Boolean, ) diff --git a/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt b/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt index 02472f89..e625d8c8 100644 --- a/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt +++ b/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt @@ -30,7 +30,8 @@ sealed interface ArtMakerAction { data object UpdateBackground : ArtMakerAction data class SelectStrokeColour(val color: Color) : ArtMakerAction data class SetStrokeWidth(val strokeWidth: Int) : ArtMakerAction - data class UpdateSetStylusOnly(val useStylusOnly: Boolean) : ArtMakerAction + data class UpdateSetStylusOnly(val shouldUseStylusOnly: Boolean) : ArtMakerAction + class UpdateStylusDialogNeverShow(val canShowStylusDialog: Boolean) : ArtMakerAction } sealed interface ExportType { diff --git a/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt b/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt index 6193791a..ac74fec8 100644 --- a/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt +++ b/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt @@ -21,10 +21,16 @@ import android.os.Build import android.view.MotionEvent import androidx.compose.foundation.Canvas import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult @@ -37,6 +43,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache @@ -88,7 +95,7 @@ internal fun ArtMakerDrawScreen( val screenHeight = configuration.screenHeightDp.dp // Used to clip the y value from the Offset during drawing so that the canvas cannot draw into the control menu - // Add an extra 2dp for line visibility + // Add an extra 2dp for line visibility. In full screen mode, we do not need to offset anything val yOffset = if (state.isFullScreenMode) { 0f } else { @@ -158,12 +165,12 @@ internal fun ArtMakerDrawScreen( val offset = Offset(event.x, event.y) when (event.action) { MotionEvent.ACTION_DOWN -> { - getDialogType(context, event, state.useStylusOnly)?.let { type -> + getDialogType(context, event, state.shouldUseStylusOnly)?.let { type -> shouldShowStylusDialog = true stylusDialogType = type } - if (!event.validateEvent(context, state.useStylusOnly)) return@pointerInteropFilter false + if (!event.validateEvent(context, state.shouldUseStylusOnly)) return@pointerInteropFilter false onDrawEvent(DrawEvent.AddNewShape(offset)) } @@ -203,26 +210,35 @@ internal fun ArtMakerDrawScreen( }, ) - if (shouldShowStylusDialog) { + if (shouldShowStylusDialog && state.canShowStylusDialog) { if (stylusDialogType.isEmpty()) return val type = StylusDialogType.valueOf(stylusDialogType) - val message: String = when (type) { - StylusDialogType.ENABLE_STYLUS_ONLY -> stringResource(R.string.styluse_input_detected_message) - StylusDialogType.DISABLE_STYLUS_ONLY -> stringResource(R.string.non_stylus_input_detected_message) - } - val title: String = when (type) { - StylusDialogType.ENABLE_STYLUS_ONLY -> stringResource(R.string.stylus_input_detected_title) - StylusDialogType.DISABLE_STYLUS_ONLY -> stringResource(R.string.non_stylus_input_detected_title) + val dialogInfo = when (type) { + StylusDialogType.ENABLE_STYLUS_ONLY -> stringResource(R.string.stylus_input_detected_title) to stringResource(R.string.styluse_input_detected_message) + StylusDialogType.DISABLE_STYLUS_ONLY -> stringResource(R.string.non_stylus_input_detected_title) to stringResource(R.string.non_stylus_input_detected_message) } AlertDialog( icon = { Icon(imageVector = Icons.Filled.Edit, contentDescription = Icons.Filled.Edit.name) }, - title = { Text(text = title) }, - text = { Text(text = message) }, + title = { Text(text = dialogInfo.first) }, + text = { Text(text = dialogInfo.second) }, onDismissRequest = { shouldShowStylusDialog = false }, confirmButton = { - Button(onClick = { shouldShowStylusDialog = false }) { - Text(text = stringResource(id = android.R.string.ok)) + Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = { shouldShowStylusDialog = false }) { + Text(text = stringResource(id = android.R.string.ok)) + } + Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.Padding7))) + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = stringResource(R.string.do_not_show_again)) + Checkbox( + checked = false, + onCheckedChange = { + shouldShowStylusDialog = false + onAction(ArtMakerAction.UpdateStylusDialogNeverShow(false)) + }, + ) + } } }, ) diff --git a/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt b/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt index e2882334..00d202e6 100644 --- a/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt +++ b/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt @@ -59,7 +59,7 @@ fun StrokeSettings(strokeWidth: Int, isStylusOnly: Boolean, onAction: (ArtMakerA checked = stylusOnly, onCheckedChange = { stylusOnly = !stylusOnly - onAction(ArtMakerAction.UpdateSetStylusOnly(useStylusOnly = stylusOnly)) + onAction(ArtMakerAction.UpdateSetStylusOnly(shouldUseStylusOnly = stylusOnly)) }, ) } diff --git a/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt b/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt index b04e318d..0e59610c 100644 --- a/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt +++ b/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt @@ -20,4 +20,5 @@ object PreferenceKeys { const val SELECTED_STROKE_COLOUR = "com.artmaker.sharedpreferences.selectedStrokeColour" const val SELECTED_STROKE_WIDTH = "com.artmaker.sharedpreferences.selectedStrokeWidth" const val USE_STYLUS_ONLY = "com.artmaker.sharedpreferences.useStylusOnly" + const val SHOW_STYLUS_DIALOG = "com.artmaker.sharedpreferences.showStylusDialog" } diff --git a/artmaker/src/main/res/values/strings.xml b/artmaker/src/main/res/values/strings.xml index a78583c4..7efe7884 100644 --- a/artmaker/src/main/res/values/strings.xml +++ b/artmaker/src/main/res/values/strings.xml @@ -25,4 +25,5 @@ Non-Stylus input detected We have detected that you are using a stylus to draw. For a better experience, please enable stylus only input using the pencil icon at the bottom of the screen. We have detected that you are not using your stylus to draw but you have enabled the stylus only input setting. Please disable this using the pencil icon at the bottom of the screen if you wish to use other input types to draw. + Do not show again. \ No newline at end of file From ba6c7474e92fa3009be2085128b7ea7fafaf1db3 Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Fri, 23 Aug 2024 12:41:30 +0300 Subject: [PATCH 08/16] only show the stylus settings if the phone has one --- .../src/main/java/io/artmaker/ArtMaker.kt | 2 +- .../io/artmaker/composables/StrokeSettings.kt | 35 +++++++++++-------- .../java/io/artmaker/utils/MotionEventUtil.kt | 2 +- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/artmaker/src/main/java/io/artmaker/ArtMaker.kt b/artmaker/src/main/java/io/artmaker/ArtMaker.kt index 65756682..f793f5a9 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMaker.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMaker.kt @@ -135,7 +135,7 @@ fun ArtMaker( configuration = artMakerConfiguration, modifier = Modifier .padding(top = dimensionResource(id = R.dimen.Padding8), end = dimensionResource(id = R.dimen.Padding12), start = dimensionResource(id = R.dimen.Padding12)), - isStylusOnly = state.shouldUseStylusOnly, + shouldUseStylusOnly = state.shouldUseStylusOnly, ) } AnimatedVisibility(visible = !isFullScreenEnabled) { diff --git a/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt b/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt index 00d202e6..92b8c572 100644 --- a/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt +++ b/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt @@ -31,13 +31,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import io.artmaker.actions.ArtMakerAction import io.artmaker.models.ArtMakerConfiguration +import io.artmaker.utils.isStylusConnected @Composable -fun StrokeSettings(strokeWidth: Int, isStylusOnly: Boolean, onAction: (ArtMakerAction) -> Unit, configuration: ArtMakerConfiguration, modifier: Modifier = Modifier) { +fun StrokeSettings(strokeWidth: Int, shouldUseStylusOnly: Boolean, onAction: (ArtMakerAction) -> Unit, configuration: ArtMakerConfiguration, modifier: Modifier = Modifier) { var sliderPosition by remember { mutableIntStateOf(strokeWidth) } - var stylusOnly by remember { mutableStateOf(isStylusOnly) } + var stylusOnly by remember { mutableStateOf(shouldUseStylusOnly) } Column(modifier = modifier, verticalArrangement = Arrangement.SpaceEvenly) { Slider( @@ -49,19 +51,22 @@ fun StrokeSettings(strokeWidth: Int, isStylusOnly: Boolean, onAction: (ArtMakerA configuration = configuration, modifier = Modifier.fillMaxWidth(), ) - HorizontalDivider() - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { - Text( - text = "Use stylus only", - style = MaterialTheme.typography.bodyLarge, - ) - Switch( - checked = stylusOnly, - onCheckedChange = { - stylusOnly = !stylusOnly - onAction(ArtMakerAction.UpdateSetStylusOnly(shouldUseStylusOnly = stylusOnly)) - }, - ) + // Only show these setting if there is a stylus connected + if (isStylusConnected(LocalContext.current)) { + HorizontalDivider() + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + Text( + text = "Use stylus only", + style = MaterialTheme.typography.bodyLarge, + ) + Switch( + checked = stylusOnly, + onCheckedChange = { + stylusOnly = !stylusOnly + onAction(ArtMakerAction.UpdateSetStylusOnly(shouldUseStylusOnly = stylusOnly)) + }, + ) + } } } } diff --git a/artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt b/artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt index e27545e5..e17fa25f 100644 --- a/artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt +++ b/artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt @@ -38,7 +38,7 @@ internal fun MotionEvent.validateEvent(context: Context, useStylusOnly: Boolean) return !useStylusOnly || isStylusDrawing } -private fun isStylusConnected(context: Context): Boolean { +internal fun isStylusConnected(context: Context): Boolean { val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager val inputDeviceIds: IntArray = inputManager.inputDeviceIds return inputDeviceIds.any { From bcda9e76f983e3c3616ab75741334e9376609013 Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Fri, 23 Aug 2024 13:43:16 +0300 Subject: [PATCH 09/16] extract drawing logic --- .../src/main/java/io/artmaker/ArtMaker.kt | 3 +- .../java/io/artmaker/ArtMakerViewModel.kt | 83 +++++----------- .../io/artmaker/actions/ArtMakerAction.kt | 7 +- .../java/io/artmaker/actions/DrawEvent.kt | 3 + .../composables/ArtMakerControlMenu.kt | 8 +- .../java/io/artmaker/export/DrawingManager.kt | 95 +++++++++++++++++++ 6 files changed, 131 insertions(+), 68 deletions(-) create mode 100644 artmaker/src/main/java/io/artmaker/export/DrawingManager.kt diff --git a/artmaker/src/main/java/io/artmaker/ArtMaker.kt b/artmaker/src/main/java/io/artmaker/ArtMaker.kt index f793f5a9..b1d1f93c 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMaker.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMaker.kt @@ -71,7 +71,7 @@ fun ArtMaker( factory = ArtMakerViewModel.provideFactory(application = context.applicationContext as Application), ) var showStrokeSettings by remember { mutableStateOf(value = false) } - val state by viewModel.artMakerUIState.collectAsStateWithLifecycle() + val state by viewModel.uiState.collectAsStateWithLifecycle() val shouldTriggerArtExport by viewModel.shouldTriggerArtExport.collectAsStateWithLifecycle() val finishedImage by viewModel.finishedImage.collectAsStateWithLifecycle() var isFullScreenEnabled by remember { mutableStateOf(false) } @@ -142,6 +142,7 @@ fun ArtMaker( ArtMakerControlMenu( state = state, onAction = viewModel::onAction, + onDrawEvent = viewModel::onDrawEvent, modifier = Modifier.height(dimensionResource(id = R.dimen.Padding60)), onShowStrokeWidthPopup = { showStrokeSettings = !showStrokeSettings }, setBackgroundImage = viewModel::setImage, diff --git a/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt b/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt index 1e0ffba0..25d25dbe 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt @@ -20,10 +20,8 @@ import android.content.Context import android.graphics.Bitmap import androidx.compose.runtime.MutableState import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asAndroidBitmap @@ -37,22 +35,24 @@ import io.artmaker.actions.ExportType import io.artmaker.data.ArtMakerSharedPreferences import io.artmaker.data.PreferenceKeys import io.artmaker.data.PreferenceKeys.SELECTED_STROKE_WIDTH +import io.artmaker.export.DrawingManager import io.artmaker.models.PointsData import io.artmaker.utils.saveToDisk import io.artmaker.utils.shareBitmap import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import java.util.Stack internal class ArtMakerViewModel( private val preferences: ArtMakerSharedPreferences, + private val drawingManager: DrawingManager, private val applicationContext: Context, ) : ViewModel() { - private var _artMakerUIState = MutableStateFlow( + private var _uiState = MutableStateFlow( value = ArtMakerUIState( strokeColour = preferences.get( key = PreferenceKeys.SELECTED_STROKE_COLOUR, @@ -72,12 +72,9 @@ internal class ArtMakerViewModel( ), ), ) - val artMakerUIState = _artMakerUIState.asStateFlow() + val uiState = _uiState.asStateFlow() - private val undoStack = Stack() - - private val _pathList = mutableStateListOf() - val pathList: SnapshotStateList = _pathList + val pathList: SnapshotStateList = drawingManager.pathList private val _shouldTriggerArtExport = MutableStateFlow(false) val shouldTriggerArtExport: StateFlow = _shouldTriggerArtExport @@ -90,13 +87,14 @@ internal class ArtMakerViewModel( private val _finishedImage = MutableStateFlow(null) val finishedImage = _finishedImage.asStateFlow() + init { + listenToUndoRedoState() + } + fun onAction(action: ArtMakerAction) { when (action) { is ArtMakerAction.TriggerArtExport -> triggerArtExport(action.type) is ArtMakerAction.ExportArt -> exportArt(action.bitmap) - ArtMakerAction.Redo -> redo() - ArtMakerAction.Undo -> undo() - ArtMakerAction.Clear -> clear() ArtMakerAction.UpdateBackground -> updateBackgroundColour() is ArtMakerAction.SelectStrokeColour -> updateStrokeColor(colour = action.color) is ArtMakerAction.SetStrokeWidth -> selectStrokeWidth(strokeWidth = action.strokeWidth) @@ -105,32 +103,14 @@ internal class ArtMakerViewModel( } } - fun onDrawEvent(event: DrawEvent) { - when (event) { - is DrawEvent.AddNewShape -> addNewShape(event.offset) - DrawEvent.UndoLastShapePoint -> undoLastShapePoint() - is DrawEvent.UpdateCurrentShape -> updateCurrentShape(event.offset) - } - } + fun onDrawEvent(event: DrawEvent) = drawingManager.onDrawEvent(event, _uiState.value.strokeColour, _uiState.value.strokeWidth) - private fun addNewShape(offset: Offset) { - val data = PointsData( - points = mutableStateListOf(offset), - strokeColor = Color(artMakerUIState.value.strokeColour), - strokeWidth = artMakerUIState.value.strokeWidth.toFloat(), - ) - _pathList.add(data) - _artMakerUIState.update { it.copy(canUndo = true, canClear = true) } - } - - private fun updateCurrentShape(offset: Offset) { - val idx = _pathList.lastIndex - _pathList[idx].points.add(offset) - } - - private fun undoLastShapePoint() { - val idx = _pathList.lastIndex - _pathList[idx].points.removeLast() + private fun listenToUndoRedoState() { + viewModelScope.launch { + drawingManager.undoRedoState.collectLatest { state -> + _uiState.update { it.copy(canRedo = state.canRedo, canUndo = state.canUndo, canClear = state.canClear) } + } + } } private fun triggerArtExport(type: ExportType) { @@ -152,26 +132,6 @@ internal class ArtMakerViewModel( } } - private fun redo() { - if (undoStack.isNotEmpty()) { - pathList.add(undoStack.pop()) - } - _artMakerUIState.update { it.copy(canUndo = true, canRedo = undoStack.isNotEmpty()) } - } - - private fun undo() { - if (_pathList.isNotEmpty()) { - undoStack.push(_pathList.removeLast()) - _artMakerUIState.update { it.copy(canRedo = true, canUndo = pathList.isNotEmpty()) } - } - } - - private fun clear() { - _pathList.clear() - undoStack.clear() - _artMakerUIState.update { it.copy(canRedo = false, canUndo = false, canClear = false) } - } - private fun updateBackgroundColour() {} private fun updateStrokeColor(colour: Color) { @@ -179,7 +139,7 @@ internal class ArtMakerViewModel( key = PreferenceKeys.SELECTED_STROKE_COLOUR, value = colour.toArgb(), ) - _artMakerUIState.update { + _uiState.update { it.copy( strokeColour = preferences.get( key = PreferenceKeys.SELECTED_STROKE_COLOUR, @@ -198,7 +158,7 @@ internal class ArtMakerViewModel( key = SELECTED_STROKE_WIDTH, value = strokeWidth, ) - _artMakerUIState.update { + _uiState.update { it.copy( strokeWidth = preferences.get( SELECTED_STROKE_WIDTH, @@ -210,7 +170,7 @@ internal class ArtMakerViewModel( private fun updateStylusSetting(useStylusOnly: Boolean) { preferences.set(PreferenceKeys.USE_STYLUS_ONLY, useStylusOnly) - _artMakerUIState.update { + _uiState.update { it.copy( shouldUseStylusOnly = preferences.get( key = PreferenceKeys.USE_STYLUS_ONLY, @@ -222,7 +182,7 @@ internal class ArtMakerViewModel( private fun updateStylusDialogShowSetting(canShowStylusDialog: Boolean) { preferences.set(PreferenceKeys.SHOW_STYLUS_DIALOG, canShowStylusDialog) - _artMakerUIState.update { + _uiState.update { it.copy( canShowStylusDialog = preferences.get( key = PreferenceKeys.SHOW_STYLUS_DIALOG, @@ -243,6 +203,7 @@ internal class ArtMakerViewModel( preferences = ArtMakerSharedPreferences( context = application, ), + drawingManager = DrawingManager(), applicationContext = application.applicationContext, ) as T } diff --git a/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt b/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt index e625d8c8..86b81af3 100644 --- a/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt +++ b/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt @@ -24,9 +24,10 @@ import androidx.compose.ui.graphics.ImageBitmap sealed interface ArtMakerAction { data class TriggerArtExport(val type: ExportType) : ArtMakerAction data class ExportArt(val bitmap: ImageBitmap) : ArtMakerAction - data object Undo : ArtMakerAction - data object Redo : ArtMakerAction - data object Clear : ArtMakerAction + +// data object Undo : ArtMakerAction +// data object Redo : ArtMakerAction +// data object Clear : ArtMakerAction data object UpdateBackground : ArtMakerAction data class SelectStrokeColour(val color: Color) : ArtMakerAction data class SetStrokeWidth(val strokeWidth: Int) : ArtMakerAction diff --git a/artmaker/src/main/java/io/artmaker/actions/DrawEvent.kt b/artmaker/src/main/java/io/artmaker/actions/DrawEvent.kt index 9ff70d9b..cb7f639f 100644 --- a/artmaker/src/main/java/io/artmaker/actions/DrawEvent.kt +++ b/artmaker/src/main/java/io/artmaker/actions/DrawEvent.kt @@ -24,4 +24,7 @@ sealed interface DrawEvent { data class AddNewShape(val offset: Offset) : DrawEvent data class UpdateCurrentShape(val offset: Offset) : DrawEvent data object UndoLastShapePoint : DrawEvent + data object Undo : DrawEvent + data object Redo : DrawEvent + data object Clear : DrawEvent } diff --git a/artmaker/src/main/java/io/artmaker/composables/ArtMakerControlMenu.kt b/artmaker/src/main/java/io/artmaker/composables/ArtMakerControlMenu.kt index 7ccb174b..ca766e53 100644 --- a/artmaker/src/main/java/io/artmaker/composables/ArtMakerControlMenu.kt +++ b/artmaker/src/main/java/io/artmaker/composables/ArtMakerControlMenu.kt @@ -62,6 +62,7 @@ import androidx.core.os.BuildCompat import com.google.modernstorage.photopicker.PhotoPicker import io.artmaker.ArtMakerUIState import io.artmaker.actions.ArtMakerAction +import io.artmaker.actions.DrawEvent import io.artmaker.models.ArtMakerConfiguration import io.artmaker.utils.ColorUtils import io.fbada006.artmaker.R @@ -79,6 +80,7 @@ private const val IMAGE_PICKER_MAX_ITEMS = 1 internal fun ArtMakerControlMenu( state: ArtMakerUIState, onAction: (ArtMakerAction) -> Unit, + onDrawEvent: (DrawEvent) -> Unit, modifier: Modifier = Modifier, onShowStrokeWidthPopup: () -> Unit, setBackgroundImage: (ImageBitmap?) -> Unit, @@ -132,21 +134,21 @@ internal fun ArtMakerControlMenu( MenuItem( imageVector = ImageVector.vectorResource(id = R.drawable.ic_undo), onItemClicked = { - onAction(ArtMakerAction.Undo) + onDrawEvent(DrawEvent.Undo) }, enabled = state.canUndo, ) MenuItem( imageVector = ImageVector.vectorResource(id = R.drawable.ic_redo), onItemClicked = { - onAction(ArtMakerAction.Redo) + onDrawEvent(DrawEvent.Redo) }, enabled = state.canRedo, ) MenuItem( imageVector = Icons.Filled.Refresh, onItemClicked = { - onAction(ArtMakerAction.Clear) + onDrawEvent(DrawEvent.Clear) }, enabled = state.canClear, ) diff --git a/artmaker/src/main/java/io/artmaker/export/DrawingManager.kt b/artmaker/src/main/java/io/artmaker/export/DrawingManager.kt new file mode 100644 index 00000000..57246071 --- /dev/null +++ b/artmaker/src/main/java/io/artmaker/export/DrawingManager.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2024 ArtMaker + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package io.artmaker.export + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import io.artmaker.actions.DrawEvent +import io.artmaker.models.PointsData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import java.util.Stack + +/** + * The drawing manager will handle all the logic related to drawing including clearing, undo, and redo + */ +internal class DrawingManager { + private val undoStack = Stack() + + private val _pathList = mutableStateListOf() + val pathList: SnapshotStateList = _pathList + + private val _undoRedoState = MutableStateFlow(UndoRedoState()) + val undoRedoState: StateFlow = _undoRedoState + + fun onDrawEvent(event: DrawEvent, strokeColor: Int, strokeWidth: Int) { + when (event) { + is DrawEvent.AddNewShape -> addNewShape(event.offset, strokeColor, strokeWidth) + DrawEvent.UndoLastShapePoint -> undoLastShapePoint() + is DrawEvent.UpdateCurrentShape -> updateCurrentShape(event.offset) + DrawEvent.Clear -> clear() + DrawEvent.Redo -> redo() + DrawEvent.Undo -> undo() + } + } + + private fun addNewShape(offset: Offset, strokeColor: Int, strokeWidth: Int) { + val data = PointsData( + points = mutableStateListOf(offset), + strokeColor = Color(strokeColor), + strokeWidth = strokeWidth.toFloat(), + ) + _pathList.add(data) + _undoRedoState.update { it.copy(canUndo = true, canClear = true) } + } + + private fun updateCurrentShape(offset: Offset) { + val idx = _pathList.lastIndex + _pathList[idx].points.add(offset) + } + + private fun undoLastShapePoint() { + val idx = _pathList.lastIndex + _pathList[idx].points.removeLast() + } + + private fun redo() { + if (undoStack.isEmpty()) return + _pathList.add(undoStack.pop()) + _undoRedoState.update { it.copy(canUndo = true, canRedo = undoStack.isNotEmpty()) } + } + + private fun undo() { + if (_pathList.isEmpty()) return + undoStack.push(_pathList.removeLast()) + _undoRedoState.update { it.copy(canRedo = true, canUndo = _pathList.isNotEmpty()) } + } + + private fun clear() { + _pathList.clear() + undoStack.clear() + _undoRedoState.update { UndoRedoState() } + } +} + +internal data class UndoRedoState( + val canUndo: Boolean = false, + val canRedo: Boolean = false, + val canClear: Boolean = false, +) From df4806b1db0d7d67cc13dbd5c7b048c7160902f4 Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Fri, 23 Aug 2024 13:44:51 +0300 Subject: [PATCH 10/16] minor improvement --- .../java/io/artmaker/export/DrawingManager.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/artmaker/src/main/java/io/artmaker/export/DrawingManager.kt b/artmaker/src/main/java/io/artmaker/export/DrawingManager.kt index 57246071..f8fb4d35 100644 --- a/artmaker/src/main/java/io/artmaker/export/DrawingManager.kt +++ b/artmaker/src/main/java/io/artmaker/export/DrawingManager.kt @@ -56,7 +56,7 @@ internal class DrawingManager { strokeWidth = strokeWidth.toFloat(), ) _pathList.add(data) - _undoRedoState.update { it.copy(canUndo = true, canClear = true) } + _undoRedoState.update { computeUndoRedoState() } } private fun updateCurrentShape(offset: Offset) { @@ -72,19 +72,27 @@ internal class DrawingManager { private fun redo() { if (undoStack.isEmpty()) return _pathList.add(undoStack.pop()) - _undoRedoState.update { it.copy(canUndo = true, canRedo = undoStack.isNotEmpty()) } + _undoRedoState.update { computeUndoRedoState() } } private fun undo() { if (_pathList.isEmpty()) return undoStack.push(_pathList.removeLast()) - _undoRedoState.update { it.copy(canRedo = true, canUndo = _pathList.isNotEmpty()) } + _undoRedoState.update { computeUndoRedoState() } } private fun clear() { _pathList.clear() undoStack.clear() - _undoRedoState.update { UndoRedoState() } + _undoRedoState.update { computeUndoRedoState() } + } + + private fun computeUndoRedoState(): UndoRedoState { + return UndoRedoState( + canUndo = _pathList.isNotEmpty(), + canRedo = undoStack.isNotEmpty(), + canClear = _pathList.isNotEmpty() + ) } } From 4f32befec467a80b4b3d65d84cd3408a565bda4c Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Fri, 23 Aug 2024 13:51:40 +0300 Subject: [PATCH 11/16] remove commented out code --- artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt b/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt index 86b81af3..d4242209 100644 --- a/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt +++ b/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt @@ -24,10 +24,6 @@ import androidx.compose.ui.graphics.ImageBitmap sealed interface ArtMakerAction { data class TriggerArtExport(val type: ExportType) : ArtMakerAction data class ExportArt(val bitmap: ImageBitmap) : ArtMakerAction - -// data object Undo : ArtMakerAction -// data object Redo : ArtMakerAction -// data object Clear : ArtMakerAction data object UpdateBackground : ArtMakerAction data class SelectStrokeColour(val color: Color) : ArtMakerAction data class SetStrokeWidth(val strokeWidth: Int) : ArtMakerAction From 0154becb7c26a340a77ffe3c4642ad8721a2e5e6 Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Fri, 23 Aug 2024 13:55:38 +0300 Subject: [PATCH 12/16] extract string resource --- .../src/main/java/io/artmaker/composables/StrokeSettings.kt | 4 +++- artmaker/src/main/res/values/strings.xml | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt b/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt index 92b8c572..2a639e93 100644 --- a/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt +++ b/artmaker/src/main/java/io/artmaker/composables/StrokeSettings.kt @@ -32,9 +32,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import io.artmaker.actions.ArtMakerAction import io.artmaker.models.ArtMakerConfiguration import io.artmaker.utils.isStylusConnected +import io.fbada006.artmaker.R @Composable fun StrokeSettings(strokeWidth: Int, shouldUseStylusOnly: Boolean, onAction: (ArtMakerAction) -> Unit, configuration: ArtMakerConfiguration, modifier: Modifier = Modifier) { @@ -56,7 +58,7 @@ fun StrokeSettings(strokeWidth: Int, shouldUseStylusOnly: Boolean, onAction: (Ar HorizontalDivider() Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Text( - text = "Use stylus only", + text = stringResource(R.string.use_stylus_only), style = MaterialTheme.typography.bodyLarge, ) Switch( diff --git a/artmaker/src/main/res/values/strings.xml b/artmaker/src/main/res/values/strings.xml index 7efe7884..95157b85 100644 --- a/artmaker/src/main/res/values/strings.xml +++ b/artmaker/src/main/res/values/strings.xml @@ -26,4 +26,5 @@ We have detected that you are using a stylus to draw. For a better experience, please enable stylus only input using the pencil icon at the bottom of the screen. We have detected that you are not using your stylus to draw but you have enabled the stylus only input setting. Please disable this using the pencil icon at the bottom of the screen if you wish to use other input types to draw. Do not show again. + Use stylus only \ No newline at end of file From 458e769387eea56e2913e79922a694396d8ff798 Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Fri, 23 Aug 2024 13:57:02 +0300 Subject: [PATCH 13/16] docs and spotless --- artmaker/src/main/java/io/artmaker/export/DrawingManager.kt | 2 +- artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/artmaker/src/main/java/io/artmaker/export/DrawingManager.kt b/artmaker/src/main/java/io/artmaker/export/DrawingManager.kt index f8fb4d35..b978afb8 100644 --- a/artmaker/src/main/java/io/artmaker/export/DrawingManager.kt +++ b/artmaker/src/main/java/io/artmaker/export/DrawingManager.kt @@ -91,7 +91,7 @@ internal class DrawingManager { return UndoRedoState( canUndo = _pathList.isNotEmpty(), canRedo = undoStack.isNotEmpty(), - canClear = _pathList.isNotEmpty() + canClear = _pathList.isNotEmpty(), ) } } diff --git a/artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt b/artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt index e17fa25f..ca71db88 100644 --- a/artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt +++ b/artmaker/src/main/java/io/artmaker/utils/MotionEventUtil.kt @@ -38,6 +38,9 @@ internal fun MotionEvent.validateEvent(context: Context, useStylusOnly: Boolean) return !useStylusOnly || isStylusDrawing } +/** + * Does this device have a stylus or not? + */ internal fun isStylusConnected(context: Context): Boolean { val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager val inputDeviceIds: IntArray = inputManager.inputDeviceIds From 337bd8f43ce7f6be3936033da96386ea78d00386 Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Mon, 26 Aug 2024 11:29:17 +0300 Subject: [PATCH 14/16] update readme to include android weekly badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 91c88c4c..2d8cf4b9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ [![Build ArtMaker](https://github.com/Fbada006/ArtMaker/actions/workflows/build.yml/badge.svg)](https://github.com/Fbada006/ArtMaker/actions/workflows/build.yml) +[![Android Weekly](https://androidweekly.net/issues/issue-637/badge)](https://androidweekly.net/issues/issue-637) +

From 7c695d57dbc84bd31c0b0a79b361bf7bac7d3521 Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Mon, 26 Aug 2024 11:29:57 +0300 Subject: [PATCH 15/16] refactor code to show enable stylus and disable stylus dialogs once only --- .../src/main/java/io/artmaker/ArtMaker.kt | 3 +- .../main/java/io/artmaker/ArtMakerUIState.kt | 3 +- .../java/io/artmaker/ArtMakerViewModel.kt | 31 ++++++++++++---- .../src/main/java/io/artmaker/DrawState.kt | 3 +- .../io/artmaker/actions/ArtMakerAction.kt | 3 +- .../composables/ArtMakerDrawScreen.kt | 36 ++++++++----------- .../java/io/artmaker/data/PreferenceKeys.kt | 3 +- artmaker/src/main/res/values/strings.xml | 3 +- 8 files changed, 51 insertions(+), 34 deletions(-) diff --git a/artmaker/src/main/java/io/artmaker/ArtMaker.kt b/artmaker/src/main/java/io/artmaker/ArtMaker.kt index 568c1971..3b778dde 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMaker.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMaker.kt @@ -126,7 +126,8 @@ fun ArtMaker( backgroundImage = viewModel.backgroundImage.value, isFullScreenMode = isFullScreenEnabled, shouldUseStylusOnly = state.shouldUseStylusOnly, - canShowStylusDialog = state.canShowStylusDialog, + canShowEnableStylusDialog = state.canShowEnableStylusDialog, + canShowDisableStylusDialog = state.canShowDisableStylusDialog, ), ) AnimatedVisibility(visible = showStrokeSettings) { diff --git a/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt b/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt index 2c04d737..2851ab84 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMakerUIState.kt @@ -29,5 +29,6 @@ data class ArtMakerUIState( val canUndo: Boolean = false, val canClear: Boolean = false, val shouldUseStylusOnly: Boolean = false, - val canShowStylusDialog: Boolean = true, + val canShowEnableStylusDialog: Boolean = true, + val canShowDisableStylusDialog: Boolean = true, ) diff --git a/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt b/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt index 25d25dbe..9adc60c1 100644 --- a/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt +++ b/artmaker/src/main/java/io/artmaker/ArtMakerViewModel.kt @@ -66,8 +66,12 @@ internal class ArtMakerViewModel( key = PreferenceKeys.USE_STYLUS_ONLY, false, ), - canShowStylusDialog = preferences.get( - key = PreferenceKeys.SHOW_STYLUS_DIALOG, + canShowEnableStylusDialog = preferences.get( + key = PreferenceKeys.SHOW_ENABLE_STYLUS_DIALOG, + true, + ), + canShowDisableStylusDialog = preferences.get( + key = PreferenceKeys.SHOW_DISABLE_STYLUS_DIALOG, true, ), ), @@ -99,7 +103,8 @@ internal class ArtMakerViewModel( is ArtMakerAction.SelectStrokeColour -> updateStrokeColor(colour = action.color) is ArtMakerAction.SetStrokeWidth -> selectStrokeWidth(strokeWidth = action.strokeWidth) is ArtMakerAction.UpdateSetStylusOnly -> updateStylusSetting(useStylusOnly = action.shouldUseStylusOnly) - is ArtMakerAction.UpdateStylusDialogNeverShow -> updateStylusDialogShowSetting(canShowStylusDialog = action.canShowStylusDialog) + is ArtMakerAction.UpdateEnableStylusDialogShow -> updateEnableStylusDialog(canShow = action.canShowEnableStylusDialog) + is ArtMakerAction.UpdateDisableStylusDialogShow -> updateDisableStylusDialog(canShow = action.canShowDisableStylusDialog) } } @@ -180,12 +185,24 @@ internal class ArtMakerViewModel( } } - private fun updateStylusDialogShowSetting(canShowStylusDialog: Boolean) { - preferences.set(PreferenceKeys.SHOW_STYLUS_DIALOG, canShowStylusDialog) + private fun updateEnableStylusDialog(canShow: Boolean) { + preferences.set(PreferenceKeys.SHOW_ENABLE_STYLUS_DIALOG, canShow) + _uiState.update { + it.copy( + canShowEnableStylusDialog = preferences.get( + key = PreferenceKeys.SHOW_ENABLE_STYLUS_DIALOG, + true, + ), + ) + } + } + + private fun updateDisableStylusDialog(canShow: Boolean) { + preferences.set(PreferenceKeys.SHOW_DISABLE_STYLUS_DIALOG, canShow) _uiState.update { it.copy( - canShowStylusDialog = preferences.get( - key = PreferenceKeys.SHOW_STYLUS_DIALOG, + canShowDisableStylusDialog = preferences.get( + key = PreferenceKeys.SHOW_DISABLE_STYLUS_DIALOG, true, ), ) diff --git a/artmaker/src/main/java/io/artmaker/DrawState.kt b/artmaker/src/main/java/io/artmaker/DrawState.kt index b4a89e22..f17eb36f 100644 --- a/artmaker/src/main/java/io/artmaker/DrawState.kt +++ b/artmaker/src/main/java/io/artmaker/DrawState.kt @@ -28,5 +28,6 @@ internal data class DrawState( val shouldTriggerArtExport: Boolean, val isFullScreenMode: Boolean, val shouldUseStylusOnly: Boolean, - val canShowStylusDialog: Boolean, + val canShowEnableStylusDialog: Boolean, + val canShowDisableStylusDialog: Boolean, ) diff --git a/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt b/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt index d4242209..080fdd05 100644 --- a/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt +++ b/artmaker/src/main/java/io/artmaker/actions/ArtMakerAction.kt @@ -28,7 +28,8 @@ sealed interface ArtMakerAction { data class SelectStrokeColour(val color: Color) : ArtMakerAction data class SetStrokeWidth(val strokeWidth: Int) : ArtMakerAction data class UpdateSetStylusOnly(val shouldUseStylusOnly: Boolean) : ArtMakerAction - class UpdateStylusDialogNeverShow(val canShowStylusDialog: Boolean) : ArtMakerAction + class UpdateEnableStylusDialogShow(val canShowEnableStylusDialog: Boolean) : ArtMakerAction + class UpdateDisableStylusDialogShow(val canShowDisableStylusDialog: Boolean) : ArtMakerAction } sealed interface ExportType { diff --git a/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt b/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt index ac74fec8..d3741cb2 100644 --- a/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt +++ b/artmaker/src/main/java/io/artmaker/composables/ArtMakerDrawScreen.kt @@ -22,15 +22,11 @@ import android.view.MotionEvent import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button -import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult @@ -210,12 +206,13 @@ internal fun ArtMakerDrawScreen( }, ) - if (shouldShowStylusDialog && state.canShowStylusDialog) { + if (shouldShowStylusDialog && (state.canShowEnableStylusDialog || state.canShowDisableStylusDialog)) { if (stylusDialogType.isEmpty()) return val type = StylusDialogType.valueOf(stylusDialogType) - val dialogInfo = when (type) { - StylusDialogType.ENABLE_STYLUS_ONLY -> stringResource(R.string.stylus_input_detected_title) to stringResource(R.string.styluse_input_detected_message) - StylusDialogType.DISABLE_STYLUS_ONLY -> stringResource(R.string.non_stylus_input_detected_title) to stringResource(R.string.non_stylus_input_detected_message) + val dialogInfo = when { + state.canShowEnableStylusDialog && type == StylusDialogType.ENABLE_STYLUS_ONLY -> stringResource(R.string.stylus_input_detected_title) to stringResource(R.string.stylus_input_detected_message) + state.canShowDisableStylusDialog && type == StylusDialogType.DISABLE_STYLUS_ONLY -> stringResource(R.string.non_stylus_input_detected_title) to stringResource(R.string.non_stylus_input_detected_message) + else -> return } AlertDialog( @@ -225,19 +222,16 @@ internal fun ArtMakerDrawScreen( onDismissRequest = { shouldShowStylusDialog = false }, confirmButton = { Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = { shouldShowStylusDialog = false }) { - Text(text = stringResource(id = android.R.string.ok)) - } - Spacer(modifier = Modifier.height(dimensionResource(id = R.dimen.Padding7))) - Row(verticalAlignment = Alignment.CenterVertically) { - Text(text = stringResource(R.string.do_not_show_again)) - Checkbox( - checked = false, - onCheckedChange = { - shouldShowStylusDialog = false - onAction(ArtMakerAction.UpdateStylusDialogNeverShow(false)) - }, - ) + Button( + onClick = { + shouldShowStylusDialog = false + when (type) { + StylusDialogType.ENABLE_STYLUS_ONLY -> onAction(ArtMakerAction.UpdateEnableStylusDialogShow(false)) + StylusDialogType.DISABLE_STYLUS_ONLY -> onAction(ArtMakerAction.UpdateDisableStylusDialogShow(false)) + } + }, + ) { + Text(text = stringResource(id = R.string.got_it)) } } }, diff --git a/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt b/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt index 0e59610c..0276dc9f 100644 --- a/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt +++ b/artmaker/src/main/java/io/artmaker/data/PreferenceKeys.kt @@ -20,5 +20,6 @@ object PreferenceKeys { const val SELECTED_STROKE_COLOUR = "com.artmaker.sharedpreferences.selectedStrokeColour" const val SELECTED_STROKE_WIDTH = "com.artmaker.sharedpreferences.selectedStrokeWidth" const val USE_STYLUS_ONLY = "com.artmaker.sharedpreferences.useStylusOnly" - const val SHOW_STYLUS_DIALOG = "com.artmaker.sharedpreferences.showStylusDialog" + const val SHOW_ENABLE_STYLUS_DIALOG = "com.artmaker.sharedpreferences.showEnableStylusDialog" + const val SHOW_DISABLE_STYLUS_DIALOG = "com.artmaker.sharedpreferences.showDisableStylusDialog" } diff --git a/artmaker/src/main/res/values/strings.xml b/artmaker/src/main/res/values/strings.xml index 95157b85..28740454 100644 --- a/artmaker/src/main/res/values/strings.xml +++ b/artmaker/src/main/res/values/strings.xml @@ -23,8 +23,9 @@ Clear Image Stylus input detected Non-Stylus input detected - We have detected that you are using a stylus to draw. For a better experience, please enable stylus only input using the pencil icon at the bottom of the screen. + We have detected that you are using a stylus to draw. For a better experience, please enable stylus only input using the pencil icon at the bottom of the screen. We have detected that you are not using your stylus to draw but you have enabled the stylus only input setting. Please disable this using the pencil icon at the bottom of the screen if you wish to use other input types to draw. Do not show again. Use stylus only + Got it \ No newline at end of file From cdd59c5c4536741f23f0e60a93be437a83a3351c Mon Sep 17 00:00:00 2001 From: Ferdinand Bada Date: Mon, 26 Aug 2024 11:42:00 +0300 Subject: [PATCH 16/16] update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2d8cf4b9..b10e84fa 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Flexible and lightweight Drawing Library

+ ArtMaker is a flexible and customisable library that allows users to draw anything they want on screen and has been built fully with Jetpack Compose. It allows drawing through the `Canvas`, sharing the drawn `Bitmap`, or programmatically exposing the `Bitmap` for use in the calling application. ## Getting started