Blame | Last modification | View Log | RSS feed
/** Copyright (C) 2011 Patrik Akerfeldt* Copyright (C) 2011 Jake Wharton** 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.viewpagerindicator;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.Paint.Style;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 static android.graphics.Paint.ANTI_ALIAS_FLAG;import static android.widget.LinearLayout.HORIZONTAL;import static android.widget.LinearLayout.VERTICAL;/*** Draws circles (one for each view). The current view position is filled and* others are only stroked.*/public class CirclePageIndicator extends View implements PageIndicator {private static final int INVALID_POINTER = -1;private float mRadius;private final Paint mPaintPageFill = new Paint(ANTI_ALIAS_FLAG);private final Paint mPaintStroke = new Paint(ANTI_ALIAS_FLAG);private final Paint mPaintFill = new Paint(ANTI_ALIAS_FLAG);private ViewPager mViewPager;private ViewPager.OnPageChangeListener mListener;private int mCurrentPage;private int mSnapPage;private float mPageOffset;private int mScrollState;private int mOrientation;private boolean mCentered;private boolean mSnap;private int mTouchSlop;private float mLastMotionX = -1;private int mActivePointerId = INVALID_POINTER;private boolean mIsDragging;public CirclePageIndicator(Context context) {this(context, null);}public CirclePageIndicator(Context context, AttributeSet attrs) {this(context, attrs, R.attr.vpiCirclePageIndicatorStyle);}public CirclePageIndicator(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);if (isInEditMode()) return;//Load defaults from resourcesfinal Resources res = getResources();final int defaultPageColor = res.getColor(R.color.default_circle_indicator_page_color);final int defaultFillColor = res.getColor(R.color.default_circle_indicator_fill_color);final int defaultOrientation = res.getInteger(R.integer.default_circle_indicator_orientation);final int defaultStrokeColor = res.getColor(R.color.default_circle_indicator_stroke_color);final float defaultStrokeWidth = res.getDimension(R.dimen.default_circle_indicator_stroke_width);final float defaultRadius = res.getDimension(R.dimen.default_circle_indicator_radius);final boolean defaultCentered = res.getBoolean(R.bool.default_circle_indicator_centered);final boolean defaultSnap = res.getBoolean(R.bool.default_circle_indicator_snap);//Retrieve styles attributesTypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePageIndicator, defStyle, 0);mCentered = a.getBoolean(R.styleable.CirclePageIndicator_centered, defaultCentered);mOrientation = a.getInt(R.styleable.CirclePageIndicator_android_orientation, defaultOrientation);mPaintPageFill.setStyle(Style.FILL);mPaintPageFill.setColor(a.getColor(R.styleable.CirclePageIndicator_pageColor, defaultPageColor));mPaintStroke.setStyle(Style.STROKE);mPaintStroke.setColor(a.getColor(R.styleable.CirclePageIndicator_strokeColor, defaultStrokeColor));mPaintStroke.setStrokeWidth(a.getDimension(R.styleable.CirclePageIndicator_strokeWidth, defaultStrokeWidth));mPaintFill.setStyle(Style.FILL);mPaintFill.setColor(a.getColor(R.styleable.CirclePageIndicator_fillColor, defaultFillColor));mRadius = a.getDimension(R.styleable.CirclePageIndicator_radius, defaultRadius);mSnap = a.getBoolean(R.styleable.CirclePageIndicator_snap, defaultSnap);Drawable background = a.getDrawable(R.styleable.CirclePageIndicator_android_background);if (background != null) {setBackgroundDrawable(background);}a.recycle();final ViewConfiguration configuration = ViewConfiguration.get(context);mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);}public void setCentered(boolean centered) {mCentered = centered;invalidate();}public boolean isCentered() {return mCentered;}public void setPageColor(int pageColor) {mPaintPageFill.setColor(pageColor);invalidate();}public int getPageColor() {return mPaintPageFill.getColor();}public void setFillColor(int fillColor) {mPaintFill.setColor(fillColor);invalidate();}public int getFillColor() {return mPaintFill.getColor();}public void setOrientation(int orientation) {switch (orientation) {case HORIZONTAL:case VERTICAL:mOrientation = orientation;requestLayout();break;default:throw new IllegalArgumentException("Orientation must be either HORIZONTAL or VERTICAL.");}}public int getOrientation() {return mOrientation;}public void setStrokeColor(int strokeColor) {mPaintStroke.setColor(strokeColor);invalidate();}public int getStrokeColor() {return mPaintStroke.getColor();}public void setStrokeWidth(float strokeWidth) {mPaintStroke.setStrokeWidth(strokeWidth);invalidate();}public float getStrokeWidth() {return mPaintStroke.getStrokeWidth();}public void setRadius(float radius) {mRadius = radius;invalidate();}public float getRadius() {return mRadius;}public void setSnap(boolean snap) {mSnap = snap;invalidate();}public boolean isSnap() {return mSnap;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);if (mViewPager == null) {return;}final int count = mViewPager.getAdapter().getCount();if (count == 0) {return;}if (mCurrentPage >= count) {setCurrentItem(count - 1);return;}int longSize;int longPaddingBefore;int longPaddingAfter;int shortPaddingBefore;if (mOrientation == HORIZONTAL) {longSize = getWidth();longPaddingBefore = getPaddingLeft();longPaddingAfter = getPaddingRight();shortPaddingBefore = getPaddingTop();} else {longSize = getHeight();longPaddingBefore = getPaddingTop();longPaddingAfter = getPaddingBottom();shortPaddingBefore = getPaddingLeft();}final float threeRadius = mRadius * 3;final float shortOffset = shortPaddingBefore + mRadius;float longOffset = longPaddingBefore + mRadius;if (mCentered) {longOffset += ((longSize - longPaddingBefore - longPaddingAfter) / 2.0f) - ((count * threeRadius) / 2.0f);}float dX;float dY;float pageFillRadius = mRadius;if (mPaintStroke.getStrokeWidth() > 0) {pageFillRadius -= mPaintStroke.getStrokeWidth() / 2.0f;}//Draw stroked circlesfor (int iLoop = 0; iLoop < count; iLoop++) {float drawLong = longOffset + (iLoop * threeRadius);if (mOrientation == HORIZONTAL) {dX = drawLong;dY = shortOffset;} else {dX = shortOffset;dY = drawLong;}// Only paint fill if not completely transparentif (mPaintPageFill.getAlpha() > 0) {canvas.drawCircle(dX, dY, pageFillRadius, mPaintPageFill);}// Only paint stroke if a stroke width was non-zeroif (pageFillRadius != mRadius) {canvas.drawCircle(dX, dY, mRadius, mPaintStroke);}}//Draw the filled circle according to the current scrollfloat cx = (mSnap ? mSnapPage : mCurrentPage) * threeRadius;if (!mSnap) {cx += mPageOffset * threeRadius;}if (mOrientation == HORIZONTAL) {dX = longOffset + cx;dY = shortOffset;} else {dX = shortOffset;dY = longOffset + cx;}canvas.drawCircle(dX, dY, mRadius, mPaintFill);}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;if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) {if (action != MotionEvent.ACTION_CANCEL) {mViewPager.setCurrentItem(mCurrentPage - 1);}return true;} else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) {if (action != MotionEvent.ACTION_CANCEL) {mViewPager.setCurrentItem(mCurrentPage + 1);}return true;}}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;}@Overridepublic 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();}@Overridepublic void setViewPager(ViewPager view, int initialPosition) {setViewPager(view);setCurrentItem(initialPosition);}@Overridepublic void setCurrentItem(int item) {if (mViewPager == null) {throw new IllegalStateException("ViewPager has not been bound.");}mViewPager.setCurrentItem(item);mCurrentPage = item;invalidate();}@Overridepublic void notifyDataSetChanged() {invalidate();}@Overridepublic void onPageScrollStateChanged(int state) {mScrollState = state;if (mListener != null) {mListener.onPageScrollStateChanged(state);}}@Overridepublic void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {mCurrentPage = position;mPageOffset = positionOffset;invalidate();if (mListener != null) {mListener.onPageScrolled(position, positionOffset, positionOffsetPixels);}}@Overridepublic void onPageSelected(int position) {if (mSnap || mScrollState == ViewPager.SCROLL_STATE_IDLE) {mCurrentPage = position;mSnapPage = position;invalidate();}if (mListener != null) {mListener.onPageSelected(position);}}@Overridepublic void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {mListener = listener;}/** (non-Javadoc)** @see android.view.View#onMeasure(int, int)*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {if (mOrientation == HORIZONTAL) {setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec));} else {setMeasuredDimension(measureShort(widthMeasureSpec), measureLong(heightMeasureSpec));}}/*** Determines the width of this view** @param measureSpec* A measureSpec packed into an int* @return The width of the view, honoring constraints from measureSpec*/private int measureLong(int measureSpec) {int result;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) {//We were told how big to beresult = specSize;} else {//Calculate the width according the views countfinal int count = mViewPager.getAdapter().getCount();result = (int)(getPaddingLeft() + getPaddingRight()+ (count * 2 * mRadius) + (count - 1) * mRadius + 1);//Respect AT_MOST value if that was what is called for by measureSpecif (specMode == MeasureSpec.AT_MOST) {result = Math.min(result, specSize);}}return result;}/*** Determines the height of this view** @param measureSpec* A measureSpec packed into an int* @return The height of the view, honoring constraints from measureSpec*/private int measureShort(int measureSpec) {int result;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);if (specMode == MeasureSpec.EXACTLY) {//We were told how big to beresult = specSize;} else {//Measure the heightresult = (int)(2 * mRadius + getPaddingTop() + getPaddingBottom() + 1);//Respect AT_MOST value if that was what is called for by measureSpecif (specMode == MeasureSpec.AT_MOST) {result = Math.min(result, specSize);}}return result;}@Overridepublic void onRestoreInstanceState(Parcelable state) {SavedState savedState = (SavedState)state;super.onRestoreInstanceState(savedState.getSuperState());mCurrentPage = savedState.currentPage;mSnapPage = savedState.currentPage;requestLayout();}@Overridepublic 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();}@Overridepublic 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>() {@Overridepublic SavedState createFromParcel(Parcel in) {return new SavedState(in);}@Overridepublic SavedState[] newArray(int size) {return new SavedState[size];}};}}