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"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<alpha android:duration="1"
android:fromAlpha="0"
android:toAlpha="1"/>
<translate
android:duration="250"
android:fromYDelta="-100%"

View File

@ -3,7 +3,24 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_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:id="@android:id/list"

View File

@ -124,7 +124,7 @@ public class ContactSelectionListFragment extends Fragment
isMulti());
selectedContacts = adapter.getSelectedContacts();
recyclerView.setAdapter(adapter);
recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, true));
recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, true, true));
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 {
private TextView textView;
protected TextView textView;
public HeaderViewHolder(View itemView) {
super(itemView);
textView = ViewUtil.findById(itemView, R.id.text);
}
public HeaderViewHolder(TextView textView) {
super(textView);
this.textView = textView;
}
public void setText(CharSequence text) {
textView.setText(text);
}
@ -283,6 +288,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
@Override
public long getHeaderId(int position) {
if (!isActiveCursor()) return -1;
Cursor cursor = getCursorAtPositionOrThrow(position);
MessageRecord record = getMessageRecord(cursor);
@ -300,10 +307,5 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
Cursor cursor = getCursorAtPositionOrThrow(position);
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.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -94,6 +96,7 @@ public class ConversationFragment extends Fragment
private View loadMoreView;
private View composeDivider;
private View scrollToBottomButton;
private TextView scrollDateHeader;
@Override
public void onCreate(Bundle icicle) {
@ -108,6 +111,7 @@ public class ConversationFragment extends Fragment
list = ViewUtil.findById(view, android.R.id.list);
composeDivider = ViewUtil.findById(view, R.id.compose_divider);
scrollToBottomButton = ViewUtil.findById(view, R.id.scroll_to_bottom_button);
scrollDateHeader = ViewUtil.findById(view, R.id.scroll_date_header);
scrollToBottomButton.setOnClickListener(new OnClickListener() {
@Override
@ -185,7 +189,7 @@ public class ConversationFragment extends Fragment
if (this.recipients != null && this.threadId != -1) {
ConversationAdapter adapter = new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, this.recipients);
list.setAdapter(adapter);
list.addItemDecoration(new StickyHeaderDecoration(adapter, false));
list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false));
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
}
@ -413,15 +417,18 @@ public class ConversationFragment extends Fragment
private class ConversationScrollListener extends OnScrollListener {
private final Animation scrollButtonInAnimation;
private final Animation scrollButtonOutAnimation;
private final Animation scrollButtonInAnimation;
private final Animation scrollButtonOutAnimation;
private final ConversationDateHeader conversationDateHeader;
private boolean wasAtBottom = true;
private boolean wasAtZoomScrollHeight = false;
private long lastPositionId = -1;
ConversationScrollListener(@NonNull Context context) {
this.scrollButtonInAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_in);
this.scrollButtonOutAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_out);
this.conversationDateHeader = new ConversationDateHeader(context, scrollDateHeader);
this.scrollButtonInAnimation.setDuration(100);
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) {
boolean currentlyAtBottom = isAtBottom();
boolean currentlyAtZoomScrollHeight = isAtZoomScrollHeight();
int positionId = getHeaderPositionId();
if (currentlyAtBottom && !wasAtBottom) {
ViewUtil.fadeOut(composeDivider, 50, View.INVISIBLE);
@ -443,8 +451,22 @@ public class ConversationFragment extends Fragment
ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation);
}
if (positionId != lastPositionId) {
bindScrollHeader(conversationDateHeader, positionId);
}
wasAtBottom = currentlyAtBottom;
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() {
@ -460,6 +482,14 @@ public class ConversationFragment extends Fragment
private boolean isAtZoomScrollHeight() {
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 {
@ -553,4 +583,43 @@ public class ConversationFragment extends Fragment
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
public long getHeaderId(int i) {
if (!isActiveCursor()) return -1;
return Util.hashCode(getHeaderString(i), isPush(i));
}
@ -145,11 +147,6 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
return getHeaderString(position);
}
@Override
public boolean isActive() {
return isActiveCursor();
}
public Map<Long, String> getSelectedContacts() {
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.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.util.Log;
import android.view.View;
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 long NO_HEADER_ID = -1L;
private final Map<Long, ViewHolder> headerCache;
private final StickyHeaderAdapter adapter;
private final boolean renderInline;
private boolean sticky;
/**
* @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.headerCache = new HashMap<>();
this.renderInline = renderInline;
this.sticky = sticky;
}
/**
@ -42,9 +45,9 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state)
{
int position = parent.getChildAdapterPosition(view);
int position = parent.getChildAdapterPosition(view);
int headerHeight = 0;
if (position != RecyclerView.NO_POSITION && hasHeader(parent, adapter, position)) {
View header = getHeader(parent, adapter, position).itemView;
headerHeight = getHeaderHeightForLayout(header);
@ -56,16 +59,14 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
private boolean hasHeader(RecyclerView parent, StickyHeaderAdapter adapter, int adapterPos) {
boolean isReverse = isReverseLayout(parent);
if (!adapter.isActive()) {
return false;
}
if (isReverse && adapterPos == ((RecyclerView.Adapter)adapter).getItemCount() - 1 || !isReverse && adapterPos == 0) {
return true;
}
int previous = adapterPos + (isReverse ? 1 : -1);
return adapter.getHeaderId(adapterPos) != adapter.getHeaderId(previous);
int previous = adapterPos + (isReverse ? 1 : -1);
long headerId = adapter.getHeaderId(adapterPos);
return headerId != NO_HEADER_ID && (headerId != adapter.getHeaderId(previous));
}
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);
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;
c.save();
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;
@ -204,7 +205,5 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
* @param position the header's item position
*/
void onBindHeaderViewHolder(T viewHolder, int position);
boolean isActive();
}
}