mirror of
https://github.com/MatomoCamp/matomocamp-companion-android.git
synced 2024-09-19 16:13:46 +02:00
Migrated most ListViews to RecyclerViews
- Use ConcatAdapter to replace header View in PersonInfoListFragment - Use MultiChoiceHelper to implement MultiChoiceMode in RecyclerView - Instances of TracksListFragment share a common RecycledViewPool to lower memory usage.
This commit is contained in:
parent
71b030c994
commit
9fd6c6c374
24 changed files with 1721 additions and 245 deletions
|
@ -34,6 +34,7 @@ ext {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile "com.android.support:appcompat-v7:$supportLibraryVersion"
|
compile "com.android.support:appcompat-v7:$supportLibraryVersion"
|
||||||
|
compile "com.android.support:recyclerview-v7:$supportLibraryVersion"
|
||||||
compile "com.android.support:cardview-v7:$supportLibraryVersion"
|
compile "com.android.support:cardview-v7:$supportLibraryVersion"
|
||||||
compile 'com.github.chrisbanes.photoview:library:1.2.4'
|
compile 'com.github.chrisbanes.photoview:library:1.2.4'
|
||||||
}
|
}
|
||||||
|
|
202
app/src/main/java/android/support/v7/widget/ConcatAdapter.java
Normal file
202
app/src/main/java/android/support/v7/widget/ConcatAdapter.java
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
package android.support.v7.widget;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter which concatenates the items of multiple adapters.
|
||||||
|
* Doesn't support stable ids, but properly delegates changes notifications.
|
||||||
|
* <p>
|
||||||
|
* Adapters may provide multiple view types but they must not overlap.
|
||||||
|
* It's recommended to always use the item layout id as view type.
|
||||||
|
* <p>
|
||||||
|
* Warning: You need to use ConcatAdapter.getAdapterPosition(ViewHolder)
|
||||||
|
* in place of ViewHolder.getAdapterPosition() inside child adapters.
|
||||||
|
*
|
||||||
|
* @author Christophe Beyls
|
||||||
|
*/
|
||||||
|
public class ConcatAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||||
|
|
||||||
|
private final RecyclerView.Adapter<RecyclerView.ViewHolder>[] adapters;
|
||||||
|
private final RecyclerView.AdapterDataObserver[] adapterObservers;
|
||||||
|
final int[] offsets;
|
||||||
|
private final SparseArray<RecyclerView.Adapter<RecyclerView.ViewHolder>> viewTypeAdapters = new SparseArray<>();
|
||||||
|
|
||||||
|
private class InternalObserver extends RecyclerView.AdapterDataObserver {
|
||||||
|
|
||||||
|
private final int adapterIndex;
|
||||||
|
|
||||||
|
InternalObserver(int adapterIndex) {
|
||||||
|
this.adapterIndex = adapterIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChanged() {
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeChanged(int positionStart, int itemCount) {
|
||||||
|
notifyItemRangeChanged(positionStart + offsets[adapterIndex], itemCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
|
||||||
|
notifyItemRangeChanged(positionStart + offsets[adapterIndex], itemCount, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||||
|
for (int i = adapterIndex + 1, size = offsets.length; i < size; ++i) {
|
||||||
|
offsets[i] += itemCount;
|
||||||
|
}
|
||||||
|
notifyItemRangeInserted(positionStart + offsets[adapterIndex], itemCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
||||||
|
for (int i = adapterIndex + 1, size = offsets.length; i < size; ++i) {
|
||||||
|
offsets[i] -= itemCount;
|
||||||
|
}
|
||||||
|
notifyItemRangeRemoved(positionStart + offsets[adapterIndex], itemCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
|
||||||
|
final int offset = offsets[adapterIndex];
|
||||||
|
notifyItemMoved(fromPosition + offset, toPosition + offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public ConcatAdapter(RecyclerView.Adapter<? extends RecyclerView.ViewHolder>... adapters) {
|
||||||
|
this.adapters = (RecyclerView.Adapter<RecyclerView.ViewHolder>[]) adapters;
|
||||||
|
final int size = adapters.length;
|
||||||
|
adapterObservers = new RecyclerView.AdapterDataObserver[size];
|
||||||
|
for (int i = 0; i < size; ++i) {
|
||||||
|
adapterObservers[i] = new InternalObserver(i);
|
||||||
|
}
|
||||||
|
offsets = new int[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The adapter position relative to the child adapter, if any.
|
||||||
|
*/
|
||||||
|
public static int getAdapterPosition(@NonNull RecyclerView.ViewHolder holder) {
|
||||||
|
int position = holder.getAdapterPosition();
|
||||||
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
|
RecyclerView.Adapter adapter = holder.mOwnerRecyclerView.getAdapter();
|
||||||
|
if (adapter instanceof ConcatAdapter) {
|
||||||
|
final int[] offsets = ((ConcatAdapter) adapter).offsets;
|
||||||
|
final int index = Arrays.binarySearch(offsets, position);
|
||||||
|
if (index >= 0) {
|
||||||
|
position = 0;
|
||||||
|
} else {
|
||||||
|
position -= offsets[~index - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getAdapterIndexForPosition(int position) {
|
||||||
|
int index = Arrays.binarySearch(offsets, position);
|
||||||
|
return (index < 0) ? ~index - 1 : index;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
final int index = getAdapterIndexForPosition(position);
|
||||||
|
RecyclerView.Adapter<RecyclerView.ViewHolder> adapter = adapters[index];
|
||||||
|
int viewType = adapter.getItemViewType(position - offsets[index]);
|
||||||
|
if (viewTypeAdapters.get(viewType) == null) {
|
||||||
|
viewTypeAdapters.put(viewType, adapter);
|
||||||
|
}
|
||||||
|
return viewType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
return viewTypeAdapters.get(viewType).onCreateViewHolder(parent, viewType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||||
|
final int index = getAdapterIndexForPosition(position);
|
||||||
|
adapters[index].onBindViewHolder(holder, position - offsets[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List<Object> payloads) {
|
||||||
|
final int index = getAdapterIndexForPosition(position);
|
||||||
|
adapters[index].onBindViewHolder(holder, position - offsets[index], payloads);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0, size = adapters.length; i < size; ++i) {
|
||||||
|
offsets[i] = count;
|
||||||
|
count += adapters[i].getItemCount();
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) {
|
||||||
|
if (!hasObservers()) {
|
||||||
|
for (int i = 0, size = adapters.length; i < size; ++i) {
|
||||||
|
adapters[i].registerAdapterDataObserver(adapterObservers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.registerAdapterDataObserver(observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver observer) {
|
||||||
|
super.unregisterAdapterDataObserver(observer);
|
||||||
|
if (!hasObservers()) {
|
||||||
|
for (int i = 0, size = adapters.length; i < size; ++i) {
|
||||||
|
adapters[i].unregisterAdapterDataObserver(adapterObservers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
|
||||||
|
for (RecyclerView.Adapter<RecyclerView.ViewHolder> adapter : adapters) {
|
||||||
|
adapter.onAttachedToRecyclerView(recyclerView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
|
||||||
|
for (RecyclerView.Adapter<RecyclerView.ViewHolder> adapter : adapters) {
|
||||||
|
adapter.onDetachedFromRecyclerView(recyclerView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
|
||||||
|
viewTypeAdapters.get(holder.getItemViewType()).onViewAttachedToWindow(holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
|
||||||
|
viewTypeAdapters.get(holder.getItemViewType()).onViewDetachedFromWindow(holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewRecycled(RecyclerView.ViewHolder holder) {
|
||||||
|
viewTypeAdapters.get(holder.getItemViewType()).onViewRecycled(holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) {
|
||||||
|
return viewTypeAdapters.get(holder.getItemViewType()).onFailedToRecycleView(holder);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package android.support.v7.widget;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.v4.view.ViewCompat;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DividerItemDecoration is a {@link RecyclerView.ItemDecoration} that can be used as a divider
|
||||||
|
* between items of a {@link LinearLayoutManager}. It supports both {@link #HORIZONTAL} and
|
||||||
|
* {@link #VERTICAL} orientations.
|
||||||
|
* <p>
|
||||||
|
* <pre>
|
||||||
|
* mDividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(),
|
||||||
|
* mLayoutManager.getOrientation());
|
||||||
|
* recyclerView.addItemDecoration(mDividerItemDecoration);
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
|
||||||
|
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
|
||||||
|
public static final int VERTICAL = LinearLayout.VERTICAL;
|
||||||
|
|
||||||
|
private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
|
||||||
|
|
||||||
|
private Drawable mDivider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
|
||||||
|
*/
|
||||||
|
private int mOrientation;
|
||||||
|
|
||||||
|
private final Rect mBounds = new Rect();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a
|
||||||
|
* {@link LinearLayoutManager}.
|
||||||
|
*
|
||||||
|
* @param context Current context, it will be used to access resources.
|
||||||
|
* @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}.
|
||||||
|
*/
|
||||||
|
public DividerItemDecoration(Context context, int orientation) {
|
||||||
|
final TypedArray a = context.obtainStyledAttributes(ATTRS);
|
||||||
|
mDivider = a.getDrawable(0);
|
||||||
|
a.recycle();
|
||||||
|
setOrientation(orientation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the orientation for this divider. This should be called if
|
||||||
|
* {@link RecyclerView.LayoutManager} changes orientation.
|
||||||
|
*
|
||||||
|
* @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
|
||||||
|
*/
|
||||||
|
public void setOrientation(int orientation) {
|
||||||
|
if (orientation != HORIZONTAL && orientation != VERTICAL) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Invalid orientation. It should be either HORIZONTAL or VERTICAL");
|
||||||
|
}
|
||||||
|
mOrientation = orientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link Drawable} for this divider.
|
||||||
|
*
|
||||||
|
* @param drawable Drawable that should be used as a divider.
|
||||||
|
*/
|
||||||
|
public void setDrawable(@NonNull Drawable drawable) {
|
||||||
|
if (drawable == null) {
|
||||||
|
throw new IllegalArgumentException("Drawable cannot be null.");
|
||||||
|
}
|
||||||
|
mDivider = drawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
|
||||||
|
if (parent.getLayoutManager() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mOrientation == VERTICAL) {
|
||||||
|
drawVertical(c, parent);
|
||||||
|
} else {
|
||||||
|
drawHorizontal(c, parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void getDecoratedBoundsWithMargins(View view, Rect outBounds) {
|
||||||
|
final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
|
||||||
|
final Rect insets = lp.mDecorInsets;
|
||||||
|
outBounds.set(view.getLeft() - insets.left - lp.leftMargin,
|
||||||
|
view.getTop() - insets.top - lp.topMargin,
|
||||||
|
view.getRight() + insets.right + lp.rightMargin,
|
||||||
|
view.getBottom() + insets.bottom + lp.bottomMargin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawVertical(Canvas canvas, RecyclerView parent) {
|
||||||
|
canvas.save();
|
||||||
|
final int left = 0;
|
||||||
|
final int right = parent.getWidth();
|
||||||
|
|
||||||
|
final int childCount = parent.getChildCount();
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
final View child = parent.getChildAt(i);
|
||||||
|
getDecoratedBoundsWithMargins(child, mBounds);
|
||||||
|
final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
|
||||||
|
final int top = bottom - mDivider.getIntrinsicHeight();
|
||||||
|
mDivider.setBounds(left, top, right, bottom);
|
||||||
|
mDivider.draw(canvas);
|
||||||
|
}
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
|
||||||
|
canvas.save();
|
||||||
|
final int top = 0;
|
||||||
|
final int bottom = parent.getHeight();
|
||||||
|
|
||||||
|
final int childCount = parent.getChildCount();
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
final View child = parent.getChildAt(i);
|
||||||
|
getDecoratedBoundsWithMargins(child, mBounds);
|
||||||
|
final int right = mBounds.right + Math.round(ViewCompat.getTranslationX(child));
|
||||||
|
final int left = right - mDivider.getIntrinsicWidth();
|
||||||
|
mDivider.setBounds(left, top, right, bottom);
|
||||||
|
mDivider.draw(canvas);
|
||||||
|
}
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
|
||||||
|
RecyclerView.State state) {
|
||||||
|
if (mOrientation == VERTICAL) {
|
||||||
|
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
|
||||||
|
} else {
|
||||||
|
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,13 +3,19 @@ package be.digitalia.fosdem.adapters;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Parcelable;
|
||||||
import android.support.annotation.ColorInt;
|
import android.support.annotation.ColorInt;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.support.v7.view.ActionMode;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.StyleSpan;
|
import android.text.style.StyleSpan;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -17,20 +23,76 @@ import java.util.Date;
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.db.DatabaseManager;
|
||||||
import be.digitalia.fosdem.model.Event;
|
import be.digitalia.fosdem.model.Event;
|
||||||
|
import be.digitalia.fosdem.widgets.MultiChoiceHelper;
|
||||||
|
|
||||||
public class BookmarksAdapter extends EventsAdapter {
|
public class BookmarksAdapter extends EventsAdapter {
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private final int errorColor;
|
private final int errorColor;
|
||||||
|
final MultiChoiceHelper multiChoiceHelper;
|
||||||
|
|
||||||
public BookmarksAdapter(Context context) {
|
public BookmarksAdapter(AppCompatActivity activity) {
|
||||||
super(context);
|
super(activity);
|
||||||
errorColor = ContextCompat.getColor(context, R.color.error_material);
|
errorColor = ContextCompat.getColor(activity, R.color.error_material);
|
||||||
|
multiChoiceHelper = new MultiChoiceHelper(activity, this);
|
||||||
|
multiChoiceHelper.setMultiChoiceModeListener(new MultiChoiceHelper.MultiChoiceModeListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||||
|
mode.getMenuInflater().inflate(R.menu.action_mode_bookmarks, menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSelectedCountDisplay(ActionMode mode) {
|
||||||
|
int count = multiChoiceHelper.getCheckedItemCount();
|
||||||
|
mode.setTitle(multiChoiceHelper.getContext().getResources().getQuantityString(R.plurals.selected, count, count));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void bindView(View view, Context context, Cursor cursor) {
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||||
ViewHolder holder = (ViewHolder) view.getTag();
|
updateSelectedCountDisplay(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.delete:
|
||||||
|
// Remove multiple bookmarks at once
|
||||||
|
new RemoveBookmarksAsyncTask().execute(multiChoiceHelper.getCheckedItemIds());
|
||||||
|
mode.finish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
|
||||||
|
updateSelectedCountDisplay(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyActionMode(ActionMode mode) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Parcelable onSaveInstanceState() {
|
||||||
|
return multiChoiceHelper.onSaveInstanceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onRestoreInstanceState(Parcelable state) {
|
||||||
|
multiChoiceHelper.onRestoreInstanceState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDestroyView() {
|
||||||
|
multiChoiceHelper.clearChoices();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(ViewHolder holder, Cursor cursor) {
|
||||||
|
final int position = cursor.getPosition();
|
||||||
|
Context context = holder.itemView.getContext();
|
||||||
Event event = DatabaseManager.toEvent(cursor, holder.event);
|
Event event = DatabaseManager.toEvent(cursor, holder.event);
|
||||||
holder.event = event;
|
holder.event = event;
|
||||||
|
|
||||||
|
@ -60,12 +122,16 @@ public class BookmarksAdapter extends EventsAdapter {
|
||||||
holder.details.setText(details);
|
holder.details.setText(details);
|
||||||
}
|
}
|
||||||
holder.details.setContentDescription(context.getString(R.string.details_content_description, detailsContentDescription));
|
holder.details.setContentDescription(context.getString(R.string.details_content_description, detailsContentDescription));
|
||||||
|
|
||||||
|
// Enable MultiChoice selection and update checked state
|
||||||
|
holder.bind(multiChoiceHelper, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the current event is overlapping with the previous or next one.
|
* Checks if the current event is overlapping with the previous or next one.
|
||||||
|
* Warning: this methods will update the cursor's position.
|
||||||
*/
|
*/
|
||||||
public static boolean isOverlapping(Cursor cursor, Date startTime, Date endTime) {
|
private static boolean isOverlapping(Cursor cursor, Date startTime, Date endTime) {
|
||||||
final int position = cursor.getPosition();
|
final int position = cursor.getPosition();
|
||||||
|
|
||||||
if ((startTime != null) && (position > 0) && cursor.moveToPosition(position - 1)) {
|
if ((startTime != null) && (position > 0) && cursor.moveToPosition(position - 1)) {
|
||||||
|
@ -86,4 +152,14 @@ public class BookmarksAdapter extends EventsAdapter {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class RemoveBookmarksAsyncTask extends AsyncTask<long[], Void, Void> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(long[]... params) {
|
||||||
|
DatabaseManager.getInstance().removeBookmarks(params[0]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package be.digitalia.fosdem.adapters;
|
package be.digitalia.fosdem.adapters;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.support.v4.widget.CursorAdapter;
|
|
||||||
import android.support.v4.widget.TextViewCompat;
|
import android.support.v4.widget.TextViewCompat;
|
||||||
import android.support.v7.widget.AppCompatDrawableManager;
|
import android.support.v7.widget.AppCompatDrawableManager;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
@ -16,11 +16,13 @@ import java.text.DateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
|
import be.digitalia.fosdem.activities.EventDetailsActivity;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.db.DatabaseManager;
|
||||||
import be.digitalia.fosdem.model.Event;
|
import be.digitalia.fosdem.model.Event;
|
||||||
import be.digitalia.fosdem.utils.DateUtils;
|
import be.digitalia.fosdem.utils.DateUtils;
|
||||||
|
import be.digitalia.fosdem.widgets.MultiChoiceHelper;
|
||||||
|
|
||||||
public class EventsAdapter extends CursorAdapter {
|
public class EventsAdapter extends RecyclerViewCursorAdapter<EventsAdapter.ViewHolder> {
|
||||||
|
|
||||||
protected final LayoutInflater inflater;
|
protected final LayoutInflater inflater;
|
||||||
protected final DateFormat timeDateFormat;
|
protected final DateFormat timeDateFormat;
|
||||||
|
@ -31,7 +33,6 @@ public class EventsAdapter extends CursorAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventsAdapter(Context context, boolean showDay) {
|
public EventsAdapter(Context context, boolean showDay) {
|
||||||
super(context, null, 0);
|
|
||||||
inflater = LayoutInflater.from(context);
|
inflater = LayoutInflater.from(context);
|
||||||
timeDateFormat = DateUtils.getTimeDateFormat(context);
|
timeDateFormat = DateUtils.getTimeDateFormat(context);
|
||||||
this.showDay = showDay;
|
this.showDay = showDay;
|
||||||
|
@ -43,22 +44,19 @@ public class EventsAdapter extends CursorAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
public int getItemViewType(int position) {
|
||||||
View view = inflater.inflate(R.layout.item_event, parent, false);
|
return R.layout.item_event;
|
||||||
|
|
||||||
ViewHolder holder = new ViewHolder();
|
|
||||||
holder.title = (TextView) view.findViewById(R.id.title);
|
|
||||||
holder.persons = (TextView) view.findViewById(R.id.persons);
|
|
||||||
holder.trackName = (TextView) view.findViewById(R.id.track_name);
|
|
||||||
holder.details = (TextView) view.findViewById(R.id.details);
|
|
||||||
view.setTag(holder);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void bindView(View view, Context context, Cursor cursor) {
|
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
ViewHolder holder = (ViewHolder) view.getTag();
|
View view = inflater.inflate(R.layout.item_event, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(ViewHolder holder, Cursor cursor) {
|
||||||
|
Context context = holder.itemView.getContext();
|
||||||
Event event = DatabaseManager.toEvent(cursor, holder.event);
|
Event event = DatabaseManager.toEvent(cursor, holder.event);
|
||||||
holder.event = event;
|
holder.event = event;
|
||||||
|
|
||||||
|
@ -92,11 +90,29 @@ public class EventsAdapter extends CursorAdapter {
|
||||||
holder.details.setContentDescription(context.getString(R.string.details_content_description, details));
|
holder.details.setContentDescription(context.getString(R.string.details_content_description, details));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static class ViewHolder {
|
static class ViewHolder extends MultiChoiceHelper.ViewHolder implements View.OnClickListener {
|
||||||
TextView title;
|
TextView title;
|
||||||
TextView persons;
|
TextView persons;
|
||||||
TextView trackName;
|
TextView trackName;
|
||||||
TextView details;
|
TextView details;
|
||||||
|
|
||||||
Event event;
|
Event event;
|
||||||
|
|
||||||
|
public ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
title = (TextView) itemView.findViewById(R.id.title);
|
||||||
|
persons = (TextView) itemView.findViewById(R.id.persons);
|
||||||
|
trackName = (TextView) itemView.findViewById(R.id.track_name);
|
||||||
|
details = (TextView) itemView.findViewById(R.id.details);
|
||||||
|
setOnClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
Context context = view.getContext();
|
||||||
|
Intent intent = new Intent(context, EventDetailsActivity.class)
|
||||||
|
.putExtra(EventDetailsActivity.EXTRA_EVENT, event);
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package be.digitalia.fosdem.adapters;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplified CursorAdapter designed for RecyclerView.
|
||||||
|
*
|
||||||
|
* @author Christophe Beyls
|
||||||
|
*/
|
||||||
|
public abstract class RecyclerViewCursorAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
|
||||||
|
|
||||||
|
private Cursor cursor;
|
||||||
|
private int rowIDColumn = -1;
|
||||||
|
|
||||||
|
public RecyclerViewCursorAdapter() {
|
||||||
|
setHasStableIds(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swap in a new Cursor, returning the old Cursor.
|
||||||
|
* The old cursor is not closed.
|
||||||
|
*
|
||||||
|
* @return The previously set Cursor, if any.
|
||||||
|
* If the given new Cursor is the same instance as the previously set
|
||||||
|
* Cursor, null is also returned.
|
||||||
|
*/
|
||||||
|
public Cursor swapCursor(Cursor newCursor) {
|
||||||
|
if (newCursor == cursor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Cursor oldCursor = cursor;
|
||||||
|
cursor = newCursor;
|
||||||
|
rowIDColumn = (newCursor == null) ? -1 : newCursor.getColumnIndexOrThrow("_id");
|
||||||
|
notifyDataSetChanged();
|
||||||
|
return oldCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cursor getCursor() {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return (cursor == null) ? 0 : cursor.getCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The cursor initialized to the specified position.
|
||||||
|
*/
|
||||||
|
public Object getItem(int position) {
|
||||||
|
if (cursor != null) {
|
||||||
|
cursor.moveToPosition(position);
|
||||||
|
}
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
if ((cursor != null) && cursor.moveToPosition(position)) {
|
||||||
|
return cursor.getLong(rowIDColumn);
|
||||||
|
}
|
||||||
|
return RecyclerView.NO_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(VH holder, int position) {
|
||||||
|
if (cursor == null) {
|
||||||
|
throw new IllegalStateException("this should only be called when the cursor is not null");
|
||||||
|
}
|
||||||
|
if (!cursor.moveToPosition(position)) {
|
||||||
|
throw new IllegalStateException("couldn't move cursor to position " + position);
|
||||||
|
}
|
||||||
|
onBindViewHolder(holder, cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void onBindViewHolder(VH holder, Cursor cursor);
|
||||||
|
}
|
|
@ -1,17 +1,16 @@
|
||||||
package be.digitalia.fosdem.fragments;
|
package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
import android.view.View;
|
import android.support.v7.widget.DividerItemDecoration;
|
||||||
import android.widget.ListView;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import be.digitalia.fosdem.activities.EventDetailsActivity;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import be.digitalia.fosdem.adapters.EventsAdapter;
|
|
||||||
import be.digitalia.fosdem.model.Event;
|
|
||||||
|
|
||||||
public abstract class BaseLiveListFragment extends SmoothListFragment implements LoaderCallbacks<Cursor> {
|
import be.digitalia.fosdem.adapters.EventsAdapter;
|
||||||
|
|
||||||
|
public abstract class BaseLiveListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
||||||
|
|
||||||
private static final int EVENTS_LOADER_ID = 1;
|
private static final int EVENTS_LOADER_ID = 1;
|
||||||
|
|
||||||
|
@ -21,7 +20,13 @@ public abstract class BaseLiveListFragment extends SmoothListFragment implements
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
adapter = new EventsAdapter(getActivity(), false);
|
adapter = new EventsAdapter(getActivity(), false);
|
||||||
setListAdapter(adapter);
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRecyclerViewCreated(RecyclerView recyclerView, Bundle savedInstanceState) {
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext()));
|
||||||
|
recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL));
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -29,7 +34,7 @@ public abstract class BaseLiveListFragment extends SmoothListFragment implements
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
|
||||||
setEmptyText(getEmptyText());
|
setEmptyText(getEmptyText());
|
||||||
setListShown(false);
|
setProgressBarVisible(true);
|
||||||
|
|
||||||
getLoaderManager().initLoader(EVENTS_LOADER_ID, null, this);
|
getLoaderManager().initLoader(EVENTS_LOADER_ID, null, this);
|
||||||
}
|
}
|
||||||
|
@ -42,18 +47,11 @@ public abstract class BaseLiveListFragment extends SmoothListFragment implements
|
||||||
adapter.swapCursor(data);
|
adapter.swapCursor(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
setListShown(true);
|
setProgressBarVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(Loader<Cursor> loader) {
|
public void onLoaderReset(Loader<Cursor> loader) {
|
||||||
adapter.swapCursor(null);
|
adapter.swapCursor(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onListItemClick(ListView l, View v, int position, long id) {
|
|
||||||
Event event = adapter.getItem(position);
|
|
||||||
Intent intent = new Intent(getActivity(), EventDetailsActivity.class).putExtra(EventDetailsActivity.EXTRA_EVENT, event);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,38 @@
|
||||||
package be.digitalia.fosdem.fragments;
|
package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
import android.support.v4.content.SharedPreferencesCompat;
|
import android.support.v4.content.SharedPreferencesCompat;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.support.v7.widget.DividerItemDecoration;
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.text.format.DateUtils;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ListView;
|
|
||||||
|
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.activities.EventDetailsActivity;
|
|
||||||
import be.digitalia.fosdem.adapters.BookmarksAdapter;
|
import be.digitalia.fosdem.adapters.BookmarksAdapter;
|
||||||
import be.digitalia.fosdem.adapters.EventsAdapter;
|
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.db.DatabaseManager;
|
||||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
||||||
import be.digitalia.fosdem.model.Event;
|
|
||||||
import be.digitalia.fosdem.widgets.BookmarksMultiChoiceModeListener;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bookmarks list, optionally filterable.
|
* Bookmarks list, optionally filterable.
|
||||||
*
|
*
|
||||||
* @author Christophe Beyls
|
* @author Christophe Beyls
|
||||||
*/
|
*/
|
||||||
public class BookmarksListFragment extends SmoothListFragment implements LoaderCallbacks<Cursor> {
|
public class BookmarksListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
||||||
|
|
||||||
private static final int BOOKMARKS_LOADER_ID = 1;
|
private static final int BOOKMARKS_LOADER_ID = 1;
|
||||||
private static final String PREF_UPCOMING_ONLY = "bookmarks_upcoming_only";
|
private static final String PREF_UPCOMING_ONLY = "bookmarks_upcoming_only";
|
||||||
|
private static final String STATE_ADAPTER = "adapter";
|
||||||
|
|
||||||
private EventsAdapter adapter;
|
private BookmarksAdapter adapter;
|
||||||
private boolean upcomingOnly;
|
private boolean upcomingOnly;
|
||||||
|
|
||||||
private MenuItem filterMenuItem;
|
private MenuItem filterMenuItem;
|
||||||
|
@ -44,33 +42,41 @@ public class BookmarksListFragment extends SmoothListFragment implements LoaderC
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
adapter = new BookmarksAdapter(getActivity());
|
adapter = new BookmarksAdapter((AppCompatActivity) getActivity());
|
||||||
setListAdapter(adapter);
|
if (savedInstanceState != null) {
|
||||||
|
adapter.onRestoreInstanceState(savedInstanceState.getParcelable(STATE_ADAPTER));
|
||||||
|
}
|
||||||
upcomingOnly = getActivity().getPreferences(Context.MODE_PRIVATE).getBoolean(PREF_UPCOMING_ONLY, false);
|
upcomingOnly = getActivity().getPreferences(Context.MODE_PRIVATE).getBoolean(PREF_UPCOMING_ONLY, false);
|
||||||
|
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRecyclerViewCreated(RecyclerView recyclerView, Bundle savedInstanceState) {
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext()));
|
||||||
|
recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL));
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityCreated(Bundle savedInstanceState) {
|
public void onActivityCreated(Bundle savedInstanceState) {
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
|
||||||
BookmarksMultiChoiceModeListener.register(getListView());
|
|
||||||
}
|
|
||||||
|
|
||||||
setEmptyText(getString(R.string.no_bookmark));
|
setEmptyText(getString(R.string.no_bookmark));
|
||||||
setListShown(false);
|
setProgressBarVisible(true);
|
||||||
|
|
||||||
getLoaderManager().initLoader(BOOKMARKS_LOADER_ID, null, this);
|
getLoaderManager().initLoader(BOOKMARKS_LOADER_ID, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
super.onSaveInstanceState(outState);
|
||||||
BookmarksMultiChoiceModeListener.unregister(getListView());
|
outState.putParcelable(STATE_ADAPTER, adapter.onSaveInstanceState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
adapter.onDestroyView();
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +122,7 @@ public class BookmarksListFragment extends SmoothListFragment implements LoaderC
|
||||||
private static class BookmarksLoader extends SimpleCursorLoader {
|
private static class BookmarksLoader extends SimpleCursorLoader {
|
||||||
|
|
||||||
// Events that just started are still shown for 5 minutes
|
// Events that just started are still shown for 5 minutes
|
||||||
private static final long TIME_OFFSET = 5L * 60L * 1000L;
|
private static final long TIME_OFFSET = 5L * DateUtils.MINUTE_IN_MILLIS;
|
||||||
|
|
||||||
private final boolean upcomingOnly;
|
private final boolean upcomingOnly;
|
||||||
private final Handler handler;
|
private final Handler handler;
|
||||||
|
@ -179,18 +185,11 @@ public class BookmarksListFragment extends SmoothListFragment implements LoaderC
|
||||||
adapter.swapCursor(data);
|
adapter.swapCursor(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
setListShown(true);
|
setProgressBarVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(Loader<Cursor> loader) {
|
public void onLoaderReset(Loader<Cursor> loader) {
|
||||||
adapter.swapCursor(null);
|
adapter.swapCursor(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onListItemClick(ListView l, View v, int position, long id) {
|
|
||||||
Event event = adapter.getItem(position);
|
|
||||||
Intent intent = new Intent(getActivity(), EventDetailsActivity.class).putExtra(EventDetailsActivity.EXTRA_EVENT, event);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,22 +7,23 @@ import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
|
import android.support.v7.widget.ConcatAdapter;
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ListView;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.activities.EventDetailsActivity;
|
|
||||||
import be.digitalia.fosdem.adapters.EventsAdapter;
|
import be.digitalia.fosdem.adapters.EventsAdapter;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.db.DatabaseManager;
|
||||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
||||||
import be.digitalia.fosdem.model.Event;
|
|
||||||
import be.digitalia.fosdem.model.Person;
|
import be.digitalia.fosdem.model.Person;
|
||||||
|
|
||||||
public class PersonInfoListFragment extends SmoothListFragment implements LoaderCallbacks<Cursor> {
|
public class PersonInfoListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
||||||
|
|
||||||
private static final int PERSON_EVENTS_LOADER_ID = 1;
|
private static final int PERSON_EVENTS_LOADER_ID = 1;
|
||||||
private static final String ARG_PERSON = "person";
|
private static final String ARG_PERSON = "person";
|
||||||
|
@ -63,23 +64,23 @@ public class PersonInfoListFragment extends SmoothListFragment implements Loader
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRecyclerViewCreated(RecyclerView recyclerView, Bundle savedInstanceState) {
|
||||||
|
final int contentMargin = getResources().getDimensionPixelSize(R.dimen.content_margin);
|
||||||
|
recyclerView.setPadding(contentMargin, contentMargin, contentMargin, contentMargin);
|
||||||
|
recyclerView.setClipToPadding(false);
|
||||||
|
recyclerView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
|
||||||
|
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext()));
|
||||||
|
recyclerView.setAdapter(new ConcatAdapter(new HeaderAdapter(), adapter));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityCreated(Bundle savedInstanceState) {
|
public void onActivityCreated(Bundle savedInstanceState) {
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
|
||||||
setEmptyText(getString(R.string.no_data));
|
setEmptyText(getString(R.string.no_data));
|
||||||
|
setProgressBarVisible(true);
|
||||||
int contentMargin = getResources().getDimensionPixelSize(R.dimen.content_margin);
|
|
||||||
ListView listView = getListView();
|
|
||||||
listView.setPadding(contentMargin, contentMargin, contentMargin, contentMargin);
|
|
||||||
listView.setClipToPadding(false);
|
|
||||||
listView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
|
|
||||||
|
|
||||||
View headerView = LayoutInflater.from(getActivity()).inflate(R.layout.header_person_info, null);
|
|
||||||
getListView().addHeaderView(headerView, null, false);
|
|
||||||
|
|
||||||
setListAdapter(adapter);
|
|
||||||
setListShown(false);
|
|
||||||
|
|
||||||
getLoaderManager().initLoader(PERSON_EVENTS_LOADER_ID, null, this);
|
getLoaderManager().initLoader(PERSON_EVENTS_LOADER_ID, null, this);
|
||||||
}
|
}
|
||||||
|
@ -110,7 +111,7 @@ public class PersonInfoListFragment extends SmoothListFragment implements Loader
|
||||||
adapter.swapCursor(data);
|
adapter.swapCursor(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
setListShown(true);
|
setProgressBarVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -118,10 +119,34 @@ public class PersonInfoListFragment extends SmoothListFragment implements Loader
|
||||||
adapter.swapCursor(null);
|
adapter.swapCursor(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class HeaderAdapter extends RecyclerView.Adapter<HeaderAdapter.ViewHolder> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onListItemClick(ListView l, View v, int position, long id) {
|
public int getItemCount() {
|
||||||
Event event = adapter.getItem(position - 1);
|
return 1;
|
||||||
Intent intent = new Intent(getActivity(), EventDetailsActivity.class).putExtra(EventDetailsActivity.EXTRA_EVENT, event);
|
}
|
||||||
startActivity(intent);
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
return R.layout.header_person_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.header_person_info, null);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||||
|
// Nothing to bind
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
|
public ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Components implementing this interface allow to share a RecycledViewPool between similar fragments.
|
||||||
|
*/
|
||||||
|
public interface RecycledViewPoolProvider {
|
||||||
|
RecyclerView.RecycledViewPool getRecycledViewPool();
|
||||||
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import be.digitalia.fosdem.R;
|
||||||
|
import be.digitalia.fosdem.widgets.ContentLoadingProgressBar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment providing a RecyclerView, an empty view and a progress bar.
|
||||||
|
*
|
||||||
|
* @author Christophe Beyls
|
||||||
|
*/
|
||||||
|
public class RecyclerViewFragment extends Fragment {
|
||||||
|
|
||||||
|
private static final int DEFAULT_EMPTY_VIEW_PADDING_DIPS = 16;
|
||||||
|
|
||||||
|
static class ViewHolder {
|
||||||
|
FrameLayout container;
|
||||||
|
RecyclerView recyclerView;
|
||||||
|
View emptyView;
|
||||||
|
ContentLoadingProgressBar progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class EmptyViewAwareRecyclerView extends RecyclerView {
|
||||||
|
|
||||||
|
private final AdapterDataObserver mEmptyObserver = new AdapterDataObserver() {
|
||||||
|
@Override
|
||||||
|
public void onChanged() {
|
||||||
|
updateEmptyViewVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||||
|
updateEmptyViewVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
||||||
|
updateEmptyViewVisibility();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public EmptyViewAwareRecyclerView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EmptyViewAwareRecyclerView(Context context, @Nullable AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EmptyViewAwareRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAdapter(Adapter adapter) {
|
||||||
|
final Adapter oldAdapter = getAdapter();
|
||||||
|
if (oldAdapter != null) {
|
||||||
|
oldAdapter.unregisterAdapterDataObserver(mEmptyObserver);
|
||||||
|
}
|
||||||
|
super.setAdapter(adapter);
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter.registerAdapterDataObserver(mEmptyObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEmptyViewVisibility();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ViewHolder mHolder;
|
||||||
|
private boolean mIsProgressBarVisible;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this method to provide a custom Empty View.
|
||||||
|
* The default one is a TextView with some padding.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected View onCreateEmptyView(LayoutInflater inflater, ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
TextView textView = new TextView(inflater.getContext());
|
||||||
|
textView.setGravity(Gravity.CENTER);
|
||||||
|
int textPadding = (int) (getResources().getDisplayMetrics().density * DEFAULT_EMPTY_VIEW_PADDING_DIPS);
|
||||||
|
textView.setPadding(textPadding, textPadding, textPadding, textPadding);
|
||||||
|
return textView;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this method to setup the RecyclerView (LayoutManager, ItemDecoration, Adapter)
|
||||||
|
*/
|
||||||
|
protected void onRecyclerViewCreated(RecyclerView recyclerView, @Nullable Bundle savedInstanceState) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
final Context context = inflater.getContext();
|
||||||
|
|
||||||
|
mHolder = new ViewHolder();
|
||||||
|
|
||||||
|
mHolder.container = new FrameLayout(context);
|
||||||
|
|
||||||
|
mHolder.recyclerView = new EmptyViewAwareRecyclerView(context, null, R.attr.recyclerViewStyle);
|
||||||
|
mHolder.recyclerView.setId(android.R.id.list);
|
||||||
|
mHolder.recyclerView.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
|
||||||
|
mHolder.recyclerView.setHasFixedSize(true);
|
||||||
|
mHolder.container.addView(mHolder.recyclerView,
|
||||||
|
new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
mHolder.emptyView = onCreateEmptyView(inflater, mHolder.container, savedInstanceState);
|
||||||
|
mHolder.emptyView.setId(android.R.id.empty);
|
||||||
|
mHolder.emptyView.setVisibility(View.GONE);
|
||||||
|
mHolder.container.addView(mHolder.emptyView,
|
||||||
|
new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
mHolder.progress = new ContentLoadingProgressBar(context, null, android.R.attr.progressBarStyleLarge);
|
||||||
|
mHolder.progress.setId(android.R.id.progress);
|
||||||
|
mHolder.progress.hide();
|
||||||
|
mHolder.container.addView(mHolder.progress,
|
||||||
|
new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
||||||
|
|
||||||
|
mHolder.container.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
onRecyclerViewCreated(mHolder.recyclerView, savedInstanceState);
|
||||||
|
|
||||||
|
return mHolder.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
// Ensure the RecyclerView is properly unregistered as an observer of the adapter
|
||||||
|
mHolder.recyclerView.setAdapter(null);
|
||||||
|
mHolder = null;
|
||||||
|
mIsProgressBarVisible = false;
|
||||||
|
super.onDestroyView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the fragments's RecyclerView widget.
|
||||||
|
*/
|
||||||
|
public RecyclerView getRecyclerView() {
|
||||||
|
return mHolder.recyclerView;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default content for a RecyclerViewFragment has a TextView that can be shown when the list is empty.
|
||||||
|
* Call this method to supply the text it should use.
|
||||||
|
*/
|
||||||
|
public void setEmptyText(CharSequence text) {
|
||||||
|
((TextView) mHolder.emptyView).setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateEmptyViewVisibility() {
|
||||||
|
if (!mIsProgressBarVisible) {
|
||||||
|
RecyclerView.Adapter adapter = mHolder.recyclerView.getAdapter();
|
||||||
|
final boolean isEmptyViewVisible = (adapter != null) && (adapter.getItemCount() == 0);
|
||||||
|
mHolder.recyclerView.setVisibility(isEmptyViewVisible ? View.GONE : View.VISIBLE);
|
||||||
|
mHolder.emptyView.setVisibility(isEmptyViewVisible ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this method to show or hide the indeterminate progress bar.
|
||||||
|
* When shown, the RecyclerView will be hidden.
|
||||||
|
*
|
||||||
|
* @param visible true to show the progress bar, false to hide it. The initial value is false.
|
||||||
|
*/
|
||||||
|
public void setProgressBarVisible(boolean visible) {
|
||||||
|
if (mIsProgressBarVisible != visible) {
|
||||||
|
mIsProgressBarVisible = visible;
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
mHolder.recyclerView.setVisibility(View.GONE);
|
||||||
|
mHolder.emptyView.setVisibility(View.GONE);
|
||||||
|
mHolder.progress.show();
|
||||||
|
} else {
|
||||||
|
updateEmptyViewVisibility();
|
||||||
|
mHolder.progress.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,20 @@
|
||||||
package be.digitalia.fosdem.fragments;
|
package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
import android.view.View;
|
import android.support.v7.widget.DividerItemDecoration;
|
||||||
import android.widget.ListView;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.activities.EventDetailsActivity;
|
|
||||||
import be.digitalia.fosdem.adapters.EventsAdapter;
|
import be.digitalia.fosdem.adapters.EventsAdapter;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.db.DatabaseManager;
|
||||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
||||||
import be.digitalia.fosdem.model.Event;
|
|
||||||
|
|
||||||
public class SearchResultListFragment extends SmoothListFragment implements LoaderCallbacks<Cursor> {
|
public class SearchResultListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
||||||
|
|
||||||
private static final int EVENTS_LOADER_ID = 1;
|
private static final int EVENTS_LOADER_ID = 1;
|
||||||
private static final String ARG_QUERY = "query";
|
private static final String ARG_QUERY = "query";
|
||||||
|
@ -34,7 +33,13 @@ public class SearchResultListFragment extends SmoothListFragment implements Load
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
adapter = new EventsAdapter(getActivity());
|
adapter = new EventsAdapter(getActivity());
|
||||||
setListAdapter(adapter);
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRecyclerViewCreated(RecyclerView recyclerView, Bundle savedInstanceState) {
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext()));
|
||||||
|
recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL));
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -42,7 +47,7 @@ public class SearchResultListFragment extends SmoothListFragment implements Load
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
|
||||||
setEmptyText(getString(R.string.no_search_result));
|
setEmptyText(getString(R.string.no_search_result));
|
||||||
setListShown(false);
|
setProgressBarVisible(true);
|
||||||
|
|
||||||
getLoaderManager().initLoader(EVENTS_LOADER_ID, null, this);
|
getLoaderManager().initLoader(EVENTS_LOADER_ID, null, this);
|
||||||
}
|
}
|
||||||
|
@ -74,18 +79,11 @@ public class SearchResultListFragment extends SmoothListFragment implements Load
|
||||||
adapter.swapCursor(data);
|
adapter.swapCursor(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
setListShown(true);
|
setProgressBarVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(Loader<Cursor> loader) {
|
public void onLoaderReset(Loader<Cursor> loader) {
|
||||||
adapter.swapCursor(null);
|
adapter.swapCursor(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onListItemClick(ListView l, View v, int position, long id) {
|
|
||||||
Event event = adapter.getItem(position);
|
|
||||||
Intent intent = new Intent(getActivity(), EventDetailsActivity.class).putExtra(EventDetailsActivity.EXTRA_EVENT, event);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ public class SmoothListFragment extends Fragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
final Context context = getActivity();
|
final Context context = inflater.getContext();
|
||||||
|
|
||||||
mHolder = new ViewHolder();
|
mHolder = new ViewHolder();
|
||||||
|
|
||||||
|
@ -138,7 +138,7 @@ public class SmoothListFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default content for a SwipeRefreshListFragment has a TextView that can be shown when the list is empty.
|
* The default content for a SmoothListFragment has a TextView that can be shown when the list is empty.
|
||||||
* Call this method to supply the text it should use.
|
* Call this method to supply the text it should use.
|
||||||
*/
|
*/
|
||||||
public void setEmptyText(CharSequence text) {
|
public void setEmptyText(CharSequence text) {
|
||||||
|
|
|
@ -14,20 +14,20 @@ import android.support.v4.content.Loader;
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.support.v4.content.SharedPreferencesCompat;
|
import android.support.v4.content.SharedPreferencesCompat;
|
||||||
import android.support.v4.view.ViewPager;
|
import android.support.v4.view.ViewPager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import be.digitalia.fosdem.widgets.SlidingTabLayout;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.db.DatabaseManager;
|
||||||
import be.digitalia.fosdem.loaders.GlobalCacheLoader;
|
import be.digitalia.fosdem.loaders.GlobalCacheLoader;
|
||||||
import be.digitalia.fosdem.model.Day;
|
import be.digitalia.fosdem.model.Day;
|
||||||
|
import be.digitalia.fosdem.widgets.SlidingTabLayout;
|
||||||
|
|
||||||
public class TracksFragment extends Fragment implements LoaderCallbacks<List<Day>> {
|
public class TracksFragment extends Fragment implements RecycledViewPoolProvider, LoaderCallbacks<List<Day>> {
|
||||||
|
|
||||||
static class ViewHolder {
|
static class ViewHolder {
|
||||||
View contentView;
|
View contentView;
|
||||||
|
@ -35,6 +35,7 @@ public class TracksFragment extends Fragment implements LoaderCallbacks<List<Day
|
||||||
ViewPager pager;
|
ViewPager pager;
|
||||||
SlidingTabLayout slidingTabs;
|
SlidingTabLayout slidingTabs;
|
||||||
DaysAdapter daysAdapter;
|
DaysAdapter daysAdapter;
|
||||||
|
RecyclerView.RecycledViewPool recycledViewPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int DAYS_LOADER_ID = 1;
|
private static final int DAYS_LOADER_ID = 1;
|
||||||
|
@ -63,6 +64,7 @@ public class TracksFragment extends Fragment implements LoaderCallbacks<List<Day
|
||||||
holder.pager = (ViewPager) view.findViewById(R.id.pager);
|
holder.pager = (ViewPager) view.findViewById(R.id.pager);
|
||||||
holder.slidingTabs = (SlidingTabLayout) view.findViewById(R.id.sliding_tabs);
|
holder.slidingTabs = (SlidingTabLayout) view.findViewById(R.id.sliding_tabs);
|
||||||
holder.daysAdapter = new DaysAdapter(getChildFragmentManager());
|
holder.daysAdapter = new DaysAdapter(getChildFragmentManager());
|
||||||
|
holder.recycledViewPool = new RecyclerView.RecycledViewPool();
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
@ -93,6 +95,11 @@ public class TracksFragment extends Fragment implements LoaderCallbacks<List<Day
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecyclerView.RecycledViewPool getRecycledViewPool() {
|
||||||
|
return (holder == null) ? null : holder.recycledViewPool;
|
||||||
|
}
|
||||||
|
|
||||||
private static class DaysLoader extends GlobalCacheLoader<List<Day>> {
|
private static class DaysLoader extends GlobalCacheLoader<List<Day>> {
|
||||||
|
|
||||||
private final BroadcastReceiver scheduleRefreshedReceiver = new BroadcastReceiver() {
|
private final BroadcastReceiver scheduleRefreshedReceiver = new BroadcastReceiver() {
|
||||||
|
|
|
@ -4,28 +4,31 @@ import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
import android.support.v4.widget.CursorAdapter;
|
import android.support.v7.widget.DividerItemDecoration;
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.activities.TrackScheduleActivity;
|
import be.digitalia.fosdem.activities.TrackScheduleActivity;
|
||||||
|
import be.digitalia.fosdem.adapters.RecyclerViewCursorAdapter;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.db.DatabaseManager;
|
||||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
||||||
import be.digitalia.fosdem.model.Day;
|
import be.digitalia.fosdem.model.Day;
|
||||||
import be.digitalia.fosdem.model.Track;
|
import be.digitalia.fosdem.model.Track;
|
||||||
|
|
||||||
public class TracksListFragment extends SmoothListFragment implements LoaderCallbacks<Cursor> {
|
public class TracksListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
||||||
|
|
||||||
private static final int TRACKS_LOADER_ID = 1;
|
private static final int TRACKS_LOADER_ID = 1;
|
||||||
private static final String ARG_DAY = "day";
|
private static final String ARG_DAY = "day";
|
||||||
|
|
||||||
private Day day;
|
Day day;
|
||||||
private TracksAdapter adapter;
|
private TracksAdapter adapter;
|
||||||
|
|
||||||
public static TracksListFragment newInstance(Day day) {
|
public static TracksListFragment newInstance(Day day) {
|
||||||
|
@ -39,9 +42,20 @@ public class TracksListFragment extends SmoothListFragment implements LoaderCall
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
adapter = new TracksAdapter(getActivity());
|
adapter = new TracksAdapter();
|
||||||
day = getArguments().getParcelable(ARG_DAY);
|
day = getArguments().getParcelable(ARG_DAY);
|
||||||
setListAdapter(adapter);
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRecyclerViewCreated(RecyclerView recyclerView, Bundle savedInstanceState) {
|
||||||
|
Fragment parentFragment = getParentFragment();
|
||||||
|
if (parentFragment instanceof RecycledViewPoolProvider) {
|
||||||
|
recyclerView.setRecycledViewPool(((RecycledViewPoolProvider) parentFragment).getRecycledViewPool());
|
||||||
|
}
|
||||||
|
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext()));
|
||||||
|
recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL));
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -49,7 +63,7 @@ public class TracksListFragment extends SmoothListFragment implements LoaderCall
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
|
||||||
setEmptyText(getString(R.string.no_data));
|
setEmptyText(getString(R.string.no_data));
|
||||||
setListShown(false);
|
setProgressBarVisible(true);
|
||||||
|
|
||||||
getLoaderManager().initLoader(TRACKS_LOADER_ID, null, this);
|
getLoaderManager().initLoader(TRACKS_LOADER_ID, null, this);
|
||||||
}
|
}
|
||||||
|
@ -80,7 +94,7 @@ public class TracksListFragment extends SmoothListFragment implements LoaderCall
|
||||||
adapter.swapCursor(data);
|
adapter.swapCursor(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
setListShown(true);
|
setProgressBarVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -88,21 +102,12 @@ public class TracksListFragment extends SmoothListFragment implements LoaderCall
|
||||||
adapter.swapCursor(null);
|
adapter.swapCursor(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private class TracksAdapter extends RecyclerViewCursorAdapter<TrackViewHolder> {
|
||||||
public void onListItemClick(ListView l, View v, int position, long id) {
|
|
||||||
Track track = adapter.getItem(position);
|
|
||||||
Intent intent = new Intent(getActivity(), TrackScheduleActivity.class).putExtra(TrackScheduleActivity.EXTRA_DAY, day).putExtra(
|
|
||||||
TrackScheduleActivity.EXTRA_TRACK, track);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class TracksAdapter extends CursorAdapter {
|
|
||||||
|
|
||||||
private final LayoutInflater inflater;
|
private final LayoutInflater inflater;
|
||||||
|
|
||||||
public TracksAdapter(Context context) {
|
public TracksAdapter() {
|
||||||
super(context, null, 0);
|
inflater = LayoutInflater.from(getContext());
|
||||||
inflater = LayoutInflater.from(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -111,29 +116,41 @@ public class TracksListFragment extends SmoothListFragment implements LoaderCall
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
public TrackViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
View view = inflater.inflate(R.layout.simple_list_item_2_material, parent, false);
|
View view = inflater.inflate(R.layout.simple_list_item_2_material, parent, false);
|
||||||
|
return new TrackViewHolder(view);
|
||||||
ViewHolder holder = new ViewHolder();
|
|
||||||
holder.name = (TextView) view.findViewById(android.R.id.text1);
|
|
||||||
holder.type = (TextView) view.findViewById(android.R.id.text2);
|
|
||||||
view.setTag(holder);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void bindView(View view, Context context, Cursor cursor) {
|
public void onBindViewHolder(TrackViewHolder holder, Cursor cursor) {
|
||||||
ViewHolder holder = (ViewHolder) view.getTag();
|
holder.day = day;
|
||||||
holder.track = DatabaseManager.toTrack(cursor, holder.track);
|
holder.track = DatabaseManager.toTrack(cursor, holder.track);
|
||||||
holder.name.setText(holder.track.getName());
|
holder.name.setText(holder.track.getName());
|
||||||
holder.type.setText(holder.track.getType().getNameResId());
|
holder.type.setText(holder.track.getType().getNameResId());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static class ViewHolder {
|
static class TrackViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||||
TextView name;
|
TextView name;
|
||||||
TextView type;
|
TextView type;
|
||||||
|
|
||||||
|
Day day;
|
||||||
Track track;
|
Track track;
|
||||||
|
|
||||||
|
TrackViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
name = (TextView) itemView.findViewById(android.R.id.text1);
|
||||||
|
type = (TextView) itemView.findViewById(android.R.id.text2);
|
||||||
|
itemView.setOnClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
Context context = view.getContext();
|
||||||
|
Intent intent = new Intent(context, TrackScheduleActivity.class)
|
||||||
|
.putExtra(TrackScheduleActivity.EXTRA_DAY, day)
|
||||||
|
.putExtra(TrackScheduleActivity.EXTRA_TRACK, track);
|
||||||
|
context.startActivity(intent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
package be.digitalia.fosdem.widgets;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.view.ActionMode;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.widget.AbsListView;
|
|
||||||
import android.widget.AbsListView.MultiChoiceModeListener;
|
|
||||||
|
|
||||||
import be.digitalia.fosdem.R;
|
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Context menu for the bookmarks list items, available for API 11+ only.
|
|
||||||
*
|
|
||||||
* @author Christophe Beyls
|
|
||||||
*/
|
|
||||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
|
||||||
public class BookmarksMultiChoiceModeListener implements MultiChoiceModeListener {
|
|
||||||
|
|
||||||
private AbsListView listView;
|
|
||||||
|
|
||||||
public static void register(AbsListView listView) {
|
|
||||||
listView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL);
|
|
||||||
BookmarksMultiChoiceModeListener listener = new BookmarksMultiChoiceModeListener(listView);
|
|
||||||
listView.setMultiChoiceModeListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unregister(AbsListView listView) {
|
|
||||||
// Will close the ActionMode if open
|
|
||||||
listView.setChoiceMode(AbsListView.CHOICE_MODE_NONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BookmarksMultiChoiceModeListener(AbsListView listView) {
|
|
||||||
this.listView = listView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
|
||||||
mode.getMenuInflater().inflate(R.menu.action_mode_bookmarks, menu);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSelectedCountDisplay(ActionMode mode) {
|
|
||||||
int count = listView.getCheckedItemCount();
|
|
||||||
mode.setTitle(listView.getContext().getResources().getQuantityString(R.plurals.selected, count, count));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
|
||||||
updateSelectedCountDisplay(mode);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.delete:
|
|
||||||
// Remove multiple bookmarks at once
|
|
||||||
new RemoveBookmarksAsyncTask().execute(listView.getCheckedItemIds());
|
|
||||||
mode.finish();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
|
|
||||||
updateSelectedCountDisplay(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyActionMode(ActionMode mode) {
|
|
||||||
}
|
|
||||||
|
|
||||||
static class RemoveBookmarksAsyncTask extends AsyncTask<long[], Void, Void> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(long[]... params) {
|
|
||||||
DatabaseManager.getInstance().removeBookmarks(params[0]);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
package be.digitalia.fosdem.widgets;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.widget.Checkable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link android.widget.LinearLayout} implementing the {@link android.widget.Checkable}
|
||||||
|
* interface by keeping an internal 'checked' state flag.
|
||||||
|
*/
|
||||||
|
public class CheckableLinearLayout extends ForegroundLinearLayout implements Checkable {
|
||||||
|
private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
|
||||||
|
|
||||||
|
private boolean mChecked = false;
|
||||||
|
|
||||||
|
public CheckableLinearLayout(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isChecked() {
|
||||||
|
return mChecked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChecked(boolean b) {
|
||||||
|
if (b != mChecked) {
|
||||||
|
mChecked = b;
|
||||||
|
refreshDrawableState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggle() {
|
||||||
|
setChecked(!mChecked);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] onCreateDrawableState(int extraSpace) {
|
||||||
|
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||||
|
if (isChecked()) {
|
||||||
|
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
|
||||||
|
}
|
||||||
|
return drawableState;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package be.digitalia.fosdem.widgets;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.v7.widget.LinearLayoutCompat;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.Gravity;
|
||||||
|
|
||||||
|
import be.digitalia.fosdem.R;
|
||||||
|
|
||||||
|
public class ForegroundLinearLayout extends LinearLayoutCompat {
|
||||||
|
|
||||||
|
private Drawable mForeground;
|
||||||
|
|
||||||
|
private final Rect mSelfBounds = new Rect();
|
||||||
|
|
||||||
|
private final Rect mOverlayBounds = new Rect();
|
||||||
|
|
||||||
|
private int mForegroundGravity = Gravity.FILL;
|
||||||
|
|
||||||
|
protected boolean mForegroundInPadding = true;
|
||||||
|
|
||||||
|
boolean mForegroundBoundsChanged = false;
|
||||||
|
|
||||||
|
public ForegroundLinearLayout(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ForegroundLinearLayout(Context context, AttributeSet attrs) {
|
||||||
|
this(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ForegroundLinearLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
|
||||||
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundLinearLayout,
|
||||||
|
defStyle, 0);
|
||||||
|
|
||||||
|
mForegroundGravity = a.getInt(
|
||||||
|
R.styleable.ForegroundLinearLayout_android_foregroundGravity, mForegroundGravity);
|
||||||
|
|
||||||
|
final Drawable d = a.getDrawable(R.styleable.ForegroundLinearLayout_android_foreground);
|
||||||
|
if (d != null) {
|
||||||
|
setForeground(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
mForegroundInPadding = a.getBoolean(
|
||||||
|
R.styleable.ForegroundLinearLayout_foregroundInsidePadding, true);
|
||||||
|
|
||||||
|
a.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes how the foreground is positioned.
|
||||||
|
*
|
||||||
|
* @return foreground gravity.
|
||||||
|
* @see #setForegroundGravity(int)
|
||||||
|
*/
|
||||||
|
public int getForegroundGravity() {
|
||||||
|
return mForegroundGravity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes how the foreground is positioned. Defaults to START and TOP.
|
||||||
|
*
|
||||||
|
* @param foregroundGravity See {@link Gravity}
|
||||||
|
* @see #getForegroundGravity()
|
||||||
|
*/
|
||||||
|
public void setForegroundGravity(int foregroundGravity) {
|
||||||
|
if (mForegroundGravity != foregroundGravity) {
|
||||||
|
if ((foregroundGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
|
||||||
|
foregroundGravity |= Gravity.START;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((foregroundGravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
|
||||||
|
foregroundGravity |= Gravity.TOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
mForegroundGravity = foregroundGravity;
|
||||||
|
|
||||||
|
if (mForegroundGravity == Gravity.FILL && mForeground != null) {
|
||||||
|
Rect padding = new Rect();
|
||||||
|
mForeground.getPadding(padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean verifyDrawable(@NonNull Drawable who) {
|
||||||
|
return super.verifyDrawable(who) || (who == mForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||||
|
@Override
|
||||||
|
public void jumpDrawablesToCurrentState() {
|
||||||
|
super.jumpDrawablesToCurrentState();
|
||||||
|
if (mForeground != null) {
|
||||||
|
mForeground.jumpToCurrentState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void drawableStateChanged() {
|
||||||
|
super.drawableStateChanged();
|
||||||
|
if (mForeground != null && mForeground.isStateful()) {
|
||||||
|
mForeground.setState(getDrawableState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supply a Drawable that is to be rendered on top of all of the child
|
||||||
|
* views in the frame layout. Any padding in the Drawable will be taken
|
||||||
|
* into account by ensuring that the children are inset to be placed
|
||||||
|
* inside of the padding area.
|
||||||
|
*
|
||||||
|
* @param drawable The Drawable to be drawn on top of the children.
|
||||||
|
*/
|
||||||
|
public void setForeground(Drawable drawable) {
|
||||||
|
if (mForeground != drawable) {
|
||||||
|
if (mForeground != null) {
|
||||||
|
mForeground.setCallback(null);
|
||||||
|
unscheduleDrawable(mForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
mForeground = drawable;
|
||||||
|
|
||||||
|
if (drawable != null) {
|
||||||
|
setWillNotDraw(false);
|
||||||
|
drawable.setCallback(this);
|
||||||
|
if (drawable.isStateful()) {
|
||||||
|
drawable.setState(getDrawableState());
|
||||||
|
}
|
||||||
|
if (mForegroundGravity == Gravity.FILL) {
|
||||||
|
Rect padding = new Rect();
|
||||||
|
drawable.getPadding(padding);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setWillNotDraw(true);
|
||||||
|
}
|
||||||
|
requestLayout();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the drawable used as the foreground of this FrameLayout. The
|
||||||
|
* foreground drawable, if non-null, is always drawn on top of the children.
|
||||||
|
*
|
||||||
|
* @return A Drawable or null if no foreground was set.
|
||||||
|
*/
|
||||||
|
public Drawable getForeground() {
|
||||||
|
return mForeground;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||||
|
super.onLayout(changed, left, top, right, bottom);
|
||||||
|
mForegroundBoundsChanged |= changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
|
mForegroundBoundsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(@NonNull Canvas canvas) {
|
||||||
|
super.draw(canvas);
|
||||||
|
|
||||||
|
if (mForeground != null) {
|
||||||
|
final Drawable foreground = mForeground;
|
||||||
|
|
||||||
|
if (mForegroundBoundsChanged) {
|
||||||
|
mForegroundBoundsChanged = false;
|
||||||
|
final Rect selfBounds = mSelfBounds;
|
||||||
|
final Rect overlayBounds = mOverlayBounds;
|
||||||
|
|
||||||
|
final int w = getRight() - getLeft();
|
||||||
|
final int h = getBottom() - getTop();
|
||||||
|
|
||||||
|
if (mForegroundInPadding) {
|
||||||
|
selfBounds.set(0, 0, w, h);
|
||||||
|
} else {
|
||||||
|
selfBounds.set(getPaddingLeft(), getPaddingTop(),
|
||||||
|
w - getPaddingRight(), h - getPaddingBottom());
|
||||||
|
}
|
||||||
|
|
||||||
|
Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(),
|
||||||
|
foreground.getIntrinsicHeight(), selfBounds, overlayBounds);
|
||||||
|
foreground.setBounds(overlayBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreground.draw(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
@Override
|
||||||
|
public void drawableHotspotChanged(float x, float y) {
|
||||||
|
super.drawableHotspotChanged(x, y);
|
||||||
|
if (mForeground != null) {
|
||||||
|
mForeground.setHotspot(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,483 @@
|
||||||
|
package be.digitalia.fosdem.widgets;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.v4.util.LongSparseArray;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.support.v7.view.ActionMode;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.util.SparseBooleanArray;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Checkable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to reproduce ListView's modal MultiChoice mode with a RecyclerView.
|
||||||
|
* Compatible with API 7+.
|
||||||
|
* Declare and use this class from inside your Adapter.
|
||||||
|
*
|
||||||
|
* @author Christophe Beyls
|
||||||
|
*/
|
||||||
|
public class MultiChoiceHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A handy ViewHolder base class which works with the MultiChoiceHelper
|
||||||
|
* and reproduces the default behavior of a ListView.
|
||||||
|
*/
|
||||||
|
public static abstract class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
|
View.OnClickListener clickListener;
|
||||||
|
MultiChoiceHelper multiChoiceHelper;
|
||||||
|
|
||||||
|
public ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
itemView.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
if (isMultiChoiceActive()) {
|
||||||
|
int position = getAdapterPosition();
|
||||||
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
|
multiChoiceHelper.toggleItemChecked(position, false);
|
||||||
|
updateCheckedState(position);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (clickListener != null) {
|
||||||
|
clickListener.onClick(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onLongClick(View view) {
|
||||||
|
if ((multiChoiceHelper == null) || isMultiChoiceActive()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int position = getAdapterPosition();
|
||||||
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
|
multiChoiceHelper.setItemChecked(position, true, false);
|
||||||
|
updateCheckedState(position);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCheckedState(int position) {
|
||||||
|
final boolean isChecked = multiChoiceHelper.isItemChecked(position);
|
||||||
|
if (itemView instanceof Checkable) {
|
||||||
|
((Checkable) itemView).setChecked(isChecked);
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||||
|
itemView.setActivated(isChecked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnClickListener(View.OnClickListener clickListener) {
|
||||||
|
this.clickListener = clickListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bind(MultiChoiceHelper multiChoiceHelper, int position) {
|
||||||
|
this.multiChoiceHelper = multiChoiceHelper;
|
||||||
|
if (multiChoiceHelper != null) {
|
||||||
|
updateCheckedState(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMultiChoiceActive() {
|
||||||
|
return (multiChoiceHelper != null) && (multiChoiceHelper.getCheckedItemCount() > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface MultiChoiceModeListener extends ActionMode.Callback {
|
||||||
|
/**
|
||||||
|
* Called when an item is checked or unchecked during selection mode.
|
||||||
|
*
|
||||||
|
* @param mode The {@link ActionMode} providing the selection startSupportActionModemode
|
||||||
|
* @param position Adapter position of the item that was checked or unchecked
|
||||||
|
* @param id Adapter ID of the item that was checked or unchecked
|
||||||
|
* @param checked <code>true</code> if the item is now checked, <code>false</code>
|
||||||
|
* if the item is now unchecked.
|
||||||
|
*/
|
||||||
|
void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
|
||||||
|
|
||||||
|
private final AppCompatActivity activity;
|
||||||
|
private final RecyclerView.Adapter adapter;
|
||||||
|
private SparseBooleanArray checkStates;
|
||||||
|
private LongSparseArray<Integer> checkedIdStates;
|
||||||
|
private int checkedItemCount = 0;
|
||||||
|
private MultiChoiceModeWrapper multiChoiceModeCallback;
|
||||||
|
ActionMode choiceActionMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure this constructor is called before setting the adapter on the RecyclerView
|
||||||
|
* so this class will be notified before the RecyclerView in case of data set changes.
|
||||||
|
*/
|
||||||
|
public MultiChoiceHelper(@NonNull AppCompatActivity activity, @NonNull RecyclerView.Adapter adapter) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.adapter = adapter;
|
||||||
|
adapter.registerAdapterDataObserver(new AdapterDataSetObserver());
|
||||||
|
checkStates = new SparseBooleanArray(0);
|
||||||
|
if (adapter.hasStableIds()) {
|
||||||
|
checkedIdStates = new LongSparseArray<>(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Context getContext() {
|
||||||
|
return activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
|
||||||
|
if (listener == null) {
|
||||||
|
multiChoiceModeCallback = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (multiChoiceModeCallback == null) {
|
||||||
|
multiChoiceModeCallback = new MultiChoiceModeWrapper();
|
||||||
|
}
|
||||||
|
multiChoiceModeCallback.setWrapped(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCheckedItemCount() {
|
||||||
|
return checkedItemCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isItemChecked(int position) {
|
||||||
|
return checkStates.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SparseBooleanArray getCheckedItemPositions() {
|
||||||
|
return checkStates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long[] getCheckedItemIds() {
|
||||||
|
final LongSparseArray<Integer> idStates = checkedIdStates;
|
||||||
|
if (idStates == null) {
|
||||||
|
return new long[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
final int count = idStates.size();
|
||||||
|
final long[] ids = new long[count];
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
ids[i] = idStates.keyAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearChoices() {
|
||||||
|
if (checkedItemCount > 0) {
|
||||||
|
final int start = checkStates.keyAt(0);
|
||||||
|
final int end = checkStates.keyAt(checkStates.size() - 1);
|
||||||
|
checkStates.clear();
|
||||||
|
if (checkedIdStates != null) {
|
||||||
|
checkedIdStates.clear();
|
||||||
|
}
|
||||||
|
checkedItemCount = 0;
|
||||||
|
|
||||||
|
adapter.notifyItemRangeChanged(start, end - start + 1);
|
||||||
|
|
||||||
|
if (choiceActionMode != null) {
|
||||||
|
choiceActionMode.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItemChecked(int position, boolean value, boolean notifyChanged) {
|
||||||
|
// Start selection mode if needed. We don't need to if we're unchecking something.
|
||||||
|
if (value) {
|
||||||
|
startSupportActionModeIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean oldValue = checkStates.get(position);
|
||||||
|
checkStates.put(position, value);
|
||||||
|
|
||||||
|
if (oldValue != value) {
|
||||||
|
final long id = adapter.getItemId(position);
|
||||||
|
|
||||||
|
if (checkedIdStates != null) {
|
||||||
|
if (value) {
|
||||||
|
checkedIdStates.put(id, position);
|
||||||
|
} else {
|
||||||
|
checkedIdStates.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
checkedItemCount++;
|
||||||
|
} else {
|
||||||
|
checkedItemCount--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyChanged) {
|
||||||
|
adapter.notifyItemChanged(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (choiceActionMode != null) {
|
||||||
|
multiChoiceModeCallback.onItemCheckedStateChanged(choiceActionMode, position, id, value);
|
||||||
|
if (checkedItemCount == 0) {
|
||||||
|
choiceActionMode.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleItemChecked(int position, boolean notifyChanged) {
|
||||||
|
setItemChecked(position, !isItemChecked(position), notifyChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Parcelable onSaveInstanceState() {
|
||||||
|
SavedState savedState = new SavedState();
|
||||||
|
savedState.checkedItemCount = checkedItemCount;
|
||||||
|
savedState.checkStates = clone(checkStates);
|
||||||
|
if (checkedIdStates != null) {
|
||||||
|
savedState.checkedIdStates = checkedIdStates.clone();
|
||||||
|
}
|
||||||
|
return savedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SparseBooleanArray clone(SparseBooleanArray original) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||||
|
return original.clone();
|
||||||
|
}
|
||||||
|
final int size = original.size();
|
||||||
|
SparseBooleanArray clone = new SparseBooleanArray(size);
|
||||||
|
for (int i = 0; i < size; ++i) {
|
||||||
|
clone.append(original.keyAt(i), original.valueAt(i));
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onRestoreInstanceState(Parcelable state) {
|
||||||
|
if ((state != null) && (checkedItemCount == 0)) {
|
||||||
|
SavedState savedState = (SavedState) state;
|
||||||
|
checkedItemCount = savedState.checkedItemCount;
|
||||||
|
checkStates = savedState.checkStates;
|
||||||
|
checkedIdStates = savedState.checkedIdStates;
|
||||||
|
|
||||||
|
if (checkedItemCount > 0) {
|
||||||
|
// Empty adapter is given a chance to be populated before completeRestoreInstanceState()
|
||||||
|
if (adapter.getItemCount() > 0) {
|
||||||
|
confirmCheckedPositions();
|
||||||
|
}
|
||||||
|
activity.getWindow().getDecorView().post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
completeRestoreInstanceState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void completeRestoreInstanceState() {
|
||||||
|
if (checkedItemCount > 0) {
|
||||||
|
if (adapter.getItemCount() == 0) {
|
||||||
|
// Adapter was not populated, clear the selection
|
||||||
|
confirmCheckedPositions();
|
||||||
|
} else {
|
||||||
|
startSupportActionModeIfNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startSupportActionModeIfNeeded() {
|
||||||
|
if (choiceActionMode == null) {
|
||||||
|
if (multiChoiceModeCallback == null) {
|
||||||
|
throw new IllegalStateException("No callback set");
|
||||||
|
}
|
||||||
|
choiceActionMode = activity.startSupportActionMode(multiChoiceModeCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SavedState implements Parcelable {
|
||||||
|
|
||||||
|
int checkedItemCount;
|
||||||
|
SparseBooleanArray checkStates;
|
||||||
|
LongSparseArray<Integer> checkedIdStates;
|
||||||
|
|
||||||
|
SavedState() {
|
||||||
|
}
|
||||||
|
|
||||||
|
SavedState(Parcel in) {
|
||||||
|
checkedItemCount = in.readInt();
|
||||||
|
checkStates = in.readSparseBooleanArray();
|
||||||
|
final int n = in.readInt();
|
||||||
|
if (n >= 0) {
|
||||||
|
checkedIdStates = new LongSparseArray<>(n);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
final long key = in.readLong();
|
||||||
|
final int value = in.readInt();
|
||||||
|
checkedIdStates.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel out, int flags) {
|
||||||
|
out.writeInt(checkedItemCount);
|
||||||
|
out.writeSparseBooleanArray(checkStates);
|
||||||
|
final int n = checkedIdStates != null ? checkedIdStates.size() : -1;
|
||||||
|
out.writeInt(n);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
out.writeLong(checkedIdStates.keyAt(i));
|
||||||
|
out.writeInt(checkedIdStates.valueAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
|
||||||
|
@Override
|
||||||
|
public SavedState createFromParcel(Parcel in) {
|
||||||
|
return new SavedState(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SavedState[] newArray(int size) {
|
||||||
|
return new SavedState[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void confirmCheckedPositions() {
|
||||||
|
if (checkedItemCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int itemCount = adapter.getItemCount();
|
||||||
|
boolean checkedCountChanged = false;
|
||||||
|
|
||||||
|
if (itemCount == 0) {
|
||||||
|
// Optimized path for empty adapter: remove all items.
|
||||||
|
checkStates.clear();
|
||||||
|
if (checkedIdStates != null) {
|
||||||
|
checkedIdStates.clear();
|
||||||
|
}
|
||||||
|
checkedItemCount = 0;
|
||||||
|
checkedCountChanged = true;
|
||||||
|
} else if (checkedIdStates != null) {
|
||||||
|
// Clear out the positional check states, we'll rebuild it below from IDs.
|
||||||
|
checkStates.clear();
|
||||||
|
|
||||||
|
for (int checkedIndex = 0; checkedIndex < checkedIdStates.size(); checkedIndex++) {
|
||||||
|
final long id = checkedIdStates.keyAt(checkedIndex);
|
||||||
|
final int lastPos = checkedIdStates.valueAt(checkedIndex);
|
||||||
|
|
||||||
|
if ((lastPos >= itemCount) || (id != adapter.getItemId(lastPos))) {
|
||||||
|
// Look around to see if the ID is nearby. If not, uncheck it.
|
||||||
|
final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE);
|
||||||
|
final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, itemCount);
|
||||||
|
boolean found = false;
|
||||||
|
for (int searchPos = start; searchPos < end; searchPos++) {
|
||||||
|
final long searchId = adapter.getItemId(searchPos);
|
||||||
|
if (id == searchId) {
|
||||||
|
found = true;
|
||||||
|
checkStates.put(searchPos, true);
|
||||||
|
checkedIdStates.setValueAt(checkedIndex, searchPos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
checkedIdStates.delete(id);
|
||||||
|
checkedIndex--;
|
||||||
|
checkedItemCount--;
|
||||||
|
checkedCountChanged = true;
|
||||||
|
if (choiceActionMode != null && multiChoiceModeCallback != null) {
|
||||||
|
multiChoiceModeCallback.onItemCheckedStateChanged(choiceActionMode, lastPos, id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
checkStates.put(lastPos, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the total number of items decreased, remove all out-of-range check indexes.
|
||||||
|
for (int i = checkStates.size() - 1; (i >= 0) && (checkStates.keyAt(i) >= itemCount); i--) {
|
||||||
|
if (checkStates.valueAt(i)) {
|
||||||
|
checkedItemCount--;
|
||||||
|
checkedCountChanged = true;
|
||||||
|
}
|
||||||
|
checkStates.delete(checkStates.keyAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkedCountChanged && choiceActionMode != null) {
|
||||||
|
if (checkedItemCount == 0) {
|
||||||
|
choiceActionMode.finish();
|
||||||
|
} else {
|
||||||
|
choiceActionMode.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AdapterDataSetObserver extends RecyclerView.AdapterDataObserver {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChanged() {
|
||||||
|
confirmCheckedPositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||||
|
confirmCheckedPositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
|
||||||
|
confirmCheckedPositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
||||||
|
confirmCheckedPositions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiChoiceModeWrapper implements MultiChoiceModeListener {
|
||||||
|
|
||||||
|
private MultiChoiceModeListener wrapped;
|
||||||
|
|
||||||
|
public void setWrapped(@NonNull MultiChoiceModeListener wrapped) {
|
||||||
|
this.wrapped = wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||||
|
return wrapped.onCreateActionMode(mode, menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||||
|
return wrapped.onPrepareActionMode(mode, menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||||
|
return wrapped.onActionItemClicked(mode, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyActionMode(ActionMode mode) {
|
||||||
|
wrapped.onDestroyActionMode(mode);
|
||||||
|
choiceActionMode = null;
|
||||||
|
clearChoices();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
|
||||||
|
wrapped.onItemCheckedStateChanged(mode, position, id, checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
app/src/main/res/drawable/checkable_foreground.xml
Normal file
11
app/src/main/res/drawable/checkable_foreground.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item android:state_checked="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/fosdem_blue_translucent"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:drawable="@android:color/transparent"/>
|
||||||
|
|
||||||
|
</selector>
|
|
@ -1,9 +1,12 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<be.digitalia.fosdem.widgets.CheckableLinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/activatedBackgroundIndicator"
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="@drawable/checkable_foreground"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingBottom="@dimen/list_item_padding"
|
android:paddingBottom="@dimen/list_item_padding"
|
||||||
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
|
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
|
||||||
|
@ -41,4 +44,4 @@
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
tools:text="Saturday, 09:30 - 09:55 | Janson"/>
|
tools:text="Saturday, 09:30 - 09:55 | Janson"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</be.digitalia.fosdem.widgets.CheckableLinearLayout>
|
|
@ -2,6 +2,8 @@
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:focusable="true"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:minHeight="?attr/listPreferredItemHeight"
|
android:minHeight="?attr/listPreferredItemHeight"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
<!-- Drawable used as a background for activated items. -->
|
<!-- Drawable used as a background for activated items. -->
|
||||||
<attr name="activatedBackgroundIndicator" format="reference"/>
|
<attr name="activatedBackgroundIndicator" format="reference"/>
|
||||||
|
<attr name="recyclerViewStyle" format="reference"/>
|
||||||
|
|
||||||
<declare-styleable name="SlidingTabLayout">
|
<declare-styleable name="SlidingTabLayout">
|
||||||
<attr name="indicatorColor" format="color"/>
|
<attr name="indicatorColor" format="color"/>
|
||||||
|
@ -26,6 +27,12 @@
|
||||||
<attr name="insetForeground" format="color|reference"/>
|
<attr name="insetForeground" format="color|reference"/>
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
|
<declare-styleable name="ForegroundLinearLayout">
|
||||||
|
<attr name="android:foreground"/>
|
||||||
|
<attr name="android:foregroundGravity"/>
|
||||||
|
<attr name="foregroundInsidePadding" format="boolean"/>
|
||||||
|
</declare-styleable>
|
||||||
|
|
||||||
<declare-styleable name="PrimaryTextColors">
|
<declare-styleable name="PrimaryTextColors">
|
||||||
<attr name="android:textColorPrimary"/>
|
<attr name="android:textColorPrimary"/>
|
||||||
<attr name="android:textColorPrimaryInverse"/>
|
<attr name="android:textColorPrimaryInverse"/>
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
<item name="android:windowContentOverlay">@null</item>
|
<item name="android:windowContentOverlay">@null</item>
|
||||||
|
|
||||||
<item name="activatedBackgroundIndicator">@drawable/activated_background</item>
|
<item name="activatedBackgroundIndicator">@drawable/activated_background</item>
|
||||||
|
<item name="recyclerViewStyle">@style/RecyclerView</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme" parent="Base.AppTheme">
|
<style name="AppTheme" parent="Base.AppTheme">
|
||||||
|
@ -61,6 +62,10 @@
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
|
|
||||||
|
<style name="RecyclerView" parent="android:Widget">
|
||||||
|
<item name="android:scrollbars">vertical</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Design.ScrimInsetsFrameLayout" parent="">
|
<style name="Widget.Design.ScrimInsetsFrameLayout" parent="">
|
||||||
<item name="insetForeground">#4000</item>
|
<item name="insetForeground">#4000</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue