package com.beloo.widget.chipslayoutmanager;
import android.content.Context;
import android.graphics.Rect;
import android.os.Parcelable;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.annotation.VisibleForTesting;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
import android.util.SparseArray;
import android.view.View;
import com.beloo.widget.chipslayoutmanager.anchor.AnchorViewState;
import com.beloo.widget.chipslayoutmanager.anchor.IAnchorFactory;
import com.beloo.widget.chipslayoutmanager.layouter.ColumnsStateFactory;
import com.beloo.widget.chipslayoutmanager.layouter.ICanvas;
import com.beloo.widget.chipslayoutmanager.layouter.IMeasureSupporter;
import com.beloo.widget.chipslayoutmanager.layouter.IStateFactory;
import com.beloo.widget.chipslayoutmanager.layouter.MeasureSupporter;
import com.beloo.widget.chipslayoutmanager.layouter.RowsStateFactory;
import com.beloo.widget.chipslayoutmanager.layouter.breaker.EmptyRowBreaker;
import com.beloo.widget.chipslayoutmanager.layouter.breaker.IRowBreaker;
import com.beloo.widget.chipslayoutmanager.cache.IViewCacheStorage;
import com.beloo.widget.chipslayoutmanager.cache.ViewCacheFactory;
import com.beloo.widget.chipslayoutmanager.gravity.CenterChildGravity;
import com.beloo.widget.chipslayoutmanager.gravity.CustomGravityResolver;
import com.beloo.widget.chipslayoutmanager.gravity.IChildGravityResolver;
import com.beloo.widget.chipslayoutmanager.layouter.LayouterFactory;
import com.beloo.widget.chipslayoutmanager.layouter.AbstractPositionIterator;
import com.beloo.widget.chipslayoutmanager.layouter.ILayouter;
import com.beloo.widget.chipslayoutmanager.layouter.criteria.AbstractCriteriaFactory;
import com.beloo.widget.chipslayoutmanager.layouter.criteria.ICriteriaFactory;
import com.beloo.widget.chipslayoutmanager.layouter.criteria.InfiniteCriteriaFactory;
import com.beloo.widget.chipslayoutmanager.layouter.placer.PlacerFactory;
import com.beloo.widget.chipslayoutmanager.util.log.IFillLogger;
import com.beloo.widget.chipslayoutmanager.util.log.LoggerFactory;
import com.beloo.widget.chipslayoutmanager.util.AssertionUtils;
import com.beloo.widget.chipslayoutmanager.util.LayoutManagerUtil;
import com.beloo.widget.chipslayoutmanager.util.log.Log;
import com.beloo.widget.chipslayoutmanager.util.log.LogSwitcherFactory;
import com.beloo.widget.chipslayoutmanager.util.testing.EmptySpy;
import com.beloo.widget.chipslayoutmanager.util.testing.ISpy;
import java.util.Locale;
public class ChipsLayoutManager extends RecyclerView.LayoutManager implements IChipsLayoutManagerContract,
IStateHolder,
ScrollingController.IScrollerListener {
///////////////////////////////////////////////////////////////////////////
// orientation types
///////////////////////////////////////////////////////////////////////////
@SuppressWarnings("WeakerAccess")
public static final int HORIZONTAL = 1;
@SuppressWarnings("WeakerAccess")
public static final int VERTICAL = 2;
///////////////////////////////////////////////////////////////////////////
// row strategy types
///////////////////////////////////////////////////////////////////////////
@SuppressWarnings("WeakerAccess")
public static final int STRATEGY_DEFAULT = 1;
@SuppressWarnings("WeakerAccess")
public static final int STRATEGY_FILL_VIEW = 2;
@SuppressWarnings("WeakerAccess")
public static final int STRATEGY_FILL_SPACE = 4;
@SuppressWarnings("WeakerAccess")
public static final int STRATEGY_CENTER = 5;
@SuppressWarnings("WeakerAccess")
public static final int STRATEGY_CENTER_DENSE = 6;
///////////////////////////////////////////////////////////////////////////
// inner constants
///////////////////////////////////////////////////////////////////////////
private static final String TAG = ChipsLayoutManager.class.getSimpleName();
private static final int INT_ROW_SIZE_APPROXIMATELY_FOR_CACHE = 10;
private static final int APPROXIMATE_ADDITIONAL_ROWS_COUNT = 5;
/**
* coefficient to support fast scrolling, caching views only for one row may not be enough
*/
private static final float FAST_SCROLLING_COEFFICIENT = 2;
/** delegate which represents available canvas for drawing views according to layout*/
private ICanvas canvas;
private IDisappearingViewsManager disappearingViewsManager;
/** iterable over views added to RecyclerView */
private ChildViewsIterable childViews = new ChildViewsIterable(this);
private SparseArray<View> childViewPositions = new SparseArray<>();
///////////////////////////////////////////////////////////////////////////
// contract parameters
///////////////////////////////////////////////////////////////////////////
/** determine gravity of child inside row*/
private IChildGravityResolver childGravityResolver;
private boolean isScrollingEnabledContract = true;
/** strict restriction of max count of views in particular row */
private Integer maxViewsInRow = null;
/** determines whether LM should break row from view position */
private IRowBreaker rowBreaker = new EmptyRowBreaker();
//--- end contract parameters
/** layoutOrientation of layout. Could have HORIZONTAL or VERTICAL style */
@Orientation
private int layoutOrientation = HORIZONTAL;
@RowStrategy
private int rowStrategy = STRATEGY_DEFAULT;
private boolean isStrategyAppliedWithLastRow;
/** @see #setSmoothScrollbarEnabled(boolean). True by default */
private boolean isSmoothScrollbarEnabled = false;
///////////////////////////////////////////////////////////////////////////
// cache
///////////////////////////////////////////////////////////////////////////
/** store positions of placed view to know when LM should break row while moving back
* this cache mostly needed to place views when scrolling down to the same places, where they have been previously */
private IViewCacheStorage viewPositionsStorage;
/**
* when scrolling reached this position {@link ChipsLayoutManager} is able to restore items layout according to cached items with positions above.
* That layout would exactly correspond to current item view situation
*/
@Nullable
private Integer cacheNormalizationPosition = null;
/**
* store detached views to probably reattach it if them still visible.
* Used while scrolling
*/
private SparseArray<View> viewCache = new SparseArray<>();
/**
* storing state due layoutOrientation changes
*/
private ParcelableContainer container = new ParcelableContainer();
///////////////////////////////////////////////////////////////////////////
// loggers
///////////////////////////////////////////////////////////////////////////
private IFillLogger logger;
//--- end loggers
/**
* is layout in RTL mode. Variable needed to detect mode changes
*/
private boolean isLayoutRTL = false;
/**
* current device layoutOrientation
*/
@DeviceOrientation
private int orientation;
///////////////////////////////////////////////////////////////////////////
// borders
///////////////////////////////////////////////////////////////////////////
/**
* stored current anchor view due to scroll state changes
*/
private AnchorViewState anchorView;
///////////////////////////////////////////////////////////////////////////
// state-dependent
///////////////////////////////////////////////////////////////////////////
/** factory for state-dependent layouter factories*/
private IStateFactory stateFactory;
/** manage auto-measuring */
private IMeasureSupporter measureSupporter;
/** factory which could retrieve anchorView on which layouting based*/
private IAnchorFactory anchorFactory;
/** manage scrolling of layout manager according to current state */
private IScrollingController scrollingController;
//--- end state-dependent vars
/** factory for placers factories*/
private PlacerFactory placerFactory = new PlacerFactory(this);
/** used for testing purposes to spy for {@link ChipsLayoutManager} behaviour */
private ISpy spy = new EmptySpy();
private boolean isAfterPreLayout;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
ChipsLayoutManager(Context context) {
@DeviceOrientation
int orientation = context.getResources().getConfiguration().orientation;
this.orientation = orientation;
LoggerFactory loggerFactory = new LoggerFactory();
logger = loggerFactory.getFillLogger(viewCache);
viewPositionsStorage = new ViewCacheFactory(this).createCacheStorage();
measureSupporter = new MeasureSupporter(this);
setAutoMeasureEnabled(true);
}
///////////////////////////////////////////////////////////////////////////
// ChipsLayoutManager contract methods
///////////////////////////////////////////////////////////////////////////
public static Builder newBuilder(Context context) {
if (context == null) throw new IllegalArgumentException("you have passed null context to builder");
return new ChipsLayoutManager(context).new StrategyBuilder();
}
public IChildGravityResolver getChildGravityResolver() {
return childGravityResolver;
}
/** use it to strictly disable scrolling.
* If scrolling enabled it would be disabled in case all items fit on the screen */
@Override
public void setScrollingEnabledContract(boolean isEnabled) {
isScrollingEnabledContract = isEnabled;
}
@Override
public boolean isScrollingEnabledContract() {
return isScrollingEnabledContract;
}
/**
* change max count of row views in runtime
*/
@SuppressWarnings("unused")
public void setMaxViewsInRow(@IntRange(from = 1) Integer maxViewsInRow) {
if (maxViewsInRow < 1)
throw new IllegalArgumentException("maxViewsInRow should be positive, but is = " + maxViewsInRow);
this.maxViewsInRow = maxViewsInRow;
onRuntimeLayoutChanges();
}
private void onRuntimeLayoutChanges() {
cacheNormalizationPosition = 0;
viewPositionsStorage.purge();
requestLayoutWithAnimations();
}
@Override
public Integer getMaxViewsInRow() {
return maxViewsInRow;
}
@Override
public IRowBreaker getRowBreaker() {
return rowBreaker;
}
@Override
@RowStrategy
public int getRowStrategyType() {
return rowStrategy;
}
///////////////////////////////////////////////////////////////////////////
// non-contract public methods. Used only for inner purposes
///////////////////////////////////////////////////////////////////////////
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public boolean isStrategyAppliedWithLastRow() {
return isStrategyAppliedWithLastRow;
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public IViewCacheStorage getViewPositionsStorage() {
return viewPositionsStorage;
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public ICanvas getCanvas() {
return canvas;
}
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
AnchorViewState getAnchor() {
return anchorView;
}
@VisibleForTesting
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
void setSpy(ISpy spy) {
this.spy = spy;
}
///////////////////////////////////////////////////////////////////////////
// builder
///////////////////////////////////////////////////////////////////////////
//create decorator if any other builders would be added
@SuppressWarnings("WeakerAccess")
public class StrategyBuilder extends Builder {
/** @param withLastRow true, if row strategy should be applied to last row.
* @see Builder#setRowStrategy(int) */
@SuppressWarnings("unused")
public Builder withLastRow(boolean withLastRow) {
ChipsLayoutManager.this.isStrategyAppliedWithLastRow = withLastRow;
return this;
}
}
@SuppressWarnings("WeakerAccess")
public class Builder {
@SpanLayoutChildGravity
private Integer gravity;
private Builder() {
}
/**
* set vertical gravity in a row for all children. Default = CENTER_VERTICAL
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public Builder setChildGravity(@SpanLayoutChildGravity int gravity) {
this.gravity = gravity;
return this;
}
/**
* set gravity resolver in case you need special gravity for items. This method have priority over {@link #setChildGravity(int)}
*/
@SuppressWarnings("unused")
public Builder setGravityResolver(@NonNull IChildGravityResolver gravityResolver) {
AssertionUtils.assertNotNull(gravityResolver, "gravity resolver couldn't be null");
childGravityResolver = gravityResolver;
return this;
}
/**
* strictly disable scrolling if needed
*/
@SuppressWarnings("unused")
public Builder setScrollingEnabled(boolean isEnabled) {
ChipsLayoutManager.this.setScrollingEnabledContract(isEnabled);
return this;
}
/** row strategy for views in completed row.
* Any row has some space left, where is impossible to place the next view, because that space is too small.
* But we could distribute that space for available views in that row
* @param rowStrategy is a mode of distribution left space<br/>
* {@link #STRATEGY_DEFAULT} is used by default. Left space is placed at the end of the row.<br/>
* {@link #STRATEGY_FILL_VIEW} available space is distributed among views<br/>
* {@link #STRATEGY_FILL_SPACE} available space is distributed among spaces between views, start & end views are docked to a nearest border<br/>
* {@link #STRATEGY_CENTER} available space is distributed among spaces between views, start & end spaces included. Views are placed in center of canvas<br/>
* {@link #STRATEGY_CENTER_DENSE} available space is distributed among start & end spaces. Views are placed in center of canvas<br/>
* <br/>
* In such layouts by default last row isn't considered completed. So strategy isn't applied for last row.<br/>
* But you can also enable opposite behaviour.
* @see StrategyBuilder#withLastRow(boolean)
*/
@SuppressWarnings("unused")
public StrategyBuilder setRowStrategy(@RowStrategy int rowStrategy) {
ChipsLayoutManager.this.rowStrategy = rowStrategy;
return (StrategyBuilder) this;
}
/**
* set maximum possible count of views in row
*/
@SuppressWarnings("unused")
public Builder setMaxViewsInRow(@IntRange(from = 1) int maxViewsInRow) {
if (maxViewsInRow < 1)
throw new IllegalArgumentException("maxViewsInRow should be positive, but is = " + maxViewsInRow);
ChipsLayoutManager.this.maxViewsInRow = maxViewsInRow;
return this;
}
/** @param breaker override to determine whether ChipsLayoutManager should breaks row due to position of view. */
@SuppressWarnings("unused")
public Builder setRowBreaker(@NonNull IRowBreaker breaker) {
AssertionUtils.assertNotNull(breaker, "breaker couldn't be null");
ChipsLayoutManager.this.rowBreaker = breaker;
return this;
}
/** @param orientation of layout manager. Could be {@link #HORIZONTAL} or {@link #VERTICAL}
* {@link #HORIZONTAL} by default */
public Builder setOrientation(@Orientation int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
return this;
}
ChipsLayoutManager.this.layoutOrientation = orientation;
return this;
}
/**
* create SpanLayoutManager
*/
public ChipsLayoutManager build() {
// setGravityResolver always have priority
if (childGravityResolver == null) {
if (gravity != null) {
childGravityResolver = new CustomGravityResolver(gravity);
} else {
childGravityResolver = new CenterChildGravity();
}
}
stateFactory = layoutOrientation == HORIZONTAL ? new RowsStateFactory(ChipsLayoutManager.this) : new ColumnsStateFactory(ChipsLayoutManager.this);
canvas = stateFactory.createCanvas();
anchorFactory = stateFactory.anchorFactory();
scrollingController = stateFactory.scrollingController();
anchorView = anchorFactory.createNotFound();
disappearingViewsManager = new DisappearingViewsManager(canvas, childViews, stateFactory);
return ChipsLayoutManager.this;
}
}
/**
* {@inheritDoc}
*/
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(
RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT);
}
private void requestLayoutWithAnimations() {
LayoutManagerUtil.requestLayoutWithAnimations(this);
}
///////////////////////////////////////////////////////////////////////////
// instance state
///////////////////////////////////////////////////////////////////////////
/**
* {@inheritDoc}
*/
@Override
public void onRestoreInstanceState(Parcelable state) {
container = (ParcelableContainer) state;
anchorView = container.getAnchorViewState();
if (orientation != container.getOrientation()) {
//orientation have been changed, clear anchor rect
int anchorPos = anchorView.getPosition();
anchorView = anchorFactory.createNotFound();
anchorView.setPosition(anchorPos);
}
viewPositionsStorage.onRestoreInstanceState(container.getPositionsCache(orientation));
cacheNormalizationPosition = container.getNormalizationPosition(orientation);
Log.d(TAG, "RESTORE. last cache position before cleanup = " + viewPositionsStorage.getLastCachePosition());
if (cacheNormalizationPosition != null) {
viewPositionsStorage.purgeCacheFromPosition(cacheNormalizationPosition);
}
viewPositionsStorage.purgeCacheFromPosition(anchorView.getPosition());
Log.d(TAG, "RESTORE. anchor position =" + anchorView.getPosition());
Log.d(TAG, "RESTORE. layoutOrientation = " + orientation + " normalizationPos = " + cacheNormalizationPosition);
Log.d(TAG, "RESTORE. last cache position = " + viewPositionsStorage.getLastCachePosition());
}
/**
* {@inheritDoc}
*/
@Override
public Parcelable onSaveInstanceState() {
container.putAnchorViewState(anchorView);
container.putPositionsCache(orientation, viewPositionsStorage.onSaveInstanceState());
container.putOrientation(orientation);
Log.d(TAG, "STORE. last cache position =" + viewPositionsStorage.getLastCachePosition());
Integer storedNormalizationPosition = cacheNormalizationPosition != null ? cacheNormalizationPosition : viewPositionsStorage.getLastCachePosition();
Log.d(TAG, "STORE. layoutOrientation = " + orientation + " normalizationPos = " + storedNormalizationPosition);
container.putNormalizationPosition(orientation, storedNormalizationPosition);
return container;
}
/**
* {@inheritDoc}
*/
@Override
public boolean supportsPredictiveItemAnimations() {
return true;
}
///////////////////////////////////////////////////////////////////////////
// visible items
///////////////////////////////////////////////////////////////////////////
/** returns count of completely visible views
* @see #findFirstCompletelyVisibleItemPosition() ()
* @see #findLastCompletelyVisibleItemPosition() */
@SuppressWarnings("WeakerAccess")
public int getCompletelyVisibleViewsCount() {
int visibleViewsCount = 0;
for (View child : childViews) {
if (canvas.isFullyVisible(child)){
visibleViewsCount++;
}
}
return visibleViewsCount;
}
///////////////////////////////////////////////////////////////////////////
// positions contract
///////////////////////////////////////////////////////////////////////////
/**
* Returns the adapter position of the first visible view. This position does not include
* adapter changes that were dispatched after the last layout pass.
* If RecyclerView has item decorators, they will be considered in calculations as well.
* <p>
* LayoutManager may pre-cache some views that are not necessarily visible. Those views
* are ignored in this method.
*
* @return The adapter position of the first visible item or {@link RecyclerView#NO_POSITION} if
* there aren't any visible items.
* @see #findFirstCompletelyVisibleItemPosition()
* @see #findLastVisibleItemPosition()
*/
@Override
public int findFirstVisibleItemPosition() {
if (getChildCount() == 0)
return RecyclerView.NO_POSITION;
return canvas.getMinPositionOnScreen();
}
/**
* Returns the adapter position of the first fully visible view. This position does not include
* adapter changes that were dispatched after the last layout pass.
*
* @return The adapter position of the first fully visible item or
* {@link RecyclerView#NO_POSITION} if there aren't any visible items.
* @see #findFirstVisibleItemPosition()
* @see #findLastCompletelyVisibleItemPosition()
*/
@Override
public int findFirstCompletelyVisibleItemPosition() {
for (View view : childViews) {
Rect rect = canvas.getViewRect(view);
if (!canvas.isFullyVisible(rect)) continue;
if (canvas.isInside(rect)) {
return getPosition(view);
}
}
return RecyclerView.NO_POSITION;
}
/**
* Returns the adapter position of the last visible view. This position does not include
* adapter changes that were dispatched after the last layout pass.
* If RecyclerView has item decorators, they will be considered in calculations as well.
* <p>
* LayoutManager may pre-cache some views that are not necessarily visible. Those views
* are ignored in this method.
*
* @return The adapter position of the last visible view or {@link RecyclerView#NO_POSITION} if
* there aren't any visible items.
* @see #findLastCompletelyVisibleItemPosition()
* @see #findFirstVisibleItemPosition()
*/
@Override
public int findLastVisibleItemPosition() {
if (getChildCount() == 0)
return RecyclerView.NO_POSITION;
return canvas.getMaxPositionOnScreen();
}
/**
* Returns the adapter position of the last fully visible view. This position does not include
* adapter changes that were dispatched after the last layout pass.
*
* @return The adapter position of the last fully visible view or
* {@link RecyclerView#NO_POSITION} if there aren't any visible items.
* @see #findLastVisibleItemPosition()
* @see #findFirstCompletelyVisibleItemPosition()
*/
@Override
public int findLastCompletelyVisibleItemPosition() {
for (int i = getChildCount() - 1; i >=0; i--) {
View view = getChildAt(i);
Rect rect = canvas.getViewRect(view);
if (!canvas.isFullyVisible(rect)) continue;
if (canvas.isInside(view)) {
return getPosition(view);
}
}
return RecyclerView.NO_POSITION;
}
/** @return child for requested position. Null if that child haven't added to layout manager*/
@Nullable
View getChildWithPosition(int position) {
return childViewPositions.get(position);
}
///////////////////////////////////////////////////////////////////////////
// orientation
///////////////////////////////////////////////////////////////////////////
/**
* @return true if RTL mode enabled in RecyclerView
*/
public boolean isLayoutRTL() {
return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
}
@Override
@Orientation
public int layoutOrientation() {
return layoutOrientation;
}
///////////////////////////////////////////////////////////////////////////
// layouting
///////////////////////////////////////////////////////////////////////////
/**
* {@inheritDoc}
*/
@Override
public int getItemCount() {
//in pre-layouter drawing we need item count with items will be actually deleted to pre-draw appearing items properly
return super.getItemCount() + disappearingViewsManager.getDeletingItemsOnScreenCount();
}
/**
* {@inheritDoc}
*/
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
spy.onLayoutChildren(recycler, state);
Log.d(TAG, "onLayoutChildren. State =" + state);
//We have nothing to show for an empty data set but clear any existing views
if (getItemCount() == 0) {
detachAndScrapAttachedViews(recycler);
return;
}
Log.i("onLayoutChildren", "isPreLayout = " + state.isPreLayout(), LogSwitcherFactory.PREDICTIVE_ANIMATIONS);
if (isLayoutRTL() != isLayoutRTL) {
//if layout direction changed programmatically we should clear anchors
isLayoutRTL = isLayoutRTL();
//so detach all views before we start searching for anchor view
detachAndScrapAttachedViews(recycler);
}
calcRecyclerCacheSize(recycler);
if (state.isPreLayout()) {
//inside pre-layout stage. It is called when item animation reconstruction will be played
//it is NOT called on layoutOrientation changes
int additionalLength = disappearingViewsManager.calcDisappearingViewsLength(recycler);
Log.d("LayoutManager", "height =" + getHeight(), LogSwitcherFactory.PREDICTIVE_ANIMATIONS);
Log.d("onDeletingHeightCalc", "additional height = " + additionalLength, LogSwitcherFactory.PREDICTIVE_ANIMATIONS);
anchorView = anchorFactory.getAnchor();
anchorFactory.resetRowCoordinates(anchorView);
Log.w(TAG, "anchor state in pre-layout = " + anchorView);
detachAndScrapAttachedViews(recycler);
//in case removing draw additional rows to show predictive animations for appearing views
AbstractCriteriaFactory criteriaFactory = stateFactory.createDefaultFinishingCriteriaFactory();
criteriaFactory.setAdditionalRowsCount(APPROXIMATE_ADDITIONAL_ROWS_COUNT);
criteriaFactory.setAdditionalLength(additionalLength);
LayouterFactory layouterFactory = stateFactory.createLayouterFactory(criteriaFactory, placerFactory.createRealPlacerFactory());
logger.onBeforeLayouter(anchorView);
fill(recycler,
layouterFactory.getBackwardLayouter(anchorView),
layouterFactory.getForwardLayouter(anchorView));
isAfterPreLayout = true;
} else {
detachAndScrapAttachedViews(recycler);
//we perform layouting stage from scratch, so cache will be rebuilt soon, we could purge it and avoid unnecessary normalization
viewPositionsStorage.purgeCacheFromPosition(anchorView.getPosition());
if (cacheNormalizationPosition != null && anchorView.getPosition() <= cacheNormalizationPosition) {
cacheNormalizationPosition = null;
}
/* In case some moving views
* we should place it at layout to support predictive animations
* we can't place all possible moves on theirs real place, because concrete layout position of particular view depends on placing of previous views
* and there could be moving from 0 position to 10k. But it is preferably to place nearest moved view to real positions to make moving more natural
* like moving from 0 position to 15 for example, where user could scroll fast and check
* so we fill additional rows to cover nearest moves
*/
AbstractCriteriaFactory criteriaFactory = stateFactory.createDefaultFinishingCriteriaFactory();
criteriaFactory.setAdditionalRowsCount(APPROXIMATE_ADDITIONAL_ROWS_COUNT);
LayouterFactory layouterFactory = stateFactory.createLayouterFactory(criteriaFactory, placerFactory.createRealPlacerFactory());
ILayouter backwardLayouter = layouterFactory.getBackwardLayouter(anchorView);
ILayouter forwardLayouter = layouterFactory.getForwardLayouter(anchorView);
fill(recycler, backwardLayouter, forwardLayouter);
/* should be executed before {@link #layoutDisappearingViews} */
if (scrollingController.normalizeGaps(recycler, null)) {
Log.d(TAG, "normalize gaps");
//we should re-layout with new anchor after normalizing gaps
anchorView = anchorFactory.getAnchor();
requestLayoutWithAnimations();
}
if (isAfterPreLayout) {
//we should layout disappearing views after pre-layout to support natural movements)
layoutDisappearingViews(recycler, backwardLayouter, forwardLayouter);
}
isAfterPreLayout = false;
}
disappearingViewsManager.reset();
if (!state.isMeasuring()) {
measureSupporter.onSizeChanged();
}
}
@Override
public void detachAndScrapAttachedViews(RecyclerView.Recycler recycler) {
super.detachAndScrapAttachedViews(recycler);
childViewPositions.clear();
}
/** layout disappearing view to support predictive animations */
private void layoutDisappearingViews(RecyclerView.Recycler recycler, @NonNull ILayouter upLayouter, ILayouter downLayouter) {
ICriteriaFactory criteriaFactory = new InfiniteCriteriaFactory();
LayouterFactory layouterFactory = stateFactory.createLayouterFactory(criteriaFactory, placerFactory.createDisappearingPlacerFactory());
DisappearingViewsManager.DisappearingViewsContainer disappearingViews = disappearingViewsManager.getDisappearingViews(recycler);
if (disappearingViews.size() > 0) {
Log.d("disappearing views", "count = " + disappearingViews.size());
Log.d("fill disappearing views", "");
downLayouter = layouterFactory.buildForwardLayouter(downLayouter);
//we should layout disappearing views left somewhere, just continue layout them in current layouter
for (int i = 0; i< disappearingViews.getForwardViews().size(); i++) {
int position = disappearingViews.getForwardViews().keyAt(i);
downLayouter.placeView(recycler.getViewForPosition(position));
}
//layout last row
downLayouter.layoutRow();
upLayouter = layouterFactory.buildBackwardLayouter(upLayouter);
//we should layout disappearing views left somewhere, just continue layout them in current layouter
for (int i = 0; i< disappearingViews.getBackwardViews().size(); i++) {
int position = disappearingViews.getBackwardViews().keyAt(i);
upLayouter.placeView(recycler.getViewForPosition(position));
}
//layout last row
upLayouter.layoutRow();
}
}
/**
* place all added views to cache (in case scrolling)...
*/
private void fillCache() {
for (int i = 0, cnt = getChildCount(); i < cnt; i++) {
View view = getChildAt(i);
int pos = getPosition(view);
viewCache.put(pos, view);
}
}
/**
* place all views on theirs right places according to current state
*/
private void fill(RecyclerView.Recycler recycler, ILayouter backwardLayouter, ILayouter forwardLayouter) {
int startingPos = anchorView.getPosition();
fillCache();
//... and remove from layout
for (int i = 0; i < viewCache.size(); i++) {
detachView(viewCache.valueAt(i));
}
logger.onStartLayouter(startingPos - 1);
/* there is no sense to perform backward layouting when anchor is null.
null anchor means that layout will be performed from absolutely top corner with start at anchor position
*/
if (anchorView.getAnchorViewRect() != null) {
//up layouter should be invoked earlier than down layouter, because views with lower positions positioned above anchorView
//start from anchor position
fillWithLayouter(recycler, backwardLayouter, startingPos - 1);
}
logger.onStartLayouter(startingPos);
//start from anchor position
fillWithLayouter(recycler, forwardLayouter, startingPos);
logger.onAfterLayouter();
//move to trash everything, which haven't used in this layout cycle
//that views gone from a screen or was removed outside from adapter
for (int i = 0; i < viewCache.size(); i++) {
removeAndRecycleView(viewCache.valueAt(i), recycler);
logger.onRemovedAndRecycled(i);
}
canvas.findBorderViews();
buildChildWithPositionsMap();
viewCache.clear();
logger.onAfterRemovingViews();
}
private void buildChildWithPositionsMap() {
childViewPositions.clear();
for (View view : childViews) {
int position = getPosition(view);
childViewPositions.put(position, view);
}
}
/**
* place views in layout started from chosen position with chosen layouter
*/
private void fillWithLayouter(RecyclerView.Recycler recycler, ILayouter layouter, int startingPos) {
if (startingPos < 0) return;
AbstractPositionIterator iterator = layouter.positionIterator();
iterator.move(startingPos);
while (iterator.hasNext()) {
int pos = iterator.next();
View view = viewCache.get(pos);
if (view == null) { // we don't have view from previous layouter stage, request new one
try {
view = recycler.getViewForPosition(pos);
} catch (IndexOutOfBoundsException e) {
/* WTF sometimes on prediction animation playing in case very fast sequential changes in adapter
* {@link #getItemCount} could return value bigger than real count of items
* & {@link RecyclerView.Recycler#getViewForPosition(int)} throws exception in this case!
* to handle it, just leave the loop*/
break;
}
logger.onItemRequested();
if (!layouter.placeView(view)) {
/* reached end of visible bounds, exit.
recycle view, which was requested previously
*/
recycler.recycleView(view);
logger.onItemRecycled();
break;
}
} else { //we have detached views from previous layouter stage, attach it if needed
if (!layouter.onAttachView(view)) {
break;
}
//remove reattached view from cache
viewCache.remove(pos);
}
}
logger.onFinishedLayouter();
//layout last row, in case iterator fully processed
layouter.layoutRow();
}
/**
* recycler should contain all recycled views from a longest row, not just 2 holders by default
*/
private void calcRecyclerCacheSize(RecyclerView.Recycler recycler) {
int viewsInRow = maxViewsInRow == null ? INT_ROW_SIZE_APPROXIMATELY_FOR_CACHE : maxViewsInRow;
recycler.setViewCacheSize((int) (viewsInRow * FAST_SCROLLING_COEFFICIENT));
}
/**
* after several layout changes our item views probably haven't placed on right places,
* because we don't memorize whole positions of items.
* So them should be normalized to real positions when we can do it.
*/
private void performNormalizationIfNeeded() {
if (cacheNormalizationPosition != null && getChildCount() > 0) {
final View firstView = getChildAt(0);
int firstViewPosition = getPosition(firstView);
if (firstViewPosition < cacheNormalizationPosition ||
(cacheNormalizationPosition == 0 && cacheNormalizationPosition == firstViewPosition)) {
//perform normalization when we have reached previous position then normalization position
Log.d("normalization", "position = " + cacheNormalizationPosition + " top view position = " + firstViewPosition);
Log.d(TAG, "cache purged from position " + firstViewPosition);
viewPositionsStorage.purgeCacheFromPosition(firstViewPosition);
//reset normalization position
cacheNormalizationPosition = null;
requestLayoutWithAnimations();
}
}
}
///////////////////////////////////////////////////////////////////////////
// measure
///////////////////////////////////////////////////////////////////////////
/**
* {@inheritDoc}
*/
@Override
public void setMeasuredDimension(int widthSize, int heightSize) {
measureSupporter.measure(widthSize, heightSize);
Log.i(TAG, "measured dimension = " + heightSize);
super.setMeasuredDimension(measureSupporter.getMeasuredWidth(), measureSupporter.getMeasuredHeight());
}
///////////////////////////////////////////////////////////////////////////
// data set changed events
///////////////////////////////////////////////////////////////////////////
/**
* {@inheritDoc}
*/
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
RecyclerView.Adapter newAdapter) {
if (oldAdapter != null && measureSupporter.isRegistered()) {
try {
measureSupporter.setRegistered(false);
oldAdapter.unregisterAdapterDataObserver((RecyclerView.AdapterDataObserver) measureSupporter);
} catch (IllegalStateException e) {
//skip unregister errors
}
}
if (newAdapter != null) {
measureSupporter.setRegistered(true);
newAdapter.registerAdapterDataObserver((RecyclerView.AdapterDataObserver) measureSupporter);
}
//Completely scrap the existing layout
removeAllViews();
}
/**
* {@inheritDoc}
*/
@Override
public void onItemsRemoved(final RecyclerView recyclerView, int positionStart, int itemCount) {
Log.d("onItemsRemoved", "starts from = " + positionStart + ", item count = " + itemCount, LogSwitcherFactory.ADAPTER_ACTIONS);
super.onItemsRemoved(recyclerView, positionStart, itemCount);
onLayoutUpdatedFromPosition(positionStart);
measureSupporter.onItemsRemoved(recyclerView);
}
/**
* {@inheritDoc}
*/
@Override
public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
Log.d("onItemsAdded", "starts from = " + positionStart + ", item count = " + itemCount, LogSwitcherFactory.ADAPTER_ACTIONS);
super.onItemsAdded(recyclerView, positionStart, itemCount);
onLayoutUpdatedFromPosition(positionStart);
}
/**
* {@inheritDoc}
*/
@Override
public void onItemsChanged(RecyclerView recyclerView) {
Log.d("onItemsChanged", "", LogSwitcherFactory.ADAPTER_ACTIONS);
super.onItemsChanged(recyclerView);
viewPositionsStorage.purge();
onLayoutUpdatedFromPosition(0);
}
/**
* {@inheritDoc}
*/
@Override
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
Log.d("onItemsUpdated", "starts from = " + positionStart + ", item count = " + itemCount, LogSwitcherFactory.ADAPTER_ACTIONS);
super.onItemsUpdated(recyclerView, positionStart, itemCount);
onLayoutUpdatedFromPosition(positionStart);
}
/**
* {@inheritDoc}
*/
@Override
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, Object payload) {
onItemsUpdated(recyclerView, positionStart, itemCount);
}
/**
* {@inheritDoc}
*/
@Override
public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
Log.d("onItemsMoved", String.format(Locale.US, "from = %d, to = %d, itemCount = %d", from, to, itemCount), LogSwitcherFactory.ADAPTER_ACTIONS);
super.onItemsMoved(recyclerView, from, to, itemCount);
onLayoutUpdatedFromPosition(Math.min(from, to));
}
/** update cache according to data changes */
private void onLayoutUpdatedFromPosition(int position) {
Log.d(TAG, "cache purged from position " + position);
viewPositionsStorage.purgeCacheFromPosition(position);
int startRowPos = viewPositionsStorage.getStartOfRow(position);
cacheNormalizationPosition = cacheNormalizationPosition == null ?
startRowPos : Math.min(cacheNormalizationPosition, startRowPos);
}
///////////////////////////////////////////////////////////////////////////
// Scrolling
///////////////////////////////////////////////////////////////////////////
/**
* When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed
* based on the number of visible pixels in the visible items. This however assumes that all
* list items have similar or equal widths or heights (depending on list orientation).
*
* Also this is {@link ChipsLayoutManager} specific issue, that we can't predict exact count of items on screen
* in general case, because we can't predict items count in row.
* So to enable it you should accomplish one of those conditions:
* <ul>
* <li> Your items have same width and height </li>
* <li> You have {@link ChipsLayoutManager#setMaxViewsInRow(Integer)} set and you able to make sure, that there won't be many rows with lower items count.
* The best is none. </li>
* </ul>
*
* If you use a list in which items have different dimensions, the scrollbar will change
* appearance as the user scrolls through the list. To avoid this issue, you need to disable
* this property.
*
* When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based
* solely on the number of items in the adapter and the position of the visible items inside
* the adapter. This provides a stable scrollbar as the user navigates through a list of items
* with varying widths / heights.
*
* @param enabled Whether or not to enable smooth scrollbar.
*
* @see #isSmoothScrollbarEnabled()
*/
@Override
public void setSmoothScrollbarEnabled(boolean enabled) {
isSmoothScrollbarEnabled = enabled;
}
/**
* Returns the current state of the smooth scrollbar feature. It is NOT enabled by default.
*
* @return True if smooth scrollbar is enabled, false otherwise.
*
* @see #setSmoothScrollbarEnabled(boolean)
*/
@Override
public boolean isSmoothScrollbarEnabled() {
return isSmoothScrollbarEnabled;
}
/**
* {@inheritDoc}
*/
public void scrollToPosition(int position) {
if (position >= getItemCount() || position < 0) {
Log.e("span layout manager", "Cannot scroll to " + position + ", item count " + getItemCount());
return;
}
Integer lastCachePosition = viewPositionsStorage.getLastCachePosition();
cacheNormalizationPosition = cacheNormalizationPosition != null ? cacheNormalizationPosition : lastCachePosition;
if (lastCachePosition != null && position < lastCachePosition) {
position = viewPositionsStorage.getStartOfRow(position);
}
anchorView = anchorFactory.createNotFound();
anchorView.setPosition(position);
//Trigger a new view layout
super.requestLayout();
}
/**
* {@inheritDoc}
*/
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, final int position) {
if (position >= getItemCount() || position < 0) {
Log.e("span layout manager", "Cannot scroll to " + position + ", item count " + getItemCount());
return;
}
RecyclerView.SmoothScroller scroller = scrollingController.createSmoothScroller(recyclerView.getContext(), position, 150, anchorView);
scroller.setTargetPosition(position);
startSmoothScroll(scroller);
}
@Override
public boolean canScrollHorizontally() {
return scrollingController.canScrollHorizontally();
}
@Override
public boolean canScrollVertically() {
return scrollingController.canScrollVertically();
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
return scrollingController.scrollVerticallyBy(dy, recycler, state);
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
return scrollingController.scrollHorizontallyBy(dx, recycler, state);
}
public VerticalScrollingController verticalScrollingController() {
return new VerticalScrollingController(this, stateFactory, this);
}
public HorizontalScrollingController horizontalScrollingController() {
return new HorizontalScrollingController(this, stateFactory, this);
}
@Override
public void onScrolled(IScrollingController scrollingController, RecyclerView.Recycler recycler, RecyclerView.State state) {
performNormalizationIfNeeded();
anchorView = anchorFactory.getAnchor();
AbstractCriteriaFactory criteriaFactory = stateFactory.createDefaultFinishingCriteriaFactory();
criteriaFactory.setAdditionalRowsCount(1);
LayouterFactory factory = stateFactory.createLayouterFactory(criteriaFactory, placerFactory.createRealPlacerFactory());
fill(recycler,
factory.getBackwardLayouter(anchorView),
factory.getForwardLayouter(anchorView));
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Override
public int computeVerticalScrollOffset(RecyclerView.State state) {
return scrollingController.computeVerticalScrollOffset(state);
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Override
public int computeVerticalScrollExtent(RecyclerView.State state) {
return scrollingController.computeVerticalScrollExtent(state);
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Override
public int computeVerticalScrollRange(RecyclerView.State state) {
return scrollingController.computeVerticalScrollRange(state);
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Override
public int computeHorizontalScrollExtent(RecyclerView.State state) {
return scrollingController.computeHorizontalScrollExtent(state);
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Override
public int computeHorizontalScrollOffset(RecyclerView.State state) {
return scrollingController.computeHorizontalScrollOffset(state);
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Override
public int computeHorizontalScrollRange(RecyclerView.State state) {
return scrollingController.computeHorizontalScrollRange(state);
}
}