blob: 024d3a1b2a81ed6ebc6dad5482ac19505d518265 [file]
/*
* Copyright 2025 The Android Open Source Project
*
* 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 androidx.pdf.ink
import android.content.Context
import android.graphics.Matrix
import android.graphics.Path
import android.graphics.PointF
import android.graphics.RectF
import android.os.Build
import android.os.Bundle
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import androidx.annotation.RequiresExtension
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.ink.authoring.InProgressStrokeId
import androidx.ink.authoring.InProgressStrokesView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.pdf.PdfSandboxHandle
import androidx.pdf.PdfWriteHandle
import androidx.pdf.SandboxedPdfLoader
import androidx.pdf.annotation.AnnotationsView
import androidx.pdf.annotation.AnnotationsView.PageAnnotationsData
import androidx.pdf.annotation.KeyedPdfAnnotation
import androidx.pdf.annotation.LocatedAnnotations
import androidx.pdf.annotation.OnAnnotationLocatedListener
import androidx.pdf.annotation.highlights.InProgressTextHighlightsListener
import androidx.pdf.annotation.highlights.models.InProgressHighlightId
import androidx.pdf.annotation.models.AnnotationsDisplayState
import androidx.pdf.annotation.models.PdfAnnotation
import androidx.pdf.annotation.models.VisiblePdfAnnotations
import androidx.pdf.exceptions.RequestFailedException
import androidx.pdf.featureflag.PdfFeatureFlags
import androidx.pdf.ink.model.ApplyEditsState
import androidx.pdf.ink.model.ApplyInProgressException
import androidx.pdf.ink.state.AnnotationDrawingMode
import androidx.pdf.ink.state.PdfEditMode
import androidx.pdf.ink.state.PdfEditMode.Companion.EDITING_JOURNEY_ANNOTATIONS
import androidx.pdf.ink.state.PdfEditMode.Companion.EDITING_JOURNEY_FORM_FILLING
import androidx.pdf.ink.util.PageTransformCalculator
import androidx.pdf.ink.util.toHighlighterConfig
import androidx.pdf.ink.util.toInkBrush
import androidx.pdf.ink.view.AnnotationToolbar
import androidx.pdf.ink.view.draganddrop.ToolbarCoordinator
import androidx.pdf.ink.view.tool.AnnotationToolInfo
import androidx.pdf.models.FormEditInfo
import androidx.pdf.view.PdfContentLayout
import androidx.pdf.view.PdfView
import androidx.pdf.viewer.fragment.PdfStylingOptions
import androidx.pdf.viewer.fragment.PdfViewerFragment
import java.util.Collections
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
/**
* A [androidx.fragment.app.Fragment] that extends [PdfViewerFragment] to provide PDF editing
* capabilities, including annotation and form filling, leveraging the 'androidx.ink' library.
*
* <p>This fragment coordinates the underlying PDF content with editing layers, enabling users to
* add ink strokes, create annotations, and modify form fields. It manages the interaction logic
* between viewing the document and performing edits.
*
* <p><b>Editing Workflow:</b>
* <ol>
* <li><b>Viewing:</b> Behaves exactly like [PdfViewerFragment].
* <li><b>Editing:</b> When [isEditModeEnabled] is set to `true`, user can leverage editing
* capabilities(such as annotating or filling forms).
* <li><b>Saving:</b> Edits are accumulated as "drafts". To persist changes, the host must call
* [applyDraftEdits], which asynchronously applies unsaved edits and creates a [PdfWriteHandle]
* used to write the modified document to a file.
* </ol>
*
* @see PdfViewerFragment
* @see applyDraftEdits
*/
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 18)
public open class EditablePdfViewerFragment : PdfViewerFragment {
public constructor() : super()
protected constructor(pdfStylingOptions: PdfStylingOptions) : super(pdfStylingOptions)
/**
* If `true`, the fragment is in edit mode, allowing for annotating or editing. If `false`, the
* fragment is in viewing mode.
*
* Note: The host is responsible for setting this to `false` after a write operation is
* complete.
*/
public var isEditModeEnabled: Boolean
get() {
return documentViewModel.pdfEditMode is PdfEditMode.Enabled
}
set(value) {
documentViewModel.pdfEditMode =
if (value) PdfEditMode.Enabled() else PdfEditMode.Disabled
}
/**
* Returns `true` if an `applyDraftEdits` operation is currently in progress.
*
* @see applyDraftEdits
*/
public val isApplyEditsInProgress: Boolean
get() = documentViewModel.applyEditsStatus.value is ApplyEditsState.InProgress
/**
* Returns `true` if there are any draft edits that have not yet been applied to the document,
* `false` otherwise.
*
* This can be used to prompt the user to save changes before navigating away, as draft edits
* will be lost if the fragment is removed from the stack or comes out of edit mode.
*/
@get:JvmName("hasUnsavedChanges")
public val hasUnsavedChanges: Boolean
get() = documentViewModel.hasUnsavedChanges()
/**
* Callback invoked when [EditablePdfViewerFragment] enters edit mode. This is triggered when
* the user begins an edit for example modifying a form field or interaction via toolbox.
*
* <p> This callback can be used by the developers to make any UI changes required when the user
* enters edit mode, e.g. showing the "Save" button to the user.
*/
public open fun onEnterEditMode() {}
/**
* Callback invoked when [EditablePdfViewerFragment] exits edit mode. This is triggered when the
* the edit mode is disabled and the fragment completes cleaning up it's edit state.
*
* <p> This callback can be used by the developers to make any UI changes required when the user
* exits edit mode e.g. hiding the "Save" button.
*
* @see isEditModeEnabled
*/
public open fun onExitEditMode() {}
/**
* Applies all draft edits to the document.
*
* This operation executes asynchronously. The operation will be terminated if
* [EditablePdfViewerFragment] is removed from the fragment manager while an [applyDraftEdits]
* is in progress. [EditablePdfViewerFragment] internally disallows editing capabilities during
* complete operation. Upon completion, either [onApplyEditsSuccess] or [onApplyEditsFailed]
* will be invoked with the result.
*
* @throws ApplyInProgressException if another apply operation is already in progress.
*/
public fun applyDraftEdits() {
if (isApplyEditsInProgress) {
throw ApplyInProgressException()
}
documentViewModel.applyDraftEdits()
}
/**
* Callback invoked when draft edits have been successfully applied to the document.
*
* The host should override this method to perform the write operation. The provided
* [PdfWriteHandle] allows writing the document changes to a [android.os.ParcelFileDescriptor].
* The handle **must** be closed after writing to ensure proper resource cleanup.
*
* After the write operation is complete, the host is responsible for exiting the edit mode by
* setting [isEditModeEnabled] to `false`.
*
* @param handle A [PdfWriteHandle] to be used for writing the changes to a file.
* @see applyDraftEdits
*/
public open fun onApplyEditsSuccess(handle: PdfWriteHandle) {}
/**
* Callback invoked when applying draft edits has failed.
*
* @param error The [Throwable] that caused the failure.
* @see applyDraftEdits
*/
public open fun onApplyEditsFailed(error: Throwable) {}
private lateinit var wetStrokesView: InProgressStrokesView
private lateinit var annotationView: AnnotationsView
private lateinit var onViewportChangedListener: PdfView.OnViewportChangedListener
private lateinit var gestureStateChangedListener: PdfView.OnGestureStateChangedListener
private lateinit var wetStrokesOnFinishedListener: WetStrokesOnFinishedListener
private lateinit var onFormWidgetInfoUpdatedListener: PdfView.OnFormWidgetInfoUpdatedListener
private lateinit var annotationsTouchEventDispatcher: AnnotationsTouchEventDispatcher
private lateinit var wetStrokesViewTouchHandler: WetStrokesViewTouchHandler
private lateinit var pdfContentLayoutTouchListener: PdfContentLayoutTouchListener
@VisibleForTesting internal lateinit var annotationToolbar: AnnotationToolbar
private lateinit var toolbarCoordinator: ToolbarCoordinator
private lateinit var pdfLoaderHandle: PdfSandboxHandle
private val toolbarLayoutChangeListener =
View.OnLayoutChangeListener {
_,
left,
top,
right,
bottom,
oldLeft,
oldTop,
oldRight,
oldBottom ->
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
wetStrokesView.maskPath = createToolbarMaskPath()
}
}
private lateinit var pageInfoProvider: PageInfoProviderImpl
private val annotationsViewDispatcher = AnnotationsViewTouchEventDispatcher()
private val inkViewDispatcher = InkViewTouchEventDispatcher()
private var pageTransformCalculator: PageTransformCalculator = PageTransformCalculator()
private val strokeIdToPageNumMap: MutableMap<InProgressStrokeId, Int> =
Collections.synchronizedMap(mutableMapOf<InProgressStrokeId, Int>())
private val inProgressTextHighlightsListener =
object : InProgressTextHighlightsListener {
override fun onTextHighlightStarted(
viewPoint: PointF,
inProgressHighlightId: InProgressHighlightId,
) {
annotationsTouchEventDispatcher.switchActiveDispatcher(
annotationsViewDispatcher,
viewPoint,
)
}
override fun onTextHighlightRejected(viewPoint: PointF) {
annotationsTouchEventDispatcher.switchActiveDispatcher(inkViewDispatcher, viewPoint)
}
override fun onTextHighlightFinished(
annotations: Map<InProgressHighlightId, PdfAnnotation>
) {
annotations.forEach { (_, annotation) ->
documentViewModel.addDraftAnnotation(annotation)
}
}
override fun onTextHighlightError(exception: RequestFailedException) {
// TODO(b/409464802): Propagate it through event callback
}
}
private val onAnnotationLocatedListener =
object : OnAnnotationLocatedListener {
override fun onAnnotationsLocated(locatedAnnotations: LocatedAnnotations) {
if (documentViewModel.drawingMode.value == AnnotationDrawingMode.EraserMode) {
val topAnnotation = locatedAnnotations.annotations.first()
documentViewModel.removeAnnotation(topAnnotation.key)
}
}
}
@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override val documentViewModel: EditableDocumentViewModel by viewModels {
EditableDocumentViewModel.Factory
}
override fun onAttach(context: Context) {
super.onAttach(context)
/**
* By starting initialization early, subsequent calls to load a document—whether through a
* new [documentUri] or a forced reload via[EditableDocumentViewModel.forceLoadDocument] can
* reuse the existing service connection, significantly reducing latency by avoiding
* repeated connect/disconnect cycles.
*/
pdfLoaderHandle = SandboxedPdfLoader.startInitialization(context)
}
override fun onDetach() {
super.onDetach()
pdfLoaderHandle.close()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val rootView =
super.onCreateView(inflater, container, savedInstanceState) as ConstraintLayout
wetStrokesView =
InProgressStrokesView(requireContext()).apply {
id = R.id.pdf_wet_strokes_view
layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
visibility = VISIBLE
}
annotationView =
AnnotationsView(requireContext()).apply {
id = R.id.pdf_annotation_view
layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
}
annotationToolbar =
inflater.inflate(R.layout.annotation_toolbar_layout, null, false) as AnnotationToolbar
toolbarCoordinator =
ToolbarCoordinator(requireContext()).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
val pdfContentLayout =
rootView.findViewById<PdfContentLayout>(
androidx.pdf.viewer.fragment.R.id.pdfContentLayout
)
pdfContentLayout.addView(annotationView)
pdfContentLayout.addView(wetStrokesView)
rootView.addView(toolbarCoordinator)
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pageInfoProvider = PageInfoProviderImpl()
viewLifecycleOwner.lifecycleScope.launch {
documentViewModel.applyEditsStatus.collect { status ->
when (status) {
is ApplyEditsState.Success -> {
onApplyEditsSuccess(status.handle)
documentViewModel.resetApplyEditsStatus()
}
is ApplyEditsState.Failure -> {
onApplyEditsFailed(status.error)
documentViewModel.resetApplyEditsStatus()
}
else -> {
/* No-Op */
}
}
}
}
setupUiStateCollectors()
setupTouchListeners()
setupPdfViewListeners()
setupAnnotationViewListeners()
setupAnnotationToolbar()
setupToolbarCoordinator(annotationToolbar)
}
private fun setupAnnotationViewListeners() {
annotationView.pageInfoProvider = pageInfoProvider
annotationView.addOnAnnotationLocatedListener(onAnnotationLocatedListener)
annotationView.addInProgressTextHighlightsListener(inProgressTextHighlightsListener)
}
private fun setupUiStateCollectors() {
collectFlowOnLifecycleScope {
documentViewModel.shouldShowAnnotationToolbar.collect {
updateAnnotationToolbarVisibility(it)
}
}
collectFlowOnLifecycleScope {
documentViewModel.pdfEditModeFlow.collect { editMode ->
if (editMode is PdfEditMode.Enabled) onEnterEditMode() else onExitEditMode()
updateUiForEditMode(editMode)
}
}
collectFlowOnLifecycleScope {
documentViewModel.annotationsDisplayStateFlow.collect { displayState ->
updateAnnotationsView(displayState)
}
}
collectFlowOnLifecycleScope {
documentViewModel.areAnnotationsVisibleFlow.collect { areVisible ->
annotationView.visibility = if (areVisible) VISIBLE else GONE
}
}
collectFlowOnLifecycleScope {
documentViewModel.isAnnotationInteractionEnabled.collect { isEnabled ->
pdfContentLayoutTouchListener.isAnnotationInteractionEnabled = isEnabled
}
}
}
private fun setupToolbarCoordinator(toolbar: AnnotationToolbar) {
toolbarCoordinator.apply { attachToolbar(toolbar) }
}
override fun onDestroyView() {
// Clean up the listener to avoid potential memory leaks
super.onDestroyView()
pdfView.removeOnViewportChangedListener(onViewportChangedListener)
pdfView.removeOnGestureStateChangedListener(gestureStateChangedListener)
pdfView.setOnBitmapUpdatedListener(null)
annotationView.removeInProgressTextHighlightsListener(inProgressTextHighlightsListener)
pdfView.removeOnFormWidgetInfoUpdatedListener(onFormWidgetInfoUpdatedListener)
wetStrokesView.removeFinishedStrokesListener(wetStrokesOnFinishedListener)
annotationToolbar.setAnnotationToolbarListener(null)
pdfContainer.setOnTouchListener(null)
if (::annotationToolbar.isInitialized) {
annotationToolbar.removeOnLayoutChangeListener(toolbarLayoutChangeListener)
}
}
private fun updateUiForEditMode(editMode: PdfEditMode) {
toolboxView.visibility = if (editMode is PdfEditMode.Enabled) GONE else VISIBLE
when (editMode) {
is PdfEditMode.Enabled -> {
if (editMode.journey == EDITING_JOURNEY_ANNOTATIONS) {
documentViewModel.initialFormFillingEnabledState = pdfView.isFormFillingEnabled
// Disable form filling when in annotations mode
pdfView.isFormFillingEnabled = false
updateUiForAnnotationsEditMode(true)
}
}
is PdfEditMode.Disabled -> {
// Restore form filling state when exiting edit mode
documentViewModel.initialFormFillingEnabledState?.let {
pdfView.isFormFillingEnabled = it
documentViewModel.initialFormFillingEnabledState = null
}
updateUiForAnnotationsEditMode(false)
}
}
}
private fun updateAnnotationToolbarVisibility(isAnnotationToolbarVisible: Boolean) {
toolbarCoordinator.isVisible = isAnnotationToolbarVisible
annotationToolbar.isVisible = isAnnotationToolbarVisible
}
private fun updateUiForAnnotationsEditMode(isEnabled: Boolean) {
PdfFeatureFlags.isMultiTouchScrollEnabled = isEnabled
if (isEnabled) {
pdfView.clearCurrentSelection()
// Wait for the toolbar to be laid out, as we need to utilize its width and height
annotationToolbar.post { wetStrokesView.maskPath = createToolbarMaskPath() }
} else {
annotationToolbar.apply {
reset()
wetStrokesView.maskPath = null
}
toolbarCoordinator.updateLayout()
}
}
private fun setupTouchListeners() {
toolboxView.setOnEditClickListener { isEditModeEnabled = true }
wetStrokesOnFinishedListener =
WetStrokesOnFinishedListener(
wetStrokesView = wetStrokesView,
strokeIdToPageNumMap = strokeIdToPageNumMap,
annotationsViewModel = documentViewModel,
)
wetStrokesView.apply {
addFinishedStrokesListener(wetStrokesOnFinishedListener)
wetStrokesViewTouchHandler =
WetStrokesViewTouchHandler(pageInfoProvider::getPageInfoFromViewCoordinates) {
strokeId,
pageNum ->
strokeIdToPageNumMap[strokeId] = pageNum
}
setOnTouchListener(wetStrokesViewTouchHandler)
}
annotationsTouchEventDispatcher =
AnnotationsTouchEventDispatcher(annotationsViewDispatcher, inkViewDispatcher)
val popupDismissalTouchListener = PopupDismissalTouchListener(annotationToolbar)
pdfContentLayoutTouchListener =
PdfContentLayoutTouchListener(
requireContext(),
annotationsTouchEventDispatcher,
PdfViewTouchEventDispatcher(),
)
// The order of touch listeners is important, as touch events will be delegated
// sequentially.
val pdfCompositeTouchListener =
PdfCompositeTouchListener(popupDismissalTouchListener, pdfContentLayoutTouchListener)
pdfContainer.setOnTouchListener(pdfCompositeTouchListener)
pdfContainer.isAnnotationInteractionEnabled = true
}
private fun updateAnnotationsView(displayState: AnnotationsDisplayState) {
val pageRenderDataArray = SparseArray<PageAnnotationsData>()
val firstVisiblePage = pdfView.firstVisiblePage
val lastVisiblePage = firstVisiblePage + pdfView.visiblePagesCount - 1
val visiblePageAnnotations = displayState.visiblePageAnnotations
val transformationMatrices = displayState.transformationMatrices
(firstVisiblePage..lastVisiblePage).forEach { pageNum ->
val pageAnnotationData =
createPageAnnotationsData(pageNum, visiblePageAnnotations, transformationMatrices)
pageRenderDataArray.put(pageNum, pageAnnotationData)
}
annotationView.annotations = pageRenderDataArray
}
private fun createPageAnnotationsData(
pageNum: Int,
visiblePageAnnotations: VisiblePdfAnnotations,
transformationMatrices: Map<Int, Matrix>,
): PageAnnotationsData {
val annotationsForPage: List<KeyedPdfAnnotation> =
visiblePageAnnotations.getKeyedAnnotationsForPage(pageNum)
val transformMatrix = transformationMatrices[pageNum]
if (transformMatrix == null) {
return PageAnnotationsData(emptyList(), Matrix())
}
return PageAnnotationsData(annotationsForPage, transformMatrix)
}
private fun setupPdfViewListeners() {
gestureStateChangedListener =
object : PdfView.OnGestureStateChangedListener {
override fun onGestureStateChanged(newState: Int) {
if (newState == PdfView.GESTURE_STATE_IDLE) {
documentViewModel.isPdfViewGestureActive = false
} else {
documentViewModel.isPdfViewGestureActive = true
}
}
}
onViewportChangedListener =
object : PdfView.OnViewportChangedListener {
override fun onViewportChanged(
firstVisiblePage: Int,
visiblePagesCount: Int,
pageLocations: SparseArray<RectF>,
zoomLevel: Float,
) {
updateAnnotationDisplayState(
firstVisiblePage,
visiblePagesCount,
pageLocations,
zoomLevel,
)
pageInfoProvider.zoom = zoomLevel
pageInfoProvider.pageLocations = pageLocations
}
}
onFormWidgetInfoUpdatedListener =
object : PdfView.OnFormWidgetInfoUpdatedListener {
override fun onFormWidgetInfoUpdated(formEditInfo: FormEditInfo) {
if (!isEditModeEnabled) {
documentViewModel.pdfEditMode =
PdfEditMode.Enabled(EDITING_JOURNEY_FORM_FILLING)
}
}
}
pdfView.addOnGestureStateChangedListener(gestureStateChangedListener)
pdfView.addOnViewportChangedListener(onViewportChangedListener)
pdfView.addOnFormWidgetInfoUpdatedListener(onFormWidgetInfoUpdatedListener)
pdfView.setOnBitmapUpdatedListener(
object : PdfView.OnBitmapUpdatedListener {
override fun onBitmapFetched(pageNum: Int) {
documentViewModel.onBitmapFetched(pageNum)
}
override fun onBitmapCleared(pageNum: Int) {
documentViewModel.onBitmapCleared(pageNum)
}
}
)
}
private fun updateAnnotationDisplayState(
firstVisiblePage: Int,
visiblePagesCount: Int,
pageLocations: SparseArray<RectF>,
zoomLevel: Float,
) {
val lastVisiblePage = firstVisiblePage + visiblePagesCount - 1
updateTransformationMatrices(firstVisiblePage, visiblePagesCount, pageLocations, zoomLevel)
documentViewModel.fetchAnnotationsForPageRange(
startPage = firstVisiblePage,
endPage = lastVisiblePage,
)
}
private fun generatePageRangeTransformationMatrices(
firstVisiblePage: Int,
visiblePagesCount: Int,
pageLocations: SparseArray<RectF>,
zoomLevel: Float,
): Map<Int, Matrix> {
val lastVisiblePage = firstVisiblePage + visiblePagesCount - 1
documentViewModel.visiblePageRange = firstVisiblePage..lastVisiblePage
return pageTransformCalculator.calculate(
firstVisiblePage,
visiblePagesCount,
pageLocations,
zoomLevel,
)
}
private fun updateTransformationMatrices(
firstVisiblePage: Int,
visiblePagesCount: Int,
pageLocations: SparseArray<RectF>,
zoomLevel: Float,
) {
val transformationMatrices =
generatePageRangeTransformationMatrices(
firstVisiblePage,
visiblePagesCount,
pageLocations,
zoomLevel,
)
documentViewModel.updateTransformationMatrices(transformationMatrices)
}
private fun setupAnnotationToolbar() {
annotationToolbar.addOnLayoutChangeListener(toolbarLayoutChangeListener)
annotationToolbar.setAnnotationToolbarListener(
object : AnnotationToolbar.AnnotationToolbarListener {
override fun onToolChanged(toolInfo: AnnotationToolInfo) {
documentViewModel.setCurrentToolInfo(toolInfo)
}
override fun onUndo() {
documentViewModel.undo()
}
override fun onRedo() {
documentViewModel.redo()
}
override fun onAnnotationVisibilityChanged(isVisible: Boolean) {
documentViewModel.areAnnotationsVisible = isVisible
}
}
)
collectFlowOnLifecycleScope {
documentViewModel.canUndo.collect { annotationToolbar.canUndo = it }
}
collectFlowOnLifecycleScope {
documentViewModel.canRedo.collect { annotationToolbar.canRedo = it }
}
collectFlowOnLifecycleScope {
documentViewModel.drawingMode.collect { updateDrawingMode(it) }
}
}
private fun updateDrawingMode(drawingMode: AnnotationDrawingMode) {
annotationsTouchEventDispatcher.drawingMode = drawingMode
when (drawingMode) {
// TODO(b/448242937): Revisit touch interception logic;
// Based on drawingMode, enable/disable touch interception
is AnnotationDrawingMode.PenMode -> {
wetStrokesView.setOnTouchListener(wetStrokesViewTouchHandler)
wetStrokesViewTouchHandler.brushForInking = drawingMode.toInkBrush()
annotationView.interactionMode = null
}
is AnnotationDrawingMode.HighlighterMode -> {
wetStrokesView.setOnTouchListener(wetStrokesViewTouchHandler)
wetStrokesViewTouchHandler.brushForInking = drawingMode.toInkBrush()
annotationView.interactionMode =
AnnotationsView.AnnotationMode.Highlight(drawingMode.toHighlighterConfig())
}
is AnnotationDrawingMode.EraserMode -> {
annotationView.interactionMode = AnnotationsView.AnnotationMode.Select()
}
}
}
private fun collectFlowOnLifecycleScope(block: suspend () -> Unit): Job {
return viewLifecycleOwner.lifecycleScope.launch {
/**
* [repeatOnLifecycle] launches the block in a new coroutine every time the lifecycle is
* in the STARTED state (or above) and cancels it when it's STOPPED.
*/
repeatOnLifecycle(Lifecycle.State.STARTED) { block() }
}
}
/**
* Creates a [android.graphics.Path] that encapsulate [AnnotationToolbar] and set it as a
* [InProgressStrokesView.maskPath] where no ink should be visible.
*
* @return [Path] surrounding [AnnotationToolbar].
*/
private fun createToolbarMaskPath(): Path {
val toolbarLocation = IntArray(2)
annotationToolbar.getLocationOnScreen(toolbarLocation)
val wetStrokesLocation = IntArray(2)
wetStrokesView.getLocationOnScreen(wetStrokesLocation)
val left = (toolbarLocation[0] - wetStrokesLocation[0]).toFloat()
val top = (toolbarLocation[1] - wetStrokesLocation[1]).toFloat()
val right = left + annotationToolbar.width
val bottom = top + annotationToolbar.height
val cornerRadiusPx = resources.getDimension(R.dimen.annotation_toolbar_corner_radius)
return Path().apply {
addRoundRect(
left,
top,
right,
bottom,
cornerRadiusPx,
cornerRadiusPx,
Path.Direction.CW,
)
}
}
internal inner class AnnotationsViewTouchEventDispatcher : TouchEventDispatcher {
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
return annotationView.dispatchTouchEvent(event)
}
}
internal inner class InkViewTouchEventDispatcher : TouchEventDispatcher {
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
return wetStrokesView.dispatchTouchEvent(event)
}
}
internal inner class PdfViewTouchEventDispatcher : TouchEventDispatcher {
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
return pdfView.dispatchTouchEvent(event)
}
}
private companion object {
private const val TOUCH_TOLERANCE_IN_DP = 2f
}
}