Whatsapp Message Layout - How to get time-view in the same row

AndroidAndroid LayoutTextview

Android Problem Overview


I was wondering how WhatsApp handles the time shown in every message.

For those who don't know:

  1. If the message is very short, the text and time are in the same row.
  2. If the message is long, the time is in the bottom right corner - the text wrapped around it.

With a RelativeLayout and toLeftOf I would get 1) but not 2) as previous lines would be "cut off" at the position of the time view. Same behaviour If i use a LinearLayout.

So I tried to use a FrameLayout or a RelativeLayout without any connection between text and time.

However, if the text is as long as the message-view is big, both views would overlap. If I put blank characters to my message I wouldn't have the time on the right.

Do they really have some kind of text-wrapping-lib for this or is it possible to do only with layouts?

Here is the requested screenshot:

enter image description here

Android Solutions


Solution 1 - Android

@Hisham Muneer 's answer very good.

But there are some problems. For example:

  • If the TextView has 2 full lines (end to end), the text will intersect with datetime text layout. Finally, the views will look like onion effect.
  • The text line wraps can't works efficiently. You must control this lines and relocate the datetime view.

I'm going to share my solution, if you will need like this problem.

This is example screenshot Example screenshot

ImFlexboxLayout.java

    public class ImFlexboxLayout extends RelativeLayout {
    private TextView viewPartMain;
    private View viewPartSlave;

    private TypedArray a;

    private RelativeLayout.LayoutParams viewPartMainLayoutParams;
    private int viewPartMainWidth;
    private int viewPartMainHeight;

    private RelativeLayout.LayoutParams viewPartSlaveLayoutParams;
    private int viewPartSlaveWidth;
    private int viewPartSlaveHeight;


    public ImFlexboxLayout(Context context) {
        super(context);
    }

    public ImFlexboxLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        a = context.obtainStyledAttributes(attrs, R.styleable.ImFlexboxLayout, 0, 0);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        try {
            viewPartMain = (TextView) this.findViewById(a.getResourceId(R.styleable.ImFlexboxLayout_viewPartMain, -1));
            viewPartSlave = this.findViewById(a.getResourceId(R.styleable.ImFlexboxLayout_viewPartSlave, -1));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (viewPartMain == null || viewPartSlave == null || widthSize <= 0) {
            return;
        }

        int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
        int availableHeight = heightSize - getPaddingTop() - getPaddingBottom();

        viewPartMainLayoutParams = (LayoutParams) viewPartMain.getLayoutParams();
        viewPartMainWidth = viewPartMain.getMeasuredWidth() + viewPartMainLayoutParams.leftMargin + viewPartMainLayoutParams.rightMargin;
        viewPartMainHeight = viewPartMain.getMeasuredHeight() + viewPartMainLayoutParams.topMargin + viewPartMainLayoutParams.bottomMargin;

        viewPartSlaveLayoutParams = (LayoutParams) viewPartSlave.getLayoutParams();
        viewPartSlaveWidth = viewPartSlave.getMeasuredWidth() + viewPartSlaveLayoutParams.leftMargin + viewPartSlaveLayoutParams.rightMargin;
        viewPartSlaveHeight = viewPartSlave.getMeasuredHeight() + viewPartSlaveLayoutParams.topMargin + viewPartSlaveLayoutParams.bottomMargin;

        int viewPartMainLineCount = viewPartMain.getLineCount();
        float viewPartMainLastLineWitdh = viewPartMainLineCount > 0 ? viewPartMain.getLayout().getLineWidth(viewPartMainLineCount - 1) : 0;

        widthSize = getPaddingLeft() + getPaddingRight();
        heightSize = getPaddingTop() + getPaddingBottom();

        if (viewPartMainLineCount > 1 && !(viewPartMainLastLineWitdh + viewPartSlaveWidth >= viewPartMain.getMeasuredWidth())) {
            widthSize += viewPartMainWidth;
            heightSize += viewPartMainHeight;
        } else if (viewPartMainLineCount > 1 && (viewPartMainLastLineWitdh + viewPartSlaveWidth >= availableWidth)) {
            widthSize += viewPartMainWidth;
            heightSize += viewPartMainHeight + viewPartSlaveHeight;
        } else if (viewPartMainLineCount == 1 && (viewPartMainWidth + viewPartSlaveWidth >= availableWidth)) {
            widthSize += viewPartMain.getMeasuredWidth();
            heightSize += viewPartMainHeight + viewPartSlaveHeight;
        } else {
            widthSize += viewPartMainWidth + viewPartSlaveWidth;
            heightSize += viewPartMainHeight;
        }

        this.setMeasuredDimension(widthSize, heightSize);
        super.onMeasure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        if (viewPartMain == null || viewPartSlave == null) {
            return;
        }

        viewPartMain.layout(
                getPaddingLeft(),
                getPaddingTop(),
                viewPartMain.getWidth() + getPaddingLeft(),
                viewPartMain.getHeight() + getPaddingTop());

        viewPartSlave.layout(
                right - left - viewPartSlaveWidth - getPaddingRight(),
                bottom - top - getPaddingBottom() - viewPartSlaveHeight,
                right - left - getPaddingRight(),
                bottom - top - getPaddingBottom());
    }
}

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ImFlexboxLayout">
        <attr name="viewPartMain" format="reference"></attr>
        <attr name="viewPartSlave" format="reference"></attr>
    </declare-styleable>

