We have started the process of making our application more internationalization friendly. That typically requires the removal of all hardcoded strings and replacing them with the reference to the string.xml
.Just doing that alone will be a good starting point in the translation project. There is of course a bunch of different problems that need to be solved before a translated app can be released, like string formatting, string concatenation, plurals, text in images, etc.
Text in images is a problem that we faced here at OkCupid during our translation project. In some cases, text in images can be replaced with just a different image that is more expressive and doesn’t require any words in it. But sometimes we need to add some special effect to the text so it stands out to the user and can also be localized properly. This post will show an example of an extrude effect that was applied to the text and then integrated into the app as an image and how it was replaced with a regular TextView
that can be easily translated.
The problem
The goal was to display a text that would have an extrude effect and it should also work with different languages. Something that looks like this.

One solution would be to use a custom font with an extrude effect but that doesn't work well with characters in different languages. Custom fonts have to specify how each character will look like when the font is applied. If the font doesn't handle the character, then Android's default font will be used just on that character. In addition, we should remember that some Asian languages don't have a traditional alphabet, and applying custom font on those languages can be difficult.Custom TextView
that will draw a extrude effect behind the text was a better, faster, and more scalable solution in our case. We also have more control with custom TextView
which allows us to adjust colors, extrude direction, and extrude depth.
The solution
Lets create a class that extends AppCompatTextView
and overrides onDraw()
method
class ExtrudeEffectText @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
.........
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
}
}
The idea here is to draw the same text multiple times shifting x and y position each time the text is drawn to produce an extrude effect. First, we will get TextView
paint object. This is an object that defines the style of the text to be drawn and it is responsible for changing text size, text color, font, etc. We will take a paint object, change text color (all other text properties will be preserved) and just draw text multiple times.
class ExtrudeEffectText @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
.........
val extrudeColor = Color.BLACK
val extrudeDepth = 10
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
paint.color = extrudeColor
canvas?.save()
val dx = 1f
val dy = -1f
// draw same text multiple times with shifting x and y position
for (i in 0..extrudeDepth) {
canvas?.translate(dx, dy)
layout.draw(canvas)
}
canvas?.restore()
}
}
dx
and dy
specifies the distance we want to move in this case it is 1 pixel. +/- signs indicate the direction of the move.
dx = 1f moves to the right
dx = -1f moves to the left
dy = 1f moves down
dy = -1f moves up
`canvas.save()` helps us to save the state of the canvas before we do any manipulations like translate. canvas?.restore()
restores the state of the canvas. layout
is another property of TextView
that used to display a text
Now when we are done with drawing extrude effect we need to draw our text with original color and then add a stroke to the text.
val strokeWidth = 2f
val strokeColor = Color.RED
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val originalTextColor = paint.color
paint.color = extrudeColor
canvas?.save()
........
// draw text in original color
paint.setColor(originalTextColor)
layout.draw(canvas)
// add stroke around text
paint.setColor(strokeColor)
paint.style = Paint.Style.STROKE
paint.strokeWidth = strokeWidth
layout.draw(canvas)
canvas?.restore()
}
This all works great but there is still a little problem. We are drawing extrude effect in diagonal from bottom left to top right. The bigger the `extrudeDepth` the higher the risk that `TextView` will be drawn out of bounds and look cut off on the right and top edges. To solve this issue we need to add some extra padding to the view to ensure that nothing cut off.
class ExtrudeEffectText @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
init {
addExtraPadding()
}
.....
private fun addExtraPadding() {
setPadding(
paddingStart,
paddingTop + extrudeDepth,
paddingEnd + extrudeDepth,
paddingBottom
)
}
}
We should also take into account that some languages have the Right to Left text orientation so we need to add some more logic to handle this case. First, let’s determine the layout direction. This can be done using the snippet below
val config = context.resources.configuration
val isRtl = config.layoutDirection == View.LAYOUT_DIRECTION_RTL
We can use the isRtl
flag to determine the direction of extruding (in our case we will draw from the bottom right to top left for RTL) and on which edge to set padding.
class ExtrudeEffectText @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
.....
// change extrude direction based on RTL
val dx = if (isRtl) -1f else 1f
val dy = -1f
// draw same text multiple times with shifting x and y position
for (i in 0..extrudeDepth) {
canvas?.translate(dx, dy)
layout.draw(canvas)
}
......
}
private fun addExtraPadding() {
val config = context.resources.configuration
isRtl = config.layoutDirection == View.LAYOUT_DIRECTION_RTL
if (isRtl) {
// in Right to left the start is on the right
setPadding(
paddingEnd + extrudeDepth,
paddingTop + extrudeDepth,
paddingStart,
paddingBottom
)
} else {
setPadding(
paddingStart,
paddingTop + extrudeDepth,
paddingEnd + extrudeDepth,
paddingBottom
)
}
}
}
Here some examples with different languages.


