/*
 * Copyright 2016 - 2018 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.bottomsheet;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.FrameLayout;
import android.widget.GridView;
import android.widget.ListAdapter;
import android.widget.TextView;

import java.util.List;

import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.core.content.ContextCompat;
import de.mrapp.android.bottomsheet.adapter.DividableGridAdapter;
import de.mrapp.android.bottomsheet.model.AbstractItem;
import de.mrapp.android.bottomsheet.model.Divider;
import de.mrapp.android.bottomsheet.model.Item;
import de.mrapp.android.bottomsheet.view.DividableGridView;
import de.mrapp.android.bottomsheet.view.DraggableView;
import de.mrapp.android.util.DisplayUtil;
import de.mrapp.android.util.ViewUtil;
import de.mrapp.util.Condition;

import static android.os.Build.VERSION_CODES;
import static de.mrapp.android.util.DisplayUtil.getDeviceType;
import static de.mrapp.android.util.DisplayUtil.getOrientation;

/**
 * A bottom sheet, which is designed according to the Android 5's Material Design guidelines even on
 * pre-Lollipop devices. Such a bottom sheet appears at the bottom of the window and consists of a
 * title and multiple items. It is possible to customize the appearance of the bottom sheet or to
 * replace its title and items with custom views.
 * <p>
 * For creating or showing such bottom sheets, the methods {@link Builder#create()} or {@link
 * Builder#show()} of the builder {@link de.mrapp.android.bottomsheet.BottomSheet.Builder} can be
 * used.
 *
 * @author Michael Rapp
 * @since 1.0.0
 */
public class BottomSheet extends Dialog implements DialogInterface, DraggableView.Callback {

    /**
     * A builder, which allows to create and show bottom sheets, which are designed according to
     * Android 5's Material Design guidelines even on pre-Lollipop devices. Such a bottom sheet
     * appears at the bottom of the window and consists of a title and multiple items. It is
     * possible to customize the appearance of the bottom sheet or to replace its title and items
     * with custom views.
     */
    public static class Builder {

        /**
         * The bottom sheet, which is created by the builder.
         */
        private BottomSheet bottomSheet;

