6

I am working on a chat application but I am struggling to figure out how to display the calendar date on the top of chat messages; for example, something like this:

Chat Messages

Another image with timestamps:

Chat Messages with Timestamps

As you see in the example, the date is displayed on top of a fresh new batch of text messages. I would like to do the same, just for a batch of messages related to a specific date. Say if I have messages for October 19, it shows October 19 on top followed by messages for October 20 and so on... Here's the working sample code similar to mine:

http://www.codeproject.com/Tips/897826/Designing-Android-Chat-Bubble-Chat-UI

The construction of the code is same as mine except for the date being displayed on top which is something I am stuck on. I have my timestamps showing for every message, I just want to display the date in the format "Monday, October 19, 2015" for the batch of messages; just once on top, from October 19, and likewise calendar dates for past and future messages, as shown in the image. Any clues? Thanks!

Peter David Carter
  • 2,548
  • 8
  • 25
  • 44
  • I have time showing for each message as of now. I want to do 2 things , 1) club the messages with same time together, like if there are 3-4 messages all sent at 7:54, it will only show 7:54 at the top for the first message for the rest no time will be shown until the minute changes to 7:55 and second thing would be I would like to display the day of the conversation just once on top, like if the messages were all sent on oct 10 then it shows oct 10 on top followed by messages from oct 11 and so on if any, like group them as per the day, similar behavior to standard android native text message –  Oct 22 '15 at 04:36
  • could you please post the layout you are using for the message ? – Blackbelt Oct 23 '15 at 10:45
  • it's in the sample code, its called list_tem_chat_message.xml , it's a very small project ,just 3 java files and 2 layout files –  Oct 23 '15 at 20:05

7 Answers7

21

As far as I understand you want to show time/date only for certain group of messages and not for each message. So here is how to do that. Precondition: I assume each message item has time stamp based on which we will do our grouping Idea: we will need each list item to have timeview:TextView element and we will show and hide that element based on it's position and TS (time stamp) Example:

item.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="match_parent"
              android:background="@color/white"
              android:orientation="vertical"
              android:padding="@dimen/padding_small">

    <TextView
        android:id="@+id/timeText"
        style="@style/DefaultText"
        android:paddingBottom="@dimen/padding_small"
        android:paddingRight="@dimen/padding_small"
        android:paddingTop="@dimen/padding_small"
        android:text="@string/hello_world"
        android:textSize="@dimen/font_size_small_10"/>

    <TextView
        android:id="@+id/textView"
        style="@style/DefaultText"
        android:layout_width="wrap_content"
        android:background="@drawable/bg_gray_rounded"
        android:paddingBottom="@dimen/padding_extra_small"
        android:paddingLeft="@dimen/padding_small"
        android:paddingRight="@dimen/padding_small"
        android:paddingTop="@dimen/padding_extra_small"
        android:textColor="@color/gray"
        android:textSize="@dimen/font_size_md"/>

</LinearLayout>

ChatRecyclerAdapter.java

public class ChatEGRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    public static class TextViewHolder extends RecyclerView.ViewHolder {
        public TextView textView;
        public TextView timeText;
        public TextViewHolder(View v) {
            super(v);
            timeText = (TextView) v.findViewById(R.id.timeText);
            textView = (TextView) v.findViewById(R.id.textView);
        }
    }

    private final List<Message> messages;


    public ChatEGRecyclerAdapter(List<Message> messages) {
        this.messages = messages;
    }


    // Create new views (invoked by the layout manager)
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item, parent, false);
        return new TextViewHolder(v);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
        final Message m = messages.get(position);
        final Context context = viewHolder.itemView.getContext();

        TextViewHolder holder = (TextViewHolder)viewHolder;
        holder.textView.setVisibility(View.VISIBLE);
        holder.textView.setText(m.getText());

        long previousTs = 0;
        if(position>1){
            Message pm = messages.get(position-1);
            previousTs = pm.getTimeStamp();
        }
        setTimeTextVisibility(m.getTimeStamp(), previousTs, holder.timeText);
    }

    private void setTimeTextVisibility(long ts1, long ts2, TextView timeText){

        if(ts2==0){
            timeText.setVisibility(View.VISIBLE);
            timeText.setText(Utils.formatDayTimeHtml(ts1));
        }else {
            Calendar cal1 = Calendar.getInstance();
            Calendar cal2 = Calendar.getInstance();
            cal1.setTimeInMillis(ts1);
            cal2.setTimeInMillis(ts2);

            boolean sameMonth = cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
                    cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH);

            if(sameMonth){
                timeText.setVisibility(View.GONE);
                timeText.setText("");
            }else {
                timeText.setVisibility(View.VISIBLE);
                timeText.setText(Utils.formatDayTimeHtml(ts2));
            }

        }
    }


    @Override
    public int getItemCount() {
        return messages.size();
    }

}

