diff --git a/photoeditor/src/androidTest/java/ja/burhanrashid52/photoeditor/DrawingViewTouchEventTest.kt b/photoeditor/src/androidTest/java/ja/burhanrashid52/photoeditor/DrawingViewTouchEventTest.kt index 3724f8ab..55e5e208 100644 --- a/photoeditor/src/androidTest/java/ja/burhanrashid52/photoeditor/DrawingViewTouchEventTest.kt +++ b/photoeditor/src/androidTest/java/ja/burhanrashid52/photoeditor/DrawingViewTouchEventTest.kt @@ -1,8 +1,14 @@ package ja.burhanrashid52.photoeditor +import android.graphics.Paint import android.view.MotionEvent import androidx.test.ext.junit.runners.AndroidJUnit4 +import ja.burhanrashid52.photoeditor.shape.OvalShape +import ja.burhanrashid52.photoeditor.shape.RectangleShape +import ja.burhanrashid52.photoeditor.shape.ShapeBuilder +import ja.burhanrashid52.photoeditor.shape.ShapeType import junit.framework.TestCase.assertFalse +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith @@ -73,6 +79,64 @@ internal class DrawingViewTouchEventTest : BaseDrawingViewTest() { assertTrue(redoPaths.empty()) } + @Test + fun testTapWithOvalDrawsUniformFilledCircle() { + val drawingView = setupDrawingView() + drawingView.currentShapeBuilder = ShapeBuilder().withShapeType(ShapeType.Oval) + + tapView(drawingView, 150.0f, 100.0f) + + val top = drawingView.drawingPath.first + assertFalse(top.empty()) + assertTrue(top.peek()?.shape is OvalShape) + assertEquals(Paint.Style.FILL, top.peek()?.paint?.style) + } + + @Test + fun testTapWithRectangleDrawsUniformFilledSquare() { + val drawingView = setupDrawingView() + drawingView.currentShapeBuilder = ShapeBuilder().withShapeType(ShapeType.Rectangle) + + tapView(drawingView, 150.0f, 100.0f) + + val top = drawingView.drawingPath.first + assertFalse(top.empty()) + assertTrue(top.peek()?.shape is RectangleShape) + assertEquals(Paint.Style.FILL, top.peek()?.paint?.style) + } + + @Test + fun testTapWithPointlessShapeDrawsNothing() { + for (shapeType in listOf(ShapeType.Line, ShapeType.Brush, ShapeType.Arrow())) { + val drawingView = setupDrawingView() + drawingView.currentShapeBuilder = ShapeBuilder().withShapeType(shapeType) + + tapView(drawingView, 150.0f, 100.0f) + + assertTrue("tap with $shapeType should draw nothing", drawingView.drawingPath.first.empty()) + } + } + + @Test + fun testTapWhileErasingDrawsNothing() { + val drawingView = setupDrawingView() + drawingView.brushEraser() + + tapView(drawingView, 150.0f, 100.0f) + + val drawnShapes = drawingView.drawingPath.first + assertTrue(drawnShapes.empty()) + } + + private fun tapView(drawingView: DrawingView, x: Float, y: Float) { + drawingView.dispatchTouchEvent( + MotionEvent.obtain(200, 300, MotionEvent.ACTION_DOWN, x, y, 0) + ) + drawingView.dispatchTouchEvent( + MotionEvent.obtain(200, 300, MotionEvent.ACTION_UP, x, y, 0) + ) + } + @Test fun testPathDrawnOnTouchEvents() { val brushViewChangeListener = Mockito.mock(BrushViewChangeListener::class.java) diff --git a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/DrawingView.kt b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/DrawingView.kt index 80feab2d..b7b3bdde 100644 --- a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/DrawingView.kt +++ b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/DrawingView.kt @@ -12,6 +12,7 @@ import android.view.MotionEvent import android.view.View import ja.burhanrashid52.photoeditor.shape.* import java.util.* +import kotlin.math.abs /** * @@ -41,6 +42,11 @@ class DrawingView @JvmOverloads constructor( private var isErasing = false var eraserSize = DEFAULT_ERASER_SIZE + // tap detection: a touch that never moves beyond TAP_TOLERANCE is a tap, not a drag + private var touchDownX = 0f + private var touchDownY = 0f + private var hasMovedBeyondTap = false + // endregion private fun createPaint(): Paint { val paint = Paint() @@ -110,11 +116,17 @@ class DrawingView @JvmOverloads constructor( } private fun onTouchEventDown(touchX: Float, touchY: Float) { + touchDownX = touchX + touchDownY = touchY + hasMovedBeyondTap = false createShape() currentShape?.shape?.startShape(touchX, touchY) } private fun onTouchEventMove(touchX: Float, touchY: Float) { + if (abs(touchX - touchDownX) >= TAP_TOLERANCE || abs(touchY - touchDownY) >= TAP_TOLERANCE) { + hasMovedBeyondTap = true + } currentShape?.shape?.moveShape(touchX, touchY) } @@ -157,10 +169,8 @@ class DrawingView @JvmOverloads constructor( } private fun endShape(touchX: Float, touchY: Float) { - if (currentShape?.shape?.hasBeenTapped() == true) { - // just a tap, this is not a shape, so remove it - drawShapes.remove(currentShape) - //handleTap(touchX, touchY); + if (!hasMovedBeyondTap) { + handleTap(touchX, touchY) } viewChangeListener?.apply { onStopDrawing() @@ -171,6 +181,26 @@ class DrawingView @JvmOverloads constructor( } } + /** + * A single tap (no drag) drops a uniform filled version of the selected shape, sized from the + * shape thickness: a circle for an oval, a square for a rectangle. Shapes that can't be derived + * from a single point (line, arrow, freehand) and taps while erasing draw nothing, so the + * half-formed shape from the touch-down is discarded. + */ + private fun handleTap(touchX: Float, touchY: Float) { + drawShapes.remove(currentShape) + currentShape = null + if (isErasing) return + val size = currentShapeBuilder.shapeSize + val shape: AbstractShape = when (currentShapeBuilder.shapeType) { + ShapeType.Oval -> OvalShape().apply { drawCircle(touchX, touchY, size) } + ShapeType.Rectangle -> RectangleShape().apply { drawSquare(touchX, touchY, size) } + else -> return + } + currentShape = ShapeAndPaint(shape, createPaint().apply { style = Paint.Style.FILL }) + drawShapes.push(currentShape) + } + fun undo(): Boolean { if (!drawShapes.empty()) { redoShapes.push(drawShapes.pop()) @@ -212,6 +242,7 @@ class DrawingView @JvmOverloads constructor( companion object { const val DEFAULT_ERASER_SIZE = 50.0f + private const val TAP_TOLERANCE = 4f } // region constructors diff --git a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/shape/OvalShape.kt b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/shape/OvalShape.kt index b7b8e63d..61865e47 100644 --- a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/shape/OvalShape.kt +++ b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/shape/OvalShape.kt @@ -36,6 +36,15 @@ class OvalShape : AbstractShape("OvalShape") { return path } + /** Build a uniform circle of [radius] centered at ([cx], [cy]), used when the shape is tapped. */ + fun drawCircle(cx: Float, cy: Float, radius: Float) { + left = cx - radius + top = cy - radius + right = cx + radius + bottom = cy + radius + path = createOvalPath() + } + override fun stopShape() { Log.d(tag, "stopShape") } diff --git a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/shape/RectangleShape.kt b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/shape/RectangleShape.kt index 5a4e0684..30c154fe 100644 --- a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/shape/RectangleShape.kt +++ b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/shape/RectangleShape.kt @@ -36,6 +36,15 @@ class RectangleShape : AbstractShape("RectangleShape") { return path } + /** Build a uniform square of half-side [halfSide] centered at ([cx], [cy]), used when tapped. */ + fun drawSquare(cx: Float, cy: Float, halfSide: Float) { + left = cx - halfSide + top = cy - halfSide + right = cx + halfSide + bottom = cy + halfSide + path = createRectanglePath() + } + override fun stopShape() { Log.d(tag, "stopShape") }