From 4c7518988432b5f18aba637e52eaf18a99dbe750 Mon Sep 17 00:00:00 2001 From: Ghasem Shirdel Date: Fri, 22 Sep 2023 17:02:11 +0330 Subject: [PATCH] fix: vertical/horizontal zooming --- .../affogato/pdfviewer/HorizontalPDFReader.kt | 63 ++++++++-------- .../affogato/pdfviewer/VerticalPDFReader.kt | 10 +-- .../affogato/pdfviewer/zoomable/Zoomable.kt | 74 +++++-------------- .../pdfviewer/zoomable/ZoomableState.kt | 41 ++-------- gradle/libs.versions.toml | 2 +- sample/build.gradle.kts | 1 + 6 files changed, 62 insertions(+), 129 deletions(-) diff --git a/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/HorizontalPDFReader.kt b/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/HorizontalPDFReader.kt index ca0ff3f..79145d6 100644 --- a/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/HorizontalPDFReader.kt +++ b/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/HorizontalPDFReader.kt @@ -19,12 +19,16 @@ import com.parsuomash.affogato.pdfviewer.internal.loadPdf import com.parsuomash.affogato.pdfviewer.state.HorizontalPdfReaderState import com.parsuomash.affogato.pdfviewer.zoomable.Zoomable import com.parsuomash.affogato.pdfviewer.zoomable.ZoomableDefaults +import com.parsuomash.affogato.pdfviewer.zoomable.ZoomableState import com.parsuomash.affogato.pdfviewer.zoomable.rememberZoomableState @OptIn(ExperimentalFoundationApi::class) @Composable fun HorizontalPDFReader( state: HorizontalPdfReaderState, + zoomableState: ZoomableState = rememberZoomableState( + minScale = ZoomableDefaults.DefaultScale + ), modifier: Modifier ) { BoxWithConstraints( @@ -49,43 +53,40 @@ fun HorizontalPDFReader( } state.pdfRender?.let { pdf -> - val zoomableState = rememberZoomableState( - minScale = ZoomableDefaults.DefaultScale - ) - Zoomable( - state = zoomableState, - enabled = state.isZoomEnable - ) { - HorizontalPager( - modifier = Modifier.fillMaxSize(), - state = state.pagerState, - userScrollEnabled = state.scale == 1f - ) { page -> - val pageContent = pdf.pageLists[page].stateFlow.collectAsState().value - DisposableEffect(key1 = Unit) { - pdf.pageLists[page].load() - onDispose { - pdf.pageLists[page].recycle() - } - } - when (pageContent) { - is PageContentInt.PageContent -> { - PdfImage( - bitmap = { pageContent.bitmap.asImageBitmap() }, - contentDescription = pageContent.contentDescription - ) + zoomableState = zoomableState, + enabled = state.isZoomEnable, + content = { + HorizontalPager( + modifier = Modifier.fillMaxSize(), + state = state.pagerState, + userScrollEnabled = state.scale == 1f + ) { page -> + val pageContent = pdf.pageLists[page].stateFlow.collectAsState().value + DisposableEffect(key1 = Unit) { + pdf.pageLists[page].load() + onDispose { + pdf.pageLists[page].recycle() + } } + when (pageContent) { + is PageContentInt.PageContent -> { + PdfImage( + bitmap = { pageContent.bitmap.asImageBitmap() }, + contentDescription = pageContent.contentDescription + ) + } - is PageContentInt.BlankPage -> { - BlackPage( - width = pageContent.width, - height = pageContent.height - ) + is PageContentInt.BlankPage -> { + BlackPage( + width = pageContent.width, + height = pageContent.height + ) + } } } } - } + ) } } } diff --git a/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/VerticalPDFReader.kt b/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/VerticalPDFReader.kt index 180c770..e6f120f 100644 --- a/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/VerticalPDFReader.kt +++ b/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/VerticalPDFReader.kt @@ -18,11 +18,15 @@ import com.parsuomash.affogato.pdfviewer.internal.loadPdf import com.parsuomash.affogato.pdfviewer.state.VerticalPdfReaderState import com.parsuomash.affogato.pdfviewer.zoomable.Zoomable import com.parsuomash.affogato.pdfviewer.zoomable.ZoomableDefaults +import com.parsuomash.affogato.pdfviewer.zoomable.ZoomableState import com.parsuomash.affogato.pdfviewer.zoomable.rememberZoomableState @Composable fun VerticalPDFReader( state: VerticalPdfReaderState, + zoomableState: ZoomableState = rememberZoomableState( + minScale = ZoomableDefaults.DefaultScale + ), modifier: Modifier ) { BoxWithConstraints( @@ -48,12 +52,8 @@ fun VerticalPDFReader( } state.pdfRender?.let { pdf -> - val zoomableState = rememberZoomableState( - minScale = ZoomableDefaults.DefaultScale - ) - Zoomable( - state = zoomableState, + zoomableState = zoomableState, enabled = state.isZoomEnable ) { LazyColumn( diff --git a/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/zoomable/Zoomable.kt b/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/zoomable/Zoomable.kt index eb2138e..5d8428b 100644 --- a/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/zoomable/Zoomable.kt +++ b/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/zoomable/Zoomable.kt @@ -1,6 +1,5 @@ package com.parsuomash.affogato.pdfviewer.zoomable -import androidx.compose.foundation.gestures.animateZoomBy import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation @@ -11,10 +10,7 @@ import androidx.compose.foundation.gestures.rememberTransformableState import androidx.compose.foundation.gestures.transformable import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size @@ -35,47 +31,28 @@ import kotlinx.coroutines.launch * A zoomable layout that supports zooming in and out, dragging, double tap and dismiss gesture. * * @param modifier The modifier to apply to this layout. - * @param state The state object to be used to control or observe the state. + * @param zoomableState The state object to be used to control or observe the state. * @param enabled Controls the enabled state. When false, all gestures will be ignored. - * @param dismissGestureEnabled Whether to enable dismiss gesture detection. - * @param onDismiss Will be called when dismiss gesture is detected. Should return a boolean * indicating whether the dismiss request is handled. * @param content a block, which describes the content. */ @Composable internal fun Zoomable( modifier: Modifier = Modifier, - state: ZoomableState = rememberZoomableState(), + zoomableState: ZoomableState = rememberZoomableState(), enabled: Boolean = true, - dismissGestureEnabled: Boolean = false, - onDismiss: () -> Boolean = { false }, content: @Composable () -> Unit ) { - val dismissGestureEnabledState = rememberUpdatedState(dismissGestureEnabled) - val scope = rememberCoroutineScope() + val coroutineScope = rememberCoroutineScope() val transformableState = rememberTransformableState { zoomChange, panChange, _ -> - if (state.dismissDragAbsoluteOffsetY == 0f) { - scope.launch { - state.onZoomChange(zoomChange) - state.onDrag(panChange) - } - } - } - LaunchedEffect(transformableState.isTransformInProgress) { - if (!transformableState.isTransformInProgress && state.scale < ZoomableDefaults.DefaultScale) { - scope.launch { - val zoomFactor = ZoomableDefaults.DefaultScale / state.scale - transformableState.animateZoomBy(zoomFactor) - } + coroutineScope.launch { + zoomableState.onDrag(panChange) + zoomableState.onZoomChange(zoomChange) } } val gesturesModifier = if (!enabled) Modifier else Modifier - .pointerInput(state) { - detectTapAndDragGestures( - state = state, - dismissGestureEnabled = dismissGestureEnabledState, - onDismiss = onDismiss - ) + .pointerInput(zoomableState) { + detectTapAndDragGestures(zoomableState) } .transformable(state = transformableState) @@ -87,20 +64,19 @@ internal fun Zoomable( val height = constraints.maxHeight val placeable = measurable.measure( Constraints( - maxWidth = (width * state.scale).roundToInt(), - maxHeight = (height * state.scale).roundToInt() + maxWidth = (width * zoomableState.scale).roundToInt(), + maxHeight = (height * zoomableState.scale).roundToInt() ) ) - state.size = IntSize(width, height) - state.childSize = Size( - placeable.width / state.scale, - placeable.height / state.scale + zoomableState.size = IntSize(width, height) + zoomableState.childSize = Size( + placeable.width / zoomableState.scale, + placeable.height / zoomableState.scale ) layout(width, height) { placeable.placeWithLayer( - state.translationX.roundToInt() - state.boundOffset.x, - state.translationY.roundToInt() - state.boundOffset.y - + state.dismissDragOffsetY.roundToInt() + zoomableState.translationX.roundToInt() - zoomableState.boundOffset.x, + zoomableState.translationY.roundToInt() - zoomableState.boundOffset.y ) } } @@ -110,9 +86,7 @@ internal fun Zoomable( } internal suspend fun PointerInputScope.detectTapAndDragGestures( - state: ZoomableState, - dismissGestureEnabled: State, - onDismiss: () -> Boolean + state: ZoomableState ) = coroutineScope { launch { detectTapGestures( @@ -136,7 +110,6 @@ internal suspend fun PointerInputScope.detectTapAndDragGestures( launch { detectDragGestures( state = state, - dismissGestureEnabled = dismissGestureEnabled, startDragImmediately = { state.isDragInProgress }, onDragStart = { state.onDragStart() @@ -148,27 +121,17 @@ internal suspend fun PointerInputScope.detectTapAndDragGestures( state.onDrag(dragAmount) state.addPosition(change.uptimeMillis, change.position) } - } else { - state.onDismissDrag(dragAmount.y) } }, onDragCancel = { if (state.isZooming) { state.resetTracking() - } else { - launch { - state.onDismissDragEnd() - } } }, onDragEnd = { launch { if (state.isZooming) { state.onDragEnd() - } else { - if (!(state.shouldDismiss && onDismiss())) { - state.onDismissDragEnd() - } } } } @@ -178,7 +141,6 @@ internal suspend fun PointerInputScope.detectTapAndDragGestures( private suspend fun PointerInputScope.detectDragGestures( state: ZoomableState, - dismissGestureEnabled: State, startDragImmediately: () -> Boolean, onDragStart: (PointerInputChange) -> Unit = {}, onDragEnd: () -> Unit = {}, @@ -188,7 +150,7 @@ private suspend fun PointerInputScope.detectDragGestures( awaitEachGesture { // We have to always call this, or we'll get a crash if we do nothing. val down = awaitFirstDown(requireUnconsumed = false) - if (state.isZooming || dismissGestureEnabled.value) { + if (state.isZooming) { var overSlop = Offset.Zero val drag = if (state.isZooming) { if (startDragImmediately()) down else { diff --git a/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/zoomable/ZoomableState.kt b/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/zoomable/ZoomableState.kt index e029914..c6db1ef 100644 --- a/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/zoomable/ZoomableState.kt +++ b/affogato-pdf-viewer/src/main/java/com/parsuomash/affogato/pdfviewer/zoomable/ZoomableState.kt @@ -23,10 +23,7 @@ import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.Velocity -import kotlin.math.PI -import kotlin.math.abs import kotlin.math.roundToInt -import kotlin.math.sin import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -42,7 +39,7 @@ import kotlinx.coroutines.launch * @param initialTranslationY The initial value for [ZoomableState.translationY]. */ @Composable -internal fun rememberZoomableState( +fun rememberZoomableState( @FloatRange(from = 0.0) minScale: Float = ZoomableDefaults.MinScale, @FloatRange(from = 0.0) maxScale: Float = ZoomableDefaults.MaxScale, @FloatRange(from = 0.0) doubleTapScale: Float = ZoomableDefaults.DoubleTapScale, @@ -68,7 +65,7 @@ internal fun rememberZoomableState( * @see rememberZoomableState */ @Stable -internal class ZoomableState( +class ZoomableState( @FloatRange(from = 0.0) initialScale: Float = ZoomableDefaults.MinScale, @FloatRange(from = 0.0) initialTranslationX: Float = 0f, @FloatRange(from = 0.0) initialTranslationY: Float = 0f @@ -112,21 +109,6 @@ internal class ZoomableState( internal var boundOffset by mutableStateOf(IntOffset.Zero) private set - internal var dismissDragAbsoluteOffsetY by mutableFloatStateOf(0f) - private set - - internal val dismissDragOffsetY: Float - get() { - val maxOffset = childSize.height - return if (maxOffset == 0f) 0f else { - val progress = (dismissDragAbsoluteOffsetY / maxOffset).coerceIn(-1f, 1f) - maxOffset / DismissDragResistanceFactor * sin(progress * PI.toFloat() / 2) - } - } - - internal val shouldDismiss: Boolean - get() = abs(dismissDragAbsoluteOffsetY) > size.height * DismissDragThreshold - internal var size = IntSize.Zero set(value) { if (field != value) { @@ -289,19 +271,6 @@ internal class ZoomableState( velocityTracker.resetTracking() } - internal fun onDismissDrag(dragAmountY: Float) { - dismissDragAbsoluteOffsetY += dragAmountY - } - - internal suspend fun onDismissDragEnd() { - animate( - initialValue = dismissDragAbsoluteOffsetY, - targetValue = 0f - ) { value, _ -> - dismissDragAbsoluteOffsetY = value - } - } - override fun toString(): String = "ZoomableState(translateX=%.1f,translateY=%.1f,scale=%.2f)".format( translationX, translationY, scale @@ -342,15 +311,15 @@ internal object ZoomableDefaults { /** * The default value for [ZoomableState.minScale]. */ - const val MinScale = 1 / 4f + const val MinScale = 1 / 5f /** * The default value for [ZoomableState.maxScale]. */ - const val MaxScale = 4f + const val MaxScale = 5f /** * The default value for [ZoomableState.doubleTapScale]. */ - const val DoubleTapScale = 2f + const val DoubleTapScale = 2.5f } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b69c684..000ec04 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] accompanist = "0.32.0" -affogato = "1.8.2" +affogato = "1.8.3" agp = "8.1.1" case-format = "0.2.0" compose = "1.5.1" diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 6829e45..7f62bb0 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -34,6 +34,7 @@ android { getByName("release") { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + signingConfig = signingConfigs.getByName("debug") } } compileOptions {