diff --git a/build.gradle b/build.gradle index 8f0b269c0..8c604341c 100644 --- a/build.gradle +++ b/build.gradle @@ -7,11 +7,9 @@ repositories { google() mavenCentral() maven { - // Used only for PhotoView url "https://www.jitpack.io" name 'JitPack Github wrapper' } - jcenter() } android { @@ -35,7 +33,7 @@ android { applicationId "com.b44t.messenger" multiDexEnabled true - minSdkVersion 16 + minSdkVersion 19 targetSdkVersion 33 vectorDrawables.useSupportLibrary = true @@ -164,10 +162,11 @@ dependencies { implementation 'androidx.work:work-runtime:2.8.1' implementation 'androidx.emoji2:emoji2-emojipicker:1.4.0' implementation 'com.google.guava:guava:29.0-android' - implementation 'com.google.android.exoplayer:exoplayer-core:2.9.6' // plays video and audio - implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.6' + implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // plays video and audio + implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'com.journeyapps:zxing-android-embedded:3.4.0' // QR Code scanner + implementation 'com.google.zxing:core:3.3.0' // fixed version to support SDK<24 + implementation ('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } // QR Code scanner implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1' // used as JSON library implementation 'com.google.code.gson:gson:2.9.1' // used as JSON library. Don't upgrade to 2.10.1: https://github.com/deltachat/deltachat-android/pull/2610 implementation "me.leolin:ShortcutBadger:1.1.16" // display messagecount on the home screen icon. @@ -182,16 +181,15 @@ dependencies { implementation 'com.pnikosis:materialish-progress:1.5' // used only in the "Progress Wheel" in Share Activity. implementation 'com.soundcloud.android:android-crop:1.0.1@aar' // used for profile and group avatar selection in Android SDK<19 implementation 'com.nineoldandroids:library:2.4.0' // DEPRECATED! Used to slide in the half-camera. - implementation 'mobi.upod:time-duration-picker:1.1.3' // Used to pick the time for inactivity. - implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' // number of unread messages, + implementation 'com.github.amulyakhare:TextDrawable:558677ea31' // number of unread messages, // the one-letter circle for the contacts (when there is not avatar) and a white background. implementation 'com.googlecode.mp4parser:isoparser:1.0.6' // MP4 recoding; upgrading eg. to 1.1.22 breaks recoding, however, i have not investigated further, just reset to 1.0.6 implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') { // for the zooming on photos / media exclude group: 'com.android.support', module: 'support-annotations' } implementation 'com.annimon:stream:1.1.8' // brings future java streams api to SDK Version < 24 - implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4' // glues the current time segment text in the gallery to the top. + implementation 'com.getkeepsafe.relinker:relinker:1.4.4' // needed to avoid safe-content-resolver-v14 trying to fetch older non-existing version // Replacement for ContentResolver // that protects against the Surreptitious Sharing attack. // diff --git a/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridAdapter.java b/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridAdapter.java new file mode 100644 index 000000000..fb409a73d --- /dev/null +++ b/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridAdapter.java @@ -0,0 +1,612 @@ +package com.codewaves.stickyheadergrid; + +import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.security.InvalidParameterException; +import java.util.ArrayList; + +/** + * Created by Sergej Kravcenko on 4/24/2017. + * Copyright (c) 2017 Sergej Kravcenko + */ + +@SuppressWarnings({"unused", "WeakerAccess"}) +public abstract class StickyHeaderGridAdapter extends RecyclerView.Adapter { + public static final String TAG = "StickyHeaderGridAdapter"; + + public static final int TYPE_HEADER = 0; + public static final int TYPE_ITEM = 1; + + private ArrayList
mSections; + private int[] mSectionIndices; + private int mTotalItemNumber; + + @SuppressWarnings("WeakerAccess") + public static class ViewHolder extends RecyclerView.ViewHolder { + public ViewHolder(View itemView) { + super(itemView); + } + + public boolean isHeader() { + return false; + } + + + public int getSectionItemViewType() { + return StickyHeaderGridAdapter.externalViewType(getItemViewType()); + } + } + + public static class ItemViewHolder extends ViewHolder { + public ItemViewHolder(View itemView) { + super(itemView); + } + } + + public static class HeaderViewHolder extends ViewHolder { + public HeaderViewHolder(View itemView) { + super(itemView); + } + + @Override + public boolean isHeader() { + return true; + } + } + + private static class Section { + private int position; + private int itemNumber; + private int length; + } + + private void calculateSections() { + mSections = new ArrayList<>(); + + int total = 0; + int sectionCount = getSectionCount(); + for (int s = 0; s < sectionCount; s++) { + final Section section = new Section(); + section.position = total; + section.itemNumber = getSectionItemCount(s); + section.length = section.itemNumber + 1; + mSections.add(section); + + total += section.length; + } + mTotalItemNumber = total; + + total = 0; + mSectionIndices = new int[mTotalItemNumber]; + for (int s = 0; s < sectionCount; s++) { + final Section section = mSections.get(s); + for (int i = 0; i < section.length; i++) { + mSectionIndices[total + i] = s; + } + total += section.length; + } + } + + protected int getItemViewInternalType(int position) { + final int section = getAdapterPositionSection(position); + final Section sectionObject = mSections.get(section); + final int sectionPosition = position - sectionObject.position; + + return getItemViewInternalType(section, sectionPosition); + } + + private int getItemViewInternalType(int section, int position) { + return position == 0 ? TYPE_HEADER : TYPE_ITEM; + } + + static private int internalViewType(int type) { + return type & 0xFF; + } + + static private int externalViewType(int type) { + return type >> 8; + } + + @Override + final public int getItemCount() { + if (mSections == null) { + calculateSections(); + } + return mTotalItemNumber; + } + + @NonNull + @Override + final public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final int internalType = internalViewType(viewType); + final int externalType = externalViewType(viewType); + + switch (internalType) { + case TYPE_HEADER: + return onCreateHeaderViewHolder(parent, externalType); + case TYPE_ITEM: + return onCreateItemViewHolder(parent, externalType); + default: + throw new InvalidParameterException("Invalid viewType: " + viewType); + } + } + + @Override + final public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + if (mSections == null) { + calculateSections(); + } + + final int section = mSectionIndices[position]; + final int internalType = internalViewType(holder.getItemViewType()); + final int externalType = externalViewType(holder.getItemViewType()); + + switch (internalType) { + case TYPE_HEADER: + onBindHeaderViewHolder((HeaderViewHolder)holder, section); + break; + case TYPE_ITEM: + final ItemViewHolder itemHolder = (ItemViewHolder)holder; + final int offset = getItemSectionOffset(section, position); + onBindItemViewHolder((ItemViewHolder)holder, section, offset); + break; + default: + throw new InvalidParameterException("invalid viewType: " + internalType); + } + } + + @Override + final public int getItemViewType(int position) { + final int section = getAdapterPositionSection(position); + final Section sectionObject = mSections.get(section); + final int sectionPosition = position - sectionObject.position; + final int internalType = getItemViewInternalType(section, sectionPosition); + int externalType = 0; + + switch (internalType) { + case TYPE_HEADER: + externalType = getSectionHeaderViewType(section); + break; + case TYPE_ITEM: + externalType = getSectionItemViewType(section, sectionPosition - 1); + break; + } + + return ((externalType & 0xFF) << 8) | (internalType & 0xFF); + } + + // Helpers + private int getItemSectionHeaderPosition(int position) { + return getSectionHeaderPosition(getAdapterPositionSection(position)); + } + + private int getAdapterPosition(int section, int offset) { + if (mSections == null) { + calculateSections(); + } + + if (section < 0) { + throw new IndexOutOfBoundsException("section " + section + " < 0"); + } + + if (section >= mSections.size()) { + throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size()); + } + + final Section sectionObject = mSections.get(section); + return sectionObject.position + offset; + } + + /** + * Given a section and an adapter position get the offset of an item + * inside section. + * + * @param section section to query + * @param position adapter position + * @return The item offset inside the section. + */ + public int getItemSectionOffset(int section, int position) { + if (mSections == null) { + calculateSections(); + } + + if (section < 0) { + throw new IndexOutOfBoundsException("section " + section + " < 0"); + } + + if (section >= mSections.size()) { + throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size()); + } + + final Section sectionObject = mSections.get(section); + final int localPosition = position - sectionObject.position; + if (localPosition >= sectionObject.length) { + throw new IndexOutOfBoundsException("localPosition: " + localPosition + " >=" + sectionObject.length); + } + + return localPosition - 1; + } + + /** + * Returns the section index having item or header with provided + * provider position. + * + * @param position adapter position + * @return The section containing provided adapter position. + */ + public int getAdapterPositionSection(int position) { + if (mSections == null) { + calculateSections(); + } + + if (getItemCount() == 0) { + return NO_POSITION; + } + + if (position < 0) { + throw new IndexOutOfBoundsException("position " + position + " < 0"); + } + + if (position >= getItemCount()) { + throw new IndexOutOfBoundsException("position " + position + " >=" + getItemCount()); + } + + return mSectionIndices[position]; + } + + /** + * Returns the adapter position for given section header. Use + * this only for {@link RecyclerView#scrollToPosition(int)} or similar functions. + * Never directly manipulate adapter items using this position. + * + * @param section section to query + * @return The adapter position. + */ + public int getSectionHeaderPosition(int section) { + return getAdapterPosition(section, 0); + } + + /** + * Returns the adapter position for given section and + * offset. Use this only for {@link RecyclerView#scrollToPosition(int)} + * or similar functions. Never directly manipulate adapter items using this position. + * + * @param section section to query + * @param position item position inside the section + * @return The adapter position. + */ + public int getSectionItemPosition(int section, int position) { + return getAdapterPosition(section, position + 1); + } + + // Overrides + /** + * Returns the total number of sections in the data set held by the adapter. + * + * @return The total number of section in this adapter. + */ + public int getSectionCount() { + return 0; + } + + /** + * Returns the number of items in the section. + * + * @param section section to query + * @return The total number of items in the section. + */ + public int getSectionItemCount(int section) { + return 0; + } + + /** + * Return the view type of the section header for the purposes + * of view recycling. + * + *