        /**
         * Initializes the builder.
         *
         * @param context
         *         The context, which should be used by the builder, as an instance of the class
         *         {@link Context}. The context may not be null
         * @param themeResourceId
         *         The resource id of the theme, which should be used by the dialog, as an {@link
         *         Integer} value, or -1, if the default theme should be used
         */
        private void initialize(@NonNull final Context context,
                                @StyleRes final int themeResourceId) {
            TypedValue typedValue = new TypedValue();
            context.getTheme().resolveAttribute(R.attr.bottomSheetTheme, typedValue, true);
            int themeId = typedValue.resourceId;
            themeId = themeId != 0 ? themeId : R.style.BottomSheet_Light;
            bottomSheet = new BottomSheet(context, themeId);
            bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE);
            bottomSheet.setCanceledOnTouchOutside(true);
            bottomSheet.setCancelable(true);
            bottomSheet.setContentView(createContentView(),
                    new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                            ViewGroup.LayoutParams.MATCH_PARENT));
            obtainStyledAttributes(themeId);
        }

        /**
         * Creates and returns the bottom sheetview stub, which allows to inflate the bottom sheet's
         * layout when shown.
         *
         * @return The view stub, which has been created
         */
        private View createContentView() {
            FrameLayout contentView = new FrameLayout(getContext());
            contentView.setId(android.R.id.content);
            return contentView;
        }

        /**
         * Obtains all relevant attributes from the current theme.
         */
        private void obtainStyledAttributes(@StyleRes final int themeResourceId) {
            obtainBackground(themeResourceId);
            obtainTitleColor(themeResourceId);
            obtainItemColor(themeResourceId);
            obtainDividerColor(themeResourceId);
            obtainDimAmount(themeResourceId);
            obtainDragSensitivity(themeResourceId);
        }

        /**
         * Obtains the background from the current theme.
         *
         * @param themeResourceId
         *         The resource id of the theme, the background should be obtained from, as an
         *         {@link Integer} value
         */
        private void obtainBackground(@StyleRes final int themeResourceId) {
            TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(themeResourceId,
                    new int[]{R.attr.bottomSheetBackground});
            int color = typedArray.getColor(0, -1);

            if (color != -1) {
                setBackgroundColor(color);
            } else {
                int resourceId = typedArray.getResourceId(0, 0);

                if (resourceId != 0) {
                    setBackground(resourceId);
                }
            }
        }

        /**
         * Obtains the title color from the current theme.
         *
         * @param themeResourceId
         *         The resource id of the theme, the title color should be obtained from, as an
         *         {@link Integer} value
         */
        private void obtainTitleColor(@StyleRes final int themeResourceId) {
            TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(themeResourceId,
                    new int[]{R.attr.bottomSheetTitleColor});
            int color = typedArray.getColor(0, -1);

            if (color != -1) {
                setTitleColor(color);
            }
        }

        /**
         * Obtains the divider color from the current theme.
         *
         * @param themeResourceId
         *         The resource id of the theme, the divider color should be obtained from, as an
         *         {@link Integer} value
         */
        private void obtainDividerColor(@StyleRes final int themeResourceId) {
            TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(themeResourceId,
                    new int[]{R.attr.bottomSheetDividerColor});
            int color = typedArray.getColor(0, -1);

            if (color != -1) {
                setDividerColor(color);
            }
        }

        /**
         * Obtains the item color from the current theme.
         *
         * @param themeResourceId
         *         The resource id of the theme, the item color should be obtained from, as an
         *         {@link Integer} value
         */
        private void obtainItemColor(@StyleRes final int themeResourceId) {
            TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(themeResourceId,
                    new int[]{R.attr.bottomSheetItemColor});
            int color = typedArray.getColor(0, -1);

            if (color != -1) {
                setItemColor(color);
            }
        }

        /**
         * Obtains the dim amount from the current theme.
         *
         * @param themeResourceId
         *         The resource id of the theme, the dim amount should be obtained from, as an
         *         {@link Integer} value
         */
        private void obtainDimAmount(@StyleRes final int themeResourceId) {
            TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(themeResourceId,
                    new int[]{R.attr.bottomSheetDimAmount});
            float dimAmount = typedArray.getFraction(0, 1, 1, -1);

            if (dimAmount != -1) {
                setDimAmount(dimAmount);
            }
        }

        /**
         * Obtains the drag sensitivity from the current theme.
         *
         * @param themeResourceId
         *         The resource id of the theme, the drag sensitivity should be obtained from, as an
         *         {@link Integer} value
         */
        private void obtainDragSensitivity(@StyleRes final int themeResourceId) {
            TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(themeResourceId,
                    new int[]{R.attr.bottomSheetDragSensitivity});
            float dragSensitivity = typedArray.getFraction(0, 1, 1, -1);

            if (dragSensitivity != -1) {
                setDragSensitivity(dragSensitivity);
            }
        }

        /**
         * Creates a new builder, which allows to create bottom sheets, which are designed according
         * to Android 5's Material Design guidelines even on pre-Lollipop devices.
         *
         * @param context
         *         The context, which should be used by the builder, as an instance of the class
         *         {@link Context}. The context may not be null
         */
        public Builder(@NonNull final Context context) {
            this(context, -1);
        }

        /**
         * Creates a new builder, which allows to create bottom sheets, which are designed according
         * to Android 5's Material Design guidelines even on pre-Lollipop devices.
         *
         * @param context
         *         The context, which should be used by the builder, as an instance of the class
         *         {@link Context}. The context may not be null
         * @param themeResourceId
         *         The resource id of the theme, which should be used by the bottom sheet, as an
         *         {@link Integer} value. The resource id must correspond to a valid theme
         */
        public Builder(@NonNull final Context context, @StyleRes final int themeResourceId) {
            initialize(context, themeResourceId);
        }

        /**
         * Returns the context, which is used by the builder.
         *
         * @return The context, which is used by the builder, as an instance of the class {@link
         * Context}
         */
        public final Context getContext() {
            return bottomSheet.getContext();
        }

        /**
         * Sets, whether the bottom sheet, which is created by the builder, should be cancelable, or
         * not.
         *
         * @param cancelable
         *         True, if the bottom sheet, which is created by the builder, should be cancelable,
         *         false otherwise
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setCancelable(final boolean cancelable) {
            bottomSheet.setCancelable(cancelable);
            return this;
        }

        /**
         * Sets the style of the bottom sheet, which is created by the builder.
         *
         * @param style
         *         The style, which should be set, as a value of the enum {@link Style}. The style
         *         may either be <code>LIST</code>, <code>LIST_COLUMNS</code> or <code>GRID</code>
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setStyle(@NonNull final Style style) {
            bottomSheet.setStyle(style);
            return this;
        }

        /**
         * Sets the listener, which should be notified, when an item of the bottom sheet has been
         * clicked.
         *
         * @param listener
         *         The listener, which should be set, as an instance of the type {@link
         *         OnItemClickListener} or null, if no listener should be notified
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setOnItemClickListener(@Nullable final OnItemClickListener listener) {
            bottomSheet.setOnItemClickListener(listener);
            return this;
        }

        /**
         * Sets the listener, which should be notified, when an item of the bottom sheet has been
         * long-clicked.
         *
         * @param listener
         *         The listener, which should be set, as an instance of the type {@link
         *         OnItemLongClickListener} or null, if no listener should be notified
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setOnItemLongClickListener(
                @Nullable final OnItemLongClickListener listener) {
            bottomSheet.setOnItemLongClickListener(listener);
            return this;
        }

        /**
         * Sets the listener, which should be notified, when the bottom sheet, which is created by
         * the builder, has been maximized.
         *
         * @param listener
         *         The listener, which should be set, as an instance of the type {@link
         *         OnMaximizeListener} or null, if no listener should be notified
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setOnMaximizeListener(@Nullable final OnMaximizeListener listener) {
            bottomSheet.setOnMaximizeListener(listener);
            return this;
        }

        /**
         * Sets the listener, which should be notified, when the bottom sheet, which is created by
         * the builder, is canceled.
         * <p>
         * If you are interested in listening for all cases where the bottom sheet is dismissed and
         * not just when it is canceled, see {@link #setOnDismissListener(android.content.DialogInterface.OnDismissListener)
         * setOnDismissListener}.
         *
         * @param listener
         *         The listener, which should be set, as an instance of the type {@link
         *         OnCancelListener}, or null, if no listener should be set
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         * @see #setOnDismissListener(android.content.DialogInterface.OnDismissListener)
         */
        public Builder setOnCancelListener(@Nullable final OnCancelListener listener) {
            bottomSheet.setOnCancelListener(listener);
            return this;
        }

        /**
         * Sets the listener, which should be notified, when the bottom sheet, which is created by
         * the builder, is dismissed for any reason.
         *
         * @param listener
         *         The listener, which should be set, as an instance of the type {@link
         *         OnDismissListener}, or null, if no listener should be set
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setOnDismissListener(@Nullable final OnDismissListener listener) {
            bottomSheet.setOnDismissListener(listener);
            return this;
        }

        /**
         * Sets the listener, which should be notified, if a key is dispatched to the bottom sheet,
         * which is created by the builder.
         *
         * @param listener
         *         The listener, which should be set, as an instance of the type {@link
         *         OnKeyListener}, or null, if no listener should be set
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setOnKeyListener(@Nullable final OnKeyListener listener) {
            bottomSheet.setOnKeyListener(listener);
            return this;
        }

        /**
         * Sets the color of the title of the bottom sheet, which is created by the builder.
         *
         * @param color
         *         The color, which should be set, as an {@link Integer} value or -1, if no custom
         *         color should be set
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setTitleColor(@ColorInt final int color) {
            bottomSheet.setTitleColor(color);
            return this;
        }

        /**
         * Sets the color of the items of the bottom sheet, which is created by the builder.
         *
         * @param color
         *         The color, which should be set, as an {@link Integer} value or -1, if no custom
         *         color should be set
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setItemColor(@ColorInt final int color) {
            bottomSheet.setItemColor(color);
            return this;
        }

        /**
         * Sets the color of the dividers of the bottom sheet, which is created by the builder.
         *
         * @param color
         *         The color, which should be set, as an {@link Integer} value or -1, if no custom
         *         color should be set
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setDividerColor(@ColorInt final int color) {
            bottomSheet.setDividerColor(color);
            return this;
        }

        /**
         * Sets the background of the bottom sheet, which is created by the builder.
         *
         * @param background
         *         The background, which should be set, as an instance of the class {@link Bitmap}
         *         or null, if no custom background should be set
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setBackground(@Nullable final Bitmap background) {
            bottomSheet.setBackground(background);
            return this;
        }

        /**
         * Sets the background of the bottom sheet, which is created by the builder.
         *
         * @param resourceId
         *         The resource id of the background, which should be set, as an {@link Integer}
         *         value. The resource id must correspond to a valid drawable resource
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setBackground(@DrawableRes final int resourceId) {
            bottomSheet.setBackground(resourceId);
            return this;
        }

        /**
         * Sets the background color of the bottom sheet, which is created by the builder.
         *
         * @param color
         *         The background color, which should be set, as an {@link Integer} value or -1, if
         *         no custom background color should be set
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setBackgroundColor(@ColorInt final int color) {
            bottomSheet.setBackgroundColor(color);
            return this;
        }

        /**
         * Sets the title of the bottom sheet, which is created by the builder.
         *
         * @param title
         *         The title, which should be set, as an instance of the type {@link CharSequence}
         *         or null, if no title should be shown
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setTitle(@Nullable final CharSequence title) {
            bottomSheet.setTitle(title);
            return this;
        }

        /**
         * Sets the title of the bottom sheet, which is created by the builder.
         *
         * @param resourceId
         *         The resource id of the title, which should be set, as an {@link Integer} value.
         *         The resource id must correspond to a valid string resource
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setTitle(@StringRes final int resourceId) {
            bottomSheet.setTitle(resourceId);
            return this;
        }

        /**
         * Sets the icon of the bottom sheet, which is created by the builder.
         *
         * @param icon
         *         The icon, which should be set, as an instance of the class {@link Bitmap} or
         *         null, if no icon should be shown
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setIcon(@Nullable final Bitmap icon) {
            bottomSheet.setIcon(icon);
            return this;
        }

        /**
         * Sets the icon of the bottom sheet, which is created by the builder.
         *
         * @param resourceId
         *         The resource id of the icon, which should be set, as an {@link Integer} value.
         *         The resource id must correspond to a valid drawable resource
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setIcon(@DrawableRes final int resourceId) {
            bottomSheet.setIcon(resourceId);
            return this;
        }

        /**
         * Set the icon of the bottom sheet, which is created by the builder.
         *
         * @param attributeId
         *         The id of the theme attribute, which supplies the icon, which should be set, as
         *         an {@link Integer} value. The id must point to a valid drawable resource
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setIconAttribute(@AttrRes final int attributeId) {
            bottomSheet.setIconAttribute(attributeId);
            return this;
        }

        /**
         * Sets the custom view, which should be shown by the bottom sheet, which is created by the
         * builder.
         *
         * @param view
         *         The view, which should be set, as an instance of the class {@link View} or null,
         *         if no custom view should be shown
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setView(@Nullable final View view) {
            bottomSheet.setView(view);
            return this;
        }

        /**
         * Sets the custom view, which should be shown by the bottom sheet, which is created by the
         * builder.
         *
         * @param resourceId
         *         The resource id of the view, which should be set, as an {@link Integer} value.
         *         The resource id must correspond to a valid layout resource
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setView(@LayoutRes final int resourceId) {
            bottomSheet.setView(resourceId);
            return this;
        }

        /**
         * Sets the custom view, which should be used to show the title of the bottom sheet, which
         * is created by the builder.
         *
         * @param view
         *         The view, which should be set, as an instance of the class {@link View} or null,
         *         if no custom view should be used to show the title
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setCustomTitle(@Nullable final View view) {
            bottomSheet.setCustomTitle(view);
            return this;
        }

        /**
         * Sets the custom view, which should be used to show the title of the bottom sheet, which
         * is created by the builder.
         *
         * @param resourceId
         *         The resource id of the view, which should be set, as an {@link Integer} value.
         *         The resource id must correspond to a valid layout resource
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setCustomTitle(@LayoutRes final int resourceId) {
            bottomSheet.setCustomTitle(resourceId);
            return this;
        }

        /**
         * Sets the sensitivity, which specifies the distance after which dragging has an effect on
         * the bottom sheet, in relation to an internal value range.
         *
         * @param dragSensitivity
         *         The drag sensitivity, which should be set, as a {@link Float} value. The drag
         *         sensitivity must be at lest 0 and at maximum 1
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setDragSensitivity(final float dragSensitivity) {
            bottomSheet.setDragSensitivity(dragSensitivity);
            return this;
        }

        /**
         * Sets the dim amount, which should be used to darken the area outside the bottom sheet,
         * which is created by the builder.
         *
         * @param dimAmount
         *         The dim amount, which should be set, as a {@link Float} value. The dim amount
         *         must be at least 0 (fully transparent) and at maximum 1 (fully opaque)
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setDimAmount(final float dimAmount) {
            bottomSheet.setDimAmount(dimAmount);
            return this;
        }

        /**
         * Sets the width of the bottom sheet, which is created by the builder. The width is only
         * used on tablet devices or in landscape mode.
         *
         * @param width
         *         The width, which should be set, in pixels as an {@link Integer} value. The width
         *         must be at least 1
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setWidth(final int width) {
            bottomSheet.setWidth(width);
            return this;
        }

        /**
         * Adds a new item to the bottom sheet, which is created by the builder.
         *
         * @param id
         *         The id of the item, which should be added, as an {@link Integer} value. The id
         *         must be at least 0
         * @param title
         *         The title of the item, which should be added, as an instance of the type {@link
         *         CharSequence}. The title may neither be null, nor empty
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder addItem(final int id, @NonNull final CharSequence title) {
            bottomSheet.addItem(id, title);
            return this;
        }

        /**
         * Adds a new item to the bottom sheet, which is created by the builder.
         *
         * @param id
         *         The id of the item, which should be added, as an {@link Integer} value. The id
         *         must be at least 0
         * @param title
         *         The title of the item, which should be added, as an instance of the type {@link
         *         CharSequence}. The title may neither be null, nor empty
         * @param icon
         *         The icon of the item, which should be added, as an instance of the class {@link
         *         Drawable}, or null, if no item should be used
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder addItem(final int id, @NonNull final CharSequence title,
                                     @Nullable final Drawable icon) {
            bottomSheet.addItem(id, title, icon);
            return this;
        }

        /**
         * Adds a new item to the bottom sheet, which is created by the builder.
         *
         * @param id
         *         The id of the item, which should be added, as an {@link Integer} value. The id
         *         must be at least 0
         * @param titleId
         *         The resource id of the title of the item, which should be added, as an {@link
         *         Integer} value. The resource id must correspond to a valid string resource
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder addItem(final int id, @StringRes final int titleId) {
            bottomSheet.addItem(id, titleId);
            return this;
        }

        /**
         * Adds a new item to the bottom sheet, which is created by the builder.
         *
         * @param id
         *         The id of the item, which should be added, as an {@link Integer} value. The id
         *         must be at least 0
         * @param titleId
         *         The resource id of the title of the item, which should be added, as an {@link
         *         Integer} value. The resource id must correspond to a valid string resource
         * @param iconId
         *         The resource id of the icon of the item, which should be added, as an {@link
         *         Integer} value. The resource id must correspond to a valid drawable resource
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder addItem(final int id, @StringRes final int titleId,
                                     @DrawableRes final int iconId) {
            bottomSheet.addItem(id, titleId, iconId);
            return this;
        }

        /**
         * Adds a new divider to the bottom sheet, which is created by the builder.
         *
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder addDivider() {
            bottomSheet.addDivider();
            return this;
        }

        /**
         * Adds a new divider to the bottom sheet, which is created by the builder.
         *
         * @param title
         *         The title of the divider, which should be added, as an instance of the type
         *         {@link CharSequence}, or null, if no title should be used
         */
        public final Builder addDivider(@Nullable final CharSequence title) {
            bottomSheet.addDivider(title);
            return this;
        }

        /**
         * Adds a new divider to the bottom sheet, which is created by the builder.
         *
         * @param titleId
         *         The resource id of the title, which should be added, as an {@link Integer} value.
         *         The resource id must correspond to a valid string resource
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder addDivider(@StringRes final int titleId) {
            bottomSheet.addDivider(titleId);
            return this;
        }

        /**
         * Sets, whether the item at a specific index should be enabled, or not.
         *
         * @param index
         *         The index of the item as an {@link Integer} value
         * @param enabled
         *         True, if the item should be enabled, false otherwise
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setItemEnabled(final int index, final boolean enabled) {
            bottomSheet.setItemEnabled(index, enabled);
            return this;
        }

        /**
         * Adds the apps, which are able to handle a specific intent, as items to the bottom sheet,
         * which is created by the builder. This causes all previously added items to be removed.
         * When an item is clicked, the corresponding app is started.
         *
         * @param activity
         *         The activity, the bottom sheet, which is created by the builder, belongs to, as
         *         an instance of the class {@link Activity}. The activity may not be null
         * @param intent
         *         The intent as an instance of the class {@link Intent}. The intent may not be
         *         null
         * @return The builder, the method has been called upon, as an instance of the class {@link
         * Builder}
         */
        public final Builder setIntent(@NonNull final Activity activity,
                                       @NonNull final Intent intent) {
            bottomSheet.setIntent(activity, intent);
            return this;
        }

        /**
         * Creates a bottom sheet with the arguments, which have been supplied to the builder.
         * Calling this method does not display the bottom sheet.
         *
         * @return The bottom sheet, which has been created as an instance of the class {@link
         * BottomSheet}
         */
        public final BottomSheet create() {
            return bottomSheet;
        }

        /**
         * Creates a bottom sheet with the arguments, which have been supplied to the builder and
         * immediately displays it.
         *
         * @return The bottom sheet, which has been created, as an instance of the class {@link
         * BottomSheet}
         */
        public final BottomSheet show() {
            bottomSheet.show();
            return bottomSheet;
        }

        /**
         * Creates a bottom sheet with the arguments, which have been supplied to the builder and
         * immediately maximizes it.
         *
         * @return The bottom sheet, which has been created, as an instance of the class {@link
         * BottomSheet}
         */
        @TargetApi(VERSION_CODES.FROYO)
        public final BottomSheet maximize() {
            bottomSheet.maximize();
            return bottomSheet;
        }

    }

    /**
     * Contains all possible styles of a {@link BottomSheet}.
     */
    public enum Style {

        /**
         * If the bottom sheet's items should be shown in a list.
         */
        LIST,

        /**
         * If the bottom sheet's items should be shown as a two-columned list on tablet devices and
         * in landscape mode.
         */
        LIST_COLUMNS,

        /**
         * If the bottom sheet's items should be shown in a grid.
         */
        GRID

    }

    /**
     * The name of the extra, which is used to store the title of the bottom sheet within a bundle.
     */
    private static final String TITLE_EXTRA = BottomSheet.class.getSimpleName() + "::title";

    /**
     * The name of the extra, which is used to store the bitmap of the icon of the bottom sheet
     * within a bundle.
     */
    private static final String ICON_BITMAP_EXTRA =
            BottomSheet.class.getSimpleName() + "::iconBitmap";

    /**
     * The name of the extra, which is used to store the resource id of the icon of the bottom sheet
     * within a bundle.
     */
    private static final String ICON_ID_EXTRA = BottomSheet.class.getSimpleName() + "::iconId";

    /**
     * The name of the extra, which is used to store the attribute id of the icon of the bottom
     * sheet within a bundle.
     */
    private static final String ICON_ATTRIBUTE_ID_EXTRA =
            BottomSheet.class.getSimpleName() + "::iconAttributeId";

    /**
     * The name of the extra, which is used to store the color of the title of the bottom sheet
     * within a bundle.
     */
    private static final String TITLE_COLOR_EXTRA =
            BottomSheet.class.getSimpleName() + "::titleColor";

    /**
     * The name of the extra, which is used to store the bitmap of the background of the bottom
     * sheet within a bundle.
     */
    private static final String BACKGROUND_BITMAP_EXTRA =
            BottomSheet.class.getSimpleName() + "::backgroundBitmap";

    /**
     * The name of the extra, which is used to store the resource id of the background of the bottom
     * sheet within a bundle.
     */
    private static final String BACKGROUND_ID_EXTRA =
            BottomSheet.class.getSimpleName() + "::backgroundId";

    /**
     * The name of the extra, which is used to store the color of the background of the bottom sheet
     * within a bundle.
     */
    private static final String BACKGROUND_COLOR_EXTRA =
            BottomSheet.class.getSimpleName() + "::backgroundColor";

    /**
     * The name of the extra, which is used to store, whether the bottom sheet is cancelable, or
     * not, within a bundle.
     */
    private static final String CANCELABLE_EXTRA =
            BottomSheet.class.getSimpleName() + "::cancelable";

    /**
     * The name of the extra, which is used to store, whether the bottom sheet is canceled when it
     * is clicked outside, or not, within a bundle.
     */
    private static final String CANCELED_ON_TOUCH_OUTSIDE_EXTRA =
            BottomSheet.class.getSimpleName() + "::canceledOnTouchOutside";

    /**
     * The name of the extra, which is used to store the drag sensitivity within a bundle.
     */
    private static final String DRAG_SENSITIVITY_EXTRA =
            BottomSheet.class.getSimpleName() + "::dragSensitivity";

    /**
     * The name of the extra, which is used to store the dim amount within a bundle.
     */
    private static final String DIM_AMOUNT_EXTRA =
            BottomSheet.class.getSimpleName() + "::dimAmount";

    /**
     * The name of the extra, which is used to store the width of the bottom sheet within a bundle.
     */
    private static final String WIDTH_EXTRA = BottomSheet.class.getSimpleName() + "::width";

    /**
     * The minimum value of the internal value range, which specifies after which distance dragging
     * has an effect on the bottom sheet.
     */
    private static final int MIN_DRAG_SENSITIVITY = 10;

    /**
     * The maximum value of the internal value range, which specifies after which distance dragging
     * has an effect on the bottom sheet.
     */
    private static final int MAX_DRAG_SENSITIVITY = 260;

    /**
     * The root view of the bottom sheet.
     */
    private DraggableView rootView;

    /**
     * The layout, which is used to show the bottom sheet's title.
     */
    private ViewGroup titleContainer;

    /**
     * The text view, which is used to show the bottom sheet's title.
     */
    private TextView titleTextView;

    /**
     * The layout, which is used to show the bottom sheet's content.
     */
    private ViewGroup contentContainer;

    /**
     * The grid view, which is used to show the bottom sheet's items.
     */
    private GridView gridView;

    /**
     * The adapter, which is used to manage the bottom sheet's items.
     */
    private DividableGridAdapter adapter;

    /**
     * The title of the bottom sheet.
     */
    private CharSequence title;

    /**
     * The icon of the bottom sheet.
     */
    private Drawable icon;

    /**
     * The bitmap of the icon of the bottom sheet.
     */
    private Bitmap iconBitmap;

    /**
     * The resource id of the icon of the bottom sheet.
     */
    private int iconId = -1;

    /**
     * The attribute id of the icon of the bottom sheet.
     */
    private int iconAttributeId = -1;

    /**
     * The color of the title of the bottom sheet.
     */
    private int titleColor = -1;

    /**
     * The background of the bottom sheet.
     */
    private Drawable background;

    /**
     * The bitmap of the background of the bottom sheet.
     */
    private Bitmap backgroundBitmap;

    /**
     * The resource id of the background of the bottom sheet.
     */
    private int backgroundId = -1;

    /**
     * The color of the background of the bottom sheet.
     */
    private int backgroundColor = -1;

    /**
     * True, if the bottom sheet is cancelable, false otherwise.
     */
    private boolean cancelable;

    /**
     * True, if the bottom sheet is canceled, when the decor view is touched, false otherwise.
     */
    private boolean canceledOnTouchOutside;

    /**
     * The sensitivity, which specifies the distance after which dragging has an effect on the
     * bottom sheet, in relation to an internal value range.
     */
    private float dragSensitivity;

    /**
     * The dim amount, which is used to darken the area outside the bottom sheet.
     */
    private float dimAmount;

    /**
     * The width of the bottom sheet in pixels.
     */
    private int width;

    /**
     * The custom content view of the bottom sheet.
     */
    private View customView;

    /**
     * The resource id of the custom content view of the bottom sheet.
     */
    private int customViewId = -1;

    /**
     * The custom title view of the bottom sheet.
     */
    private View customTitleView;

    /**
     * The resource id of the custom title view of the bottom sheet.
     */
    private int customTitleViewId = -1;

    /**
     * The listener, which is notified, when the bottom sheet has been shown.
     */
    private OnShowListener onShowListener;

    /**
     * The listener, which is notified, when an item of the bottom sheet has been clicked.
     */
    private OnItemClickListener itemClickListener;

    /**
     * The listener, which is notified, when an item of the bottom sheet has been long-clicked.
     */
    private OnItemLongClickListener itemLongClickListener;

    /**
     * The listener, which is notified, when the bottom sheet is maximized.
     */
    private OnMaximizeListener maximizeListener;

    /**
     * True, if the bottom sheet should be maximized immediately after it has been shown, false
     * otherwise.
     */
    private boolean maximize;

    /**
     * Initializes the bottom sheet.
     */
    private void initialize() {
        width = getContext().getResources().getDimensionPixelSize(R.dimen.default_width);
        maximize = false;
        adapter = new DividableGridAdapter(getContext(), Style.LIST, width);
        super.setOnShowListener(createOnShowListener());
    }

    /**
     * Creates and returns the layout params, which should be used to show the bottom sheet.
     *
     * @return The layout params, which have been created, as an instance of the class {@link
     * android.view.WindowManager.LayoutParams}
     */
    private WindowManager.LayoutParams createLayoutParams() {
        WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
        layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
        layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
        return layoutParams;
    }

    /**
     * Creates and returns the layout params, which should be used to show the bottom sheet's root
     * view.
     *
     * @return The layout params, which have been created, as an instance of the class {@link
     * android.widget.FrameLayout.LayoutParams }
     */
    private FrameLayout.LayoutParams createRootViewLayoutParams() {
        FrameLayout.LayoutParams layoutParams =
                new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
                        FrameLayout.LayoutParams.MATCH_PARENT);
        layoutParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
        return layoutParams;
    }

    /**
     * Initializes the bottom sheet's root view.
     */
    private void inflateRootView() {
        ViewGroup contentView = findViewById(android.R.id.content);
        contentView.removeAllViews();
        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
        rootView =
                (DraggableView) layoutInflater.inflate(R.layout.bottom_sheet, contentView, false);
        rootView.setCallback(this);
        contentView.addView(rootView, createRootViewLayoutParams());
    }

    /**
     * Inflates the layout, which is used to show the bottom sheet's title. The layout may either be
     * the default one or a custom view, if one has been set before.
     */
    private void inflateTitleView() {
        titleContainer = rootView.findViewById(R.id.title_container);
        titleContainer.removeAllViews();

        if (customTitleView != null) {
            titleContainer.addView(customTitleView);
        } else if (customTitleViewId != -1) {
            LayoutInflater layoutInflater = LayoutInflater.from(getContext());
            View view = layoutInflater.inflate(customTitleViewId, titleContainer, false);
            titleContainer.addView(view);
        } else {
            LayoutInflater layoutInflater = LayoutInflater.from(getContext());
            View view = layoutInflater.inflate(R.layout.bottom_sheet_title, titleContainer, false);
            titleContainer.addView(view);
        }

        if (getStyle() == Style.LIST) {
            int padding = getContext().getResources()
                    .getDimensionPixelSize(R.dimen.bottom_sheet_list_item_horizontal_padding);
            titleContainer.setPadding(padding, 0, padding, 0);
        } else {
            int padding = getContext().getResources()
                    .getDimensionPixelSize(R.dimen.bottom_sheet_grid_item_horizontal_padding);
            titleContainer.setPadding(padding, 0, padding, 0);
        }

        View titleView = titleContainer.findViewById(android.R.id.title);
        titleTextView = titleView instanceof TextView ? (TextView) titleView : null;
    }

    /**
     * Inflates the layout, which is used to show the bottom sheet's content. The layout may either
     * be the default one or a custom view, if one has been set before.
     */
    private void inflateContentView() {
        contentContainer = rootView.findViewById(R.id.content_container);
        contentContainer.removeAllViews();

        if (customView != null) {
            contentContainer.setVisibility(View.VISIBLE);
            contentContainer.addView(customView);
        } else if (customViewId != -1) {
            contentContainer.setVisibility(View.VISIBLE);
            LayoutInflater layoutInflater = LayoutInflater.from(getContext());
            View view = layoutInflater.inflate(customViewId, contentContainer, false);
            contentContainer.addView(view);
        } else {
            LayoutInflater layoutInflater = LayoutInflater.from(getContext());
            View view = layoutInflater
                    .inflate(R.layout.bottom_sheet_grid_view, contentContainer, false);
            contentContainer.addView(view);
        }

        showGridView();
    }

    /**
     * Shows the grid view, which is used to show the bottom sheet's items.
     */
    private void showGridView() {
        gridView = contentContainer.findViewById(R.id.bottom_sheet_grid_view);

        if (gridView != null) {
            contentContainer.setVisibility(View.VISIBLE);

            if (getStyle() == Style.GRID) {
                int horizontalPadding = getContext().getResources()
                        .getDimensionPixelSize(R.dimen.bottom_sheet_grid_item_horizontal_padding);
                int paddingBottom = getContext().getResources()
                        .getDimensionPixelSize(R.dimen.bottom_sheet_grid_padding_bottom);
                gridView.setPadding(horizontalPadding, 0, horizontalPadding, paddingBottom);
                gridView.setNumColumns(GridView.AUTO_FIT);
                gridView.setColumnWidth(getContext().getResources()
                        .getDimensionPixelSize(R.dimen.bottom_sheet_grid_item_size));
            } else {
                int paddingBottom = getContext().getResources()
                        .getDimensionPixelSize(R.dimen.bottom_sheet_list_padding_bottom);
                gridView.setPadding(0, 0, 0, paddingBottom);
                gridView.setNumColumns(getStyle() == Style.LIST_COLUMNS &&
                        (getDeviceType(getContext()) == DisplayUtil.DeviceType.TABLET ||
                                getOrientation(getContext()) == DisplayUtil.Orientation.LANDSCAPE) ?
                        2 : 1);
            }

            gridView.setOnItemClickListener(createItemClickListener());
            gridView.setOnItemLongClickListener(createItemLongClickListener());
            gridView.setAdapter(adapter);
        }
    }

    /**
     * Adapts the view, which is used to show the dialog's content.
     */
    private void adaptContentView() {
        if (contentContainer != null) {
            inflateContentView();
        }
    }

    /**
     * Adapts the root view.
     */
    private void adaptRootView() {
        if (rootView != null) {
            if (getStyle() == Style.LIST) {
                int paddingTop = getContext().getResources()
                        .getDimensionPixelSize(R.dimen.bottom_sheet_list_padding_top);
                rootView.setPadding(0, paddingTop, 0, 0);
            } else {
                int paddingTop = getContext().getResources()
                        .getDimensionPixelSize(R.dimen.bottom_sheet_grid_padding_top);
                rootView.setPadding(0, paddingTop, 0, 0);
            }
        }
    }

    /**
     * Adapts the view, which is used to show the dialog's title.
     */
    private void adaptTitleView() {
        if (titleContainer != null) {
            inflateTitleView();
            adaptTitle();
            adaptTitleColor();
            adaptIcon();
        }
    }

    /**
     * Adapts the color of the bottom sheet's title.
     */
    private void adaptTitleColor() {
        if (titleTextView != null && titleColor != -1) {
            titleTextView.setTextColor(titleColor);
        }
    }

    /**
     * Adapts the bottom sheet's title.
     */
    private void adaptTitle() {
        if (titleTextView != null) {
            titleTextView.setText(title);
        }

        adaptTitleContainerVisibility();
    }

    /**
     * Adapts the bottom sheet's icon.
     */
    private void adaptIcon() {
        if (titleTextView != null) {
            titleTextView.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
        }

        adaptTitleContainerVisibility();
    }

    /**
     * Adapts the visibility of the layout, which is used to show the bottom sheet's title.
     */
    private void adaptTitleContainerVisibility() {
        if (titleContainer != null) {
            if (customTitleView == null && customTitleViewId == -1) {
                titleContainer.setVisibility(
                        !TextUtils.isEmpty(title) || icon != null ? View.VISIBLE : View.GONE);
            } else {
                titleContainer.setVisibility(View.VISIBLE);
            }
        }
    }

    /**
     * Adapts the bottom sheet's background.
     */
    private void adaptBackground() {
        if (rootView != null && background != null) {
            ViewUtil.setBackground(rootView, background);
        }
    }

    /**
     * Adapts the bottom sheet's drag sensitivity.
     */
    private void adaptDragSensitivity() {
        if (rootView != null) {
            rootView.setDragSensitivity(calculateDragSensitivity());
        }
    }

    /**
     * Adapts the width of the bottom sheet.
     */
    private void adaptWidth() {
        adapter.setWidth(width);

        if (rootView != null) {
            rootView.setWidth(width);
            rootView.requestLayout();
        }
    }

    /**
     * Adapts the height of the grid view, which is used to show the bottom sheet's items.
     */
    private void adaptGridViewHeight() {
        if (gridView instanceof DividableGridView) {
            ((DividableGridView) gridView).adaptHeightToChildren();
        }
    }

    /**
     * Creates and returns a listener, which allows to immediately maximize the bottom sheet after
     * it has been shown.
     *
     * @return The listener, which has been created, as an instance of the type {@link
     * OnShowListener}
     */
    @TargetApi(VERSION_CODES.FROYO)
    private OnShowListener createOnShowListener() {
        return new OnShowListener() {

            @Override
            public void onShow(final DialogInterface dialog) {
                if (onShowListener != null) {
                    onShowListener.onShow(dialog);
                }

                if (maximize) {
                    maximize = false;
                    rootView.maximize(new DecelerateInterpolator());
                }
            }

        };
    }

    /**
     * Creates and returns a listener, which allows to cancel the bottom sheet, when the decor view
     * is touched.
     *
     * @return The listener, which has been created, as an instance of the type {@link
     * View.OnTouchListener}
     */
    private View.OnTouchListener createCancelOnTouchListener() {
        return new View.OnTouchListener() {

            @Override
            public boolean onTouch(final View v, final MotionEvent event) {
                if (cancelable && canceledOnTouchOutside) {
                    cancel();
                    v.performClick();
                    return true;
                }

                return false;
            }

        };
    }

    /**
     * Calculates and returns the distance after which dragging has an effect on the bottom sheet in
     * pixels. The distance depends on the current set drag sensitivity, which corresponds to an
     * internal value range.
     *
     * @return The distance after which dragging has an effect on the bottom sheet in pixels as an
     * {@link Integer} value
     */
    private int calculateDragSensitivity() {
        int range = MAX_DRAG_SENSITIVITY - MIN_DRAG_SENSITIVITY;
        return Math.round((1 - getDragSensitivity()) * range + MIN_DRAG_SENSITIVITY);
    }

    /**
     * Creates and returns a listener, which allows to observe when the items of a bottom sheet have
     * been clicked.
     *
     * @return The listener, which has been created, as an instance of the type {@link
     * OnItemClickListener}
     */
    private OnItemClickListener createItemClickListener() {
        return new OnItemClickListener() {

            @Override
            public void onItemClick(final AdapterView<?> parent, final View view,
                                    final int position, final long id) {
                if (itemClickListener != null && !rootView.isDragging() &&
                        !rootView.isAnimationRunning()) {
                    int index = position;

                    if (adapter.containsDividers()) {
                        for (int i = position; i >= 0; i--) {
                            if (adapter.getItem(i) == null ||
                                    (adapter.getItem(i) instanceof Divider &&
                                            i % adapter.getColumnCount() > 0)) {
                                index--;
                            }
                        }
                    }

                    itemClickListener.onItemClick(parent, view, index, getId(position));
                }

                dismiss();
            }

        };
    }

    /**
     * Creates and returns a listener, which allows to observe when the items of a bottom sheet have
     * been long-clicked.
     *
     * @return The listener, which has been created, as an instance of the type {qlink
     * OnItemLongClickListener}
     */
    private OnItemLongClickListener createItemLongClickListener() {
        return new OnItemLongClickListener() {

            @Override
            public boolean onItemLongClick(final AdapterView<?> parent, final View view,
                                           final int position, final long id) {

                if (!rootView.isDragging() && !rootView.isAnimationRunning() &&
                        itemLongClickListener != null) {
                    int index = position;

                    if (adapter.containsDividers()) {
                        for (int i = position; i >= 0; i--) {
                            if (adapter.getItem(i) == null ||
                                    (adapter.getItem(i) instanceof Divider &&
                                            i % adapter.getColumnCount() > 0)) {
                                index--;
                            }
                        }
                    }

                    return itemLongClickListener
                            .onItemLongClick(parent, view, index, getId(position));
                }

                return false;
            }

        };
    }

    /**
     * Creates and returns a listener, which allows to start an app, when an item of the bottom
     * sheet has been clicked.
     *
     * @param activity
     *         The activity, the bottom sheet belongs to, as an instance of the class {@link
     *         Activity}. The activity may not be null
     * @param intent
     *         The intent, which should be passed to the started app, as an instance of the class
     *         {@link Intent}. The intent may not be null
     * @param resolveInfos
     *         A list, which contains the resolve infos, which correspond to the apps, which are
     *         able to handle the intent, as an instance of the type {@link List} or an empty list,
     *         if no apps are able to handle the intent
     * @return The listener, which has been created, as an instance of the type {@link
     * OnItemClickListener}
     */
    private OnItemClickListener createIntentClickListener(@NonNull final Activity activity,
                                                          @NonNull final Intent intent,
                                                          @NonNull final List<ResolveInfo> resolveInfos) {
        return new OnItemClickListener() {

            @Override
            public void onItemClick(final AdapterView<?> parent, final View view,
                                    final int position, final long id) {
                ActivityInfo activityInfo = resolveInfos.get(position).activityInfo;
                ComponentName componentName =
                        new ComponentName(activityInfo.applicationInfo.packageName,
                                activityInfo.name);
                intent.setFlags(
                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
                intent.setComponent(componentName);
                activity.startActivity(intent);
                dismiss();
            }

        };
    }

    /**
     * Notifies, the listener, which has been registered to be notified, when the bottom sheet has
     * been maximized, about the bottom sheet being maximized.
     */
    private void notifyOnMaximize() {
        if (maximizeListener != null) {
            maximizeListener.onMaximize(this);
        }
    }

    /**
     * Creates a bottom sheet, which is designed according to Android 5's Material Design guidelines
     * even on pre-Lollipop devices.
     *
     * @param context
     *         The context, which should be used by the bottom sheet, as an instance of the class
     *         {@link Context}. The context may not be null
     * @param themeResourceId
     *         The resource id of the theme, which should be used by the bottom sheet, as an {@link
     *         Integer} value. The resource id must correspond to a valid theme
     */
    protected BottomSheet(@NonNull final Context context, @StyleRes final int themeResourceId) {
        super(context, themeResourceId);
        initialize();
    }

    /**
     * Sets the listener, which should be notified, when an item of the bottom sheet has been
     * clicked.
     *
     * @param listener
     *         The listener, which should be set, as an instance of the type {@link
     *         OnItemClickListener} or null, if no listener should be notified
     */
    public final void setOnItemClickListener(@Nullable final OnItemClickListener listener) {
        this.itemClickListener = listener;
    }

    /**
     * Sets the listener, which should be notified, when an item of the bottom sheet has been
     * long-clicked.
     *
     * @param listener
     *         The listener, which should be set, as an instance of the type {@link
     *         OnItemLongClickListener} or null, if no listener should be notified
     */
    public final void setOnItemLongClickListener(@Nullable final OnItemLongClickListener listener) {
        this.itemLongClickListener = listener;
    }

    /**
     * Sets the listener, which should be notified, when the bottom sheet has been maximized.
     *
     * @param listener
     *         The listener, which should be set, as an instance of the type {@link
     *         OnMaximizeListener} or null, if no listener should be notified
     */
    public final void setOnMaximizeListener(@Nullable final OnMaximizeListener listener) {
        this.maximizeListener = listener;
    }

    /**
     * Returns the grid view, which is contained by the bottom sheet.
     *
     * @return The grid view, which is contained by the bottom sheet, as an instance of the class
     * {@link GridView} or null, if the bottom sheet does not show any items or has not been shown
     * yet
     */
    public final GridView getGridView() {
        return (gridView != null && gridView.getVisibility() == View.VISIBLE) ? gridView : null;
    }

    /**
     * Returns the adapter of the grid view, which is contained by the bottom sheet.
     *
     * @return The adapter of the grid view, which is contained by the bottom sheet, as an instance
     * of the type {@link ListAdapter}
     */
    public final ListAdapter getListAdapter() {
        return adapter;
    }

    /**
     * Returns the icon of the bottom sheet.
     *
     * @return The icon of the bottom sheet, as an instance of the class {@link Drawable} or null,
     * if no icon has been set
     */
    public final Drawable getIcon() {
        return icon;
    }

    /**
     * Sets the icon of the bottom sheet.
     *
     * @param icon
     *         The icon, which should be set, as an instance of the class {@link Bitmap} or null, if
     *         no icon should be shown
     */
    public final void setIcon(@Nullable final Bitmap icon) {
        this.icon = new BitmapDrawable(getContext().getResources(), icon);
        this.iconBitmap = icon;
        this.iconId = -1;
        this.iconAttributeId = -1;
        adaptIcon();
    }

    /**
     * Sets the icon of the bottom sheet.
     *
     * @param resourceId
     *         The resource id of the icon, which should be set, as an {@link Integer} value. The
     *         resource id must correspond to a valid drawable resource
     */
    public final void setIcon(@DrawableRes final int resourceId) {
        this.icon = ContextCompat.getDrawable(getContext(), resourceId);
        this.iconBitmap = null;
        this.iconId = resourceId;
        this.iconAttributeId = -1;
        adaptIcon();
    }

    /**
     * Set the icon of the bottom sheet.
     *
     * @param attributeId
     *         The id of the theme attribute, which supplies the icon, which should be set, as an
     *         {@link Integer} value. The id must point to a valid drawable resource
     */
    public final void setIconAttribute(@AttrRes final int attributeId) {
        TypedArray typedArray =
                getContext().getTheme().obtainStyledAttributes(new int[]{attributeId});
        this.icon = typedArray.getDrawable(0);
        this.iconBitmap = null;
        this.iconId = -1;
        this.iconAttributeId = attributeId;
        adaptIcon();
    }

    /**
     * Returns the color of the title of the bottom sheet.
     *
     * @return The color of the title of the bottom sheet as an {@link Integer} value or -1, if no
     * custom color has been set
     */
    public final int getTitleColor() {
        return titleColor;
    }

    /**
     * Sets the color of the title of the bottom sheet.
     *
     * @param color
     *         The color, which should be set, as an {@link Integer} value
     */
    public final void setTitleColor(@ColorInt final int color) {
        titleColor = color;
        adaptTitleColor();
    }

    /**
     * Returns the color of the items of the bottom sheet.
     *
     * @return The color of the items of the bottom sheet as an {@link Integer} value or -1, if no
     * custom color has been set
     */
    public final int getItemColor() {
        return adapter.getItemColor();
    }

    /**
     * Sets the color of the items of the bottom sheet.
     *
     * @param color
     *         The color, which should be set, as an {@link Integer} value or -1, if no custom color
     *         should be set
     */
    public final void setItemColor(@ColorInt final int color) {
        adapter.setItemColor(color);
        adapter.notifyDataSetChanged();
    }

    /**
     * Returns the color of the dividers of the bottom sheet.
     *
     * @return The color of the dividers of the bottom sheet as an {@link Integer} value or -1, if
     * no custom color has been set
     */
    public final int getDividerColor() {
        return adapter.getDividerColor();
    }

    /**
     * Sets the color of the dividers of the bottom sheet.
     *
     * @param color
     *         The color, which should be set, as an {@link Integer} value or -1, if no custom color
     *         should be set
     */
    public final void setDividerColor(@ColorInt final int color) {
        adapter.setDividerColor(color);
        adapter.notifyDataSetChanged();
    }

    /**
     * Returns the background of the bottom sheet.
     *
     * @return The background of the bottom sheet as an instance of the class {@link Drawable} or
     * null, if no custom background has been set
     */
    public final Drawable getBackground() {
        return background;
    }

    /**
     * Sets the background of the bottom sheet.
     *
     * @param background
     *         The background, which should be set, as an instance of the class {@link Bitmap} or
     *         null, if no custom background should be set
     */
    public final void setBackground(@Nullable final Bitmap background) {
        this.background = new BitmapDrawable(getContext().getResources(), background);
        this.backgroundBitmap = background;
        this.backgroundId = -1;
        this.backgroundColor = -1;
        adaptBackground();
    }

    /**
     * Sets the background of the bottom sheet.
     *
     * @param resourceId
     *         The resource id of the background, which should be set, as an {@link Integer} value.
     *         The resource id must correspond to a valid drawable resource
     */
    public final void setBackground(@DrawableRes final int resourceId) {
        this.background = ContextCompat.getDrawable(getContext(), resourceId);
        this.backgroundBitmap = null;
        this.backgroundId = -1;
        this.backgroundColor = -1;
        adaptBackground();
    }

    /**
     * Sets the background color of the bottom sheet.
     *
     * @param color
     *         The background color, which should be set, as an {@link Integer} value or -1, if no
     *         custom background color should be set
     */
    public final void setBackgroundColor(@ColorInt final int color) {
        this.background = new ColorDrawable(color);
        this.backgroundBitmap = null;
        this.backgroundId = -1;
        this.backgroundColor = color;
        adaptBackground();
    }

    /**
     * Sets the custom view, which should be used to show the title of the bottom sheet.
     *
     * @param view
     *         The view, which should be set, as an instance of the class {@link View} or null, if
     *         no custom view should be used to show the title
     */
    public final void setCustomTitle(@Nullable final View view) {
        customTitleView = view;
        customTitleViewId = -1;
        adaptTitleView();
    }

    /**
     * Sets the custom view, which should be used to show the title of the bottom sheet.
     *
     * @param resourceId
     *         The resource id of the view, which should be set, as an {@link Integer} value. The
     *         resource id must correspond to a valid layout resource
     */
    public final void setCustomTitle(@LayoutRes final int resourceId) {
        customTitleView = null;
        customTitleViewId = resourceId;
        adaptTitleView();
    }

    /**
     * Sets the custom view, which should be shown by the bottom sheet.
     *
     * @param view
     *         The view, which should be set, as an instance of the class {@link View} or null, if
     *         no custom view should be shown
     */
    public final void setView(@Nullable final View view) {
        customView = view;
        customViewId = -1;
        adaptContentView();
    }

    /**
     * Sets the custom view, which should be shown by the bottom sheet.
     *
     * @param resourceId
     *         The resource id of the view, which should be set, as an {@link Integer} value. The
     *         resource id must correspond to a valid layout resource
     */
    public final void setView(@LayoutRes final int resourceId) {
        customView = null;
        customViewId = resourceId;
        adaptContentView();
        adaptGridViewHeight();
    }

    /**
     * Returns the sensitivity, which specifies the distance after which dragging has an effect on
     * the bottom sheet, in relation to an internal value range.
     *
     * @return The drag sensitivity as a {@link Float} value. The drag sensitivity must be at lest 0
     * and at maximum 1
     */
    public final float getDragSensitivity() {
        return dragSensitivity;
    }

    /**
     * Sets the sensitivity, which specifies the distance after which dragging has an effect on the
     * bottom sheet, in relation to an internal value range.
     *
     * @param dragSensitivity
     *         The drag sensitivity, which should be set, as a {@link Float} value. The drag
     *         sensitivity must be at lest 0 and at maximum 1
     */
    public final void setDragSensitivity(final float dragSensitivity) {
        Condition.INSTANCE
                .ensureAtLeast(dragSensitivity, 0, "The drag sensitivity must be at least 0");
        Condition.INSTANCE
                .ensureAtMaximum(dragSensitivity, 1, "The drag sensitivity must be at maximum 1");
        this.dragSensitivity = dragSensitivity;
        adaptDragSensitivity();
    }

    /**
     * Returns the dim amount, which is used to darken the area outside the bottom sheet.
     *
     * @return The dim amount, which is used to darken the area outside the bottom sheet, as a
     * {@link Float} value
     */
    public final float getDimAmount() {
        return dimAmount;
    }

    /**
     * Sets the dim amount, which should be used to darken the area outside the bottom sheet.
     *
     * @param dimAmount
     *         The dim amount, which should be set, as a {@link Float} value. The dim amount must be
     *         at least 0 (fully transparent) and at maximum 1 (fully opaque)
     */
    public final void setDimAmount(final float dimAmount) {
        Condition.INSTANCE.ensureAtLeast(dimAmount, 0, "The dim amount must be at least 0");
        Condition.INSTANCE.ensureAtMaximum(dimAmount, 1, "The dim amount must be at maximum 1");
        this.dimAmount = dimAmount;
        getWindow().getAttributes().dimAmount = dimAmount;
    }

    /**
     * Returns the width of the bottom sheet. The width is only used on tablet devices or in
     * landscape mode.
     *
     * @return The width of the bottom sheet in pixels as an {@link Integer} value
     */
    public final int getWidth() {
        return width;
    }

    /**
     * Sets the width of the bottom sheet. The width is only used on tablet devices or in landscape
     * mode.
     *
     * @param width
     *         The width, which should be set, in pixels as an {@link Integer} value. The width must
     *         be at least 1
     */
    public final void setWidth(final int width) {
        Condition.INSTANCE.ensureAtLeast(width, 1, "The width must be at least 1");
        this.width = width;
        adaptWidth();
    }

    /**
     * Adds a new item to the bottom sheet.
     *
     * @param id
     *         The id of the item, which should be added, as an {@link Integer} value. The id must
     *         be at least 0
     * @param title
     *         The title of the item, which should be added, as an instance of the type {@link
     *         CharSequence}. The title may neither be null, nor empty
     */
    public final void addItem(final int id, @NonNull final CharSequence title) {
        Item item = new Item(id, title);
        adapter.add(item);
        adaptGridViewHeight();
    }

    /**
     * Adds a new item to the bottom sheet.
     *
     * @param id
     *         The id of the item, which should be added, as an {@link Integer} value. The id must
     *         be at least 0
     * @param title
     *         The title of the item, which should be added, as an instance of the type {@link
     *         CharSequence}. The title may neither be null, nor empty
     * @param icon
     *         The icon of the item, which should be added, as an instance of the class {@link
     *         Drawable}, or null, if no item should be used
     */
    public final void addItem(final int id, @NonNull final CharSequence title,
                              @Nullable final Drawable icon) {
        Item item = new Item(id, title);
        item.setIcon(icon);
        adapter.add(item);
        adaptGridViewHeight();
    }

    /**
     * Adds a new item to the bottom sheet.
     *
     * @param id
     *         The id of the item, which should be added, as an {@link Integer} value. The id must
     *         be at least 0
     * @param titleId
     *         The resource id of the title of the item, which should be added, as an {@link
     *         Integer} value. The resource id must correspond to a valid string resource
     */
    public final void addItem(final int id, @StringRes final int titleId) {
        Item item = new Item(getContext(), id, titleId);
        adapter.add(item);
        adaptGridViewHeight();
    }

    /**
     * Adds a new item to the bottom sheet.
     *
     * @param id
     *         The id of the item, which should be added, as an {@link Integer} value. The id must
     *         be at least 0
     * @param titleId
     *         The resource id of the title of the item, which should be added, as an {@link
     *         Integer} value. The resource id must correspond to a valid string resource
     * @param iconId
     *         The resource id of the icon of the item, which should be added, as an {@link Integer}
     *         value. The resource id must correspond to a valid drawable resource
     */
    public final void addItem(final int id, @StringRes final int titleId,
                              @DrawableRes final int iconId) {
        Item item = new Item(getContext(), id, titleId);
        item.setIcon(getContext(), iconId);
        adapter.add(item);
        adaptGridViewHeight();
    }

    /**
     * Replaces the item at a specific index with another item.
     *
     * @param index
     *         The index of the item, which should be replaced, as an {@link Integer} value
     * @param id
     *         The id of the item, which should be added, as an {@link Integer} value. The id must
     *         be at least 0
     * @param title
     *         The title of the item, which should be added, as an instance of the type {@link
     *         CharSequence}. The title may neither be null, nor empty
     */
    public final void setItem(final int index, final int id, @NonNull final CharSequence title) {
        Item item = new Item(id, title);
        adapter.set(index, item);
        adaptGridViewHeight();
    }

    /**
     * Replaces the item at a specific index with another item.
     *
     * @param index
     *         The index of the item, which should be replaced, as an {@link Integer} value
     * @param id
     *         The id of the item, which should be added, as an {@link Integer} value. The id must
     *         be at least 0
     * @param title
     *         The title of the item, which should be added, as an instance of the type {@link
     *         CharSequence}. The title may neither be null, nor empty
     * @param icon
     *         The icon of the item, which should be added, as an instance of the class {@link
     *         Drawable}, or null, if no item should be used
     */
    public final void setItem(final int index, final int id, @NonNull final CharSequence title,
                              @Nullable final Drawable icon) {
        Item item = new Item(id, title);
        item.setIcon(icon);
        adapter.set(index, item);
        adaptGridViewHeight();
    }

    /**
     * Replaces the item at a specific index with another item.
     *
     * @param index
     *         The index of the item, which should be replaced, as an {@link Integer} value
     * @param id
     *         The id of the item, which should be added, as an {@link Integer} value. The id must
     *         be at least 0
     * @param titleId
     *         The resource id of the title of the item, which should be added, as an {@link
     *         Integer} value. The resource id must correspond to a valid string resource
     */
    public final void setItem(final int index, final int id, @StringRes final int titleId) {
        Item item = new Item(getContext(), id, titleId);
        adapter.set(index, item);
        adaptGridViewHeight();
    }

    /**
     * Replaces the item at a specific index with another item.
     *
     * @param index
     *         The index of the item, which should be replaced, as an {@link Integer} value
     * @param id
     *         The id of the item, which should be added, as an {@link Integer} value. The id must
     *         be at least 0
     * @param titleId
     *         The resource id of the title of the item, which should be added, as an {@link
     *         Integer} value. The resource id must correspond to a valid string resource
     * @param iconId
     *         The resource id of the icon of the item, which should be added, as an {@link Integer}
     *         value. The resource id must correspond to a valid drawable resource
     */
    public final void setItem(final int index, final int id, @StringRes final int titleId,
                              @DrawableRes final int iconId) {
        Item item = new Item(getContext(), id, titleId);
        item.setIcon(getContext(), iconId);
        adapter.set(index, item);
        adaptGridViewHeight();
    }

    /**
     * Adds a new divider to the bottom sheet.
     */
    public final void addDivider() {
        adapter.add(new Divider());
        adaptGridViewHeight();
    }

    /**
     * Adds a new divider to the bottom sheet.
     *
     * @param title
     *         The title of the divider, which should be added, as an instance of the type {@link
     *         CharSequence}, or null, if no title should be used
     */
    public final void addDivider(@Nullable final CharSequence title) {
        Divider divider = new Divider();
        divider.setTitle(title);
        adapter.add(divider);
        adaptGridViewHeight();
    }

    /**
     * Adds a new divider to the bottom sheet.
     *
     * @param titleId
     *         The resource id of the title of the divider, which should be added, as an {@link
     *         Integer} value. The resource id must correspond to a valid string resource
     */
    public final void addDivider(@StringRes final int titleId) {
        Divider divider = new Divider();
        divider.setTitle(getContext(), titleId);
        adapter.add(divider);
        adaptGridViewHeight();
    }

    /**
     * Replaces the item at a specific index with a divider.
     *
     * @param index
     *         The index of the item, which should be replaced, as an {@link Integer} value
     */
    public final void setDivider(final int index) {
        Divider divider = new Divider();
        adapter.set(index, divider);
        adaptGridViewHeight();
    }

    /**
     * Replaces the item at a specific index with a divider.
     *
     * @param index
     *         The index of the item, which should be replaced, as an {@link Integer} value
     * @param title
     *         The title of the divider, which should be added, as an instance of the type {@link
     *         CharSequence}, or null, if no title should be used
     */
    public final void setDivider(final int index, @Nullable final CharSequence title) {
        Divider divider = new Divider();
        divider.setTitle(title);
        adapter.set(index, divider);
        adaptGridViewHeight();
    }

    /**
     * Replaces the item at a specific index with a divider.
     *
     * @param index
     *         The index of the item, which should be replaced, as an {@link Integer} value
     * @param titleId
     *         The resource id of the title of the divider, which should be added, as an {@link
     *         Integer} value. The resource id must correspond to a valid string resource
     */
    public final void setDivider(final int index, @StringRes final int titleId) {
        Divider divider = new Divider();
        divider.setTitle(getContext(), titleId);
        adapter.set(index, divider);
        adaptGridViewHeight();
    }

    /**
     * Removes the item with at a specific index from the bottom sheet.
     *
     * @param index
     *         The index of the item, which should be removed, as an {@link Integer} value
     */
    public final void removeItem(final int index) {
        adapter.remove(index);
        adaptGridViewHeight();
    }

    /**
     * Removes all items from the bottom sheet.
     */
    public final void removeAllItems() {
        adapter.clear();
        adaptGridViewHeight();
    }

    /**
     * Returns, whether the bottom sheet contains any items, or not.
     *
     * @return True, if the bottom sheet contains any items, false otherwise
     */
    public final boolean isEmpty() {
        return adapter.isEmpty();
    }

    /**
     * Returns the number of items, which are currently contained by the bottom sheet.
     *
     * @return The number of items, which are currently contained by the bottom sheet, as an {@link
     * Integer} value or -1, if the bottom sheet does not show any items or has not been shown yet
     */
    public final int getItemCount() {
        return adapter.getItemCount();
    }

    /**
     * Returns the index of the item, which corresponds to a specific id.
     *
     * @param id
     *         The id of the item, whose index should be returned, as an {@link Integer} value. The
     *         id must be at least 0
     * @return The index of the item, which corresponds to the given id, or -1, if no item, which
     * corresponds to the given id, is contained by the bottom sheet
     */
    public final int indexOf(final int id) {
        Condition.INSTANCE.ensureAtLeast(id, 0, "The id must be at least 0");

        for (int i = 0; i < getItemCount(); i++) {
            AbstractItem item = adapter.getItem(i);

            if (item.getId() == id) {
                return i;
            }
        }

        return -1;
    }

    /**
     * Returns the id of the item, which corresponds to a specific index.
     *
     * @param index
     *         The index of the item, whose id should be returned, as an {@link Integer} value
     * @return The id of the item, which corresponds to the given position, or
     * <code>Divider#DIVIDER_ID</code>, if the item is a divider
     */
    public final int getId(final int index) {
        return adapter.getItem(index).getId();
    }

    /**
     * Returns, whether the item at a specific index is enabled, or not.
     *
     * @param index
     *         The index of the item, which should be checked, as an {@link Integer} value
     * @return True, if the item is enabled, false otherwise
     */
    public final boolean isItemEnabled(final int index) {
        return adapter.isItemEnabled(index);
    }

    /**
     * Sets, whether the item at a specific index should be enabled, or not.
     *
     * @param index
     *         The index of the item as an {@link Integer} value
     * @param enabled
     *         True, if the item should be enabled, false otherwise
     */
    public final void setItemEnabled(final int index, final boolean enabled) {
        adapter.setItemEnabled(index, enabled);
    }

    /**
     * Adds the apps, which are able to handle a specific intent, as items to the bottom sheet. This
     * causes all previously added items to be removed. When an item is clicked, the corresponding
     * app is started.
     *
     * @param activity
     *         The activity, the bottom sheet belongs to, as an instance of the class {@link
     *         Activity}. The activity may not be null
     * @param intent
     *         The intent as an instance of the class {@link Intent}. The intent may not be null
     */
    public final void setIntent(@NonNull final Activity activity, @NonNull final Intent intent) {
        Condition.INSTANCE.ensureNotNull(activity, "The activity may not be null");
        Condition.INSTANCE.ensureNotNull(intent, "The intent may not be null");
        removeAllItems();
        PackageManager packageManager = activity.getPackageManager();
        List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(intent, 0);

        for (int i = 0; i < resolveInfos.size(); i++) {
            ResolveInfo resolveInfo = resolveInfos.get(i);
            addItem(i, resolveInfo.loadLabel(packageManager), resolveInfo.loadIcon(packageManager));
        }

        setOnItemClickListener(
                createIntentClickListener(activity, (Intent) intent.clone(), resolveInfos));
    }

    /**
     * Invalidates the bottom sheet. This method must be called in order to update the appearance of
     * the bottom sheet, when its items have been changed.
     */
    public final void invalidate() {
        adapter.notifyDataSetChanged();
    }

    /**
     * Sets, whether the bottom sheet should automatically be invalidated, when its items have been
     * changed, or not.
     *
     * @param invalidateOnChange
     *         True, if the bottom sheet should automatically be invalidated, when its items have
     *         been changed, false otherwise
     */
    public final void invalidateOnChange(final boolean invalidateOnChange) {
        adapter.notifyOnChange(invalidateOnChange);
    }

    /**
     * Returns, whether the bottom sheet is currently maximized, or not.
     *
     * @return True, if the bottom sheet is currently maximized, false otherwise
     */
    public final boolean isMaximized() {
        return rootView != null && rootView.isMaximized();
    }

    /**
     * Maximizes the bottom sheet.
     */
    @TargetApi(VERSION_CODES.FROYO)
    public final void maximize() {
        if (!isMaximized()) {
            if (!isShowing()) {
                maximize = true;
                show();
            } else {
                rootView.maximize(new AccelerateDecelerateInterpolator());
            }
        }
    }

    /**
     * Returns the style, which is used to display the bottom sheet's items.
     *
     * @return style The style, which is used to display the bottom sheet's items, as a value of the
     * enum {@link Style}
     */
    public final Style getStyle() {
        return adapter.getStyle();
    }

    /**
     * Sets the style, which should be used to display the bottom sheet's items.
     *
     * @param style
     *         The style, which should be set, as a value of the enum {@link Style}. The style may
     *         either be <code>LIST</code>, <code>LIST_COLUMNS</code> or <code>GRID</code>
     */
    public final void setStyle(@NonNull final Style style) {
        Condition.INSTANCE.ensureNotNull(style, "The style may not be null");
        adapter.setStyle(style);
        adaptRootView();
        adaptTitleView();
        adaptContentView();
        adaptGridViewHeight();
    }

    /**
     * Returns the title of the bottom sheet.
     *
     * @return The title of the bottom sheet as an instance of the type {@link CharSequence} or
     * null, if no title has been set
     */
    public final CharSequence getTitle() {
        return title;
    }

    @Override
    public final void setTitle(@Nullable final CharSequence title) {
        super.setTitle(title);
        this.title = title;
        adaptTitle();
    }

    @Override
    public final void dismiss() {
        if (isShowing()) {
            rootView.hideView(false);
        }
    }

    @Override
    public final void cancel() {
        if (isShowing()) {
            rootView.hideView(true);
        }
    }

    @Override
    public final void setCancelable(final boolean cancelable) {
        super.setCancelable(cancelable);
        this.cancelable = cancelable;
    }

    @Override
    public final void setCanceledOnTouchOutside(final boolean canceledOnTouchOutside) {
        super.setCanceledOnTouchOutside(canceledOnTouchOutside);
        this.canceledOnTouchOutside = canceledOnTouchOutside;
    }

    @TargetApi(VERSION_CODES.FROYO)
    @Override
    public final void setOnShowListener(@Nullable final OnShowListener listener) {
        this.onShowListener = listener;
    }

    @Override
    public final void onMaximized() {
        notifyOnMaximize();
    }

    @Override
    public final void onHidden(final boolean canceled) {
        if (canceled) {
            super.cancel();
        } else {
            super.dismiss();
        }
    }

    @Override
    public final void onStart() {
        super.onStart();
        getWindow().setAttributes(createLayoutParams());
        getWindow().getDecorView().setOnTouchListener(createCancelOnTouchListener());
        inflateRootView();
        adaptRootView();
        inflateTitleView();
        inflateContentView();
        adaptTitle();
        adaptTitleColor();
        adaptIcon();
        adaptBackground();
        adaptDragSensitivity();
        adaptWidth();
        adaptGridViewHeight();
    }

    @Override
    public final void onStop() {
        super.onStop();
        rootView = null;
        titleContainer = null;
        titleTextView = null;
        contentContainer = null;
        gridView = null;
    }

    @NonNull
    @Override
    public final Bundle onSaveInstanceState() {
        Bundle outState = super.onSaveInstanceState();

        outState.putCharSequence(TITLE_EXTRA, title);
        outState.putInt(TITLE_COLOR_EXTRA, titleColor);
        outState.putBoolean(CANCELABLE_EXTRA, cancelable);
        outState.putBoolean(CANCELED_ON_TOUCH_OUTSIDE_EXTRA, canceledOnTouchOutside);
        outState.putFloat(DRAG_SENSITIVITY_EXTRA, dragSensitivity);
        outState.putFloat(DIM_AMOUNT_EXTRA, dimAmount);
        outState.putInt(WIDTH_EXTRA, width);

        if (iconBitmap != null) {
            outState.putParcelable(ICON_BITMAP_EXTRA, iconBitmap);
        } else if (iconId != -1) {
            outState.putInt(ICON_ID_EXTRA, iconId);
        }

        if (backgroundBitmap != null) {
            outState.putParcelable(BACKGROUND_BITMAP_EXTRA, backgroundBitmap);
        } else if (backgroundId != -1) {
            outState.putInt(BACKGROUND_ID_EXTRA, backgroundId);
        } else if (backgroundColor != -1) {
            outState.putInt(BACKGROUND_COLOR_EXTRA, backgroundColor);
        }

        return outState;
    }

    @Override
    public final void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
        setTitle(savedInstanceState.getCharSequence(TITLE_EXTRA));
        setTitleColor(savedInstanceState.getInt(TITLE_COLOR_EXTRA));
        setCancelable(savedInstanceState.getBoolean(CANCELABLE_EXTRA));
        setCanceledOnTouchOutside(savedInstanceState.getBoolean(CANCELED_ON_TOUCH_OUTSIDE_EXTRA));
        setDragSensitivity(savedInstanceState.getFloat(DRAG_SENSITIVITY_EXTRA));
        setDimAmount(savedInstanceState.getFloat(DIM_AMOUNT_EXTRA));
        setWidth(savedInstanceState.getInt(WIDTH_EXTRA));

        if (savedInstanceState.containsKey(ICON_BITMAP_EXTRA)) {
            setIcon((Bitmap) savedInstanceState.getParcelable(ICON_BITMAP_EXTRA));
        } else if (savedInstanceState.containsKey(ICON_ID_EXTRA)) {
            setIcon(savedInstanceState.getInt(ICON_ID_EXTRA));
        } else if (savedInstanceState.containsKey(ICON_ATTRIBUTE_ID_EXTRA)) {
            setIconAttribute(savedInstanceState.getInt(ICON_ATTRIBUTE_ID_EXTRA));
        }

        if (savedInstanceState.containsKey(BACKGROUND_BITMAP_EXTRA)) {
            setBackground((Bitmap) savedInstanceState.getParcelable(BACKGROUND_BITMAP_EXTRA));
        } else if (savedInstanceState.containsKey(BACKGROUND_ID_EXTRA)) {
            setBackground(savedInstanceState.getInt(BACKGROUND_ID_EXTRA));
        } else if (savedInstanceState.containsKey(BACKGROUND_COLOR_EXTRA)) {
            setBackgroundColor(savedInstanceState.getInt(BACKGROUND_COLOR_EXTRA));
        }

        super.onRestoreInstanceState(savedInstanceState);
    }

}