A few weeks ago, Facebook released a new feature. When I tapped into Messenger, pretty soon my attention went from the actual conversations to the funky gradient effect of the message bubbles containing them. This is a new feature of Messenger, which allows you to choose a gradient instead of a plain color for the background of the chat messages. I was so amazed. Ever since then I have always asked myself how Facebook engineers made that. In this article, I will walk through my thought process as I attempted to recreate it and explain Android API used to make it work.

Analysis

First, let’s look at the example again to see what exactly it is that we’re trying to achieve here.

In general, we have a pretty standard messaging layout: messages are divided into bubbles going from top to bottom, ours on the right and the other people in the chat on the left. The ones on the left all have a gray background color, but the ones on the right look like they’re sharing the same fixed background gradient . Before I figured out it is just fixed background gradient , I have asked around my peers. Most of them suggested to have an algorithm to generate a gradient for each ViewHolder . That sounds hard and not performant to me because when the list is scrolled, the bubble color keeps changing. Then on a beautiful, when I chatted with my friend, I saw a bug from Facebook web

hmm. Do they just have a gradient in the back and a mask in the front? Off the top of my head I visualized something

(Updated May 1st 2019)

I added a new gifs. It’s made easier to visualize

F8 2019

Bingo.

The main idea is the gradient ViewHolder row has a white background and a hole beneath the text content. I will just focus on this particular ViewHolder .

Set up the layout

<shape

xmlns:android="http://schemas.android.com/apk/res/android">

<gradient

android:angle="270"

android:centerColor="@color/center_color"

android:endColor="@color/end_color"

android:startColor="@color/start_color"

android:type="linear"/>

</shape>



chat_background.xml

activity_chat.xml

<androidx.constraintlayout.widget.ConstraintLayout

...> <ImageView

android:id="@+id/ivBackground"

android:layout_height="0dp"

android:layout_width="match_parent"

app:layout_constraintBottom_toTopOf="@+id/llInput"

app:layout_constraintEnd_toEndOf="parent"

app:layout_constraintStart_toStartOf="parent"

app:layout_constraintTop_toBottomOf="@+id/toolbar"/> <androidx.constraintlayout.widget.ConstraintLayout/>

We will use ConstraintLayout for the chat bubble

item_outgoing_image_message.xml

<com.ctech.messenger.widget.BackgroundAwareLayout

android:layout_width="match_parent"

android:layout_height="wrap_content"

..

android:background="@color/white"

app:child_id="@id/tvContent"

>



<TextView

android:id="@id/tvContent"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

...

app:layout_constraintEnd_toEndOf="parent"

app:layout_constraintTop_toTopOf="parent"/> <TextView

android:id="@id/tvTimeStamp"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

.../>



</com.ctech.messenger.widget.BackgroundAwareLayout>

BackgroundAwareLayout is a custom ConstraintLayout . To cut a hole, we need to know the position and the size of the content. It is more reusable when we provide a child reference app:child_id through xml attribute than findViewById in the class.

BackgroundAwareLayout.kt

private fun setup(attrs: AttributeSet) { val ta = context.obtainStyledAttributes(attrs, R.styleable.BackgroundAwareLayout)

this.childId = ta.getResourceId(R.styleable.BackgroundAwareLayout_child_id, 0)



if (this.childId != 0) {

ta.recycle()

return

}

throw IllegalArgumentException("unable to find childId to create a hole")

} override fun onViewAdded(view: View) {

super.onViewAdded(view)

if (view.id == this.childId) {

this.childView = view

}

}

Next step, we create an eraser to remove a portion of white background where the text context is

private fun setupEraser() {

eraser = Paint()

eraser.color = ContextCompat.getColor(context, android.R.color.transparent)

eraser.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)

eraser.isAntiAlias = true

setLayerType(View.LAYER_TYPE_HARDWARE, null)

}

It’s straightforward. One thing to note even though hardwareAccelerate is enabled by default, we have to call setLayerType(View.LAYER_TYPE_HARDWARE, null) . Then, we can draw a transparent background to see the gradient background.

override fun onDraw(canvas: Canvas) {

super.onDraw(canvas)

childRect.set(childView.left.toFloat(), childView.top.toFloat(),

childView.right.toFloat(), childView.bottom.toFloat())

canvas.drawRoundRect(childRect, radius, radius, eraser)

}

childRect is just a helper for (left,top,right,bottom) when I tried some testings during the implementation. We’re done!!

Hold on. The world isn’t so easy. Take a look at Messenger

and ours

There are two issues:

The list needs to scroll to the bottom. We can achieve by rvMessages.layoutManager!!.scrollToPosition(adapter.itemCount — 1) The color at the bottom is purple not light blue. When the keyboard shows up, the windows is resized to give space to the keyboard. This causes the content of ivBackground to scale accordingly.

To solve this problem, the original gradient background has to be cropped by the height of the keyboard.

ivBackground.doOnLayout {

if (!::backgroundBitmap.isInitialized) {

val background = ContextCompat.getDrawable(this, R.drawable.chat_background) as GradientDrawable

background.setSize(it.width, it.height)

backgroundBitmap = background.toBitmap()

ivBackground.setImageBitmap(backgroundBitmap)

}

}

After the first layout pass, we create a cached bitmap of the background. We change to ImageView to KeyboardAwareImageView

ivBackground.setKeyboardListener(object : OnKeyboardShowHideListener {

override fun onToggle(visible: Boolean, height: Int) { if (::backgroundBitmap.isInitialized) {

if (visible) {

val cropped = cropBitmap(backgroundBitmap, Rect(0, 0, ivBackground.width, height))

ivBackground.setImageBitmap(cropped)

} else {

ivBackground.setImageBitmap(backgroundBitmap)

}

}

}



})

When the keyboard is visible with the available height of the window, we crop the cached bitmap. Let see our effort 🎉🎉

The source code can be found here