/*
* Copyright (C) 2011 Patrik Akerfeldt
* Copyright (C) 2011 Francisco Figueiredo Jr.
* 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.fanfou.app.opensource.ui.viewpager;
import java.util.ArrayList;
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.RectF;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.fanfou.app.opensource.HomePage;
import com.fanfou.app.opensource.R;
/**
* 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.
*/
/**
* @author mcxiaoke
* @version 1.1 2011.08.20
* @version 1.2 2011.11.04
* @version 1.3 2011.11.08
* @version 1.4 2011.11.11
*
*/
public class TitlePageIndicator extends View implements PageIndicator {
public enum IndicatorStyle {
None(0), Triangle(1), Underline(2);
public static IndicatorStyle fromValue(final int value) {
for (final IndicatorStyle style : IndicatorStyle.values()) {
if (style.value == value) {
return style;
}
}
return null;
}
public final int value;
private IndicatorStyle(final int value) {
this.value = value;
}
}
static class SavedState extends BaseSavedState {
int currentPage;
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(final Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(final int size) {
return new SavedState[size];
}
};
private SavedState(final Parcel in) {
super(in);
this.currentPage = in.readInt();
}
public SavedState(final Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(this.currentPage);
}
}
private static final String TAG = TitlePageIndicator.class.getSimpleName();
/**
* 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;
private ViewPager mViewPager;
private TitleProvider mTitleProvider;
private int mCurrentPage;
private int mCurrentOffset;
private final Paint mPaintText;
private boolean mBoldText;
private int mColorText;
private int mColorSelected;
private final Path mPath;
private final Paint mPaintFooterLine;
private IndicatorStyle mFooterIndicatorStyle;
private final Paint mPaintFooterIndicator;
private float mFooterIndicatorHeight;
private final float mFooterIndicatorUnderlinePadding;
private float mFooterPadding;
private float mTitlePadding;
/** Left and right side padding for not active view titles. */
private float mClipPadding;
private float mFooterLineHeight;
public TitlePageIndicator(final Context context) {
this(context, null);
}
public TitlePageIndicator(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.titlePageIndicatorStyle);
}
public TitlePageIndicator(final Context context, final AttributeSet attrs,
final int defStyle) {
super(context, attrs, defStyle);
// Load defaults from resources
final Resources res = getResources();
final int defaultFooterColor = res
.getColor(R.color.default_title_indicator_footer_color);
final float defaultFooterLineHeight = res
.getDimension(R.dimen.default_title_indicator_footer_line_height);
final int defaultFooterIndicatorStyle = res
.getInteger(R.integer.default_title_indicator_footer_indicator_style);
final float defaultFooterIndicatorHeight = res
.getDimension(R.dimen.default_title_indicator_footer_indicator_height);
final float defaultFooterIndicatorUnderlinePadding = res
.getDimension(R.dimen.default_title_indicator_footer_indicator_underline_padding);
final float defaultFooterPadding = res
.getDimension(R.dimen.default_title_indicator_footer_padding);
final int defaultSelectedColor = res
.getColor(R.color.default_title_indicator_selected_color);
final boolean defaultSelectedBold = res
.getBoolean(R.bool.default_title_indicator_selected_bold);
final int defaultTextColor = res
.getColor(R.color.default_title_indicator_text_color);
final float defaultTextSize = res
.getDimension(R.dimen.default_title_indicator_text_size);
final float defaultTitlePadding = res
.getDimension(R.dimen.default_title_indicator_title_padding);
final float defaultClipPadding = res
.getDimension(R.dimen.default_title_indicator_clip_padding);
// Retrieve styles attributes
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.TitlePageIndicator, defStyle,
R.style.Widget_TitlePageIndicator);
// Retrieve the colors to be used for this view and apply them.
this.mFooterLineHeight = a.getDimension(
R.styleable.TitlePageIndicator_footerLineHeight,
defaultFooterLineHeight);
this.mFooterIndicatorStyle = IndicatorStyle.fromValue(a.getInteger(
R.styleable.TitlePageIndicator_footerIndicatorStyle,
defaultFooterIndicatorStyle));
this.mFooterIndicatorHeight = a.getDimension(
R.styleable.TitlePageIndicator_footerIndicatorHeight,
defaultFooterIndicatorHeight);
this.mFooterIndicatorUnderlinePadding = a.getDimension(
R.styleable.TitlePageIndicator_footerIndicatorUnderlinePadding,
defaultFooterIndicatorUnderlinePadding);
this.mFooterPadding = a.getDimension(
R.styleable.TitlePageIndicator_footerPadding,
defaultFooterPadding);
this.mTitlePadding = a.getDimension(
R.styleable.TitlePageIndicator_titlePadding,
defaultTitlePadding);
this.mClipPadding = a.getDimension(
R.styleable.TitlePageIndicator_clipPadding, defaultClipPadding);
this.mColorSelected = a.getColor(
R.styleable.TitlePageIndicator_selectedColor,
defaultSelectedColor);
this.mColorText = a.getColor(R.styleable.TitlePageIndicator_textColor,
defaultTextColor);
this.mBoldText = a.getBoolean(
R.styleable.TitlePageIndicator_selectedBold,
defaultSelectedBold);
final float textSize = a.getDimension(
R.styleable.TitlePageIndicator_textSize, defaultTextSize);
final int footerColor = a.getColor(
R.styleable.TitlePageIndicator_footerColor, defaultFooterColor);
this.mPaintText = new Paint();
this.mPaintText.setTextSize(textSize);
this.mPaintText.setAntiAlias(true);
this.mPaintFooterLine = new Paint();
this.mPaintFooterLine.setStyle(Paint.Style.FILL_AND_STROKE);
this.mPaintFooterLine.setStrokeWidth(this.mFooterLineHeight);
this.mPaintFooterLine.setColor(footerColor);
this.mPaintFooterIndicator = new Paint();
this.mPaintFooterIndicator.setStyle(Paint.Style.FILL_AND_STROKE);
this.mPaintFooterIndicator.setColor(footerColor);
a.recycle();
this.mPath = new Path();
}
/**
* Calculate the bounds for a view's title
*
* @param index
* @param paint
* @return
*/
private RectF calcBounds(final int index, final Paint paint) {
// Calculate the text bounds
final RectF bounds = new RectF();
bounds.right = paint.measureText(this.mTitleProvider.getTitle(index));
bounds.bottom = paint.descent() - paint.ascent();
return bounds;
}
/**
* Calculate views bounds and scroll them according to the current index
*
* @param paint
* @param currentIndex
* @return
*/
private ArrayList<RectF> calculateAllBounds(final Paint paint) {
final ArrayList<RectF> list = new ArrayList<RectF>();
// For each views (If no values then add a fake one)
final int count = getPageCount();
final int width = getWidth();
final int halfWidth = width / 2;
for (int i = 0; i < count; i++) {
final RectF bounds = calcBounds(i, paint);
final float w = (bounds.right - bounds.left);
final float h = (bounds.bottom - bounds.top);
bounds.left = ((halfWidth) - (w / 2) - this.mCurrentOffset)
+ ((i - this.mCurrentPage) * width);
bounds.right = bounds.left + w;
bounds.top = 0;
bounds.bottom = h;
list.add(bounds);
}
return list;
}
/**
* Set bounds for the left textView including clip padding.
*
* @param curViewBound
* current bounds.
* @param curViewWidth
* width of the view.
*/
private void clipViewOnTheLeft(final RectF curViewBound,
final float curViewWidth, final int left) {
curViewBound.left = left + this.mClipPadding;
curViewBound.right = this.mClipPadding + curViewWidth;
}
/**
* Set bounds for the right textView including clip padding.
*
* @param curViewBound
* current bounds.
* @param curViewWidth
* width of the view.
*/
private void clipViewOnTheRight(final RectF curViewBound,
final float curViewWidth, final int right) {
curViewBound.right = right - this.mClipPadding;
curViewBound.left = curViewBound.right - curViewWidth;
}
public float getClipPadding() {
return this.mClipPadding;
}
public int getFooterColor() {
return this.mPaintFooterLine.getColor();
}
public float getFooterIndicatorHeight() {
return this.mFooterIndicatorHeight;
}
public float getFooterIndicatorPadding() {
return this.mFooterPadding;
}
public IndicatorStyle getFooterIndicatorStyle() {
return this.mFooterIndicatorStyle;
}
public float getFooterLineHeight() {
return this.mFooterLineHeight;
}
@Override
public int getPageCount() {
return HomePage.NUMS_OF_PAGE;
}
@Override
public int getPageWidth() {
return this.mViewPager.getWidth();
}
public int getSelectedColor() {
return this.mColorSelected;
}
public int getTextColor() {
return this.mColorText;
}
public float getTextSize() {
return this.mPaintText.getTextSize();
}
public float getTitlePadding() {
return this.mTitlePadding;
}
public boolean isSelectedBold() {
return this.mBoldText;
}
/**
* 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 measureHeight(final int measureSpec) {
float result = 0;
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Calculate the text bounds
final RectF bounds = new RectF();
bounds.bottom = this.mPaintText.descent()
- this.mPaintText.ascent();
result = (bounds.bottom - bounds.top) + this.mFooterLineHeight
+ this.mFooterPadding;
if (this.mFooterIndicatorStyle != IndicatorStyle.None) {
result += this.mFooterIndicatorHeight;
}
}
return (int) result;
}
/**
* 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 measureWidth(final int measureSpec) {
int result = 0;
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
if (specMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException(getClass().getSimpleName()
+ " can only be used in EXACTLY mode.");
}
result = specSize;
return result;
}
/*
* (non-Javadoc)
*
* @see android.view.View#onDraw(android.graphics.Canvas)
*/
@Override
protected void onDraw(final Canvas canvas) {
super.onDraw(canvas);
// Calculate views bounds
final ArrayList<RectF> bounds = calculateAllBounds(this.mPaintText);
final int count = getPageCount();
final int countMinusOne = count - 1;
final float halfWidth = getWidth() / 2f;
final int left = getLeft();
final float leftClip = left + this.mClipPadding;
final int width = getWidth();
final int height = getHeight();
final int right = left + width;
final float rightClip = right - this.mClipPadding;
final float offsetPercent = (1.0f * this.mCurrentOffset) / width;
int page = this.mCurrentPage;
if (this.mCurrentOffset > halfWidth) {
page++;
}
final boolean currentSelected = (offsetPercent <= TitlePageIndicator.SELECTION_FADE_PERCENTAGE);
final boolean currentBold = (offsetPercent <= TitlePageIndicator.BOLD_FADE_PERCENTAGE);
final float selectedPercent = (TitlePageIndicator.SELECTION_FADE_PERCENTAGE - offsetPercent)
/ TitlePageIndicator.SELECTION_FADE_PERCENTAGE;
// Verify if the current view must be clipped to the screen
final RectF curPageBound = bounds.get(this.mCurrentPage);
// fix null pointer exception
if (curPageBound != null) {
final 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 (this.mCurrentPage > 0) {
for (int i = this.mCurrentPage - 1; i >= 0; i--) {
final RectF bound = bounds.get(i);
// Is left side is outside the screen
if (bound.left < leftClip) {
final float 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
final RectF rightBound = bounds.get(i + 1);
// Intersection
if ((bound.right + this.mTitlePadding) > rightBound.left) {
bound.left = rightBound.left - w - this.mTitlePadding;
bound.right = bound.left + w;
}
}
}
}
// Right views starting from the current position
if (this.mCurrentPage < countMinusOne) {
for (int i = this.mCurrentPage + 1; i < count; i++) {
final RectF bound = bounds.get(i);
// If right side is outside the screen
if (bound.right > rightClip) {
final float 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
final RectF leftBound = bounds.get(i - 1);
// Intersection
if ((bound.left - this.mTitlePadding) < leftBound.right) {
bound.left = leftBound.right + this.mTitlePadding;
bound.right = bound.left + w;
}
}
}
}
// if(App.DEBUG){
// Log.e(TAG,
// "===============================================================");
// Log.e(TAG, "onDraw() mCurrentPage="+mCurrentPage);
// Log.e(TAG, "onDraw() ");
// Log.e(TAG, "onDraw() ");
// Log.e(TAG, "onDraw() ");
// Log.e(TAG, "onDraw() ");
// Log.e(TAG, "onDraw() ");
// Log.e(TAG,
// "===============================================================");
// }
// Now draw views
for (int i = 0; i < count; i++) {
// Get the title
final RectF 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);
// Only set bold if we are within bounds
this.mPaintText.setFakeBoldText(currentPage && currentBold
&& this.mBoldText);
// Draw text as unselected
this.mPaintText.setColor(this.mColorText);
canvas.drawText(this.mTitleProvider.getTitle(i), bound.left,
bound.bottom, this.mPaintText);
// If we are within the selected bounds draw the selected text
if (currentPage && currentSelected) {
this.mPaintText.setColor(this.mColorSelected);
this.mPaintText
.setAlpha((int) ((this.mColorSelected >>> 24) * selectedPercent));
canvas.drawText(this.mTitleProvider.getTitle(i),
bound.left, bound.bottom, this.mPaintText);
}
}
}
// Draw the footer line
// mPath = new Path();
this.mPath.reset();
this.mPath.moveTo(0, height - this.mFooterLineHeight);
this.mPath.lineTo(width, height - this.mFooterLineHeight);
this.mPath.close();
canvas.drawPath(this.mPath, this.mPaintFooterLine);
switch (this.mFooterIndicatorStyle) {
case Triangle:
this.mPath.moveTo(halfWidth, height - this.mFooterLineHeight
- this.mFooterIndicatorHeight);
this.mPath.lineTo(halfWidth + this.mFooterIndicatorHeight, height
- this.mFooterLineHeight);
this.mPath.lineTo(halfWidth - this.mFooterIndicatorHeight, height
- this.mFooterLineHeight);
this.mPath.close();
canvas.drawPath(this.mPath, this.mPaintFooterIndicator);
break;
case Underline:
if (!currentSelected) {
break;
}
final RectF underlineBounds = bounds.get(page);
this.mPath.moveTo(underlineBounds.left
- this.mFooterIndicatorUnderlinePadding, height
- this.mFooterLineHeight);
this.mPath.lineTo(underlineBounds.right
+ this.mFooterIndicatorUnderlinePadding, height
- this.mFooterLineHeight);
this.mPath.lineTo(underlineBounds.right
+ this.mFooterIndicatorUnderlinePadding, height
- this.mFooterLineHeight - this.mFooterIndicatorHeight);
this.mPath.lineTo(underlineBounds.left
- this.mFooterIndicatorUnderlinePadding, height
- this.mFooterLineHeight - this.mFooterIndicatorHeight);
this.mPath.close();
this.mPaintFooterIndicator.setAlpha((int) (0xFF * selectedPercent));
canvas.drawPath(this.mPath, this.mPaintFooterIndicator);
this.mPaintFooterIndicator.setAlpha(0xFF);
break;
}
}
/*
* (non-Javadoc)
*
* @see android.view.View#onMeasure(int, int)
*/
@Override
protected void onMeasure(final int widthMeasureSpec,
final int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
@Override
public void onPageScrolled(final int position, final float positionOffset,
final int positionOffsetPixels) {
this.mCurrentPage = position % HomePage.NUMS_OF_PAGE;
this.mCurrentOffset = positionOffsetPixels;
invalidate();
}
@Override
public void onPageSelected(final int position) {
this.mCurrentPage = position % HomePage.NUMS_OF_PAGE;
invalidate();
}
@Override
public void onRestoreInstanceState(final Parcelable state) {
final SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
this.mCurrentPage = savedState.currentPage;
requestLayout();
}
@Override
public Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
final SavedState savedState = new SavedState(superState);
savedState.currentPage = this.mCurrentPage;
return savedState;
}
// @Override
public boolean onTouchEvent2(final MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
final int count = getPageCount();
final int width = getWidth();
final float halfWidth = width / 2f;
final float sixthWidth = width / 6f;
if ((this.mCurrentPage > 0)
&& (event.getX() < (halfWidth - sixthWidth))) {
this.mViewPager.setCurrentItem(this.mCurrentPage - 1);
return true;
} else if ((this.mCurrentPage < (count - 1))
&& (event.getX() > (halfWidth + sixthWidth))) {
this.mViewPager.setCurrentItem(this.mCurrentPage + 1);
return true;
}
}
return super.onTouchEvent(event);
}
public void setClipPadding(final float clipPadding) {
this.mClipPadding = clipPadding;
invalidate();
}
@Override
public void setCurrentItem(final int item) {
if (this.mViewPager == null) {
throw new IllegalStateException("ViewPager has not been bound.");
}
this.mCurrentPage = item;
invalidate();
}
public void setFooterColor(final int footerColor) {
this.mPaintFooterLine.setColor(footerColor);
this.mPaintFooterIndicator.setColor(footerColor);
invalidate();
}
public void setFooterIndicatorHeight(final float footerTriangleHeight) {
this.mFooterIndicatorHeight = footerTriangleHeight;
invalidate();
}
public void setFooterIndicatorPadding(final float footerIndicatorPadding) {
this.mFooterPadding = footerIndicatorPadding;
invalidate();
}
public void setFooterIndicatorStyle(final IndicatorStyle indicatorStyle) {
this.mFooterIndicatorStyle = indicatorStyle;
invalidate();
}
public void setFooterLineHeight(final float footerLineHeight) {
this.mFooterLineHeight = footerLineHeight;
this.mPaintFooterLine.setStrokeWidth(this.mFooterLineHeight);
invalidate();
}
public void setSelectedBold(final boolean selectedBold) {
this.mBoldText = selectedBold;
invalidate();
}
public void setSelectedColor(final int selectedColor) {
this.mColorSelected = selectedColor;
invalidate();
}
public void setTextColor(final int textColor) {
this.mPaintText.setColor(textColor);
this.mColorText = textColor;
invalidate();
}
public void setTextSize(final float textSize) {
this.mPaintText.setTextSize(textSize);
invalidate();
}
public void setTitlePadding(final float titlePadding) {
this.mTitlePadding = titlePadding;
invalidate();
}
public void setTitleProvider(final TitleProvider provider) {
this.mTitleProvider = provider;
}
@Override
public void setViewPager(final ViewPager view) {
if (view.getAdapter() == null) {
throw new IllegalStateException(
"ViewPager does not have adapter instance.");
}
this.mViewPager = view;
invalidate();
}
@Override
public void setViewPager(final ViewPager view, final int initialPosition) {
setViewPager(view);
setCurrentItem(initialPosition);
}
}