Skip to content

Commit 94fcdae

Browse files
author
Julian Raphael Jautz
committed
PAINTROID_684_apply_outline_to_text
TextTool feature: text with outline -added new button for outline -outline created by painting twice (second time in stroke mode)
1 parent 4dbda82 commit 94fcdae

File tree

12 files changed

+472
-83
lines changed

12 files changed

+472
-83
lines changed

Paintroid/src/androidTest/java/org/catrobat/paintroid/test/espresso/tools/TextToolIntegrationTest.kt

+147-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import android.graphics.Paint
2727
import android.graphics.PointF
2828
import android.graphics.Typeface
2929
import android.widget.EditText
30+
import android.widget.RelativeLayout
3031
import androidx.core.content.res.ResourcesCompat
32+
import androidx.core.view.isVisible
3133
import androidx.recyclerview.widget.RecyclerView
3234
import androidx.test.espresso.Espresso
3335
import androidx.test.espresso.Espresso.onView
@@ -61,6 +63,7 @@ import org.catrobat.paintroid.tools.FontType
6163
import org.catrobat.paintroid.tools.ToolReference
6264
import org.catrobat.paintroid.tools.ToolType
6365
import org.catrobat.paintroid.tools.implementation.BOX_OFFSET
66+
import org.catrobat.paintroid.tools.implementation.DEFAULT_TEXT_OUTLINE_WIDTH
6467
import org.catrobat.paintroid.tools.implementation.MARGIN_TOP
6568
import org.catrobat.paintroid.tools.implementation.TEXT_SIZE_MAGNIFICATION_FACTOR
6669
import org.catrobat.paintroid.tools.implementation.TextTool
@@ -90,6 +93,8 @@ class TextToolIntegrationTest {
9093
private var underlinedToggleButton: MaterialButton? = null
9194
private var italicToggleButton: MaterialButton? = null
9295
private var boldToggleButton: MaterialButton? = null
96+
private var outlineToggleButton: MaterialButton? = null
97+
private var outlineWidthLayout: RelativeLayout? = null
9398
private var textSize: EditText? = null
9499
private var layerModel: LayerContracts.Model? = null
95100
private lateinit var activity: MainActivity
@@ -116,6 +121,8 @@ class TextToolIntegrationTest {
116121
activity.findViewById(R.id.pocketpaint_text_tool_dialog_toggle_underlined)
117122
italicToggleButton = activity.findViewById(R.id.pocketpaint_text_tool_dialog_toggle_italic)
118123
boldToggleButton = activity.findViewById(R.id.pocketpaint_text_tool_dialog_toggle_bold)
124+
outlineToggleButton = activity.findViewById(R.id.pocketpaint_text_tool_dialog_toggle_outline)
125+
outlineWidthLayout = activity.findViewById(R.id.pocketpaint_outline_width_layout)
119126
textSize = activity.findViewById(R.id.pocketpaint_font_size_text)
120127
textTool?.resetBoxPosition()
121128
}
@@ -125,6 +132,7 @@ class TextToolIntegrationTest {
125132
selectFormatting(FormattingOptions.ITALIC)
126133
selectFormatting(FormattingOptions.BOLD)
127134
selectFormatting(FormattingOptions.UNDERLINE)
135+
selectFormatting(FormattingOptions.OUTLINE)
128136
enterTestText()
129137
onView(withId(R.id.pocketpaint_text_tool_dialog_input_text)).perform(click())
130138
onView(withId(R.id.pocketpaint_text_tool_dialog_input_text)).perform(
@@ -135,12 +143,14 @@ class TextToolIntegrationTest {
135143
italicToggleButton?.let { Assert.assertTrue(it.isChecked) }
136144
boldToggleButton?.let { Assert.assertTrue(it.isChecked) }
137145
underlinedToggleButton?.let { Assert.assertTrue(it.isChecked) }
146+
outlineToggleButton?.let { Assert.assertTrue(it.isChecked) }
138147
Assert.assertEquals(TEST_TEXT_ADVANCED, textEditText?.text?.toString())
139148
onView(withId(R.id.pocketpaint_text_tool_dialog_input_text)).check(matches(isDisplayed()))
140149
onView(withId(R.id.pocketpaint_text_tool_dialog_list_font)).check(matches(isDisplayed()))
141150
onView(withId(R.id.pocketpaint_text_tool_dialog_toggle_underlined)).check(matches(isDisplayed()))
142151
onView(withId(R.id.pocketpaint_text_tool_dialog_toggle_italic)).check(matches(isDisplayed()))
143152
onView(withId(R.id.pocketpaint_text_tool_dialog_toggle_bold)).check(matches(isDisplayed()))
153+
onView(withId(R.id.pocketpaint_text_tool_dialog_toggle_outline)).check(matches(isDisplayed()))
144154
onView(withId(R.id.pocketpaint_font_size_text)).check(matches(isDisplayed()))
145155
}
146156

@@ -149,13 +159,15 @@ class TextToolIntegrationTest {
149159
selectFormatting(FormattingOptions.ITALIC)
150160
selectFormatting(FormattingOptions.BOLD)
151161
selectFormatting(FormattingOptions.UNDERLINE)
162+
selectFormatting(FormattingOptions.OUTLINE)
152163
enterTestText()
153164
onDrawingSurfaceView()
154165
.perform(UiInteractions.touchAt(DrawingSurfaceLocationProvider.MIDDLE))
155166

156167
italicToggleButton?.let { Assert.assertTrue(it.isChecked) }
157168
boldToggleButton?.let { Assert.assertTrue(it.isChecked) }
158169
underlinedToggleButton?.let { Assert.assertTrue(it.isChecked) }
170+
outlineToggleButton?.let { Assert.assertTrue(it.isChecked) }
159171
Assert.assertEquals(TEST_TEXT, textEditText?.text?.toString())
160172
onView(withId(R.id.pocketpaint_text_tool_dialog_input_text))
161173
.check(matches(not(isDisplayed())))
@@ -167,6 +179,8 @@ class TextToolIntegrationTest {
167179
.check(matches(not(isDisplayed())))
168180
onView(withId(R.id.pocketpaint_text_tool_dialog_toggle_bold))
169181
.check(matches(not(isDisplayed())))
182+
onView(withId(R.id.pocketpaint_text_tool_dialog_toggle_outline))
183+
.check(matches(not(isDisplayed())))
170184
onView(withId(R.id.pocketpaint_font_size_text))
171185
.check(matches(not(isDisplayed())))
172186
}
@@ -247,6 +261,8 @@ class TextToolIntegrationTest {
247261
textTool?.let { Assert.assertFalse(it.underlined) }
248262
textTool?.let { Assert.assertFalse(it.italic) }
249263
textTool?.let { Assert.assertFalse(it.bold) }
264+
textTool?.let { Assert.assertFalse(it.outlined) }
265+
outlineWidthLayout?.let { Assert.assertFalse(it.isVisible) }
250266
}
251267

252268
@Test
@@ -292,6 +308,20 @@ class TextToolIntegrationTest {
292308
Assert.assertFalse(toolMemberBold)
293309
boldToggleButton?.let { Assert.assertFalse(it.isChecked) }
294310
Assert.assertEquals(getFormattingOptionAsString(FormattingOptions.BOLD), boldToggleButton?.text.toString())
311+
selectFormatting(FormattingOptions.OUTLINE)
312+
textTool?.let { Assert.assertTrue(it.outlined) }
313+
outlineToggleButton?.let { Assert.assertTrue(it.isChecked) }
314+
Assert.assertEquals(
315+
getFormattingOptionAsString(FormattingOptions.OUTLINE),
316+
outlineToggleButton?.text.toString()
317+
)
318+
selectFormatting(FormattingOptions.OUTLINE)
319+
textTool?.let { Assert.assertFalse(it.outlined) }
320+
outlineToggleButton?.let { Assert.assertFalse(it.isChecked) }
321+
Assert.assertEquals(
322+
getFormattingOptionAsString(FormattingOptions.OUTLINE),
323+
outlineToggleButton?.text.toString()
324+
)
295325
}
296326

297327
@Test
@@ -301,6 +331,7 @@ class TextToolIntegrationTest {
301331
selectFormatting(FormattingOptions.UNDERLINE)
302332
selectFormatting(FormattingOptions.ITALIC)
303333
selectFormatting(FormattingOptions.BOLD)
334+
selectFormatting(FormattingOptions.OUTLINE)
304335
onToolBarView().performCloseToolOptionsView()
305336

306337
val oldBoxWidth = toolMemberBoxWidth
@@ -318,6 +349,7 @@ class TextToolIntegrationTest {
318349
underlinedToggleButton?.let { Assert.assertTrue(it.isChecked) }
319350
italicToggleButton?.let { Assert.assertTrue(it.isChecked) }
320351
boldToggleButton?.let { Assert.assertTrue(it.isChecked) }
352+
outlineToggleButton?.let { Assert.assertTrue(it.isChecked) }
321353
Assert.assertTrue(oldBoxWidth == toolMemberBoxWidth && oldBoxHeight == toolMemberBoxHeight)
322354
}
323355

@@ -328,6 +360,7 @@ class TextToolIntegrationTest {
328360
selectFormatting(FormattingOptions.UNDERLINE)
329361
selectFormatting(FormattingOptions.ITALIC)
330362
selectFormatting(FormattingOptions.BOLD)
363+
selectFormatting(FormattingOptions.OUTLINE)
331364

332365
val toolMemberBoxPosition = toolMemberBoxPosition
333366
val expectedPosition = toolMemberBoxPosition?.y?.let { PointF(toolMemberBoxPosition.x, it) }
@@ -344,6 +377,7 @@ class TextToolIntegrationTest {
344377
underlinedToggleButton?.let { Assert.assertTrue(it.isChecked) }
345378
italicToggleButton?.let { Assert.assertTrue(it.isChecked) }
346379
boldToggleButton?.let { Assert.assertTrue(it.isChecked) }
380+
outlineToggleButton?.let { Assert.assertTrue(it.isChecked) }
347381
Assert.assertEquals(expectedPosition, toolMemberBoxPosition)
348382
Assert.assertEquals(oldBoxWidth.toDouble(), toolMemberBoxWidth.toDouble(), EQUALS_DELTA)
349383
Assert.assertEquals(oldBoxHeight.toDouble(), toolMemberBoxHeight.toDouble(), EQUALS_DELTA)
@@ -750,6 +784,7 @@ class TextToolIntegrationTest {
750784
selectFormatting(FormattingOptions.ITALIC)
751785
selectFormatting(FormattingOptions.BOLD)
752786
selectFormatting(FormattingOptions.UNDERLINE)
787+
selectFormatting(FormattingOptions.OUTLINE)
753788
}
754789
val boxWidth = toolMemberBoxWidth
755790
val boxHeight = toolMemberBoxHeight
@@ -759,10 +794,120 @@ class TextToolIntegrationTest {
759794
selectFormatting(FormattingOptions.ITALIC)
760795
selectFormatting(FormattingOptions.BOLD)
761796
selectFormatting(FormattingOptions.UNDERLINE)
797+
selectFormatting(FormattingOptions.OUTLINE)
762798
Assert.assertTrue(boxWidth < toolMemberBoxWidth && boxHeight < toolMemberBoxHeight)
763799
}
764800
}
765801

802+
@Test
803+
fun testTextOutlineMode() {
804+
enterTestText()
805+
val canvasPoint = centerBox()
806+
onTopBarView().performClickCheckmark()
807+
val surfaceBitmapWidth = layerModel?.width
808+
val pixelsDrawingSurface = surfaceBitmapWidth?.let { IntArray(it) }
809+
if (surfaceBitmapWidth != null && canvasPoint != null) {
810+
layerModel?.currentLayer?.bitmap?.getPixels(
811+
pixelsDrawingSurface, 0, surfaceBitmapWidth, 0,
812+
canvasPoint.y.toInt(), surfaceBitmapWidth, 1
813+
)
814+
}
815+
val blackPixelAmountNoOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.BLACK) }
816+
val whitePixelAmountNoOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.WHITE) }
817+
onTopBarView().performUndo()
818+
819+
selectFormatting(FormattingOptions.OUTLINE)
820+
textTool?.let { Assert.assertTrue(it.outlined) }
821+
822+
onTopBarView().performClickCheckmark()
823+
824+
if (surfaceBitmapWidth != null && canvasPoint != null) {
825+
layerModel?.currentLayer?.bitmap?.getPixels(
826+
pixelsDrawingSurface, 0, surfaceBitmapWidth, 0,
827+
canvasPoint.y.toInt(), surfaceBitmapWidth, 1
828+
)
829+
}
830+
831+
val blackPixelAmountWithOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.BLACK) }
832+
if (blackPixelAmountNoOutline != null && blackPixelAmountWithOutline != null) {
833+
assert(blackPixelAmountNoOutline > blackPixelAmountWithOutline)
834+
assert(blackPixelAmountWithOutline > 0)
835+
}
836+
837+
val whitePixelAmountWithOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.WHITE) }
838+
if (whitePixelAmountNoOutline != null && whitePixelAmountWithOutline != null) {
839+
assert(whitePixelAmountNoOutline < whitePixelAmountWithOutline)
840+
}
841+
}
842+
843+
@Test
844+
fun testTextOutlineWidth() {
845+
val canvasPoint = centerBox()
846+
selectFormatting(FormattingOptions.OUTLINE)
847+
textTool?.let { Assert.assertTrue(it.outlined) }
848+
outlineWidthLayout?.let { Assert.assertTrue(it.isVisible) }
849+
val outlineWidthInput = onView(withId(R.id.pocketpaint_outline_width_text))
850+
val outlineWidthSeekbar = onView(withId(R.id.pocketpaint_outline_width_seek_bar))
851+
outlineWidthInput.check(matches(ViewMatchers.withText(DEFAULT_TEXT_OUTLINE_WIDTH.toString())))
852+
outlineWidthSeekbar.check(matches(UiMatcher.withProgress(DEFAULT_TEXT_OUTLINE_WIDTH)))
853+
854+
enterTestText()
855+
856+
var testOutlineWidthText = "1"
857+
858+
outlineWidthInput.perform(
859+
ViewActions.replaceText(testOutlineWidthText),
860+
ViewActions.closeSoftKeyboard()
861+
)
862+
outlineWidthInput.check(matches(ViewMatchers.withText(testOutlineWidthText)))
863+
outlineWidthSeekbar.check(matches(UiMatcher.withProgress(testOutlineWidthText.toInt())))
864+
865+
onTopBarView().performClickCheckmark()
866+
val surfaceBitmapWidth = layerModel?.width
867+
val pixelsDrawingSurface = surfaceBitmapWidth?.let { IntArray(it) }
868+
if (surfaceBitmapWidth != null && canvasPoint != null) {
869+
layerModel?.currentLayer?.bitmap?.getPixels(
870+
pixelsDrawingSurface, 0, surfaceBitmapWidth, 0,
871+
canvasPoint.y.toInt(), surfaceBitmapWidth, 1
872+
)
873+
}
874+
val blackPixelAmountThinOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.BLACK) }
875+
val whitePixelAmountThinOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.WHITE) }
876+
onTopBarView().performUndo()
877+
878+
testOutlineWidthText = "60"
879+
880+
outlineWidthInput.perform(
881+
ViewActions.replaceText(testOutlineWidthText),
882+
ViewActions.closeSoftKeyboard()
883+
)
884+
outlineWidthInput.check(matches(ViewMatchers.withText(testOutlineWidthText)))
885+
outlineWidthSeekbar.check(matches(UiMatcher.withProgress(testOutlineWidthText.toInt())))
886+
887+
onTopBarView().performClickCheckmark()
888+
889+
if (surfaceBitmapWidth != null && canvasPoint != null) {
890+
layerModel?.currentLayer?.bitmap?.getPixels(
891+
pixelsDrawingSurface, 0, surfaceBitmapWidth, 0,
892+
canvasPoint.y.toInt(), surfaceBitmapWidth, 1
893+
)
894+
}
895+
896+
val blackPixelAmountThickOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.BLACK) }
897+
if (blackPixelAmountThinOutline != null && blackPixelAmountThickOutline != null) {
898+
assert(blackPixelAmountThinOutline > 0)
899+
assert(blackPixelAmountThickOutline > 0)
900+
assert(blackPixelAmountThickOutline > blackPixelAmountThinOutline)
901+
}
902+
903+
val whitePixelAmountThickOutline = pixelsDrawingSurface?.let { countPixelsWithColor(it, Color.WHITE) }
904+
if (whitePixelAmountThinOutline != null && whitePixelAmountThickOutline != null) {
905+
assert(whitePixelAmountThinOutline > 0)
906+
assert(whitePixelAmountThickOutline > 0)
907+
assert(whitePixelAmountThickOutline < whitePixelAmountThinOutline)
908+
}
909+
}
910+
766911
private fun centerBox(): PointF? {
767912
val screenPoint =
768913
activityHelper?.displayWidth?.div(2.0f)
@@ -892,6 +1037,7 @@ class TextToolIntegrationTest {
8921037
FormattingOptions.UNDERLINE -> activity.getString(R.string.text_tool_dialog_underline_shortcut)
8931038
FormattingOptions.ITALIC -> activity.getString(R.string.text_tool_dialog_italic_shortcut)
8941039
FormattingOptions.BOLD -> activity.getString(R.string.text_tool_dialog_bold_shortcut)
1040+
FormattingOptions.OUTLINE -> activity.getString(R.string.text_tool_dialog_outline_shortcut)
8951041
}
8961042
}
8971043

