/* * Copyright 2016 - 2020 Michael Rapp * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package de.mrapp.android.tabswitcher.layout; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.Menu; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.animation.Animation; import android.view.animation.DecelerateInterpolator; import android.view.animation.Transformation; import androidx.annotation.CallSuper; import androidx.annotation.ColorInt; import androidx.annotation.MenuRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener; import androidx.core.util.Pair; import de.mrapp.android.tabswitcher.AddTabButtonListener; import de.mrapp.android.tabswitcher.R; import de.mrapp.android.tabswitcher.Tab; import de.mrapp.android.tabswitcher.TabSwitcher; import de.mrapp.android.tabswitcher.TabSwitcherDecorator; import de.mrapp.android.tabswitcher.gesture.AbstractTouchEventHandler; import de.mrapp.android.tabswitcher.gesture.PullDownGestureEventHandler; import de.mrapp.android.tabswitcher.gesture.SwipeGestureEventHandler; import de.mrapp.android.tabswitcher.gesture.TouchEventDispatcher; import de.mrapp.android.tabswitcher.iterator.AbstractItemIterator; import de.mrapp.android.tabswitcher.iterator.ItemIterator; import de.mrapp.android.tabswitcher.layout.AbstractDragTabsEventHandler.DragState; import de.mrapp.android.tabswitcher.layout.Arithmetics.Axis; import de.mrapp.android.tabswitcher.model.AbstractItem; import de.mrapp.android.tabswitcher.model.AddTabItem; import de.mrapp.android.tabswitcher.model.Model; import de.mrapp.android.tabswitcher.model.State; import de.mrapp.android.tabswitcher.model.TabItem; import de.mrapp.android.tabswitcher.model.TabSwitcherModel; import de.mrapp.android.tabswitcher.model.TabSwitcherStyle; import de.mrapp.android.util.ViewUtil; import de.mrapp.android.util.logging.LogLevel; import de.mrapp.android.util.logging.Logger; import de.mrapp.android.util.view.AbstractViewRecycler; import de.mrapp.android.util.view.AttachedViewRecycler; import de.mrapp.util.Condition; /** * An abstract base class for all layouts, which implement the functionality of a {@link * TabSwitcher}. * * @author Michael Rapp * @since 0.1.0 */ public abstract class AbstractTabSwitcherLayout implements TabSwitcherLayout, OnGlobalLayoutListener, Model.Listener, TouchEventDispatcher.Callback, AbstractDragTabsEventHandler.Callback, SwipeGestureEventHandler.Callback, PullDownGestureEventHandler.Callback { /** * Defines the interface, a class, which should be notified about the events of a tab switcher * layout, must implement. */ public interface Callback { /* * The method, which is invoked, when all animations have been ended. */ void onAnimationsEnded(); } /** * A layout listener, which unregisters itself from the observed view, when invoked. The * listener allows to encapsulate another listener, which is notified, when the listener is * invoked. */ public static class LayoutListenerWrapper implements OnGlobalLayoutListener { /** * The observed view. */ private final View view; /** * The encapsulated listener. */ private final OnGlobalLayoutListener listener; /** * Creates a new layout listener, which unregisters itself from the observed view, when * invoked. * * @param view * The observed view as an instance of the class {@link View}. The view may not be * null * @param listener * The listener, which should be encapsulated, as an instance of the type {@link * OnGlobalLayoutListener} or null, if no listener should be encapsulated */ public LayoutListenerWrapper(@NonNull final View view, @Nullable final OnGlobalLayoutListener listener) { Condition.INSTANCE.ensureNotNull(view, "The view may not be null"); this.view = view; this.listener = listener; } @Override public void onGlobalLayout() { ViewUtil.removeOnGlobalLayoutListener(view.getViewTreeObserver(), this); if (listener != null) { listener.onGlobalLayout(); } } } /** * A animation listener, which increases the number of running animations, when the observed * animation is started, and decreases the number of accordingly, when the animation is * finished. The listener allows to encapsulate another animation listener, which is notified * when the animation has been started, canceled or ended. */ protected class AnimationListenerWrapper extends AnimatorListenerAdapter { /** * The encapsulated listener. */ private final AnimatorListener listener; /** * Decreases the number of running animations and executes the next pending action, if no * running animations remain. */ private void endAnimation() { if (--runningAnimations == 0) { notifyOnAnimationsEnded(); } } /** * Creates a new animation listener, which increases the number of running animations, when * the observed animation is started, and decreases the number of accordingly, when the * animation is finished. * * @param listener * The listener, which should be encapsulated, as an instance of the type {@link * AnimatorListener} or null, if no listener should be encapsulated */ public AnimationListenerWrapper(@Nullable final AnimatorListener listener) { this.listener = listener; runningAnimations++; } @Override public void onAnimationStart(final Animator animation) { super.onAnimationStart(animation); if (listener != null) { listener.onAnimationStart(animation); } } @Override public void onAnimationEnd(final Animator animation) { super.onAnimationEnd(animation); if (listener != null) { listener.onAnimationEnd(animation); } endAnimation(); } @Override public void onAnimationCancel(final Animator animation) { super.onAnimationCancel(animation); if (listener != null) { listener.onAnimationCancel(animation); } endAnimation(); } } /** * A builder, which allows to configure and create instances of the class {@link * InitialItemIterator}. */ protected class InitialItemIteratorBuilder extends AbstractItemIterator.AbstractBuilder<InitialItemIteratorBuilder, InitialItemIterator> { /** * The backing array, which is used to store items, once their initial position and state * has been calculated. */ private final AbstractItem[] backingArray; /** * Creates a new builder, which allows to configure and create instances of the class {@link * InitialItemIterator}. * * @param backingArray * The backing array, which should be used to store items, once their initial * position and state has been calculated. The backing array may not be null */ public InitialItemIteratorBuilder(@NonNull final AbstractItem[] backingArray) { Condition.INSTANCE.ensureNotNull(backingArray, "The backing array may not be null"); this.backingArray = backingArray; } @NonNull @Override public InitialItemIterator create() { return new InitialItemIterator(backingArray, reverse, start); } } /** * An iterator, which allows to iterate the items, which correspond to the child views of a * {@link TabSwitcher}. When an item is referenced for the first time, its initial position and * state is calculated and the item is stored in a backing array. When the item is iterated * again, it is retrieved from the backing array. */ protected class InitialItemIterator extends AbstractItemIterator { /** * The backing array, which is used to store items, once their initial position and state * has been calculated. */ private final AbstractItem[] backingArray; /** * Calculates the initial position and state of a specific item. * * @param item * The item, whose position and state should be calculated, as an instance of the * class {@link AbstractItem}. The item may not be null * @param predecessor * The predecessor of the given item as an instance of the class {@link * AbstractItem} or null, if the item does not have a predecessor */ private void calculateAndClipStartPosition(@NonNull final AbstractItem item, @Nullable final AbstractItem predecessor) { float position = calculateStartPosition(item); Pair<Float, State> pair = clipPosition(item.getIndex(), position, predecessor); item.getTag().setPosition(pair.first); item.getTag().setState(pair.second); } /** * Calculates and returns the initial position of a specific item. * * @param item * The item, whose position should be calculated, as an instance of the class {@link * AbstractItem}. The item may not be null * @return The position, which has been calculated, as a {@link Float} value */ private float calculateStartPosition(@NonNull final AbstractItem item) { if (item.getIndex() == 0) { return getCount() > getStackedTabCount() ? getStackedTabCount() * stackedTabSpacing : (getCount() - 1) * stackedTabSpacing; } else { return -1; } } /** * Creates a new iterator, which allows to iterate the items, which corresponds to the child * views of a {@link TabSwitcher}. When an item is referenced for the first time, its * initial position and state is calculated and the item is stored in a backing array. When * the item is iterated again, it is retrieved from the backing array. * * @param backingArray * The backing array, which should be used to store items, once their initial * position and state has been calculated, as an array of the type {@link * AbstractItem}. The array may not be null * @param reverse * True, if the items should be iterated in reverse order, false otherwise * @param start * The index of the first item, which should be iterated, as an {@link Integer} * value or -1, if all items should be iterated */ private InitialItemIterator(@NonNull final AbstractItem[] backingArray, final boolean reverse, final int start) { Condition.INSTANCE.ensureNotNull(backingArray, "The backing array may not be null"); this.backingArray = backingArray; initialize(reverse, start); } @Override public final int getCount() { return backingArray.length; } @NonNull @Override public final AbstractItem getItem(final int index) { AbstractItem item = backingArray[index]; if (item == null) { if (index == 0 && getModel().isAddTabButtonShown()) { item = AddTabItem.create(getTabViewRecycler()); } else { item = TabItem.create(getModel(), getTabViewRecycler(), index - (getModel().isAddTabButtonShown() ? 1 : 0)); } calculateAndClipStartPosition(item, index > 0 ? getItem(index - 1) : null); backingArray[index] = item; } return item; } } /** * An animation, which allows to fling the tabs. */ private class FlingAnimation extends android.view.animation.Animation { /** * The distance, the tabs should be moved. */ private final float distance; /** * Creates a new fling animation. * * @param distance * The distance, the tabs should be moved, in pixels as a {@link Float} value */ FlingAnimation(final float distance) { this.distance = distance; } @Override protected void applyTransformation(final float interpolatedTime, final Transformation t) { if (flingAnimation != null) { getDragHandler().handleDrag(distance * interpolatedTime, 0); } } } /** * The tab switcher, the layout belongs to. */ private final TabSwitcher tabSwitcher; /** * The model of the tab switcher, the layout belongs to. */ private final TabSwitcherModel model; /** * The arithmetics, which are used by the layout. */ private final Arithmetics arithmetics; /** * The style, which allows to retrieve style attributes of the tab switcher. */ private final TabSwitcherStyle style; /** * The dispatcher, which is used to dispatch touch events to event handlers. */ private final TouchEventDispatcher touchEventDispatcher; /** * The space between tabs, which are part of a stack, in pixels. */ private final int stackedTabSpacing; /** * The logger, which is used for logging. */ private final Logger logger; /** * The callback, which is notified about the layout's events. */ private Callback callback; /** * The number of animations, which are currently running. */ private int runningAnimations; /** * The animation, which is used to fling the tabs. */ private android.view.animation.Animation flingAnimation; /** * The index of the first visible tab. */ private int firstVisibleIndex; /** * Registers the layout as the callback of all touch event handlers. */ private void registerEventHandlerCallbacks() { for (AbstractTouchEventHandler eventHandler : touchEventDispatcher) { registerEventHandlerCallback(eventHandler); } touchEventDispatcher.setCallback(this); } /** * Registers the layout as the callback of a specific event handler. * * @param eventHandler * The event handler as an instance of the class {@link AbstractTouchEventHandler}. The * event handler may not be null */ private void registerEventHandlerCallback( @NonNull final AbstractTouchEventHandler eventHandler) { if (eventHandler instanceof SwipeGestureEventHandler) { ((SwipeGestureEventHandler) eventHandler).setCallback(this); } else if (eventHandler instanceof PullDownGestureEventHandler) { ((PullDownGestureEventHandler) eventHandler).setCallback(this); } } /** * Unregisters the layout as the callback of all touch event handlers. */ private void unregisterEventHandlerCallbacks() { for (AbstractTouchEventHandler eventHandler : touchEventDispatcher) { unregisterEventHandlerCallback(eventHandler); } touchEventDispatcher.setCallback(null); } /** * Unregisters the layout as the callback of a specific event handler. * * @param eventHandler * The event handler as an instance of the class {@link AbstractTouchEventHandler}. The * event handler may not be null */ private void unregisterEventHandlerCallback( @NonNull final AbstractTouchEventHandler eventHandler) { if (eventHandler instanceof SwipeGestureEventHandler) { ((SwipeGestureEventHandler) eventHandler).setCallback(null); } else if (eventHandler instanceof PullDownGestureEventHandler) { ((PullDownGestureEventHandler) eventHandler).setCallback(null); } } /** * Adapts the visibility of the toolbars, which are shown, when the tab switcher is shown. */ private void adaptToolbarVisibility() { Toolbar[] toolbars = getToolbars(); if (toolbars != null) { for (Toolbar toolbar : toolbars) { toolbar.setVisibility( getTabSwitcher().isSwitcherShown() && getModel().areToolbarsShown() ? View.VISIBLE : View.INVISIBLE); } } // TODO: Detach and re-inflate layout } /** * Adapts the title of the toolbar, which is shown, when the tab switcher is shown. */ private void adaptToolbarTitle() { Toolbar[] toolbars = getToolbars(); if (toolbars != null) { CharSequence title = style.getToolbarTitle(); toolbars[TabSwitcher.PRIMARY_TOOLBAR_INDEX].setTitle(title); } } /** * Adapts the navigation icon of the toolbar, which is shown, when the tab switcher is shown. */ private void adaptToolbarNavigationIcon() { Toolbar[] toolbars = getToolbars(); if (toolbars != null) { Toolbar toolbar = toolbars[TabSwitcher.PRIMARY_TOOLBAR_INDEX]; Drawable icon = style.getToolbarNavigationIcon(); toolbar.setNavigationIcon(icon); toolbar.setNavigationOnClickListener(getModel().getToolbarNavigationIconListener()); } } /** * Adapts the decorator. */ private void adaptDecorator() { getContentViewRecycler().setAdapter(onCreateContentRecyclerAdapter()); } /** * Adapts the log level. */ private void adaptLogLevel() { getTabViewRecycler().setLogLevel(getModel().getLogLevel()); getContentViewRecycler().setLogLevel(getModel().getLogLevel()); } /** * Inflates the menu of the toolbar, which is shown, when the tab switcher is shown. */ private void inflateToolbarMenu() { Toolbar[] toolbars = getToolbars(); int menuId = getModel().getToolbarMenuId(); if (toolbars != null && menuId != -1) { Toolbar toolbar = toolbars.length > 1 ? toolbars[TabSwitcher.SECONDARY_TOOLBAR_INDEX] : toolbars[TabSwitcher.PRIMARY_TOOLBAR_INDEX]; Menu previousMenu = toolbar.getMenu(); if (previousMenu != null) { previousMenu.clear(); } toolbar.inflateMenu(menuId); toolbar.setOnMenuItemClickListener(getModel().getToolbarMenuItemListener()); } } /** * Creates and returns an animation listener, which allows to handle, when a fling animation * ended. * * @return The listener, which has been created, as an instance of the class {@link * Animation.AnimationListener}. The listener may not be null */ @NonNull private Animation.AnimationListener createFlingAnimationListener() { return new Animation.AnimationListener() { @Override public void onAnimationStart(final android.view.animation.Animation animation) { } @Override public void onAnimationEnd(final android.view.animation.Animation animation) { getDragHandler().onUp(null); flingAnimation = null; notifyOnAnimationsEnded(); } @Override public void onAnimationRepeat(final android.view.animation.Animation animation) { } }; } /** * Calculates the positions of all items, when dragging towards the start. * * @param dragDistance * The current drag distance in pixels as a {@link Float} value */ private void calculatePositionsWhenDraggingToEnd(final float dragDistance) { firstVisibleIndex = -1; AbstractItemIterator iterator = new ItemIterator.Builder(getTabSwitcher(), getTabViewRecycler()).create(); AbstractItem item; boolean abort = false; while ((item = iterator.next()) != null && !abort) { if (getItemCount() - item.getIndex() > 1) { abort = calculatePositionWhenDraggingToEnd(dragDistance, item, iterator.previous()); if (firstVisibleIndex == -1 && item.getTag().getState() == State.FLOATING) { firstVisibleIndex = item.getIndex(); } } else { Pair<Float, State> pair = clipPosition(item.getIndex(), item.getTag().getPosition(), iterator.previous()); item.getTag().setPosition(pair.first); item.getTag().setState(pair.second); } inflateOrRemoveView(item, true); } } /** * The method, which is invoked on implementing subclasses in order to calculate the position of * a specific item, when dragging towards the end. * * @param dragDistance * The current drag distance in pixels as a {@link Float} value * @param item * The item whose position should be calculated, as an instance of the class {@link * AbstractItem}. The item may not be null * @param predecessor * The predecessor of the given item as an instance of the class {@link AbstractItem} or * null, if the item does not have a predecessor * @return True, if calculating the position of subsequent items can be omitted, false otherwise */ private boolean calculatePositionWhenDraggingToEnd(final float dragDistance, @NonNull final AbstractItem item, @Nullable final AbstractItem predecessor) { if (predecessor == null || predecessor.getTag().getState() != State.FLOATING) { if ((item.getTag().getState() == State.STACKED_START_ATOP && item.getIndex() == 0) || item.getTag().getState() == State.FLOATING) { float currentPosition = item.getTag().getPosition(); float newPosition = currentPosition + dragDistance; float maxEndPosition = calculateMaxEndPosition(item.getIndex()); if (maxEndPosition != -1) { newPosition = Math.min(newPosition, maxEndPosition); } Pair<Float, State> pair = clipPosition(item.getIndex(), newPosition, predecessor); item.getTag().setPosition(pair.first); item.getTag().setState(pair.second); } else if (item.getTag().getState() == State.STACKED_START_ATOP) { return true; } } else { float newPosition = calculateSuccessorPosition(item, predecessor); float maxEndPosition = calculateMaxEndPosition(item.getIndex()); if (maxEndPosition != -1) { newPosition = Math.min(newPosition, maxEndPosition); } Pair<Float, State> pair = clipPosition(item.getIndex(), newPosition, predecessor); item.getTag().setPosition(pair.first); item.getTag().setState(pair.second); } return false; } /** * Calculates the positions of all items, when dragging towards the end. * * @param dragDistance * The current drag distance in pixels as a {@link Float} value */ private void calculatePositionsWhenDraggingToStart(final float dragDistance) { AbstractItemIterator iterator = new ItemIterator.Builder(getTabSwitcher(), getTabViewRecycler()) .start(Math.max(0, firstVisibleIndex)).create(); AbstractItem item; boolean abort = false; while ((item = iterator.next()) != null && !abort) { if (getItemCount() - item.getIndex() > 1) { abort = calculatePositionWhenDraggingToStart(dragDistance, item, iterator.previous()); } else { Pair<Float, State> pair = clipPosition(item.getIndex(), item.getTag().getPosition(), iterator.previous()); item.getTag().setPosition(pair.first); item.getTag().setState(pair.second); } inflateOrRemoveView(item, true); } if (firstVisibleIndex > 0) { int start = firstVisibleIndex - 1; iterator = new ItemIterator.Builder(getTabSwitcher(), getTabViewRecycler()).reverse(true) .start(start).create(); while ((item = iterator.next()) != null) { AbstractItem successor = iterator.previous(); if (item.getIndex() < start) { float successorPosition = successor.getTag().getPosition(); Pair<Float, State> pair = clipPosition(successor.getIndex(), successorPosition, item); successor.getTag().setPosition(pair.first); successor.getTag().setState(pair.second); inflateOrRemoveView(successor, true); if (successor.getTag().getState() == State.FLOATING) { firstVisibleIndex = successor.getIndex(); } else { break; } } float newPosition = calculatePredecessorPosition(item, successor); item.getTag().setPosition(newPosition); if (!iterator.hasNext()) { Pair<Float, State> pair = clipPosition(item.getIndex(), newPosition, (AbstractItem) null); item.getTag().setPosition(pair.first); item.getTag().setState(pair.second); inflateOrRemoveView(item, true); if (item.getTag().getState() == State.FLOATING) { firstVisibleIndex = item.getIndex(); } } } } } /** * Calculates the position of a specific item, when dragging towards the start. * * @param dragDistance * The current drag distance in pixels as a {@link Float} value * @param item * The item, whose position should be calculated, as an instance of the class {@link * AbstractItem}. The item may not be null * @param predecessor * The predecessor of the given item as an instance of the class {@link AbstractItem} or * null, if the item does not have a predecessor * @return True, if calculating the position of subsequent items can be omitted, false otherwise */ private boolean calculatePositionWhenDraggingToStart(final float dragDistance, @NonNull final AbstractItem item, @Nullable final AbstractItem predecessor) { float attachedPosition = calculateAttachedPosition(getTabSwitcher().getCount()); if (predecessor == null || predecessor.getTag().getState() != State.FLOATING || (attachedPosition != -1 && predecessor.getTag().getPosition() > attachedPosition)) { if (item.getTag().getState() == State.FLOATING) { float currentPosition = item.getTag().getPosition(); float newPosition = currentPosition + dragDistance; float minStartPosition = calculateMinStartPosition(item.getIndex()); if (minStartPosition != -1) { newPosition = Math.max(newPosition, minStartPosition); } Pair<Float, State> pair = clipPosition(item.getIndex(), newPosition, predecessor); item.getTag().setPosition(pair.first); item.getTag().setState(pair.second); } else if (item.getTag().getState() == State.STACKED_START_ATOP) { float currentPosition = item.getTag().getPosition(); Pair<Float, State> pair = clipPosition(item.getIndex(), currentPosition, predecessor); item.getTag().setPosition(pair.first); item.getTag().setState(pair.second); return true; } else if (item.getTag().getState() == State.HIDDEN || item.getTag().getState() == State.STACKED_START) { return true; } } else { float newPosition = calculateSuccessorPosition(item, predecessor); float minStartPosition = calculateMinStartPosition(item.getIndex()); if (minStartPosition != -1) { newPosition = Math.max(newPosition, minStartPosition); } Pair<Float, State> pair = clipPosition(item.getIndex(), newPosition, predecessor); item.getTag().setPosition(pair.first); item.getTag().setState(pair.second); } return false; } /** * Notifies the callback, that all animations have been ended. */ private void notifyOnAnimationsEnded() { if (callback != null) { callback.onAnimationsEnded(); } } /** * Returns the tab switcher, the layout belongs to. * * @return The tab switcher, the layout belongs to, as an instance of the class {@link * TabSwitcher}. The tab switcher may not be null */ @NonNull protected final TabSwitcher getTabSwitcher() { return tabSwitcher; } /** * Returns the model of the tab switcher, the layout belongs to. * * @return The model of the tab switcher, the layout belongs to, as an instance of the class * {@link TabSwitcherModel}. The model may not be null */ @NonNull protected final TabSwitcherModel getModel() { return model; } /** * Returns the arithmetics, which are used by the layout. * * @return The arithmetics, which are used by the layout, as an instance of the type {@link * Arithmetics}. The arithmetics may not be null */ @NonNull protected final Arithmetics getArithmetics() { return arithmetics; } /** * Returns the style, which allows to retrieve style attributes of the tab switcher. * * @return The style, which allows to retrieve style attributes of the tab switcher, as an * instance of the class {@link TabSwitcherStyle}. The style may not be null */ @NonNull protected final TabSwitcherStyle getStyle() { return style; } /** * Returns the space between tabs, which are part of a stack. * * @return The space between tabs, which are part of a stack, in pixels as an {@link Integer} * value */ protected final int getStackedTabSpacing() { return stackedTabSpacing; } /** * Returns the logger, which is used for logging. * * @return The logger, which is used for logging, as an instance of the class Logger. The logger * may not be null */ @NonNull protected final Logger getLogger() { return logger; } /** * Returns the context, which is used by the layout. * * @return The context, which is used by the layout, as an instance of the class {@link * Context}. The context may not be null */ @NonNull protected final Context getContext() { return tabSwitcher.getContext(); } /** * Returns the index of the first visible tab. * * @return The index of the first visible tab as an {@link Integer} value or -1, if no tabs is * visible */ protected final int getFirstVisibleIndex() { return firstVisibleIndex; } /** * Sets the index of the first visible tab. * * @param firstVisibleIndex * The index, which should be set, as an {@link Integer} value or -1, if no tab is * visible */ protected final void setFirstVisibleIndex(final int firstVisibleIndex) { this.firstVisibleIndex = firstVisibleIndex; } /** * Returns the number of child views, which are contained by the tab switcher. * * @return The number of child views, which are contained by the tab switcher, as an {@link * Integer} value */ protected final int getItemCount() { return getModel().getCount() + (getModel().isAddTabButtonShown() ? 1 : 0); } /** * Returns, whether a hidden tab at a specific index, is part of the stack, which is located at * the start, or not. * * @param index * The index of the hidden tab, as an {@link Integer} value * @return True, if the hidden tab is part of the stack, which is located at the start, false * otherwise */ protected final boolean isStackedAtStart(final int index) { boolean start = true; AbstractItemIterator iterator = new ItemIterator.Builder(getTabSwitcher(), getTabViewRecycler()).start(index + 1) .create(); AbstractItem item; while ((item = iterator.next()) != null) { State state = item.getTag().getState(); if (state == State.STACKED_START) { start = true; break; } else if (state == State.FLOATING) { start = false; break; } } return start; } /** * Clips the position of a specific item. * * @param index * The index of the item, whose position should be clipped, as an {@link Integer} value * @param position * The position, which should be clipped, in pixels as a {@link Float} value * @param predecessor * The predecessor of the given item as an instance of the class {@link AbstractItem} or * null, if the item does not have a predecessor * @return A pair, which contains the position and state of the item, as an instance of the * class Pair. The pair may not be null */ @NonNull protected final Pair<Float, State> clipPosition(final int index, final float position, @Nullable final AbstractItem predecessor) { return clipPosition(index, position, predecessor != null ? predecessor.getTag().getState() : null); } /** * Clips the position of a specific item. * * @param index * The index of the item, whose position should be clipped, as an {@link Integer} value * @param position * The position, which should be clipped, in pixels as a {@link Float} value * @param predecessorState * The state of the predecessor of the given item as a value of the enum {@link State} * or null, if the item does not have a predecessor * @return A pair, which contains the position and state of the item, as an instance of the * class Pair. The pair may not be null */ protected final Pair<Float, State> clipPosition(final int index, final float position, @Nullable final State predecessorState) { Pair<Float, State> startPair = calculatePositionAndStateWhenStackedAtStart(getItemCount(), index, predecessorState); float startPosition = startPair.first; if (position <= startPosition) { State state = startPair.second; return Pair.create(startPosition, state); } else { Pair<Float, State> endPair = calculatePositionAndStateWhenStackedAtEnd(index); float endPosition = endPair.first; if (position >= endPosition) { State state = endPair.second; return Pair.create(endPosition, state); } else { State state = State.FLOATING; return Pair.create(position, state); } } } /** * Calculates and returns the position and state of a specific item, when stacked at the start. * * @param count * The total number of items, which are currently contained by the tab switcher, as an * {@link Integer} value * @param index * The index of the item, whose position and state should be returned, as an {@link * Integer} value * @param predecessor * The predecessor of the given item as an instance of the class {@link AbstractItem} or * null, if the item does not have a predecessor * @return A pair, which contains the position and state of the given item, when stacked at the * start, as an instance of the class Pair. The pair may not be null */ @NonNull protected final Pair<Float, State> calculatePositionAndStateWhenStackedAtStart(final int count, final int index, @Nullable final AbstractItem predecessor) { return calculatePositionAndStateWhenStackedAtStart(count, index, predecessor != null ? predecessor.getTag().getState() : null); } /** * Inflates or removes the view, which is used to visualize a specific item, depending on the * item's current state. * * @param item * The item, whose view should be inflated or removed, as an instance of the class * {@link AbstractItem}. The item may not be null * @param dragging * True, if the item is currently being dragged, false otherwise */ protected final void inflateOrRemoveView(@NonNull final AbstractItem item, final boolean dragging) { if (item.isInflated() && !item.isVisible()) { getTabViewRecycler().remove(item); } else if (item.isVisible()) { if (!item.isInflated()) { inflateAndUpdateView(item, dragging, null); } else { updateView(item, dragging); } } } /** * Inflates the view, which is used to visualize a specific item. * * @param item * The item, whose view should be inflated, as an instance of the class {@link * AbstractItem}. The item may not be null * @param listener * The layout listener, which should be notified, when the view has been inflated, as an * instance of the type {@link OnGlobalLayoutListener} or null, if no listener should be * notified * @param params * An array, which contains optional parameters, which should be passed to the view * recycler, which is used to inflate the view, as an {@link Integer} array or null, if * no optional parameters should be used */ protected final void inflateView(@NonNull final AbstractItem item, @Nullable final OnGlobalLayoutListener listener, @NonNull final Integer... params) { Pair<View, Boolean> pair = getTabViewRecycler().inflate(item, params); if (listener != null) { boolean inflated = pair.second; if (inflated) { View view = pair.first; view.getViewTreeObserver() .addOnGlobalLayoutListener(new LayoutListenerWrapper(view, listener)); } else { listener.onGlobalLayout(); } } } /** * Updates the view, which is used to visualize a specific item. * * @param item * The item, whose view should be updated, as an instance of the class {@link * AbstractItem}. The item may not be null * @param dragging * True, if the item is currently being dragged, false otherwise */ @CallSuper protected void updateView(@NonNull final AbstractItem item, final boolean dragging) { float position = item.getTag().getPosition(); getArithmetics().setPosition(Axis.DRAGGING_AXIS, item, position); getArithmetics().setPosition(Axis.ORTHOGONAL_AXIS, item, 0); } /** * Calculates and returns the position on the dragging axis, where the distance between an item * and its predecessor should have reached the maximum. * * @param count * The total number of items, which are contained by the tab switcher, as an {@link * Integer} value * @return The position, which has been calculated, in pixels as an {@link Float} value or -1, * if no attached position is used */ protected float calculateAttachedPosition(final int count) { return -1; } /** * Creates a new layout, which implements the functionality of a {@link TabSwitcher}. * * @param tabSwitcher * The tab switcher, the layout belongs to, as an instance of the class {@link * TabSwitcher}. The tab switcher may not be null * @param model * The model of the tab switcher, the layout belongs to, as an instance of the class * {@link TabSwitcherModel}. The model may not be null * @param arithmetics * The arithmetics, which should be used by the layout, as an instance of the type * {@link Arithmetics}. The arithmetics may not be null * @param style * The style, which allows to retrieve style attributes of the tab switcher, as an * instance of the class {@link TabSwitcherStyle}. The style may not be null * @param touchEventDispatcher * The dispatcher, which is used to dispatch touch events to event handlers, as an * instance of the class {@link TouchEventDispatcher}. The dispatcher may not be null */ public AbstractTabSwitcherLayout(@NonNull final TabSwitcher tabSwitcher, @NonNull final TabSwitcherModel model, @NonNull final Arithmetics arithmetics, @NonNull final TabSwitcherStyle style, @NonNull final TouchEventDispatcher touchEventDispatcher) { Condition.INSTANCE.ensureNotNull(tabSwitcher, "The tab switcher may not be null"); Condition.INSTANCE.ensureNotNull(model, "The model may not be null"); Condition.INSTANCE.ensureNotNull(arithmetics, "The arithmetics may not be null"); Condition.INSTANCE.ensureNotNull(style, "The style may not be null"); Condition.INSTANCE.ensureNotNull(touchEventDispatcher, "The dispatcher may not be null"); this.tabSwitcher = tabSwitcher; this.model = model; this.arithmetics = arithmetics; this.style = style; this.touchEventDispatcher = touchEventDispatcher; Resources resources = tabSwitcher.getResources(); this.stackedTabSpacing = resources.getDimensionPixelSize(R.dimen.stacked_tab_spacing); this.logger = new Logger(model.getLogLevel()); this.callback = null; this.runningAnimations = 0; this.flingAnimation = null; this.firstVisibleIndex = -1; } /** * The method, which is invoked on implementing subclasses in order to retrieve the drag * handler, which is used by the layout. * * @return The drag handler, which is used by the layout, as an instance of the class {@link * AbstractDragTabsEventHandler} or null, if the drag handler has not been initialized yet */ public abstract AbstractDragTabsEventHandler<?> getDragHandler(); /** * The method, which is invoked on implementing subclasses in order to inflate the layout. * * @param inflater * The layout inflater, which should be used, as an instance of the class {@link * LayoutInflater}. The layout inflater may not be null * @param tabsOnly * True, if only the tabs should be inflated, false otherwise */ protected abstract void onInflateLayout(@NonNull final LayoutInflater inflater, final boolean tabsOnly); /** * The method, which is invoked on implementing subclasses in order to detach the layout. * * @param tabsOnly * True, if only the tabs should be detached, false otherwise * @return A pair, which contains the index of the tab, which should be used as a reference, * when restoring the positions of tabs, as well as its current position in relation to the * available space, as an instance of the class Pair or null, if the positions of tabs should * not be restored */ @Nullable protected abstract Pair<Integer, Float> onDetachLayout(final boolean tabsOnly); /** * The method, which is invoked on implementing subclasses in order to retrieve the view * recycler, which allows to recycle the views, which are associated with tabs. * * @return The view recycler, which allows to recycle the views, which are associated with tabs, * as an instance of the class ViewRecycler or null, if the view recycler has not been * initialized yet */ public abstract AbstractViewRecycler<Tab, Void> getContentViewRecycler(); /** * The method, which is invoked on implementing subclasses in order to retrieve the view * recycler, which allows to inflate the views, which are used to visualize the tabs. * * @return The view recycler, which allows to inflate the views, which are used to visualize the * tabs, as an instance of the class AttachedViewRecycler or null, if the view recycler has not * been initialized yet */ protected abstract AttachedViewRecycler<AbstractItem, Integer> getTabViewRecycler(); /** * The method, which is invoked on implementing subclasses in order to retrieve the adapter of * the view recycler, which allows to inflate the views, which are used to visualize the tabs. * * @return The adapter of the view recycler, which allows to inflated the views, which are used * to visualize the tabs, as an instance of the class {@link AbstractTabRecyclerAdapter} or * null, if the view recycler has not been initialized yet */ protected abstract AbstractTabRecyclerAdapter getTabRecyclerAdapter(); /** * The method, which is invoked on implementing subclasses in order to inflate and update the * view, which is used to visualize a specific item. * * @param item * The item, whose view should be inflated, as an instance of the class {@link * AbstractItem}. The item may not be null * @param dragging * True, if the view is currently being dragged, false otherwise * @param listener * The layout listener, which should be notified, when the view has been inflated, as an * instance of the type {@link OnGlobalLayoutListener} or null, if no listener should be * notified */ protected abstract void inflateAndUpdateView(@NonNull final AbstractItem item, final boolean dragging, @Nullable final OnGlobalLayoutListener listener); /** * The method, which is invoked on implementing subclasses in order to retrieve the number of * tabs, which are contained by a stack. * * @return The number of tabs, which are contained by a stack, as an {@link Integer} value */ protected abstract int getStackedTabCount(); /** * The method, which is invoked on implementing subclasses in order to retrieve the position and * state of a specific item, when stacked at the start. * * @param count * The total number of items, which are currently contained by the tab switcher, as an * {@link Integer} value * @param index * The index of the item, whose position and state should be returned, as an {@link * Integer} value * @param predecessorState * The state of the predecessor of the given item as a value of the enum {@link State} * or null, if the item does not have a predecessor * @return A pair, which contains the position and state of the given item, when stacked at the * start, as an instance of the class Pair. The pair may not be null */ @NonNull protected abstract Pair<Float, State> calculatePositionAndStateWhenStackedAtStart( final int count, final int index, @Nullable final State predecessorState); /** * The method, which is invoked on implementing subclasses in order to retrieve the position and * state of a specific item, when stacked at the end. * * @param index * The index of the item, whose position and state should be returned, as an {@link * Integer} value * @return A pair, which contains the position and state of the given item, when stacked at the * end, as an instance of the class Pair. The pair may not be null */ @NonNull protected abstract Pair<Float, State> calculatePositionAndStateWhenStackedAtEnd( final int index); /** * Calculates the position of an item in relation to the position of its predecessor. * * @param item * The item, whose position should be calculated, as an instance of the class {@link * AbstractItem}. The item may not be null * @param predecessor * The predecessor as an instance of the class {@link AbstractItem}. The predecessor may * not be null * @return The position, which has been calculated, as a {@link Float} value */ protected abstract float calculateSuccessorPosition(@NonNull final AbstractItem item, @NonNull final AbstractItem predecessor); /** * Calculates the position of an item in relation to the position of its successor. * * @param item * The item, whose position should be calculated, as an instance of the class {@link * AbstractItem}. The item may not be null * @param successor * The successor as an instance of the class {@link AbstractItem}. The successor may not * be null * @return The position, which has been calculated, as a {@link Float} value */ protected abstract float calculatePredecessorPosition(@NonNull final AbstractItem item, @NonNull final AbstractItem successor); /** * The method, which is invoked on implementing subclasses in order to retrieve the minimum * position of a specific item, when dragging towards the start. * * @param index * The index of the item, whose position should be calculated, as an {@link Integer} * value * @return The position, which has been calculated, as a {@link Float} value or -1, if no * minimum position is available */ protected float calculateMinStartPosition(final int index) { return -1; } /** * The method, which is invoked on implementing subclasses in order to retrieve the maximum * position of a specific item, when dragging towards the end. * * @param index * The index of the item, whose position should be calculated, as an {@link Integer} * value * @return The position, which has been calculated, as a {@link Float} value or -1, if no * maximum position is available */ protected float calculateMaxEndPosition(final int index) { return -1; } /** * The method, which is invoked on implementing subclasses in order to retrieve, whether the * items are overshooting at the start. * * @return True, if the items are overshooting at the start, false otherwise */ protected boolean isOvershootingAtStart() { return false; } /** * The method, which is invoked on implementing subclasses in order to retrieve, whether the * items are overshooting at the end. * * @param dragState * The current drag state as an instance of the enum {@link DragState}. The drag state * may not be null * @param iterator * An iterator, which allows to iterate the items, which are contained by the tab * switcher, as an instance of the class {@link AbstractItemIterator}. The iterator may * not be null * @return True, if the items are overshooting at the end, false otherwise */ protected boolean isOvershootingAtEnd(@NonNull final DragState dragState, @NonNull final AbstractItemIterator iterator) { return false; } /** * The method, which is called when dragging after the positions and states of all tabs have * been calculated. It may be overridden by subclasses in order to implement a second layout * pass, which requires the information, which has been calculated in the first pass, and allows * to perform additional modifications of the tabs based on that information. * * @param builder * The builder, which allows to create the iterator, which should be used to iterate the * tabs, as an instance of the class {@link AbstractItemIterator.AbstractBuilder}. The * builder may not be null */ protected void secondLayoutPass(@NonNull final AbstractItemIterator.AbstractBuilder builder) { } /** * The method, which is invoked on implementing subclasses in order to create the view recycler * adapter, which allows to inflate the views, which are associated with tabs. * * @return The view recycler adapter, which has been created, as an instance of the class * AttachedViewRecycler.Adapter. The recycler adapter may not be null } */ @NonNull protected AttachedViewRecycler.Adapter<Tab, Void> onCreateContentRecyclerAdapter() { return getModel().getContentRecyclerAdapter(); } /** * Inflates the layout. * * @param tabsOnly * True, if only the tabs should be inflated, false otherwise */ public final void inflateLayout(final boolean tabsOnly) { int themeResourceId = style.getThemeHelper().getThemeResourceId(tabSwitcher.getLayout()); LayoutInflater inflater = LayoutInflater.from(new ContextThemeWrapper(getContext(), themeResourceId)); onInflateLayout(inflater, tabsOnly); registerEventHandlerCallbacks(); adaptDecorator(); adaptLogLevel(); if (!tabsOnly) { adaptToolbarVisibility(); adaptToolbarTitle(); adaptToolbarNavigationIcon(); inflateToolbarMenu(); } } /** * Detaches the layout. * * @param tabsOnly * True, if only the tabs should be detached, false otherwise * @return A pair, which contains the index of the first visible tab, as well as its current * position in relation to the available space, as an instance of the class Pair or null, if the * tab switcher is not shown */ @Nullable public final Pair<Integer, Float> detachLayout(final boolean tabsOnly) { Pair<Integer, Float> pair = onDetachLayout(tabsOnly); getTabViewRecycler().removeAll(); getTabViewRecycler().clearCache(); unregisterEventHandlerCallbacks(); touchEventDispatcher.removeEventHandler(getDragHandler()); if (!tabsOnly) { getTabSwitcher().removeAllViews(); } return pair; } /** * Sets the callback, which should be notified about the layout's events. * * @param callback * The callback, which should be set, as an instance of the type {@link Callback} or * null, if no callback should be notified */ public final void setCallback(@Nullable final Callback callback) { this.callback = callback; } @Override public final boolean isAnimationRunning() { return runningAnimations > 0 || flingAnimation != null; } @Nullable @Override public final Menu getToolbarMenu() { Toolbar[] toolbars = getToolbars(); if (toolbars != null) { Toolbar toolbar = toolbars.length > 1 ? toolbars[TabSwitcher.SECONDARY_TOOLBAR_INDEX] : toolbars[TabSwitcher.PRIMARY_TOOLBAR_INDEX]; return toolbar.getMenu(); } return null; } @Override public final void onLogLevelChanged(@NonNull final LogLevel logLevel) { adaptLogLevel(); } @CallSuper @Override public void onDecoratorChanged(@NonNull final TabSwitcherDecorator decorator) { adaptDecorator(); detachLayout(true); onGlobalLayout(); } @Override public void onAddTabButtonVisibilityChanged(final boolean visible) { } @Override public final void onAddTabButtonColorChanged(@Nullable final ColorStateList colorStateList) { } @Override public final void onToolbarVisibilityChanged(final boolean visible) { adaptToolbarVisibility(); } @Override public final void onToolbarTitleChanged(@Nullable final CharSequence title) { adaptToolbarTitle(); } @Override public final void onToolbarNavigationIconChanged(@Nullable final Drawable icon, @Nullable final OnClickListener listener) { adaptToolbarNavigationIcon(); } @Override public final void onToolbarMenuInflated(@MenuRes final int resourceId, @Nullable final OnMenuItemClickListener listener) { inflateToolbarMenu(); } @Override public final void onTabIconChanged(@Nullable final Drawable icon) { } @Override public void onTabBackgroundColorChanged(@Nullable final ColorStateList colorStateList) { } @Override public void onTabContentBackgroundColorChanged(@ColorInt final int color) { } @Override public final void onTabTitleColorChanged(@Nullable final ColorStateList colorStateList) { } @Override public final void onTabCloseButtonIconChanged(@Nullable final Drawable icon) { } @Override public final void onTabProgressBarColorChanged(@ColorInt final int color) { } @Override public void onSwitcherShown() { } @Override public void onSwitcherHidden() { } @Override public final void onAddedEventHandler(@NonNull final TouchEventDispatcher dispatcher, @NonNull final AbstractTouchEventHandler eventHandler) { registerEventHandlerCallback(eventHandler); } @Override public final void onRemovedEventHandler(@NonNull final TouchEventDispatcher dispatcher, @NonNull final AbstractTouchEventHandler eventHandler) { unregisterEventHandlerCallback(eventHandler); } @Nullable @Override public final DragState onDrag(@NonNull final DragState dragState, final float dragDistance) { if (dragDistance != 0) { if (dragState == DragState.DRAG_TO_END) { calculatePositionsWhenDraggingToEnd(dragDistance); } else { calculatePositionsWhenDraggingToStart(dragDistance); } secondLayoutPass(new ItemIterator.Builder(getTabSwitcher(), getTabViewRecycler())); } DragState overshoot = isOvershootingAtEnd(dragState, new ItemIterator.Builder(getTabSwitcher(), getTabViewRecycler()).create()) ? DragState.OVERSHOOT_END : (isOvershootingAtStart() ? DragState.OVERSHOOT_START : null); getLogger().logVerbose(getClass(), "Dragging using a distance of " + dragDistance + " pixels. Drag state is " + dragState + ", overshoot is " + overshoot); return overshoot; } @Override public final void onPressStarted(@NonNull final AbstractItem item) { ColorStateList colorStateList = null; boolean selected = false; if (item instanceof TabItem) { TabItem tabItem = (TabItem) item; Tab tab = tabItem.getTab(); colorStateList = style.getTabBackgroundColor(tab); selected = getModel().getSelectedTab() == tab; } else if (item instanceof AddTabItem) { colorStateList = style.getAddTabButtonColor(); } if (colorStateList != null) { int[] stateSet = selected ? new int[]{android.R.attr.state_pressed, android.R.attr.state_selected} : new int[]{android.R.attr.state_pressed}; int color = colorStateList.getColorForState(stateSet, -1); if (color != -1) { View view = item.getView(); Drawable background = view.getBackground(); background.setColorFilter(color, PorterDuff.Mode.MULTIPLY); } } } @Override public final void onPressEnded(@NonNull final AbstractItem item) { getTabRecyclerAdapter().onTabBackgroundColorChanged(getModel().getTabBackgroundColor()); } @Override public final void onClick(@NonNull final AbstractItem item) { if (item instanceof TabItem) { TabItem tabItem = (TabItem) item; getModel().selectTab(tabItem.getTab()); getLogger().logVerbose(getClass(), "Clicked tab at index " + (tabItem.getIndex() - (getModel().isAddTabButtonShown() ? 1 : 0))); } else if (item instanceof AddTabItem) { AddTabButtonListener listener = getModel().getAddTabButtonListener(); if (listener != null) { listener.onAddTab(getTabSwitcher()); } getLogger().logVerbose(getClass(), "Clicked add tab button"); } } @Override public final void onFling(final float distance, final long duration) { if (getDragHandler() != null) { flingAnimation = new FlingAnimation(distance); flingAnimation.setFillAfter(true); flingAnimation.setAnimationListener(createFlingAnimationListener()); flingAnimation.setDuration(duration); flingAnimation.setInterpolator(new DecelerateInterpolator()); getTabSwitcher().startAnimation(flingAnimation); logger.logVerbose(getClass(), "Started fling animation using a distance of " + distance + " pixels and a duration of " + duration + " milliseconds"); } } @Override public final void onCancelFling() { if (flingAnimation != null) { flingAnimation.cancel(); flingAnimation = null; getDragHandler().onUp(null); logger.logVerbose(getClass(), "Canceled fling animation"); } } @Override public void onRevertStartOvershoot() { } @Override public void onRevertEndOvershoot() { } @Override public void onSwipe(@NonNull final TabItem tabItem, final float distance) { } @Override public void onSwipeEnded(@NonNull final TabItem tabItem, final boolean remove, final float velocity) { } @Override public void onPulledDown() { } }