Added stylus-only mode for PDF drawing and annotations. (#27)
This change introduces a "Stylus Only Mode" toggle in the annotation dock, allowing users to restrict drawing input to stylus pens while maintaining touch support for scrolling and navigation. Key changes include: - Added `isStylusOnlyMode` state and persistence logic in `PdfViewerScreen`. - Updated `AnnotationDock` with a toggle button for the new mode. - Modified pointer input handling in `PdfPageComposable` and `PdfVerticalReader` to filter touch events when stylus-only mode is active. - Adjusted tap gesture detection to ensure scrolling and page clicks still function correctly under various input modes.
This commit is contained in:
parent
d459535ffd
commit
b47b6a9164
5 changed files with 97 additions and 25 deletions
|
|
@ -35,6 +35,8 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.automirrored.filled.Redo
|
||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.DoNotTouch
|
||||
import androidx.compose.material.icons.filled.TouchApp
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -68,7 +70,9 @@ fun AnnotationDock(
|
|||
modifier: Modifier = Modifier,
|
||||
isSticky: Boolean = false,
|
||||
isMinimized: Boolean,
|
||||
onToggleMinimize: () -> Unit
|
||||
onToggleMinimize: () -> Unit,
|
||||
isStylusOnlyMode: Boolean,
|
||||
onToggleStylusOnlyMode: () -> Unit
|
||||
) {
|
||||
val showFullDock = isSticky || !isMinimized
|
||||
|
||||
|
|
@ -141,6 +145,29 @@ fun AnnotationDock(
|
|||
horizontalArrangement = Arrangement.spacedBy(spacing),
|
||||
modifier = Modifier.alpha(toolsAlpha)
|
||||
) {
|
||||
|
||||
// Stylus Only Toggle
|
||||
if (!isMinimized) {
|
||||
val iconVector = if (isStylusOnlyMode) Icons.Default.DoNotTouch else Icons.Default.TouchApp
|
||||
val iconTint = if (isStylusOnlyMode) Color(0xFFE57373) else Color.White
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(buttonSize)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White.copy(alpha = if (isStylusOnlyMode) 0.15f else 0f))
|
||||
.clickable(onClick = onToggleStylusOnlyMode),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = iconVector,
|
||||
contentDescription = "Stylus Only Mode",
|
||||
tint = iconTint,
|
||||
modifier = Modifier.size(iconSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Pen Group
|
||||
val isPenActive = !isMinimized && (selectedTool == InkType.PEN ||
|
||||
selectedTool == InkType.FOUNTAIN_PEN ||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
|
||||
import androidx.compose.ui.input.pointer.PointerType
|
||||
import androidx.compose.ui.input.pointer.changedToUp
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.positionChanged
|
||||
|
|
@ -416,6 +417,7 @@ internal fun PdfPageComposable(
|
|||
draggingBoxId: String? = null,
|
||||
isScrollLocked: Boolean = false,
|
||||
isVisible: Boolean = true,
|
||||
isStylusOnlyMode: Boolean = false
|
||||
) {
|
||||
SideEffect { Timber.tag("PdfDrawPerf").v("PdfPageComposable Recompose: Page $pageIndex") }
|
||||
val pdfDocumentItem = pdfDocument.item
|
||||
|
|
@ -2237,9 +2239,14 @@ internal fun PdfPageComposable(
|
|||
isZoomEnabled,
|
||||
isVerticalScroll,
|
||||
isEditMode,
|
||||
selectedTool
|
||||
selectedTool,
|
||||
isStylusOnlyMode
|
||||
) {
|
||||
if (isEditMode && selectedTool != InkType.TEXT) return@pointerInput
|
||||
val isTapDetectionAllowed = !isEditMode ||
|
||||
selectedTool == InkType.TEXT ||
|
||||
isStylusOnlyMode
|
||||
|
||||
if (!isTapDetectionAllowed) return@pointerInput
|
||||
|
||||
detectTapGestures(onTap = { tapOffset ->
|
||||
Timber.d(
|
||||
|
|
@ -2600,7 +2607,8 @@ internal fun PdfPageComposable(
|
|||
offset,
|
||||
isScrolling,
|
||||
isVerticalScroll,
|
||||
selectedTool
|
||||
selectedTool,
|
||||
isStylusOnlyMode
|
||||
) {
|
||||
val canDraw = isEditMode && selectedTool != InkType.TEXT && !isScrolling && !isVerticalScroll && actualBitmapWidthPx > 0 && actualBitmapHeightPx > 0
|
||||
|
||||
|
|
@ -2611,6 +2619,13 @@ internal fun PdfPageComposable(
|
|||
try {
|
||||
awaitEachGesture {
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
|
||||
Timber.tag("PointerTypeDebug").d("Page $pageIndex: Input Type detected: ${down.type}")
|
||||
|
||||
if (isStylusOnlyMode && down.type == PointerType.Touch) {
|
||||
return@awaitEachGesture
|
||||
}
|
||||
|
||||
val dragPointerId = down.id
|
||||
val startPos = down.position
|
||||
var dragStarted = false
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.PointerType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.positionChanged
|
||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||
|
|
@ -216,7 +217,8 @@ internal fun PdfVerticalReader(
|
|||
isAutoScrollTempPaused: Boolean = false,
|
||||
isScrollLocked: Boolean = false,
|
||||
autoScrollSpeed: Float = 1.0f,
|
||||
onInteractionListener: () -> Unit = {}
|
||||
onInteractionListener: () -> Unit = {},
|
||||
isStylusOnlyMode: Boolean = false
|
||||
) {
|
||||
SideEffect { Timber.tag("PdfDrawPerf").v("LIST: PdfVerticalReader Recomposing.") }
|
||||
var globalEraserPosition by remember { mutableStateOf<Offset?>(null) }
|
||||
|
|
@ -726,13 +728,17 @@ internal fun PdfVerticalReader(
|
|||
}
|
||||
}
|
||||
|
||||
val globalDrawingModifier = Modifier.pointerInput(isEditMode, layoutInfo, selectedTool) {
|
||||
val globalDrawingModifier = Modifier.pointerInput(isEditMode, layoutInfo, selectedTool, isStylusOnlyMode) {
|
||||
if (!isEditMode) return@pointerInput
|
||||
if (selectedTool == InkType.TEXT) return@pointerInput
|
||||
|
||||
awaitEachGesture {
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
|
||||
if (isStylusOnlyMode && down.type == PointerType.Touch) {
|
||||
return@awaitEachGesture
|
||||
}
|
||||
|
||||
fun getPageAndPoint(screenOffset: Offset): Pair<Int, PdfPoint>? {
|
||||
val zoom = zoomAnimatable.value
|
||||
val panX = panXAnimatable.value
|
||||
|
|
@ -814,31 +820,31 @@ internal fun PdfVerticalReader(
|
|||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(globalDrawingModifier)
|
||||
.pointerInput(isEditMode, selectedTool) {
|
||||
.pointerInput(isEditMode, selectedTool, isStylusOnlyMode) {
|
||||
Timber.tag("PdfTouchDebug").v(
|
||||
"VerticalReader: TapPointerInput init. isEditMode=$isEditMode"
|
||||
)
|
||||
if (isEditMode && selectedTool != InkType.TEXT) return@pointerInput
|
||||
|
||||
val isTapDetectionAllowed = !isEditMode ||
|
||||
selectedTool == InkType.TEXT ||
|
||||
isStylusOnlyMode
|
||||
|
||||
if (!isTapDetectionAllowed) return@pointerInput
|
||||
|
||||
detectTapGestures(onTap = {
|
||||
if (!isEditMode) {
|
||||
Timber.tag("PdfTouchDebug").d(
|
||||
"VerticalReader: Tap detected (Toggling bars)"
|
||||
)
|
||||
Timber.tag("PdfTouchDebug").d("VerticalReader: Tap detected")
|
||||
selectionClearTrigger++
|
||||
onPageClick()
|
||||
} else if (selectedTool == InkType.TEXT) {
|
||||
onPageClick()
|
||||
}
|
||||
}, onDoubleTap = { offset ->
|
||||
if (!isEditMode) {
|
||||
Timber.tag("PdfTouchDebug").d("VerticalReader: DoubleTap detected")
|
||||
onDoubleTapToZoom(offset)
|
||||
}
|
||||
Timber.tag("PdfTouchDebug").d("VerticalReader: DoubleTap detected")
|
||||
onDoubleTapToZoom(offset)
|
||||
})
|
||||
}
|
||||
|
||||
.pointerInput(totalDocHeight, isEditMode, selectedTool, isScrollLocked) {
|
||||
.pointerInput(totalDocHeight, isEditMode, selectedTool, isScrollLocked, isStylusOnlyMode) {
|
||||
val tracker = VelocityTracker()
|
||||
val decay = exponentialDecay<Float>()
|
||||
val touchSlop = viewConfiguration.touchSlop
|
||||
|
|
@ -849,11 +855,14 @@ internal fun PdfVerticalReader(
|
|||
)
|
||||
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
Timber.tag("PdfTouchDebug").v(
|
||||
"VerticalReader: DOWN received. Pressed=${down.pressed}, Consumed=${down.isConsumed}"
|
||||
)
|
||||
|
||||
if (isEditMode && selectedTool != InkType.TEXT && down.pressed) {
|
||||
Timber.tag("PointerTypeDebug").d("VerticalReader: Input Type detected: ${down.type}")
|
||||
|
||||
val isDrawingGesture = isEditMode &&
|
||||
selectedTool != InkType.TEXT &&
|
||||
(!isStylusOnlyMode || down.type != PointerType.Touch)
|
||||
|
||||
if (isDrawingGesture && down.pressed) {
|
||||
val event = awaitPointerEvent()
|
||||
Timber.tag("PdfTouchDebug").v(
|
||||
"VerticalReader: EditMode check. Event changes: ${event.changes.size}"
|
||||
|
|
@ -959,8 +968,9 @@ internal fun PdfVerticalReader(
|
|||
}
|
||||
}
|
||||
|
||||
val shouldScroll =
|
||||
(panLocked || gestureDisambiguationMode != 0) && (!isEditMode || selectedTool == InkType.TEXT || isMultiTouch)
|
||||
val isTouchInput = event.changes.all { it.type == PointerType.Touch }
|
||||
val shouldScroll = (panLocked || gestureDisambiguationMode != 0) &&
|
||||
(!isEditMode || selectedTool == InkType.TEXT || isMultiTouch || (isStylusOnlyMode && isTouchInput))
|
||||
|
||||
if (shouldScroll) {
|
||||
if (!isInteracting) {
|
||||
|
|
@ -1432,6 +1442,7 @@ internal fun PdfVerticalReader(
|
|||
onOcrModelDownloading = onOcrModelDownloading,
|
||||
selectedTool = selectedTool,
|
||||
richTextController = richTextController,
|
||||
isStylusOnlyMode = isStylusOnlyMode,
|
||||
textBoxes = textBoxes.filter { it.pageIndex == page.index },
|
||||
selectedTextBoxId = selectedTextBoxId,
|
||||
onTextBoxChange = onTextBoxChange,
|
||||
|
|
|
|||
|
|
@ -291,12 +291,23 @@ private const val PDF_AUTO_SCROLL_LOCKED_KEY = "pdf_auto_scroll_locked"
|
|||
private const val PDF_AUTO_SCROLL_USE_SLIDER_KEY = "pdf_auto_scroll_use_slider"
|
||||
private const val PDF_AUTO_SCROLL_MIN_SPEED_KEY = "pdf_auto_scroll_min_speed"
|
||||
private const val PDF_AUTO_SCROLL_MAX_SPEED_KEY = "pdf_auto_scroll_max_speed"
|
||||
private const val STYLUS_ONLY_MODE_KEY = "stylus_only_mode"
|
||||
|
||||
private fun savePdfAutoScrollMinSpeed(context: Context, speed: Float) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit { putFloat(PDF_AUTO_SCROLL_MIN_SPEED_KEY, speed) }
|
||||
}
|
||||
|
||||
private fun saveStylusOnlyMode(context: Context, isEnabled: Boolean) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit { putBoolean(STYLUS_ONLY_MODE_KEY, isEnabled) }
|
||||
}
|
||||
|
||||
private fun loadStylusOnlyMode(context: Context): Boolean {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return prefs.getBoolean(STYLUS_ONLY_MODE_KEY, false)
|
||||
}
|
||||
|
||||
private fun loadPdfAutoScrollMinSpeed(context: Context): Float {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return prefs.getFloat(PDF_AUTO_SCROLL_MIN_SPEED_KEY, 0.1f)
|
||||
|
|
@ -649,6 +660,7 @@ fun PdfViewerScreen(
|
|||
|
||||
var isAutoScrollLocked by remember { mutableStateOf(loadPdfAutoScrollLocked(context)) }
|
||||
var autoScrollUseSlider by remember { mutableStateOf(loadPdfAutoScrollUseSlider(context)) }
|
||||
var isStylusOnlyMode by remember { mutableStateOf(loadStylusOnlyMode(context)) }
|
||||
var currentTtsMode by remember { mutableStateOf(loadTtsMode(context)) }
|
||||
var showTtsSettingsSheet by remember { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -3298,6 +3310,7 @@ fun PdfViewerScreen(
|
|||
}
|
||||
},
|
||||
richTextController = richTextController,
|
||||
isStylusOnlyMode = isStylusOnlyMode,
|
||||
isEditMode = isDrawingActive,
|
||||
textBoxes = textBoxes.filter { it.pageIndex == pageIndex },
|
||||
selectedTextBoxId = selectedTextBoxId,
|
||||
|
|
@ -3649,6 +3662,7 @@ fun PdfViewerScreen(
|
|||
},
|
||||
selectedTool = selectedTool,
|
||||
richTextController = richTextController,
|
||||
isStylusOnlyMode = isStylusOnlyMode,
|
||||
isEditMode = isDrawingActive,
|
||||
textBoxes = textBoxes,
|
||||
selectedTextBoxId = selectedTextBoxId,
|
||||
|
|
@ -4894,6 +4908,11 @@ fun PdfViewerScreen(
|
|||
activePenColor = dockPenColor,
|
||||
activeHighlighterColor = dockHighlighterColor,
|
||||
lastPenTool = lastPenTool,
|
||||
isStylusOnlyMode = isStylusOnlyMode,
|
||||
onToggleStylusOnlyMode = {
|
||||
isStylusOnlyMode = !isStylusOnlyMode
|
||||
saveStylusOnlyMode(context, isStylusOnlyMode)
|
||||
},
|
||||
onToolClick = { clickedTool ->
|
||||
if (clickedTool == InkType.ERASER) {
|
||||
annotationSettingsRepo.updateSelectedTool(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue