/*
* Copyright (C) 2011 Cyril Mottier (http://www.cyrilmottier.com)
*
* 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 greendroid.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import com.cyrilmottier.android.greendroid.R;
/**
* <p>
* Visual indicator of a paged content. The PageIndicator usually displays a
* line of dots. Each dot represents a page. The PageIndicator supports two
* types of dots.
* </p>
* <ul>
* <li>{@link DotType#SINGLE}: all dots are drawn but only one is in the
* selected state at a time. The selected one represents the currently visible
* page.</li>
* <li>{@link DotType#MULTIPLE}: the selected page is actually represented by
* the amount of dots currently being drawn. This behavior is similar to the one
* visible on the Android Launcher application.</li>
* </ul>
* <p>
* You can have a look at GDCatalog to get a sample code of how to use a
* PageIndicator in addition to a {@link PagedView}.
* </p>
*
* @author Cyril Mottier
*/
public class PageIndicator extends View {
/**
* Constant that may be used to select none of the dots in the PageIndicator
*/
public static final int NO_ACTIVE_DOT = -1;
/**
* Interface containing of dot types supported by the PageIndicator class.
*
* @author Cyril Mottier
*/
public interface DotType {
/**
* Represents the single dot type. Only one selected dot may be drawn at
* a time.
*/
int SINGLE = 0;
/**
* Represents the multiple dot type. Several selected dot may be drawn
* at a time. The number of dots drawn represents the currently
* remaining page count.
*/
int MULTIPLE = 1;
}
private static final int MIN_DOT_COUNT = 1;
private static Rect sInRect = new Rect();
private static Rect sOutRect = new Rect();
private int mGravity;
private int mDotSpacing;
private Drawable mDotDrawable;
private int mDotCount;
private int mDotType;
private int mActiveDot;
private int[] mExtraState;
private boolean mInitializing;
public PageIndicator(Context context) {
this(context, null);
}
public PageIndicator(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.gdPageIndicatorStyle);
}
public PageIndicator(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initPageIndicator();
mInitializing = true;
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PageIndicator, defStyle, 0);
setDotCount(a.getInt(R.styleable.PageIndicator_dotCount, mDotCount));
setActiveDot(a.getInt(R.styleable.PageIndicator_activeDot, mActiveDot));
setDotDrawable(a.getDrawable(R.styleable.PageIndicator_dotDrawable));
setDotSpacing(a.getDimensionPixelSize(R.styleable.PageIndicator_dotSpacing, mDotSpacing));
setGravity(a.getInt(R.styleable.PageIndicator_gravity, mGravity));
setDotType(a.getInt(R.styleable.PageIndicator_dotType, mDotType));
a.recycle();
mInitializing = false;
}
private void initPageIndicator() {
mDotCount = MIN_DOT_COUNT;
mGravity = Gravity.CENTER;
mActiveDot = 0;
mDotSpacing = 0;
mDotType = DotType.SINGLE;
mExtraState = onCreateDrawableState(1);
mergeDrawableStates(mExtraState, SELECTED_STATE_SET);
}
/**
* Get the maximum number of dots to be drawn.
*
* @return The maximum number of dots.
* @see #setDotCount(int)
*/
public int getDotCount() {
return mDotCount;
}
/**
* Set the number of dots.
*
* @param dotCount The number oF dots
* @see #getDotCount()
*/
public void setDotCount(int dotCount) {
if (dotCount < MIN_DOT_COUNT) {
dotCount = MIN_DOT_COUNT;
}
if (mDotCount != dotCount) {
mDotCount = dotCount;
requestLayout();
invalidate();
}
}
/**
* Return the current active dot. Depending on the current dot type of this
* PageIndicator the current active dot may be the number of displayed dots
* or the index of the selected dot
*
* @return The current active dot index or dots count
* @see #setActiveDot(int)
*/
public int getActiveDot() {
return mActiveDot;
}
/**
* Set the index of the active dot or the number of active dots. Depending
* on the current dot type of this PageIndicator the current active dot may
* be the number of displayed dots or the index of the selected dot
*
* @param activeDot The number/index of (the) active dot(s)
* @see #getActiveDot()
*/
public void setActiveDot(int activeDot) {
if (activeDot < 0) {
activeDot = NO_ACTIVE_DOT;
}
switch (mDotType) {
case DotType.SINGLE:
if (activeDot > mDotCount - 1) {
activeDot = NO_ACTIVE_DOT;
}
break;
case DotType.MULTIPLE:
if (activeDot > mDotCount) {
activeDot = NO_ACTIVE_DOT;
}
}
mActiveDot = activeDot;
invalidate();
}
/**
* Return the Drawable currently used for each dot.
*
* @return The Drawable used to draw each dot.
*/
public Drawable getDotDrawable() {
return mDotDrawable;
}
/**
* Set the Drawable used for each dot. The given Drawable may be a
* StateListDrawable in order to take advantage of the selection system. If
* your StateListDrawable contains a android.R.attr.state_selected state,
* the Drawable will be used to represent a selected dot.
* <em><strong>Note :</strong> this methods does not support Drawable
* that has no intrinsic dimensions.</em>
*
* @param dotDrawable The Drawable used to represents a dot
*/
public void setDotDrawable(Drawable dotDrawable) {
if (dotDrawable != mDotDrawable) {
if (mDotDrawable != null) {
mDotDrawable.setCallback(null);
}
mDotDrawable = dotDrawable;
if (dotDrawable != null) {
if (dotDrawable.getIntrinsicHeight() == -1 || dotDrawable.getIntrinsicWidth() == -1) {
// Do not accept Drawable with no intrinsic dimensions.
return;
}
dotDrawable.setBounds(0, 0, dotDrawable.getIntrinsicWidth(), dotDrawable.getIntrinsicHeight());
dotDrawable.setCallback(this);
if (dotDrawable.isStateful()) {
dotDrawable.setState(getDrawableState());
}
}
requestLayout();
invalidate();
}
}
/**
* The spacing between each dot
*
* @return The spacing between each dot
*/
public int getDotSpacing() {
return mDotSpacing;
}
/**
* Set the spacing between each dot
*
* @param dotSpacing The spacing between each dot.
*/
public void setDotSpacing(int dotSpacing) {
if (dotSpacing != mDotSpacing) {
mDotSpacing = dotSpacing;
requestLayout();
invalidate();
}
}
/**
* Return the gravity used to draw dots/
*
* @return The current gravity
*/
public int getGravity() {
return mGravity;
}
/**
* Specifies how to align the dots by the view's x- and/or y-axis when the
* space taken by the dots is smaller than the view.
*
* @param gravity The gravity
*/
public void setGravity(int gravity) {
if (mGravity != gravity) {
mGravity = gravity;
invalidate();
}
}
/**
* The current dot type
*
* @return The dot type of this {@link PageIndicator}
* @see DotType
*/
public int getDotType() {
return mDotType;
}
/**
* Specifies the type of dot actually drawn by this {@link PageIndicator}
*
* @param dotType The dot type to use
* @see DotType
*/
public void setDotType(int dotType) {
if (dotType == DotType.SINGLE || dotType == DotType.MULTIPLE) {
if (mDotType != dotType) {
mDotType = dotType;
invalidate();
}
}
}
@Override
public void requestLayout() {
if (!mInitializing) {
super.requestLayout();
}
}
@Override
public void invalidate() {
if (!mInitializing) {
super.invalidate();
}
}
@Override
protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || who == mDotDrawable;
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
mExtraState = onCreateDrawableState(1);
mergeDrawableStates(mExtraState, SELECTED_STATE_SET);
invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Drawable d = mDotDrawable;
int width = 0;
int height = 0;
if (d != null) {
width = mDotCount * (d.getIntrinsicWidth() + mDotSpacing) - mDotSpacing;
height = d.getIntrinsicHeight();
}
width += getPaddingRight() + getPaddingLeft();
height += getPaddingBottom() + getPaddingTop();
setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
}
@Override
protected void onDraw(Canvas canvas) {
final Drawable d = mDotDrawable;
if (d != null) {
final int count = mDotType == DotType.SINGLE ? mDotCount : mActiveDot;
if (count <= 0) {
return;
}
final int h = d.getIntrinsicHeight();
final int w = Math.max(0, count * (d.getIntrinsicWidth() + mDotSpacing) - mDotSpacing);
final int pRight = getPaddingRight();
final int pLeft = getPaddingLeft();
final int pTop = getPaddingTop();
final int pBottom = getPaddingBottom();
sInRect.set(pLeft, pTop, getWidth() - pRight, getHeight() - pBottom);
Gravity.apply(mGravity, w, h, sInRect, sOutRect);
canvas.save();
canvas.translate(sOutRect.left, sOutRect.top);
for (int i = 0; i < count; i++) {
if (d.isStateful()) {
int[] state = getDrawableState();
if (mDotType == DotType.MULTIPLE || i == mActiveDot) {
state = mExtraState;
}
// HACK Cyril: The following code prevent the setState call
// from invalidating the View again (which will result in
// calling onDraw over and over again).
d.setCallback(null);
d.setState(state);
d.setCallback(this);
}
d.draw(canvas);
canvas.translate(mDotSpacing + d.getIntrinsicWidth(), 0);
}
canvas.restore();
}
}
static class SavedState extends BaseSavedState {
int activeDot;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
activeDot = in.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(activeDot);
}
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.activeDot = mActiveDot;
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mActiveDot = ss.activeDot;
}
}