com.oguzbabaoglu.cardpager.CardPager.java Source code

Java tutorial

Introduction

Here is the source code for com.oguzbabaoglu.cardpager.CardPager.java

Source

/*
 * Copyright (C) 2015 Oguz Babaoglu
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.oguzbabaoglu.cardpager;

import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Rect;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewConfigurationCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Interpolator;
import android.widget.Scroller;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

/**
 * A modified ViewPager that behaves like a card stack.
 * Pages can be swiped left or right, moving onto the next page in the stack.
 * A common use case is to like or dislike content based on swipe direction.
 * Original class: {@link android.support.v4.view.ViewPager}.
 *
 * @author Oguz Babaoglu
 */
public class CardPager extends ViewGroup {

    // If the page is at most this far from its idle position,
    // allow the user to "catch" the page.
    private static final int CATCH_ALLOWANCE = 75; // dp

    private static final int DEFAULT_OFFSCREEN_PAGES = 1;
    private static final int MAX_SETTLE_DURATION = 600; // ms
    private static final int MIN_DISTANCE_FOR_FLING = 25; // dp
    private static final int SMOOTH_SCROLL_FACTOR = 4;
    private static final float SNAP_FACTOR = .3f;

    // Minimum fling velocity is larger than of ViewPager.
    // We want to minimize accidental swipes, there is no way of going back.
    private static final int MIN_FLING_VELOCITY = 800; // dp

    private static final float MIN_SCALE = 0.75f;

    // Offsets of the first and last items, if known.
    // Set during population, used to determine if we are at the beginning
    // or end of the pager data set during touch scrolling.
    private float firstOffset = -Float.MAX_VALUE;
    private float lastOffset = Float.MAX_VALUE;

    private boolean inLayout;
    private boolean firstLayout = true;
    private boolean populatePending;
    private boolean dragInProgress;
    private boolean unableToDrag;

    private int currentItem; // Index of currently displayed page.
    private float virtualPos; // Relative position of displayed page.
    private int lastScroll;
    private boolean reversePos; // True if we are currently inverting touches.

    private float density;
    private int restoredCurItem = -1;
    private Parcelable restoredAdapterState = null;
    private ClassLoader restoredClassLoader = null;

    private Scroller scroller;
    private PagerAdapter pagerAdapter;
    private PagerObserver pagerObserver;
    private OnCardChangeListener onCardChangeListener;

    private ArrayList<View> drawingOrderedChildren;

    private static final Comparator<ItemInfo> ITEM_COMPARATOR = new Comparator<ItemInfo>() {
        @Override
        public int compare(ItemInfo lhs, ItemInfo rhs) {
            return lhs.position - rhs.position;
        }
    };

    private static final Comparator<View> VIEW_COMPARATOR = new Comparator<View>() {
        @Override
        public int compare(View lhs, View rhs) {
            final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
            final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
            return llp.position - rlp.position;
        }
    };

    private static final Interpolator INTERPOLATOR = new Interpolator() {
        public float getInterpolation(float t) {
            t -= 1.0f;
            return t * t * t * t * t + 1.0f;
        }
    };

    private int offscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;

    /**
     * Allow sloppy touch work from the user.
     */
    private int touchSlop;

    /**
     * ID of the active pointer. This is used to retain consistency during
     * drags/flings if multiple pointers are used.
     */
    private int activePointerId = INVALID_POINTER;

    /**
     * Sentinel value for no current active pointer.
     * Used by {@link #activePointerId}.
     */
    private static final int INVALID_POINTER = -1;

    /**
     * Position of the last motion event.
     */
    private float lastMotionX;
    private float lastMotionY;
    private float initialMotionX;
    private float initialMotionY;

    /**
     * Determines speed during touch scrolling.
     */
    private VelocityTracker velocityTracker;
    private int minimumVelocity;
    private int maximumVelocity;
    private int flingDistance;
    private int catchAllowance;

    /**
     * Indicates that the pager is in an idle, settled state. The current page
     * is fully in view and no animation is in progress.
     */
    public static final int SCROLL_STATE_IDLE = 0;

    /**
     * Indicates that the pager is currently being dragged by the user.
     */
    public static final int SCROLL_STATE_DRAGGING = 1;

    /**
     * Indicates that the pager is in the process of settling to a final position.
     */
    public static final int SCROLL_STATE_SETTLING = 2;

    private final Runnable endScrollRunnable = new Runnable() {
        public void run() {
            setScrollState(SCROLL_STATE_IDLE);
            populate();
        }
    };

    private int scrollState = SCROLL_STATE_IDLE;

    private final ArrayList<ItemInfo> items = new ArrayList<>();
    private final ItemInfo tempItem = new ItemInfo();

    /**
     * Holds info about page.
     */
    static class ItemInfo {
        Object object;
        int position;
        boolean scrolling;
        float widthFactor;
        float offset;
    }

    public CardPager(Context context) {
        super(context);
        initCardPager();
    }

