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

Solution 1:

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

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

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