/*
* Copyright (C) 2014 Lucas Rocha
*
* 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 com.marshalchen.common.uimodule.twowayview.widget;
import android.content.Context;
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.Adapter;
import android.support.v7.widget.RecyclerView.LayoutParams;
import android.support.v7.widget.RecyclerView.Recycler;
import android.support.v7.widget.RecyclerView.State;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import com.marshalchen.common.uimodule.twowayview.TwoWayLayoutManager;
import com.marshalchen.common.uimodule.twowayview.widget.Lanes.LaneInfo;
import static com.marshalchen.common.uimodule.twowayview.widget.Lanes.calculateLaneSize;
public abstract class BaseLayoutManager extends TwoWayLayoutManager {
private static final String LOGTAG = "BaseLayoutManager";
protected static class ItemEntry implements Parcelable {
public int startLane;
public int anchorLane;
private int[] spanMargins;
public ItemEntry(int startLane, int anchorLane) {
this.startLane = startLane;
this.anchorLane = anchorLane;
}
public ItemEntry(Parcel in) {
startLane = in.readInt();
anchorLane = in.readInt();
final int marginCount = in.readInt();
if (marginCount > 0) {
spanMargins = new int[marginCount];
for (int i = 0; i < marginCount; i++) {
spanMargins[i] = in.readInt();
}
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(startLane);
out.writeInt(anchorLane);
final int marginCount = (spanMargins != null ? spanMargins.length : 0);
out.writeInt(marginCount);
for (int i = 0; i < marginCount; i++) {
out.writeInt(spanMargins[i]);
}
}
void setLane(LaneInfo laneInfo) {
startLane = laneInfo.startLane;
anchorLane = laneInfo.anchorLane;
}
void invalidateLane() {
startLane = Lanes.NO_LANE;
anchorLane = Lanes.NO_LANE;
spanMargins = null;
}
private boolean hasSpanMargins() {
return (spanMargins != null);
}
private int getSpanMargin(int index) {
if (spanMargins == null) {
return 0;
}
return spanMargins[index];
}
private void setSpanMargin(int index, int margin, int span) {
if (spanMargins == null) {
spanMargins = new int[span];
}
spanMargins[index] = margin;
}
public static final Creator<ItemEntry> CREATOR
= new Creator<ItemEntry>() {
@Override
public ItemEntry createFromParcel(Parcel in) {
return new ItemEntry(in);
}
@Override
public ItemEntry[] newArray(int size) {
return new ItemEntry[size];
}
};
}
private enum UpdateOp {
ADD,
REMOVE,
UPDATE,
MOVE
}
private Lanes mLanes;
private Lanes mLanesToRestore;
private ItemEntries mItemEntries;
private ItemEntries mItemEntriesToRestore;
protected final Rect mChildFrame = new Rect();
protected final Rect mTempRect = new Rect();
protected final LaneInfo mTempLaneInfo = new LaneInfo();
public BaseLayoutManager(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BaseLayoutManager(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public BaseLayoutManager(Orientation orientation) {
super(orientation);
}
protected void pushChildFrame(ItemEntry entry, Rect childFrame, int lane, int laneSpan,
Direction direction) {
final boolean shouldSetMargins = (direction == Direction.END &&
entry != null && !entry.hasSpanMargins());
for (int i = lane; i < lane + laneSpan; i++) {
final int spanMargin;
if (entry != null && direction != Direction.END) {
spanMargin = entry.getSpanMargin(i - lane);
} else {
spanMargin = 0;
}
final int margin = mLanes.pushChildFrame(childFrame, i, spanMargin, direction);
if (laneSpan > 1 && shouldSetMargins) {
entry.setSpanMargin(i - lane, margin, laneSpan);
}
}
}
private void popChildFrame(ItemEntry entry, Rect childFrame, int lane, int laneSpan,
Direction direction) {
for (int i = lane; i < lane + laneSpan; i++) {
final int spanMargin;
if (entry != null && direction != Direction.END) {
spanMargin = entry.getSpanMargin(i - lane);
} else {
spanMargin = 0;
}
mLanes.popChildFrame(childFrame, i, spanMargin, direction);
}
}
void getDecoratedChildFrame(View child, Rect childFrame) {
childFrame.left = getDecoratedLeft(child);
childFrame.top = getDecoratedTop(child);
childFrame.right = getDecoratedRight(child);
childFrame.bottom = getDecoratedBottom(child);
}
boolean isVertical() {
return (getOrientation() == Orientation.VERTICAL);
}
Lanes getLanes() {
return mLanes;
}
void setItemEntryForPosition(int position, ItemEntry entry) {
if (mItemEntries != null) {
mItemEntries.putItemEntry(position, entry);
}
}
ItemEntry getItemEntryForPosition(int position) {
return (mItemEntries != null ? mItemEntries.getItemEntry(position) : null);
}
void clearItemEntries() {
if (mItemEntries != null) {
mItemEntries.clear();
}
}
void invalidateItemLanesAfter(int position) {
if (mItemEntries != null) {
mItemEntries.invalidateItemLanesAfter(position);
}
}
void offsetForAddition(int positionStart, int itemCount) {
if (mItemEntries != null) {
mItemEntries.offsetForAddition(positionStart, itemCount);
}
}
void offsetForRemoval(int positionStart, int itemCount) {
if (mItemEntries != null) {
mItemEntries.offsetForRemoval(positionStart, itemCount);
}
}
private void requestMoveLayout() {
if (getPendingScrollPosition() != RecyclerView.NO_POSITION) {
return;
}
final int position = getFirstVisiblePosition();
final View firstChild = findViewByPosition(position);
final int offset = (firstChild != null ? getChildStart(firstChild) : 0);
setPendingScrollPositionWithOffset(position, offset);
}
private boolean canUseLanes(Lanes lanes) {
if (lanes == null) {
return false;
}
final int laneCount = getLaneCount();
final int laneSize = calculateLaneSize(this, laneCount);
return (lanes.getOrientation() == getOrientation() &&
lanes.getCount() == laneCount &&
lanes.getLaneSize() == laneSize);
}
private boolean ensureLayoutState() {
final int laneCount = getLaneCount();
if (laneCount == 0 || getWidth() == 0 || getHeight() == 0 || canUseLanes(mLanes)) {
return false;
}
final Lanes oldLanes = mLanes;
mLanes = new Lanes(this, laneCount);
requestMoveLayout();
if (mItemEntries == null) {
mItemEntries = new ItemEntries();
}
if (oldLanes != null && oldLanes.getOrientation() == mLanes.getOrientation() &&
oldLanes.getLaneSize() == mLanes.getLaneSize()) {
invalidateItemLanesAfter(0);
} else {
mItemEntries.clear();
}
return true;
}
private void handleUpdate(int positionStart, int itemCountOrToPosition, UpdateOp cmd) {
invalidateItemLanesAfter(positionStart);
switch (cmd) {
case ADD:
offsetForAddition(positionStart, itemCountOrToPosition);
break;
case REMOVE:
offsetForRemoval(positionStart, itemCountOrToPosition);
break;
case MOVE:
offsetForRemoval(positionStart, 1);
offsetForAddition(itemCountOrToPosition, 1);
break;
}
if (positionStart + itemCountOrToPosition <= getFirstVisiblePosition()) {
return;
}
if (positionStart <= getLastVisiblePosition()) {
requestLayout();
}
}
@Override
public void offsetChildrenHorizontal(int offset) {
if (!isVertical()) {
mLanes.offset(offset);
}
super.offsetChildrenHorizontal(offset);
}
@Override
public void offsetChildrenVertical(int offset) {
super.offsetChildrenVertical(offset);
if (isVertical()) {
mLanes.offset(offset);
}
}
@Override
public void onLayoutChildren(Recycler recycler, State state) {
final boolean restoringLanes = (mLanesToRestore != null);
if (restoringLanes) {
mLanes = mLanesToRestore;
mItemEntries = mItemEntriesToRestore;
mLanesToRestore = null;
mItemEntriesToRestore = null;
}
final boolean refreshingLanes = ensureLayoutState();
// Still not able to create lanes, nothing we can do here,
// just bail for now.
if (mLanes == null) {
return;
}
final int itemCount = state.getItemCount();
mItemEntries.setAdapterSize(itemCount);
final int anchorItemPosition = getAnchorItemPosition(state);
// Only move layout if we're not restoring a layout state.
if (anchorItemPosition > 0 && (refreshingLanes || !restoringLanes)) {
moveLayoutToPosition(anchorItemPosition, getPendingScrollOffset(), recycler, state);
}
mLanes.reset(Direction.START);
super.onLayoutChildren(recycler, state);
}
@Override
protected void onLayoutScrapList(Recycler recycler, State state) {
mLanes.save();
super.onLayoutScrapList(recycler, state);
mLanes.restore();
}
@Override
public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
handleUpdate(positionStart, itemCount, UpdateOp.ADD);
super.onItemsAdded(recyclerView, positionStart, itemCount);
}
@Override
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
handleUpdate(positionStart, itemCount, UpdateOp.REMOVE);
super.onItemsRemoved(recyclerView, positionStart, itemCount);
}
@Override
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
handleUpdate(positionStart, itemCount, UpdateOp.UPDATE);
super.onItemsUpdated(recyclerView, positionStart, itemCount);
}
@Override
public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
handleUpdate(from, to, UpdateOp.MOVE);
super.onItemsMoved(recyclerView, from, to, itemCount);
}
@Override
public void onItemsChanged(RecyclerView recyclerView) {
clearItemEntries();
super.onItemsChanged(recyclerView);
}
@Override
public Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
final LanedSavedState state = new LanedSavedState(superState);
final int laneCount = (mLanes != null ? mLanes.getCount() : 0);
state.lanes = new Rect[laneCount];
for (int i = 0; i < laneCount; i++) {
final Rect laneRect = new Rect();
mLanes.getLane(i, laneRect);
state.lanes[i] = laneRect;
}
state.orientation = getOrientation();
state.laneSize = (mLanes != null ? mLanes.getLaneSize() : 0);
state.itemEntries = mItemEntries;
return state;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
final LanedSavedState ss = (LanedSavedState) state;
if (ss.lanes != null && ss.laneSize > 0) {
mLanesToRestore = new Lanes(this, ss.orientation, ss.lanes, ss.laneSize);
mItemEntriesToRestore = ss.itemEntries;
}
super.onRestoreInstanceState(ss.getSuperState());
}
@Override
protected boolean canAddMoreViews(Direction direction, int limit) {
if (direction == Direction.START) {
return (mLanes.getInnerStart() > limit);
} else {
return (mLanes.getInnerEnd() < limit);
}
}
private int getWidthUsed(View child) {
if (!isVertical()) {
return 0;
}
final int size = getLanes().getLaneSize() * getLaneSpanForChild(child);
return getWidth() - getPaddingLeft() - getPaddingRight() - size;
}
private int getHeightUsed(View child) {
if (isVertical()) {
return 0;
}
final int size = getLanes().getLaneSize() * getLaneSpanForChild(child);
return getHeight() - getPaddingTop() - getPaddingBottom() - size;
}
void measureChildWithMargins(View child) {
measureChildWithMargins(child, getWidthUsed(child), getHeightUsed(child));
}
@Override
protected void measureChild(View child, Direction direction) {
cacheChildLaneAndSpan(child, direction);
measureChildWithMargins(child);
}
@Override
protected void layoutChild(View child, Direction direction) {
getLaneForChild(mTempLaneInfo, child, direction);
mLanes.getChildFrame(mChildFrame, getDecoratedMeasuredWidth(child),
getDecoratedMeasuredHeight(child), mTempLaneInfo, direction);
final ItemEntry entry = cacheChildFrame(child, mChildFrame);
layoutDecorated(child, mChildFrame.left, mChildFrame.top, mChildFrame.right,
mChildFrame.bottom);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.isItemRemoved()) {
pushChildFrame(entry, mChildFrame, mTempLaneInfo.startLane,
getLaneSpanForChild(child), direction);
}
}
@Override
protected void detachChild(View child, Direction direction) {
final int position = getPosition(child);
getLaneForPosition(mTempLaneInfo, position, direction);
getDecoratedChildFrame(child, mChildFrame);
popChildFrame(getItemEntryForPosition(position), mChildFrame, mTempLaneInfo.startLane,
getLaneSpanForChild(child), direction);
}
void getLaneForChild(LaneInfo outInfo, View child, Direction direction) {
getLaneForPosition(outInfo, getPosition(child), direction);
}
int getLaneSpanForChild(View child) {
return 1;
}
int getLaneSpanForPosition(int position) {
return 1;
}
ItemEntry cacheChildLaneAndSpan(View child, Direction direction) {
// Do nothing by default.
return null;
}
ItemEntry cacheChildFrame(View child, Rect childFrame) {
// Do nothing by default.
return null;
}
@Override
public boolean checkLayoutParams(LayoutParams lp) {
if (isVertical()) {
return (lp.width == LayoutParams.MATCH_PARENT);
} else {
return (lp.height == LayoutParams.MATCH_PARENT);
}
}
@Override
public LayoutParams generateDefaultLayoutParams() {
if (isVertical()) {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
} else {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
}
}
@Override
public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
final LayoutParams lanedLp = new LayoutParams((MarginLayoutParams) lp);
if (isVertical()) {
lanedLp.width = LayoutParams.MATCH_PARENT;
lanedLp.height = lp.height;
} else {
lanedLp.width = lp.width;
lanedLp.height = LayoutParams.MATCH_PARENT;
}
return lanedLp;
}
@Override
public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
return new LayoutParams(c, attrs);
}
abstract int getLaneCount();
abstract void getLaneForPosition(LaneInfo outInfo, int position, Direction direction);
abstract void moveLayoutToPosition(int position, int offset, Recycler recycler, State state);
protected static class LanedSavedState extends SavedState {
private Orientation orientation;
private Rect[] lanes;
private int laneSize;
private ItemEntries itemEntries;
protected LanedSavedState(Parcelable superState) {
super(superState);
}
private LanedSavedState(Parcel in) {
super(in);
orientation = Orientation.values()[in.readInt()];
laneSize = in.readInt();
final int laneCount = in.readInt();
if (laneCount > 0) {
lanes = new Rect[laneCount];
for (int i = 0; i < laneCount; i++) {
final Rect lane = new Rect();
lane.readFromParcel(in);
lanes[i] = lane;
}
}
final int itemEntriesCount = in.readInt();
if (itemEntriesCount > 0) {
itemEntries = new ItemEntries();
for (int i = 0; i < itemEntriesCount; i++) {
final ItemEntry entry = in.readParcelable(((Object)this).getClass().getClassLoader());
itemEntries.putItemEntry(i, entry);
}
}
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(orientation.ordinal());
out.writeInt(laneSize);
final int laneCount = (lanes != null ? lanes.length : 0);
out.writeInt(laneCount);
for (int i = 0; i < laneCount; i++) {
lanes[i].writeToParcel(out, Rect.PARCELABLE_WRITE_RETURN_VALUE);
}
final int itemEntriesCount = (itemEntries != null ? itemEntries.size() : 0);
out.writeInt(itemEntriesCount);
for (int i = 0; i < itemEntriesCount; i++) {
out.writeParcelable(itemEntries.getItemEntry(i), flags);
}
}
public static final Parcelable.Creator<LanedSavedState> CREATOR
= new Parcelable.Creator<LanedSavedState>() {
@Override
public LanedSavedState createFromParcel(Parcel in) {
return new LanedSavedState(in);
}
@Override
public LanedSavedState[] newArray(int size) {
return new LanedSavedState[size];
}
};
}
}