package com.beloo.widget.chipslayoutmanager;
|
|
import android.content.Context;
|
import android.graphics.Rect;
|
import android.os.Parcelable;
|
import android.util.SparseArray;
|
import android.view.View;
|
|
import androidx.annotation.IntRange;
|
import androidx.annotation.NonNull;
|
import androidx.annotation.Nullable;
|
import androidx.annotation.RestrictTo;
|
import androidx.annotation.VisibleForTesting;
|
import androidx.core.view.ViewCompat;
|
import androidx.recyclerview.widget.RecyclerView;
|
|
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 kuaiyun, 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);
|
}
|
}
|