mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-30 13:35:18 +00:00
Make the sticky date header only visible during scroll
// FREEBIE
This commit is contained in:
parent
2e16c6cf41
commit
d46d3b72c8
@ -1,5 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<set xmlns:android="http://schemas.android.com/apk/res/android" >
|
<set xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||||
|
<alpha android:duration="1"
|
||||||
|
android:fromAlpha="0"
|
||||||
|
android:toAlpha="1"/>
|
||||||
|
|
||||||
<translate
|
<translate
|
||||||
android:duration="250"
|
android:duration="250"
|
||||||
android:fromYDelta="-100%"
|
android:fromYDelta="-100%"
|
||||||
|
@ -3,7 +3,24 @@
|
|||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<TextView android:id="@+id/scroll_date_header"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_gravity="center_horizontal|top"
|
||||||
|
android:paddingLeft="6dp"
|
||||||
|
android:paddingRight="6dp"
|
||||||
|
android:paddingTop="3dp"
|
||||||
|
android:paddingBottom="3dp"
|
||||||
|
android:layout_marginTop="3dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:background="?conversation_item_header_background"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:text="March 1, 2015" />
|
||||||
|
|
||||||
<android.support.v7.widget.RecyclerView
|
<android.support.v7.widget.RecyclerView
|
||||||
android:id="@android:id/list"
|
android:id="@android:id/list"
|
||||||
|
@ -124,7 +124,7 @@ public class ContactSelectionListFragment extends Fragment
|
|||||||
isMulti());
|
isMulti());
|
||||||
selectedContacts = adapter.getSelectedContacts();
|
selectedContacts = adapter.getSelectedContacts();
|
||||||
recyclerView.setAdapter(adapter);
|
recyclerView.setAdapter(adapter);
|
||||||
recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, true));
|
recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, true, true));
|
||||||
this.getLoaderManager().initLoader(0, null, this);
|
this.getLoaderManager().initLoader(0, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,13 +105,18 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||||||
|
|
||||||
|
|
||||||
protected static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
protected static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
||||||
private TextView textView;
|
protected TextView textView;
|
||||||
|
|
||||||
public HeaderViewHolder(View itemView) {
|
public HeaderViewHolder(View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
textView = ViewUtil.findById(itemView, R.id.text);
|
textView = ViewUtil.findById(itemView, R.id.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HeaderViewHolder(TextView textView) {
|
||||||
|
super(textView);
|
||||||
|
this.textView = textView;
|
||||||
|
}
|
||||||
|
|
||||||
public void setText(CharSequence text) {
|
public void setText(CharSequence text) {
|
||||||
textView.setText(text);
|
textView.setText(text);
|
||||||
}
|
}
|
||||||
@ -283,6 +288,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getHeaderId(int position) {
|
public long getHeaderId(int position) {
|
||||||
|
if (!isActiveCursor()) return -1;
|
||||||
|
|
||||||
Cursor cursor = getCursorAtPositionOrThrow(position);
|
Cursor cursor = getCursorAtPositionOrThrow(position);
|
||||||
MessageRecord record = getMessageRecord(cursor);
|
MessageRecord record = getMessageRecord(cursor);
|
||||||
|
|
||||||
@ -300,10 +307,5 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||||||
Cursor cursor = getCursorAtPositionOrThrow(position);
|
Cursor cursor = getCursorAtPositionOrThrow(position);
|
||||||
viewHolder.setText(DateUtils.getRelativeDate(getContext(), locale, getMessageRecord(cursor).getDateReceived()));
|
viewHolder.setText(DateUtils.getRelativeDate(getContext(), locale, getMessageRecord(cursor).getDateReceived()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isActive() {
|
|
||||||
return isActiveCursor();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,8 +47,10 @@ import android.view.ViewGroup;
|
|||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationUtils;
|
import android.view.animation.AnimationUtils;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
|
||||||
import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener;
|
import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
@ -94,6 +96,7 @@ public class ConversationFragment extends Fragment
|
|||||||
private View loadMoreView;
|
private View loadMoreView;
|
||||||
private View composeDivider;
|
private View composeDivider;
|
||||||
private View scrollToBottomButton;
|
private View scrollToBottomButton;
|
||||||
|
private TextView scrollDateHeader;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle icicle) {
|
public void onCreate(Bundle icicle) {
|
||||||
@ -108,6 +111,7 @@ public class ConversationFragment extends Fragment
|
|||||||
list = ViewUtil.findById(view, android.R.id.list);
|
list = ViewUtil.findById(view, android.R.id.list);
|
||||||
composeDivider = ViewUtil.findById(view, R.id.compose_divider);
|
composeDivider = ViewUtil.findById(view, R.id.compose_divider);
|
||||||
scrollToBottomButton = ViewUtil.findById(view, R.id.scroll_to_bottom_button);
|
scrollToBottomButton = ViewUtil.findById(view, R.id.scroll_to_bottom_button);
|
||||||
|
scrollDateHeader = ViewUtil.findById(view, R.id.scroll_date_header);
|
||||||
|
|
||||||
scrollToBottomButton.setOnClickListener(new OnClickListener() {
|
scrollToBottomButton.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
@ -185,7 +189,7 @@ public class ConversationFragment extends Fragment
|
|||||||
if (this.recipients != null && this.threadId != -1) {
|
if (this.recipients != null && this.threadId != -1) {
|
||||||
ConversationAdapter adapter = new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, this.recipients);
|
ConversationAdapter adapter = new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, this.recipients);
|
||||||
list.setAdapter(adapter);
|
list.setAdapter(adapter);
|
||||||
list.addItemDecoration(new StickyHeaderDecoration(adapter, false));
|
list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false));
|
||||||
|
|
||||||
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
|
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
|
||||||
}
|
}
|
||||||
@ -415,13 +419,16 @@ public class ConversationFragment extends Fragment
|
|||||||
|
|
||||||
private final Animation scrollButtonInAnimation;
|
private final Animation scrollButtonInAnimation;
|
||||||
private final Animation scrollButtonOutAnimation;
|
private final Animation scrollButtonOutAnimation;
|
||||||
|
private final ConversationDateHeader conversationDateHeader;
|
||||||
|
|
||||||
private boolean wasAtBottom = true;
|
private boolean wasAtBottom = true;
|
||||||
private boolean wasAtZoomScrollHeight = false;
|
private boolean wasAtZoomScrollHeight = false;
|
||||||
|
private long lastPositionId = -1;
|
||||||
|
|
||||||
ConversationScrollListener(@NonNull Context context) {
|
ConversationScrollListener(@NonNull Context context) {
|
||||||
this.scrollButtonInAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_in);
|
this.scrollButtonInAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_in);
|
||||||
this.scrollButtonOutAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_out);
|
this.scrollButtonOutAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_out);
|
||||||
|
this.conversationDateHeader = new ConversationDateHeader(context, scrollDateHeader);
|
||||||
|
|
||||||
this.scrollButtonInAnimation.setDuration(100);
|
this.scrollButtonInAnimation.setDuration(100);
|
||||||
this.scrollButtonOutAnimation.setDuration(50);
|
this.scrollButtonOutAnimation.setDuration(50);
|
||||||
@ -431,6 +438,7 @@ public class ConversationFragment extends Fragment
|
|||||||
public void onScrolled(final RecyclerView rv, final int dx, final int dy) {
|
public void onScrolled(final RecyclerView rv, final int dx, final int dy) {
|
||||||
boolean currentlyAtBottom = isAtBottom();
|
boolean currentlyAtBottom = isAtBottom();
|
||||||
boolean currentlyAtZoomScrollHeight = isAtZoomScrollHeight();
|
boolean currentlyAtZoomScrollHeight = isAtZoomScrollHeight();
|
||||||
|
int positionId = getHeaderPositionId();
|
||||||
|
|
||||||
if (currentlyAtBottom && !wasAtBottom) {
|
if (currentlyAtBottom && !wasAtBottom) {
|
||||||
ViewUtil.fadeOut(composeDivider, 50, View.INVISIBLE);
|
ViewUtil.fadeOut(composeDivider, 50, View.INVISIBLE);
|
||||||
@ -443,8 +451,22 @@ public class ConversationFragment extends Fragment
|
|||||||
ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation);
|
ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (positionId != lastPositionId) {
|
||||||
|
bindScrollHeader(conversationDateHeader, positionId);
|
||||||
|
}
|
||||||
|
|
||||||
wasAtBottom = currentlyAtBottom;
|
wasAtBottom = currentlyAtBottom;
|
||||||
wasAtZoomScrollHeight = currentlyAtZoomScrollHeight;
|
wasAtZoomScrollHeight = currentlyAtZoomScrollHeight;
|
||||||
|
lastPositionId = positionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
|
||||||
|
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
|
||||||
|
conversationDateHeader.show();
|
||||||
|
} else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||||
|
conversationDateHeader.hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAtBottom() {
|
private boolean isAtBottom() {
|
||||||
@ -460,6 +482,14 @@ public class ConversationFragment extends Fragment
|
|||||||
private boolean isAtZoomScrollHeight() {
|
private boolean isAtZoomScrollHeight() {
|
||||||
return ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition() > 4;
|
return ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition() > 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getHeaderPositionId() {
|
||||||
|
return ((LinearLayoutManager)list.getLayoutManager()).findLastVisibleItemPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindScrollHeader(HeaderViewHolder headerViewHolder, int positionId) {
|
||||||
|
((ConversationAdapter)list.getAdapter()).onBindHeaderViewHolder(headerViewHolder, positionId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ConversationFragmentItemClickListener implements ItemClickListener {
|
private class ConversationFragmentItemClickListener implements ItemClickListener {
|
||||||
@ -553,4 +583,43 @@ public class ConversationFragment extends Fragment
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class ConversationDateHeader extends HeaderViewHolder {
|
||||||
|
|
||||||
|
private final Animation animateIn;
|
||||||
|
private final Animation animateOut;
|
||||||
|
|
||||||
|
private boolean pendingHide = false;
|
||||||
|
|
||||||
|
private ConversationDateHeader(Context context, TextView textView) {
|
||||||
|
super(textView);
|
||||||
|
this.animateIn = AnimationUtils.loadAnimation(context, R.anim.slide_from_top);
|
||||||
|
this.animateOut = AnimationUtils.loadAnimation(context, R.anim.slide_to_top);
|
||||||
|
|
||||||
|
this.animateIn.setDuration(100);
|
||||||
|
this.animateOut.setDuration(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show() {
|
||||||
|
if (pendingHide) {
|
||||||
|
pendingHide = false;
|
||||||
|
} else {
|
||||||
|
ViewUtil.animateIn(textView, animateIn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hide() {
|
||||||
|
pendingHide = true;
|
||||||
|
|
||||||
|
textView.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (pendingHide) {
|
||||||
|
pendingHide = false;
|
||||||
|
ViewUtil.animateOut(textView, animateOut, View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,6 +103,8 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getHeaderId(int i) {
|
public long getHeaderId(int i) {
|
||||||
|
if (!isActiveCursor()) return -1;
|
||||||
|
|
||||||
return Util.hashCode(getHeaderString(i), isPush(i));
|
return Util.hashCode(getHeaderString(i), isPush(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,11 +147,6 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||||||
return getHeaderString(position);
|
return getHeaderString(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isActive() {
|
|
||||||
return isActiveCursor();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<Long, String> getSelectedContacts() {
|
public Map<Long, String> getSelectedContacts() {
|
||||||
return selectedContacts;
|
return selectedContacts;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import android.support.v4.view.ViewCompat;
|
|||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
@ -22,17 +21,21 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
|
|||||||
|
|
||||||
private static final String TAG = StickyHeaderDecoration.class.getName();
|
private static final String TAG = StickyHeaderDecoration.class.getName();
|
||||||
|
|
||||||
|
private static final long NO_HEADER_ID = -1L;
|
||||||
|
|
||||||
private final Map<Long, ViewHolder> headerCache;
|
private final Map<Long, ViewHolder> headerCache;
|
||||||
private final StickyHeaderAdapter adapter;
|
private final StickyHeaderAdapter adapter;
|
||||||
private final boolean renderInline;
|
private final boolean renderInline;
|
||||||
|
private boolean sticky;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param adapter the sticky header adapter to use
|
* @param adapter the sticky header adapter to use
|
||||||
*/
|
*/
|
||||||
public StickyHeaderDecoration(StickyHeaderAdapter adapter, boolean renderInline) {
|
public StickyHeaderDecoration(StickyHeaderAdapter adapter, boolean renderInline, boolean sticky) {
|
||||||
this.adapter = adapter;
|
this.adapter = adapter;
|
||||||
this.headerCache = new HashMap<>();
|
this.headerCache = new HashMap<>();
|
||||||
this.renderInline = renderInline;
|
this.renderInline = renderInline;
|
||||||
|
this.sticky = sticky;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,8 +46,8 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
|
|||||||
RecyclerView.State state)
|
RecyclerView.State state)
|
||||||
{
|
{
|
||||||
int position = parent.getChildAdapterPosition(view);
|
int position = parent.getChildAdapterPosition(view);
|
||||||
|
|
||||||
int headerHeight = 0;
|
int headerHeight = 0;
|
||||||
|
|
||||||
if (position != RecyclerView.NO_POSITION && hasHeader(parent, adapter, position)) {
|
if (position != RecyclerView.NO_POSITION && hasHeader(parent, adapter, position)) {
|
||||||
View header = getHeader(parent, adapter, position).itemView;
|
View header = getHeader(parent, adapter, position).itemView;
|
||||||
headerHeight = getHeaderHeightForLayout(header);
|
headerHeight = getHeaderHeightForLayout(header);
|
||||||
@ -56,16 +59,14 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
|
|||||||
private boolean hasHeader(RecyclerView parent, StickyHeaderAdapter adapter, int adapterPos) {
|
private boolean hasHeader(RecyclerView parent, StickyHeaderAdapter adapter, int adapterPos) {
|
||||||
boolean isReverse = isReverseLayout(parent);
|
boolean isReverse = isReverseLayout(parent);
|
||||||
|
|
||||||
if (!adapter.isActive()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isReverse && adapterPos == ((RecyclerView.Adapter)adapter).getItemCount() - 1 || !isReverse && adapterPos == 0) {
|
if (isReverse && adapterPos == ((RecyclerView.Adapter)adapter).getItemCount() - 1 || !isReverse && adapterPos == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int previous = adapterPos + (isReverse ? 1 : -1);
|
int previous = adapterPos + (isReverse ? 1 : -1);
|
||||||
return adapter.getHeaderId(adapterPos) != adapter.getHeaderId(previous);
|
long headerId = adapter.getHeaderId(adapterPos);
|
||||||
|
|
||||||
|
return headerId != NO_HEADER_ID && (headerId != adapter.getHeaderId(previous));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter adapter, int position) {
|
private ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter adapter, int position) {
|
||||||
@ -109,7 +110,7 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
|
|||||||
|
|
||||||
final int adapterPos = parent.getChildAdapterPosition(child);
|
final int adapterPos = parent.getChildAdapterPosition(child);
|
||||||
|
|
||||||
if (adapterPos != RecyclerView.NO_POSITION && ((layoutPos == 0 && adapter.isActive()) || hasHeader(parent, adapter, adapterPos))) {
|
if (adapterPos != RecyclerView.NO_POSITION && ((layoutPos == 0 && sticky) || hasHeader(parent, adapter, adapterPos))) {
|
||||||
View header = getHeader(parent, adapter, adapterPos).itemView;
|
View header = getHeader(parent, adapter, adapterPos).itemView;
|
||||||
c.save();
|
c.save();
|
||||||
final int left = child.getLeft();
|
final int left = child.getLeft();
|
||||||
@ -146,7 +147,7 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
top = Math.max(0, top);
|
if (sticky) top = Math.max(0, top);
|
||||||
}
|
}
|
||||||
|
|
||||||
return top;
|
return top;
|
||||||
@ -204,7 +205,5 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
|
|||||||
* @param position the header's item position
|
* @param position the header's item position
|
||||||
*/
|
*/
|
||||||
void onBindHeaderViewHolder(T viewHolder, int position);
|
void onBindHeaderViewHolder(T viewHolder, int position);
|
||||||
|
|
||||||
boolean isActive();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user