com.google.blockly.android.ToolboxFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.google.blockly.android.ToolboxFragment.java

Source

/*
 *  Copyright 2015 Google Inc. All Rights Reserved.
 *  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 com.google.blockly.android;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Bundle;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

import com.google.blockly.android.control.BlocklyController;
import com.google.blockly.android.ui.BlockGroup;
import com.google.blockly.android.ui.BlockListView;
import com.google.blockly.android.ui.CategoryTabs;
import com.google.blockly.android.ui.Rotation;
import com.google.blockly.android.ui.BlockDrawerFragment;
import com.google.blockly.android.ui.WorkspaceHelper;
import com.google.blockly.model.Block;
import com.google.blockly.model.ToolboxCategory;
import com.google.blockly.model.WorkspacePoint;
import com.google.blockly.utils.ColorUtils;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;

/**
 * A tabbed drawer UI to show the available {@link Block}s one can drag into the workspace. The
 * available blocks are divided into drawers by {@link ToolboxCategory}s. Assign the categories
 * using {@link #setContents(ToolboxCategory)}. This top level category can contain either a list of
 * blocks or a list of subcategories, but not both. If it has blocks, the {@code ToolboxFragment}
 * renders as a single tab/group. If it has subcategories, it will render each subcategory with its
 * own tab. If there is only one category (top level or subcategory) and the fragment is not
 * closeable, no tab will render with the list of blocks.
 * <p/>
 * The look of the {@code ToolboxFragment} is highly configurable. It inherits from
 * {@link BlockDrawerFragment}, including the {@code closeable} and {@code scrollOrientation}
 * attributes. Additionally, it supports configuration for which edge the tab is bound to, and
 * whether to rotate the tab labels when attached to vertical edges.
 * <p/>
 * For example:
 * <blockquote><pre>
 * &lt;fragment
 *     xmlns:android="http://schemas.android.com/apk/res/android"
 *     xmlns:blockly="http://schemas.android.com/apk/res-auto"
 *     android:name="com.google.blockly.ToolboxFragment"
 *     android:id="@+id/blockly_toolbox"
 *     android:layout_width="wrap_content"
 *     android:layout_height="match_parent"
 *     <b>blockly:closeable</b>="true"
 *     <b>blockly:scrollOrientation</b>="vertical"
 *     <b>blockly:tabEdge</b>="start"
 *     <b>blockly:rotateTabs</b>="true"
 *     /&gt;
 * </pre></blockquote>
 * <p/>
 * When {@code blockly:closeable} is true, the drawer of blocks will hide in the closed state. The
 * tabs will remain visible, providing the user a way to open the drawers.
 * <p/>
 * {@code blockly:scrollOrientation} can be either {@code horizontal} or {@code vertical}, and
 * affects only the block list. The tab scroll orientation is determined by the {@code tabEdge}.
 * <p/>
 * {@code blockly:rotateTabs} is a boolean. If true, the tab labels (text and background) will
 * rotate counter-clockwise on the left edge, and clockwise on the right edge. Top and bottom edge
 * tabs will never rotate.
 * <p/>
 * {@code blockly:tabEdge} takes the following values:
 * <table>
 *     <tr><th>XML attribute {@code blockly:tabEdge}</th><th>Fragment argument {@link #ARG_TAB_EDGE}</th></tr>
 *     <tr><td>{@code top}</td><td>{@link Gravity#TOP}</td><td>The top edge, with tabs justified to the start.  The default.</td></tr>
 *     <tr><td>{@code bottom}</td><td>{@link Gravity#BOTTOM}</td><td>The bottom edge, justified to the start.</td></tr>
 *     <tr><td>{@code left}</td><td>{@link Gravity#LEFT}</td><td>Left edge, justified to the top.</td></tr>
 *     <tr><td>{@code right}</td><td>{@link Gravity#RIGHT}</td><td>Right edge, justified to the top.</td></tr>
 *     <tr><td>{@code start}</td><td>{@link GravityCompat#START}</td><td>Starting edge (left in left-to-right), justified to the top.</td></tr>
 *     <tr><td>{@code end}</td><td>{@link GravityCompat#END}</td><td>Ending edge (right in left-to-right), justified to the top.</td></tr>
 * </table>
 * If there are more tabs than space allows, the tabs will be scrollable by dragging along the edge.
 * If this behavior is required, make sure the space is not draggable by other views, such as a
 * DrawerLayout.
 * <p/>
 * Developers can further customize the tab look by overriding {@link #onCreateLabelAdapter()} and
 * providing their own {@link CategoryTabs.LabelAdapter}.
 *
 * @attr ref com.google.blockly.R.styleable#BlockDrawerFragment_closeable
 * @attr ref com.google.blockly.R.styleable#BlockDrawerFragment_scrollOrientation
 * @attr ref com.google.blockly.R.styleable#ToolboxFragment_tabEdge
 * @attr ref com.google.blockly.R.styleable#ToolboxFragment_rotateTabs
 */