The default implementation of this method returns 0, making the assumption of + * a single view type for the headers. Unlike ListView adapters, types need not + * be contiguous. Consider using id resources to uniquely identify item view types. + * + * @param section section to query + * @return integer value identifying the type of the view needed to represent the header in + * section. Type codes need not be contiguous. + */ + public int getSectionHeaderViewType(int section) { + return 0; + } + + /** + * Return the view type of the item at position in section for + * the purposes of view recycling. + * + *

The default implementation of this method returns 0, making the assumption of + * a single view type for the adapter. Unlike ListView adapters, types need not + * be contiguous. Consider using id resources to uniquely identify item view types. + * + * @param section section to query + * @param offset section position to query + * @return integer value identifying the type of the view needed to represent the item at + * position in section. Type codes need not be + * contiguous. + */ + public int getSectionItemViewType(int section, int offset) { + return 0; + } + + /** + * Returns true if header in section is sticky. + * + * @param section section to query + * @return true if section header is sticky. + */ + public boolean isSectionHeaderSticky(int section) { + return true; + } + + /** + * Called when RecyclerView needs a new {@link HeaderViewHolder} of the given type to represent + * a header. + *

+ * This new HeaderViewHolder should be constructed with a new View that can represent the headers + * of the given type. You can either create a new View manually or inflate it from an XML + * layout file. + *

+ * The new HeaderViewHolder will be used to display items of the adapter using + * {@link #onBindHeaderViewHolder(HeaderViewHolder, int)}. Since it will be re-used to display + * different items in the data set, it is a good idea to cache references to sub views of + * the View to avoid unnecessary {@link View#findViewById(int)} calls. + * + * @param parent The ViewGroup into which the new View will be added after it is bound to + * an adapter position. + * @param headerType The view type of the new View. + * + * @return A new ViewHolder that holds a View of the given view type. + * @see #getSectionHeaderViewType(int) + * @see #onBindHeaderViewHolder(HeaderViewHolder, int) + */ + public abstract HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType); + + /** + * Called when RecyclerView needs a new {@link ItemViewHolder} of the given type to represent + * an item. + *

+ * This new ViewHolder should be constructed with a new View that can represent the items + * of the given type. You can either create a new View manually or inflate it from an XML + * layout file. + *

+ * The new ViewHolder will be used to display items of the adapter using + * {@link #onBindItemViewHolder(ItemViewHolder, int, int)}. Since it will be re-used to display + * different items in the data set, it is a good idea to cache references to sub views of + * the View to avoid unnecessary {@link View#findViewById(int)} calls. + * + * @param parent The ViewGroup into which the new View will be added after it is bound to + * an adapter position. + * @param itemType The view type of the new View. + * + * @return A new ViewHolder that holds a View of the given view type. + * @see #getSectionItemViewType(int, int) + * @see #onBindItemViewHolder(ItemViewHolder, int, int) + */ + public abstract ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType); + + /** + * Called by RecyclerView to display the data at the specified position. This method should + * update the contents of the {@link HeaderViewHolder#itemView} to reflect the header at the given + * position. + *

+ * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method + * again if the position of the header changes in the data set unless the header itself is + * invalidated or the new position cannot be determined. For this reason, you should only + * use the section parameter while acquiring the + * related header data inside this method and should not keep a copy of it. If you need the + * position of a header later on (e.g. in a click listener), use + * {@link HeaderViewHolder#getAdapterPosition()} which will have the updated adapter + * position. Then you can use {@link #getAdapterPositionSection(int)} to get section index. + * + * + * @param viewHolder The ViewHolder which should be updated to represent the contents of the + * header at the given position in the data set. + * @param section The index of the section. + */ + public abstract void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int section); + + /** + * Called by RecyclerView to display the data at the specified position. This method should + * update the contents of the {@link ItemViewHolder#itemView} to reflect the item at the given + * position. + *

+ * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method + * again if the position of the item changes in the data set unless the item itself is + * invalidated or the new position cannot be determined. For this reason, you should only + * use the offset and section parameters while acquiring the + * related data item inside this method and should not keep a copy of it. If you need the + * position of an item later on (e.g. in a click listener), use + * {@link ItemViewHolder#getAdapterPosition()} which will have the updated adapter + * position. Then you can use {@link #getAdapterPositionSection(int)} and + * {@link #getItemSectionOffset(int, int)} + * + * + * @param viewHolder The ViewHolder which should be updated to represent the contents of the + * item at the given position in the data set. + * @param section The index of the section. + * @param offset The position of the item within the section. + */ + public abstract void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset); + + // Notify + /** + * Notify any registered observers that the data set has changed. + * + *

There are two different classes of data change events, item changes and structural + * changes. Item changes are when a single item has its data updated but no positional + * changes have occurred. Structural changes are when items are inserted, removed or moved + * within the data set.

+ * + *

