Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
Expand Down Expand Up @@ -489,6 +491,22 @@ class ReviewerFragment :
}

private fun setupWhiteboard() {
childFragmentManager.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(
fm: FragmentManager,
f: Fragment,
v: View,
savedInstanceState: Bundle?,
) {
if (f !is WhiteboardFragment) return
f.setOnScrollByListener { y ->
webViewLayout.scrollVerticallyBy(y)
}
}
},
false,
)
viewModel.whiteboardEnabledFlow.flowWithLifecycle(lifecycle).collectIn(lifecycleScope) { isEnabled ->
binding.whiteboardContainer.isVisible = isEnabled
val whiteboardFragment = childFragmentManager.findFragmentById(binding.whiteboardContainer.id)
Expand All @@ -499,7 +517,7 @@ class ReviewerFragment :
}
}
viewModel.onCardUpdatedFlow.collectIn(lifecycleScope) {
val whiteboardFragment = childFragmentManager.findFragmentByTag(WhiteboardFragment::class.jvmName)
val whiteboardFragment = childFragmentManager.findFragmentById(binding.whiteboardContainer.id)
(whiteboardFragment as? WhiteboardFragment)?.resetCanvas()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,15 @@ class WhiteboardFragment :
return true
}

/**
* Sets a listener to when the whiteboard is scrolled vertically,
* which can happen by scrolling with two fingers, or with just one
* if the `Stylus mode` is enabled.
*/
fun setOnScrollByListener(listener: OnScrollByListener) {
binding.whiteboardView.setOnScrollByListener(listener)
}

fun resetCanvas() = viewModel.reset()

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import android.graphics.PorterDuffXfermode
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.core.graphics.createBitmap
import com.ichi2.anki.R
import kotlin.math.abs

/**
* A custom view for the whiteboard that handles drawing and touch events.
Expand All @@ -36,18 +38,14 @@ class WhiteboardView : View {
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context) : this(context, null)

// Callbacks for user actions
var onNewPath: ((Path) -> Unit)? = null
var onEraseGestureStart: (() -> Unit)? = null
var onEraseGestureMove: ((Float, Float) -> Unit)? = null
var onEraseGestureEnd: (() -> Unit)? = null

// Public properties for tool state
var isEraserActive: Boolean = false
var eraserMode: EraserMode = EraserMode.INK
var isStylusOnlyMode: Boolean = false

// Internal drawing state
private val currentPath = Path()
private val currentPaint =
Paint().apply {
Expand All @@ -67,6 +65,15 @@ class WhiteboardView : View {
private val canvasPaint = Paint(Paint.DITHER_FLAG)

private var hasMoved = false
private var isDrawing = false
private val multiTouchDetector =
MultiTouchDetector(
touchSlop = ViewConfiguration.get(context).scaledTouchSlop,
)

fun setOnScrollByListener(listener: OnScrollByListener) {
multiTouchDetector.setOnScrollByListener(listener)
}

/**
* Recreates the drawing buffer when the view size changes.
Expand Down Expand Up @@ -107,6 +114,14 @@ class WhiteboardView : View {
* Ignores finger input if stylus-only mode is enabled.
*/
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.pointerCount == 2) {
isDrawing = false
currentPath.reset()
invalidate()

return multiTouchDetector.onTouchEvent(event)
}

if (isStylusOnlyMode && event.getToolType(0) != MotionEvent.TOOL_TYPE_STYLUS) {
return false
}
Expand All @@ -117,6 +132,7 @@ class WhiteboardView : View {

when (event.action) {
MotionEvent.ACTION_DOWN -> {
isDrawing = true
hasMoved = false
currentPath.moveTo(touchX, touchY)
if (isPathEraser) {
Expand All @@ -126,6 +142,8 @@ class WhiteboardView : View {
invalidate()
}
MotionEvent.ACTION_MOVE -> {
if (!isDrawing) return false

hasMoved = true
currentPath.lineTo(touchX, touchY)
if (isPathEraser) {
Expand All @@ -134,6 +152,8 @@ class WhiteboardView : View {
invalidate()
}
MotionEvent.ACTION_UP -> {
if (!isDrawing) return false

if (isPathEraser) {
onEraseGestureEnd?.invoke()
} else {
Expand All @@ -146,6 +166,7 @@ class WhiteboardView : View {
}
// Reset the path for the next gesture
currentPath.reset()
isDrawing = false
invalidate()
}
else -> return false
Expand Down Expand Up @@ -203,3 +224,79 @@ class WhiteboardView : View {
invalidate()
}
}

fun interface OnScrollByListener {
/**
* @param y the amount of pixels to scroll vertically.
* @see [View.scrollBy]
*/
fun onVerticalScrollBy(y: Int)
}

/**
* Detects multi-finger touch and scroll gestures and triggers a callback with the vertical delta.
* TODO Improve detection when lifting a finger up then down again
*/
class MultiTouchDetector(
/** Distance in pixels a touch can wander before we think the user is scrolling */
private val touchSlop: Int,
) {
private var startX: Float = 0f
private var startY: Float = 0f
private var currentX: Float = 0f
private var currentY: Float = 0f
private var isWithinTapTolerance: Boolean = false
private var onScrollByListener: OnScrollByListener? = null

fun setOnScrollByListener(listener: OnScrollByListener) {
onScrollByListener = listener
}

/**
* Processes the motion event.
* @return True if the event was handled (consumed), False otherwise.
*/
fun onTouchEvent(event: MotionEvent): Boolean {
if (event.pointerCount != 2) return false

return when (event.actionMasked) {
MotionEvent.ACTION_POINTER_DOWN -> {
reinitialize(event)
true
}
MotionEvent.ACTION_MOVE -> tryScroll(event)
else -> false
}
}

private fun reinitialize(event: MotionEvent) {
isWithinTapTolerance = true
startX = (event.getX(0) + event.getX(1)) / 2f
startY = (event.getY(0) + event.getY(1)) / 2f
}

private fun updatePositions(event: MotionEvent): Boolean {
currentX = (event.getX(0) + event.getX(1)) / 2f
currentY = (event.getY(0) + event.getY(1)) / 2f

val dx = abs(startX - currentX)
val dy = abs(startY - currentY)
if (dx >= touchSlop || dy >= touchSlop) {
isWithinTapTolerance = false
}
return true
}

private fun tryScroll(event: MotionEvent): Boolean {
if (!updatePositions(event) || isWithinTapTolerance) {
return false
}
val dy = (startY - currentY).toInt()
if (dy != 0) {
onScrollByListener?.onVerticalScrollBy(dy)
startX = currentX
startY = currentY
}
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ open class SafeWebViewLayout :
@MainThread
fun destroy() = webView.destroy()

@MainThread
fun scrollVerticallyBy(y: Int) {
if (webView.canScrollVertically(y)) {
webView.scrollBy(0, y)
}
}

@MainThread
fun createPrintDocumentAdapter(documentName: String) = webView.createPrintDocumentAdapter(documentName)

Expand Down
Loading