package com.beloo.widget.chipslayoutmanager.layouter; import android.graphics.Rect; import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Pair; import android.view.View; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import com.beloo.widget.chipslayoutmanager.ChipsLayoutManager; import com.beloo.widget.chipslayoutmanager.IBorder; import com.beloo.widget.chipslayoutmanager.SpanLayoutChildGravity; import com.beloo.widget.chipslayoutmanager.gravity.IGravityModifiersFactory; import com.beloo.widget.chipslayoutmanager.gravity.IRowStrategy; import com.beloo.widget.chipslayoutmanager.layouter.breaker.ILayoutRowBreaker; import com.beloo.widget.chipslayoutmanager.cache.IViewCacheStorage; import com.beloo.widget.chipslayoutmanager.gravity.IChildGravityResolver; import com.beloo.widget.chipslayoutmanager.gravity.IGravityModifier; import com.beloo.widget.chipslayoutmanager.layouter.criteria.IFinishingCriteria; import com.beloo.widget.chipslayoutmanager.layouter.placer.IPlacer; import com.beloo.widget.chipslayoutmanager.util.AssertionUtils; /** this class performs measuring, calculation, and placing of views on border (layout manager) according to state criterias */ public abstract class AbstractLayouter implements ILayouter, IBorder { private int currentViewWidth; private int currentViewHeight; private int currentViewPosition; List<Pair<Rect, View>> rowViews = new LinkedList<>(); /** bottom of current row*/ int viewBottom; /** top of current row*/ int viewTop; /** right offset */ int viewRight; /** left offset*/ int viewLeft; private int rowSize = 0; private int previousRowSize; /** is row completed when {@link #layoutRow()} called*/ private boolean isRowCompleted; /////////////////////////////////////////////////////////////////////////// // input dependencies /////////////////////////////////////////////////////////////////////////// @NonNull private ChipsLayoutManager layoutManager; @NonNull private IViewCacheStorage cacheStorage; @NonNull private IBorder border; @NonNull private IChildGravityResolver childGravityResolver; @NonNull private IFinishingCriteria finishingCriteria; @NonNull private IPlacer placer; @NonNull private ILayoutRowBreaker breaker; @NonNull private IRowStrategy rowStrategy; private Set<ILayouterListener> layouterListeners = new HashSet<>(); @NonNull private IGravityModifiersFactory gravityModifiersFactory; @NonNull private AbstractPositionIterator positionIterator; //--- end input dependencies AbstractLayouter(Builder builder) { //--- read builder layoutManager = builder.layoutManager; cacheStorage = builder.cacheStorage; border = builder.border; childGravityResolver = builder.childGravityResolver; this.finishingCriteria = builder.finishingCriteria; placer = builder.placer; this.viewTop = builder.offsetRect.top; this.viewBottom = builder.offsetRect.bottom; this.viewRight = builder.offsetRect.right; this.viewLeft = builder.offsetRect.left; this.layouterListeners = builder.layouterListeners; this.breaker = builder.breaker; this.gravityModifiersFactory = builder.gravityModifiersFactory; this.rowStrategy = builder.rowStrategy; this.positionIterator = builder.positionIterator; //--- end read builder } void setFinishingCriteria(@NonNull IFinishingCriteria finishingCriteria) { this.finishingCriteria = finishingCriteria; } @Override public AbstractPositionIterator positionIterator() { return positionIterator; } public boolean isRowCompleted() { return isRowCompleted; } public List<Item> getCurrentRowItems() { List<Item> items = new LinkedList<>(); List<Pair<Rect, View>> mutableRowViews = new LinkedList<>(rowViews); if (isReverseOrder()) { Collections.reverse(mutableRowViews); } for (Pair<Rect, View> rowView : mutableRowViews) { items.add(new Item(rowView.first, layoutManager.getPosition(rowView.second))); } return items; } public final int getCurrentViewPosition() { return currentViewPosition; } final IViewCacheStorage getCacheStorage() { return cacheStorage; } public void addLayouterListener(ILayouterListener layouterListener) { if (layouterListener != null) layouterListeners.add(layouterListener); } @Override public void removeLayouterListener(ILayouterListener layouterListener) { layouterListeners.remove(layouterListener); } private void notifyLayouterListeners() { for (ILayouterListener layouterListener : layouterListeners) { layouterListener.onLayoutRow(this); } } @Override public final int getPreviousRowSize() { return previousRowSize; } /** read view params to memory */ private void calculateView(View view) { currentViewHeight = layoutManager.getDecoratedMeasuredHeight(view); currentViewWidth = layoutManager.getDecoratedMeasuredWidth(view); currentViewPosition = layoutManager.getPosition(view); } @Override @CallSuper /** calculate view positions, view won't be actually added to layout when calling this method * @return true if view successfully placed, false if view can't be placed because out of space on screen and have to be recycled */ public final boolean placeView(View view) { layoutManager.measureChildWithMargins(view, 0, 0); calculateView(view); if (canNotBePlacedInCurrentRow()) { isRowCompleted = true; layoutRow(); } if (isFinishedLayouting()) return false; rowSize++; Rect rect = createViewRect(view); rowViews.add(new Pair<>(rect, view)); return true; } /** if all necessary view have placed*/ public final boolean isFinishedLayouting() { return finishingCriteria.isFinishedLayouting(this); } /** check if we can not add current view to row * we determine it on the next layouter step, because we need next view size to determine whether it fits in row or not */ @SuppressWarnings("WeakerAccess") public final boolean canNotBePlacedInCurrentRow() { return breaker.isRowBroke(this); } /** factory method for Rect, where view will be placed. Creation based on inner layouter parameters */ abstract Rect createViewRect(View view); /** check whether items in {@link #rowViews} are in reverse order. It is true for backward layouters */ abstract boolean isReverseOrder(); /** called when layouter ready to add row to border. Children could perform normalization actions on created row*/ abstract void onPreLayout(); /** called after row have been layouted. Children should prepare new row here. */ abstract void onAfterLayout(); abstract boolean isAttachedViewFromNewRow(View view); abstract void onInterceptAttachView(View view); void setPlacer(@NonNull IPlacer placer) { this.placer = placer; } @CallSuper @Override /** Read layouter state from current attached view. We need only last of it, but we can't determine here which is last. * Based on characteristics of last attached view, layouter algorithm will be able to continue placing from it. * This method have to be called on attaching view*/ public final boolean onAttachView(View view) { calculateView(view); if (isAttachedViewFromNewRow(view)) { //new row, reset row size notifyLayouterListeners(); rowSize = 0; } onInterceptAttachView(view); if (isFinishedLayouting()) return false; rowSize++; layoutManager.attachView(view); return true; } @Override /** add views from current row to layout*/ public final void layoutRow() { onPreLayout(); //apply modifiers to whole row if (rowViews.size() > 0) { rowStrategy.applyStrategy(this, getCurrentRowItems()); } /** layout pre-calculated row on a recyclerView border */ for (Pair<Rect, View> rowViewRectPair : rowViews) { Rect viewRect = rowViewRectPair.first; View view = rowViewRectPair.second; viewRect = applyChildGravity(view, viewRect); //add view to layout placer.addView(view); //layout whole views in a row layoutManager.layoutDecorated(view, viewRect.left, viewRect.top, viewRect.right, viewRect.bottom); } onAfterLayout(); notifyLayouterListeners(); previousRowSize = rowSize; //clear row data this.rowSize = 0; rowViews.clear(); isRowCompleted = false; } /** by default items placed and attached to a top of the row. * Modify theirs relative positions according to the selected child gravity * @return modified rect with applied gravity */ private Rect applyChildGravity(View view, Rect viewRect) { @SpanLayoutChildGravity int viewGravity = childGravityResolver.getItemGravity(getLayoutManager().getPosition(view)); IGravityModifier gravityModifier = gravityModifiersFactory.getGravityModifier(viewGravity); return gravityModifier.modifyChildRect(getStartRowBorder(), getEndRowBorder(), viewRect); } @NonNull public ChipsLayoutManager getLayoutManager() { return layoutManager; } /** get count of items inside current row */ @Override public int getRowSize() { return rowSize; } public int getViewTop() { return viewTop; } /** get a start coordinate of row border which is perpendicular to row general extension*/ public abstract int getStartRowBorder(); /** get an end coordinate of row border which is perpendicular to row general extension*/ public abstract int getEndRowBorder(); @Override public Rect getRowRect() { return new Rect(getCanvasLeftBorder(), getViewTop(), getCanvasRightBorder(), getViewBottom()); } public int getViewBottom() { return viewBottom; } final Rect getOffsetRect() { return new Rect(viewLeft, viewTop, viewRight, viewBottom); } public final int getViewLeft() { return viewLeft; } public final int getViewRight() { return viewRight; } public final int getCurrentViewWidth() { return currentViewWidth; } public final int getCurrentViewHeight() { return currentViewHeight; } public abstract int getRowLength(); public abstract static class Builder { private ChipsLayoutManager layoutManager; private IViewCacheStorage cacheStorage; private IBorder border; private IChildGravityResolver childGravityResolver; private IFinishingCriteria finishingCriteria; private IPlacer placer; private ILayoutRowBreaker breaker; private Rect offsetRect; private HashSet<ILayouterListener> layouterListeners = new HashSet<>(); private IGravityModifiersFactory gravityModifiersFactory; private IRowStrategy rowStrategy; private AbstractPositionIterator positionIterator; Builder() {} @SuppressWarnings("WeakerAccess") @NonNull public Builder offsetRect(@NonNull Rect offsetRect) { this.offsetRect = offsetRect; return this; } @NonNull public final Builder layoutManager(@NonNull ChipsLayoutManager layoutManager) { this.layoutManager = layoutManager; return this; } @NonNull final Builder cacheStorage(@NonNull IViewCacheStorage cacheStorage) { this.cacheStorage = cacheStorage; return this; } @NonNull Builder rowStrategy(IRowStrategy rowStrategy) { this.rowStrategy = rowStrategy; return this; } @NonNull final Builder canvas(@NonNull IBorder border) { this.border = border; return this; } @NonNull final Builder gravityModifiersFactory(@NonNull IGravityModifiersFactory gravityModifiersFactory) { this.gravityModifiersFactory = gravityModifiersFactory; return this; } @NonNull final Builder childGravityResolver(@NonNull IChildGravityResolver childGravityResolver) { this.childGravityResolver = childGravityResolver; return this; } @NonNull final Builder finishingCriteria(@NonNull IFinishingCriteria finishingCriteria) { this.finishingCriteria = finishingCriteria; return this; } @NonNull public final Builder placer(@NonNull IPlacer placer) { this.placer = placer; return this; } @SuppressWarnings("unused") @NonNull final Builder addLayouterListener(@Nullable ILayouterListener layouterListener) { if (layouterListener != null) { layouterListeners.add(layouterListener); } return this; } @NonNull final Builder breaker(@NonNull ILayoutRowBreaker breaker) { AssertionUtils.assertNotNull(breaker, "breaker shouldn't be null"); this.breaker = breaker; return this; } @NonNull final Builder addLayouterListeners(@NonNull List<ILayouterListener> layouterListeners) { this.layouterListeners.addAll(layouterListeners); return this; } @NonNull public Builder positionIterator(AbstractPositionIterator positionIterator) { this.positionIterator = positionIterator; return this; } @NonNull protected abstract AbstractLayouter createLayouter(); public final AbstractLayouter build() { if (layoutManager == null) throw new IllegalStateException("layoutManager can't be null, call #layoutManager()"); if (breaker == null) throw new IllegalStateException("breaker can't be null, call #breaker()"); if (border == null) throw new IllegalStateException("border can't be null, call #border()"); if (cacheStorage == null) throw new IllegalStateException("cacheStorage can't be null, call #cacheStorage()"); if (rowStrategy == null) throw new IllegalStateException("rowStrategy can't be null, call #rowStrategy()"); if (offsetRect == null) throw new IllegalStateException("offsetRect can't be null, call #offsetRect()"); if (finishingCriteria == null) throw new IllegalStateException("finishingCriteria can't be null, call #finishingCriteria()"); if (placer == null) throw new IllegalStateException("placer can't be null, call #placer()"); if (gravityModifiersFactory == null) throw new IllegalStateException("gravityModifiersFactory can't be null, call #gravityModifiersFactory()"); if (childGravityResolver == null) throw new IllegalStateException("childGravityResolver can't be null, call #childGravityResolver()"); if (positionIterator == null) throw new IllegalStateException("positionIterator can't be null, call #positionIterator()"); return createLayouter(); } } /////////////////////////////////////////////////////////////////////////// // border delegate /////////////////////////////////////////////////////////////////////////// public final int getCanvasRightBorder() { return border.getCanvasRightBorder(); } public final int getCanvasBottomBorder() { return border.getCanvasBottomBorder(); } public final int getCanvasLeftBorder() { return border.getCanvasLeftBorder(); } public final int getCanvasTopBorder() { return border.getCanvasTopBorder(); } }