blob: 2c984a7e88a4373db7d2c2a582ca08a81c16379b [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 android.support.v17.leanback.widget;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.ColorInt;
import android.support.annotation.VisibleForTesting;
import android.support.v17.leanback.R;
import android.util.AttributeSet;
import android.util.Property;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
/**
* A page indicator with dots.
* @hide
*/
public class PagingIndicator extends View {
private static final long DURATION_ALPHA = 167;
private static final long DURATION_DIAMETER = 417;
private static final long DURATION_TRANSLATION_X = DURATION_DIAMETER;
private static final TimeInterpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
private static final Property<Dot, Float> DOT_ALPHA
= new Property<Dot, Float>(Float.class, "alpha") {
@Override
public Float get(Dot dot) {
return dot.getAlpha();
}
@Override
public void set(Dot dot, Float value) {
dot.setAlpha(value);
}
};
private static final Property<Dot, Float> DOT_DIAMETER
= new Property<Dot, Float>(Float.class, "diameter") {
@Override
public Float get(Dot dot) {
return dot.getDiameter();
}
@Override
public void set(Dot dot, Float value) {
dot.setDiameter(value);
}
};
private static final Property<Dot, Float> DOT_TRANSLATION_X
= new Property<Dot, Float>(Float.class, "translation_x") {
@Override
public Float get(Dot dot) {
return dot.getTranslationX();
}
@Override
public void set(Dot dot, Float value) {
dot.setTranslationX(value);
}
};
// attribute
boolean mIsLtr;
final int mDotDiameter;
final int mDotRadius;
private final int mDotGap;
final int mArrowDiameter;
final int mArrowRadius;
private final int mArrowGap;
private final int mShadowRadius;
private Dot[] mDots;
// X position when the dot is selected.
private int[] mDotSelectedX;
// X position when the dot is located to the left of the selected dot.
private int[] mDotSelectedPrevX;
// X position when the dot is located to the right of the selected dot.
private int[] mDotSelectedNextX;
int mDotCenterY;
// state
private int mPageCount;
private int mCurrentPage;
private int mPreviousPage;
// drawing
@ColorInt
final int mDotFgSelectColor;
final Paint mBgPaint;
final Paint mFgPaint;
private final AnimatorSet mShowAnimator;
private final AnimatorSet mHideAnimator;
private final AnimatorSet mAnimator = new AnimatorSet();
Bitmap mArrow;
final Rect mArrowRect;
final float mArrowToBgRatio;
public PagingIndicator(Context context) {
this(context, null, 0);
}
public PagingIndicator(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PagingIndicator(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
Resources res = getResources();
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PagingIndicator,
defStyle, 0);
mDotRadius = getDimensionFromTypedArray(typedArray, R.styleable.PagingIndicator_dotRadius,
R.dimen.lb_page_indicator_dot_radius);
mDotDiameter = mDotRadius * 2;
mArrowRadius = getDimensionFromTypedArray(typedArray,
R.styleable.PagingIndicator_arrowRadius, R.dimen.lb_page_indicator_arrow_radius);
mArrowDiameter = mArrowRadius * 2;
mDotGap = getDimensionFromTypedArray(typedArray, R.styleable.PagingIndicator_dotToDotGap,
R.dimen.lb_page_indicator_dot_gap);
mArrowGap = getDimensionFromTypedArray(typedArray,
R.styleable.PagingIndicator_dotToArrowGap, R.dimen.lb_page_indicator_arrow_gap);
int bgColor = getColorFromTypedArray(typedArray, R.styleable.PagingIndicator_dotBgColor,
R.color.lb_page_indicator_dot);
mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBgPaint.setColor(bgColor);
mDotFgSelectColor = getColorFromTypedArray(typedArray,
R.styleable.PagingIndicator_arrowBgColor,
R.color.lb_page_indicator_arrow_background);
typedArray.recycle();
mIsLtr = res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
int shadowColor = res.getColor(R.color.lb_page_indicator_arrow_shadow);
mShadowRadius = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_shadow_radius);
mFgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
int shadowOffset = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_shadow_offset);
mFgPaint.setShadowLayer(mShadowRadius, shadowOffset, shadowOffset, shadowColor);
mArrow = loadArrow();
mArrowRect = new Rect(0, 0, mArrow.getWidth(), mArrow.getHeight());
mArrowToBgRatio = (float) mArrow.getWidth() / (float) mArrowDiameter;
// Initialize animations.
mShowAnimator = new AnimatorSet();
mShowAnimator.playTogether(createDotAlphaAnimator(0.0f, 1.0f),
createDotDiameterAnimator(mDotRadius * 2, mArrowRadius * 2),
createDotTranslationXAnimator());
mHideAnimator = new AnimatorSet();
mHideAnimator.playTogether(createDotAlphaAnimator(1.0f, 0.0f),
createDotDiameterAnimator(mArrowRadius * 2, mDotRadius * 2),
createDotTranslationXAnimator());
mAnimator.playTogether(mShowAnimator, mHideAnimator);
// Use software layer to show shadows.
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
private int getDimensionFromTypedArray(TypedArray typedArray, int attr, int defaultId) {
return typedArray.getDimensionPixelOffset(attr,
getResources().getDimensionPixelOffset(defaultId));
}
private int getColorFromTypedArray(TypedArray typedArray, int attr, int defaultId) {
return typedArray.getColor(attr, getResources().getColor(defaultId));
}
private Bitmap loadArrow() {
Bitmap arrow = BitmapFactory.decodeResource(getResources(), R.drawable.lb_ic_nav_arrow);
if (mIsLtr) {
return arrow;
} else {
Matrix matrix = new Matrix();
matrix.preScale(-1, 1);
return Bitmap.createBitmap(arrow, 0, 0, arrow.getWidth(), arrow.getHeight(), matrix,
false);
}
}
private Animator createDotAlphaAnimator(float from, float to) {
ObjectAnimator animator = ObjectAnimator.ofFloat(null, DOT_ALPHA, from, to);
animator.setDuration(DURATION_ALPHA);
animator.setInterpolator(DECELERATE_INTERPOLATOR);
return animator;
}
private Animator createDotDiameterAnimator(float from, float to) {
ObjectAnimator animator = ObjectAnimator.ofFloat(null, DOT_DIAMETER, from, to);
animator.setDuration(DURATION_DIAMETER);
animator.setInterpolator(DECELERATE_INTERPOLATOR);
return animator;
}
private Animator createDotTranslationXAnimator() {
// The direction is determined in the Dot.
ObjectAnimator animator = ObjectAnimator.ofFloat(null, DOT_TRANSLATION_X,
-mArrowGap + mDotGap, 0.0f);
animator.setDuration(DURATION_TRANSLATION_X);
animator.setInterpolator(DECELERATE_INTERPOLATOR);
return animator;
}
/**
* Sets the page count.
*/
public void setPageCount(int pages) {
if (pages <= 0) {
throw new IllegalArgumentException("The page count should be a positive integer");
}
mPageCount = pages;
mDots = new Dot[mPageCount];
for (int i = 0; i < mPageCount; ++i) {
mDots[i] = new Dot();
}
calculateDotPositions();
setSelectedPage(0);
}
/**
* Called when the page has been selected.
*/
public void onPageSelected(int pageIndex, boolean withAnimation) {
if (mCurrentPage == pageIndex) {
return;
}
if (mAnimator.isStarted()) {
mAnimator.end();
}
mPreviousPage = mCurrentPage;
if (withAnimation) {
mHideAnimator.setTarget(mDots[mPreviousPage]);
mShowAnimator.setTarget(mDots[pageIndex]);
mAnimator.start();
}
setSelectedPage(pageIndex);
}
private void calculateDotPositions() {
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getWidth() - getPaddingRight();
int requiredWidth = getRequiredWidth();
int mid = (left + right) / 2;
mDotSelectedX = new int[mPageCount];
mDotSelectedPrevX = new int[mPageCount];
mDotSelectedNextX = new int[mPageCount];
if (mIsLtr) {
int startLeft = mid - requiredWidth / 2;
// mDotSelectedX[0] should be mDotSelectedPrevX[-1] + mArrowGap
mDotSelectedX[0] = startLeft + mDotRadius - mDotGap + mArrowGap;
mDotSelectedPrevX[0] = startLeft + mDotRadius;
mDotSelectedNextX[0] = startLeft + mDotRadius - 2 * mDotGap + 2 * mArrowGap;
for (int i = 1; i < mPageCount; i++) {
mDotSelectedX[i] = mDotSelectedPrevX[i - 1] + mArrowGap;
mDotSelectedPrevX[i] = mDotSelectedPrevX[i - 1] + mDotGap;
mDotSelectedNextX[i] = mDotSelectedX[i - 1] + mArrowGap;
}
} else {
int startRight = mid + requiredWidth / 2;
// mDotSelectedX[0] should be mDotSelectedPrevX[-1] - mArrowGap
mDotSelectedX[0] = startRight - mDotRadius + mDotGap - mArrowGap;
mDotSelectedPrevX[0] = startRight - mDotRadius;
mDotSelectedNextX[0] = startRight - mDotRadius + 2 * mDotGap - 2 * mArrowGap;
for (int i = 1; i < mPageCount; i++) {
mDotSelectedX[i] = mDotSelectedPrevX[i - 1] - mArrowGap;
mDotSelectedPrevX[i] = mDotSelectedPrevX[i - 1] - mDotGap;
mDotSelectedNextX[i] = mDotSelectedX[i - 1] - mArrowGap;
}
}
mDotCenterY = top + mArrowRadius;
adjustDotPosition();
}
@VisibleForTesting
int getPageCount() {
return mPageCount;
}
@VisibleForTesting
int[] getDotSelectedX() {
return mDotSelectedX;
}
@VisibleForTesting
int[] getDotSelectedLeftX() {
return mDotSelectedPrevX;
}
@VisibleForTesting
int[] getDotSelectedRightX() {
return mDotSelectedNextX;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desiredHeight = getDesiredHeight();
int height;
switch (MeasureSpec.getMode(heightMeasureSpec)) {
case MeasureSpec.EXACTLY:
height = MeasureSpec.getSize(heightMeasureSpec);
break;
case MeasureSpec.AT_MOST:
height = Math.min(desiredHeight, MeasureSpec.getSize(heightMeasureSpec));
break;
case MeasureSpec.UNSPECIFIED:
default:
height = desiredHeight;
break;
}
int desiredWidth = getDesiredWidth();
int width;
switch (MeasureSpec.getMode(widthMeasureSpec)) {
case MeasureSpec.EXACTLY:
width = MeasureSpec.getSize(widthMeasureSpec);
break;
case MeasureSpec.AT_MOST:
width = Math.min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec));
break;
case MeasureSpec.UNSPECIFIED:
default:
width = desiredWidth;
break;
}
setMeasuredDimension(width, height);
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
setMeasuredDimension(width, height);
calculateDotPositions();
}
private int getDesiredHeight() {
return getPaddingTop() + mArrowDiameter + getPaddingBottom() + mShadowRadius;
}
private int getRequiredWidth() {
return 2 * mDotRadius + 2 * mArrowGap + (mPageCount - 3) * mDotGap;
}
private int getDesiredWidth() {
return getPaddingLeft() + getRequiredWidth() + getPaddingRight();
}
@Override
protected void onDraw(Canvas canvas) {
for (int i = 0; i < mPageCount; ++i) {
mDots[i].draw(canvas);
}
}
private void setSelectedPage(int now) {
if (now == mCurrentPage) {
return;
}
mCurrentPage = now;
adjustDotPosition();
}
private void adjustDotPosition() {
for (int i = 0; i < mCurrentPage; ++i) {
mDots[i].deselect();
mDots[i].mDirection = i == mPreviousPage ? Dot.LEFT : Dot.RIGHT;
mDots[i].mCenterX = mDotSelectedPrevX[i];
}
mDots[mCurrentPage].select();
mDots[mCurrentPage].mDirection = mPreviousPage < mCurrentPage ? Dot.LEFT : Dot.RIGHT;
mDots[mCurrentPage].mCenterX = mDotSelectedX[mCurrentPage];
for (int i = mCurrentPage + 1; i < mPageCount; ++i) {
mDots[i].deselect();
mDots[i].mDirection = Dot.RIGHT;
mDots[i].mCenterX = mDotSelectedNextX[i];
}
}
@Override
public void onRtlPropertiesChanged(int layoutDirection) {
super.onRtlPropertiesChanged(layoutDirection);
boolean isLtr = layoutDirection == View.LAYOUT_DIRECTION_LTR;
if (mIsLtr != isLtr) {
mIsLtr = isLtr;
mArrow = loadArrow();
if (mDots != null) {
for (Dot dot : mDots) {
dot.onRtlPropertiesChanged();
}
}
calculateDotPositions();
invalidate();
}
}
public class Dot {
static final float LEFT = -1;
static final float RIGHT = 1;
static final float LTR = 1;
static final float RTL = -1;
float mAlpha;
@ColorInt
int mFgColor;
float mTranslationX;
float mCenterX;
float mDiameter;
float mRadius;
float mArrowImageRadius;
float mDirection = RIGHT;
float mLayoutDirection = mIsLtr ? LTR : RTL;
void select() {
mTranslationX = 0.0f;
mCenterX = 0.0f;
mDiameter = mArrowDiameter;
mRadius = mArrowRadius;
mArrowImageRadius = mRadius * mArrowToBgRatio;
mAlpha = 1.0f;
adjustAlpha();
}
void deselect() {
mTranslationX = 0.0f;
mCenterX = 0.0f;
mDiameter = mDotDiameter;
mRadius = mDotRadius;
mArrowImageRadius = mRadius * mArrowToBgRatio;
mAlpha = 0.0f;
adjustAlpha();
}
public void adjustAlpha() {
int alpha = Math.round(0xFF * mAlpha);
int red = Color.red(mDotFgSelectColor);
int green = Color.green(mDotFgSelectColor);
int blue = Color.blue(mDotFgSelectColor);
mFgColor = Color.argb(alpha, red, green, blue);
}
public float getAlpha() {
return mAlpha;
}
public void setAlpha(float alpha) {
this.mAlpha = alpha;
adjustAlpha();
invalidate();
}
public float getTranslationX() {
return mTranslationX;
}
public void setTranslationX(float translationX) {
this.mTranslationX = translationX * mDirection * mLayoutDirection;
invalidate();
}
public float getDiameter() {
return mDiameter;
}
public void setDiameter(float diameter) {
this.mDiameter = diameter;
this.mRadius = diameter / 2;
this.mArrowImageRadius = diameter / 2 * mArrowToBgRatio;
invalidate();
}
void draw(Canvas canvas) {
float centerX = mCenterX + mTranslationX;
canvas.drawCircle(centerX, mDotCenterY, mRadius, mBgPaint);
if (mAlpha > 0) {
mFgPaint.setColor(mFgColor);
canvas.drawCircle(centerX, mDotCenterY, mRadius, mFgPaint);
canvas.drawBitmap(mArrow, mArrowRect, new Rect((int) (centerX - mArrowImageRadius),
(int) (mDotCenterY - mArrowImageRadius),
(int) (centerX + mArrowImageRadius),
(int) (mDotCenterY + mArrowImageRadius)), null);
}
}
void onRtlPropertiesChanged() {
mLayoutDirection = mIsLtr ? LTR : RTL;
}
}
}