introduced sroll to bottom button. based on #2777 by @harshitbansal05

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/entities/Conversation.java   | 19 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java | 76 
src/main/res/drawable/ic_scroll_to_end_black.xml                  |  7 
src/main/res/drawable/ic_scroll_to_end_white.xml                  | 36 
src/main/res/layout/fragment_conversation.xml                     | 26 
src/main/res/values/attrs.xml                                     |  1 
src/main/res/values/themes.xml                                    |  2 
7 files changed, 159 insertions(+), 8 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -925,6 +925,25 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
 				&& sentMessagesCount() == 0;
 	}
 
+	public int getReceivedMessagesCountSinceUuid(String uuid) {
+		if (uuid == null) {
+			return  0;
+		}
+		int count = 0;
+		synchronized (this.messages) {
+			for (int i = messages.size() - 1; i >= 0; i--) {
+				final Message message = messages.get(i);
+				if (uuid.equals(message.getUuid())) {
+					return count;
+				}
+				if (message.getStatus() <= Message.STATUS_RECEIVED) {
+					++count;
+				}
+			}
+		}
+		return 0;
+	}
+
 	public interface OnMessageFound {
 		void onMessageFound(final Message message);
 	}

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -128,14 +128,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 	public static final String STATE_CONVERSATION_UUID = ConversationFragment.class.getName() + ".uuid";
 	public static final String STATE_SCROLL_POSITION = ConversationFragment.class.getName() + ".scroll_position";
 	public static final String STATE_PHOTO_URI = ConversationFragment.class.getName() + ".take_photo_uri";
-
+	private static final String STATE_LAST_MESSAGE_UUID = "state_last_message_uuid";
 
 	final protected List<Message> messageList = new ArrayList<>();
+	private String lastMessageUuid = null;
 	private final PendingItem<ActivityResult> postponedActivityResult = new PendingItem<>();
 	private final PendingItem<String> pendingConversationsUuid = new PendingItem<>();
 	private final PendingItem<Bundle> pendingExtras = new PendingItem<>();
 	private final PendingItem<Uri> pendingTakePhotoUri = new PendingItem<>();
 	private final PendingItem<ScrollState> pendingScrollState = new PendingItem<>();
+	private final PendingItem<String> pendingLastMessageUuid = new PendingItem<>();
 	private final PendingItem<Message> pendingMessage = new PendingItem<>();
 	public Uri mPendingEditorContent = null;
 	protected MessageAdapter messageListAdapter;
@@ -183,16 +185,37 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 			});
 		}
 	};
+
+	private void toggleScrollDownButton() {
+		toggleScrollDownButton(binding.messagesView, binding.messagesView.getCount());
+	}
+
+	private void toggleScrollDownButton(AbsListView listView, int count) {
+		if (listView.getLastVisiblePosition() < count - 5) {
+			binding.scrollToBottomButton.setEnabled(true);
+			binding.scrollToBottomButton.setVisibility(View.VISIBLE);
+			if (lastMessageUuid == null) {
+				lastMessageUuid = conversation.getLatestMessage().getUuid();
+			}
+			if (conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid) > 0) {
+				binding.unreadCountCustomView.setVisibility(View.VISIBLE);
+			}
+		} else if (scrolledToBottom(listView)){
+			lastMessageUuid = null;
+			hideUnreadMessagesCount();
+		}
+	}
+
 	private OnScrollListener mOnScrollListener = new OnScrollListener() {
 
 		@Override
 		public void onScrollStateChanged(AbsListView view, int scrollState) {
-			// TODO Auto-generated method stub
 
 		}
 
 		@Override
 		public void onScroll(final AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+			toggleScrollDownButton(view, totalItemCount);
 			synchronized (ConversationFragment.this.messageList) {
 				if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true, false) && messageList.size() > 0) {
 					long timestamp;
@@ -361,6 +384,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 			return false;
 		}
 	};
+	private OnClickListener mScrollButtonListener = new OnClickListener() {
+
+ 		@Override
+ 		public void onClick(View v) {
+ 			stopScrolling();
+ 			setSelection(binding.messagesView.getCount() - 1);
+		}
+ 	};
 	private OnClickListener mSendButtonListener = new OnClickListener() {
 
 		@Override
@@ -523,10 +554,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 		}
 	}
 
-	private void setScrollPosition(ScrollState scrollPosition) {
+	private void setScrollPosition(ScrollState scrollPosition, String lastMessageUuid) {
 		if (scrollPosition != null) {
+
+			this.lastMessageUuid = lastMessageUuid;
+			if (lastMessageUuid != null) {
+				binding.unreadCountCustomView.setUnreadCount(conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid));
+			}
 			//TODO maybe this needs a 'post'
 			this.binding.messagesView.setSelectionFromTop(scrollPosition.position, scrollPosition.offset);
+			toggleScrollDownButton();
 		}
 	}
 