what left is to create your RecylcerView and give it this adapter

Vilen
  • 5,061
  • 3
  • 28
  • 39
  • do i actually need to use the recyclerview? I have the code sample attached, based on that, does your code sample work for a normal listview when you integrate it? the parameters used for date are txtInfo and for messages are txtMessage in ChatAdapter.java –  Oct 22 '15 at 10:43
  • this approach will work for listview as well, but why do you need it ? ListView is about to be deprecated because of RecyclerView which is more simple and flexible component and it automatically implements ViewHolder pattern in order to get rid of android junk. anycase no mater what view are you using, trick here is to compare timestamp of message and accordingly show and hide date text view. – Vilen Oct 22 '15 at 10:48
  • will it work for showing the calendar day on top, JUST ONCE ,for the messages belonging to the same day? Say, if I have a conversation with someone on October 8, it should show October 8 ,2015 on top just once for the entire list of messages sent that day. The next time I see the calendar day on top is the following day when I had the conversation irrespective of whether its the next day or 10 days later. –  Oct 22 '15 at 11:06
  • I am not quite sure what you mean by "JUST ONCE", once for each day or no matter how many days conversation are there only show last day ? – Vilen Oct 22 '15 at 13:36
  • once for each day, at the top –  Oct 22 '15 at 13:40
  • yes you only need to modify this line " cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH);" to " cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);" so it will show date time for each day , meaning if you have 4 days of conversation you will have 4 date text – Vilen Oct 22 '15 at 13:45
  • i like that logic ,its smart, let me make some adjustments and get back to you, i am planning a different approach but with similar logic, if both these instances work you are the winner! if not, i will let you know what issues i face, should be a simpler approach i hope –  Oct 22 '15 at 14:13
  • what's utils in setTimeTextVisibility ? how do i reference it –  Oct 22 '15 at 17:06
  • please look at code it is a method in ChatEGRecyclerAdapter class which decides whether to show date text or not – Vilen Oct 22 '15 at 17:57
  • I am getting errors on Utils. how is it referenced? what do i have to import for that? –  Oct 22 '15 at 18:07
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/93100/discussion-between-vilen-melkumyan-and-justice-bauer). – Vilen Oct 22 '15 at 18:09
2

Vilen answer is right. I am just improving perfomance. Instead of doing view gone and visible for every view, we can add a text view programmatically when condition true.

<?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:background="@android:color/white"
    android:orientation="vertical"
    android:padding="10dp">

    <LinearLayout
        android:id="@+id/timeTextLayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/bg_gray_rounded"
        android:paddingBottom="@dimen/padding_extra_small"
        android:paddingLeft="@dimen/padding_small"
        android:paddingRight="@dimen/padding_small"
        android:paddingTop="@dimen/padding_extra_small"
        android:textColor="@color/gray"
        android:textSize="@dimen/font_size_md" />

</LinearLayout>

ChatAdapter.java

public class ChatEGRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    public static class TextViewHolder extends RecyclerView.ViewHolder {
        public TextView textView;
        public LinearLayout timeText;
        public TextViewHolder(View v) {
            super(v);
            timeText = (LinearLayout) v.findViewById(R.id.timeText);
            textView = (TextView) v.findViewById(R.id.textView);
        }
    }

    private final List<Message> messages;


    public ChatEGRecyclerAdapter(List<Message> messages) {
        this.messages = messages;
    }


    // Create new views (invoked by the layout manager)
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item, parent, false);
        return new TextViewHolder(v);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
        final Message m = messages.get(position);
        final Context context = viewHolder.itemView.getContext();

        TextViewHolder holder = (TextViewHolder)viewHolder;
        holder.textView.setVisibility(View.VISIBLE);
        holder.textView.setText(m.getText());

        //Group by Date
        long previousTs = 0;
        if(position >= 1){
            Message previousMessage = messages.get(position-1);
            previousTs = Long.parseLong(previousMessage.getSentTime());
        }
        Calendar cal1 = Calendar.getInstance();
        Calendar cal2 = Calendar.getInstance();
        cal1.setTimeInMillis(Long.parseLong(messages.get(position).getSentTime())*1000);
        cal2.setTimeInMillis(previousTs*1000);
        boolean sameDay = cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
                cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
        if (!sameDay) {
            TextView dateView = new TextView(context);
            dateView.setText(messages.get(position).getSentTime());
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT);
            params.gravity = Gravity.CENTER_HORIZONTAL;
            dateView.setLayoutParams(params);
            holder.timeText.addView(dateView);
        }
    }


    @Override
    public int getItemCount() {
        return messages.size();
    }

}