// TODO(#9): Attribute and arguments to set the tab background.
public class ToolboxFragment extends BlockDrawerFragment {
    private static final String TAG = "ToolboxFragment";

    protected static final float BLOCKS_BACKGROUND_LIGHTNESS = 0.5f;
    protected static final int DEFAULT_BLOCKS_BACKGROUND_ALPHA = 0xBB;
    protected static final int DEFAULT_BLOCKS_BACKGROUND_COLOR = Color.LTGRAY;

    public static final String ARG_TAB_EDGE = "ToolboxFragment_tabEdge";
    public static final String ARG_ROTATE_TABS = "ToolboxFragment_rotateTabs";

    public static final int DEFAULT_TAB_EDGE = Gravity.TOP;
    public static final boolean DEFAULT_ROTATE_TABS = true;

    /** Subset of Gravity to identify the edge the category tabs should be bound to. */
    @IntDef(flag = true, value = { Gravity.TOP, Gravity.LEFT, Gravity.BOTTOM, Gravity.RIGHT, GravityCompat.START,
            GravityCompat.END })
    @Retention(RetentionPolicy.SOURCE)
    private @interface EdgeEnum {
    }

    protected final Rect mScrollablePadding = new Rect();

    protected ToolboxRoot mRootView;
    protected CategoryTabs mCategoryTabs;
    protected Button mActionButton;

    protected BlocklyController mController;
    protected WorkspaceHelper mHelper;

    private @EdgeEnum int mTabEdge = DEFAULT_TAB_EDGE;
    private boolean mRotateTabs = DEFAULT_ROTATE_TABS;

    private BlockListView.OnDragListBlock mDragHandler = new BlockListView.OnDragListBlock() {
        @Override
        public BlockGroup getDraggableBlockGroup(int index, Block blockInList,
                WorkspacePoint initialBlockPosition) {
            Block copy = blockInList.deepCopy();
            copy.setPosition(initialBlockPosition.x, initialBlockPosition.y);
            BlockGroup copyView = mController.addRootBlock(copy);
            if (mCloseable) {
                closeBlocksDrawer();
            }
            return copyView;
        }
    };

