mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-30 13:35:18 +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_received_text_secondary_color" format="reference|color"/>
|
||||||
<attr name="conversation_item_sent_text_indicator_tab_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_sent_indicator_text_background" format="reference" />
|
||||||
|
<attr name="conversation_item_header_background" format="reference"/>
|
||||||
|
|
||||||
<attr name="dialog_info_icon" format="reference" />
|
<attr name="dialog_info_icon" format="reference" />
|
||||||
<attr name="dialog_alert_icon" format="reference" />
|
<attr name="dialog_alert_icon" format="reference" />
|
||||||
|
@ -227,6 +227,8 @@
|
|||||||
<!-- DateUtils -->
|
<!-- DateUtils -->
|
||||||
<string name="DateUtils_just_now">Just now</string>
|
<string name="DateUtils_just_now">Just now</string>
|
||||||
<string name="DateUtils_minutes_ago">%d min</string>
|
<string name="DateUtils_minutes_ago">%d min</string>
|
||||||
|
<string name="DateUtils_today">Today</string>
|
||||||
|
<string name="DateUtils_yesterday">Yesterday</string>
|
||||||
|
|
||||||
<!-- DeviceListActivity -->
|
<!-- DeviceListActivity -->
|
||||||
<string name="DeviceListActivity_unlink_s">Unlink \'%s\'?</string>
|
<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_sent_text_indicator_tab_color">#99000000</item>
|
||||||
<item name="conversation_item_received_text_primary_color">@color/white</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_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_camera_icon">@drawable/quick_camera_light</item>
|
||||||
<item name="quick_mic_icon">@drawable/ic_mic_grey600_24dp</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_primary_color">@color/white</item>
|
||||||
<item name="conversation_item_received_text_secondary_color">#BFffffff</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_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>
|
<item name="verification_background">#ff333333</item>
|
||||||
|
|
||||||
|
@ -18,16 +18,12 @@ package org.thoughtcrime.securesms;
|
|||||||
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Build.VERSION;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.LoaderManager;
|
import android.support.v4.app.LoaderManager;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
import android.support.v4.view.ViewCompat;
|
|
||||||
import android.support.v4.widget.SwipeRefreshLayout;
|
import android.support.v4.widget.SwipeRefreshLayout;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
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.ContactSelectionListItem;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||||
|
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -205,180 +201,4 @@ public class ContactSelectionListFragment extends Fragment
|
|||||||
void onContactDeselected(String number);
|
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.OnClickListener;
|
||||||
import android.view.View.OnLongClickListener;
|
import android.view.View.OnLongClickListener;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
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.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||||
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.LRUCache;
|
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.util.ViewUtil;
|
||||||
|
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
|
||||||
|
|
||||||
import java.lang.ref.SoftReference;
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.util.Calendar;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -59,6 +66,7 @@ import java.util.Set;
|
|||||||
*/
|
*/
|
||||||
public class ConversationAdapter <V extends View & BindableConversationItem>
|
public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||||
extends CursorRecyclerViewAdapter<ConversationAdapter.ViewHolder>
|
extends CursorRecyclerViewAdapter<ConversationAdapter.ViewHolder>
|
||||||
|
implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder>
|
||||||
{
|
{
|
||||||
|
|
||||||
private static final int MAX_CACHE_SIZE = 40;
|
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 Recipients recipients;
|
||||||
private final @NonNull MmsSmsDatabase db;
|
private final @NonNull MmsSmsDatabase db;
|
||||||
private final @NonNull LayoutInflater inflater;
|
private final @NonNull LayoutInflater inflater;
|
||||||
|
private final @NonNull Calendar calendar;
|
||||||
|
|
||||||
protected static class ViewHolder extends RecyclerView.ViewHolder {
|
protected static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
|
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 {
|
public interface ItemClickListener {
|
||||||
void onItemClick(MessageRecord item);
|
void onItemClick(MessageRecord item);
|
||||||
void onItemLongClick(MessageRecord item);
|
void onItemLongClick(MessageRecord item);
|
||||||
@ -109,6 +133,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||||||
this.recipients = null;
|
this.recipients = null;
|
||||||
this.inflater = null;
|
this.inflater = null;
|
||||||
this.db = null;
|
this.db = null;
|
||||||
|
this.calendar = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConversationAdapter(@NonNull Context context,
|
public ConversationAdapter(@NonNull Context context,
|
||||||
@ -125,6 +150,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||||||
this.recipients = recipients;
|
this.recipients = recipients;
|
||||||
this.inflater = LayoutInflater.from(context);
|
this.inflater = LayoutInflater.from(context);
|
||||||
this.db = DatabaseFactory.getMmsSmsDatabase(context);
|
this.db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||||
|
this.calendar = Calendar.getInstance();
|
||||||
|
|
||||||
setHasStableIds(true);
|
setHasStableIds(true);
|
||||||
}
|
}
|
||||||
@ -137,10 +163,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
|
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
|
MessageRecord messageRecord = getMessageRecord(cursor);
|
||||||
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
|
|
||||||
MessageRecord messageRecord = getMessageRecord(id, cursor, type);
|
|
||||||
|
|
||||||
viewHolder.getView().bind(masterSecret, messageRecord, locale, batchSelected, recipients);
|
viewHolder.getView().bind(masterSecret, messageRecord, locale, batchSelected, recipients);
|
||||||
Log.w(TAG, "Bind time: " + (System.currentTimeMillis() - start));
|
Log.w(TAG, "Bind time: " + (System.currentTimeMillis() - start));
|
||||||
@ -191,11 +215,9 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemViewType(@NonNull Cursor cursor) {
|
public int getItemViewType(@NonNull Cursor cursor) {
|
||||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.ID));
|
MessageRecord messageRecord = getMessageRecord(cursor);
|
||||||
String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT));
|
|
||||||
MessageRecord messageRecord = getMessageRecord(id, cursor, type);
|
|
||||||
|
|
||||||
if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined() ||
|
if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined() ||
|
||||||
messageRecord.isExpirationTimerUpdate() || messageRecord.isEndSession() || messageRecord.isIdentityUpdate()) {
|
messageRecord.isExpirationTimerUpdate() || messageRecord.isEndSession() || messageRecord.isIdentityUpdate()) {
|
||||||
return MESSAGE_TYPE_UPDATE;
|
return MESSAGE_TYPE_UPDATE;
|
||||||
} else if (hasAudio(messageRecord)) {
|
} else if (hasAudio(messageRecord)) {
|
||||||
@ -216,7 +238,10 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||||||
return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.UNIQUE_ROW_ID));
|
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);
|
final SoftReference<MessageRecord> reference = messageRecordCache.get(type + messageId);
|
||||||
if (reference != null) {
|
if (reference != null) {
|
||||||
final MessageRecord record = reference.get();
|
final MessageRecord record = reference.get();
|
||||||
@ -254,4 +279,26 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||||||
private boolean hasThumbnail(MessageRecord messageRecord) {
|
private boolean hasThumbnail(MessageRecord messageRecord) {
|
||||||
return messageRecord.isMms() && ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide() != null;
|
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.view.ActionMode;
|
||||||
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.ItemAnimator.ItemAnimatorFinishedListener;
|
|
||||||
import android.support.v7.widget.RecyclerView.OnScrollListener;
|
import android.support.v7.widget.RecyclerView.OnScrollListener;
|
||||||
import android.text.ClipboardManager;
|
import android.text.ClipboardManager;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@ -60,6 +59,7 @@ import org.thoughtcrime.securesms.recipients.Recipients;
|
|||||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||||
|
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||||
|
|
||||||
@ -170,7 +170,10 @@ public class ConversationFragment extends Fragment
|
|||||||
|
|
||||||
private void initializeListAdapter() {
|
private void initializeListAdapter() {
|
||||||
if (this.recipients != null && this.threadId != -1) {
|
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);
|
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller.FastScrollAdapter;
|
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.HeaderViewHolder;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolder;
|
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolder;
|
||||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||||
|
@ -18,6 +18,7 @@ package org.thoughtcrime.securesms.util;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.text.format.DateFormat;
|
import android.text.format.DateFormat;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
@ -37,6 +38,10 @@ public class DateUtils extends android.text.format.DateUtils {
|
|||||||
return System.currentTimeMillis() - millis <= unit.toMillis(span);
|
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) {
|
private static int convertDelta(final long millis, TimeUnit to) {
|
||||||
return (int) to.convert(System.currentTimeMillis() - millis, TimeUnit.MILLISECONDS);
|
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);
|
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) {
|
private static String getLocalizedPattern(String template, Locale locale) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
return DateFormat.getBestDateTimePattern(locale, template);
|
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