</resources>

Example right ballon layout (balloon.xml)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:baselineAligned="false"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="right|center_vertical"
        android:layout_weight="1"
        android:gravity="right">

        <tr.com.client.ImFlexboxLayout
            android:id="@+id/msg_layout"
            style="@style/BalloonMessageLayoutRight"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right|bottom"
            android:gravity="left|center_vertical"
            app:viewPartMain="@+id/chat_msg"
            app:viewPartSlave="@+id/lytStatusContainer">

            <TextView
                android:id="@+id/chat_msg"
                style="@style/BalloonMessageRightTextItem"
                android:layout_width="wrap_content"
                android:layout_gravity="right|bottom"
                android:focusableInTouchMode="false"
                android:gravity="left|top"
                android:text="hjjfg" />

            <LinearLayout
                android:id="@+id/lytStatusContainer"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="5dp"
                android:gravity="right"
                android:minWidth="60dp">

                <TextView
                    android:id="@+id/date_view"
                    style="@style/BallonMessageTimeText"
                    android:layout_alignParentRight="true"
                    android:layout_gravity="right|bottom"
                    android:layout_marginRight="5dp"
                    android:gravity="right"
                    android:maxLines="1" />

                <include
                    android:id="@+id/lytStatus"
                    layout="@layout/layout_im_message_status"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="bottom"
                    android:layout_marginRight="5dp"
                    android:minWidth="40dp" />

            </LinearLayout>

        </tr.com.client.ImFlexboxLayout>
    </LinearLayout>
</LinearLayout>

You can modify layout xml and some sections related your scenario.

There are 2 important point: you must define in layout xml "viewPartMain", "viewPartSlave" attributes. Because the code will decide measure via your main(chat textview) and slave(datetime text view) elements.

I wish have good days. Greets.

Solution 2 - Android

Adding HTML non-breaking spaces did the trick. Tested the code on most devices and working absolutely fine. Maybe whatsapp is also doing the same thing. Below is the chat code:

See images below to see it working.