Also time formatting and checking for every message is expensive. To minimize this code execution actually we can compare the time stamp while sending the message. Get the time stamp of last message and compare with current message. If greater than 24 hrs then send to the server with isNewGroup:true. Then populating adapter can done without time comparison like below.

@Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
        final Message m = messages.get(position);
        final Context context = viewHolder.itemView.getContext();

        TextViewHolder holder = (TextViewHolder)viewHolder;
        holder.textView.setVisibility(View.VISIBLE);
        holder.textView.setText(m.getText());

        if (messages.get(position).getIsNewGroup()) {
            TextView dateView = new TextView(context);
            dateView.setText(messages.get(position).getSentTime());
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT);
            params.gravity = Gravity.CENTER_HORIZONTAL;
            dateView.setLayoutParams(params);
            holder.timeText.addView(dateView);
        }
    }  
  • Don't forget to support recycling by removing the view for not new group items. And also not adding twice.. – Shirane85 Nov 01 '18 at 06:56
0

Just create a viewgroup of messages within that date. Or I would recommend a structure like this:

ListView
 - ListViewItem1
    - (TextView with date)
    - ListViewOfMsgsForThatDate
      a) Message1ForThisDate
      b) Message2ForThisDate
- ListViewItem2
   - (TextView with date)
   - ListViewOfMsgsForThatDate
      a) Message1ForThisDate
      b) Message2ForThisDate
MetaSnarf
  • 5,857
  • 3
  • 25
  • 41
  • do you have a sample code for the same structure? it would be useful –  Oct 20 '15 at 01:38
0

Ok; this is a solution that will give you an idea to build upon. It will solve your date issues and gives you a hint on how to solve how you display your times. This kind of program involves a lot of work to make it viable as an app. You need to take into account timezones, where you store the messages and so on.

Now stack overflow does not serve the purpose of designing programs for people, but to try and help steer people into the right direction.

For the purposes of answering your question; I am covering the basics, I'm not giving you the code ready to run, you will need to do some of the work :)

This could be solved in numerous ways I have chosen the following:

Using shared preferences to store the latest date time. I considered accessing the chat message arrays, but figured this may be simpler. I chose shared preferences so that the date of the last message would be retained when the app is closed and reopened. (Eventually the chat messages would need to be retained in a database [SQLite] so that the messages will still be there when the app is closed and opened). Please see my comment at the end.

public class ChatActivity extends ActionBarActivity {

    public static final String MyPREF = "MyPrefs" ;
    public static final String DATE_KEY = "DateKey" ;

    SharedPreferences prefs;
    SharedPreferences.Editor editor;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chat);
        initControls();
        pref = getSharedPreferences(MyPREF, MODE_PRIVATE);

        // Check that the shared preferences has a value
        if(contains(DATE_KEY)){
            editor = prefs.edit();
            // get the current datetime.
            editor.Long(DATE_KEY, date);
            editor.commit();
            // set the text of the textview android:id="@+id/date"
            // with the current formatted date.
        }

    }

In your onclick listener, you test that the last stored date is todays date, if it's not, it is converted into a chatmessage, so it's value can be added to the array and displayed. You will have to tweak how you format these message types, and you can even add a unique id for these date messages and test for that, or test for no id.. there's many solutions.

sendBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        /.../
        }
        ChatMessage chatMessage = new ChatMessage();
        long LastDate = pref.getLong(DATE_KEY, date);
        // create an empty chat message with only a date.
        // check if the last used date is not today's date.
        // if it's not today's date, save it and display it as
        // an archived bubble.
        if(!LastDate.isToday()){

            ChatMessage dateholder = new ChatMessage();
            // You'll need to format the date
            dateholder.setDate(Formatted LastDate);
            // set/change the text of the textview android:id="@+id/date"
            // with the current formatted date.

        }
        // put the current date time into your preferences.
        editor = prefs.edit();
        editor.putString(DATE_KEY, date);
        editor.commit();
        /.../
        // this is now setting up the new chat message.
        chatMessage.setDate(DateFormat.getDateTimeInstance().format(new Date()));
        /.../
        displayMessage(chatMessage);
    }
});

xml Ok, now you don't want the date floating at the top of the screen when there's only one message. So group it with the scroll list and then set this relative to the screen layout as the list alone was. A linear layout will do for this, just ensure it's orientation is set to vertical.

Pin these together.

<LinearLayout 
    android:layout_above="@+id/messageEdit"
    android:layout_below="@+id/meLbl"
    android:id="@+id/layout"
    .../orentation:vertical ../

<TextView
        android:id="@+id/date"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:singleLine="true"
        /..//>

<ListView
    android:id="@+id/messagesContainer"
    .../

    ...//>

</LinearLayout>

    <TextView
        android:id="@+id/meLbl"
        /.../
        />

    <TextView
        android:id="@+id/friendLabel"
        /.../
         />