@@ -921,7 +1067,7 @@ class TextToolIntegrationTest {
9211067
private val toolMemberMultilineText: Array<String>
9221068
get() = textTool!!.multilineText
9231069

924-
private enum class FormattingOptions { UNDERLINE, ITALIC, BOLD }
1070+
private enum class FormattingOptions { UNDERLINE, ITALIC, BOLD, OUTLINE }
9251071

9261072
companion object {
9271073
private const val TEST_TEXT = "123 www 123"

Paintroid/src/androidTest/java/org/catrobat/paintroid/test/junit/serialization/CommandSerializationTest.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,9 @@ class CommandSerializationTest {
190190
underline = false,
191191
italic = true,
192192
textSize = 25f,
193-
textSkewX = -0.25f
193+
textSkewX = -0.25f,
194+
outlined = false,
195+
outlineWidth = 25
194196
)
195197

196198
expectedModel.commands.add(

Paintroid/src/main/java/org/catrobat/paintroid/command/implementation/TextToolCommand.kt

+21-1
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@
2020
package org.catrobat.paintroid.command.implementation
2121

2222
import android.graphics.Canvas
23+
import android.graphics.Color
2324
import android.graphics.Paint
2425
import android.graphics.PointF
2526
import org.catrobat.paintroid.command.Command
2627
import org.catrobat.paintroid.command.serialization.SerializableTypeface
2728
import org.catrobat.paintroid.common.ITALIC_FONT_BOX_ADJUSTMENT
2829
import org.catrobat.paintroid.contract.LayerContracts
30+
import org.catrobat.paintroid.tools.implementation.OUTLINED_FONT_WIDTH_ADJUSTMENT
31+
32+
const val TEXT_SIZE_MAGNIFICATION_FACTOR = 3f
2933

3034
class TextToolCommand(
3135
multilineText: Array<String>,
@@ -74,14 +78,30 @@ class TextToolCommand(
7478
val scaledBoxWidth = boxWidth / widthScaling
7579
val scaledBoxHeight = boxHeight / heightScaling
7680

81+
val fillPaint = Paint(textPaint)
82+
if (typeFaceInfo.outlined) fillPaint.color = Color.WHITE
7783
multilineText.forEachIndexed { index, textLine ->
7884
canvas.drawText(
7985
textLine,
8086
scaledWidthOffset - scaledBoxWidth / 2 / if (typeFaceInfo.italic) ITALIC_FONT_BOX_ADJUSTMENT else 1f,
8187
-(scaledBoxHeight / 2) + scaledHeightOffset - textAscent + lineHeight * index,
82-
textPaint
88+
fillPaint
8389
)
8490
}
91+
if (typeFaceInfo.outlined) {
92+
val outlinePaint = Paint(textPaint)
93+
val adjustedStrokeWidth = if (typeFaceInfo.outlineWidth == 0) 0f else java.lang.Float.max(textPaint.textSize / TEXT_SIZE_MAGNIFICATION_FACTOR * (typeFaceInfo.outlineWidth / OUTLINED_FONT_WIDTH_ADJUSTMENT), 1f)
94+
outlinePaint.style = Paint.Style.STROKE
95+
outlinePaint.strokeWidth = adjustedStrokeWidth
96+
multilineText.forEachIndexed { index, textLine ->
97+
canvas.drawText(
98+
textLine,
99+
scaledWidthOffset - scaledBoxWidth / 2 / if (typeFaceInfo.italic) ITALIC_FONT_BOX_ADJUSTMENT else 1f,
100+
-(scaledBoxHeight / 2) + scaledHeightOffset - textAscent + lineHeight * index,
101+
outlinePaint
102+
)
103+
}
104+
}
85105
restore()
86106
}
87107
}

Paintroid/src/main/java/org/catrobat/paintroid/command/serialization/SerializableTypeface.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import com.esotericsoftware.kryo.io.Input
2323
import com.esotericsoftware.kryo.io.Output
2424
import org.catrobat.paintroid.tools.FontType
2525

26-
data class SerializableTypeface(val font: FontType, val bold: Boolean, val underline: Boolean, val italic: Boolean, val textSize: Float, val textSkewX: Float) {
26+
data class SerializableTypeface(val font: FontType, val bold: Boolean, val underline: Boolean, val italic: Boolean, val textSize: Float, val textSkewX: Float, val outlined: Boolean, val outlineWidth: Int) {
2727

2828
class TypefaceSerializer(version: Int) : VersionSerializer<SerializableTypeface>(version) {
2929
override fun write(kryo: Kryo, output: Output, typeface: SerializableTypeface) {
@@ -34,6 +34,8 @@ data class SerializableTypeface(val font: FontType, val bold: Boolean, val under
3434
writeBoolean(typeface.italic)
3535
writeFloat(typeface.textSize)
3636
writeFloat(typeface.textSkewX)
37+
writeBoolean(typeface.outlined)
38+
writeInt(typeface.outlineWidth)
3739
}
3840
}
3941

@@ -42,7 +44,7 @@ data class SerializableTypeface(val font: FontType, val bold: Boolean, val under
4244

4345
override fun readCurrentVersion(kryo: Kryo, input: Input, type: Class<out SerializableTypeface>): SerializableTypeface {
4446
return with(input) {
45-
SerializableTypeface(FontType.valueOf(readString()), readBoolean(), readBoolean(), readBoolean(), readFloat(), readFloat())
47+
SerializableTypeface(FontType.valueOf(readString()), readBoolean(), readBoolean(), readBoolean(), readFloat(), readFloat(), readBoolean(), readInt())
4648
}
4749
}
4850
}

0 commit comments

Comments
 (0)