    @Override
    public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) {
        super.onInflate(context, attrs, savedInstanceState);

        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ToolboxFragment, 0, 0);
        try {
            //noinspection ResourceType
            mTabEdge = a.getInt(R.styleable.ToolboxFragment_tabEdge, DEFAULT_TAB_EDGE);
            mRotateTabs = a.getBoolean(R.styleable.ToolboxFragment_rotateTabs, DEFAULT_ROTATE_TABS);
        } finally {
            a.recycle();
        }

        // Store values in arguments, so fragment resume works (no inflation during resume).
        Bundle args = getArguments();
        if (args == null) {
            setArguments(args = new Bundle());
        }
        args.putInt(ARG_TAB_EDGE, mTabEdge);
        args.putBoolean(ARG_ROTATE_TABS, mRotateTabs);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        // Read configure
        readArgumentsFromBundle(getArguments());
        readArgumentsFromBundle(savedInstanceState); // Overwrite initial state with stored state.

        mActionButton = (Button) inflater.inflate(R.layout.default_create_variable_button, null);
        mActionButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mController != null) {
                    mController.requestAddVariable("item");
                }
            }
        });

        mBlockListView = new BlockListView(getContext());
        mBlockListView.setLayoutManager(createLinearLayoutManager());
        mBlockListView.addItemDecoration(new BlocksItemDecoration());
        mBlockListView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.blockly_toolbox_bg));// Replace with attrib
        mCategoryTabs = new CategoryTabs(getContext());
        mCategoryTabs.setLabelAdapter(onCreateLabelAdapter());
        mCategoryTabs.setCallback(new CategoryTabs.Callback() {
            @Override
            public void onCategorySelected(ToolboxCategory category) {
                setCurrentCategory(category);
            }
        });
        mRootView = new ToolboxRoot(getContext());
        updateViews();

        return mRootView;
    }

    /**
     * Connects the {@link ToolboxFragment} to the application's {@link BlocklyController}. It is
     * called by {@link BlocklyController#setToolboxFragment(ToolboxFragment)} and should not be
     * called by the application developer.
     *
     * @param controller The application's {@link BlocklyController}.
     */
    public void setController(BlocklyController controller) {
        if (mController != null && mController.getToolboxFragment() != this) {
            throw new IllegalStateException("Call BlockController.setToolboxFragment(..) instead of"
                    + " ToolboxFragment.setController(..).");
        }

        mController = controller;
        if (mController == null) {
            mBlockListView.setContents(new ArrayList<Block>(0));
            mActionButton.setVisibility(View.GONE);
        }
        mBlockListView.init(mController, mDragHandler);
    }

    /**
     * Sets the top level category used to populate the toolbox. This top level category can contain
     * either a list of blocks or a list of subcategories, but not both. If it has blocks, the
     * {@code ToolboxFragment} renders as a single tab/group. If it has subcategories, it will
     * render each subcategory with its own tab.
     *
     * @param topLevelCategory The top-level category in the toolbox.
     */
    public void setContents(final ToolboxCategory topLevelCategory) {
        List<Block> blocks = topLevelCategory.getBlocks();
        List<ToolboxCategory> subcats = topLevelCategory.getSubcategories();

        if (!blocks.isEmpty() && !subcats.isEmpty()) {
            throw new IllegalArgumentException("Toolbox cannot have both blocks and categories in the root level.");
        }

        if (blocks.isEmpty()) {
            mCategoryTabs.setCategories(subcats);
        } else {
            List<ToolboxCategory> singleCategory = new ArrayList<>(1);
            singleCategory.add(topLevelCategory);
            mCategoryTabs.setCategories(singleCategory);
        }
        updateViews();

        if (mBlockListView.getVisibility() == View.VISIBLE) {
            ToolboxCategory curCategory = subcats.isEmpty() ? topLevelCategory : subcats.get(0);
            setCurrentCategory(curCategory);
        }
    }

    /**
     * Sets the Toolbox's current {@link ToolboxCategory}, including opening or closing the drawer.
     * In closeable toolboxes, {@code null} {@code category} is equivalent to closing the drawer.
     * Otherwise, the drawer will be rendered empty.
     *
     * @param category The {@link ToolboxCategory} with blocks to display.
     */
    // TODO(#80): Add mBlockList animation hooks for subclasses.
    public void setCurrentCategory(@Nullable ToolboxCategory category) {
        if (category == null) {
            closeBlocksDrawer();
            return;
        }

        mCategoryTabs.setSelectedCategory(category);
        updateCategoryColors(category);
        mBlockListView.setVisibility(View.VISIBLE);
        if (category.isVariableCategory()) {
            mActionButton.setVisibility(View.VISIBLE);
        } else {
            mActionButton.setVisibility(View.GONE);
        }
        mBlockListView.setContents(category.getBlocks());
    }

    /**
     * Attempts to close the blocks drawer.
     *
     * @return True if an action was taken (the drawer is closeable and was previously open).
     */
    // TODO(#80): Add mBlockList animation hooks for subclasses.
    public boolean closeBlocksDrawer() {
        if (mCloseable && mBlockListView.getVisibility() == View.VISIBLE) {
            mBlockListView.setVisibility(View.GONE);
            mActionButton.setVisibility(View.GONE);
            mCategoryTabs.setSelectedCategory(null);
            return true;
        }
        return false;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt(ARG_TAB_EDGE, mTabEdge);
        outState.putBoolean(ARG_ROTATE_TABS, mRotateTabs);
    }

    /**
     * @return True if this {@code ToolboxFragment} displays tabs. Otherwise false.
     */
    public boolean hasTabs() {
        return mCloseable || mCategoryTabs.getTabCount() > 1;
    }

    protected CategoryTabs.LabelAdapter onCreateLabelAdapter() {
        return new DefaultTabsAdapter();
    }

    @Override
    protected void readArgumentsFromBundle(Bundle bundle) {
        super.readArgumentsFromBundle(bundle);
        if (bundle != null) {
            //noinspection ResourceType
            mTabEdge = bundle.getInt(ARG_TAB_EDGE, mTabEdge);
            mRotateTabs = bundle.getBoolean(ARG_ROTATE_TABS, mRotateTabs);
        }
    }

    /**
     * Update the fragment's views based on the current values of {@link #mCloseable},
     * {@link #mTabEdge}, and {@link #mRotateTabs}.
     */
    protected void updateViews() {
        // If there is only one drawer and the drawer is not closeable, we don't need the tab.
        if (!hasTabs()) {
            mCategoryTabs.setVisibility(View.GONE);
        } else {
            mCategoryTabs.setVisibility(View.VISIBLE);
            mCategoryTabs.setOrientation(isTabsHorizontal() ? CategoryTabs.HORIZONTAL : CategoryTabs.VERTICAL);
            mCategoryTabs.setLabelRotation(getLabelRotation());
            mCategoryTabs.setTapSelectedDeselects(mCloseable);
        }

        if (!mCloseable) {
            mBlockListView.setVisibility(View.VISIBLE);
        } // Otherwise leave it in the current state.
    }

    protected void updateCategoryColors(ToolboxCategory curCategory) {
        Integer maybeColor = curCategory.getColor();
        int bgColor = DEFAULT_BLOCKS_BACKGROUND_COLOR;
        if (maybeColor != null) {
            bgColor = getBackgroundColor(maybeColor);
        }
        int alphaBgColor = Color.argb(mCloseable ? DEFAULT_BLOCKS_BACKGROUND_ALPHA : ColorUtils.ALPHA_OPAQUE,
                Color.red(bgColor), Color.green(bgColor), Color.blue(bgColor));
        mBlockListView.setBackgroundColor(alphaBgColor);
    }

    protected int getBackgroundColor(int categoryColor) {
        return ColorUtils.blendRGB(categoryColor, Color.WHITE, BLOCKS_BACKGROUND_LIGHTNESS);
    }

    /**
     * @return True if {@link #mTabEdge} is {@link Gravity#TOP} or {@link Gravity#BOTTOM}.
     */
    protected boolean isTabsHorizontal() {
        return (mTabEdge == Gravity.TOP || mTabEdge == Gravity.BOTTOM);
    }

    /**
     * Updates the padding used to calculate the margins of the scrollable blocks, based on the size
     * and placement of the tabs.
     */
    private void updateScrollablePadding() {
        int buttonHeight = mActionButton.getVisibility() == View.GONE ? 0 : mActionButton.getMeasuredHeight();
        int buttonWidth = mActionButton.getVisibility() == View.GONE ? 0 : mActionButton.getMeasuredWidth();
        int buttonVerticalPadding = 0, buttonHorizontalPadding = 0;
        if (mScrollOrientation == SCROLL_VERTICAL) {
            buttonVerticalPadding = buttonHeight;
        } else {
            buttonHorizontalPadding = buttonWidth;
        }
        int layoutDir = ViewCompat.getLayoutDirection(mRootView);
        int spacingForTabs;
        switch (GravityCompat.getAbsoluteGravity(mTabEdge, layoutDir)) {
        case Gravity.LEFT:
            spacingForTabs = hasTabs() ? mCategoryTabs.getMeasuredWidth() : 0; // Horizontal
            mScrollablePadding.set(spacingForTabs + buttonHorizontalPadding, buttonVerticalPadding, 0, 0);
            break;
        case Gravity.TOP:
            spacingForTabs = hasTabs() ? mCategoryTabs.getMeasuredHeight() : 0; // Vertical
            if (layoutDir == ViewCompat.LAYOUT_DIRECTION_LTR) {
                mScrollablePadding.set(buttonHorizontalPadding, spacingForTabs + buttonVerticalPadding, 0, 0);
            } else {
                mScrollablePadding.set(0, spacingForTabs + buttonVerticalPadding, buttonHorizontalPadding, 0);
            }
            break;
        case Gravity.RIGHT:
            spacingForTabs = hasTabs() ? mCategoryTabs.getMeasuredWidth() : 0; // Horizontal
            mScrollablePadding.set(0, buttonVerticalPadding, spacingForTabs + buttonHorizontalPadding, 0);
            break;
        case Gravity.BOTTOM:
            spacingForTabs = hasTabs() ? mCategoryTabs.getMeasuredHeight() : 0; // Vertical
            if (layoutDir == ViewCompat.LAYOUT_DIRECTION_LTR) {
                mScrollablePadding.set(buttonHorizontalPadding, 0, 0, spacingForTabs + buttonVerticalPadding);
            } else {
                mScrollablePadding.set(0, 0, buttonHorizontalPadding, spacingForTabs + buttonVerticalPadding);
            }
            break;
        }
    }

    /**
     * @return Computed {@link Rotation} constant for {@link #mRotateTabs} and {@link #mTabEdge}.
     */
    @Rotation.Enum
    private int getLabelRotation() {
        if (!mRotateTabs) {
            return Rotation.NONE;
        }
        switch (mTabEdge) {
        case Gravity.LEFT:
            return Rotation.COUNTER_CLOCKWISE;
        case Gravity.RIGHT:
            return Rotation.CLOCKWISE;
        case Gravity.TOP:
            return Rotation.NONE;
        case Gravity.BOTTOM:
            return Rotation.NONE;
        case GravityCompat.START:
            return Rotation.ADAPTIVE_COUNTER_CLOCKWISE;
        case GravityCompat.END:
            return Rotation.ADAPTIVE_CLOCKWISE;
        default:
            throw new IllegalArgumentException("Invalid tabEdge: " + mTabEdge);
        }
    }

    /** Manages TextView labels derived from {@link R.layout#default_toolbox_tab}. */
    protected class DefaultTabsAdapter extends CategoryTabs.LabelAdapter {
        @Override
        public View onCreateLabel() {
            return (TextView) LayoutInflater.from(getContext()).inflate(R.layout.default_toolbox_tab, null);
        }

        /**
         * Assigns the category name to the {@link TextView}. Tabs without labels will be assigned
         * the text {@link R.string#blockly_toolbox_default_category_name} ("Blocks" in English).
         *
         * @param labelView The view used as the label.
         * @param category The {@link ToolboxCategory}.
         * @param position The ordering position of the tab.
         */
        @Override
        public void onBindLabel(View labelView, ToolboxCategory category, int position) {
            String labelText = category.getCategoryName();
            if (TextUtils.isEmpty(labelText)) {
                labelText = getContext().getString(R.string.blockly_toolbox_default_category_name);
            }
            ((TextView) labelView).setText(labelText);
        }

    }

    /**
     * A custom view to manage the measure and draw order of the tabs and blocks drawer.
     * TODO (#393): Refactor ToolboxRoot into its own view class with appropriate config/subviews.
     */
    private class ToolboxRoot extends ViewGroup {
        ToolboxRoot(Context context) {
            super(context);

            // Always add the BlockListView before the tabs, for draw order.
            addView(mBlockListView);
            addView(mCategoryTabs);
            addView(mActionButton);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // Measure the tabs before the the block list.
            measureChild(mCategoryTabs, widthMeasureSpec, heightMeasureSpec);
            measureChild(mActionButton, widthMeasureSpec, heightMeasureSpec);
            updateScrollablePadding();
            measureChild(mBlockListView, widthMeasureSpec, heightMeasureSpec);

            int listWidth = mBlockListView.getVisibility() == View.GONE ? 0 : mBlockListView.getMeasuredWidth();
            int listHeight = mBlockListView.getVisibility() == View.GONE ? 0 : mBlockListView.getMeasuredHeight();

            // The scrollable padding should already take the button size into account.
            int width = Math.max(mCategoryTabs.getMeasuredWidth(), listWidth);
            int height = Math.max(mCategoryTabs.getMeasuredHeight(), listHeight);
            width = getSizeForSpec(widthMeasureSpec, width);
            height = getSizeForSpec(heightMeasureSpec, height);

            setMeasuredDimension(width, height);
        }

        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            int width = right - left;
            int height = bottom - top;

            mBlockListView.layout(0, 0, width, height);

            int tabMeasuredWidth = mCategoryTabs.getMeasuredWidth();
            int tabMeasuredHeight = mCategoryTabs.getMeasuredHeight();
            int layoutDir = ViewCompat.getLayoutDirection(mRootView);
            int tabGravity = GravityCompat.getAbsoluteGravity(mTabEdge, layoutDir);
            int tabRight = 0, tabLeft = 0, tabBottom = 0, tabTop = 0;
            switch (tabGravity) {
            case Gravity.LEFT:
            case Gravity.TOP:
                tabRight = Math.min(width, tabMeasuredWidth);
                tabBottom = Math.min(height, tabMeasuredHeight);
                break;
            case Gravity.RIGHT:
                tabLeft = Math.max(0, width - tabMeasuredWidth);
                tabRight = width;
                tabBottom = Math.min(height, tabMeasuredHeight);
                break;
            case Gravity.BOTTOM:
                tabTop = Math.max(0, height - tabMeasuredHeight);
                tabRight = Math.min(width, tabMeasuredWidth);
                tabBottom = bottom;
                break;
            }
            mCategoryTabs.layout(tabLeft, tabTop, tabRight, tabBottom);

            switch (tabGravity) {
            case Gravity.LEFT:
                mActionButton.layout(tabRight, 0, tabRight + mActionButton.getMeasuredWidth(),
                        mActionButton.getMeasuredHeight());
                break;
            case Gravity.RIGHT:
                mActionButton.layout(tabLeft - mActionButton.getMeasuredWidth(), 0, tabLeft,
                        mActionButton.getMeasuredHeight());
                break;
            case Gravity.TOP:
                if (layoutDir == ViewCompat.LAYOUT_DIRECTION_LTR) {
                    mActionButton.layout(0, tabBottom, mActionButton.getMeasuredWidth(),
                            tabBottom + mActionButton.getMeasuredHeight());
                } else {
                    mActionButton.layout(width - mActionButton.getMeasuredWidth(), tabBottom, width,
                            tabBottom + mActionButton.getMeasuredHeight());
                }
                break;
            case Gravity.BOTTOM:
                if (layoutDir == ViewCompat.LAYOUT_DIRECTION_LTR) {
                    mActionButton.layout(0, tabTop - mActionButton.getMeasuredHeight(),
                            mActionButton.getMeasuredWidth(), tabTop);
                } else {
                    mActionButton.layout(width - mActionButton.getMeasuredWidth(),
                            tabTop - mActionButton.getMeasuredHeight(), width, tabTop);
                }
                break;
            }
        }
    }

    private int getSizeForSpec(int measureSpec, int desiredSize) {
        int mode = View.MeasureSpec.getMode(measureSpec);
        int size = View.MeasureSpec.getSize(measureSpec);

        switch (mode) {
        case View.MeasureSpec.AT_MOST:
            return Math.min(size, desiredSize);
        case View.MeasureSpec.EXACTLY:
            return size;
        case View.MeasureSpec.UNSPECIFIED:
            return desiredSize;
        }
        return desiredSize;
    }

    /**
     * {@link RecyclerView.ItemDecoration} to assign padding to block items, to avoid initial
     * overlap with the tabs.
     */
    private class BlocksItemDecoration extends RecyclerView.ItemDecoration {
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            boolean ltr = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_LTR;
            int itemCount = state.getItemCount();
            int itemIndex = parent.getChildAdapterPosition(view);

            boolean isFirst = (itemIndex == 0);
            boolean isLast = (itemIndex == itemCount - 1);
            int scrollDirection = ((LinearLayoutManager) parent.getLayoutManager()).getOrientation();

            switch (scrollDirection) {
            case LinearLayoutManager.HORIZONTAL: {
                boolean leftmost = ltr ? isFirst : isLast;
                boolean rightmost = ltr ? isLast : isFirst;
                outRect.set(leftmost ? mScrollablePadding.left : 0, mScrollablePadding.top,
                        rightmost ? mScrollablePadding.right : 0, mScrollablePadding.bottom);
                break;
            }
            case LinearLayoutManager.VERTICAL: {
                boolean topmost = ltr ? isFirst : isLast;
                boolean bottommost = ltr ? isLast : isFirst;
                outRect.set(mScrollablePadding.left, topmost ? mScrollablePadding.top : 0, mScrollablePadding.right,
                        bottommost ? mScrollablePadding.bottom : 0);
                break;
            }
            }
        }
    }
}