This event does not specify what about the data set has changed, forcing + * any observers to assume that all existing items and structure may no longer be valid. + * LayoutManagers will be forced to fully rebind and relayout all visible views.

+ * + *

RecyclerView will attempt to synthesize visible structural change events + * for adapters that report that they have {@link #hasStableIds() stable IDs} when + * this method is used. This can help for the purposes of animation and visual + * object persistence but individual item views will still need to be rebound + * and relaid out.

+ * + *

If you are writing an adapter it will always be more efficient to use the more + * specific change events if you can. Rely on notifyDataSetChanged() + * as a last resort.

+ * + * @see #notifySectionDataSetChanged(int) + * @see #notifySectionHeaderChanged(int) + * @see #notifySectionItemChanged(int, int) + * @see #notifySectionInserted(int) + * @see #notifySectionItemInserted(int, int) + * @see #notifySectionItemRangeInserted(int, int, int) + * @see #notifySectionRemoved(int) + * @see #notifySectionItemRemoved(int, int) + * @see #notifySectionItemRangeRemoved(int, int, int) + */ + public void notifyAllSectionsDataSetChanged() { + calculateSections(); + notifyDataSetChanged(); + } + + public void notifySectionDataSetChanged(int section) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } + else { + final Section sectionObject = mSections.get(section); + notifyItemRangeChanged(sectionObject.position, sectionObject.length); + } + } + + public void notifySectionHeaderChanged(int section) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } + else { + final Section sectionObject = mSections.get(section); + notifyItemRangeChanged(sectionObject.position, 1); + } + } + + public void notifySectionItemChanged(int section, int position) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } + else { + final Section sectionObject = mSections.get(section); + + if (position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); + } + + notifyItemChanged(sectionObject.position + position + 1); + } + } + + public void notifySectionInserted(int section) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } + else { + final Section sectionObject = mSections.get(section); + notifyItemRangeInserted(sectionObject.position, sectionObject.length); + } + } + + public void notifySectionItemInserted(int section, int position) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } + else { + final Section sectionObject = mSections.get(section); + + if (position < 0 || position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); + } + + notifyItemInserted(sectionObject.position + position + 1); + } + } + + public void notifySectionItemRangeInserted(int section, int position, int count) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } + else { + final Section sectionObject = mSections.get(section); + + if (position < 0 || position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); + } + if (position + count > sectionObject.itemNumber) { + throw new IndexOutOfBoundsException("Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber); + } + + notifyItemRangeInserted(sectionObject.position + position + 1, count); + } + } + + public void notifySectionRemoved(int section) { + if (mSections == null) { + calculateSections(); + notifyAllSectionsDataSetChanged(); + } + else { + final Section sectionObject = mSections.get(section); + calculateSections(); + notifyItemRangeRemoved(sectionObject.position, sectionObject.length); + } + } + + public void notifySectionItemRemoved(int section, int position) { + if (mSections == null) { + calculateSections(); + notifyAllSectionsDataSetChanged(); + } + else { + final Section sectionObject = mSections.get(section); + + if (position < 0 || position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); + } + + calculateSections(); + notifyItemRemoved(sectionObject.position + position + 1); + } + } + + public void notifySectionItemRangeRemoved(int section, int position, int count) { + if (mSections == null) { + calculateSections(); + notifyAllSectionsDataSetChanged(); + } + else { + final Section sectionObject = mSections.get(section); + + if (position < 0 || position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); + } + if (position + count > sectionObject.itemNumber) { + throw new IndexOutOfBoundsException("Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber); + } + + calculateSections(); + notifyItemRangeRemoved(sectionObject.position + position + 1, count); + } + } +} diff --git a/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManager.java b/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManager.java new file mode 100644 index 000000000..24aa99eea --- /dev/null +++ b/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManager.java @@ -0,0 +1,1372 @@ +package com.codewaves.stickyheadergrid; + +import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; +import static com.codewaves.stickyheadergrid.StickyHeaderGridAdapter.TYPE_HEADER; +import static com.codewaves.stickyheadergrid.StickyHeaderGridAdapter.TYPE_ITEM; + +import android.content.Context; +import android.graphics.PointF; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Created by Sergej Kravcenko on 4/24/2017. + * Copyright (c) 2017 Sergej Kravcenko + */ + +@SuppressWarnings({"unused", "WeakerAccess"}) +public class StickyHeaderGridLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider { + public static final String TAG = "StickyLayoutManager"; + + private static final int DEFAULT_ROW_COUNT = 16; + + private int mSpanCount; + private SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup(); + + private StickyHeaderGridAdapter mAdapter; + + private int mHeadersStartPosition; + + private View mFloatingHeaderView; + private int mFloatingHeaderPosition; + private int mStickOffset; + private int mAverageHeaderHeight; + private int mHeaderOverlapMargin; + + private HeaderStateChangeListener mHeaderStateListener; + private int mStickyHeaderSection = NO_POSITION; + private View mStickyHeaderView; + private HeaderState mStickyHeadeState; + + private View mFillViewSet[]; + + private SavedState mPendingSavedState; + private int mPendingScrollPosition = NO_POSITION; + private int mPendingScrollPositionOffset; + private AnchorPosition mAnchor = new AnchorPosition(); + + private final FillResult mFillResult = new FillResult(); + private ArrayList mLayoutRows = new ArrayList<>(DEFAULT_ROW_COUNT); + + public enum HeaderState { + NORMAL, + STICKY, + PUSHED + } + + /** + * The interface to be implemented by listeners to header events from this + * LayoutManager. + */ + public interface HeaderStateChangeListener { + /** + * Called when a section header state changes. The position can be HeaderState.NORMAL, + * HeaderState.STICKY, HeaderState.PUSHED. + * + *

+ *

    + *
  • NORMAL - the section header is invisible or has normal position
  • + *
  • STICKY - the section header is sticky at the top of RecyclerView
  • + *
  • PUSHED - the section header is sticky and pushed up by next header
  • + *
0) { + state.mAnchorSection = mAnchor.section; + state.mAnchorItem = mAnchor.item; + state.mAnchorOffset = mAnchor.offset; + } + else { + state.invalidateAnchor(); + } + + return state; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof SavedState) { + mPendingSavedState = (SavedState) state; + requestLayout(); + } + else { + Log.d(TAG, "invalid saved state class"); + } + } + + @Override + public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { + return lp instanceof LayoutParams; + } + + @Override + public boolean canScrollVertically() { + return true; + } + + /** + *

Scroll the RecyclerView to make the position visible.

+ * + *

RecyclerView will scroll the minimum amount that is necessary to make the + * target position visible. + * + *

Note that scroll position change will not be reflected until the next layout call.

+ * + * @param position Scroll to this adapter position + */ + @Override + public void scrollToPosition(int position) { + if (position < 0 || position > getItemCount()) { + throw new IndexOutOfBoundsException("adapter position out of range"); + } + + mPendingScrollPosition = position; + mPendingScrollPositionOffset = 0; + if (mPendingSavedState != null) { + mPendingSavedState.invalidateAnchor(); + } + requestLayout(); + } + + private int getExtraLayoutSpace(RecyclerView.State state) { + if (state.hasTargetScrollPosition()) { + return getHeight(); + } + else { + return 0; + } + } + + @Override + public void smoothScrollToPosition(final RecyclerView recyclerView, RecyclerView.State state, int position) { + final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) { + @Override + public int calculateDyToMakeVisible(View view, int snapPreference) { + final RecyclerView.LayoutManager layoutManager = getLayoutManager(); + if (layoutManager == null || !layoutManager.canScrollVertically()) { + return 0; + } + + final int adapterPosition = getPosition(view); + final int topOffset = getPositionSectionHeaderHeight(adapterPosition); + final int top = layoutManager.getDecoratedTop(view); + final int bottom = layoutManager.getDecoratedBottom(view); + final int start = layoutManager.getPaddingTop() + topOffset; + final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom(); + return calculateDtToFit(top, bottom, start, end, snapPreference); + } + }; + linearSmoothScroller.setTargetPosition(position); + startSmoothScroll(linearSmoothScroller); + } + + @Override + public PointF computeScrollVectorForPosition(int targetPosition) { + if (getChildCount() == 0) { + return null; + } + + final LayoutRow firstRow = getFirstVisibleRow(); + if (firstRow == null) { + return null; + } + + return new PointF(0, targetPosition - firstRow.adapterPosition); + } + + private int getAdapterPositionFromAnchor(AnchorPosition anchor) { + if (anchor.section < 0 || anchor.section >= mAdapter.getSectionCount()) { + anchor.reset(); + return NO_POSITION; + } + else if (anchor.item < 0 || anchor.item >= mAdapter.getSectionItemCount(anchor.section)) { + anchor.offset = 0; + return mAdapter.getSectionHeaderPosition(anchor.section); + } + return mAdapter.getSectionItemPosition(anchor.section, anchor.item); + } + + private int getAdapterPositionChecked(int section, int offset) { + if (section < 0 || section >= mAdapter.getSectionCount()) { + return NO_POSITION; + } + else if (offset < 0 || offset >= mAdapter.getSectionItemCount(section)) { + return mAdapter.getSectionHeaderPosition(section); + } + return mAdapter.getSectionItemPosition(section, offset); + } + + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + if (mAdapter == null || state.getItemCount() == 0) { + removeAndRecycleAllViews(recycler); + clearState(); + return; + } + + int pendingAdapterPosition; + int pendingAdapterOffset; + if (mPendingScrollPosition >= 0) { + pendingAdapterPosition = mPendingScrollPosition; + pendingAdapterOffset = mPendingScrollPositionOffset; + } + else if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { + pendingAdapterPosition = getAdapterPositionChecked(mPendingSavedState.mAnchorSection, mPendingSavedState.mAnchorItem); + pendingAdapterOffset = mPendingSavedState.mAnchorOffset; + mPendingSavedState = null; + } + else { + pendingAdapterPosition = getAdapterPositionFromAnchor(mAnchor); + pendingAdapterOffset = mAnchor.offset; + } + + if (pendingAdapterPosition < 0 || pendingAdapterPosition >= state.getItemCount()) { + pendingAdapterPosition = 0; + pendingAdapterOffset = 0; + mPendingScrollPosition = NO_POSITION; + } + + if (pendingAdapterOffset > 0) { + pendingAdapterOffset = 0; + } + + detachAndScrapAttachedViews(recycler); + clearState(); + + // Make sure mFirstViewPosition is the start of the row + pendingAdapterPosition = findFirstRowItem(pendingAdapterPosition); + + int left = getPaddingLeft(); + int right = getWidth() - getPaddingRight(); + final int recyclerBottom = getHeight() - getPaddingBottom(); + int totalHeight = 0; + + int adapterPosition = pendingAdapterPosition; + int top = getPaddingTop() + pendingAdapterOffset; + while (true) { + if (adapterPosition >= state.getItemCount()) { + break; + } + + int bottom; + final int viewType = mAdapter.getItemViewInternalType(adapterPosition); + if (viewType == TYPE_HEADER) { + final View v = recycler.getViewForPosition(adapterPosition); + addView(v); + measureChildWithMargins(v, 0, 0); + + int height = getDecoratedMeasuredHeight(v); + final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height; + bottom = top + height; + layoutDecorated(v, left, top, right, bottom); + + bottom -= margin; + height -= margin; + mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, bottom)); + adapterPosition++; + mAverageHeaderHeight = height; + } + else { + final FillResult result = fillBottomRow(recycler, state, adapterPosition, top); + bottom = top + result.height; + mLayoutRows.add(new LayoutRow(result.adapterPosition, result.length, top, bottom)); + adapterPosition += result.length; + } + top = bottom; + + if (bottom >= recyclerBottom + getExtraLayoutSpace(state)) { + break; + } + } + + if (getBottomRow().bottom < recyclerBottom) { + scrollVerticallyBy(getBottomRow().bottom - recyclerBottom, recycler, state); + } + else { + clearViewsAndStickHeaders(recycler, state, false); + } + + // If layout was caused by the pending scroll, adjust top item position and move it under sticky header + if (mPendingScrollPosition >= 0) { + mPendingScrollPosition = NO_POSITION; + + final int topOffset = getPositionSectionHeaderHeight(pendingAdapterPosition); + if (topOffset != 0) { + scrollVerticallyBy(-topOffset, recycler, state); + } + } + } + + @Override + public void onLayoutCompleted(RecyclerView.State state) { + super.onLayoutCompleted(state); + mPendingSavedState = null; + } + + private int getPositionSectionHeaderHeight(int adapterPosition) { + final int section = mAdapter.getAdapterPositionSection(adapterPosition); + if (section >= 0 && mAdapter.isSectionHeaderSticky(section)) { + final int offset = mAdapter.getItemSectionOffset(section, adapterPosition); + if (offset >= 0) { + final int headerAdapterPosition = mAdapter.getSectionHeaderPosition(section); + if (mFloatingHeaderView != null && headerAdapterPosition == mFloatingHeaderPosition) { + return Math.max(0, getDecoratedMeasuredHeight(mFloatingHeaderView) - mHeaderOverlapMargin); + } + else { + final LayoutRow header = getHeaderRow(headerAdapterPosition); + if (header != null) { + return header.getHeight(); + } + else { + // Fall back to cached header size, can be incorrect + return mAverageHeaderHeight; + } + } + } + } + + return 0; + } + + private int findFirstRowItem(int adapterPosition) { + final int section = mAdapter.getAdapterPositionSection(adapterPosition); + int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); + while (sectionPosition > 0 && mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount) != 0) { + sectionPosition--; + adapterPosition--; + } + + return adapterPosition; + } + + private int getSpanWidth(int recyclerWidth, int spanIndex, int spanSize) { + final int spanWidth = recyclerWidth / mSpanCount; + final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount; + final int widthCorrection = Math.min(Math.max(0, spanWidthReminder - spanIndex), spanSize); + + return spanWidth * spanSize + widthCorrection; + } + + private int getSpanLeft(int recyclerWidth, int spanIndex) { + final int spanWidth = recyclerWidth / mSpanCount; + final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount; + final int widthCorrection = Math.min(spanWidthReminder, spanIndex); + + return spanWidth * spanIndex + widthCorrection; + } + + private FillResult fillBottomRow(RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) { + final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + final int section = mAdapter.getAdapterPositionSection(position); + int adapterPosition = position; + int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); + int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); + int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount); + int count = 0; + int maxHeight = 0; + + // Create phase + Arrays.fill(mFillViewSet, null); + while (spanIndex + spanSize <= mSpanCount) { + // Create view and fill layout params + final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize); + final View v = recycler.getViewForPosition(adapterPosition); + final LayoutParams params = (LayoutParams)v.getLayoutParams(); + params.mSpanIndex = spanIndex; + params.mSpanSize = spanSize; + + addView(v, mHeadersStartPosition); + mHeadersStartPosition++; + measureChildWithMargins(v, recyclerWidth - spanWidth, 0); + mFillViewSet[count] = v; + count++; + + final int height = getDecoratedMeasuredHeight(v); + if (maxHeight < height) { + maxHeight = height; + } + + // Check next + adapterPosition++; + sectionPosition++; + if (sectionPosition >= mAdapter.getSectionItemCount(section)) { + break; + } + + spanIndex += spanSize; + spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); + } + + // Layout phase + int left = getPaddingLeft(); + for (int i = 0; i < count; ++i) { + final View v = mFillViewSet[i]; + final int height = getDecoratedMeasuredHeight(v); + final int width = getDecoratedMeasuredWidth(v); + layoutDecorated(v, left, top, left + width, top + height); + left += width; + } + + mFillResult.edgeView = mFillViewSet[count - 1]; + mFillResult.adapterPosition = position; + mFillResult.length = count; + mFillResult.height = maxHeight; + + return mFillResult; + } + + private FillResult fillTopRow(RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) { + final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + final int section = mAdapter.getAdapterPositionSection(position); + int adapterPosition = position; + int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); + int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); + int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount); + int count = 0; + int maxHeight = 0; + + Arrays.fill(mFillViewSet, null); + while (spanIndex >= 0) { + // Create view and fill layout params + final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize); + final View v = recycler.getViewForPosition(adapterPosition); + final LayoutParams params = (LayoutParams)v.getLayoutParams(); + params.mSpanIndex = spanIndex; + params.mSpanSize = spanSize; + + addView(v, 0); + mHeadersStartPosition++; + measureChildWithMargins(v, recyclerWidth - spanWidth, 0); + mFillViewSet[count] = v; + count++; + + final int height = getDecoratedMeasuredHeight(v); + if (maxHeight < height) { + maxHeight = height; + } + + // Check next + adapterPosition--; + sectionPosition--; + if (sectionPosition < 0) { + break; + } + + spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); + spanIndex -= spanSize; + } + + // Layout phase + int left = getPaddingLeft(); + for (int i = count - 1; i >= 0; --i) { + final View v = mFillViewSet[i]; + final int height = getDecoratedMeasuredHeight(v); + final int width = getDecoratedMeasuredWidth(v); + layoutDecorated(v, left, top - maxHeight, left + width, top - (maxHeight - height)); + left += width; + } + + mFillResult.edgeView = mFillViewSet[count - 1]; + mFillResult.adapterPosition = adapterPosition + 1; + mFillResult.length = count; + mFillResult.height = maxHeight; + + return mFillResult; + } + + private void clearHiddenRows(RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) { + if (mLayoutRows.size() <= 0) { + return; + } + + final int recyclerTop = getPaddingTop(); + final int recyclerBottom = getHeight() - getPaddingBottom(); + + if (top) { + LayoutRow row = getTopRow(); + while (row.bottom < recyclerTop - getExtraLayoutSpace(state) || row.top > recyclerBottom) { + if (row.header) { + removeAndRecycleViewAt(mHeadersStartPosition + (mFloatingHeaderView != null ? 1 : 0), recycler); + } + else { + for (int i = 0; i < row.length; ++i) { + removeAndRecycleViewAt(0, recycler); + mHeadersStartPosition--; + } + } + mLayoutRows.remove(0); + row = getTopRow(); + } + } + else { + LayoutRow row = getBottomRow(); + while (row.bottom < recyclerTop || row.top > recyclerBottom + getExtraLayoutSpace(state)) { + if (row.header) { + removeAndRecycleViewAt(getChildCount() - 1, recycler); + } + else { + for (int i = 0; i < row.length; ++i) { + removeAndRecycleViewAt(mHeadersStartPosition - 1, recycler); + mHeadersStartPosition--; + } + } + mLayoutRows.remove(mLayoutRows.size() - 1); + row = getBottomRow(); + } + } + } + + private void clearViewsAndStickHeaders(RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) { + clearHiddenRows(recycler, state, top); + if (getChildCount() > 0) { + stickTopHeader(recycler); + } + updateTopPosition(); + } + + private LayoutRow getBottomRow() { + return mLayoutRows.get(mLayoutRows.size() - 1); + } + + private LayoutRow getTopRow() { + return mLayoutRows.get(0); + } + + private void offsetRowsVertical(int offset) { + for (LayoutRow row : mLayoutRows) { + row.top += offset; + row.bottom += offset; + } + offsetChildrenVertical(offset); + } + + private void addRow(RecyclerView.Recycler recycler, RecyclerView.State state, boolean isTop, int adapterPosition, int top) { + final int left = getPaddingLeft(); + final int right = getWidth() - getPaddingRight(); + + // Reattach floating header if needed + if (isTop && mFloatingHeaderView != null && adapterPosition == mFloatingHeaderPosition) { + removeFloatingHeader(recycler); + } + + final int viewType = mAdapter.getItemViewInternalType(adapterPosition); + if (viewType == TYPE_HEADER) { + final View v = recycler.getViewForPosition(adapterPosition); + if (isTop) { + addView(v, mHeadersStartPosition); + } + else { + addView(v); + } + measureChildWithMargins(v, 0, 0); + final int height = getDecoratedMeasuredHeight(v); + final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height; + if (isTop) { + layoutDecorated(v, left, top - height + margin, right, top + margin); + mLayoutRows.add(0, new LayoutRow(v, adapterPosition, 1, top - height + margin, top)); + } + else { + layoutDecorated(v, left, top, right, top + height); + mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, top + height - margin)); + } + mAverageHeaderHeight = height - margin; + } + else { + if (isTop) { + final FillResult result = fillTopRow(recycler, state, adapterPosition, top); + mLayoutRows.add(0, new LayoutRow(result.adapterPosition, result.length, top - result.height, top)); + } + else { + final FillResult result = fillBottomRow(recycler, state, adapterPosition, top); + mLayoutRows.add(new LayoutRow(result.adapterPosition, result.length, top, top + result.height)); + } + } + } + + private void addOffScreenRows(RecyclerView.Recycler recycler, RecyclerView.State state, int recyclerTop, int recyclerBottom, boolean bottom) { + if (bottom) { + // Bottom + while (true) { + final LayoutRow bottomRow = getBottomRow(); + final int adapterPosition = bottomRow.adapterPosition + bottomRow.length; + if (bottomRow.bottom >= recyclerBottom + getExtraLayoutSpace(state) || adapterPosition >= state.getItemCount()) { + break; + } + addRow(recycler, state, false, adapterPosition, bottomRow.bottom); + } + } + else { + // Top + while (true) { + final LayoutRow topRow = getTopRow(); + final int adapterPosition = topRow.adapterPosition - 1; + if (topRow.top < recyclerTop - getExtraLayoutSpace(state) || adapterPosition < 0) { + break; + } + addRow(recycler, state, true, adapterPosition, topRow.top); + } + } + } + + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (getChildCount() == 0) { + return 0; + } + + int scrolled = 0; + int left = getPaddingLeft(); + int right = getWidth() - getPaddingRight(); + final int recyclerTop = getPaddingTop(); + final int recyclerBottom = getHeight() - getPaddingBottom(); + + // If we have simple header stick, offset it back + final int firstHeader = getFirstVisibleSectionHeader(); + if (firstHeader != NO_POSITION) { + mLayoutRows.get(firstHeader).headerView.offsetTopAndBottom(-mStickOffset); + } + + if (dy >= 0) { + // Up + while (scrolled < dy) { + final LayoutRow bottomRow = getBottomRow(); + final int scrollChunk = -Math.min(Math.max(bottomRow.bottom - recyclerBottom, 0), dy - scrolled); + + offsetRowsVertical(scrollChunk); + scrolled -= scrollChunk; + + final int adapterPosition = bottomRow.adapterPosition + bottomRow.length; + if (scrolled >= dy || adapterPosition >= state.getItemCount()) { + break; + } + + addRow(recycler, state, false, adapterPosition, bottomRow.bottom); + } + } + else { + // Down + while (scrolled > dy) { + final LayoutRow topRow = getTopRow(); + final int scrollChunk = Math.min(Math.max(-topRow.top + recyclerTop, 0), scrolled - dy); + + offsetRowsVertical(scrollChunk); + scrolled -= scrollChunk; + + final int adapterPosition = topRow.adapterPosition - 1; + if (scrolled <= dy || adapterPosition >= state.getItemCount() || adapterPosition < 0) { + break; + } + + addRow(recycler, state, true, adapterPosition, topRow.top); + } + } + + // Fill extra offscreen rows for smooth scroll + if (scrolled == dy) { + addOffScreenRows(recycler, state, recyclerTop, recyclerBottom, dy >= 0); + } + + clearViewsAndStickHeaders(recycler, state, dy >= 0); + return scrolled; + } + + /** + * Returns first visible item excluding headers. + * + * @param visibleTop Whether item top edge should be visible or not + * @return The first visible item adapter position closest to top of the layout. + */ + public int getFirstVisibleItemPosition(boolean visibleTop) { + return getFirstVisiblePosition(TYPE_ITEM, visibleTop); + } + + /** + * Returns last visible item excluding headers. + * + * @return The last visible item adapter position closest to bottom of the layout. + */ + public int getLastVisibleItemPosition() { + return getLastVisiblePosition(TYPE_ITEM); + } + + /** + * Returns first visible header. + * + * @param visibleTop Whether header top edge should be visible or not + * @return The first visible header adapter position closest to top of the layout. + */ + public int getFirstVisibleHeaderPosition(boolean visibleTop) { + return getFirstVisiblePosition(TYPE_HEADER, visibleTop); + } + + /** + * Returns last visible header. + * + * @return The last visible header adapter position closest to bottom of the layout. + */ + public int getLastVisibleHeaderPosition() { + return getLastVisiblePosition(TYPE_HEADER); + } + + private int getFirstVisiblePosition(int type, boolean visibleTop) { + if (type == TYPE_ITEM && mHeadersStartPosition <= 0) { + return NO_POSITION; + } + else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) { + return NO_POSITION; + } + + int viewFrom = type == TYPE_ITEM ? 0 : mHeadersStartPosition; + int viewTo = type == TYPE_ITEM ? mHeadersStartPosition : getChildCount(); + final int recyclerTop = getPaddingTop(); + for (int i = viewFrom; i < viewTo; ++i) { + final View v = getChildAt(i); + final int adapterPosition = getPosition(v); + final int headerHeight = getPositionSectionHeaderHeight(adapterPosition); + final int top = getDecoratedTop(v); + final int bottom = getDecoratedBottom(v); + + if (visibleTop) { + if (top >= recyclerTop + headerHeight) { + return adapterPosition; + } + } + else { + if (bottom >= recyclerTop + headerHeight) { + return adapterPosition; + } + } + } + + return NO_POSITION; + } + + private int getLastVisiblePosition(int type) { + if (type == TYPE_ITEM && mHeadersStartPosition <= 0) { + return NO_POSITION; + } + else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) { + return NO_POSITION; + } + + int viewFrom = type == TYPE_ITEM ? mHeadersStartPosition - 1 : getChildCount() - 1; + int viewTo = type == TYPE_ITEM ? 0 : mHeadersStartPosition; + final int recyclerBottom = getHeight() - getPaddingBottom(); + for (int i = viewFrom; i >= viewTo; --i) { + final View v = getChildAt(i); + final int top = getDecoratedTop(v); + + if (top < recyclerBottom) { + return getPosition(v); + } + } + + return NO_POSITION; + } + + private LayoutRow getFirstVisibleRow() { + final int recyclerTop = getPaddingTop(); + for (LayoutRow row : mLayoutRows) { + if (row.bottom > recyclerTop) { + return row; + } + } + return null; + } + + private int getFirstVisibleSectionHeader() { + final int recyclerTop = getPaddingTop(); + + int header = NO_POSITION; + for (int i = 0, n = mLayoutRows.size(); i < n; ++i) { + final LayoutRow row = mLayoutRows.get(i); + if (row.header) { + header = i; + } + if (row.bottom > recyclerTop) { + return header; + } + } + return NO_POSITION; + } + + private LayoutRow getNextVisibleSectionHeader(int headerFrom) { + for (int i = headerFrom + 1, n = mLayoutRows.size(); i < n; ++i) { + final LayoutRow row = mLayoutRows.get(i); + if (row.header) { + return row; + } + } + return null; + } + + private LayoutRow getHeaderRow(int adapterPosition) { + for (int i = 0, n = mLayoutRows.size(); i < n; ++i) { + final LayoutRow row = mLayoutRows.get(i); + if (row.header && row.adapterPosition == adapterPosition) { + return row; + } + } + return null; + } + + private void removeFloatingHeader(RecyclerView.Recycler recycler) { + if (mFloatingHeaderView == null) { + return; + } + + final View view = mFloatingHeaderView; + mFloatingHeaderView = null; + mFloatingHeaderPosition = NO_POSITION; + removeAndRecycleView(view, recycler); + } + + private void onHeaderChanged(int section, View view, HeaderState state, int pushOffset) { + if (mStickyHeaderSection != NO_POSITION && section != mStickyHeaderSection) { + onHeaderUnstick(); + } + + final boolean headerStateChanged = mStickyHeaderSection != section || !mStickyHeadeState.equals(state) || state.equals(HeaderState.PUSHED); + + mStickyHeaderSection = section; + mStickyHeaderView = view; + mStickyHeadeState = state; + + if (headerStateChanged && mHeaderStateListener != null) { + mHeaderStateListener.onHeaderStateChanged(section, view, state, pushOffset); + } + } + + private void onHeaderUnstick() { + if (mStickyHeaderSection != NO_POSITION) { + if (mHeaderStateListener != null) { + mHeaderStateListener.onHeaderStateChanged(mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0); + } + mStickyHeaderSection = NO_POSITION; + mStickyHeaderView = null; + mStickyHeadeState = HeaderState.NORMAL; + } + } + + private void stickTopHeader(RecyclerView.Recycler recycler) { + final int firstHeader = getFirstVisibleSectionHeader(); + final int top = getPaddingTop(); + final int left = getPaddingLeft(); + final int right = getWidth() - getPaddingRight(); + + int notifySection = NO_POSITION; + View notifyView = null; + HeaderState notifyState = HeaderState.NORMAL; + int notifyOffset = 0; + + if (firstHeader != NO_POSITION) { + // Top row is header, floating header is not visible, remove + removeFloatingHeader(recycler); + + final LayoutRow firstHeaderRow = mLayoutRows.get(firstHeader); + final int section = mAdapter.getAdapterPositionSection(firstHeaderRow.adapterPosition); + if (mAdapter.isSectionHeaderSticky(section)) { + final LayoutRow nextHeaderRow = getNextVisibleSectionHeader(firstHeader); + int offset = 0; + if (nextHeaderRow != null) { + final int height = firstHeaderRow.getHeight(); + offset = Math.min(Math.max(top - nextHeaderRow.top, -height) + height, height); + } + + mStickOffset = top - firstHeaderRow.top - offset; + firstHeaderRow.headerView.offsetTopAndBottom(mStickOffset); + + onHeaderChanged(section, firstHeaderRow.headerView, offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, offset); + } + else { + onHeaderUnstick(); + mStickOffset = 0; + } + } + else { + // We don't have first visible sector header in layout, create floating + final LayoutRow firstVisibleRow = getFirstVisibleRow(); + if (firstVisibleRow != null) { + final int section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition); + if (mAdapter.isSectionHeaderSticky(section)) { + final int headerPosition = mAdapter.getSectionHeaderPosition(section); + if (mFloatingHeaderView == null || mFloatingHeaderPosition != headerPosition) { + removeFloatingHeader(recycler); + + // Create floating header + final View v = recycler.getViewForPosition(headerPosition); + addView(v, mHeadersStartPosition); + measureChildWithMargins(v, 0, 0); + mFloatingHeaderView = v; + mFloatingHeaderPosition = headerPosition; + } + + // Push floating header up, if needed + final int height = getDecoratedMeasuredHeight(mFloatingHeaderView); + int offset = 0; + if (getChildCount() - mHeadersStartPosition > 1) { + final View nextHeader = getChildAt(mHeadersStartPosition + 1); + final int contentHeight = Math.max(0, height - mHeaderOverlapMargin); + offset = Math.max(top - getDecoratedTop(nextHeader), -contentHeight) + contentHeight; + } + + layoutDecorated(mFloatingHeaderView, left, top - offset, right, top + height - offset); + onHeaderChanged(section, mFloatingHeaderView, offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, offset); + } + else { + onHeaderUnstick(); + } + } + else { + onHeaderUnstick(); + } + } + } + + private void updateTopPosition() { + if (getChildCount() == 0) { + mAnchor.reset(); + } + + final LayoutRow firstVisibleRow = getFirstVisibleRow(); + if (firstVisibleRow != null) { + mAnchor.section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition); + mAnchor.item = mAdapter.getItemSectionOffset(mAnchor.section, firstVisibleRow.adapterPosition); + mAnchor.offset = Math.min(firstVisibleRow.top - getPaddingTop(), 0); + } + } + + private int getViewType(View view) { + return getItemViewType(view) & 0xFF; + } + + private int getViewType(int position) { + return mAdapter.getItemViewType(position) & 0xFF; + } + + private void clearState() { + mHeadersStartPosition = 0; + mStickOffset = 0; + mFloatingHeaderView = null; + mFloatingHeaderPosition = -1; + mAverageHeaderHeight = 0; + mLayoutRows.clear(); + + if (mStickyHeaderSection != NO_POSITION) { + if (mHeaderStateListener != null) { + mHeaderStateListener.onHeaderStateChanged(mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0); + } + mStickyHeaderSection = NO_POSITION; + mStickyHeaderView = null; + mStickyHeadeState = HeaderState.NORMAL; + } + } + + @Override + public int computeVerticalScrollExtent(RecyclerView.State state) { + if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { + return 0; + } + + final View startChild = getChildAt(0); + final View endChild = getChildAt(mHeadersStartPosition - 1); + if (startChild == null || endChild == null) { + return 0; + } + + return Math.abs(getPosition(startChild) - getPosition(endChild)) + 1; + } + + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { + return 0; + } + + final View startChild = getChildAt(0); + final View endChild = getChildAt(mHeadersStartPosition - 1); + if (startChild == null || endChild == null) { + return 0; + } + + final int recyclerTop = getPaddingTop(); + final LayoutRow topRow = getTopRow(); + final int scrollChunk = Math.max(-topRow.top + recyclerTop, 0); + if (scrollChunk == 0) { + return 0; + } + + final int minPosition = Math.min(getPosition(startChild), getPosition(endChild)); + final int maxPosition = Math.max(getPosition(startChild), getPosition(endChild)); + return Math.max(0, minPosition); + } + + @Override + public int computeVerticalScrollRange(RecyclerView.State state) { + if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { + return 0; + } + + final View startChild = getChildAt(0); + final View endChild = getChildAt(mHeadersStartPosition - 1); + if (startChild == null || endChild == null) { + return 0; + } + + return state.getItemCount(); + } + + public static class LayoutParams extends RecyclerView.LayoutParams { + public static final int INVALID_SPAN_ID = -1; + + private int mSpanIndex = INVALID_SPAN_ID; + private int mSpanSize = 0; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(RecyclerView.LayoutParams source) { + super(source); + } + + public int getSpanIndex() { + return mSpanIndex; + } + + public int getSpanSize() { + return mSpanSize; + } + } + + public static final class DefaultSpanSizeLookup extends SpanSizeLookup { + @Override + public int getSpanSize(int section, int position) { + return 1; + } + + @Override + public int getSpanIndex(int section, int position, int spanCount) { + return position % spanCount; + } + } + + /** + * An interface to provide the number of spans each item occupies. + *

