diff --git a/res/drawable/conversation_item_header_background.xml b/res/drawable/conversation_item_header_background.xml
new file mode 100644
index 0000000000..6cc80221f1
--- /dev/null
+++ b/res/drawable/conversation_item_header_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/conversation_item_header_background_dark.xml b/res/drawable/conversation_item_header_background_dark.xml
new file mode 100644
index 0000000000..2cc83dbe26
--- /dev/null
+++ b/res/drawable/conversation_item_header_background_dark.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/conversation_item_header.xml b/res/layout/conversation_item_header.xml
new file mode 100644
index 0000000000..ef6fb13203
--- /dev/null
+++ b/res/layout/conversation_item_header.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index be6834af3d..131369a817 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -71,6 +71,7 @@
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 31496313cc..befdf1b6c0 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -227,6 +227,8 @@
Just now
%d min
+ Today
+ Yesterday
Unlink \'%s\'?
diff --git a/res/values/themes.xml b/res/values/themes.xml
index fe5a50402e..556afe5abc 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -156,6 +156,7 @@
- #99000000
- @color/white
- #BFffffff
+ - @drawable/conversation_item_header_background
- @drawable/quick_camera_light
- @drawable/ic_mic_grey600_24dp
@@ -242,6 +243,7 @@
- @color/white
- #BFffffff
- @drawable/conversation_item_sent_indicator_text_shape_dark
+ - @drawable/conversation_item_header_background_dark
- #ff333333
diff --git a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java
index be3b3083d9..b48f187786 100644
--- a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java
+++ b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java
@@ -18,16 +18,12 @@ package org.thoughtcrime.securesms;
import android.database.Cursor;
-import android.graphics.Canvas;
-import android.graphics.Rect;
import android.os.Build;
-import android.os.Build.VERSION;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
-import android.support.v4.view.ViewCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -41,9 +37,9 @@ import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
+import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.ViewUtil;
-import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -205,180 +201,4 @@ public class ContactSelectionListFragment extends Fragment
void onContactDeselected(String number);
}
- /**
- * A sticky header decoration for android's RecyclerView.
- */
- public static class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
-
- private Map mHeaderCache;
-
- private StickyHeaderAdapter mAdapter;
-
- private boolean mRenderInline;
-
- /**
- * @param adapter the sticky header adapter to use
- */
- public StickyHeaderDecoration(StickyHeaderAdapter adapter, boolean renderInline) {
- mAdapter = adapter;
- mHeaderCache = new HashMap<>();
- mRenderInline = renderInline;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
- RecyclerView.State state)
- {
- int position = parent.getChildAdapterPosition(view);
-
- int headerHeight = 0;
- if (position != RecyclerView.NO_POSITION && hasHeader(position)) {
- View header = getHeader(parent, position).itemView;
- headerHeight = getHeaderHeightForLayout(header);
- }
-
- outRect.set(0, headerHeight, 0, 0);
- }
-
- private boolean hasHeader(int position) {
- if (position == 0) {
- return true;
- }
-
- int previous = position - 1;
- return mAdapter.getHeaderId(position) != mAdapter.getHeaderId(previous);
- }
-
- private RecyclerView.ViewHolder getHeader(RecyclerView parent, int position) {
- final long key = mAdapter.getHeaderId(position);
-
- if (mHeaderCache.containsKey(key)) {
- return mHeaderCache.get(key);
- } else {
- final RecyclerView.ViewHolder holder = mAdapter.onCreateHeaderViewHolder(parent);
- final View header = holder.itemView;
-
- //noinspection unchecked
- mAdapter.onBindHeaderViewHolder(holder, position);
-
- int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
- int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
-
- int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
- parent.getPaddingLeft() + parent.getPaddingRight(), header.getLayoutParams().width);
- int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
- parent.getPaddingTop() + parent.getPaddingBottom(), header.getLayoutParams().height);
-
- header.measure(childWidth, childHeight);
- header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
-
- mHeaderCache.put(key, holder);
-
- return holder;
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
- final int count = parent.getChildCount();
-
- for (int layoutPos = 0; layoutPos < count; layoutPos++) {
- final View child = parent.getChildAt(layoutPos);
-
- final int adapterPos = parent.getChildAdapterPosition(child);
-
- if (adapterPos != RecyclerView.NO_POSITION && (layoutPos == 0 || hasHeader(adapterPos))) {
- View header = getHeader(parent, adapterPos).itemView;
- c.save();
- final int left = child.getLeft();
- final int top = getHeaderTop(parent, child, header, adapterPos, layoutPos);
- c.translate(left, top);
- header.draw(c);
- c.restore();
- }
- }
- }
-
- private int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos,
- int layoutPos)
- {
- int headerHeight = getHeaderHeightForLayout(header);
- int top = getChildY(parent, child) - headerHeight;
- if (layoutPos == 0) {
- final int count = parent.getChildCount();
- final long currentId = mAdapter.getHeaderId(adapterPos);
- // find next view with header and compute the offscreen push if needed
- for (int i = 1; i < count; i++) {
- int adapterPosHere = parent.getChildAdapterPosition(parent.getChildAt(i));
- if (adapterPosHere != RecyclerView.NO_POSITION) {
- long nextId = mAdapter.getHeaderId(adapterPosHere);
- if (nextId != currentId) {
- final View next = parent.getChildAt(i);
- final int offset = getChildY(parent, next) - (headerHeight + getHeader(parent, adapterPosHere).itemView.getHeight());
- if (offset < 0) {
- return offset;
- } else {
- break;
- }
- }
- }
- }
-
- top = Math.max(0, top);
- }
-
- return top;
- }
-
- private int getChildY(RecyclerView parent, View child) {
- if (VERSION.SDK_INT < 11) {
- Rect rect = new Rect();
- parent.getChildVisibleRect(child, rect, null);
- return rect.top;
- } else {
- return (int)ViewCompat.getY(child);
- }
- }
-
- private int getHeaderHeightForLayout(View header) {
- return mRenderInline ? 0 : header.getHeight();
- }
- }
-
- /**
- * The adapter to assist the {@link StickyHeaderDecoration} in creating and binding the header views.
- *
- * @param the header view holder
- */
- public interface StickyHeaderAdapter {
-
- /**
- * Returns the header id for the item at the given position.
- *
- * @param position the item position
- * @return the header id
- */
- long getHeaderId(int position);
-
- /**
- * Creates a new header ViewHolder.
- *
- * @param parent the header's view parent
- * @return a view holder for the created view
- */
- T onCreateHeaderViewHolder(ViewGroup parent);
-
- /**
- * Updates the header view to reflect the header data for the given position
- * @param viewHolder the header view holder
- * @param position the header's item position
- */
- void onBindHeaderViewHolder(T viewHolder, int position);
- }
}
diff --git a/src/org/thoughtcrime/securesms/ConversationAdapter.java b/src/org/thoughtcrime/securesms/ConversationAdapter.java
index de7bcac90f..c08f4fcd58 100644
--- a/src/org/thoughtcrime/securesms/ConversationAdapter.java
+++ b/src/org/thoughtcrime/securesms/ConversationAdapter.java
@@ -29,6 +29,7 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
+import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
@@ -39,11 +40,17 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.recipients.Recipients;
+import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.LRUCache;
+import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
+import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
+import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
import java.lang.ref.SoftReference;
+import java.util.Calendar;
import java.util.Collections;
+import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
@@ -59,6 +66,7 @@ import java.util.Set;
*/
public class ConversationAdapter
extends CursorRecyclerViewAdapter
+ implements StickyHeaderDecoration.StickyHeaderAdapter
{
private static final int MAX_CACHE_SIZE = 40;
@@ -82,6 +90,7 @@ public class ConversationAdapter
private final @NonNull Recipients recipients;
private final @NonNull MmsSmsDatabase db;
private final @NonNull LayoutInflater inflater;
+ private final @NonNull Calendar calendar;
protected static class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(final @NonNull V itemView) {
@@ -94,6 +103,21 @@ public class ConversationAdapter
}
}
+
+ protected static class HeaderViewHolder extends RecyclerView.ViewHolder {
+ private TextView textView;
+
+ public HeaderViewHolder(View itemView) {
+ super(itemView);
+ textView = ViewUtil.findById(itemView, R.id.text);
+ }
+
+ public void setText(CharSequence text) {
+ textView.setText(text);
+ }
+ }
+
+
public interface ItemClickListener {
void onItemClick(MessageRecord item);
void onItemLongClick(MessageRecord item);
@@ -109,6 +133,7 @@ public class ConversationAdapter
this.recipients = null;
this.inflater = null;
this.db = null;
+ this.calendar = null;
}
public ConversationAdapter(@NonNull Context context,
@@ -125,6 +150,7 @@ public class ConversationAdapter
this.recipients = recipients;
this.inflater = LayoutInflater.from(context);
this.db = DatabaseFactory.getMmsSmsDatabase(context);
+ this.calendar = Calendar.getInstance();
setHasStableIds(true);
}
@@ -137,10 +163,8 @@ public class ConversationAdapter
@Override
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
- long start = System.currentTimeMillis();
- long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
- String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
- MessageRecord messageRecord = getMessageRecord(id, cursor, type);
+ long start = System.currentTimeMillis();
+ MessageRecord messageRecord = getMessageRecord(cursor);
viewHolder.getView().bind(masterSecret, messageRecord, locale, batchSelected, recipients);
Log.w(TAG, "Bind time: " + (System.currentTimeMillis() - start));
@@ -191,11 +215,9 @@ public class ConversationAdapter
@Override
public int getItemViewType(@NonNull Cursor cursor) {
- long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.ID));
- String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
- MessageRecord messageRecord = getMessageRecord(id, cursor, type);
+ MessageRecord messageRecord = getMessageRecord(cursor);
- if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined() ||
+ if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined() ||
messageRecord.isExpirationTimerUpdate() || messageRecord.isEndSession() || messageRecord.isIdentityUpdate()) {
return MESSAGE_TYPE_UPDATE;
} else if (hasAudio(messageRecord)) {
@@ -216,7 +238,10 @@ public class ConversationAdapter
return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.UNIQUE_ROW_ID));
}
- private MessageRecord getMessageRecord(long messageId, Cursor cursor, String type) {
+ private MessageRecord getMessageRecord(Cursor cursor) {
+ long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.ID));
+ String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
+
final SoftReference reference = messageRecordCache.get(type + messageId);
if (reference != null) {
final MessageRecord record = reference.get();
@@ -254,4 +279,26 @@ public class ConversationAdapter
private boolean hasThumbnail(MessageRecord messageRecord) {
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null;
}
+
+
+ @Override
+ public long getHeaderId(int position) {
+ Cursor cursor = getCursorAtPositionOrThrow(position);
+ MessageRecord record = getMessageRecord(cursor);
+
+ calendar.setTime(new Date(record.getDateSent()));
+ return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
+ }
+
+ @Override
+ public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
+ return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false));
+ }
+
+ @Override
+ public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
+ Cursor cursor = getCursorAtPositionOrThrow(position);
+ viewHolder.setText(DateUtils.getRelativeDate(getContext(), locale, getMessageRecord(cursor).getDateReceived()));
+ }
}
+
diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java
index 90b155a0d7..5db12fd418 100644
--- a/src/org/thoughtcrime/securesms/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/ConversationFragment.java
@@ -32,7 +32,6 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.ItemAnimator.ItemAnimatorFinishedListener;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.text.ClipboardManager;
import android.text.TextUtils;
@@ -60,6 +59,7 @@ import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
+import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
@@ -170,7 +170,10 @@ public class ConversationFragment extends Fragment
private void initializeListAdapter() {
if (this.recipients != null && this.threadId != -1) {
- list.setAdapter(new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, this.recipients));
+ ConversationAdapter adapter = new ConversationAdapter(getActivity(), masterSecret, locale, selectionClickListener, null, this.recipients);
+ list.setAdapter(adapter);
+ list.addItemDecoration(new StickyHeaderDecoration(adapter, false));
+
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
}
}
diff --git a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java
index c889fde7b3..c723f1f9f2 100644
--- a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java
+++ b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java
@@ -35,7 +35,7 @@ import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller.FastScrollAdapter;
-import org.thoughtcrime.securesms.ContactSelectionListFragment.StickyHeaderAdapter;
+import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolder;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
diff --git a/src/org/thoughtcrime/securesms/util/DateUtils.java b/src/org/thoughtcrime/securesms/util/DateUtils.java
index 6d12ad4eea..c53039c132 100644
--- a/src/org/thoughtcrime/securesms/util/DateUtils.java
+++ b/src/org/thoughtcrime/securesms/util/DateUtils.java
@@ -18,6 +18,7 @@ package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.os.Build;
+import android.support.annotation.NonNull;
import android.text.format.DateFormat;
import java.text.SimpleDateFormat;
@@ -37,6 +38,10 @@ public class DateUtils extends android.text.format.DateUtils {
return System.currentTimeMillis() - millis <= unit.toMillis(span);
}
+ private static boolean isYesterday(final long when) {
+ return DateUtils.isToday(when + TimeUnit.DAYS.toMillis(1));
+ }
+
private static int convertDelta(final long millis, TimeUnit to) {
return (int) to.convert(System.currentTimeMillis() - millis, TimeUnit.MILLISECONDS);
}
@@ -111,6 +116,19 @@ public class DateUtils extends android.text.format.DateUtils {
return new SimpleDateFormat(dateFormatPattern, locale);
}
+ public static String getRelativeDate(@NonNull Context context,
+ @NonNull Locale locale,
+ long timestamp)
+ {
+ if (isToday(timestamp)) {
+ return context.getString(R.string.DateUtils_today);
+ } else if (isYesterday(timestamp)) {
+ return context.getString(R.string.DateUtils_yesterday);
+ } else {
+ return getFormattedDateTime(timestamp, "EEE, MMM d, yyyy", locale);
+ }
+ }
+
private static String getLocalizedPattern(String template, Locale locale) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
return DateFormat.getBestDateTimePattern(locale, template);
diff --git a/src/org/thoughtcrime/securesms/util/StickyHeaderDecoration.java b/src/org/thoughtcrime/securesms/util/StickyHeaderDecoration.java
new file mode 100644
index 0000000000..c4bc881d7a
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/util/StickyHeaderDecoration.java
@@ -0,0 +1,201 @@
+package org.thoughtcrime.securesms.util;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Build.VERSION;
+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.view.View;
+import android.view.ViewGroup;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A sticky header decoration for android's RecyclerView.
+ * Currently only supports LinearLayoutManager in VERTICAL orientation.
+ */
+public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
+
+ private final Map headerCache;
+ private final StickyHeaderAdapter adapter;
+ private final boolean renderInline;
+
+ /**
+ * @param adapter the sticky header adapter to use
+ */
+ public StickyHeaderDecoration(StickyHeaderAdapter adapter, boolean renderInline) {
+ this.adapter = adapter;
+ this.headerCache = new HashMap<>();
+ this.renderInline = renderInline;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+ RecyclerView.State state)
+ {
+ int position = parent.getChildAdapterPosition(view);
+
+ int headerHeight = 0;
+ if (position != RecyclerView.NO_POSITION && hasHeader(parent, position)) {
+ View header = getHeader(parent, position).itemView;
+ headerHeight = getHeaderHeightForLayout(header);
+ }
+
+ outRect.set(0, headerHeight, 0, 0);
+ }
+
+ private boolean hasHeader(RecyclerView parent, int adapterPos) {
+ boolean isReverse = isReverseLayout(parent);
+ if (isReverse && adapterPos == parent.getAdapter().getItemCount() - 1 ||
+ !isReverse && adapterPos == 0) {
+ return true;
+ }
+
+ int previous = adapterPos + (isReverse ? 1 : -1);
+ return adapter.getHeaderId(adapterPos) != adapter.getHeaderId(previous);
+ }
+
+ private ViewHolder getHeader(RecyclerView parent, int position) {
+ final long key = adapter.getHeaderId(position);
+
+ if (headerCache.containsKey(key)) {
+ return headerCache.get(key);
+ } else {
+ final ViewHolder holder = adapter.onCreateHeaderViewHolder(parent);
+ final View header = holder.itemView;
+
+ //noinspection unchecked
+ adapter.onBindHeaderViewHolder(holder, position);
+
+ int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
+ int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
+
+ int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
+ parent.getPaddingLeft() + parent.getPaddingRight(), header.getLayoutParams().width);
+ int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
+ parent.getPaddingTop() + parent.getPaddingBottom(), header.getLayoutParams().height);
+
+ header.measure(childWidth, childHeight);
+ header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
+
+ headerCache.put(key, holder);
+
+ return holder;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ final int count = parent.getChildCount();
+
+ for (int layoutPos = 0; layoutPos < count; layoutPos++) {
+ final View child = parent.getChildAt(translatedChildPosition(parent, layoutPos));
+
+ final int adapterPos = parent.getChildAdapterPosition(child);
+
+ if (adapterPos != RecyclerView.NO_POSITION && (layoutPos == 0 || hasHeader(parent, adapterPos))) {
+ View header = getHeader(parent, adapterPos).itemView;
+ c.save();
+ final int left = child.getLeft();
+ final int top = getHeaderTop(parent, child, header, adapterPos, layoutPos);
+ c.translate(left, top);
+ header.draw(c);
+ c.restore();
+ }
+ }
+ }
+
+ private int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos,
+ int layoutPos)
+ {
+ int headerHeight = getHeaderHeightForLayout(header);
+ int top = getChildY(parent, child) - headerHeight;
+ if (layoutPos == 0) {
+ final int count = parent.getChildCount();
+ final long currentId = adapter.getHeaderId(adapterPos);
+ // find next view with header and compute the offscreen push if needed
+ for (int i = 1; i < count; i++) {
+ int adapterPosHere = parent.getChildAdapterPosition(parent.getChildAt(translatedChildPosition(parent, i)));
+ if (adapterPosHere != RecyclerView.NO_POSITION) {
+ long nextId = adapter.getHeaderId(adapterPosHere);
+ if (nextId != currentId) {
+ final View next = parent.getChildAt(translatedChildPosition(parent, i));
+ final int offset = getChildY(parent, next) - (headerHeight + getHeader(parent, adapterPosHere).itemView.getHeight());
+ if (offset < 0) {
+ return offset;
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ top = Math.max(0, top);
+ }
+
+ return top;
+ }
+
+ private int translatedChildPosition(RecyclerView parent, int position) {
+ return isReverseLayout(parent) ? parent.getChildCount() - 1 - position : position;
+ }
+
+ private int getChildY(RecyclerView parent, View child) {
+ if (VERSION.SDK_INT < 11) {
+ Rect rect = new Rect();
+ parent.getChildVisibleRect(child, rect, null);
+ return rect.top;
+ } else {
+ return (int)ViewCompat.getY(child);
+ }
+ }
+
+ private int getHeaderHeightForLayout(View header) {
+ return renderInline ? 0 : header.getHeight();
+ }
+
+ private boolean isReverseLayout(final RecyclerView parent) {
+ return (parent.getLayoutManager() instanceof LinearLayoutManager) &&
+ ((LinearLayoutManager)parent.getLayoutManager()).getReverseLayout();
+ }
+
+ /**
+ * The adapter to assist the {@link StickyHeaderDecoration} in creating and binding the header views.
+ *
+ * @param the header view holder
+ */
+ public interface StickyHeaderAdapter {
+
+ /**
+ * Returns the header id for the item at the given position.
+ *
+ * @param position the item position
+ * @return the header id
+ */
+ long getHeaderId(int position);
+
+ /**
+ * Creates a new header ViewHolder.
+ *
+ * @param parent the header's view parent
+ * @return a view holder for the created view
+ */
+ T onCreateHeaderViewHolder(ViewGroup parent);
+
+ /**
+ * Updates the header view to reflect the header data for the given position
+ * @param viewHolder the header view holder
+ * @param position the header's item position
+ */
+ void onBindHeaderViewHolder(T viewHolder, int position);
+ }
+}
\ No newline at end of file