Change display for each message You will need to stop displaying the date time for each message. If you wish to show the time for each message individually, and skip those within the same minute, as you mentioned in the comments, then I've given you the framework, to work out how to test for this and the program logic of whether or not it is displayed.

You need to change this in your:

public View getView(final int position, View convertView, ViewGroup parent)    {

    holder.txtMessage.setText(chatMessage.getMessage());
    // TODO, 
    // do a check if(mins are the same don't post 
    // else post the format without the date)
    // holder.txtInfo.setText(chatMessage.getDate());

Don't forget to apply all necessary imports.

I suggest if you start to use databases that you access the last date from there without shared preferences, or continue to use shared preferences. This becomes more complicated as there are multiple chats being saved, in which case using the database would be preferable.

0

Use this xml which helps to creating layout which you are looking for as per your link - Designing Android Chat Bubble (Chat UI) suggest.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
    android:id="@+id/content"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentRight="true"
    android:orientation="vertical">

    <TextView
        android:id="@+id/txtDate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:layout_gravity="right"
        android:textSize="12sp"
        android:textColor="@android:color/darker_gray" />

    <LinearLayout
        android:id="@+id/contentWithBackground"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:background="@drawable/your_msg_bubble_bg"
        android:paddingLeft="10dp"
        android:paddingBottom="10dp"
        android:orientation="vertical">

        <TextView
            android:id="@+id/txtMessage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@android:color/black"
            android:maxWidth="240dp" />

    </LinearLayout>

</LinearLayout>

enter image description here

I update this code for date texview:-

<TextView
        android:id="@+id/txtDate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:layout_gravity="right"
        android:textSize="12sp"
        android:textColor="@android:color/darker_gray" />
pRaNaY
  • 24,642
  • 24
  • 96
  • 146
0

@Vilen Answer help me for this problem.

By Api respone give data accoring to date and time in diffrent key so it's to easy to manage me in adapter

Api response like......

    {
  "status": "success",
  "unread": "0",
  "data": [
    {
      "from_id": "152000000",
      "to_id": "15500545452",
      "message": "do their Jami hit",
      "profile_image": "1667822677.jpg",
      "addedon": "8:34 am",
      "groupby": "April 2, 2021"
    }
  ],
  
  "message": "Chat History fetch successfully."
}

In Bind ViewHolder

  val dateGroupBy=""
        if (position > 0) {
                    dateGroupBy = list.get(position - 1).groupby
        }
 if (holder is MassageReceiverViewHolder) {
            holder.bind(data, dateGroupBy)
        } else if (holder is MassageSenderViewHolder) {
            holder.bind(data, dateGroupBy)
        }

In Both Viewholder bind Funtion

if (dateGroupBy != data.groupby) {
            binding.tvDateGroup.visibility = View.VISIBLE
            binding.tvDateGroup.text = data.groupby
        }

Bindig is simple your xml. I have done it through Bindig.

Umesh Yadav
  • 1,042
  • 9
  • 17
0

I guess this problem does not need another answer stacking upon already accepted solutions but this might help. Keeping @Vilen suggestions in mind I did:

-> Pushed a date string value to every child in API

-> Compare each one's date in adapter onBindViewHolder()

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, model: Message) {
    val isSameUser: Boolean =
        if (position > 0) model.userId == getItem(position - 1).userId else false
    val isSameDate: Boolean =
        if (position > 0) model.date == getItem(position - 1).date else false

    when (viewHolder) {
        // Based on data I initiated val [viewHolder] with appropriate 
        // enum class member in [getItemViewType()]
        EnumViewHolder.SenderViewHolder ->
            (holder as SenderMessageViewHolder).bindData(model, isSameDate, isSameUser)
        EnumViewHolder.MessageViewHolder ->
            (holder as MessageViewHolder).bindData(model, isSameDate, isSameUser)
        else -> false
    }
}
...
....
inner class MessageViewHolder(private val binding: MessageBinding) : RecyclerView.ViewHolder(binding.root) {
    fun bindData(item: Message,
                 isSameDate: Boolean,
                 isSameUser: Boolean
    ) {
        binding.messageTextTextView.text = item.text
        binding.messageTimeTextView.text = item.time

        if (isSameDate) {
            binding.messageDateTextView.visibility = View.GONE
        } else binding.messageDateTextView.text = item.date
        
        // Omit user profile picture in case of repeated message
        if (isSameUser && isSameDate) {
            binding.messageLinearLayout.setBackgroundResource(R.drawable.message_background)
            binding.userImageImageView.visibility = View.INVISIBLE
        } else {
            // loading image URL to imageView
            ...
        }
    }
}
// I did the same for SenderMessageViewHolder()

This is the result I get: Screenshot

Ronnie
  • 207
  • 4
  • 11