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
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>
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
📝 Read this story later in Journal.
👩💻 Wake up every Sunday morning to the week’s most noteworthy stories in Tech waiting in your inbox. Read the Noteworthy in Tech newsletter.