@@ -868,6 +905,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 
 		binding.textSendButton.setOnClickListener(this.mSendButtonListener);
 
+		binding.scrollToBottomButton.setOnClickListener(this.mScrollButtonListener);
 		binding.messagesView.setOnScrollListener(mOnScrollListener);
 		binding.messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
 		messageListAdapter = new MessageAdapter((XmppActivity) getActivity(), this.messageList);
@@ -1687,6 +1725,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 		super.onSaveInstanceState(outState);
 		if (conversation != null) {
 			outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid());
+			outState.putString(STATE_LAST_MESSAGE_UUID, lastMessageUuid);
 			final Uri uri = pendingTakePhotoUri.peek();
 			if (uri != null) {
 				outState.putString(STATE_PHOTO_URI, uri.toString());
@@ -1705,6 +1744,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 			return;
 		}
 		String uuid = savedInstanceState.getString(STATE_CONVERSATION_UUID);
+		pendingLastMessageUuid.push(savedInstanceState.getString(STATE_LAST_MESSAGE_UUID, null));
 		if (uuid != null) {
 			this.pendingConversationsUuid.push(uuid);
 			String takePhotoUri = savedInstanceState.getString(STATE_PHOTO_URI);
@@ -1781,6 +1821,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 			this.reInitRequiredOnStart = true;
 			pendingExtras.push(extras);
 		}
