I was wondering how WhatsApp handles the time shown in every message.
For those who don\'t know:
You can use the layout and code below to achieve the desired effect. Source code gist
What I have used is get the width of the text + the time layout and check if this exceeds the container layout width, and adjust the height of the container accordingly. We have to extend from FrameLayout since this is the one which allows overlapping of two child views.
This is tested to be working on English locale. Suggestions and improvements are always welcome :)
Hope I've helped someone looking for the same solution.
Based on @Sinan Ergin answer but slightly improved:
/**
* Layout that allows a [TextView] to flow around a [View].
*
* First child must be of type [TextView].
* Second child must be of type [View].
*/
class TextViewContainerFlowLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
private val textView by lazy(NONE) { getChildAt(0) as TextView }
private val containerView by lazy(NONE) { getChildAt(1) }
private val viewPartMainLayoutParams by lazy(NONE) { textView.layoutParams as LayoutParams }
private val viewPartSlaveLayoutParams by lazy(NONE) { containerView.layoutParams as LayoutParams }
private var containerWidth = 0
private var containerHeight = 0
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var widthSize = MeasureSpec.getSize(widthMeasureSpec)
if (widthSize <= 0) {
return
}
val availableWidth = widthSize - paddingLeft - paddingRight
val textViewWidth = textView.measuredWidth + viewPartMainLayoutParams.leftMargin + viewPartMainLayoutParams.rightMargin
val textViewHeight = textView.measuredHeight + viewPartMainLayoutParams.topMargin + viewPartMainLayoutParams.bottomMargin
containerWidth = containerView.measuredWidth + viewPartSlaveLayoutParams.leftMargin + viewPartSlaveLayoutParams.rightMargin
containerHeight = containerView.measuredHeight + viewPartSlaveLayoutParams.topMargin + viewPartSlaveLayoutParams.bottomMargin
val viewPartMainLineCount = textView.lineCount
val viewPartMainLastLineWidth = if (viewPartMainLineCount > 0) textView.layout.getLineWidth(viewPartMainLineCount - 1) else 0.0f
widthSize = paddingLeft + paddingRight
var heightSize = paddingTop + paddingBottom
if (viewPartMainLineCount > 1 && viewPartMainLastLineWidth + containerWidth < textView.measuredWidth) {
widthSize += textViewWidth
heightSize += textViewHeight
} else if (viewPartMainLineCount > 1 && viewPartMainLastLineWidth + containerWidth >= availableWidth) {
widthSize += textViewWidth
heightSize += textViewHeight + containerHeight
} else if (viewPartMainLineCount == 1 && textViewWidth + containerWidth >= availableWidth) {
widthSize += textView.measuredWidth
heightSize += textViewHeight + containerHeight
} else {
widthSize += textViewWidth + containerWidth
heightSize += textViewHeight
}
setMeasuredDimension(widthSize, heightSize)
super.onMeasure(
MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY)
)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
textView.layout(
paddingLeft,
paddingTop,
textView.width + paddingLeft,
textView.height + paddingTop
)
containerView.layout(
right - left - containerWidth - paddingRight,
bottom - top - paddingBottom - containerHeight,
right - left - paddingRight,
bottom - top - paddingBottom
)
}
}
view_chat_entry.xml
<my.ui.view.TextViewContainerFlowLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|bottom"
android:gravity="left|center_vertical"
android:padding="12dp"
>
<my.ui.android.TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|bottom"
android:gravity="left|top"
android:textIsSelectable="true"
tools:text="hjjfg"
/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="bottom"
>
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:maxLines="1"
/>
<my.ui.view.ChatEntryStatusView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_marginStart="4dp"
android:layout_height="wrap_content"
/>
</LinearLayout>
</my.ui.view.TextViewContainerFlowLayout>
adapter_item_chat_left.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="64dp"
android:layout_marginTop="4dp"
android:gravity="left"
>
<include
layout="@layout/view_chat_entry"
android:id="@+id/chatEntry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
adapter_item_chat_right.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginStart="64dp"
android:layout_marginTop="4dp"
android:gravity="right"
>
<include
layout="@layout/view_chat_entry"
android:id="@+id/chatEntry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
End result (styling is done on the container with a drawable):
The previous answers didn't satisfy my needs as they were too complex and the scrolling on RecyclerView was way too slow! I could feel the stutter while scrolling. So, I modified @Rahul Shuklas answer to make it more efficient. I am sharing my result below. The code is self explanatory, I have added comments for more understandability.
class ChatBubbleLayout : FrameLayout {
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
@TargetApi(21)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
doMeasure()
}
private fun doMeasure() {
val messageTextView = findViewById<TextView>(R.id.tv_message)
val dateTextView = findViewById<TextView>(R.id.tv_message_time)
// Message line count
val lineCount = messageTextView.lineCount
// Message padding
val messageTextViewPadding = messageTextView.paddingLeft + messageTextView.paddingRight
// First / Second last line of message
val lastLineStart = messageTextView.layout.getLineStart(lineCount - 1)
val lastLineEnd = messageTextView.layout.getLineEnd(lineCount - 1)
// Width of First / Second last line of message
var desiredWidth = Layout.getDesiredWidth(messageTextView.text.subSequence(lastLineStart,
lastLineEnd), messageTextView.paint).toInt()
var desiredHeight = measuredHeight
if (desiredWidth < minimumWidth && messageTextView.measuredWidth < minimumWidth) {
// Probably a small or single line message
desiredWidth = minimumWidth + messageTextViewPadding
} else {
// Probably a bit long or multiple line message
desiredWidth = messageTextView.measuredWidth + messageTextViewPadding
}
if(desiredHeight < minimumHeight) {
desiredHeight = minimumHeight
}
setMeasuredDimension(desiredWidth, desiredHeight)
}
}
My Layout XML file
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<com.app.chat.ui.ChatBubbleLayout
android:id="@+id/chat_bubble_item_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="@dimen/height_16dp"
android:background="@drawable/medium_green_rounded_corner"
android:minWidth="96dp"
android:minHeight="44dp">
<TextView
android:id="@+id/tv_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|left"
android:autoLink="all"
android:linksClickable="true"
android:maxWidth="280dp"
android:paddingLeft="@dimen/margin_8dp"
android:paddingTop="@dimen/margin_8dp"
android:paddingRight="@dimen/margin_8dp"
android:paddingBottom="@dimen/margin_8dp"
android:text="@{chatMessageVM.iMessage.message}"
android:textColor="@color/white"
android:textColorLink="@color/white"
android:textIsSelectable="true"
android:textSize="@dimen/text_14sp"
tools:text="Nope" />
<TextView
android:id="@+id/tv_message_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|right|bottom"
android:layout_marginRight="@dimen/margin_4dp"
android:layout_marginBottom="@dimen/margin_2dp"
android:gravity="center_vertical"
android:text="@{chatMessageVM.iMessage.readableTimestamp}"
android:textColor="@color/gray_5"
android:textSize="@dimen/text_12sp"
tools:text="11:21 AM" />
</com.app.chat.ui.ChatBubbleLayout>
</LinearLayout>
I hope it helps future readers.
Here is my Layout file chat_row_right_1.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_toLeftOf="@+id/test_arrow"
android:id="@+id/message_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="15dp"
android:paddingBottom="7dp"
android:paddingTop="5dp"
android:paddingRight="15dp"
android:layout_marginTop="5dp"
android:maxWidth="200dp"
android:background="@drawable/layout_bg2_1"
tools:ignore="UselessParent">
<TextView
android:layout_marginEnd="10dp"
android:id="@+id/text"
android:text="demo Text"
android:textColor="#222"
android:textSize="17sp"
android:layout_width="wrap_content"
android:maxWidth="200dp"
android:layout_height="wrap_content" />
<TextClock
android:id="@+id/msg_time"
android:layout_toEndOf="@+id/text"
android:layout_alignBottom="@+id/text"
android:text="1:30 P.M."
android:textColor="#888"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/is_Read_iv"
android:layout_toEndOf="@+id/msg_time"
android:layout_alignBottom="@+id/text"
android:layout_width="wrap_content"
android:src="@drawable/ic_done_black_24dp"
android:layout_height="wrap_content" />
</RelativeLayout>
<ImageView
android:id="@+id/test_arrow"
android:layout_alignParentRight="true"
android:layout_width="20dp"
android:background="@null"
android:layout_marginTop="-2dp"
android:layout_marginLeft="-8dp"
android:layout_height="wrap_content"
android:src="@drawable/ic_play_arrow_black_24dp"/>
</RelativeLayout>
And here is ic_right_bubble.xml file in drawable folder
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#cfc"
android:pathData="M8,5v14l11,-14z"/>
</vector>
You will get exact same as WhatsApp See screenshot
layout_chat_left.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layoutChat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp">
<RelativeLayout
android:id="@+id/message_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_toRightOf="@id/test_arrow"
android:background="@drawable/bg_msg_left"
android:paddingLeft="15dp"
android:paddingTop="5dp"
android:paddingRight="15dp"
android:paddingBottom="7dp"
tools:ignore="UselessParent">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:maxWidth="200dp"
android:text="demo Text"
android:textColor="#222"
android:textSize="17sp" />
<TextClock
android:id="@+id/msg_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/text"
android:layout_toEndOf="@+id/text"
android:text="1:30 P.M."
android:textColor="#888" />
<ImageView
android:id="@+id/is_Read_iv"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginBottom="2dp"
android:layout_marginLeft="2dp"
android:layout_alignBottom="@+id/text"
android:layout_toEndOf="@+id/msg_time"
android:src="@drawable/icon_tick"
android:tint="@color/BlueTint"/>
</RelativeLayout>
<ImageView
android:id="@+id/test_arrow"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignParentLeft="true"
android:layout_marginTop="1dp"
android:layout_marginRight="-6dp"
android:background="@null"
android:scaleX="-1.5"
android:src="@drawable/v_bubble_corner_left" />
</RelativeLayout>
layout_chat_right.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layoutChat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp">
<RelativeLayout
android:id="@+id/message_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_toLeftOf="@id/test_arrow"
android:background="@drawable/bg_msg_right"
android:paddingLeft="15dp"
android:paddingTop="5dp"
android:paddingRight="15dp"
android:paddingBottom="7dp"
tools:ignore="UselessParent">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:maxWidth="200dp"
android:text="demo Text"
android:textColor="#222"
android:textSize="17sp" />
<TextClock
android:id="@+id/msg_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/text"
android:layout_toEndOf="@+id/text"
android:text="1:30 P.M."
android:textColor="#888" />
<ImageView
android:id="@+id/is_Read_iv"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginBottom="2dp"
android:layout_marginLeft="2dp"
android:layout_alignBottom="@+id/text"
android:layout_toEndOf="@+id/msg_time"
android:src="@drawable/icon_tick"
android:tint="@color/BlueTint" />
</RelativeLayout>
<ImageView
android:id="@+id/test_arrow"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignParentRight="true"
android:layout_marginLeft="-6dp"
android:layout_marginTop="1dp"
android:background="@null"
android:scaleX="1.5"
android:src="@drawable/v_bubble_corner_right" />
</RelativeLayout>
bg_msg_left.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<!-- view background color -->
<!--<solid android:color="@color/bg_msg_right" >-->
<solid android:color="@color/white" >
</solid>
<corners
android:topLeftRadius="0dp"
android:topRightRadius="5dp"
android:bottomLeftRadius="5dp"
android:bottomRightRadius="5dp">
</corners>
</shape>
bg_msg_right.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<!-- view background color -->
<!--<solid android:color="@color/bg_msg_right" >-->
<solid android:color="@color/whatsapp_green" >
</solid>
<corners
android:topLeftRadius="5dp"
android:topRightRadius="0dp"
android:bottomLeftRadius="5dp"
android:bottomRightRadius="5dp">
</corners>
</shape>
v_bubble_corner_left.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/white"
android:pathData="M8,5v14l11,-14z" />
</vector>
v_bubble_corner_right.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/whatsapp_green"
android:pathData="M8,5v14l11,-14z"/>
</vector>
And the CommentAdapter.java is
import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.daimajia.androidanimations.library.Techniques;
import com.daimajia.androidanimations.library.YoYo;
import com.google.android.material.card.MaterialCardView;
import java.util.ArrayList;
import java.util.List;
public class CommentAdapter extends RecyclerView.Adapter<CommentAdapter.ViewHolder> {
private List<String> mComment;
private List<String> mTimeData;
private List<Integer> mIcon;
private List<Integer> mDirection;
private List<Integer> mRecordID;
private Context mContext;
private LayoutInflater mInflater;
private static final String TAG = "CommentAdapter";
private ItemLongClickListener mLongClickListener;
// data is passed into the constructor
CommentAdapter(Context context, List<String> dataComment, List<String> dataTimeData, List<Integer> dataDirection, List<Integer> dataRecordID) {
mContext = context;
this.mInflater = LayoutInflater.from( context );
this.mComment = dataComment;
this.mTimeData = dataTimeData;
this.mDirection = dataDirection;
this.mRecordID = dataRecordID;
}
// inflates the row layout from xml when needed
@NonNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
if (viewType == 1) {
view = mInflater.inflate( R.layout.layout_chat_left, parent, false );
} else {
view = mInflater.inflate( R.layout.layout_chat_right, parent, false );
}
return new ViewHolder( view );
}
// binds the data to the TextView in each row
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
String mTitle = mComment.get( position );
holder.tvComment.setText( mTitle );
String mSubTitle = mTimeData.get( position );
holder.tvTime.setText( mSubTitle );
int maxWidth = mContext.getResources().getDisplayMetrics().widthPixels;
holder.layoutChat.getLayoutParams().width = maxWidth;
}
// total number of rows
@Override
public int getItemCount() {
return mComment.size();
}
// stores and recycles views as they are scrolled off screen
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnLongClickListener {
TextView tvComment;
TextView tvTime;
TextView tvSerial;
RelativeLayout layoutChat;
MaterialCardView cardView;
ViewHolder(View itemView) {
super( itemView );
tvComment = itemView.findViewById( R.id.text );
tvTime = itemView.findViewById( R.id.msg_time );
layoutChat = itemView.findViewById( R.id.layoutChat );
itemView.setOnLongClickListener( this );
}
@Override
public boolean onLongClick(View v) {
Log.d( TAG, "onLongClick: " + getAdapterPosition() );
if (mLongClickListener!=null)
mLongClickListener.onItemLongClick( v, mRecordID.get( getAdapterPosition() ) );
return true;
}
}
void setOnLongClickListener(ItemLongClickListener itemLongClickListener) {
this.mLongClickListener = itemLongClickListener;
}
// parent activity will implement this method to respond to click events
public interface ItemLongClickListener {
void onItemLongClick(View view, int position);
}
@Override
public int getItemViewType(int position) {
if (mDirection.get( position ) == 1)
return 1;
return 2;
}
}
Here are the screenshots, one from a live demo
I guess the easiest way to achieve this kind oflayout would be to add enough blank space in your message to be sure there is enough space on the right to not cover the time (I don't see any other easy way to have a margin/padding/positioning for the last line of your text only) Then you just place the time in the relative as an align bottom right