+ * Default implementation sets each item to occupy exactly 1 span. + * + * @see StickyHeaderGridLayoutManager#setSpanSizeLookup(StickyHeaderGridLayoutManager.SpanSizeLookup) + */ + public static abstract class SpanSizeLookup { + /** + * Returns the number of span occupied by the item in section at position. + * + * @param section The adapter section of the item + * @param position The adapter position of the item in section + * @return The number of spans occupied by the item at the provided section and position + */ + abstract public int getSpanSize(int section, int position); + + /** + * Returns the final span index of the provided position. + * + *

+ * If you override this method, you need to make sure it is consistent with + * {@link #getSpanSize(int, int)}. StickyHeaderGridLayoutManager does not call this method for + * each item. It is called only for the reference item and rest of the items + * are assigned to spans based on the reference item. For example, you cannot assign a + * position to span 2 while span 1 is empty. + *

+ * + * @param section The adapter section of the item + * @param position The adapter position of the item in section + * @param spanCount The total number of spans in the grid + * @return The final span position of the item. Should be between 0 (inclusive) and + * spanCount(exclusive) + */ + public int getSpanIndex(int section, int position, int spanCount) { + // TODO: cache them? + final int positionSpanSize = getSpanSize(section, position); + if (positionSpanSize >= spanCount) { + return 0; + } + + int spanIndex = 0; + for (int i = 0; i < position; ++i) { + final int spanSize = getSpanSize(section, i); + spanIndex += spanSize; + + if (spanIndex == spanCount) { + spanIndex = 0; + } + else if (spanIndex > spanCount) { + spanIndex = spanSize; + } + } + + if (spanIndex + positionSpanSize <= spanCount) { + return spanIndex; + } + + return 0; + } + } + + public static class SavedState implements Parcelable { + private int mAnchorSection; + private int mAnchorItem; + private int mAnchorOffset; + + public SavedState() { + + } + + SavedState(Parcel in) { + mAnchorSection = in.readInt(); + mAnchorItem = in.readInt(); + mAnchorOffset = in.readInt(); + } + + public SavedState(SavedState other) { + mAnchorSection = other.mAnchorSection; + mAnchorItem = other.mAnchorItem; + mAnchorOffset = other.mAnchorOffset; + } + + boolean hasValidAnchor() { + return mAnchorSection >= 0; + } + + void invalidateAnchor() { + mAnchorSection = NO_POSITION; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mAnchorSection); + dest.writeInt(mAnchorItem); + dest.writeInt(mAnchorOffset); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + private static class LayoutRow { + private boolean header; + private View headerView; + private int adapterPosition; + private int length; + private int top; + private int bottom; + + public LayoutRow(int adapterPosition, int length, int top, int bottom) { + this.header = false; + this.headerView = null; + this.adapterPosition = adapterPosition; + this.length = length; + this.top = top; + this.bottom = bottom; + } + + public LayoutRow(View headerView, int adapterPosition, int length, int top, int bottom) { + this.header = true; + this.headerView = headerView; + this.adapterPosition = adapterPosition; + this.length = length; + this.top = top; + this.bottom = bottom; + } + + int getHeight() { + return bottom - top; + } + } + + private static class FillResult { + private View edgeView; + private int adapterPosition; + private int length; + private int height; + } + + private static class AnchorPosition { + private int section; + private int item; + private int offset; + + public AnchorPosition() { + reset(); + } + + public void reset() { + section = NO_POSITION; + item = 0; + offset = 0; + } + } +} diff --git a/src/main/java/org/thoughtcrime/securesms/ResolveMediaTask.java b/src/main/java/org/thoughtcrime/securesms/ResolveMediaTask.java index 8ade92314..68f2df2fc 100644 --- a/src/main/java/org/thoughtcrime/securesms/ResolveMediaTask.java +++ b/src/main/java/org/thoughtcrime/securesms/ResolveMediaTask.java @@ -17,7 +17,6 @@ import java.util.HashSet; import static org.thoughtcrime.securesms.util.MediaUtil.getMimeType; import de.cketti.safecontentresolver.SafeContentResolver; -import de.cketti.safecontentresolver.SafeContentResolverCompat; public class ResolveMediaTask extends AsyncTask { @@ -50,7 +49,7 @@ public class ResolveMediaTask extends AsyncTask { String fileName = null; Long fileSize = null; - SafeContentResolver safeContentResolver = SafeContentResolverCompat.newInstance(contextRef.get()); + SafeContentResolver safeContentResolver = SafeContentResolver.newInstance(contextRef.get()); inputStream = safeContentResolver.openInputStream(uri); if (inputStream == null) { diff --git a/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java b/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java index b48d1301b..375291bd5 100644 --- a/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java +++ b/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java @@ -16,16 +16,16 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.LoadControl; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -76,10 +76,13 @@ public class AudioSlidePlayer { public void requestDuration() { try { - LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl(); - durationCalculator = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl); + LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).build(); + durationCalculator = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context)) + .setTrackSelector(new DefaultTrackSelector(context)) + .setLoadControl(loadControl) + .build(); durationCalculator.setPlayWhenReady(false); - durationCalculator.addListener(new Player.EventListener() { + durationCalculator.addListener(new Player.Listener() { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { if (playbackState == Player.STATE_READY) { @@ -113,16 +116,19 @@ public class AudioSlidePlayer { throw new IOException("Slide has no URI!"); } - LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl(); - this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl); + LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).build(); + this.mediaPlayer = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context)) + .setTrackSelector(new DefaultTrackSelector(context)) + .setLoadControl(loadControl) + .build(); mediaPlayer.prepare(createMediaSource(slide.getUri())); mediaPlayer.setPlayWhenReady(true); mediaPlayer.setAudioAttributes(new AudioAttributes.Builder() - .setContentType(earpiece ? C.CONTENT_TYPE_SPEECH : C.CONTENT_TYPE_MUSIC) + .setContentType(earpiece ? C.AUDIO_CONTENT_TYPE_SPEECH : C.AUDIO_CONTENT_TYPE_MUSIC) .setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA) - .build()); - mediaPlayer.addListener(new Player.EventListener() { + .build(), false); + mediaPlayer.addListener(new Player.Listener() { boolean started = false; @@ -171,7 +177,7 @@ public class AudioSlidePlayer { } @Override - public void onPlayerError(ExoPlaybackException error) { + public void onPlayerError(PlaybackException error) { Log.w(TAG, "MediaPlayer Error: " + error); synchronized (AudioSlidePlayer.this) { @@ -194,9 +200,8 @@ public class AudioSlidePlayer { AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(defaultDataSourceFactory); ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true); - return new ExtractorMediaSource.Factory(attachmentDataSourceFactory) - .setExtractorsFactory(extractorsFactory) - .createMediaSource(uri); + return new ProgressiveMediaSource.Factory(attachmentDataSourceFactory, extractorsFactory) + .createMediaSource(MediaItem.fromUri(uri)); } public synchronized void stop() { diff --git a/src/main/java/org/thoughtcrime/securesms/qr/QrScanFragment.java b/src/main/java/org/thoughtcrime/securesms/qr/QrScanFragment.java index 76233bd65..e8d70d0e4 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/QrScanFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/QrScanFragment.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.qr; import android.app.Activity; import android.os.Bundle; +import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -106,8 +107,11 @@ public class QrScanFragment extends Fragment { // which makes _showing_ the QR-code impossible if scanning goes wrong. // therefore, we only show a non-disturbing error here. @Override - protected void displayFrameworkBugMessageAndExit() { - Toast.makeText(myActivity, R.string.zxing_msg_camera_framework_bug, Toast.LENGTH_SHORT).show(); + protected void displayFrameworkBugMessageAndExit(String message) { + if (TextUtils.isEmpty(message)) { + message = myActivity.getString(R.string.zxing_msg_camera_framework_bug); + } + Toast.makeText(myActivity, message, Toast.LENGTH_SHORT).show(); } } diff --git a/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java b/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java index e5b001a41..818a478fb 100644 --- a/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java +++ b/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java @@ -17,27 +17,26 @@ package org.thoughtcrime.securesms.video; import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.LoadControl; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -49,7 +48,7 @@ import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory; public class VideoPlayer extends FrameLayout { - @Nullable private final SimpleExoPlayerView exoView; + @Nullable private final PlayerView exoView; @Nullable private SimpleExoPlayer exoPlayer; @Nullable private Window window; @@ -93,12 +92,15 @@ public class VideoPlayer extends FrameLayout { private void setExoViewSource(@NonNull VideoSlide videoSource, boolean autoplay) { - BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); - TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(getContext()).build(); + TrackSelector trackSelector = new DefaultTrackSelector(getContext()); LoadControl loadControl = new DefaultLoadControl(); - exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, loadControl); + exoPlayer = new SimpleExoPlayer.Builder(getContext()) + .setTrackSelector(trackSelector) + .setBandwidthMeter(bandwidthMeter) + .setLoadControl(loadControl) + .build(); exoPlayer.addListener(new ExoPlayerListener(window)); //noinspection ConstantConditions exoView.setPlayer(exoPlayer); @@ -107,13 +109,14 @@ public class VideoPlayer extends FrameLayout { AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(defaultDataSourceFactory); ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); - MediaSource mediaSource = new ExtractorMediaSource(videoSource.getUri(), attachmentDataSourceFactory, extractorsFactory, null, null); + MediaSource mediaSource = new ProgressiveMediaSource.Factory(attachmentDataSourceFactory, extractorsFactory) + .createMediaSource(MediaItem.fromUri(videoSource.getUri())); exoPlayer.prepare(mediaSource); exoPlayer.setPlayWhenReady(autoplay); } - private static class ExoPlayerListener implements Player.EventListener { + private static class ExoPlayerListener implements Player.Listener { private final Window window; ExoPlayerListener(Window window) { diff --git a/src/main/res/layout-v16/video_player.xml b/src/main/res/layout-v16/video_player.xml deleted file mode 100644 index 9d8b07520..000000000 --- a/src/main/res/layout-v16/video_player.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/main/res/layout/video_player.xml b/src/main/res/layout/video_player.xml index 144e0f51e..25f852928 100644 --- a/src/main/res/layout/video_player.xml +++ b/src/main/res/layout/video_player.xml @@ -4,10 +4,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - \ No newline at end of file +