XML Design:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rel_layout_left"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_below="@+id/txtDate"
    android:visibility="visible"
    android:orientation="vertical"
   >

    <TextView
        android:id="@+id/lblMsgFrom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:text="kfhdjbh"
        android:textColor="@color/lblFromName"
        android:textSize="12dp"
        android:textStyle="italic"
        android:visibility="gone" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/lblMsgFrom"
        android:layout_marginRight="-5dp"
        android:src="@drawable/bubble_corner" />

    <FrameLayout
        android:orientation="horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:background="@drawable/bg_msg_from"
        android:layout_toRightOf="@+id/imageView">

        <TextView
            android:id="@+id/txtTimeFrom"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingRight="@dimen/d5"
            android:text="Time"
            android:textColor="@android:color/darker_gray"
            android:layout_gravity="bottom|right"
            android:padding="4dp"
            android:textSize="10dp"
            android:textStyle="italic"
            android:layout_below="@+id/txtMsgFrom"
            android:layout_alignRight="@+id/txtMsgFrom"
            android:layout_alignEnd="@+id/txtMsgFrom" />

       <TextView
            android:id="@+id/txtMsgFrom"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignTop="@+id/imageView"
            android:layout_toEndOf="@+id/lblMsgFrom"
            android:layout_toRightOf="@+id/imageView"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="5dp"
            android:paddingBottom="5dp"
            android:text="kdfjhgjfhf"
            android:textColor="@color/black"
            android:textSize="16dp"
            android:layout_alignParentLeft="true"
            android:layout_marginLeft="0dp"
            android:layout_alignParentTop="true"
            android:layout_marginTop="0dp"
            android:layout_gravity="left|center_vertical" />
    </FrameLayout>

</RelativeLayout>

##Code: bg_msg_from.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_from" >-->
    <solid android:color="@android:color/white" >
    </solid>

    <corners android:radius="@dimen/d5" >
    </corners>

</shape>

##** File: bubble_corner.png** Right Arrow Image enter image description here

enter image description here enter image description here enter image description here

txtMsgFrom.setText(Html.fromHtml(convertToHtml(txtMsgFrom.getText().toString()) + " &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;")); // 10 spaces

Solution 3 - Android

Its easier with Unicode from here.

So with this you can archive the Unicode format

 new TextView("Hello\u00A0world");

better than HTML string.

source: https://stackoverflow.com/a/6565049

Solution 4 - Android

I propose another solution

public static final String TAG = "MainActivity";
    private TextView mText;
    private RelativeLayout relativeLayout;
    private Boolean mFirstTime = true;
    private static final int WIDH_HOUR = 382;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        final int width = getScreensWidh();

        mText = (TextView) findViewById(R.id.activity_main_text);
        relativeLayout = (RelativeLayout) findViewById(R.id.activity_main_relative);

        mText.setText("aaaaa dfsafsa afdsfa fdsafas adfas fdasf adfsa dsa aaaa dfsafsa afdsfa fdsafas adfas fdasf adfsa");

        ViewTreeObserver vto = mText.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (mFirstTime) {
                    Layout layout = mText.getLayout();
                    int lines = layout.getLineCount();

                    int offset = layout.layout.getLineWidth(lines - 1);
                    int freeSpace = width - offset;

                    TextView hour = new TextView(MainActivity.this);
                    hour.setText("12:20");
                    RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
                    params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
                    if (freeSpace > WIDH_HOUR) {
                        params.addRule(RelativeLayout.ALIGN_BOTTOM, R.id.activity_main_text);
                    } else {
                        params.addRule(RelativeLayout.BELOW, R.id.activity_main_text);
                    }
                    hour.setLayoutParams(params);
                    relativeLayout.addView(hour);
                    Log.d(TAG, String.valueOf(freeSpace));
                    mFirstTime = false;
                }

            }
        });


    }

    public int getScreensWidh() {
        Display display = getWindowManager().getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        return size.x;

    }

Two Public Methods

Return the number of lines of text in this layout.

Gets the unsigned horizontal extent of the specified line, including leading margin indent and trailing whitespace.

Solution 5 - Android

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.

Solution 6 - Android

Based on @Sinan Ergin answer but slightly improved:

  • less layouts
  • simplified gravity + layout_gravity usage
  • left + right layout files
  • FrameLayout instead of RelativeLayout
  • converted to Kotlin
  • no attrs.xml & style references
