mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
parent
b677370597
commit
4c815db076
6
res/drawable/conversation_item_header_background.xml
Normal file
6
res/drawable/conversation_item_header_background.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#bb999999" />
|
||||
<corners android:radius="3dp" />
|
||||
</shape>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#bb444444" />
|
||||
<corners android:radius="3dp" />
|
||||
</shape>
|
21
res/layout/conversation_item_header.xml
Normal file
21
res/layout/conversation_item_header.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingBottom="3dp">
|
||||
|
||||
<TextView android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:paddingLeft="6dp"
|
||||
android:paddingRight="6dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:textColor="@color/white"
|
||||
android:background="?conversation_item_header_background"
|
||||
android:textSize="14sp"
|
||||
tools:text="March 1, 2015" />
|
||||
</FrameLayout>
|
@ -71,6 +71,7 @@
|
||||
<attr name="conversation_item_received_text_secondary_color" format="reference|color"/>
|
||||
<attr name="conversation_item_sent_text_indicator_tab_color" format="reference|color"/>
|
||||
<attr name="conversation_item_sent_indicator_text_background" format="reference" />
|
||||
<attr name="conversation_item_header_background" format="reference"/>
|
||||
|
||||
<attr name="dialog_info_icon" format="reference" />
|
||||
<attr name="dialog_alert_icon" format="reference" />
|
||||
|
@ -227,6 +227,8 @@
|
||||
<!-- DateUtils -->
|
||||
<string name="DateUtils_just_now">Just now</string>
|
||||
<string name="DateUtils_minutes_ago">%d min</string>
|
||||
<string name="DateUtils_today">Today</string>
|
||||
<string name="DateUtils_yesterday">Yesterday</string>
|
||||
|
||||
<!-- DeviceListActivity -->
|
||||
<string name="DeviceListActivity_unlink_s">Unlink \'%s\'?</string>
|
||||
|
@ -156,6 +156,7 @@
|
||||
<item name="conversation_item_sent_text_indicator_tab_color">#99000000</item>
|
||||
<item name="conversation_item_received_text_primary_color">@color/white</item>
|
||||
<item name="conversation_item_received_text_secondary_color">#BFffffff</item>
|
||||
<item name="conversation_item_header_background">@drawable/conversation_item_header_background</item>
|
||||
|
||||
<item name="quick_camera_icon">@drawable/quick_camera_light</item>
|
||||
<item name="quick_mic_icon">@drawable/ic_mic_grey600_24dp</item>
|
||||
@ -242,6 +243,7 @@
|
||||
<item name="conversation_item_received_text_primary_color">@color/white</item>
|
||||
<item name="conversation_item_received_text_secondary_color">#BFffffff</item>
|
||||
<item name="conversation_item_sent_indicator_text_background">@drawable/conversation_item_sent_indicator_text_shape_dark</item>
|
||||
<item name="conversation_item_header_background">@drawable/conversation_item_header_background_dark</item>
|
||||
|
||||
<item name="verification_background">#ff333333</item>
|
||||
|
||||
|
@ -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<Long, RecyclerView.ViewHolder> 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 <T> the header view holder
|
||||
*/
|
||||
public interface StickyHeaderAdapter<T extends RecyclerView.ViewHolder> {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
@ -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 <V extends View & BindableConversationItem>
|
||||
extends CursorRecyclerViewAdapter<ConversationAdapter.ViewHolder>
|
||||
implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder>
|
||||
{
|
||||
|
||||
private static final int MAX_CACHE_SIZE = 40;
|
||||
@ -82,6 +90,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
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 <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
|
||||
@ -94,6 +103,21 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 <V extends View & BindableConversationItem>
|
||||
this.recipients = null;
|
||||
this.inflater = null;
|
||||
this.db = null;
|
||||
this.calendar = null;
|
||||
}
|
||||
|
||||
public ConversationAdapter(@NonNull Context context,
|
||||
@ -125,6 +150,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
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 <V extends View & BindableConversationItem>
|
||||
|
||||
@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 <V extends View & BindableConversationItem>
|
||||
|
||||
@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 <V extends View & BindableConversationItem>
|
||||
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<MessageRecord> reference = messageRecordCache.get(type + messageId);
|
||||
if (reference != null) {
|
||||
final MessageRecord record = reference.get();
|
||||
@ -254,4 +279,26 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
201
src/org/thoughtcrime/securesms/util/StickyHeaderDecoration.java
Normal file
201
src/org/thoughtcrime/securesms/util/StickyHeaderDecoration.java
Normal file
@ -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<Long, ViewHolder> 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 <T> the header view holder
|
||||
*/
|
||||
public interface StickyHeaderAdapter<T extends ViewHolder> {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user