Make the sticky date header only visible during scroll

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2017-01-25 16:38:36 -08:00
parent 2e16c6cf41
commit d46d3b72c8
7 changed files with 118 additions and 30 deletions

View File

@ -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%"

View File

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

View File

@ -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);
} }

View File

@ -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();
}
} }

View File

@ -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);
}
}
} }

View File

@ -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;
} }

View File

@ -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();
} }
} }