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"/>