/* * Copyright (C) 2011 Jake Wharton * Copyright (C) 2011 Patrik Akerfeldt * Copyright (C) 2011 Francisco Figueiredo Jr. * * 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.smartandroid.sa.vpic; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewConfigurationCompat; import android.support.v4.view.ViewPager; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import java.util.ArrayList; /** * A TitlePageIndicator is a PageIndicator which displays the title of left view * (if exist), the title of the current select view (centered) and the title of * the right view (if exist). When the user scrolls the ViewPager then titles * are also scrolled. */ public class TitlePageIndicator extends View implements PageIndicator { /** * Percentage indicating what percentage of the screen width away from * center should the underline be fully faded. A value of 0.25 means that * halfway between the center of the screen and an edge. */ private static final float SELECTION_FADE_PERCENTAGE = 0.25f; /** * Percentage indicating what percentage of the screen width away from * center should the selected text bold turn off. A value of 0.05 means that * 10% between the center and an edge. */ private static final float BOLD_FADE_PERCENTAGE = 0.05f; /** * Title text used when no title is provided by the adapter. */ private static final String EMPTY_TITLE = ""; /** * Interface for a callback when the center item has been clicked. */ public interface OnCenterItemClickListener { /** * Callback when the center item has been clicked. * * @param position * Position of the current center item. */ void onCenterItemClick(int position); } public enum IndicatorStyle { None(0), Triangle(1), Underline(2); public final int value; private IndicatorStyle(int value) { this.value = value; } public static IndicatorStyle fromValue(int value) { for (IndicatorStyle style : IndicatorStyle.values()) { if (style.value == value) { return style; } } return null; } } public enum LinePosition { Bottom(0), Top(1); public final int value; private LinePosition(int value) { this.value = value; } public static LinePosition fromValue(int value) { for (LinePosition position : LinePosition.values()) { if (position.value == value) { return position; } } return null; } } private ViewPager mViewPager; private ViewPager.OnPageChangeListener mListener; private int mCurrentPage = -1; private float mPageOffset; private int mScrollState; private final Paint mPaintText = new Paint(); private boolean mBoldText; private int mColorText; private int mColorSelected; private Path mPath = new Path(); private final Rect mBounds = new Rect(); private final Paint mPaintFooterLine = new Paint(); private IndicatorStyle mFooterIndicatorStyle; private LinePosition mLinePosition; private final Paint mPaintFooterIndicator = new Paint(); private float mFooterIndicatorHeight; private float mFooterIndicatorUnderlinePadding; private float mFooterPadding; private float mTitlePadding; private float mTopPadding; /** Left and right side padding for not active view titles. */ private float mClipPadding; private float mFooterLineHeight; private static final int INVALID_POINTER = -1; private int mTouchSlop; private float mLastMotionX = -1; private int mActivePointerId = INVALID_POINTER; private boolean mIsDragging; private OnCenterItemClickListener mCenterItemClickListener; public TitlePageIndicator(Context context) { this(context, null); } public TitlePageIndicator(Context context, AttributeSet attrs) { this(context, attrs, context.getResources().getIdentifier( "vpiTitlePageIndicatorStyle", "attr", context.getPackageName())); // this(context, attrs, R.attr.vpiTitlePageIndicatorStyle); } public TitlePageIndicator(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); if (isInEditMode()) return; // Load defaults from resources final Resources res = getResources(); final int defaultFooterColor = res.getColor(context.getResources() .getIdentifier("default_title_indicator_footer_color", "color", context.getPackageName())); // final int defaultFooterColor = res // .getColor(R.color.default_title_indicator_footer_color); final float defaultFooterLineHeight = res.getDimension(context .getResources().getIdentifier( "default_title_indicator_footer_line_height", "dimen", context.getPackageName())); // final float defaultFooterLineHeight = res // .getDimension(R.dimen.default_title_indicator_footer_line_height); final int defaultFooterIndicatorStyle = res.getInteger(context .getResources().getIdentifier( "default_title_indicator_footer_indicator_style", "integer", context.getPackageName())); // final int defaultFooterIndicatorStyle = res // .getInteger(R.integer.default_title_indicator_footer_indicator_style); final float defaultFooterIndicatorHeight = res.getDimension(context .getResources().getIdentifier( "default_title_indicator_footer_indicator_height", "dimen", context.getPackageName())); // final float defaultFooterIndicatorHeight = res // .getDimension(R.dimen.default_title_indicator_footer_indicator_height); final float defaultFooterIndicatorUnderlinePadding = res .getDimension(context .getResources() .getIdentifier( "default_title_indicator_footer_indicator_underline_padding", "dimen", context.getPackageName())); // final float defaultFooterIndicatorUnderlinePadding = res // .getDimension(R.dimen.default_title_indicator_footer_indicator_underline_padding); final float defaultFooterPadding = res.getDimension(context .getResources().getIdentifier( "default_title_indicator_footer_padding", "dimen", context.getPackageName())); // final float defaultFooterPadding = res // .getDimension(R.dimen.default_title_indicator_footer_padding); final int defaultLinePosition = res.getInteger(context.getResources() .getIdentifier("default_title_indicator_line_position", "integer", context.getPackageName())); // final int defaultLinePosition = res // .getInteger(R.integer.default_title_indicator_line_position); final int defaultSelectedColor = res.getColor(context.getResources() .getIdentifier("default_title_indicator_selected_color", "color", context.getPackageName())); // final int defaultSelectedColor = res // .getColor(R.color.default_title_indicator_selected_color); final boolean defaultSelectedBold = res.getBoolean(context .getResources().getIdentifier( "default_title_indicator_selected_bold", "bool", context.getPackageName())); // final boolean defaultSelectedBold = res // .getBoolean(R.bool.default_title_indicator_selected_bold); final int defaultTextColor = res.getColor(context.getResources() .getIdentifier("default_title_indicator_text_color", "color", context.getPackageName())); // final int defaultTextColor = res // .getColor(R.color.default_title_indicator_text_color); final float defaultTextSize = res.getDimension(context.getResources() .getIdentifier("default_title_indicator_text_size", "dimen", context.getPackageName())); // final float defaultTextSize = res // .getDimension(R.dimen.default_title_indicator_text_size); final float defaultTitlePadding = res.getDimension(context .getResources().getIdentifier( "default_title_indicator_title_padding", "dimen", context.getPackageName())); // final float defaultTitlePadding = res // .getDimension(R.dimen.default_title_indicator_title_padding); final float defaultClipPadding = res.getDimension(context .getResources().getIdentifier( "default_title_indicator_clip_padding", "dimen", context.getPackageName())); // final float defaultClipPadding = res // .getDimension(R.dimen.default_title_indicator_clip_padding); final float defaultTopPadding = res.getDimension(context.getResources() .getIdentifier("default_title_indicator_top_padding", "dimen", context.getPackageName())); // final float defaultTopPadding = res // .getDimension(R.dimen.default_title_indicator_top_padding); // Retrieve styles attributes TypedArray a = context.obtainStyledAttributes( attrs, new int[] { getResources().getIdentifier("TitlePageIndicator", "styleable", context.getPackageName()) }, defStyle, 0); // TypedArray a = context.obtainStyledAttributes(attrs, // R.styleable.TitlePageIndicator, defStyle, 0); // Retrieve the colors to be used for this view and apply them. mFooterLineHeight = a.getDimension( getResources().getIdentifier( "TitlePageIndicator_footerLineHeight", "styleable", context.getPackageName()), defaultFooterLineHeight); // mFooterLineHeight = a.getDimension( // R.styleable.TitlePageIndicator_footerLineHeight, // defaultFooterLineHeight); mFooterIndicatorStyle = IndicatorStyle .fromValue(a.getInteger( getResources().getIdentifier( "TitlePageIndicator_footerIndicatorStyle", "styleable", context.getPackageName()), defaultFooterIndicatorStyle)); // mFooterIndicatorStyle = IndicatorStyle.fromValue(a.getInteger( // R.styleable.TitlePageIndicator_footerIndicatorStyle, // defaultFooterIndicatorStyle)); mFooterIndicatorHeight = a.getDimension( getResources().getIdentifier( "TitlePageIndicator_footerIndicatorHeight", "styleable", context.getPackageName()), defaultFooterIndicatorHeight); // mFooterIndicatorHeight = a.getDimension( // R.styleable.TitlePageIndicator_footerIndicatorHeight, // defaultFooterIndicatorHeight); mFooterIndicatorUnderlinePadding = a.getDimension( getResources().getIdentifier( "TitlePageIndicator_footerIndicatorUnderlinePadding", "styleable", context.getPackageName()), defaultFooterIndicatorUnderlinePadding); // mFooterIndicatorUnderlinePadding = a.getDimension( // R.styleable.TitlePageIndicator_footerIndicatorUnderlinePadding, // defaultFooterIndicatorUnderlinePadding); mFooterPadding = a.getDimension( getResources().getIdentifier( "TitlePageIndicator_footerPadding", "styleable", context.getPackageName()), defaultFooterPadding); // mFooterPadding = a.getDimension( // R.styleable.TitlePageIndicator_footerPadding, // defaultFooterPadding); mLinePosition = LinePosition.fromValue(a.getInteger( getResources().getIdentifier("TitlePageIndicator_linePosition", "styleable", context.getPackageName()), defaultLinePosition)); // mLinePosition = LinePosition.fromValue(a.getInteger( // R.styleable.TitlePageIndicator_linePosition, // defaultLinePosition)); mTopPadding = a.getDimension( getResources().getIdentifier("TitlePageIndicator_topPadding", "styleable", context.getPackageName()), defaultTopPadding); // mTopPadding = // a.getDimension(R.styleable.TitlePageIndicator_topPadding, // defaultTopPadding); mTitlePadding = a.getDimension( getResources().getIdentifier("TitlePageIndicator_titlePadding", "styleable", context.getPackageName()), defaultTitlePadding); // mTitlePadding = a.getDimension( // R.styleable.TitlePageIndicator_titlePadding, // defaultTitlePadding); mClipPadding = a.getDimension( getResources().getIdentifier("TitlePageIndicator_clipPadding", "styleable", context.getPackageName()), defaultClipPadding); // mClipPadding = a.getDimension( // R.styleable.TitlePageIndicator_clipPadding, defaultClipPadding); mColorSelected = a.getColor( getResources().getIdentifier( "TitlePageIndicator_selectedColor", "styleable", context.getPackageName()), defaultSelectedColor); // mColorSelected = a.getColor( // R.styleable.TitlePageIndicator_selectedColor, // defaultSelectedColor); mColorText = a.getColor( getResources().getIdentifier( "TitlePageIndicator_android_textColor", "styleable", context.getPackageName()), defaultTextColor); // mColorText = a.getColor( // R.styleable.TitlePageIndicator_android_textColor, // defaultTextColor); mBoldText = a.getBoolean( getResources().getIdentifier("TitlePageIndicator_selectedBold", "styleable", context.getPackageName()), defaultSelectedBold); // mBoldText = a.getBoolean(R.styleable.TitlePageIndicator_selectedBold, // defaultSelectedBold); final float textSize = a.getDimension( getResources().getIdentifier( "TitlePageIndicator_android_textSize", "styleable", context.getPackageName()), defaultTextSize); // final float textSize = a.getDimension( // R.styleable.TitlePageIndicator_android_textSize, // defaultTextSize); final int footerColor = a.getColor( getResources().getIdentifier("TitlePageIndicator_footerColor", "styleable", context.getPackageName()), defaultFooterColor); // final int footerColor = a.getColor( // R.styleable.TitlePageIndicator_footerColor, defaultFooterColor); mPaintText.setTextSize(textSize); mPaintText.setAntiAlias(true); mPaintFooterLine.setStyle(Paint.Style.FILL_AND_STROKE); mPaintFooterLine.setStrokeWidth(mFooterLineHeight); mPaintFooterLine.setColor(footerColor); mPaintFooterIndicator.setStyle(Paint.Style.FILL_AND_STROKE); mPaintFooterIndicator.setColor(footerColor); Drawable background = a.getDrawable(getResources().getIdentifier( "TitlePageIndicator_android_background", "styleable", context.getPackageName())); // Drawable background = a // .getDrawable(R.styleable.TitlePageIndicator_android_background); if (background != null) { setBackgroundDrawable(background); } a.recycle(); final ViewConfiguration configuration = ViewConfiguration.get(context); mTouchSlop = ViewConfigurationCompat .getScaledPagingTouchSlop(configuration); } public int getFooterColor() { return mPaintFooterLine.getColor(); } public void setFooterColor(int footerColor) { mPaintFooterLine.setColor(footerColor); mPaintFooterIndicator.setColor(footerColor); invalidate(); } public float getFooterLineHeight() { return mFooterLineHeight; } public void setFooterLineHeight(float footerLineHeight) { mFooterLineHeight = footerLineHeight; mPaintFooterLine.setStrokeWidth(mFooterLineHeight); invalidate(); } public float getFooterIndicatorHeight() { return mFooterIndicatorHeight; } public void setFooterIndicatorHeight(float footerTriangleHeight) { mFooterIndicatorHeight = footerTriangleHeight; invalidate(); } public float getFooterIndicatorPadding() { return mFooterPadding; } public void setFooterIndicatorPadding(float footerIndicatorPadding) { mFooterPadding = footerIndicatorPadding; invalidate(); } public IndicatorStyle getFooterIndicatorStyle() { return mFooterIndicatorStyle; } public void setFooterIndicatorStyle(IndicatorStyle indicatorStyle) { mFooterIndicatorStyle = indicatorStyle; invalidate(); } public LinePosition getLinePosition() { return mLinePosition; } public void setLinePosition(LinePosition linePosition) { mLinePosition = linePosition; invalidate(); } public int getSelectedColor() { return mColorSelected; } public void setSelectedColor(int selectedColor) { mColorSelected = selectedColor; invalidate(); } public boolean isSelectedBold() { return mBoldText; } public void setSelectedBold(boolean selectedBold) { mBoldText = selectedBold; invalidate(); } public int getTextColor() { return mColorText; } public void setTextColor(int textColor) { mPaintText.setColor(textColor); mColorText = textColor; invalidate(); } public float getTextSize() { return mPaintText.getTextSize(); } public void setTextSize(float textSize) { mPaintText.setTextSize(textSize); invalidate(); } public float getTitlePadding() { return this.mTitlePadding; } public void setTitlePadding(float titlePadding) { mTitlePadding = titlePadding; invalidate(); } public float getTopPadding() { return this.mTopPadding; } public void setTopPadding(float topPadding) { mTopPadding = topPadding; invalidate(); } public float getClipPadding() { return this.mClipPadding; } public void setClipPadding(float clipPadding) { mClipPadding = clipPadding; invalidate(); } public void setTypeface(Typeface typeface) { mPaintText.setTypeface(typeface); invalidate(); } public Typeface getTypeface() { return mPaintText.getTypeface(); } /* * (non-Javadoc) * * @see android.view.View#onDraw(android.graphics.Canvas) */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mViewPager == null) { return; } final int count = mViewPager.getAdapter().getCount(); if (count == 0) { return; } // mCurrentPage is -1 on first start and after orientation changed. If // so, retrieve the correct index from viewpager. if (mCurrentPage == -1 && mViewPager != null) { mCurrentPage = mViewPager.getCurrentItem(); } // Calculate views bounds ArrayList<Rect> bounds = calculateAllBounds(mPaintText); final int boundsSize = bounds.size(); // Make sure we're on a page that still exists if (mCurrentPage >= boundsSize) { setCurrentItem(boundsSize - 1); return; } final int countMinusOne = count - 1; final float halfWidth = getWidth() / 2f; final int left = getLeft(); final float leftClip = left + mClipPadding; final int width = getWidth(); int height = getHeight(); final int right = left + width; final float rightClip = right - mClipPadding; int page = mCurrentPage; float offsetPercent; if (mPageOffset <= 0.5) { offsetPercent = mPageOffset; } else { page += 1; offsetPercent = 1 - mPageOffset; } final boolean currentSelected = (offsetPercent <= SELECTION_FADE_PERCENTAGE); final boolean currentBold = (offsetPercent <= BOLD_FADE_PERCENTAGE); final float selectedPercent = (SELECTION_FADE_PERCENTAGE - offsetPercent) / SELECTION_FADE_PERCENTAGE; // Verify if the current view must be clipped to the screen Rect curPageBound = bounds.get(mCurrentPage); float curPageWidth = curPageBound.right - curPageBound.left; if (curPageBound.left < leftClip) { // Try to clip to the screen (left side) clipViewOnTheLeft(curPageBound, curPageWidth, left); } if (curPageBound.right > rightClip) { // Try to clip to the screen (right side) clipViewOnTheRight(curPageBound, curPageWidth, right); } // Left views starting from the current position if (mCurrentPage > 0) { for (int i = mCurrentPage - 1; i >= 0; i--) { Rect bound = bounds.get(i); // Is left side is outside the screen if (bound.left < leftClip) { int w = bound.right - bound.left; // Try to clip to the screen (left side) clipViewOnTheLeft(bound, w, left); // Except if there's an intersection with the right view Rect rightBound = bounds.get(i + 1); // Intersection if (bound.right + mTitlePadding > rightBound.left) { bound.left = (int) (rightBound.left - w - mTitlePadding); bound.right = bound.left + w; } } } } // Right views starting from the current position if (mCurrentPage < countMinusOne) { for (int i = mCurrentPage + 1; i < count; i++) { Rect bound = bounds.get(i); // If right side is outside the screen if (bound.right > rightClip) { int w = bound.right - bound.left; // Try to clip to the screen (right side) clipViewOnTheRight(bound, w, right); // Except if there's an intersection with the left view Rect leftBound = bounds.get(i - 1); // Intersection if (bound.left - mTitlePadding < leftBound.right) { bound.left = (int) (leftBound.right + mTitlePadding); bound.right = bound.left + w; } } } } // Now draw views int colorTextAlpha = mColorText >>> 24; for (int i = 0; i < count; i++) { // Get the title Rect bound = bounds.get(i); // Only if one side is visible if ((bound.left > left && bound.left < right) || (bound.right > left && bound.right < right)) { final boolean currentPage = (i == page); final CharSequence pageTitle = getTitle(i); // Only set bold if we are within bounds mPaintText.setFakeBoldText(currentPage && currentBold && mBoldText); // Draw text as unselected mPaintText.setColor(mColorText); if (currentPage && currentSelected) { // Fade out/in unselected text as the selected text fades // in/out mPaintText.setAlpha(colorTextAlpha - (int) (colorTextAlpha * selectedPercent)); } // Except if there's an intersection with the right view if (i < boundsSize - 1) { Rect rightBound = bounds.get(i + 1); // Intersection if (bound.right + mTitlePadding > rightBound.left) { int w = bound.right - bound.left; bound.left = (int) (rightBound.left - w - mTitlePadding); bound.right = bound.left + w; } } canvas.drawText(pageTitle, 0, pageTitle.length(), bound.left, bound.bottom + mTopPadding, mPaintText); // If we are within the selected bounds draw the selected text if (currentPage && currentSelected) { mPaintText.setColor(mColorSelected); mPaintText .setAlpha((int) ((mColorSelected >>> 24) * selectedPercent)); canvas.drawText(pageTitle, 0, pageTitle.length(), bound.left, bound.bottom + mTopPadding, mPaintText); } } } // If we want the line on the top change height to zero and invert the // line height to trick the drawing code float footerLineHeight = mFooterLineHeight; float footerIndicatorLineHeight = mFooterIndicatorHeight; if (mLinePosition == LinePosition.Top) { height = 0; footerLineHeight = -footerLineHeight; footerIndicatorLineHeight = -footerIndicatorLineHeight; } // Draw the footer line mPath.reset(); mPath.moveTo(0, height - footerLineHeight / 2f); mPath.lineTo(width, height - footerLineHeight / 2f); mPath.close(); canvas.drawPath(mPath, mPaintFooterLine); float heightMinusLine = height - footerLineHeight; switch (mFooterIndicatorStyle) { case Triangle: mPath.reset(); mPath.moveTo(halfWidth, heightMinusLine - footerIndicatorLineHeight); mPath.lineTo(halfWidth + footerIndicatorLineHeight, heightMinusLine); mPath.lineTo(halfWidth - footerIndicatorLineHeight, heightMinusLine); mPath.close(); canvas.drawPath(mPath, mPaintFooterIndicator); break; case Underline: if (!currentSelected || page >= boundsSize) { break; } Rect underlineBounds = bounds.get(page); final float rightPlusPadding = underlineBounds.right + mFooterIndicatorUnderlinePadding; final float leftMinusPadding = underlineBounds.left - mFooterIndicatorUnderlinePadding; final float heightMinusLineMinusIndicator = heightMinusLine - footerIndicatorLineHeight; mPath.reset(); mPath.moveTo(leftMinusPadding, heightMinusLine); mPath.lineTo(rightPlusPadding, heightMinusLine); mPath.lineTo(rightPlusPadding, heightMinusLineMinusIndicator); mPath.lineTo(leftMinusPadding, heightMinusLineMinusIndicator); mPath.close(); mPaintFooterIndicator.setAlpha((int) (0xFF * selectedPercent)); canvas.drawPath(mPath, mPaintFooterIndicator); mPaintFooterIndicator.setAlpha(0xFF); break; } } public boolean onTouchEvent(android.view.MotionEvent ev) { if (super.onTouchEvent(ev)) { return true; } if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) { return false; } final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mLastMotionX = ev.getX(); break; case MotionEvent.ACTION_MOVE: { final int activePointerIndex = MotionEventCompat.findPointerIndex( ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, activePointerIndex); final float deltaX = x - mLastMotionX; if (!mIsDragging) { if (Math.abs(deltaX) > mTouchSlop) { mIsDragging = true; } } if (mIsDragging) { mLastMotionX = x; if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) { mViewPager.fakeDragBy(deltaX); } } break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: if (!mIsDragging) { final int count = mViewPager.getAdapter().getCount(); final int width = getWidth(); final float halfWidth = width / 2f; final float sixthWidth = width / 6f; final float leftThird = halfWidth - sixthWidth; final float rightThird = halfWidth + sixthWidth; final float eventX = ev.getX(); if (eventX < leftThird) { if (mCurrentPage > 0) { if (action != MotionEvent.ACTION_CANCEL) { mViewPager.setCurrentItem(mCurrentPage - 1); } return true; } } else if (eventX > rightThird) { if (mCurrentPage < count - 1) { if (action != MotionEvent.ACTION_CANCEL) { mViewPager.setCurrentItem(mCurrentPage + 1); } return true; } } else { // Middle third if (mCenterItemClickListener != null && action != MotionEvent.ACTION_CANCEL) { mCenterItemClickListener .onCenterItemClick(mCurrentPage); } } } mIsDragging = false; mActivePointerId = INVALID_POINTER; if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag(); break; case MotionEventCompat.ACTION_POINTER_DOWN: { final int index = MotionEventCompat.getActionIndex(ev); mLastMotionX = MotionEventCompat.getX(ev, index); mActivePointerId = MotionEventCompat.getPointerId(ev, index); break; } case MotionEventCompat.ACTION_POINTER_UP: final int pointerIndex = MotionEventCompat.getActionIndex(ev); final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); if (pointerId == mActivePointerId) { final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); } mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)); break; } return true; } /** * Set bounds for the right textView including clip padding. * * @param curViewBound * current bounds. * @param curViewWidth * width of the view. */ private void clipViewOnTheRight(Rect curViewBound, float curViewWidth, int right) { curViewBound.right = (int) (right - mClipPadding); curViewBound.left = (int) (curViewBound.right - curViewWidth); } /** * Set bounds for the left textView including clip padding. * * @param curViewBound * current bounds. * @param curViewWidth * width of the view. */ private void clipViewOnTheLeft(Rect curViewBound, float curViewWidth, int left) { curViewBound.left = (int) (left + mClipPadding); curViewBound.right = (int) (mClipPadding + curViewWidth); } /** * Calculate views bounds and scroll them according to the current index * * @param paint * @return */ private ArrayList<Rect> calculateAllBounds(Paint paint) { ArrayList<Rect> list = new ArrayList<Rect>(); // For each views (If no values then add a fake one) final int count = mViewPager.getAdapter().getCount(); final int width = getWidth(); final int halfWidth = width / 2; for (int i = 0; i < count; i++) { Rect bounds = calcBounds(i, paint); int w = bounds.right - bounds.left; int h = bounds.bottom - bounds.top; bounds.left = (int) (halfWidth - (w / 2f) + ((i - mCurrentPage - mPageOffset) * width)); bounds.right = bounds.left + w; bounds.top = 0; bounds.bottom = h; list.add(bounds); } return list; } /** * Calculate the bounds for a view's title * * @param index * @param paint * @return */ private Rect calcBounds(int index, Paint paint) { // Calculate the text bounds Rect bounds = new Rect(); CharSequence title = getTitle(index); bounds.right = (int) paint.measureText(title, 0, title.length()); bounds.bottom = (int) (paint.descent() - paint.ascent()); return bounds; } @Override public void setViewPager(ViewPager view) { if (mViewPager == view) { return; } if (mViewPager != null) { mViewPager.setOnPageChangeListener(null); } if (view.getAdapter() == null) { throw new IllegalStateException( "ViewPager does not have adapter instance."); } mViewPager = view; mViewPager.setOnPageChangeListener(this); invalidate(); } @Override public void setViewPager(ViewPager view, int initialPosition) { setViewPager(view); setCurrentItem(initialPosition); } @Override public void notifyDataSetChanged() { invalidate(); } /** * Set a callback listener for the center item click. * * @param listener * Callback instance. */ public void setOnCenterItemClickListener(OnCenterItemClickListener listener) { mCenterItemClickListener = listener; } @Override public void setCurrentItem(int item) { if (mViewPager == null) { throw new IllegalStateException("ViewPager has not been bound."); } mViewPager.setCurrentItem(item); mCurrentPage = item; invalidate(); } @Override public void onPageScrollStateChanged(int state) { mScrollState = state; if (mListener != null) { mListener.onPageScrollStateChanged(state); } } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { mCurrentPage = position; mPageOffset = positionOffset; invalidate(); if (mListener != null) { mListener.onPageScrolled(position, positionOffset, positionOffsetPixels); } } @Override public void onPageSelected(int position) { if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { mCurrentPage = position; invalidate(); } if (mListener != null) { mListener.onPageSelected(position); } } @Override public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { mListener = listener; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Measure our width in whatever mode specified final int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); // Determine our height float height; final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); if (heightSpecMode == MeasureSpec.EXACTLY) { // We were told how big to be height = MeasureSpec.getSize(heightMeasureSpec); } else { // Calculate the text bounds mBounds.setEmpty(); mBounds.bottom = (int) (mPaintText.descent() - mPaintText.ascent()); height = mBounds.bottom - mBounds.top + mFooterLineHeight + mFooterPadding + mTopPadding; if (mFooterIndicatorStyle != IndicatorStyle.None) { height += mFooterIndicatorHeight; } } final int measuredHeight = (int) height; setMeasuredDimension(measuredWidth, measuredHeight); } @Override public void onRestoreInstanceState(Parcelable state) { SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); mCurrentPage = savedState.currentPage; requestLayout(); } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState savedState = new SavedState(superState); savedState.currentPage = mCurrentPage; return savedState; } static class SavedState extends BaseSavedState { int currentPage; public SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); currentPage = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(currentPage); } @SuppressWarnings("UnusedDeclaration") public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } private CharSequence getTitle(int i) { CharSequence title = mViewPager.getAdapter().getPageTitle(i); if (title == null) { title = EMPTY_TITLE; } return title; } }