com.heinrichreimersoftware.materialintro.app.IntroActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.heinrichreimersoftware.materialintro.app.IntroActivity.java

Source

/*
 * MIT License
 *
 * Copyright (c) 2017 Jan Heinrich Reimer
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.heinrichreimersoftware.materialintro.app;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.IntDef;
import android.support.annotation.IntRange;
import android.support.annotation.InterpolatorRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.constraint.ConstraintLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.ColorUtils;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.util.Pair;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.TextSwitcher;

import com.heinrichreimersoftware.materialintro.R;
import com.heinrichreimersoftware.materialintro.slide.ButtonCtaSlide;
import com.heinrichreimersoftware.materialintro.slide.Slide;
import com.heinrichreimersoftware.materialintro.slide.SlideAdapter;
import com.heinrichreimersoftware.materialintro.util.AnimUtils;
import com.heinrichreimersoftware.materialintro.util.CheatSheet;
import com.heinrichreimersoftware.materialintro.view.FadeableViewPager;
import com.heinrichreimersoftware.materialintro.view.InkPageIndicator;
import com.heinrichreimersoftware.materialintro.view.parallax.Parallaxable;

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

@SuppressLint("Registered")
public class IntroActivity extends AppCompatActivity implements IntroNavigation {
    private static final String KEY_CURRENT_ITEM = "com.heinrichreimersoftware.materialintro.app.IntroActivity.KEY_CURRENT_ITEM";
    private static final String KEY_FULLSCREEN = "com.heinrichreimersoftware.materialintro.app.IntroActivity.KEY_FULLSCREEN";
    private static final String KEY_BUTTON_CTA_VISIBLE = "com.heinrichreimersoftware.materialintro.app.IntroActivity.KEY_BUTTON_CTA_VISIBLE";

    private boolean activityCreated = false;

    private ConstraintLayout miFrame;
    private FadeableViewPager miPager;
    private InkPageIndicator miPagerIndicator;
    private TextSwitcher miButtonCta;
    private ImageButton miButtonBack;
    private ImageButton miButtonNext;

    //Settings constants
    @IntDef({ BUTTON_NEXT_FUNCTION_NEXT, BUTTON_NEXT_FUNCTION_NEXT_FINISH })
    @Retention(RetentionPolicy.SOURCE)
    @interface ButtonNextFunction {
    }

    public static final int BUTTON_NEXT_FUNCTION_NEXT = 1;
    public static final int BUTTON_NEXT_FUNCTION_NEXT_FINISH = 2;

    @IntDef({ BUTTON_BACK_FUNCTION_BACK, BUTTON_BACK_FUNCTION_SKIP })
    @Retention(RetentionPolicy.SOURCE)
    @interface ButtonBackFunction {
    }

    public static final int BUTTON_BACK_FUNCTION_BACK = 1;
    public static final int BUTTON_BACK_FUNCTION_SKIP = 2;

    @IntDef({ BUTTON_CTA_TINT_MODE_BACKGROUND, BUTTON_CTA_TINT_MODE_TEXT })
    @Retention(RetentionPolicy.SOURCE)
    @interface ButtonCtaTintMode {
    }

    public static final int BUTTON_CTA_TINT_MODE_BACKGROUND = 1;
    public static final int BUTTON_CTA_TINT_MODE_TEXT = 2;

    public static final int DEFAULT_AUTOPLAY_DELAY = 1500;
    public static final int INFINITE = -1;
    public static final int DEFAULT_AUTOPLAY_REPEAT_COUNT = INFINITE;

    public static final Interpolator ACCELERATE_DECELERATE_INTERPOLATOR = new AccelerateDecelerateInterpolator();

    private final ArgbEvaluator evaluator = new ArgbEvaluator();

    private SlideAdapter adapter;

    private IntroPageChangeListener listener = new IntroPageChangeListener();

    private int position = 0;
    private float positionOffset = 0;

    //Settings
    private boolean fullscreen = false;
    private boolean buttonCtaVisible = false;
    @ButtonNextFunction
    private int buttonNextFunction = BUTTON_NEXT_FUNCTION_NEXT_FINISH;
    @ButtonBackFunction
    private int buttonBackFunction = BUTTON_BACK_FUNCTION_SKIP;
    @ButtonCtaTintMode
    private int buttonCtaTintMode = BUTTON_CTA_TINT_MODE_BACKGROUND;
    private NavigationPolicy navigationPolicy = null;
    private List<OnNavigationBlockedListener> navigationBlockedListeners = new ArrayList<>();
    private CharSequence buttonCtaLabel = null;
    @StringRes
    private int buttonCtaLabelRes = 0;
    private View.OnClickListener buttonCtaClickListener = null;

    private Handler autoplayHandler = new Handler();
    private Runnable autoplayCallback = null;
    private int autoplayCounter;
    private long autoplayDelay;

    private Interpolator pageScrollInterpolator;
    private long pageScrollDuration;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        pageScrollInterpolator = AnimationUtils.loadInterpolator(this,
                android.R.interpolator.accelerate_decelerate);
        pageScrollDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);

        if (savedInstanceState != null) {
            if (savedInstanceState.containsKey(KEY_CURRENT_ITEM)) {
                position = savedInstanceState.getInt(KEY_CURRENT_ITEM, position);
            }
            if (savedInstanceState.containsKey(KEY_FULLSCREEN)) {
                fullscreen = savedInstanceState.getBoolean(KEY_FULLSCREEN, fullscreen);
            }
            if (savedInstanceState.containsKey(KEY_BUTTON_CTA_VISIBLE)) {
                buttonCtaVisible = savedInstanceState.getBoolean(KEY_BUTTON_CTA_VISIBLE, buttonCtaVisible);
            }
        }

        if (fullscreen) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                setSystemUiFlags(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN, true);
                updateFullscreen();
            } else {
                getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                        WindowManager.LayoutParams.FLAG_FULLSCREEN);
            }
        }

        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);

        setContentView(R.layout.mi_activity_intro);
        initViews();
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        activityCreated = true;

        updateTaskDescription();
        updateButtonNextDrawable();
        updateButtonBackDrawable();
        updateScrollPositions();
        miFrame.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop,
                    int oldRight, int oldBottom) {
                updateScrollPositions();
                v.removeOnLayoutChangeListener(this);
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        updateFullscreen();
    }

    @Override
    public void onUserInteraction() {
        if (isAutoplaying())
            cancelAutoplay();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
            @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        updateButtonCta();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt(KEY_CURRENT_ITEM, miPager.getCurrentItem());
        outState.putBoolean(KEY_FULLSCREEN, fullscreen);
        outState.putBoolean(KEY_BUTTON_CTA_VISIBLE, buttonCtaVisible);
    }

    @Override
    public void onBackPressed() {
        if (position > 0) {
            previousSlide();
            return;
        }
        Intent returnIntent = onSendActivityResult(RESULT_CANCELED);
        if (returnIntent != null)
            setResult(RESULT_CANCELED, returnIntent);
        else
            setResult(RESULT_CANCELED);
        super.onBackPressed();
    }

    public Intent onSendActivityResult(int result) {
        return null;
    }

    @Override
    protected void onDestroy() {
        if (isAutoplaying()) {
            cancelAutoplay();
        }

        activityCreated = false;
        super.onDestroy();
    }

    private void setSystemUiFlags(int flags, boolean value) {
        int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
        if (value) {
            systemUiVisibility |= flags;
        } else {
            systemUiVisibility &= ~flags;
        }
        getWindow().getDecorView().setSystemUiVisibility(systemUiVisibility);
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void setFullscreenFlags(boolean fullscreen) {
        int fullscreenFlags = View.SYSTEM_UI_FLAG_FULLSCREEN;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            fullscreenFlags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
        }

        setSystemUiFlags(fullscreenFlags, fullscreen);
    }

    private void initViews() {
        // bind views
        miFrame = (ConstraintLayout) findViewById(R.id.mi_frame);
        miPager = (FadeableViewPager) findViewById(R.id.mi_pager);
        miPagerIndicator = (InkPageIndicator) findViewById(R.id.mi_pager_indicator);
        miButtonCta = (TextSwitcher) findViewById(R.id.mi_button_cta);
        miButtonBack = (ImageButton) findViewById(R.id.mi_button_back);
        miButtonNext = (ImageButton) findViewById(R.id.mi_button_next);

        if (miButtonCta != null) {
            miButtonCta.setInAnimation(this, R.anim.mi_fade_in);
            miButtonCta.setOutAnimation(this, R.anim.mi_fade_out);
        }

        FragmentManager fragmentManager = getSupportFragmentManager();
        adapter = new SlideAdapter(fragmentManager);

        miPager.setAdapter(adapter);
        miPager.addOnPageChangeListener(listener);
        miPager.setCurrentItem(position, false);

        miPagerIndicator.setViewPager(miPager);

        resetButtonNextOnClickListener();
        resetButtonBackOnClickListener();

        CheatSheet.setup(miButtonNext);
        CheatSheet.setup(miButtonBack);
    }

    public void setButtonNextOnClickListener(View.OnClickListener onClickListener) {
        miButtonNext.setOnClickListener(onClickListener);
    }

    public void setButtonBackOnClickListener(View.OnClickListener onClickListener) {
        miButtonBack.setOnClickListener(onClickListener);
    }

    public void resetButtonNextOnClickListener() {
        miButtonNext.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                nextSlide();
            }
        });
    }

    public void resetButtonBackOnClickListener() {
        miButtonBack.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                performButtonBackPress();
            }
        });
    }

    private void smoothScrollPagerTo(final int position) {
        if (miPager.isFakeDragging())
            return;

        ValueAnimator animator = ValueAnimator.ofFloat(miPager.getCurrentItem(), position);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                if (miPager.isFakeDragging())
                    miPager.endFakeDrag();
                miPager.setCurrentItem(position);
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                if (miPager.isFakeDragging())
                    miPager.endFakeDrag();
            }
        });
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float position = (Float) animation.getAnimatedValue();

                fakeDragToPosition(position);
            }

            private boolean fakeDragToPosition(float position) {
                // The following mimics the underlying calculations in ViewPager
                float scrollX = miPager.getScrollX();
                int pagerWidth = miPager.getWidth();
                int currentPosition = miPager.getCurrentItem();

                if (position > currentPosition && Math.floor(position) != currentPosition && position % 1 != 0) {
                    miPager.setCurrentItem((int) Math.floor(position), false);
                } else if (position < currentPosition && Math.ceil(position) != currentPosition
                        && position % 1 != 0) {
                    miPager.setCurrentItem((int) Math.ceil(position), false);
                }

                if (!miPager.isFakeDragging() && !miPager.beginFakeDrag())
                    return false;

                miPager.fakeDragBy(scrollX - pagerWidth * position);
                return true;
            }
        });

        int distance = Math.abs(position - miPager.getCurrentItem());

        animator.setInterpolator(pageScrollInterpolator);
        animator.setDuration(calculateScrollDuration(distance));
        animator.start();
    }

    private long calculateScrollDuration(int distance) {
        return Math.round(pageScrollDuration * (distance + Math.sqrt(distance)) / 2);
    }

    @Override
    public boolean goToSlide(int position, boolean forceScroll) {
        int lastPosition = miPager.getCurrentItem();

        if (lastPosition >= adapter.getCount()) {
            finishIfNeeded();
        }

        int newPosition = lastPosition;

        position = Math.max(0, Math.min(position, getCount()));

        if (position > lastPosition) {
            // Go forward
            while (newPosition < position && (canGoForward(newPosition, true) || forceScroll)) {
                newPosition++;
            }
        } else if (position < lastPosition) {
            // Go backward
            while (newPosition > position && (canGoBackward(newPosition, true) || forceScroll)) {
                newPosition--;
            }
        } else {
            // Noting to do here
            return true;
        }

        boolean blocked = false;
        if (newPosition != position) {
            // Could not go the complete way to the given position.
            blocked = true;

            if (position > lastPosition) {
                AnimUtils.applyShakeAnimation(this, miButtonNext);
            } else if (position < lastPosition) {
                AnimUtils.applyShakeAnimation(this, miButtonBack);
            }
        }

        // Scroll to new position
        smoothScrollPagerTo(newPosition);

        return !blocked;
    }

    @Override
    public boolean nextSlide() {
        int currentItem = miPager.getCurrentItem();
        return goToSlide(currentItem + 1, false);
    }

    @Override
    public boolean forceNextSlide() {
        int currentItem = miPager.getCurrentItem();
        return goToSlide(currentItem + 1, true);
    }

    private int nextSlideAuto() {
        int lastPosition = miPager.getCurrentItem();
        int count = getCount();

        if (count == 1) {
            return 0;
        } else if (miPager.getCurrentItem() >= count - 1) {
            while (lastPosition >= 0 && canGoBackward(lastPosition, true)) {
                lastPosition--;
            }
            if (autoplayCounter > 0)
                autoplayCounter--;
        } else if (canGoForward(lastPosition, true)) {
            lastPosition++;
        }

        int distance = Math.abs(lastPosition - miPager.getCurrentItem());

        if (lastPosition == miPager.getCurrentItem())
            return 0;

        smoothScrollPagerTo(lastPosition);

        if (autoplayCounter == 0)
            return 0;
        return distance;

    }

    @Override
    public boolean previousSlide() {
        int currentItem = miPager.getCurrentItem();
        return goToSlide(currentItem - 1, false);
    }

    @Override
    public boolean forcePreviousSlide() {
        int currentItem = miPager.getCurrentItem();
        return goToSlide(currentItem - 1, true);
    }

    @Override
    public boolean goToLastSlide() {
        return goToSlide(getCount() - 1, false);
    }

    @Override
    public boolean goToFirstSlide() {
        return goToSlide(0, false);
    }

    private void performButtonBackPress() {
        if (buttonBackFunction == BUTTON_BACK_FUNCTION_SKIP) {
            goToSlide(getCount(), false);
        } else if (buttonBackFunction == BUTTON_BACK_FUNCTION_BACK) {
            previousSlide();
        }
    }

    private boolean canGoForward(int position, boolean notifyListeners) {
        if (position >= getCount()) {
            return false;
        }
        if (position < 0) {
            return true;
        }

        if (buttonNextFunction == BUTTON_NEXT_FUNCTION_NEXT && position >= getCount() - 1)
            //Block finishing when button "next" function is not "finish".
            return false;

        boolean canGoForward = (navigationPolicy == null || navigationPolicy.canGoForward(position))
                && getSlide(position).canGoForward();
        if (!canGoForward && notifyListeners) {
            for (OnNavigationBlockedListener listener : navigationBlockedListeners) {
                listener.onNavigationBlocked(position, OnNavigationBlockedListener.DIRECTION_FORWARD);
            }
        }
        return canGoForward;
    }

    private boolean canGoBackward(int position, boolean notifyListeners) {
        if (position <= 0) {
            return false;
        }
        if (position >= getCount()) {
            return true;
        }

        boolean canGoBackward = (navigationPolicy == null || navigationPolicy.canGoBackward(position))
                && getSlide(position).canGoBackward();
        if (!canGoBackward && notifyListeners) {
            for (OnNavigationBlockedListener listener : navigationBlockedListeners) {
                listener.onNavigationBlocked(position, OnNavigationBlockedListener.DIRECTION_BACKWARD);
            }
        }
        return canGoBackward;
    }

    private boolean finishIfNeeded() {
        if (positionOffset == 0 && position == adapter.getCount()) {
            Intent returnIntent = onSendActivityResult(RESULT_OK);
            if (returnIntent != null)
                setResult(RESULT_OK, returnIntent);
            else
                setResult(RESULT_OK);
            finish();
            overridePendingTransition(0, 0);
            return true;
        }
        return false;
    }

    @Nullable
    private Pair<CharSequence, ? extends View.OnClickListener> getButtonCta(int position) {
        if (position < getCount() && getSlide(position) instanceof ButtonCtaSlide) {
            ButtonCtaSlide slide = (ButtonCtaSlide) getSlide(position);
            if (slide.getButtonCtaClickListener() != null
                    && (slide.getButtonCtaLabel() != null || slide.getButtonCtaLabelRes() != 0)) {
                if (slide.getButtonCtaLabel() != null) {
                    return Pair.create(slide.getButtonCtaLabel(), slide.getButtonCtaClickListener());
                } else {
                    return Pair.create((CharSequence) getString(slide.getButtonCtaLabelRes()),
                            slide.getButtonCtaClickListener());
                }
            }
        }
        if (buttonCtaVisible) {
            if (buttonCtaLabelRes != 0) {
                return Pair.create((CharSequence) getString(buttonCtaLabelRes), new ButtonCtaClickListener());
            }
            if (!TextUtils.isEmpty(buttonCtaLabel)) {
                return Pair.create(buttonCtaLabel, new ButtonCtaClickListener());
            } else {
                return Pair.create((CharSequence) getString(R.string.mi_label_button_cta),
                        new ButtonCtaClickListener());
            }
        }
        return null;
    }

    private void updateTaskDescription() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            String title = getTitle().toString();
            Drawable iconDrawable = getApplicationInfo().loadIcon(getPackageManager());
            Bitmap icon = iconDrawable instanceof BitmapDrawable ? ((BitmapDrawable) iconDrawable).getBitmap()
                    : null;
            int colorPrimary;
            if (position < getCount()) {
                try {
                    colorPrimary = ContextCompat.getColor(IntroActivity.this, getBackgroundDark(position));
                } catch (Resources.NotFoundException e) {
                    colorPrimary = ContextCompat.getColor(IntroActivity.this, getBackground(position));
                }
            } else {
                TypedValue typedValue = new TypedValue();
                TypedArray a = obtainStyledAttributes(typedValue.data, new int[] { R.attr.colorPrimary });
                colorPrimary = a.getColor(0, 0);
                a.recycle();
            }
            colorPrimary = ColorUtils.setAlphaComponent(colorPrimary, 0xFF);

            setTaskDescription(new ActivityManager.TaskDescription(title, icon, colorPrimary));
        }
    }

    private void updateBackground() {
        @ColorInt
        int background;
        @ColorInt
        int backgroundNext;
        @ColorInt
        int backgroundDark;
        @ColorInt
        int backgroundDarkNext;

        if (position == getCount()) {
            background = Color.TRANSPARENT;
            backgroundNext = Color.TRANSPARENT;
            backgroundDark = Color.TRANSPARENT;
            backgroundDarkNext = Color.TRANSPARENT;
        } else {
            background = ContextCompat.getColor(IntroActivity.this, getBackground(position));
            backgroundNext = ContextCompat.getColor(IntroActivity.this,
                    getBackground(Math.min(position + 1, getCount() - 1)));

            background = ColorUtils.setAlphaComponent(background, 0xFF);
            backgroundNext = ColorUtils.setAlphaComponent(backgroundNext, 0xFF);

            try {
                backgroundDark = ContextCompat.getColor(IntroActivity.this, getBackgroundDark(position));
            } catch (Resources.NotFoundException e) {
                backgroundDark = ContextCompat.getColor(IntroActivity.this, R.color.mi_status_bar_background);
            }
            try {
                backgroundDarkNext = ContextCompat.getColor(IntroActivity.this,
                        getBackgroundDark(Math.min(position + 1, getCount() - 1)));
            } catch (Resources.NotFoundException e) {
                backgroundDarkNext = ContextCompat.getColor(IntroActivity.this, R.color.mi_status_bar_background);
            }
        }

        if (position + positionOffset >= adapter.getCount() - 1) {
            backgroundNext = ColorUtils.setAlphaComponent(background, 0x00);
            backgroundDarkNext = ColorUtils.setAlphaComponent(backgroundDark, 0x00);
        }

        background = (Integer) evaluator.evaluate(positionOffset, background, backgroundNext);
        backgroundDark = (Integer) evaluator.evaluate(positionOffset, backgroundDark, backgroundDarkNext);

        miFrame.setBackgroundColor(background);

        float[] backgroundDarkHsv = new float[3];
        Color.colorToHSV(backgroundDark, backgroundDarkHsv);
        //Slightly darken the background color a bit for more contrast
        backgroundDarkHsv[2] *= 0.95;
        int backgroundDarker = Color.HSVToColor(backgroundDarkHsv);
        miPagerIndicator.setPageIndicatorColor(backgroundDarker);
        ViewCompat.setBackgroundTintList(miButtonNext, ColorStateList.valueOf(backgroundDarker));
        ViewCompat.setBackgroundTintList(miButtonBack, ColorStateList.valueOf(backgroundDarker));

        @ColorInt
        int backgroundButtonCta = buttonCtaTintMode == BUTTON_CTA_TINT_MODE_TEXT
                ? ContextCompat.getColor(this, android.R.color.white)
                : backgroundDarker;
        ViewCompat.setBackgroundTintList(miButtonCta.getChildAt(0), ColorStateList.valueOf(backgroundButtonCta));
        ViewCompat.setBackgroundTintList(miButtonCta.getChildAt(1), ColorStateList.valueOf(backgroundButtonCta));

        int iconColor;
        if (ColorUtils.calculateLuminance(backgroundDark) > 0.4) {
            //Light background
            iconColor = ContextCompat.getColor(this, R.color.mi_icon_color_light);
        } else {
            //Dark background
            iconColor = ContextCompat.getColor(this, R.color.mi_icon_color_dark);
        }
        miPagerIndicator.setCurrentPageIndicatorColor(iconColor);
        DrawableCompat.setTint(miButtonNext.getDrawable(), iconColor);
        DrawableCompat.setTint(miButtonBack.getDrawable(), iconColor);

        @ColorInt
        int textColorButtonCta = buttonCtaTintMode == BUTTON_CTA_TINT_MODE_TEXT ? backgroundDarker : iconColor;
        ((Button) miButtonCta.getChildAt(0)).setTextColor(textColorButtonCta);
        ((Button) miButtonCta.getChildAt(1)).setTextColor(textColorButtonCta);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            getWindow().setStatusBarColor(backgroundDark);

            if (position == adapter.getCount()) {
                getWindow().setNavigationBarColor(Color.TRANSPARENT);
            } else if (position + positionOffset >= adapter.getCount() - 1) {
                TypedValue typedValue = new TypedValue();
                TypedArray a = obtainStyledAttributes(typedValue.data,
                        new int[] { android.R.attr.navigationBarColor });

                int defaultNavigationBarColor = a.getColor(0, Color.BLACK);

                a.recycle();

                int navigationBarColor = (Integer) evaluator.evaluate(positionOffset, defaultNavigationBarColor,
                        Color.TRANSPARENT);
                getWindow().setNavigationBarColor(navigationBarColor);
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
                int flagLightStatusBar = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
                if (ColorUtils.calculateLuminance(backgroundDark) > 0.4) {
                    //Light background
                    systemUiVisibility |= flagLightStatusBar;
                } else {
                    //Dark background
                    systemUiVisibility &= ~flagLightStatusBar;
                }
                getWindow().getDecorView().setSystemUiVisibility(systemUiVisibility);
            }
        }
    }

    private void updateButtonCta() {
        float realPosition = position + positionOffset;
        float yOffset = getResources().getDimensionPixelSize(R.dimen.mi_y_offset);

        if (realPosition < adapter.getCount()) {
            //Before fade
            Pair<CharSequence, ? extends View.OnClickListener> button = getButtonCta(position);
            Pair<CharSequence, ? extends View.OnClickListener> buttonNext = positionOffset == 0 ? null
                    : getButtonCta(position + 1);

            if (button == null) {
                if (buttonNext == null) {
                    //Hide button
                    miButtonCta.setVisibility(View.GONE);
                } else {
                    miButtonCta.setVisibility(View.VISIBLE);
                    //Fade in
                    if (!((Button) miButtonCta.getCurrentView()).getText().equals(buttonNext.first))
                        miButtonCta.setText(buttonNext.first);
                    miButtonCta.getChildAt(0).setOnClickListener(buttonNext.second);
                    miButtonCta.getChildAt(1).setOnClickListener(buttonNext.second);
                    miButtonCta.setAlpha(positionOffset);
                    miButtonCta.setScaleX(positionOffset);
                    miButtonCta.setScaleY(positionOffset);
                    ViewGroup.LayoutParams layoutParams = miButtonCta.getLayoutParams();
                    layoutParams.height = Math
                            .round(getResources().getDimensionPixelSize(R.dimen.mi_button_cta_height)
                                    * ACCELERATE_DECELERATE_INTERPOLATOR.getInterpolation(positionOffset));
                    miButtonCta.setLayoutParams(layoutParams);
                }
            } else {
                if (buttonNext == null) {
                    miButtonCta.setVisibility(View.VISIBLE);
                    //Fade out
                    if (!((Button) miButtonCta.getCurrentView()).getText().equals(button.first))
                        miButtonCta.setText(button.first);
                    miButtonCta.getChildAt(0).setOnClickListener(button.second);
                    miButtonCta.getChildAt(1).setOnClickListener(button.second);
                    miButtonCta.setAlpha(1 - positionOffset);
                    miButtonCta.setScaleX(1 - positionOffset);
                    miButtonCta.setScaleY(1 - positionOffset);
                    ViewGroup.LayoutParams layoutParams = miButtonCta.getLayoutParams();
                    layoutParams.height = Math
                            .round(getResources().getDimensionPixelSize(R.dimen.mi_button_cta_height)
                                    * ACCELERATE_DECELERATE_INTERPOLATOR.getInterpolation(1 - positionOffset));
                    miButtonCta.setLayoutParams(layoutParams);
                } else {
                    miButtonCta.setVisibility(View.VISIBLE);
                    ViewGroup.LayoutParams layoutParams = miButtonCta.getLayoutParams();
                    layoutParams.height = getResources().getDimensionPixelSize(R.dimen.mi_button_cta_height);
                    miButtonCta.setLayoutParams(layoutParams);
                    //Fade text
                    if (positionOffset >= 0.5f) {
                        if (!((Button) miButtonCta.getCurrentView()).getText().equals(buttonNext.first))
                            miButtonCta.setText(buttonNext.first);
                        miButtonCta.getChildAt(0).setOnClickListener(buttonNext.second);
                        miButtonCta.getChildAt(1).setOnClickListener(buttonNext.second);
                    } else {
                        if (!((Button) miButtonCta.getCurrentView()).getText().equals(button.first))
                            miButtonCta.setText(button.first);
                        miButtonCta.getChildAt(0).setOnClickListener(button.second);
                        miButtonCta.getChildAt(1).setOnClickListener(button.second);
                    }
                }
            }
        }

        if (realPosition < adapter.getCount() - 1) {
            //Reset
            miButtonCta.setTranslationY(0);
        } else {
            //Hide CTA button
            miButtonCta.setTranslationY(positionOffset * yOffset);
        }
    }

    private void updateButtonBackPosition() {
        float realPosition = position + positionOffset;
        float yOffset = getResources().getDimensionPixelSize(R.dimen.mi_y_offset);

        if (realPosition < 1 && buttonBackFunction == BUTTON_BACK_FUNCTION_BACK) {
            //Hide back button
            miButtonBack.setTranslationY((1 - positionOffset) * yOffset);
        } else if (realPosition < adapter.getCount() - 2) {
            //Reset
            miButtonBack.setTranslationY(0);
            miButtonBack.setTranslationX(0);
        } else if (realPosition < adapter.getCount() - 1) {
            //Scroll away skip button
            if (buttonBackFunction == BUTTON_BACK_FUNCTION_SKIP) {
                boolean rtl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1
                        && getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
                miButtonBack.setTranslationX(positionOffset * (rtl ? 1 : -1) * miPager.getWidth());
            } else {
                miButtonBack.setTranslationX(0);
            }
        } else {
            //Keep skip button scrolled away, hide next button
            if (buttonBackFunction == BUTTON_BACK_FUNCTION_SKIP) {
                boolean rtl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1
                        && getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
                miButtonBack.setTranslationX((rtl ? 1 : -1) * miPager.getWidth());
            } else {
                miButtonBack.setTranslationY(positionOffset * yOffset);
            }
        }
    }

    private void updateButtonNextPosition() {
        float realPosition = position + positionOffset;
        float yOffset = getResources().getDimensionPixelSize(R.dimen.mi_y_offset);

        if (realPosition < adapter.getCount() - 2) {
            //Reset
            miButtonNext.setTranslationY(0);
        } else if (realPosition < adapter.getCount() - 1) {
            //Reset finish button, hide next icon
            if (buttonNextFunction == BUTTON_NEXT_FUNCTION_NEXT_FINISH) {
                miButtonNext.setTranslationY(0);
            } else {
                miButtonNext.setTranslationY(positionOffset * yOffset);
            }
        } else if (realPosition >= adapter.getCount() - 1) {
            //Hide finish icon, keep next icon hidden
            if (buttonNextFunction == BUTTON_NEXT_FUNCTION_NEXT_FINISH) {
                miButtonNext.setTranslationY(positionOffset * yOffset);
            } else {
                miButtonNext.setTranslationY(-yOffset);
            }
        }
    }

    private void updatePagerIndicatorPosition() {
        float realPosition = position + positionOffset;
        float yOffset = getResources().getDimensionPixelSize(R.dimen.mi_y_offset);

        if (realPosition < adapter.getCount() - 1) {
            //Reset
            miPagerIndicator.setTranslationY(0);
        } else {
            //Hide CTA button
            miPagerIndicator.setTranslationY(positionOffset * yOffset);
        }
    }

    private void updateParallax() {
        if (position == getCount())
            return;

        Fragment fragment = getSlide(position).getFragment();
        Fragment fragmentNext = position < getCount() - 1 ? getSlide(position + 1).getFragment() : null;
        if (fragment instanceof Parallaxable) {
            ((Parallaxable) fragment).setOffset(positionOffset);
        }
        if (fragmentNext instanceof Parallaxable) {
            ((Parallaxable) fragmentNext).setOffset(-1 + positionOffset);
        }
    }

    private void updateFullscreen() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            if (adapter != null && position + positionOffset > adapter.getCount() - 1) {
                setFullscreenFlags(false);
            } else {
                setFullscreenFlags(fullscreen);
            }
        }
    }

    private void updateBackgroundFade() {
        float realPosition = position + positionOffset;

        if (realPosition < adapter.getCount() - 1) {
            //Reset
            miFrame.setAlpha(1);
        } else {
            //Fade background
            miFrame.setAlpha(1 - (positionOffset * 0.5f));
        }
    }

    private void updateScrollPositions() {
        updateBackground();
        updateButtonCta();
        updateButtonBackPosition();
        updateButtonNextPosition();
        updatePagerIndicatorPosition();
        updateParallax();
        updateFullscreen();
        updateBackgroundFade();
    }

    private void updateButtonNextDrawable() {
        float realPosition = position + positionOffset;
        float offset = 0;

        if (buttonNextFunction == BUTTON_NEXT_FUNCTION_NEXT_FINISH) {
            if (realPosition >= adapter.getCount() - 1) {
                offset = 1;
            } else if (realPosition >= adapter.getCount() - 2) {
                offset = positionOffset;
            }
        }

        if (offset <= 0) {
            miButtonNext.setImageResource(R.drawable.mi_ic_next);
            miButtonNext.getDrawable().setAlpha(0xFF);
        } else {
            miButtonNext.setImageResource(R.drawable.mi_ic_next_finish);
            if (miButtonNext.getDrawable() != null && miButtonNext.getDrawable() instanceof LayerDrawable) {
                LayerDrawable drawable = (LayerDrawable) miButtonNext.getDrawable();
                drawable.getDrawable(0).setAlpha((int) (0xFF * (1 - offset)));
                drawable.getDrawable(1).setAlpha((int) (0xFF * offset));
            } else {
                miButtonNext.setImageResource(offset > 0 ? R.drawable.mi_ic_finish : R.drawable.mi_ic_next);
            }
        }
    }

    private void updateButtonBackDrawable() {
        if (buttonBackFunction == BUTTON_BACK_FUNCTION_SKIP) {
            miButtonBack.setImageResource(R.drawable.mi_ic_skip);
        } else {
            miButtonBack.setImageResource(R.drawable.mi_ic_previous);
        }
    }

    @SuppressWarnings("unused")
    public void autoplay(@IntRange(from = 1) long delay, @IntRange(from = -1) int repeatCount) {
        autoplayCounter = repeatCount;
        autoplayDelay = delay;
        autoplayCallback = new Runnable() {
            @Override
            public void run() {
                if (autoplayCounter == 0) {
                    cancelAutoplay();
                    return;
                }
                int distance = nextSlideAuto();
                if (distance != 0)
                    autoplayHandler.postDelayed(autoplayCallback,
                            autoplayDelay + calculateScrollDuration(distance));
            }
        };
        autoplayHandler.postDelayed(autoplayCallback, autoplayDelay);
    }

    @SuppressWarnings("unused")
    public void autoplay(@IntRange(from = 1) long delay) {
        autoplay(delay, DEFAULT_AUTOPLAY_REPEAT_COUNT);
    }

    @SuppressWarnings("unused")
    public void autoplay(@IntRange(from = -1) int repeatCount) {
        autoplay(DEFAULT_AUTOPLAY_DELAY, repeatCount);
    }

    @SuppressWarnings("unused")
    public void autoplay() {
        autoplay(DEFAULT_AUTOPLAY_DELAY, DEFAULT_AUTOPLAY_REPEAT_COUNT);
    }

    @SuppressWarnings("unused")
    public void cancelAutoplay() {
        autoplayHandler.removeCallbacks(autoplayCallback);
        autoplayCallback = null;
        autoplayCounter = 0;
        autoplayDelay = 0;
    }

    @SuppressWarnings("unused")
    public boolean isAutoplaying() {
        return autoplayCallback != null;
    }

    @SuppressWarnings("unused")
    public long getPageScrollDuration() {
        return pageScrollDuration;
    }

    @SuppressWarnings("unused")
    public void setPageScrollDuration(@IntRange(from = 1) long pageScrollDuration) {
        this.pageScrollDuration = pageScrollDuration;
    }

    @SuppressWarnings("unused")
    public Interpolator getPageScrollInterpolator() {
        return pageScrollInterpolator;
    }

    @SuppressWarnings("unused")
    public void setPageScrollInterpolator(Interpolator pageScrollInterpolator) {
        this.pageScrollInterpolator = pageScrollInterpolator;
    }

    @SuppressWarnings("unused")
    public void setPageScrollInterpolator(@InterpolatorRes int interpolatorRes) {
        this.pageScrollInterpolator = AnimationUtils.loadInterpolator(this, interpolatorRes);
    }

    @SuppressWarnings("unused")
    public boolean isFullscreen() {
        return fullscreen;
    }

    @SuppressWarnings("unused")
    public void setFullscreen(boolean fullscreen) {
        this.fullscreen = fullscreen;
    }

    @SuppressWarnings("unused")
    public boolean isButtonCtaVisible() {
        return buttonCtaVisible;
    }

    @SuppressWarnings("unused")
    public void setButtonCtaVisible(boolean buttonCtaVisible) {
        this.buttonCtaVisible = buttonCtaVisible;
        updateButtonCta();
    }

    @ButtonCtaTintMode
    @SuppressWarnings("unused")
    public int getButtonCtaTintMode() {
        return buttonCtaTintMode;
    }

    @SuppressWarnings("unused")
    public void setButtonCtaTintMode(@ButtonCtaTintMode int buttonCtaTintMode) {
        this.buttonCtaTintMode = buttonCtaTintMode;
    }

    @ButtonBackFunction
    @SuppressWarnings("unused")
    public int getButtonBackFunction() {
        return buttonBackFunction;
    }

    @SuppressWarnings("unused")
    public void setButtonBackFunction(@ButtonBackFunction int buttonBackFunction) {
        this.buttonBackFunction = buttonBackFunction;
        switch (buttonBackFunction) {
        case BUTTON_BACK_FUNCTION_BACK:
            CheatSheet.setup(miButtonBack, R.string.mi_content_description_back);
            break;
        case BUTTON_BACK_FUNCTION_SKIP:
            CheatSheet.setup(miButtonBack, R.string.mi_content_description_skip);
            break;
        }
        updateButtonBackDrawable();
        updateButtonBackPosition();
    }

    @Deprecated
    @SuppressWarnings("unused")
    public boolean isSkipEnabled() {
        return buttonBackFunction == BUTTON_BACK_FUNCTION_SKIP;
    }

    @Deprecated
    @SuppressWarnings("unused")
    public void setSkipEnabled(boolean skipEnabled) {
        setButtonBackFunction(skipEnabled ? BUTTON_BACK_FUNCTION_SKIP : BUTTON_BACK_FUNCTION_BACK);
    }

    @ButtonNextFunction
    @SuppressWarnings("unused")
    public int getButtonNextFunction() {
        return buttonNextFunction;
    }

    public void setButtonNextFunction(@ButtonNextFunction int buttonNextFunction) {
        this.buttonNextFunction = buttonNextFunction;
        switch (buttonNextFunction) {
        case BUTTON_NEXT_FUNCTION_NEXT_FINISH:
            CheatSheet.setup(miButtonNext, R.string.mi_content_description_next_finish);
            break;
        case BUTTON_NEXT_FUNCTION_NEXT:
            CheatSheet.setup(miButtonNext, R.string.mi_content_description_next);
            break;
        }
        updateButtonNextDrawable();
        updateButtonNextPosition();
    }

    public View getContentView() {
        return findViewById(android.R.id.content);
    }

    @Deprecated
    @SuppressWarnings("unused")
    public boolean isFinishEnabled() {
        return buttonNextFunction == BUTTON_NEXT_FUNCTION_NEXT_FINISH;
    }

    @Deprecated
    @SuppressWarnings("unused")
    public void setFinishEnabled(boolean finishEnabled) {
        setButtonNextFunction(finishEnabled ? BUTTON_NEXT_FUNCTION_NEXT_FINISH : BUTTON_NEXT_FUNCTION_NEXT);
    }

    @SuppressWarnings("unused")
    public boolean isButtonBackVisible() {
        return miButtonBack.getVisibility() == View.VISIBLE;
    }

    @SuppressWarnings("unused")
    public void setButtonBackVisible(boolean visible) {
        miButtonBack.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
    }

    @SuppressWarnings("unused")
    public boolean isButtonNextVisible() {
        return miButtonNext.getVisibility() == View.VISIBLE;
    }

    @SuppressWarnings("unused")
    public void setButtonNextVisible(boolean visible) {
        miButtonNext.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
    }

    @SuppressWarnings("unused")
    public boolean isPagerIndicatorVisible() {
        return miPagerIndicator.getVisibility() == View.VISIBLE;
    }

    @SuppressWarnings("unused")
    public void setPagerIndicatorVisible(boolean visible) {
        miPagerIndicator.setVisibility(visible ? View.VISIBLE : View.GONE);
    }

    @Deprecated
    @SuppressWarnings("deprecation,unused")
    public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
        miPager.setOnPageChangeListener(listener);
        miPager.addOnPageChangeListener(this.listener);
    }

    @SuppressWarnings("unused")
    public void addOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
        miPager.addOnPageChangeListener(listener);
    }

    @SuppressWarnings("unused")
    public void removeOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
        if (listener != this.listener)
            miPager.removeOnPageChangeListener(listener);
    }

    @SuppressWarnings("unused")
    public View.OnClickListener getButtonCtaClickListener() {
        return buttonCtaClickListener;
    }

    @SuppressWarnings("unused")
    public void setButtonCtaClickListener(View.OnClickListener buttonCtaClickListener) {
        this.buttonCtaClickListener = buttonCtaClickListener;
        updateButtonCta();
    }

    @SuppressWarnings("unused")
    public CharSequence getButtonCtaLabel() {
        if (buttonCtaLabel != null)
            return buttonCtaLabel;
        return getString(buttonCtaLabelRes);
    }

    @SuppressWarnings("unused")
    public void setButtonCtaLabel(@StringRes int buttonCtaLabelRes) {
        this.buttonCtaLabelRes = buttonCtaLabelRes;
        this.buttonCtaLabel = null;
        updateButtonCta();
    }

    @SuppressWarnings("unused")
    public void setButtonCtaLabel(CharSequence buttonCtaLabel) {
        this.buttonCtaLabel = buttonCtaLabel;
        this.buttonCtaLabelRes = 0;
        updateButtonCta();
    }

    @SuppressWarnings("unused")
    public void setNavigationPolicy(NavigationPolicy navigationPolicy) {
        this.navigationPolicy = navigationPolicy;
    }

    @SuppressWarnings("unused")
    public void addOnNavigationBlockedListener(OnNavigationBlockedListener listener) {
        navigationBlockedListeners.add(listener);
    }

    @SuppressWarnings("unused")
    public void removeOnNavigationBlockedListener(OnNavigationBlockedListener listener) {
        navigationBlockedListeners.remove(listener);
    }

    @SuppressWarnings("unused")
    public void clearOnNavigationBlockedListeners() {
        navigationBlockedListeners.clear();
    }

    @SuppressWarnings("unused")
    public void lockSwipeIfNeeded() {
        if (position < getCount()) {
            miPager.setSwipeLeftEnabled(canGoForward(position, false));
            miPager.setSwipeRightEnabled(canGoBackward(position, false));
        }
    }

    @SuppressWarnings("unused")
    public void addSlide(int location, Slide object) {
        adapter.addSlide(location, object);
        notifyDataSetChanged();
    }

    @SuppressWarnings("unused")
    public boolean addSlide(Slide object) {
        boolean modified = adapter.addSlide(object);
        if (modified) {
            notifyDataSetChanged();
        }
        return modified;
    }

    @SuppressWarnings("unused")
    public boolean addSlides(int location, @NonNull Collection<? extends Slide> collection) {
        boolean modified = adapter.addSlides(location, collection);
        if (modified) {
            notifyDataSetChanged();
        }
        return modified;
    }

    @SuppressWarnings("unused")
    public boolean addSlides(@NonNull Collection<? extends Slide> collection) {
        boolean modified = adapter.addSlides(collection);
        if (modified) {
            notifyDataSetChanged();
        }
        return modified;
    }

    @SuppressWarnings("unused")
    public boolean clearSlides() {
        if (adapter.clearSlides()) {
            notifyDataSetChanged();
            return true;
        }
        return false;
    }

    @SuppressWarnings("unused")
    public boolean containsSlide(Object object) {
        return adapter.containsSlide(object);
    }

    @SuppressWarnings("unused")
    public boolean containsSlides(@NonNull Collection<?> collection) {
        return adapter.containsSlides(collection);
    }

    @SuppressWarnings("unused")
    public Slide getSlide(int location) {
        return adapter.getSlide(location);
    }

    @SuppressWarnings("unused")
    public int getSlidePosition(Slide slide) {
        return adapter.getItemPosition(slide);
    }

    @SuppressWarnings("unused")
    public int getCurrentSlidePosition() {
        return miPager.getCurrentItem();
    }

    @SuppressWarnings("unused")
    public Fragment getItem(int position) {
        return adapter.getItem(position);
    }

    @ColorRes
    @SuppressWarnings("unused")
    public int getBackground(int position) {
        return adapter.getBackground(position);
    }

    @ColorRes
    @SuppressWarnings("unused")
    public int getBackgroundDark(int position) {
        return adapter.getBackgroundDark(position);
    }

    @SuppressWarnings("unused")
    public List<Slide> getSlides() {
        return adapter.getSlides();
    }

    @SuppressWarnings("unused")
    public int indexOfSlide(Object object) {
        return adapter.indexOfSlide(object);
    }

    @SuppressWarnings("unused")
    public boolean isEmpty() {
        return adapter.isEmpty();
    }

    @SuppressWarnings("unused")
    public int getCount() {
        return adapter == null ? 0 : adapter.getCount();
    }

    @SuppressWarnings("unused")
    public int lastIndexOfSlide(Object object) {
        return adapter.lastIndexOfSlide(object);
    }

    @SuppressWarnings("unused")
    public Slide removeSlide(int location) {
        Slide object = adapter.removeSlide(location);
        notifyDataSetChanged();
        return object;
    }

    @SuppressWarnings("unused")
    public boolean removeSlide(Object object) {
        boolean modified = adapter.removeSlide(object);
        if (modified) {
            notifyDataSetChanged();
        }
        return modified;
    }

    @SuppressWarnings("unused")
    public boolean removeSlides(@NonNull Collection<?> collection) {
        boolean modified = adapter.removeSlides(collection);
        if (modified) {
            notifyDataSetChanged();
        }
        return modified;
    }

    @SuppressWarnings("unused")
    public boolean retainSlides(@NonNull Collection<?> collection) {
        boolean modified = adapter.retainSlides(collection);
        if (modified) {
            notifyDataSetChanged();
        }
        return modified;
    }

    @SuppressWarnings("unused")
    public Slide setSlide(int location, Slide object) {
        Slide oldObject = adapter.setSlide(location, object);
        notifyDataSetChanged();
        return oldObject;
    }

    @SuppressWarnings("unused")
    public List<Slide> setSlides(List<? extends Slide> list) {
        List<Slide> oldList = adapter.setSlides(list);
        notifyDataSetChanged();
        return oldList;
    }

    public void setPageTransformer(boolean reverseDrawingOrder, ViewPager.PageTransformer transformer) {
        miPager.setPageTransformer(reverseDrawingOrder, transformer);
    }

    public void notifyDataSetChanged() {
        if (!activityCreated) {
            // Don't notify any listener until the activity is created
            return;
        }

        int position = this.position;
        miPager.setAdapter(adapter);
        miPager.setCurrentItem(position);

        if (finishIfNeeded()) {
            return;
        }

        updateTaskDescription();
        updateButtonBackDrawable();
        updateButtonNextDrawable();
        updateScrollPositions();
        lockSwipeIfNeeded();
    }

    private class IntroPageChangeListener extends FadeableViewPager.SimpleOnOverscrollPageChangeListener {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            IntroActivity.this.position = (int) Math.floor(position + positionOffset);
            IntroActivity.this.positionOffset = (((position + positionOffset) % 1) + 1) % 1;

            if (finishIfNeeded()) {
                return;
            }

            //Lock while scrolling a slide near its edges to lock (uncommon) multiple page swipes
            if (Math.abs(positionOffset) < 0.1f) {
                lockSwipeIfNeeded();
            }

            updateButtonNextDrawable();
            updateScrollPositions();
        }

        @Override
        public void onPageSelected(int position) {
            IntroActivity.this.position = position;
            updateTaskDescription();
            lockSwipeIfNeeded();
        }
    }

    private class ButtonCtaClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            goToSlide(getCount(), false);
        }
    }
}