We can also test a view with Android pseudo localization (more information here)which can be very useful in any internationalization project. Pseudo localization will imitate scenario where app have translations. By adding
android {
...
buildTypes {
debug {
pseudoLocalesEnabled true
}
}
and changing language to English(XA), strings from string.xml
will change to strings with weird characters. Those characters look like English letters but also have some additional symbols on top of them. The length of each string will be increased by concatenating placeholder text. Hardcoded strings won't change. Pseudo localization allows developer to find any UI issue related to translation and to spot any hardcoded strings even before real translations are added.
Now we have a TextView
with extruding effect that works with any language and can be easily localized. We can still use all of the TextView
attributes like textSize, fontFamily, letterSpacing, lineSpacing, gravity, etc and it will not break our extrude effect. We can also add some custom attributes for more customization. Here is all the code together and before and after comparison
Want to work on cool projects like this? We are hiring Android engineers! Apply today at https://www.okcupid.com/careers


class ExtrudeEffectText @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
var extrudeDepth: Int = 0
set(value) {
field = value
addExtraPadding()
refreshLayout()
}
var extrudeColor: Int = Color.BLACK
set(value) {
field = value
refreshLayout()
}
var strokeWidth: Float = 0f
set(value) {
field = value
refreshLayout()
}
var strokeColor: Int = Color.WHITE
set(value) {
field = value
refreshLayout()
}
private var isRtl: Boolean = false
init {
addExtraPadding()
context.theme.obtainStyledAttributes(
attrs,
R.styleable.ExtrudeEffect,
0, 0
).apply {
try {
extrudeDepth = getInt(R.styleable.ExtrudeEffect_extrudeDepth, 0)
extrudeColor = getColor(R.styleable.ExtrudeEffect_extrudeColor, Color.BLACK)
strokeWidth = getFloat(R.styleable.ExtrudeEffect_textStrokeWidth, 0f)
strokeColor = getColor(R.styleable.ExtrudeEffect_textStrokeColor, Color.WHITE)
} finally {
recycle()
}
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// save the original text color since TextPaint will change to draw shadow and stroke
val originalTextColor = paint.color
paint.color = extrudeColor
canvas?.save()
// take into account padding set on the view
val translateX = if (isRtl) paddingEnd else paddingStart
canvas?.translate(translateX.toFloat(), paddingTop.toFloat())
// change extrude direction based on RTL
val dx = if (isRtl) -1f else 1f
val dy = -1f
// draw same text multiple times with shifting x and y position
for (i in 0..extrudeDepth) {
canvas?.translate(dx, dy)
layout.draw(canvas)
}
// draw text in original color
paint.setColor(originalTextColor)
layout.draw(canvas)
// add stroke around text
paint.setColor(strokeColor)
paint.style = Paint.Style.STROKE
paint.strokeWidth = strokeWidth
layout.draw(canvas)
// revert paint to original state
paint.color = originalTextColor
paint.style = Paint.Style.FILL
canvas?.restore()
}
private fun addExtraPadding() {
val config = context.resources.configuration
isRtl = config.layoutDirection == View.LAYOUT_DIRECTION_RTL
if (isRtl) {
// in Right to left the start is on the right
setPadding(
paddingEnd + extrudeDepth,
paddingTop + extrudeDepth,
paddingStart,
paddingBottom
)
} else {
setPadding(
paddingStart,
paddingTop + extrudeDepth,
paddingEnd + extrudeDepth,
paddingBottom
)
}
}
private fun refreshLayout() {
invalidate()
requestLayout()
}
}
// src/main/res/values/attrs.xml
<declare-styleable name="ExtrudeEffect">
<attr name="extrudeDepth" format="integer"/>
<attr name="extrudeColor" format="reference|color"/>
<attr name="textStrokeWidth" format="float"/>
<attr name="textStrokeColor" format="reference|color"/>
</declare-styleable>
// in layout
<com.example.shadoweffect.ExtrudeEffectText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textColor="#000"
android:text="Hello world!"
app:extrudeDepth="7"
app:extrudeColor="@color/colorAccent"
app:textStrokeWidth="2"
app:textStrokeColor="@color/yellow"
android:fontFamily="@font/gt_america_black"/>