+		updateUnreadMessagesCount();
 	}
 
 	private void reInit(Conversation conversation) {
@@ -1822,7 +1863,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 		messageListAdapter.updatePreferences();
 		refresh(false);
 		this.conversation.messagesLoaded.set(true);
-
 		Log.d(Config.LOGTAG, "scrolledToBottomAndNoPending=" + Boolean.toString(scrolledToBottomAndNoPending));
 
 		if (hasExtras || scrolledToBottomAndNoPending) {
@@ -1847,6 +1887,20 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 		return true;
 	}
 
+	private void updateUnreadMessagesCount() {
+		lastMessageUuid = null;
+		hideUnreadMessagesCount();
+	}
+
+	private void hideUnreadMessagesCount() {
+		if (this.binding == null) {
+			return;
+		}
+		this.binding.scrollToBottomButton.setEnabled(false);
+		this.binding.scrollToBottomButton.setVisibility(View.GONE);
+		this.binding.unreadCountCustomView.setVisibility(View.GONE);
+	}
+
 	private void setSelection(int pos) {
 		this.binding.messagesView.setSelection(pos);
 		this.binding.messagesView.post(() -> this.binding.messagesView.setSelection(pos));
@@ -1856,8 +1910,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 		if (this.binding == null) {
 			return false;
 		}
-		final ListView listView = this.binding.messagesView;
-		if (listView.getLastVisiblePosition() == listView.getAdapter().getCount() - 1) {
+		return scrolledToBottom(this.binding.messagesView);
+	}
+
+	private static boolean scrolledToBottom(AbsListView listView) {
+		if (listView.getLastVisiblePosition() == listView.getCount() - 1) {
 			final View lastChild = listView.getChildAt(listView.getChildCount() - 1);
 			return lastChild != null && lastChild.getBottom() <= listView.getHeight();
 		} else {
@@ -2009,6 +2066,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 				conversation.populateWithMessages(this.messageList);
 				updateSnackBar(conversation);
 				updateStatusMessages();
+				if (conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid) != 0) {
+					binding.unreadCountCustomView.setVisibility(View.VISIBLE);
+					binding.unreadCountCustomView.setUnreadCount(conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid));
+				}
 				this.messageListAdapter.notifyDataSetChanged();
 				updateChatMsgHint();
 				if (notifyConversationRead && activity != null) {
@@ -2474,8 +2535,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 			}
 			reInit(conversation);
 			ScrollState scrollState = pendingScrollState.pop();
+			String lastMessageUuid = pendingLastMessageUuid.pop();
 			if (scrollState != null) {
-				setScrollPosition(scrollState);
+				setScrollPosition(scrollState, lastMessageUuid);
 			}
 		} else {
 			if (!activity.xmppConnectionService.isConversationStillOpen(conversation)) {

src/main/res/drawable/ic_scroll_to_end_black.xml 🔗

@@ -0,0 +1,7 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path android:fillColor="#8a000000" android:pathData="M16.59,5.59L18,7L12,13L6,7L7.41,5.59L12,10.17L16.59,5.59M16.59,11.59L18,13L12,19L6,13L7.41,11.59L12,16.17L16.59,11.59Z" />
+</vector>

src/main/res/drawable/ic_scroll_to_end_white.xml 🔗

@@ -0,0 +1,36 @@
+<!--
+  ~ Copyright (c) 2018, Daniel Gultsch All rights reserved.
+  ~
+  ~ Redistribution and use in source and binary forms, with or without modification,
+  ~ are permitted provided that the following conditions are met:
+  ~
+  ~ 1. Redistributions of source code must retain the above copyright notice, this
+  ~ list of conditions and the following disclaimer.
+  ~
+  ~ 2. Redistributions in binary form must reproduce the above copyright notice,
+  ~ this list of conditions and the following disclaimer in the documentation and/or
+  ~ other materials provided with the distribution.
+  ~
+  ~ 3. Neither the name of the copyright holder nor the names of its contributors
+  ~ may be used to endorse or promote products derived from this software without
+  ~ specific prior written permission.
+  ~
+  ~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+  ~ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+  ~ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+  ~ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+  ~ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+  ~ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+  ~ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+  ~ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+  ~ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+  ~ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path android:fillColor="#b2ffffff" android:pathData="M16.59,5.59L18,7L12,13L6,7L7.41,5.59L12,10.17L16.59,5.59M16.59,11.59L18,13L12,19L6,13L7.41,11.59L12,16.17L16.59,11.59Z" />
+</vector>

src/main/res/layout/fragment_conversation.xml 🔗

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android">
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
 
     <RelativeLayout
         xmlns:tools="http://schemas.android.com/tools"
@@ -23,6 +24,29 @@
             tools:listitem="@layout/message_sent">
         </ListView>
 
+        <android.support.design.widget.FloatingActionButton
+            android:id="@+id/scroll_to_bottom_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="12dp"
+            android:layout_alignParentEnd="true"
+            android:layout_alignBottom="@+id/messages_view"
+            android:alpha="0.85"
+            app:backgroundTint="?attr/color_background_primary"
+            android:src="?attr/icon_scroll_down"
+            app:fabSize="mini"
+            android:visibility="gone"/>
+
+        <eu.siacs.conversations.ui.widget.UnreadCountCustomView
+            android:id="@+id/unread_count_custom_view"
+            android:layout_width="?attr/IconSize"
+            android:layout_height="?attr/IconSize"
+            android:layout_alignTop="@+id/scroll_to_bottom_button"
+            android:layout_alignEnd="@+id/scroll_to_bottom_button"
+            android:elevation="8dp"
+            android:visibility="gone"
+            app:backgroundColor="?attr/unread_count" />
+
         <RelativeLayout
             android:id="@+id/textsend"
             android:layout_width="fill_parent"

src/main/res/values/attrs.xml 🔗

@@ -58,6 +58,7 @@
     <attr name="icon_import_export" format="reference"/>
     <attr name="icon_scan_qr_code" format="reference"/>
     <attr name="icon_enable_undecided_device" format="reference"/>
+    <attr name="icon_scroll_down" format="reference"/>
 
     <attr name="icon_notifications" format="reference"/>
     <attr name="icon_notifications_off" format="reference"/>

src/main/res/values/themes.xml 🔗

@@ -70,6 +70,7 @@
         <item type="reference" name="icon_import_export">@drawable/ic_import_export_white_24dp</item>
         <item type="reference" name="icon_share">@drawable/ic_share_white_24dp</item>
         <item type="reference" name="icon_scan_qr_code">@drawable/ic_qr_code_scan_white_24dp</item>
+        <item type="reference" name="icon_scroll_down">@drawable/ic_scroll_to_end_black</item>
 
         <item type="reference" name="icon_notifications">@drawable/ic_notifications_black_24dp</item>
         <item type="reference" name="icon_notifications_off">@drawable/ic_notifications_off_black_24dp</item>
@@ -147,6 +148,7 @@
         <item type="reference" name="icon_import_export">@drawable/ic_import_export_white_24dp</item>
         <item type="reference" name="icon_share">@drawable/ic_share_white_24dp</item>
         <item type="reference" name="icon_scan_qr_code">@drawable/ic_qr_code_scan_white_24dp</item>
+        <item type="reference" name="icon_scroll_down">@drawable/ic_scroll_to_end_white</item>
 
         <item type="reference" name="icon_notifications">@drawable/ic_notifications_white_24dp</item>
         <item type="reference" name="icon_notifications_off">@drawable/ic_notifications_off_white_24dp</item>