+ * Note: Will be immediately called for the currently selected tab + * once when set. + * + * @param listener a listener for monitoring changes in tab selection. + */ + public void setOnTabSelectListener(@NonNull OnTabSelectListener listener) { + setOnTabSelectListener(listener, true); + } + + /** + * Set a listener that gets fired when the selected {@link BottomBarTab} changes. + *
+ * If {@code shouldFireInitially} is set to false, this listener isn't fired straight away
+ * it's set, but you'll get all events normally for consecutive tab selection changes.
+ *
+ * @param listener a listener for monitoring changes in tab selection.
+ * @param shouldFireInitially whether the listener should be fired the first time it's set.
+ */
+ public void setOnTabSelectListener(@NonNull OnTabSelectListener listener, boolean shouldFireInitially) {
+ onTabSelectListener = listener;
+
+ if (shouldFireInitially && getTabCount() > 0) {
+ listener.onTabSelected(getCurrentTabId());
+ }
+ }
+
+ /**
+ * Removes the current {@link OnTabSelectListener} listener
+ */
+ public void removeOnTabSelectListener() {
+ onTabSelectListener = null;
+ }
+
+ /**
+ * Set a listener that gets fired when a currently selected {@link BottomBarTab} is clicked.
+ *
+ * @param listener a listener for handling tab reselections.
+ */
+ public void setOnTabReselectListener(@NonNull OnTabReselectListener listener) {
+ onTabReselectListener = listener;
+ }
+
+ /**
+ * Removes the current {@link OnTabReselectListener} listener
+ */
+ public void removeOnTabReselectListener() {
+ onTabReselectListener = null;
+ }
+
+ /**
+ * Set the default selected to be the tab with the corresponding tab id.
+ * By default, the first tab in the container is the default tab.
+ */
+ public void setDefaultTab(@IdRes int defaultTabId) {
+ int defaultTabPosition = findPositionForTabWithId(defaultTabId);
+ setDefaultTabPosition(defaultTabPosition);
+ }
+
+ /**
+ * Sets the default tab for this BottomBar that is shown until the user changes
+ * the selection.
+ *
+ * @param defaultTabPosition the default tab position.
+ */
+ public void setDefaultTabPosition(int defaultTabPosition) {
+ if (isComingFromRestoredState) return;
+
+ selectTabAtPosition(defaultTabPosition);
+ }
+
+ /**
+ * Select the tab with the corresponding id.
+ */
+ public void selectTabWithId(@IdRes int tabResId) {
+ int tabPosition = findPositionForTabWithId(tabResId);
+ selectTabAtPosition(tabPosition);
+ }
+
+ /**
+ * Select a tab at the specified position.
+ *
+ * @param position the position to select.
+ */
+ public void selectTabAtPosition(int position) {
+ selectTabAtPosition(position, false);
+ }
+
+ /**
+ * Select a tab at the specified position.
+ *
+ * @param position the position to select.
+ * @param animate should the tab change be animated or not.
+ */
+ public void selectTabAtPosition(int position, boolean animate) {
+ if (position > getTabCount() - 1 || position < 0) {
+ throw new IndexOutOfBoundsException("Can't select tab at position " +
+ position + ". This BottomBar has no items at that position.");
+ }
+
+ BottomBarTab oldTab = getCurrentTab();
+ BottomBarTab newTab = getTabAtPosition(position);
+
+ oldTab.deselect(animate);
+ newTab.select(animate);
+
+ updateSelectedTab(position);
+ shiftingMagic(oldTab, newTab, animate);
+ handleBackgroundColorChange(newTab, animate);
+ }
+
+ public int getTabCount() {
+ return tabContainer.getChildCount();
+ }
+
+ /**
+ * Get the currently selected tab.
+ */
+ public BottomBarTab getCurrentTab() {
+ return getTabAtPosition(getCurrentTabPosition());
+ }
+
+ /**
+ * Get the tab at the specified position.
+ */
+ public BottomBarTab getTabAtPosition(int position) {
+ View child = tabContainer.getChildAt(position);
+
+ if (child instanceof BadgeContainer) {
+ return findTabInLayout((BadgeContainer) child);
+ }
+
+ return (BottomBarTab) child;
+ }
+
+ /**
+ * Get the resource id for the currently selected tab.
+ */
+ @IdRes
+ public int getCurrentTabId() {
+ return getCurrentTab().getId();
+ }
+
+ /**
+ * Get the currently selected tab position.
+ */
+ public int getCurrentTabPosition() {
+ return currentTabPosition;
+ }
+
+ /**
+ * Find the tabs' position in the container by id.
+ */
+ public int findPositionForTabWithId(@IdRes int tabId) {
+ return getTabWithId(tabId).getIndexInTabContainer();
+ }
+
+ /**
+ * Find a BottomBarTab with the corresponding id.
+ */
+ public BottomBarTab getTabWithId(@IdRes int tabId) {
+ return (BottomBarTab) tabContainer.findViewById(tabId);
+ }
+
+ /**
+ * Controls whether the long pressed tab title should be displayed in
+ * a helpful Toast if the title is not currently visible.
+ *
+ * @param enabled true if toasts should be shown to indicate the title
+ * of a long pressed tab, false otherwise.
+ */
+ public void setLongPressHintsEnabled(boolean enabled) {
+ longPressHintsEnabled = enabled;
+ }
+
+ /**
+ * Set alpha value used for inactive BottomBarTabs.
+ */
+ public void setInActiveTabAlpha(float alpha) {
+ inActiveTabAlpha = alpha;
+
+ batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() {
+ @Override
+ public void update(BottomBarTab tab) {
+ tab.setInActiveAlpha(inActiveTabAlpha);
+ }
+ });
+ }
+
+ /**
+ * Set alpha value used for active BottomBarTabs.
+ */
+ public void setActiveTabAlpha(float alpha) {
+ activeTabAlpha = alpha;
+
+ batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() {
+ @Override
+ public void update(BottomBarTab tab) {
+ tab.setActiveAlpha(activeTabAlpha);
+ }
+ });
+ }
+
+ public void setInActiveTabColor(@ColorInt int color) {
+ inActiveTabColor = color;
+
+ batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() {
+ @Override
+ public void update(BottomBarTab tab) {
+ tab.setInActiveColor(inActiveTabColor);
+ }
+ });
+ }
+
+ /**
+ * Set active color used for selected BottomBarTabs.
+ */
+ public void setActiveTabColor(@ColorInt int color) {
+ activeTabColor = color;
+
+ batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() {
+ @Override
+ public void update(BottomBarTab tab) {
+ tab.setActiveColor(activeTabColor);
+ }
+ });
+ }
+
+ /**
+ * Set background color for the badge.
+ */
+ public void setBadgeBackgroundColor(@ColorInt int color) {
+ badgeBackgroundColor = color;
+
+ batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() {
+ @Override
+ public void update(BottomBarTab tab) {
+ tab.setBadgeBackgroundColor(badgeBackgroundColor);
+ }
+ });
+ }
+
+ /**
+ * Controls whether the badge (if any) for active tabs
+ * should be hidden or not.
+ */
+ public void setBadgesHideWhenActive(final boolean hideWhenSelected) {
+ hideBadgeWhenActive = hideWhenSelected;
+ batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() {
+ @Override
+ public void update(BottomBarTab tab) {
+ tab.setBadgeHidesWhenActive(hideWhenSelected);
+ }
+ });
+ }
+
+ /**
+ * Set custom text apperance for all BottomBarTabs.
+ */
+ public void setTabTitleTextAppearance(int textAppearance) {
+ titleTextAppearance = textAppearance;
+
+ batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() {
+ @Override
+ public void update(BottomBarTab tab) {
+ tab.setTitleTextAppearance(titleTextAppearance);
+ }
+ });
+ }
+
+ /**
+ * Set a custom typeface for all tab's titles.
+ *
+ * @param fontPath path for your custom font file, such as fonts/MySuperDuperFont.ttf.
+ * In that case your font path would look like src/main/assets/fonts/MySuperDuperFont.ttf,
+ * but you only need to provide fonts/MySuperDuperFont.ttf, as the asset folder
+ * will be auto-filled for you.
+ */
+ public void setTabTitleTypeface(String fontPath) {
+ Typeface actualTypeface = getTypeFaceFromAsset(fontPath);
+ setTabTitleTypeface(actualTypeface);
+ }
+
+ /**
+ * Set a custom typeface for all tab's titles.
+ */
+ public void setTabTitleTypeface(Typeface typeface) {
+ titleTypeFace = typeface;
+
+ batchPropertyApplier.applyToAllTabs(new BatchTabPropertyApplier.TabPropertyUpdater() {
+ @Override
+ public void update(BottomBarTab tab) {
+ tab.setTitleTypeface(titleTypeFace);
+ }
+ });
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (changed) {
+ if (!isTabletMode) {
+ resizeTabsToCorrectSizes(currentTabs);
+ }
+
+ updateTitleBottomPadding();
+
+ if (isShy()) {
+ initializeShyBehavior();
+ }
+
+ if (drawUnderNav()) {
+ resizeForDrawingUnderNavbar();
+ }
+ }
+ }
+
+ private void updateTitleBottomPadding() {
+ if (isIconsOnlyMode()) {
+ return;
+ }
+
+ int tabCount = getTabCount();
+
+ if (tabContainer == null || tabCount == 0 || !isShiftingMode()) {
+ return;
+ }
+
+ for (int i = 0; i < tabCount; i++) {
+ BottomBarTab tab = getTabAtPosition(i);
+ TextView title = tab.getTitleView();
+
+ if (title == null) {
+ continue;
+ }
+
+ int baseline = title.getBaseline();
+ int height = title.getHeight();
+ int paddingInsideTitle = height - baseline;
+ int missingPadding = tenDp - paddingInsideTitle;
+
+ if (missingPadding > 0) {
+ title.setPadding(title.getPaddingLeft(), title.getPaddingTop(),
+ title.getPaddingRight(), missingPadding + title.getPaddingBottom());
+ }
+ }
+ }
+
+ private void initializeShyBehavior() {
+ ViewParent parent = getParent();
+
+ boolean hasAbusiveParent = parent != null
+ && parent instanceof CoordinatorLayout;
+
+ if (!hasAbusiveParent) {
+ throw new RuntimeException("In order to have shy behavior, the " +
+ "BottomBar must be a direct child of a CoordinatorLayout.");
+ }
+
+ if (!shyHeightAlreadyCalculated) {
+ int height = getHeight();
+
+ if (height != 0) {
+ updateShyHeight(height);
+ getShySettings().shyHeightCalculated();
+ shyHeightAlreadyCalculated = true;
+ }
+ }
+ }
+
+ private void updateShyHeight(int height) {
+ ((CoordinatorLayout.LayoutParams) getLayoutParams())
+ .setBehavior(new BottomNavigationBehavior(height, 0, false));
+ }
+
+ private void resizeForDrawingUnderNavbar() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ int currentHeight = getHeight();
+
+ if (currentHeight != 0 && !navBarAccountedHeightCalculated) {
+ navBarAccountedHeightCalculated = true;
+ tabContainer.getLayoutParams().height = currentHeight;
+
+ int navbarHeight = NavbarUtils.INSTANCE.getNavbarHeight(getContext());
+ int finalHeight = currentHeight + navbarHeight;
+ getLayoutParams().height = finalHeight;
+
+ if (isShy()) {
+ updateShyHeight(finalHeight);
+ }
+ }
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Bundle bundle = saveState();
+ bundle.putParcelable("superstate", super.onSaveInstanceState());
+ return bundle;
+ }
+
+ @VisibleForTesting
+ Bundle saveState() {
+ Bundle outState = new Bundle();
+ outState.putInt(STATE_CURRENT_SELECTED_TAB, currentTabPosition);
+
+ return outState;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof Bundle) {
+ Bundle bundle = (Bundle) state;
+ restoreState(bundle);
+
+ state = bundle.getParcelable("superstate");
+ }
+ super.onRestoreInstanceState(state);
+ }
+
+ @VisibleForTesting
+ void restoreState(Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ isComingFromRestoredState = true;
+ ignoreTabReselectionListener = true;
+
+ int restoredPosition = savedInstanceState.getInt(STATE_CURRENT_SELECTED_TAB, currentTabPosition);
+ selectTabAtPosition(restoredPosition, false);
+ }
+ }
+
+ @Override
+ public void onClick(View target) {
+ if (!(target instanceof BottomBarTab)) return;
+ handleClick((BottomBarTab) target);
+ }
+
+ @Override
+ public boolean onLongClick(View target) {
+ return !(target instanceof BottomBarTab) || handleLongClick((BottomBarTab) target);
+ }
+
+ private BottomBarTab findTabInLayout(ViewGroup child) {
+ for (int i = 0; i < child.getChildCount(); i++) {
+ View candidate = child.getChildAt(i);
+
+ if (candidate instanceof BottomBarTab) {
+ return (BottomBarTab) candidate;
+ }
+ }
+
+ return null;
+ }
+
+ private void handleClick(BottomBarTab newTab) {
+ BottomBarTab oldTab = getCurrentTab();
+
+ if (tabSelectionInterceptor != null
+ && tabSelectionInterceptor.shouldInterceptTabSelection(oldTab.getId(), newTab.getId())) {
+ return;
+ }
+
+ oldTab.deselect(true);
+ newTab.select(true);
+
+ shiftingMagic(oldTab, newTab, true);
+ handleBackgroundColorChange(newTab, true);
+ updateSelectedTab(newTab.getIndexInTabContainer());
+ }
+
+ private boolean handleLongClick(BottomBarTab longClickedTab) {
+ boolean areInactiveTitlesHidden = isShiftingMode() || isTabletMode;
+ boolean isClickedTitleHidden = !longClickedTab.isActive();
+ boolean shouldShowHint = areInactiveTitlesHidden
+ && isClickedTitleHidden
+ && longPressHintsEnabled;
+
+ if (shouldShowHint) {
+ Toast.makeText(getContext(), longClickedTab.getTitle(), Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ return true;
+ }
+
+ private void updateSelectedTab(int newPosition) {
+ int newTabId = getTabAtPosition(newPosition).getId();
+
+ if (newPosition != currentTabPosition) {
+ if (onTabSelectListener != null) {
+ onTabSelectListener.onTabSelected(newTabId);
+ }
+ } else if (onTabReselectListener != null && !ignoreTabReselectionListener) {
+ onTabReselectListener.onTabReSelected(newTabId);
+ }
+
+ currentTabPosition = newPosition;
+
+ if (ignoreTabReselectionListener) {
+ ignoreTabReselectionListener = false;
+ }
+ }
+
+ private void shiftingMagic(BottomBarTab oldTab, BottomBarTab newTab, boolean animate) {
+ if (isShiftingMode()) {
+ oldTab.updateWidth(inActiveShiftingItemWidth, animate);
+ newTab.updateWidth(activeShiftingItemWidth, animate);
+ }
+ }
+
+ private void handleBackgroundColorChange(BottomBarTab tab, boolean animate) {
+ int newColor = tab.getBarColorWhenSelected();
+
+ if (currentBackgroundColor == newColor) {
+ return;
+ }
+
+ if (!animate) {
+ outerContainer.setBackgroundColor(newColor);
+ return;
+ }
+
+ View clickedView = tab;
+
+ if (tab.hasActiveBadge()) {
+ clickedView = tab.getOuterView();
+ }
+
+ animateBGColorChange(clickedView, newColor);
+ currentBackgroundColor = newColor;
+ }
+
+ private void animateBGColorChange(View clickedView, final int newColor) {
+ prepareForBackgroundColorAnimation(newColor);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ if (!outerContainer.isAttachedToWindow()) {
+ return;
+ }
+
+ backgroundCircularRevealAnimation(clickedView, newColor);
+ } else {
+ backgroundCrossfadeAnimation(newColor);
+ }
+ }
+
+ private void prepareForBackgroundColorAnimation(int newColor) {
+ outerContainer.clearAnimation();
+ backgroundOverlay.clearAnimation();
+
+ backgroundOverlay.setBackgroundColor(newColor);
+ backgroundOverlay.setVisibility(View.VISIBLE);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private void backgroundCircularRevealAnimation(View clickedView, final int newColor) {
+ int centerX = (int) (ViewCompat.getX(clickedView) + (clickedView.getMeasuredWidth() / 2));
+ int yOffset = isTabletMode ? (int) ViewCompat.getY(clickedView) : 0;
+ int centerY = yOffset + clickedView.getMeasuredHeight() / 2;
+ int startRadius = 0;
+ int finalRadius = isTabletMode ? outerContainer.getHeight() : outerContainer.getWidth();
+
+ Animator animator = ViewAnimationUtils.createCircularReveal(
+ backgroundOverlay,
+ centerX,
+ centerY,
+ startRadius,
+ finalRadius
+ );
+
+ if (isTabletMode) {
+ animator.setDuration(500);
+ }
+
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ onEnd();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ onEnd();
+ }
+
+ private void onEnd() {
+ outerContainer.setBackgroundColor(newColor);
+ backgroundOverlay.setVisibility(View.INVISIBLE);
+ ViewCompat.setAlpha(backgroundOverlay, 1);
+ }
+ });
+
+ animator.start();
+ }
+
+ private void backgroundCrossfadeAnimation(final int newColor) {
+ ViewCompat.setAlpha(backgroundOverlay, 0);
+ ViewCompat.animate(backgroundOverlay)
+ .alpha(1)
+ .setListener(new ViewPropertyAnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(View view) {
+ onEnd();
+ }
+
+ @Override
+ public void onAnimationCancel(View view) {
+ onEnd();
+ }
+
+ private void onEnd() {
+ outerContainer.setBackgroundColor(newColor);
+ backgroundOverlay.setVisibility(View.INVISIBLE);
+ ViewCompat.setAlpha(backgroundOverlay, 1);
+ }
+ })
+ .start();
+ }
+}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarBadge.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarBadge.java
new file mode 100644
index 000000000..d7aa57294
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarBadge.java
@@ -0,0 +1,172 @@
+package com.habitrpg.android.habitica.ui.views.bottombar;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.os.Build;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.TextView;
+
+import com.habitrpg.android.habitica.R;
+
+import androidx.appcompat.widget.AppCompatImageView;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.view.ViewCompat;
+
+/*
+ * BottomBar library for Android
+ * Copyright (c) 2016 Iiro Krankka (http://github.com/roughike).
+ *
+ * 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.
+ */
+class BottomBarBadge extends AppCompatTextView {
+ private int count;
+ private boolean isVisible = false;
+
+ BottomBarBadge(Context context) {
+ super(context);
+ }
+
+ /**
+ * Set the unread / new item / whatever count for this Badge.
+ *
+ * @param count the value this Badge should show.
+ */
+ void setCount(int count) {
+ this.count = count;
+ setText(String.valueOf(count));
+ }
+
+ /**
+ * Get the currently showing count for this Badge.
+ *
+ * @return current count for the Badge.
+ */
+ int getCount() {
+ return count;
+ }
+
+ /**
+ * Shows the badge with a neat little scale animation.
+ */
+ void show() {
+ isVisible = true;
+ ViewCompat.animate(this)
+ .setDuration(150)
+ .alpha(1)
+ .scaleX(1)
+ .scaleY(1)
+ .start();
+ }
+
+ /**
+ * Hides the badge with a neat little scale animation.
+ */
+ void hide() {
+ isVisible = false;
+ ViewCompat.animate(this)
+ .setDuration(150)
+ .alpha(0)
+ .scaleX(0)
+ .scaleY(0)
+ .start();
+ }
+
+ /**
+ * Is this badge currently visible?
+ *
+ * @return true is this badge is visible, otherwise false.
+ */
+ boolean isVisible() {
+ return isVisible;
+ }
+
+ void attachToTab(BottomBarTab tab, int backgroundColor) {
+ ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ setLayoutParams(params);
+ setGravity(Gravity.CENTER);
+ MiscUtils.setTextAppearance(this, R.style.BB_BottomBarBadge_Text);
+
+ setColoredCircleBackground(backgroundColor);
+ wrapTabAndBadgeInSameContainer(tab);
+ }
+
+ void setColoredCircleBackground(int circleColor) {
+ int innerPadding = MiscUtils.dpToPixel(getContext(), 1);
+ ShapeDrawable backgroundCircle = BadgeCircle.make(innerPadding * 3, circleColor);
+ setPadding(innerPadding, innerPadding, innerPadding, innerPadding);
+ setBackgroundCompat(backgroundCircle);
+ }
+
+ private void wrapTabAndBadgeInSameContainer(final BottomBarTab tab) {
+ ViewGroup tabContainer = (ViewGroup) tab.getParent();
+ tabContainer.removeView(tab);
+
+ final BadgeContainer badgeContainer = new BadgeContainer(getContext());
+ badgeContainer.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ badgeContainer.addView(tab);
+ badgeContainer.addView(this);
+
+ tabContainer.addView(badgeContainer, tab.getIndexInTabContainer());
+
+ badgeContainer.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onGlobalLayout() {
+ badgeContainer.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ adjustPositionAndSize(tab);
+ }
+ });
+ }
+
+ void removeFromTab(BottomBarTab tab) {
+ BadgeContainer badgeAndTabContainer = (BadgeContainer) getParent();
+ ViewGroup originalTabContainer = (ViewGroup) badgeAndTabContainer.getParent();
+
+ badgeAndTabContainer.removeView(tab);
+ originalTabContainer.removeView(badgeAndTabContainer);
+ originalTabContainer.addView(tab, tab.getIndexInTabContainer());
+ }
+
+ void adjustPositionAndSize(BottomBarTab tab) {
+ AppCompatImageView iconView = tab.getIconView();
+ ViewGroup.LayoutParams params = getLayoutParams();
+
+ int size = Math.max(getWidth(), getHeight());
+ float xOffset = (float) (iconView.getWidth() / 1.25);
+
+ setX(iconView.getX() + xOffset);
+ setTranslationY(10);
+
+ if (params.width != size || params.height != size) {
+ params.width = size;
+ params.height = size;
+ setLayoutParams(params);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void setBackgroundCompat(Drawable background) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ setBackground(background);
+ } else {
+ setBackgroundDrawable(background);
+ }
+ }
+}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarTab.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarTab.java
new file mode 100644
index 000000000..d3f8d73f4
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomBarTab.java
@@ -0,0 +1,730 @@
+package com.habitrpg.android.habitica.ui.views.bottombar;
+
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcelable;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.widget.AppCompatImageView;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.ViewPropertyAnimatorCompat;
+
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.habitrpg.android.habitica.R;
+
+/*
+ * BottomBar library for Android
+ * Copyright (c) 2016 Iiro Krankka (http://github.com/roughike).
+ *
+ * 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.
+ */
+public class BottomBarTab extends LinearLayout {
+ @VisibleForTesting
+ static final String STATE_BADGE_COUNT = "STATE_BADGE_COUNT_FOR_TAB_";
+
+ private static final long ANIMATION_DURATION = 150;
+ private static final float ACTIVE_TITLE_SCALE = 1;
+ private static final float INACTIVE_FIXED_TITLE_SCALE = 0.86f;
+ private static final float ACTIVE_SHIFTING_TITLELESS_ICON_SCALE = 1.24f;
+ private static final float INACTIVE_SHIFTING_TITLELESS_ICON_SCALE = 1f;
+
+ private final int sixDps;
+ private final int eightDps;
+ private final int sixteenDps;
+
+ @VisibleForTesting
+ BottomBarBadge badge;
+
+ private Type type = Type.FIXED;
+ private boolean isTitleless;
+ private int iconResId;
+ private String title;
+ private float inActiveAlpha;
+ private float activeAlpha;
+ private int inActiveColor;
+ private int activeColor;
+ private int barColorWhenSelected;
+ private int badgeBackgroundColor;
+ private boolean badgeHidesWhenActive;
+ private AppCompatImageView iconView;
+ private TextView titleView;
+ private boolean isActive;
+ private int indexInContainer;
+ private int titleTextAppearanceResId;
+ private Typeface titleTypeFace;
+
+ BottomBarTab(Context context) {
+ super(context);
+
+ sixDps = MiscUtils.dpToPixel(context, 6);
+ eightDps = MiscUtils.dpToPixel(context, 8);
+ sixteenDps = MiscUtils.dpToPixel(context, 16);
+ }
+
+ void setConfig(@NonNull Config config) {
+ setInActiveAlpha(config.inActiveTabAlpha);
+ setActiveAlpha(config.activeTabAlpha);
+ setInActiveColor(config.inActiveTabColor);
+ setActiveColor(config.activeTabColor);
+ setBarColorWhenSelected(config.barColorWhenSelected);
+ setBadgeBackgroundColor(config.badgeBackgroundColor);
+ setBadgeHidesWhenActive(config.badgeHidesWhenSelected);
+ setTitleTextAppearance(config.titleTextAppearance);
+ setTitleTypeface(config.titleTypeFace);
+ }
+
+ void prepareLayout() {
+ inflate(getContext(), getLayoutResource(), this);
+ setOrientation(VERTICAL);
+ setGravity(isTitleless? Gravity.CENTER : Gravity.CENTER_HORIZONTAL);
+ setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+ setBackgroundResource(MiscUtils.getDrawableRes(getContext(), R.attr.selectableItemBackgroundBorderless));
+
+ iconView = (AppCompatImageView) findViewById(R.id.bb_bottom_bar_icon);
+ iconView.setImageResource(iconResId);
+
+ if (type != Type.TABLET && !isTitleless) {
+ titleView = (TextView) findViewById(R.id.bb_bottom_bar_title);
+ titleView.setVisibility(VISIBLE);
+
+ if (type == Type.SHIFTING) {
+ findViewById(R.id.spacer).setVisibility(VISIBLE);
+ }
+
+ updateTitle();
+ }
+
+ updateCustomTextAppearance();
+ updateCustomTypeface();
+ }
+
+ @VisibleForTesting
+ int getLayoutResource() {
+ int layoutResource;
+ switch (type) {
+ case FIXED:
+ layoutResource = R.layout.bb_bottom_bar_item_fixed;
+ break;
+ case SHIFTING:
+ layoutResource = R.layout.bb_bottom_bar_item_shifting;
+ break;
+ case TABLET:
+ layoutResource = R.layout.bb_bottom_bar_item_fixed_tablet;
+ break;
+ default:
+ // should never happen
+ throw new RuntimeException("Unknown BottomBarTab type.");
+ }
+ return layoutResource;
+ }
+
+ private void updateTitle() {
+ if (titleView != null) {
+ titleView.setText(title);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void updateCustomTextAppearance() {
+ if (titleView == null || titleTextAppearanceResId == 0) {
+ return;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ titleView.setTextAppearance(titleTextAppearanceResId);
+ } else {
+ titleView.setTextAppearance(getContext(), titleTextAppearanceResId);
+ }
+
+ titleView.setTag(R.id.bb_bottom_bar_appearance_id, titleTextAppearanceResId);
+ }
+
+ private void updateCustomTypeface() {
+ if (titleTypeFace != null && titleView != null) {
+ titleView.setTypeface(titleTypeFace);
+ }
+ }
+
+ Type getType() {
+ return type;
+ }
+
+ void setType(Type type) {
+ this.type = type;
+ }
+
+ boolean isTitleless() {
+ return isTitleless;
+ }
+
+ void setIsTitleless(boolean isTitleless) {
+ if (isTitleless && getIconResId() == 0) {
+ throw new IllegalStateException("This tab is supposed to be " +
+ "icon only, yet it has no icon specified. Index in " +
+ "container: " + getIndexInTabContainer());
+ }
+
+ this.isTitleless = isTitleless;
+ }
+
+ public ViewGroup getOuterView() {
+ return (ViewGroup) getParent();
+ }
+
+ AppCompatImageView getIconView() {
+ return iconView;
+ }
+
+ int getIconResId() {
+ return iconResId;
+ }
+
+ void setIconResId(int iconResId) {
+ this.iconResId = iconResId;
+ }
+
+ TextView getTitleView() {
+ return titleView;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ updateTitle();
+ }
+
+ public float getInActiveAlpha() {
+ return inActiveAlpha;
+ }
+
+ public void setInActiveAlpha(float inActiveAlpha) {
+ this.inActiveAlpha = inActiveAlpha;
+
+ if (!isActive) {
+ setAlphas(inActiveAlpha);
+ }
+ }
+
+ public float getActiveAlpha() {
+ return activeAlpha;
+ }
+
+ public void setActiveAlpha(float activeAlpha) {
+ this.activeAlpha = activeAlpha;
+
+ if (isActive) {
+ setAlphas(activeAlpha);
+ }
+ }
+
+ public int getInActiveColor() {
+ return inActiveColor;
+ }
+
+ public void setInActiveColor(int inActiveColor) {
+ this.inActiveColor = inActiveColor;
+
+ if (!isActive) {
+ setColors(inActiveColor);
+ }
+ }
+
+ public int getActiveColor() {
+ return activeColor;
+ }
+
+ public void setActiveColor(int activeIconColor) {
+ this.activeColor = activeIconColor;
+
+ if (isActive) {
+ setColors(activeColor);
+ }
+ }
+
+ public int getBarColorWhenSelected() {
+ return barColorWhenSelected;
+ }
+
+ public void setBarColorWhenSelected(int barColorWhenSelected) {
+ this.barColorWhenSelected = barColorWhenSelected;
+ }
+
+ public int getBadgeBackgroundColor() {
+ return badgeBackgroundColor;
+ }
+
+ public void setBadgeBackgroundColor(int badgeBackgroundColor) {
+ this.badgeBackgroundColor = badgeBackgroundColor;
+
+ if (badge != null) {
+ badge.setColoredCircleBackground(badgeBackgroundColor);
+ }
+ }
+
+ public boolean getBadgeHidesWhenActive() {
+ return badgeHidesWhenActive;
+ }
+
+ public void setBadgeHidesWhenActive(boolean hideWhenActive) {
+ this.badgeHidesWhenActive = hideWhenActive;
+ }
+
+ int getCurrentDisplayedIconColor() {
+ Object tag = iconView.getTag(R.id.bb_bottom_bar_color_id);
+
+ if (tag instanceof Integer) {
+ return (int) tag;
+ }
+
+ return 0;
+ }
+
+ int getCurrentDisplayedTitleColor() {
+ if (titleView != null) {
+ return titleView.getCurrentTextColor();
+ }
+
+ return 0;
+ }
+
+ int getCurrentDisplayedTextAppearance() {
+ Object tag = titleView.getTag(R.id.bb_bottom_bar_appearance_id);
+
+ if (titleView != null && tag instanceof Integer) {
+ return (int) tag;
+ }
+
+ return 0;
+ }
+
+ public void setBadgeCount(int count) {
+ if (count <= 0) {
+ if (badge != null) {
+ badge.removeFromTab(this);
+ badge = null;
+ }
+
+ return;
+ }
+
+ if (badge == null) {
+ badge = new BottomBarBadge(getContext());
+ badge.attachToTab(this, badgeBackgroundColor);
+ }
+
+ badge.setCount(count);
+
+ if (isActive && badgeHidesWhenActive) {
+ badge.hide();
+ }
+ }
+
+ public void removeBadge() {
+ setBadgeCount(0);
+ }
+
+ boolean isActive() {
+ return isActive;
+ }
+
+ boolean hasActiveBadge() {
+ return badge != null;
+ }
+
+ int getIndexInTabContainer() {
+ return indexInContainer;
+ }
+
+ void setIndexInContainer(int indexInContainer) {
+ this.indexInContainer = indexInContainer;
+ }
+
+ void setIconTint(int tint) {
+ iconView.setColorFilter(tint);
+ }
+
+ public int getTitleTextAppearance() {
+ return titleTextAppearanceResId;
+ }
+
+ @SuppressWarnings("deprecation")
+ void setTitleTextAppearance(int resId) {
+ this.titleTextAppearanceResId = resId;
+ updateCustomTextAppearance();
+ }
+
+ public void setTitleTypeface(Typeface typeface) {
+ this.titleTypeFace = typeface;
+ updateCustomTypeface();
+ }
+
+ public Typeface getTitleTypeFace() {
+ return titleTypeFace;
+ }
+
+ void select(boolean animate) {
+ isActive = true;
+
+ if (animate) {
+ animateIcon(activeAlpha, ACTIVE_SHIFTING_TITLELESS_ICON_SCALE);
+ animateTitle(sixDps, ACTIVE_TITLE_SCALE, activeAlpha);
+ animateColors(inActiveColor, activeColor);
+ } else {
+ setTitleScale(ACTIVE_TITLE_SCALE);
+ setTopPadding(sixDps);
+ setIconScale(ACTIVE_SHIFTING_TITLELESS_ICON_SCALE);
+ setColors(activeColor);
+ setAlphas(activeAlpha);
+ }
+
+ setSelected(true);
+
+ if (badge != null && badgeHidesWhenActive) {
+ badge.hide();
+ }
+ }
+
+ void deselect(boolean animate) {
+ isActive = false;
+
+ boolean isShifting = type == Type.SHIFTING;
+
+ float titleScale = isShifting ? 0 : INACTIVE_FIXED_TITLE_SCALE;
+ int iconPaddingTop = isShifting ? sixteenDps : eightDps;
+
+ if (animate) {
+ animateTitle(iconPaddingTop, titleScale, inActiveAlpha);
+ animateIcon(inActiveAlpha, INACTIVE_SHIFTING_TITLELESS_ICON_SCALE);
+ animateColors(activeColor, inActiveColor);
+ } else {
+ setTitleScale(titleScale);
+ setTopPadding(iconPaddingTop);
+ setIconScale(INACTIVE_SHIFTING_TITLELESS_ICON_SCALE);
+ setColors(inActiveColor);
+ setAlphas(inActiveAlpha);
+ }
+
+ setSelected(false);
+
+ if (!isShifting && badge != null && !badge.isVisible()) {
+ badge.show();
+ }
+ }
+
+ private void animateColors(int previousColor, int color) {
+ ValueAnimator anim = new ValueAnimator();
+ anim.setIntValues(previousColor, color);
+ anim.setEvaluator(new ArgbEvaluator());
+ anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ setColors((Integer) valueAnimator.getAnimatedValue());
+ }
+ });
+
+ anim.setDuration(150);
+ anim.start();
+ }
+
+ private void setColors(int color) {
+ if (iconView != null) {
+ iconView.setColorFilter(color);
+ iconView.setTag(R.id.bb_bottom_bar_color_id, color);
+ }
+
+ if (titleView != null) {
+ titleView.setTextColor(color);
+ }
+ }
+
+ private void setAlphas(float alpha) {
+ if (iconView != null) {
+ ViewCompat.setAlpha(iconView, alpha);
+ }
+
+ if (titleView != null) {
+ ViewCompat.setAlpha(titleView, alpha);
+ }
+ }
+
+ void updateWidth(float endWidth, boolean animated) {
+ if (!animated) {
+ getLayoutParams().width = (int) endWidth;
+
+ if (!isActive && badge != null) {
+ badge.adjustPositionAndSize(this);
+ badge.show();
+ }
+ return;
+ }
+
+ float start = getWidth();
+
+ ValueAnimator animator = ValueAnimator.ofFloat(start, endWidth);
+ animator.setDuration(150);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ ViewGroup.LayoutParams params = getLayoutParams();
+ if (params == null) return;
+
+ params.width = Math.round((float) animator.getAnimatedValue());
+ setLayoutParams(params);
+ }
+ });
+
+ // Workaround to avoid using faulty onAnimationEnd() listener
+ postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (!isActive && badge != null) {
+ clearAnimation();
+ badge.adjustPositionAndSize(BottomBarTab.this);
+ badge.show();
+ }
+ }
+ }, animator.getDuration());
+
+ animator.start();
+ }
+
+ private void updateBadgePosition() {
+ if (badge != null) {
+ badge.adjustPositionAndSize(this);
+ }
+ }
+
+ private void setTopPaddingAnimated(int start, int end) {
+ if (type == Type.TABLET || isTitleless) {
+ return;
+ }
+
+ ValueAnimator paddingAnimator = ValueAnimator.ofInt(start, end);
+ paddingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ iconView.setPadding(
+ iconView.getPaddingLeft(),
+ (Integer) animation.getAnimatedValue(),
+ iconView.getPaddingRight(),
+ iconView.getPaddingBottom()
+ );
+ }
+ });
+
+ paddingAnimator.setDuration(ANIMATION_DURATION);
+ paddingAnimator.start();
+ }
+
+ private void animateTitle(int padding, float scale, float alpha) {
+ if (type == Type.TABLET && isTitleless) {
+ return;
+ }
+
+ setTopPaddingAnimated(iconView.getPaddingTop(), padding);
+
+ ViewPropertyAnimatorCompat titleAnimator = ViewCompat.animate(titleView)
+ .setDuration(ANIMATION_DURATION)
+ .scaleX(scale)
+ .scaleY(scale);
+ titleAnimator.alpha(alpha);
+ titleAnimator.start();
+ }
+
+ private void animateIconScale(float scale) {
+ ViewCompat.animate(iconView)
+ .setDuration(ANIMATION_DURATION)
+ .scaleX(scale)
+ .scaleY(scale)
+ .start();
+ }
+
+ private void animateIcon(float alpha, float scale) {
+ ViewCompat.animate(iconView)
+ .setDuration(ANIMATION_DURATION)
+ .alpha(alpha)
+ .start();
+
+ if (isTitleless && type == Type.SHIFTING) {
+ animateIconScale(scale);
+ }
+ }
+
+ private void setTopPadding(int topPadding) {
+ if (type == Type.TABLET || isTitleless) {
+ return;
+ }
+
+ iconView.setPadding(
+ iconView.getPaddingLeft(),
+ topPadding,
+ iconView.getPaddingRight(),
+ iconView.getPaddingBottom()
+ );
+ }
+
+ private void setTitleScale(float scale) {
+ if (type == Type.TABLET || isTitleless) {
+ return;
+ }
+
+ ViewCompat.setScaleX(titleView, scale);
+ ViewCompat.setScaleY(titleView, scale);
+ }
+
+ private void setIconScale(float scale) {
+ if (isTitleless && type == Type.SHIFTING) {
+ ViewCompat.setScaleX(iconView, scale);
+ ViewCompat.setScaleY(iconView, scale);
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ if (badge != null) {
+ Bundle bundle = saveState();
+ bundle.putParcelable("superstate", super.onSaveInstanceState());
+
+ return bundle;
+ }
+
+ return super.onSaveInstanceState();
+ }
+
+ @VisibleForTesting
+ Bundle saveState() {
+ Bundle outState = new Bundle();
+ outState.putInt(STATE_BADGE_COUNT + getIndexInTabContainer(), badge.getCount());
+
+ return outState;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof Bundle) {
+ Bundle bundle = (Bundle) state;
+ restoreState(bundle);
+
+ state = bundle.getParcelable("superstate");
+ }
+
+ super.onRestoreInstanceState(state);
+ }
+
+ @VisibleForTesting
+ void restoreState(Bundle savedInstanceState) {
+ int previousBadgeCount = savedInstanceState.getInt(STATE_BADGE_COUNT + getIndexInTabContainer());
+ setBadgeCount(previousBadgeCount);
+ }
+
+ enum Type {
+ FIXED, SHIFTING, TABLET
+ }
+
+ public static class Config {
+ private final float inActiveTabAlpha;
+ private final float activeTabAlpha;
+ private final int inActiveTabColor;
+ private final int activeTabColor;
+ private final int barColorWhenSelected;
+ private final int badgeBackgroundColor;
+ private final int titleTextAppearance;
+ private final Typeface titleTypeFace;
+ private boolean badgeHidesWhenSelected = true;
+
+ private Config(Builder builder) {
+ this.inActiveTabAlpha = builder.inActiveTabAlpha;
+ this.activeTabAlpha = builder.activeTabAlpha;
+ this.inActiveTabColor = builder.inActiveTabColor;
+ this.activeTabColor = builder.activeTabColor;
+ this.barColorWhenSelected = builder.barColorWhenSelected;
+ this.badgeBackgroundColor = builder.badgeBackgroundColor;
+ this.badgeHidesWhenSelected = builder.hidesBadgeWhenSelected;
+ this.titleTextAppearance = builder.titleTextAppearance;
+ this.titleTypeFace = builder.titleTypeFace;
+ }
+
+ public static class Builder {
+ private float inActiveTabAlpha;
+ private float activeTabAlpha;
+ private int inActiveTabColor;
+ private int activeTabColor;
+ private int barColorWhenSelected;
+ private int badgeBackgroundColor;
+ private boolean hidesBadgeWhenSelected = true;
+ private int titleTextAppearance;
+ private Typeface titleTypeFace;
+
+ public Builder inActiveTabAlpha(float alpha) {
+ this.inActiveTabAlpha = alpha;
+ return this;
+ }
+
+ public Builder activeTabAlpha(float alpha) {
+ this.activeTabAlpha = alpha;
+ return this;
+ }
+
+ public Builder inActiveTabColor(@ColorInt int color) {
+ this.inActiveTabColor = color;
+ return this;
+ }
+
+ public Builder activeTabColor(@ColorInt int color) {
+ this.activeTabColor = color;
+ return this;
+ }
+
+ public Builder barColorWhenSelected(@ColorInt int color) {
+ this.barColorWhenSelected = color;
+ return this;
+ }
+
+ public Builder badgeBackgroundColor(@ColorInt int color) {
+ this.badgeBackgroundColor = color;
+ return this;
+ }
+
+ public Builder hideBadgeWhenSelected(boolean hide) {
+ this.hidesBadgeWhenSelected = hide;
+ return this;
+ }
+
+ public Builder titleTextAppearance(int titleTextAppearance) {
+ this.titleTextAppearance = titleTextAppearance;
+ return this;
+ }
+
+ public Builder titleTypeFace(Typeface titleTypeFace) {
+ this.titleTypeFace = titleTypeFace;
+ return this;
+ }
+
+ public Config build() {
+ return new Config(this);
+ }
+ }
+ }
+}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomNavigationBehavior.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomNavigationBehavior.java
new file mode 100644
index 000000000..536139bac
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/BottomNavigationBehavior.java
@@ -0,0 +1,177 @@
+package com.habitrpg.android.habitica.ui.views.bottombar;
+
+import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.ViewPropertyAnimatorCompat;
+import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+
+import com.google.android.material.snackbar.Snackbar;
+
+/**
+ * Created by Nikola D. on 3/15/2016.
+ *
+ * Credit goes to Nikola Despotoski:
+ * https://github.com/NikolaDespotoski
+ */
+class BottomNavigationBehavior
+ * This listener is fired when the current {@link BottomBar} is about to change. This gives
+ * an opportunity to interrupt the {@link BottomBarTab} change.
+ *
+ * @param oldTabId the currently visible {@link BottomBarTab}
+ * @param newTabId the {@link BottomBarTab} that will be switched to
+ * @return true if you want to override/stop the tab change, false to continue as normal
+ */
+ boolean shouldInterceptTabSelection(@IdRes int oldTabId, @IdRes int newTabId);
+}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/VerticalScrollingBehavior.java b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/VerticalScrollingBehavior.java
new file mode 100644
index 000000000..ab1e5bea8
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/bottombar/VerticalScrollingBehavior.java
@@ -0,0 +1,150 @@
+package com.habitrpg.android.habitica.ui.views.bottombar;
+
+import android.content.Context;
+import android.os.Parcelable;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.core.view.WindowInsetsCompat;
+
+import android.util.AttributeSet;
+import android.view.View;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Created by Nikola D. on 11/22/2015.
+ *
+ * Credit goes to Nikola Despotoski:
+ * https://github.com/NikolaDespotoski
+ */
+abstract class VerticalScrollingBehavior