/*
* Copyright (c) 2015 Ha Duy Trung
*
* 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 io.github.hidroh.materialistic.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.v4.util.LongSparseArray;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import io.github.hidroh.materialistic.AppUtils;
import io.github.hidroh.materialistic.Navigable;
import io.github.hidroh.materialistic.Preferences;
import io.github.hidroh.materialistic.R;
import io.github.hidroh.materialistic.ResourcesProvider;
import io.github.hidroh.materialistic.annotation.Synthetic;
import io.github.hidroh.materialistic.data.Item;
import io.github.hidroh.materialistic.data.ItemManager;
public class SinglePageItemRecyclerViewAdapter
extends ItemRecyclerViewAdapter<ToggleItemViewHolder> {
private static final int VIEW_TYPE_FOOTER = -1;
private final Object TOGGLE = new Object();
private final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
unlockBinding();
}
}
};
@Inject ResourcesProvider mResourcesProvider;
private int mLevelIndicatorWidth = 0;
private final boolean mAutoExpand;
private boolean mColorCoded = true;
private TypedArray mColors;
private final @NonNull SavedState mState;
private ItemTouchHelper mItemTouchHelper;
private int[] mLock;
public SinglePageItemRecyclerViewAdapter(ItemManager itemManager,
@NonNull SavedState state,
boolean autoExpand) {
super(itemManager);
this.mState = state;
mAutoExpand = autoExpand;
}
@Override
public void attach(Context context, RecyclerView recyclerView) {
super.attach(context, recyclerView);
mLevelIndicatorWidth = AppUtils.getDimensionInDp(mContext, R.dimen.level_indicator_width);
mColors = mResourcesProvider.obtainTypedArray(R.array.color_codes);
mItemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
0, ItemTouchHelper.RIGHT) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
@Override
public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
Item item = getItem(viewHolder.getAdapterPosition());
if (item == null || item.getKidCount() == 0) {
return 0;
}
return super.getSwipeDirs(recyclerView, viewHolder);
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
Item item = getItem(position);
if (item != null) {
notifyItemChanged(position);
toggleKids(item);
}
}
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
float swipeWidth = viewHolder.itemView.getWidth() * getSwipeThreshold(viewHolder);
dX = Math.max(dX, -swipeWidth);
dX = Math.min(dX, swipeWidth);
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
@Override
public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
return 0.1f;
}
});
mItemTouchHelper.attachToRecyclerView(recyclerView);
recyclerView.addOnScrollListener(mScrollListener);
}
@Override
public void detach(Context context, RecyclerView recyclerView) {
super.detach(context, recyclerView);
recyclerView.removeOnScrollListener(mScrollListener);
mColors.recycle();
mItemTouchHelper.attachToRecyclerView(null);
}
@Override
public ToggleItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_FOOTER) {
return new ToggleItemViewHolder(mLayoutInflater.inflate(R.layout.item_footer, parent, false), null);
}
final ToggleItemViewHolder holder =
new ToggleItemViewHolder(mLayoutInflater.inflate(R.layout.item_comment, parent, false));
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
holder.itemView.getLayoutParams();
params.leftMargin = mLevelIndicatorWidth * viewType;
holder.itemView.setLayoutParams(params);
return holder;
}
@Override
public void onBindViewHolder(ToggleItemViewHolder holder, int position, List<Object> payloads) {
if (payloads.contains(TOGGLE)) {
Item item = getItem(position);
if (item != null) {
bindToggle(holder, item, mState.isExpanded(item));
}
} else {
super.onBindViewHolder(holder, position, payloads);
}
}
@Override
public void onBindViewHolder(ToggleItemViewHolder holder, int position) {
if (holder.isFooter()) {
return;
}
if (mLock != null && mLock[0] <= position && position <= mLock[1]) {
clear(holder);
return;
}
if (mColorCoded && mColors != null && mColors.length() > 0) {
holder.mLevel.setVisibility(View.VISIBLE);
holder.mLevel.setBackgroundColor(getThreadColor(getItemViewType(position)));
} else {
holder.mLevel.setVisibility(View.GONE);
}
super.onBindViewHolder(holder, position);
}
@Override
public int getItemViewType(int position) {
Item item = getItem(position);
if (item == null) { // footer
return VIEW_TYPE_FOOTER;
}
return item.getLevel() - 1;
}
@Override
public int getItemCount() {
return mState.size();
}
@Override
public void initDisplayOptions(Context context) {
mColorCoded = Preferences.colorCodeEnabled(context);
super.initDisplayOptions(context);
}
@Override
public void getNextPosition(int position, int direction, PositionCallback callback) {
if (position < 0) {
return;
}
Item item = getItem(position);
if (item == null) {
return;
}
long id = item.getNeighbour(direction);
switch (direction) {
case Navigable.DIRECTION_UP:
if (id == 0) { // no more previous sibling, try previous list item
setSelectedPosition(position - 1, callback);
} else {
setSelectedPosition(mState.indexOf(id), callback);
}
break;
case Navigable.DIRECTION_DOWN:
if (id == 0) { // no more next sibling, try next list item
setSelectedPosition(position + 1, callback);
} else {
setSelectedPosition(mState.indexOf(id), callback);
}
break;
case Navigable.DIRECTION_LEFT:
if (id != 0) {
setSelectedPosition(mState.indexOf(id), callback);
}
break;
case Navigable.DIRECTION_RIGHT:
if (id == 0) { // no kids, try next list item
setSelectedPosition(position + 1, callback);
} else if (mState.isExpanded(item)) {
setSelectedPosition(mState.indexOf(id), callback);
} else {
expand(item, callback);
}
break;
}
}
@Override
protected Item getItem(int position) {
if (position < 0 || position >= mState.size()) {
return null;
}
return mState.get(position);
}
@Override
protected void onItemLoaded(int position, Item item) {
// item position may already be shifted due to expansion, need to get new position
int index = mState.indexOf(item);
if (index >= 0 && index < getItemCount()) {
notifyItemChanged(index);
}
}
@Override
protected void clear(ToggleItemViewHolder holder) {
super.clear(holder);
holder.mToggleButton.setVisibility(View.GONE);
}
@Override
protected void bind(ToggleItemViewHolder holder, Item item) {
super.bind(holder, item);
if (item == null) {
return;
}
holder.mPostedTextView.setText(item.getDisplayedTime(mContext));
holder.mPostedTextView.append(item.getDisplayedAuthor(mContext, true,
getThreadColor(getItemViewType(holder.getAdapterPosition()))));
bindKids(holder, item);
}
@Override
public void lockBinding(int[] lock) {
mLock = lock;
}
@Synthetic
void unlockBinding() {
if (mLock != null) {
notifyItemRangeChanged(mLock[0], mLock[1] - mLock[0] + 1);
mLock = null;
}
}
private int getThreadColor(int itemViewType) {
return mColorCoded ? mColors.getColor(itemViewType % mColors.length(), 0) : 0;
}
private void bindKids(final ToggleItemViewHolder holder, final Item item) {
holder.mToggleButton.setVisibility(View.GONE);
if (item.getKidCount() == 0) {
return;
}
if (!item.isCollapsed() && mAutoExpand) {
expand(item);
}
bindToggle(holder, item, mState.isExpanded(item));
}
@Synthetic
void toggleKids(Item item) {
boolean expanded = mState.isExpanded(item);
item.setCollapsed(!item.isCollapsed());
if (expanded) {
collapse(item);
} else {
expand(item);
}
}
private void bindToggle(ToggleItemViewHolder holder, Item item, boolean expanded) {
changeToggleState(holder, item, expanded);
holder.mToggleButton.setVisibility(View.VISIBLE);
holder.mToggleButton.setOnClickListener(v -> {
changeToggleState(holder, item, !mState.isExpanded(item));
toggleKids(item);
});
}
private void changeToggleState(ToggleItemViewHolder holder, Item item, boolean expanded) {
holder.mToggle.setText(mContext.getResources()
.getQuantityString(R.plurals.comments_count, item.getKidCount(), item.getKidCount()));
holder.mToggle.setCompoundDrawablesWithIntrinsicBounds(0, 0, expanded ?
R.drawable.ic_expand_less_white_24dp : R.drawable.ic_expand_more_white_24dp, 0);
}
private void expand(Item item) {
expand(item, null);
}
private void expand(final Item item, PositionCallback callback) {
if (mState.isExpanded(item)) {
return;
}
mRecyclerView.post(() -> {
if (mRecyclerView == null) {
return; // adapter detached
}
int index = mState.expand(item);
notifyItemRangeInserted(index, item.getKidCount());
notifyItemChanged(index - 1, TOGGLE);
mRecyclerView.getItemAnimator().isRunning(() -> setSelectedPosition(index, callback));
});
}
private void collapse(Item item) {
int[] collapsedState = mState.collapse(item);
notifyItemRangeRemoved(collapsedState[0], collapsedState[1]);
}
private void setSelectedPosition(int position, PositionCallback callback) {
if (callback != null) {
callback.onPosition(position);
}
}
public static class SavedState implements Parcelable {
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel source) {
return new SavedState(source);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
private final ArrayList<Item> list = new ArrayList<>();
private final LongSparseArray<Item> map = new LongSparseArray<>();
private final Set<String> expanded = new HashSet<>();
public SavedState(ArrayList<Item> list) {
list.add(null); // footer
addAll(0, list);
}
@SuppressWarnings("unchecked")
@Synthetic
SavedState(Parcel source) {
ArrayList<Item> savedList = source.readArrayList(Item.class.getClassLoader());
addAll(0, savedList);
expanded.addAll(source.createStringArrayList());
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeList(list);
dest.writeStringList(new ArrayList<>(expanded));
}
@Synthetic
int size() {
return list.size();
}
@Synthetic
Item get(int position) {
return list.get(position);
}
@Synthetic
int indexOf(long itemId) {
return indexOf(map.get(itemId));
}
@Synthetic
int indexOf(Item item) {
return list.indexOf(item);
}
@Synthetic
boolean isExpanded(Item item) {
return isExpanded(item.getId());
}
@Synthetic
boolean isExpanded(String itemId) {
return expanded.contains(itemId);
}
@Synthetic
int expand(Item item) {
expanded.add(item.getId());
int index = indexOf(item) + 1;
addAll(index, Arrays.asList(item.getKidItems())); // recursive
return index;
}
@Synthetic
int[] collapse(Item item) {
int index = indexOf(item) + 1;
int count = recursiveRemove(item);
return new int[]{index, count};
}
private void addAll(int index, List<Item> items) {
list.addAll(index, items);
for (Item item : items) {
if (item != null) {
map.put(item.getLongId(), item);
}
}
}
private int recursiveRemove(Item item) {
if (!isExpanded(item.getId())) {
return 0;
}
// if item is already expanded, its kids must be added, so we need to remove them
int count = item.getKidCount();
expanded.remove(item.getId());
for (Item kid : item.getKidItems()) {
count += recursiveRemove(kid);
remove(kid);
}
return count;
}
private void remove(Item item) {
list.remove(item);
map.remove(item.getLongId());
}
}
}