    public CardPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        initCardPager();
    }

    void initCardPager() {

        setWillNotDraw(true);
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
        setFocusable(true);
        setChildrenDrawingOrderEnabled(true);

        final Context context = getContext();
        final ViewConfiguration configuration = ViewConfiguration.get(context);

        density = context.getResources().getDisplayMetrics().density;
        scroller = new Scroller(context, INTERPOLATOR);

        touchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
        maximumVelocity = configuration.getScaledMaximumFlingVelocity();

        minimumVelocity = (int) (MIN_FLING_VELOCITY * density);
        flingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
        catchAllowance = (int) (CATCH_ALLOWANCE * density);
    }

    @Override
    protected void onDetachedFromWindow() {
        removeCallbacks(endScrollRunnable);
        super.onDetachedFromWindow();
    }

    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        final int index = childCount - 1 - i;
        return ((LayoutParams) drawingOrderedChildren.get(index).getLayoutParams()).childIndex;
    }

    /**
     * Set scroll state. Will enable hardware layers in children if there is motion.
     *
     * @param newState new state
     */
    private void setScrollState(int newState) {
        if (scrollState == newState) {
            return;
        }

        scrollState = newState;

        if (onCardChangeListener != null) {
            onCardChangeListener.onCardScrollStateChanged(newState);
        }

        enableLayers(newState != SCROLL_STATE_IDLE);
    }

    /**
     * Set a PagerAdapter that will supply views for this pager as needed.
     *
     * @param adapter Adapter to use
     */
    public void setAdapter(PagerAdapter adapter) {
        if (pagerAdapter != null) {
            pagerAdapter.unregisterDataSetObserver(pagerObserver);
            pagerAdapter.startUpdate(this);
            for (int i = 0; i < items.size(); i++) {
                final ItemInfo ii = items.get(i);
                pagerAdapter.destroyItem(this, ii.position, ii.object);
            }
            pagerAdapter.finishUpdate(this);
            items.clear();
            removeCardViews();
            currentItem = 0;
            scrollTo(0, 0);
        }

        pagerAdapter = adapter;

        if (pagerAdapter != null) {
            if (pagerObserver == null) {
                pagerObserver = new PagerObserver();
            }
            pagerAdapter.registerDataSetObserver(pagerObserver);
            populatePending = false;
            final boolean wasFirstLayout = firstLayout;
            firstLayout = true;

            if (restoredCurItem >= 0) {
                pagerAdapter.restoreState(restoredAdapterState, restoredClassLoader);
                setCurrentItemInternal(restoredCurItem, false, true);
                restoredCurItem = -1;
                restoredAdapterState = null;
                restoredClassLoader = null;
            } else if (!wasFirstLayout) {
                populate();
            } else {
                requestLayout();
            }
        }
    }

    /**
     * Remove all child views.
     */
    private void removeCardViews() {
        for (int i = 0; i < getChildCount(); i++) {
            removeViewAt(i);
            i--;
        }
    }

    /**
     * Retrieve the current adapter supplying pages.
     *
     * @return The currently registered PagerAdapter
     */
    public PagerAdapter getAdapter() {
        return pagerAdapter;
    }

    /**
     * @return content width
     */
    private int getClientWidth() {
        return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
    }

    /**
     * Dismiss the current card.
     *
     * @param exitRight True to scroll page to the right
     */
    public void dismissCard(boolean exitRight) {

        if (onCardChangeListener != null) {
            onCardChangeListener.onCardDismissed(currentItem, exitRight);
        }

        populatePending = false;
        reversePos = exitRight;
        setCurrentItemInternal(currentItem + 1, true, false);
    }

    public int getCurrentItem() {
        return currentItem;
    }

    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
        setCurrentItemInternal(item, smoothScroll, always, 0);
    }

    void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
        if (pagerAdapter == null || pagerAdapter.getCount() <= 0) {
            return;
        }
        if (!always && currentItem == item && items.size() != 0) {
            return;
        }

        if (item < 0) {
            item = 0;
        } else if (item >= pagerAdapter.getCount()) {
            item = pagerAdapter.getCount() - 1;
        }
        final int pageLimit = offscreenPageLimit;
        if (item > (currentItem + pageLimit) || item < (currentItem - pageLimit)) {
            // We are doing a jump by more than one page.  To avoid
            // glitches, we want to keep all current pages in the view
            // until the scroll ends.
            for (int i = 0; i < items.size(); i++) {
                items.get(i).scrolling = true;
            }
        }

        if (firstLayout) {
            // We don't have any idea how big we are yet and shouldn't have any pages either.
            // Just set things up and let the pending layout handle things.
            currentItem = item;
            requestLayout();

        } else {
            populate(item);
            scrollToItem(item, smoothScroll, velocity);
        }
    }

    private void scrollToItem(int item, boolean smoothScroll, int velocity) {
        final ItemInfo curInfo = infoForPosition(item);
        int destX = 0;
        if (curInfo != null) {
            destX = (int) (getClientWidth() * Math.max(firstOffset, Math.min(curInfo.offset, lastOffset)));
        }
        if (smoothScroll) {
            smoothScrollTo(destX, 0, velocity);

        } else {
            completeScroll(false);
            scrollTo(destX, 0);
            pageScrolled(destX);
        }
    }

    public void setOnCardChangeListener(OnCardChangeListener onCardChangeListener) {
        this.onCardChangeListener = onCardChangeListener;
    }

    /**
     * We want the duration of the page snap animation to be influenced by the distance that
     * the screen has to travel, however, we don't want this duration to be effected in a
     * purely linear fashion. Instead, we use this method to moderate the effect that the distance
     * of travel has on the overall snap duration.
     *
     * @param f unmodified distance factor
     * @return modified distance factor
     */
    float distanceInfluenceForSnapDuration(float f) {
        f -= 0.5f; // center the values about 0.
        f *= SNAP_FACTOR * Math.PI / 2.0f;
        return (float) Math.sin(f);
    }

    /**
     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
     *
     * @param x        the number of pixels to scroll by on the X axis
     * @param y        the number of pixels to scroll by on the Y axis
     * @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
     */
    void smoothScrollTo(int x, int y, int velocity) {
        if (getChildCount() == 0) {
            // Nothing to do.
            return;
        }
        final int sx = getScrollX();
        final int sy = getScrollY();
        final int dx = x - sx;
        final int dy = y - sy;
        if (dx == 0 && dy == 0) {
            completeScroll(false);
            populate();
            setScrollState(SCROLL_STATE_IDLE);
            return;
        }

        setScrollState(SCROLL_STATE_SETTLING);

        final int width = getClientWidth();
        final int halfWidth = width / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
        final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio);

        int duration;
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration = SMOOTH_SCROLL_FACTOR * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float pageWidth = width * pagerAdapter.getPageWidth(currentItem);
            final float pageDelta = (float) Math.abs(dx) / pageWidth;
            duration = (int) ((pageDelta + 1) * 100);
        }
        duration = Math.min(duration, MAX_SETTLE_DURATION);

        scroller.startScroll(sx, sy, dx, dy, duration);
        ViewCompat.postInvalidateOnAnimation(this);
    }

    ItemInfo addNewItem(int position, int index) {
        final ItemInfo ii = new ItemInfo();
        ii.position = position;
        ii.object = pagerAdapter.instantiateItem(this, position);
        ii.widthFactor = pagerAdapter.getPageWidth(position);
        if (index < 0 || index >= items.size()) {
            items.add(ii);
        } else {
            items.add(index, ii);
        }
        return ii;
    }

    void dataSetChanged() {
        // This method only gets called if our observer is attached, so pagerAdapter is non-null.

        final int adapterCount = pagerAdapter.getCount();
        boolean needPopulate = items.size() < offscreenPageLimit * 2 + 1 && items.size() < adapterCount;
        int newCurrItem = currentItem;

        boolean isUpdating = false;
        for (int i = 0; i < items.size(); i++) {
            final ItemInfo ii = items.get(i);
            final int newPos = pagerAdapter.getItemPosition(ii.object);

            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }

            if (newPos == PagerAdapter.POSITION_NONE) {
                items.remove(i);
                i--;

                if (!isUpdating) {
                    pagerAdapter.startUpdate(this);
                    isUpdating = true;
                }

                pagerAdapter.destroyItem(this, ii.position, ii.object);
                needPopulate = true;

                if (currentItem == ii.position) {
                    // Keep the current item in the valid range
                    newCurrItem = Math.max(0, Math.min(currentItem, adapterCount - 1));
                    needPopulate = true;
                }
                continue;
            }

            if (ii.position != newPos) {
                if (ii.position == currentItem) {
                    // Our current item changed position. Follow it.
                    newCurrItem = newPos;
                }

                ii.position = newPos;
                needPopulate = true;
            }
        }

        if (isUpdating) {
            pagerAdapter.finishUpdate(this);
        }

        Collections.sort(items, ITEM_COMPARATOR);

        if (needPopulate) {
            // Reset our known page widths; populate will recompute them.
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                lp.widthFactor = 0.f;
            }

            setCurrentItemInternal(newCurrItem, false, true);
            requestLayout();
        }
    }

    void populate() {
        populate(currentItem);
    }

    void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        int focusDirection = View.FOCUS_FORWARD;
        if (currentItem != newCurrentItem) {
            focusDirection = currentItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
            oldCurInfo = infoForPosition(currentItem);
            currentItem = newCurrentItem;
        }

        if (pagerAdapter == null) {
            sortChildDrawingOrder();
            return;
        }

        // Bail now if we are waiting to populate.  This is to hold off
        // on creating views from the time the user releases their finger to
        // fling to a new position until we have finished the scroll to
        // that position, avoiding glitches from happening at that point.
        if (populatePending) {
            sortChildDrawingOrder();
            return;
        }

        // Also, don't populate until we are attached to a window.  This is to
        // avoid trying to populate before we have restored our view hierarchy
        // state and conflicting with what is restored.
        if (getWindowToken() == null) {
            return;
        }

        pagerAdapter.startUpdate(this);

        final int pageLimit = offscreenPageLimit;
        final int startPos = Math.max(0, currentItem - pageLimit);
        final int adapterCount = pagerAdapter.getCount();
        final int endPos = Math.min(adapterCount - 1, currentItem + pageLimit);

        // Locate the currently focused item or add it if needed.
        int curIndex;
        ItemInfo curItem = null;
        for (curIndex = 0; curIndex < items.size(); curIndex++) {
            final ItemInfo ii = items.get(curIndex);
            if (ii.position == currentItem) {
                curItem = ii;
                break;
            }
        }

        if (curItem == null && adapterCount > 0) {
            curItem = addNewItem(currentItem, curIndex);
        }

        // Fill 3x the available width or up to the number of offscreen
        // pages requested to either side, whichever is larger.
        // If we have no current item we have no work to do.
        if (curItem != null) {
            float extraWidthLeft = 0.f;
            int itemIndex = curIndex - 1;
            ItemInfo ii = itemIndex >= 0 ? items.get(itemIndex) : null;
            final int clientWidth = getClientWidth();
            final float leftWidthNeeded = clientWidth <= 0 ? 0
                    : 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
            for (int pos = currentItem - 1; pos >= 0; pos--) {
                if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                    if (ii == null) {
                        break;
                    }
                    if (pos == ii.position && !ii.scrolling) {
                        items.remove(itemIndex);
                        pagerAdapter.destroyItem(this, pos, ii.object);

                        itemIndex--;
                        curIndex--;
                        ii = itemIndex >= 0 ? items.get(itemIndex) : null;
                    }
                } else if (ii != null && pos == ii.position) {
                    extraWidthLeft += ii.widthFactor;
                    itemIndex--;
                    ii = itemIndex >= 0 ? items.get(itemIndex) : null;
                } else {
                    ii = addNewItem(pos, itemIndex + 1);
                    extraWidthLeft += ii.widthFactor;
                    curIndex++;
                    ii = itemIndex >= 0 ? items.get(itemIndex) : null;
                }
            }

            float extraWidthRight = curItem.widthFactor;
            itemIndex = curIndex + 1;
            if (extraWidthRight < 2.f) {
                ii = itemIndex < items.size() ? items.get(itemIndex) : null;
                final float rightWidthNeeded = clientWidth <= 0 ? 0
                        : (float) getPaddingRight() / (float) clientWidth + 2.f;
                for (int pos = currentItem + 1; pos < adapterCount; pos++) {
                    if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
                        if (ii == null) {
                            break;
                        }
                        if (pos == ii.position && !ii.scrolling) {
                            items.remove(itemIndex);
                            pagerAdapter.destroyItem(this, pos, ii.object);

                            ii = itemIndex < items.size() ? items.get(itemIndex) : null;
                        }
                    } else if (ii != null && pos == ii.position) {
                        extraWidthRight += ii.widthFactor;
                        itemIndex++;
                        ii = itemIndex < items.size() ? items.get(itemIndex) : null;
                    } else {
                        ii = addNewItem(pos, itemIndex);
                        itemIndex++;
                        extraWidthRight += ii.widthFactor;
                        ii = itemIndex < items.size() ? items.get(itemIndex) : null;
                    }
                }
            }

            calculatePageOffsets(curItem, curIndex, oldCurInfo);
        }

        pagerAdapter.setPrimaryItem(this, currentItem, curItem != null ? curItem.object : null);

        pagerAdapter.finishUpdate(this);

        // Check width measurement of current pages and drawing sort order.
        // Update LayoutParams as needed.
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            lp.childIndex = i;
            if (lp.widthFactor == 0.f) {
                // 0 means requery the adapter for this, it doesn't have a valid width.
                final ItemInfo ii = infoForChild(child);
                if (ii != null) {
                    lp.widthFactor = ii.widthFactor;
                    lp.position = ii.position;
                }
            }
        }

        sortChildDrawingOrder();
        checkFocus(focusDirection);
    }

    /**
     * Check if child needs focus.
     *
     * @param focusDirection focusDirection
     */
    private void checkFocus(int focusDirection) {

        if (hasFocus()) {
            View currentFocused = findFocus();
            ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
            if (ii == null || ii.position != currentItem) {
                for (int i = 0; i < getChildCount(); i++) {
                    View child = getChildAt(i);
                    ii = infoForChild(child);
                    if (ii != null && ii.position == currentItem) {
                        if (child.requestFocus(focusDirection)) {
                            break;
                        }
                    }
                }
            }
        }
    }

    /**
     * Sorts children in reverse order for drawing.
     */
    private void sortChildDrawingOrder() {

        if (drawingOrderedChildren == null) {
            drawingOrderedChildren = new ArrayList<>();
        } else {
            drawingOrderedChildren.clear();
        }
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            drawingOrderedChildren.add(child);
        }
        Collections.sort(drawingOrderedChildren, VIEW_COMPARATOR);
    }

    private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {

        if (pagerAdapter == null) {
            return;
        }

        final int adapterCount = pagerAdapter.getCount();
        final float marginOffset = 0;
        // Fix up offsets for later layout.
        if (oldCurInfo != null) {
            final int oldCurPosition = oldCurInfo.position;
            // Base offsets off of oldCurInfo.
            if (oldCurPosition < curItem.position) {
                int itemIndex = 0;
                ItemInfo ii;
                float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
                for (int pos = oldCurPosition + 1; pos <= curItem.position && itemIndex < items.size(); pos++) {
                    ii = items.get(itemIndex);
                    while (pos > ii.position && itemIndex < items.size() - 1) {
                        itemIndex++;
                        ii = items.get(itemIndex);
                    }
                    while (pos < ii.position) {
                        // We don't have an item populated for this,
                        // ask the adapter for an offset.
                        offset += pagerAdapter.getPageWidth(pos) + marginOffset;
                        pos++;
                    }
                    ii.offset = offset;
                    offset += ii.widthFactor + marginOffset;
                }
            } else if (oldCurPosition > curItem.position) {
                int itemIndex = items.size() - 1;
                ItemInfo ii;
                float offset = oldCurInfo.offset;
                for (int pos = oldCurPosition - 1; pos >= curItem.position && itemIndex >= 0; pos--) {
                    ii = items.get(itemIndex);
                    while (pos < ii.position && itemIndex > 0) {
                        itemIndex--;
                        ii = items.get(itemIndex);
                    }
                    while (pos > ii.position) {
                        // We don't have an item populated for this,
                        // ask the adapter for an offset.
                        offset -= pagerAdapter.getPageWidth(pos) + marginOffset;
                        pos--;
                    }
                    offset -= ii.widthFactor + marginOffset;
                    ii.offset = offset;
                }
            }
        }

        // Base all offsets off of curItem.
        final int itemCount = items.size();
        float offset = curItem.offset;
        int pos = curItem.position - 1;
        firstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
        lastOffset = curItem.position == adapterCount - 1 ? curItem.offset + curItem.widthFactor - 1
                : Float.MAX_VALUE;
        // Previous pages
        for (int i = curIndex - 1; i >= 0; i--, pos--) {
            final ItemInfo ii = items.get(i);
            while (pos > ii.position) {
                offset -= pagerAdapter.getPageWidth(pos--) + marginOffset;
            }
            offset -= ii.widthFactor + marginOffset;
            ii.offset = offset;
            if (ii.position == 0) {
                firstOffset = offset;
            }
        }
        offset = curItem.offset + curItem.widthFactor + marginOffset;
        pos = curItem.position + 1;
        // Next pages
        for (int i = curIndex + 1; i < itemCount; i++, pos++) {
            final ItemInfo ii = items.get(i);
            while (pos < ii.position) {
                offset += pagerAdapter.getPageWidth(pos++) + marginOffset;
            }
            if (ii.position == adapterCount - 1) {
                lastOffset = offset + ii.widthFactor - 1;
            }
            ii.offset = offset;
            offset += ii.widthFactor + marginOffset;
        }
    }

    @Override
    public void addView(@NonNull View child, int index, ViewGroup.LayoutParams params) {
        if (!checkLayoutParams(params)) {
            params = generateLayoutParams(params);
        }
        final LayoutParams lp = (LayoutParams) params;
        if (inLayout) {
            lp.needsMeasure = true;
            addViewInLayout(child, index, params);
        } else {
            super.addView(child, index, params);
        }
    }

    @Override
    public void removeView(@NonNull View view) {
        if (inLayout) {
            removeViewInLayout(view);
        } else {
            super.removeView(view);
        }
    }

    ItemInfo infoForChild(View child) {
        for (int i = 0; i < items.size(); i++) {
            ItemInfo ii = items.get(i);
            if (pagerAdapter.isViewFromObject(child, ii.object)) {
                return ii;
            }
        }
        return null;
    }

    ItemInfo infoForAnyChild(View child) {
        ViewParent parent;
        while ((parent = child.getParent()) != this) {
            if (parent == null || !(parent instanceof View)) {
                return null;
            }
            child = (View) parent;
        }
        return infoForChild(child);
    }

    ItemInfo infoForPosition(int position) {
        for (int i = 0; i < items.size(); i++) {
            ItemInfo ii = items.get(i);
            if (ii.position == position) {
                return ii;
            }
        }
        return null;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        firstLayout = true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // For simple implementation, our internal size is always 0.
        // We depend on the container to specify the layout size of
        // our view.  We can't really know what it is since we will be
        // adding and removing different arbitrary views and do not
        // want the layout to change as this happens.
        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));

        final int measuredWidth = getMeasuredWidth();

        // Children are just made to fill our space.
        int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
        int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);

        // Make sure we have created all fragments that we need to have shown.
        inLayout = true;
        populate();
        inLayout = false;

        // Page views.
        final int size = getChildCount();
        for (int i = 0; i < size; ++i) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // Make sure scroll position is set correctly.
        if (w != oldw) {
            recomputeScrollPosition(w, oldw, 0, 0);
        }
    }

    private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) {
        if (oldWidth > 0 && !items.isEmpty()) {
            final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin;
            final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight() + oldMargin;
            final int xpos = getScrollX();
            final float pageOffset = (float) xpos / oldWidthWithMargin;
            final int newOffsetPixels = (int) (pageOffset * widthWithMargin);

            scrollTo(newOffsetPixels, getScrollY());
            if (!scroller.isFinished()) {
                // We now return to your regularly scheduled scroll, already in progress.
                final int newDuration = scroller.getDuration() - scroller.timePassed();
                ItemInfo targetInfo = infoForPosition(currentItem);
                if (targetInfo != null) {
                    scroller.startScroll(newOffsetPixels, 0, (int) (targetInfo.offset * width), 0, newDuration);
                }
            }
        } else {
            final ItemInfo ii = infoForPosition(currentItem);
            final float scrollOffset = ii != null ? Math.min(ii.offset, lastOffset) : 0;
            final int scrollPos = (int) (scrollOffset * (width - getPaddingLeft() - getPaddingRight()));
            if (scrollPos != getScrollX()) {
                completeScroll(false);
                scrollTo(scrollPos, getScrollY());
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        int width = r - l;
        int height = b - t;
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();

        final int childWidth = width - paddingLeft - paddingRight;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);

            if (child.getVisibility() == GONE) {
                continue;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            ItemInfo ii;
            if ((ii = infoForChild(child)) != null) {
                final int loff = (int) (childWidth * ii.offset);
                final int childLeft = paddingLeft + loff;
                if (lp.needsMeasure) {
                    // This was added during layout and needs measurement.
                    // Do it now that we know what we're working with.
                    lp.needsMeasure = false;
                    final int widthSpec = MeasureSpec.makeMeasureSpec((int) (childWidth * lp.widthFactor),
                            MeasureSpec.EXACTLY);
                    final int heightSpec = MeasureSpec.makeMeasureSpec(height - paddingTop - paddingBottom,
                            MeasureSpec.EXACTLY);
                    child.measure(widthSpec, heightSpec);
                }

                child.layout(childLeft, paddingTop, childLeft + child.getMeasuredWidth(),
                        paddingTop + child.getMeasuredHeight());
            }

        }

        if (firstLayout) {
            scrollToItem(currentItem, false, 0);
        }
        firstLayout = false;
    }

    @Override
    public void computeScroll() {
        if (!scroller.isFinished() && scroller.computeScrollOffset()) {
            int oldX = getScrollX();
            int oldY = getScrollY();
            int x = scroller.getCurrX();
            int y = scroller.getCurrY();

            if (oldX != x || oldY != y) {
                scrollTo(x, y);
                if (!pageScrolled(x)) {
                    scroller.abortAnimation();
                    scrollTo(0, y);
                }
            }

            // Keep on drawing until the animation has finished.
            ViewCompat.postInvalidateOnAnimation(this);
            return;
        }

        // Done with scroll, clean up state.
        completeScroll(true);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);

        if (l == getClientWidth() * currentItem) {
            virtualPos = 0;
        }
    }

    /**
     * Called when page is scrolled. Updates virtualPos of page according to scroll position.
     *
     * @param xPos xPos
     * @return true if scrolled
     */
    private boolean pageScrolled(int xPos) {

        if (items.size() == 0) {
            return false;
        }

        final int deltaScroll = xPos - lastScroll;
        virtualPos = reversePos ? virtualPos + deltaScroll : virtualPos - deltaScroll;
        lastScroll = xPos;

        final int width = getClientWidth();
        final float pageOffset = virtualPos / width;

        if (onCardChangeListener != null) {
            onCardChangeListener.onCardScrolled(currentItem, pageOffset, (int) virtualPos);
        }

        onPageScrolled();

        return true;
    }

    /**
     * This method will be invoked when the current page is scrolled, either as part
     * of a programmatically initiated smooth scroll or a user initiated touch scroll.
     */
    protected void onPageScrolled() {

        final int scrollX = getScrollX();
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
            transformPage(child, transformPos);
        }
    }

    /**
     * Transforms the top and bottom pages with changing scroll.
     * If top page is moving to the right side (virtualPos > 0)
     * we need to counteract the scroll by twice as much.
     *
     * @param view     child view
     * @param position position with offset
     */
    protected void transformPage(View view, float position) {

        int pageWidth = view.getWidth();

        if (position < -1) {
            // This page is off-screen
            view.setAlpha(0);

        } else if (position <= 0) {

            view.setAlpha(1);

            // Counteract the default slide transition
            if (virtualPos > 0) {
                view.setTranslationX(pageWidth * -position * 2);
            } else {
                view.setTranslationX(0);
            }

            // Scale the page down (between MIN_SCALE and 1)
            float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

        } else if (position <= 1) {

            // Fade the page out.
            view.setAlpha(1 - position);

            // Counteract the default slide transition
            view.setTranslationX(pageWidth * -position);

            // Scale the page down (between MIN_SCALE and 1)
            float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

        } else {
            // This page is off-screen
            view.setAlpha(0);
        }
    }

    /**
     * Complete a scroll in progress.
     *
     * @param postEvents whether endScroll runnable should wait for animation to complete
     */
    private void completeScroll(boolean postEvents) {
        boolean needPopulate = scrollState == SCROLL_STATE_SETTLING;
        if (needPopulate) {
            // Done with scroll, no longer want to cache view drawing.
            scroller.abortAnimation();
            int oldX = getScrollX();
            int oldY = getScrollY();
            int x = scroller.getCurrX();
            int y = scroller.getCurrY();
            if (oldX != x || oldY != y) {
                scrollTo(x, y);
                if (x != oldX) {
                    pageScrolled(x);
                }
            }
        }
        populatePending = false;
        for (int i = 0; i < items.size(); i++) {
            ItemInfo ii = items.get(i);
            if (ii.scrolling) {
                needPopulate = true;
                ii.scrolling = false;
            }
        }
        if (needPopulate) {
            if (postEvents) {
                ViewCompat.postOnAnimation(this, endScrollRunnable);
            } else {
                endScrollRunnable.run();
            }
        }
    }

    /**
     * Enable or disable hardware layers for drawing in children.
     *
     * @param enable enable
     */
    private void enableLayers(boolean enable) {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final int layerType = enable ? ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE;
            ViewCompat.setLayerType(getChildAt(i), layerType, null);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /*
         * This method JUST determines whether we want to intercept the motion.
         * If we return true, onTouchEvent will be called and we do the actual
         * scrolling there.
         */

        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;

        // Reset virtual pos.
        if (scrollState == SCROLL_STATE_IDLE) {
            virtualPos = 0;
        }

        // Always take care of the touch gesture being complete.
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            // Release the drag.
            dragInProgress = false;
            unableToDrag = false;
            activePointerId = INVALID_POINTER;
            if (velocityTracker != null) {
                velocityTracker.recycle();
                velocityTracker = null;
            }
            return false;
        }

        // Nothing more to do here if we have decided whether or not we
        // are dragging.
        if (action != MotionEvent.ACTION_DOWN) {
            if (dragInProgress) {
                return true;
            }
            if (unableToDrag) {
                return false;
            }
        }

        switch (action) {
        case MotionEvent.ACTION_MOVE:
            /*
             * dragInProgress == false, otherwise the shortcut would have caught it. Check
             * whether the user has moved far enough from his original down touch.
             */

            /*
            * Locally do absolute value. lastMotionY is set to the y value
            * of the down event.
            */
            final int activePointerId = this.activePointerId;
            if (activePointerId == INVALID_POINTER) {
                // If we don't have a valid id, the touch down wasn't on content.
                break;
            }

            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
            final float x = MotionEventCompat.getX(ev, pointerIndex);
            final float dx = x - lastMotionX;
            final float xDiff = Math.abs(dx);
            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float yDiff = Math.abs(y - initialMotionY);

            if (xDiff > touchSlop && xDiff * 0.5f > yDiff) {
                dragInProgress = true;
                requestParentDisallowInterceptTouchEvent(true);
                setScrollState(SCROLL_STATE_DRAGGING);
                lastMotionX = dx > 0 ? initialMotionX + touchSlop : initialMotionX - touchSlop;
                lastMotionY = y;

            } else if (yDiff > touchSlop) {
                // The finger has moved enough in the vertical
                // direction to be counted as a drag...  abort
                // any attempt to drag horizontally, to work correctly
                // with children that have scrolling containers.
                unableToDrag = true;
            }
            if (dragInProgress) {
                // Scroll to follow the motion event
                performDrag(x);
            }
            break;

        case MotionEvent.ACTION_DOWN:
            /*
             * Remember location of down touch.
             * ACTION_DOWN always refers to pointer index 0.
             */
            lastMotionX = initialMotionX = ev.getX();
            lastMotionY = initialMotionY = ev.getY();
            this.activePointerId = MotionEventCompat.getPointerId(ev, 0);
            unableToDrag = false;

            double scroll = getScrollX() / getClientWidth();

            if (scrollState == SCROLL_STATE_SETTLING && scroll * density < catchAllowance) {
                // Let the user 'catch' the pager as it animates.
                populatePending = false;
                populate();
                dragInProgress = true;
                requestParentDisallowInterceptTouchEvent(true);
                setScrollState(SCROLL_STATE_DRAGGING);
            } else {
                dragInProgress = false;
            }
            break;

        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;
        }

        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain();
        }
        velocityTracker.addMovement(ev);

        /*
         * The only time we want to intercept motion events is if we are in the
         * drag mode.
         */
        return dragInProgress;
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent ev) {

        if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
            // Don't handle edge touches immediately -- they may actually belong to one of our descendants.
            return false;
        }

        if (pagerAdapter == null || pagerAdapter.getCount() == 0) {
            // Nothing to present or scroll; nothing to touch.
            return false;
        }

        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain();
        }
        velocityTracker.addMovement(ev);

        final int action = ev.getAction();

        switch (action & MotionEventCompat.ACTION_MASK) {

        case MotionEvent.ACTION_DOWN:

            // Do not interfere with the settling action.
            if (scrollState != SCROLL_STATE_SETTLING) {
                scroller.abortAnimation();
                populatePending = false;
                populate();
            }

            // Remember where the motion event started
            lastMotionX = initialMotionX = ev.getX();
            lastMotionY = initialMotionY = ev.getY();
            activePointerId = MotionEventCompat.getPointerId(ev, 0);
            break;

        case MotionEvent.ACTION_MOVE:
            if (!dragInProgress) {
                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
                final float x = MotionEventCompat.getX(ev, pointerIndex);
                final float xDiff = Math.abs(x - lastMotionX);
                final float y = MotionEventCompat.getY(ev, pointerIndex);
                final float yDiff = Math.abs(y - lastMotionY);

                if (xDiff > touchSlop && xDiff > yDiff) {

                    dragInProgress = true;
                    lastMotionX = x - initialMotionX > 0 ? initialMotionX + touchSlop : initialMotionX - touchSlop;
                    lastMotionY = y;
                    setScrollState(SCROLL_STATE_DRAGGING);

                    // Disallow Parent Intercept, just in case
                    requestParentDisallowInterceptTouchEvent(true);
                }
            }
            // Not else! Note that dragInProgress can be set above.
            if (dragInProgress) {
                // Scroll to follow the motion event
                final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
                final float x = MotionEventCompat.getX(ev, activePointerIndex);
                performDrag(x);
            }
            break;

        case MotionEvent.ACTION_UP:

            if (!dragInProgress) {
                break;
            }

            final VelocityTracker velocityTracker = this.velocityTracker;
            velocityTracker.computeCurrentVelocity(1000, maximumVelocity);
            final int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(velocityTracker, activePointerId);

            populatePending = true;

            final int width = getClientWidth();
            final int scrollX = getScrollX();
            final ItemInfo ii = infoForCurrentScrollPosition();
            final int currentPage = ii.position;
            final float pageOffset = ((float) scrollX / width) - ii.offset;
            final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
            final float x = MotionEventCompat.getX(ev, activePointerIndex);
            final int totalDelta = (int) (x - initialMotionX);
            final int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, totalDelta);

            setCurrentItemInternal(nextPage, true, true, initialVelocity);
            activePointerId = INVALID_POINTER;
            endDrag();
            break;

        case MotionEvent.ACTION_CANCEL:

            if (!dragInProgress) {
                break;
            }

            scrollToItem(currentItem, true, 0);
            activePointerId = INVALID_POINTER;
            endDrag();
            break;

        case MotionEventCompat.ACTION_POINTER_DOWN:
            final int index = MotionEventCompat.getActionIndex(ev);
            lastMotionX = MotionEventCompat.getX(ev, index);
            activePointerId = MotionEventCompat.getPointerId(ev, index);
            break;

        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            lastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, activePointerId));
            break;
        }

        return true;
    }

    private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
        final ViewParent parent = getParent();
        if (parent != null) {
            parent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

    /**
     * Perform a user initiated drag motion. Does not allow scrolling backward.
     *
     * @param x scroll x
     */
    private void performDrag(float x) {

        float deltaX = lastMotionX - x;
        lastMotionX = x;

        if (virtualPos == 0) {

            if (deltaX < 0) {
                deltaX = -deltaX;
                reversePos = true;
            } else {
                reversePos = false;
            }

        } else if (virtualPos > 0) {

            deltaX = -deltaX;
            reversePos = true;

        } else {
            reversePos = false;
        }

        float oldScrollX = getScrollX();
        float scrollX = oldScrollX + deltaX;
        final int width = getClientWidth();

        float leftBound = 0;
        float rightBound = width * lastOffset;

        final ItemInfo currentItem = infoForPosition(this.currentItem);
        final ItemInfo lastItem = items.get(items.size() - 1);
        if (currentItem != null) {
            leftBound = currentItem.offset * width;
        }
        if (lastItem.position != pagerAdapter.getCount() - 1) {
            rightBound = lastItem.offset * width;
        }

        if (scrollX < leftBound) {
            scrollX = leftBound;

        } else if (scrollX > rightBound) {
            scrollX = rightBound;
        }
        // Don't lose the rounded component
        lastMotionX += scrollX - (int) scrollX;
        scrollTo((int) scrollX, getScrollY());
        pageScrolled((int) scrollX);
    }

    /**
     * @return Info about the page at the current scroll position.
     * This can be synthetic for a missing middle page; the 'object' field can be null.
     */
    private ItemInfo infoForCurrentScrollPosition() {
        final int width = getClientWidth();
        final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0;
        final float marginOffset = 0;
        int lastPos = -1;
        float lastOffset = 0.f;
        float lastWidth = 0.f;
        boolean first = true;

        ItemInfo lastItem = null;
        for (int i = 0; i < items.size(); i++) {
            ItemInfo ii = items.get(i);
            float offset;
            if (!first && ii.position != lastPos + 1) {
                // Create a synthetic item for a missing page.
                ii = tempItem;
                ii.offset = lastOffset + lastWidth + marginOffset;
                ii.position = lastPos + 1;
                ii.widthFactor = pagerAdapter.getPageWidth(ii.position);
                i--;
            }
            offset = ii.offset;

            final float leftBound = offset;
            final float rightBound = offset + ii.widthFactor + marginOffset;
            if (first || scrollOffset >= leftBound) {
                if (scrollOffset < rightBound || i == items.size() - 1) {
                    return ii;
                }
            } else {
                return lastItem;
            }
            first = false;
            lastPos = ii.position;
            lastOffset = offset;
            lastWidth = ii.widthFactor;
            lastItem = ii;
        }

        return lastItem;
    }

    /**
     * Figure out what the target page would be given current scroll and velocity.
     *
     * @return target page
     */
    private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
        int targetPage;
        if (Math.abs(deltaX) > flingDistance && Math.abs(velocity) > minimumVelocity) {

            if (virtualPos < 0) {
                targetPage = velocity > 0 ? currentPage : currentPage + 1;
            } else {
                targetPage = velocity > 0 ? currentPage + 1 : currentPage;
            }

        } else {
            final float truncator = currentPage >= currentItem ? 0.4f : 0.6f;
            targetPage = (int) (currentPage + pageOffset + truncator);
        }

        if (items.size() > 0) {
            final ItemInfo firstItem = items.get(0);
            final ItemInfo lastItem = items.get(items.size() - 1);

            // Only let the user target pages we have items for
            targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
        }

        if (targetPage > currentPage && onCardChangeListener != null) {
            onCardChangeListener.onCardDismissed(currentPage, virtualPos > 0);
        }

        return targetPage;
    }

    /**
     * Check whether active pointer is up and re assign accordingly.
     *
     * @param ev motion event
     */
    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        if (pointerId == activePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            lastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
            activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
            if (velocityTracker != null) {
                velocityTracker.clear();
            }
        }
    }

    private void endDrag() {
        dragInProgress = false;
        unableToDrag = false;

        if (velocityTracker != null) {
            velocityTracker.recycle();
            velocityTracker = null;
        }
    }

    /**
     * We only want the current page that is being shown to be focusable.
     */
    @Override
    public void addFocusables(@NonNull ArrayList<View> views, int direction, int focusableMode) {
        final int focusableCount = views.size();

        final int descendantFocusability = getDescendantFocusability();

        if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
            for (int i = 0; i < getChildCount(); i++) {
                final View child = getChildAt(i);
                if (child.getVisibility() == VISIBLE) {
                    ItemInfo ii = infoForChild(child);
                    if (ii != null && ii.position == currentItem) {
                        child.addFocusables(views, direction, focusableMode);
                    }
                }
            }
        }

        // we add ourselves (if focusable) in all cases except for when we are
        // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  this is
        // to avoid the focus search finding layouts when a more precise search
        // among the focusable children would be more interesting.

        if (descendantFocusability != FOCUS_AFTER_DESCENDANTS || (focusableCount == views.size())) {
            // Note that we can't call the superclass here, because it will
            // add all views in.  So we need to do the same thing View does.
            if (!isFocusable()) {
                return;
            }
            if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && isInTouchMode()
                    && !isFocusableInTouchMode()) {
                return;
            }
            views.add(this);
        }
    }

    /**
     * We only want the current page that is being shown to be touchable.
     */
    @Override
    public void addTouchables(@NonNull ArrayList<View> views) {
        // Note that we don't call super.addTouchables().
        // This is okay because a Pager is itself not touchable.
        for (int i = 0; i < getChildCount(); i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == VISIBLE) {
                ItemInfo ii = infoForChild(child);
                if (ii != null && ii.position == currentItem) {
                    child.addTouchables(views);
                }
            }
        }
    }

    /**
     * We only want the current page that is being shown to be focusable.
     */
    @Override
    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
        int index;
        int increment;
        int end;
        int count = getChildCount();
        if ((direction & FOCUS_FORWARD) != 0) {
            index = 0;
            increment = 1;
            end = count;
        } else {
            index = count - 1;
            increment = -1;
            end = -1;
        }
        for (int i = index; i != end; i += increment) {
            View child = getChildAt(i);
            if (child.getVisibility() == VISIBLE) {
                ItemInfo ii = infoForChild(child);
                if (ii != null && ii.position == currentItem) {
                    if (child.requestFocus(direction, previouslyFocusedRect)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams();
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return generateDefaultLayoutParams();
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams && super.checkLayoutParams(p);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    private class PagerObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            dataSetChanged();
        }

        @Override
        public void onInvalidated() {
            dataSetChanged();
        }
    }

    /**
     * Layout parameters that should be supplied for views added.
     */
    public static class LayoutParams extends ViewGroup.LayoutParams {

        /**
         * Width as a 0-1 multiplier of the measured pager width.
         */
        float widthFactor = 0.f;

        /**
         * true if this view was added during layout and needs to be measured
         * before being positioned.
         */
        boolean needsMeasure;

        /**
         * Adapter position this view.
         */
        int position;

        /**
         * Current child index within the CardPager that this view occupies.
         */
        int childIndex;

        public LayoutParams() {
            super(MATCH_PARENT, MATCH_PARENT);
        }

        public LayoutParams(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    }

}