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

后端 未结 12 967
执笔经年
执笔经年 2020-11-28 03:18

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
相关标签:
12条回答
  • 2020-11-28 04:00

    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.

    0 讨论(0)
  • 2020-11-28 04:07

    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>
    

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

    0 讨论(0)
  • 2020-11-28 04:08

    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.

    0 讨论(0)
  • 2020-11-28 04:11

    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

    0 讨论(0)
  • 2020-11-28 04:12

    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

    0 讨论(0)
  • 2020-11-28 04:13

    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

    0 讨论(0)
提交回复
热议问题