/**
 * 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>

view_chat_entry_status.xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:layout_width="wrap_content"
    tools:layout_height="wrap_content"
    tools:parentTag="android.widget.FrameLayout"
    >
  <ImageView
      android:contentDescription="@null"
      android:id="@+id/statusImageView"
      android:layout_width="15dp"
      android:layout_height="15dp"
      app:srcCompat="@drawable/ic_check_one"/>
</merge>

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):

enter image description here

Solution 7 - Android

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

Solution 8 - Android

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rel_layout_left"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/bubble1"
    android:orientation="vertical">

    <TextView
        android:id="@+id/lblMsgFrom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Person Name or Id"           
        android:visibility="gone" />   

    <TextView
        android:id="@+id/lblMessage_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingBottom="5dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:paddingTop="5dp"
        android:text="Sample \n Sample2 Sample2 Sample2 Sample2 Sample2 Sample2 Sample2 Sample2 Sample2 \n Sample2"
        android:textSize="16dp" />

    <TextView
        android:id="@+id/lblMessage_Time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignEnd="@+id/lblMessage_text"
        android:layout_alignRight="@+id/lblMessage_text"
        android:layout_below="@+id/lblMessage_text"
        android:text="04:50 Am"
        android:textColor="@android:color/darker_gray"
        android:textSize="10dp"
        android:textStyle="italic" />    
       
</RelativeLayout>

Solution 9 - Android

I suggest other solution. If you know max bubble width and time width, then you can precalculate how to place your views.

Layout:

<RelativeLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="12sp"
        tools:text="This is text"/>

    <TextView
        android:id="@+id/time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="10sp"
        tools:text="10:10"/>
</RelativeLayout>

Code:

fun setTextAndTime(textView: TextView, timeView: TextView, text: String, time: String) {
    // screen width - offset from bubble
    val maxWidth: Int = Resources.getSystem().displayMetrics.widthPixels - context.resources.getDimensionPixelSize(R.dimen.bubble_offset)
    val timeWidth: Int = getTextWidth(time, 10f)

    textView.text = text
    timeView.text = time

    textView.measure(makeMeasureSpec(maxWidth, EXACTLY), makeMeasureSpec(0, UNSPECIFIED))
    val offset = textView.layout.getLineWidth(textView.layout.lineCount - 1)
    val freeSpace = maxWidth - offset

    val moveTimestampBelow = freeSpace < timeWidth
    val multilineContent = textView.layout.lineCount > 1

    val params = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT)
    when {
        moveTimestampBelow -> params.apply {
            addRule(RelativeLayout.BELOW, textView.id)
            addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
        }
        multilineContent -> params.apply {
            params.addRule(RelativeLayout.ALIGN_BOTTOM, textView.id)
            addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
        }
        else -> params.apply {
            params.addRule(RelativeLayout.ALIGN_BOTTOM, textView.id)
            addRule(RelativeLayout.END_OF, textView.id)
        }
    }
    timeView.layoutParams = params
}

private fun getTextWidth(text: String, textSizeSp: Float): Int {
    val textPaint = Paint()
    val pxSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, textSizeSp, context.resources.displayMetrics)
    textPaint.textSize = pxSize
    textPaint.style = Paint.Style.FILL
    val result = Rect()
    textPaint.getTextBounds(text, 0, text.length, result)
    return result.width()
}

Solution 10 - Android

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.

Solution 11 - Android

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 enter image description here

Solution 12 - Android

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

enter image description here

enter image description here

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionFrame91View Question on Stackoverflow
Solution 1 - AndroidThe GoatView Answer on Stackoverflow
Solution 2 - AndroidHisham MuneerView Answer on Stackoverflow
Solution 3 - AndroidKelvin WongView Answer on Stackoverflow
Solution 4 - AndroidCabezasView Answer on Stackoverflow
Solution 5 - AndroidRahul ShuklaView Answer on Stackoverflow
Solution 6 - AndroidNiklasView Answer on Stackoverflow
Solution 7 - AndroidXavier FalempinView Answer on Stackoverflow
Solution 8 - AndroidluckyView Answer on Stackoverflow
Solution 9 - AndroidnortherngirlView Answer on Stackoverflow
Solution 10 - Androidrusted brainView Answer on Stackoverflow
Solution 11 - AndroidPrakash JangirView Answer on Stackoverflow
Solution 12 - AndroidibyteView Answer on Stackoverflow