blob: e2c9be72e68b8ec29e938dd1b6858b4a343a8138 [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 com.android.tv.common.ui.setup.leanback;
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorSet;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.VisibleForTesting;
import android.util.AttributeSet;
import android.view.View;
import com.android.tv.common.R;
import com.android.tv.common.annotation.UsedByReflection;
import java.util.ArrayList;
import java.util.List;
/**
* A page indicator with dots.
* @hide
*/
public class PagingIndicator extends View {
// attribute
private final int mDotDiameter;
private final int mDotRadius;
private final int mDotGap;
private final int mArrowDiameter;
private 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[] mDotSelectedLeftX;
// X position when the dot is located to the right of the selected dot.
private int[] mDotSelectedRightX;
private int mDotCenterY;
// state
private int mPageCount;
private int mCurrentPage;
private int mPreviousPage;
// drawing
@ColorInt
private final int mDotFgSelectColor;
private final Paint mBgPaint;
private final Paint mFgPaint;
private final Animator mShowAnimator;
private final Animator mHideAnimator;
private final AnimatorSet mAnimator = new AnimatorSet();
private final Bitmap mArrow;
private final Rect mArrowRect;
private 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();
mDotRadius = res.getDimensionPixelSize(R.dimen.lb_page_indicator_dot_radius);
mDotDiameter = mDotRadius * 2;
mDotGap = res.getDimensionPixelSize(R.dimen.lb_page_indicator_dot_gap);
mArrowGap = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_gap);
mArrowDiameter = res.getDimensionPixelSize(R.dimen.lb_page_indicator_arrow_diameter);
mArrowRadius = mArrowDiameter / 2;
mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDotFgSelectColor = res.getColor(R.color.lb_page_indicator_arrow_background);
int bgColor = res.getColor(R.color.lb_page_indicator_dot);
int shadowColor = res.getColor(R.color.lb_page_indicator_arrow_shadow);
mBgPaint.setColor(bgColor);
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 = BitmapFactory.decodeResource(res, R.drawable.lb_ic_nav_arrow);
mArrowRect = new Rect(0, 0, mArrow.getWidth(), mArrow.getHeight());
mArrowToBgRatio = (float) mArrow.getWidth() / (float) mArrowDiameter;
// Initialize animations.
List<Animator> animators = new ArrayList<>();
mShowAnimator = AnimatorInflater.loadAnimator(getContext(),
R.animator.lb_page_indicator_dot_show);
mHideAnimator = AnimatorInflater.loadAnimator(getContext(),
R.animator.lb_page_indicator_dot_hide);
animators.add(mShowAnimator);
animators.add(mHideAnimator);
mAnimator.playTogether(animators);
// Use software layer to show shadows.
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
/**
* 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;
int startLeft = mid - requiredWidth / 2;
mDotSelectedX = new int[mPageCount];
mDotSelectedLeftX = new int[mPageCount];
mDotSelectedRightX = new int[mPageCount];
// mDotSelectedX[0] should be mDotSelectedLeftX[-1] + mArrowGap
mDotSelectedX[0] = startLeft + mDotRadius - mDotGap + mArrowGap;
mDotSelectedLeftX[0] = startLeft + mDotRadius;
mDotSelectedRightX[0] = 0;
for (int i = 1; i < mPageCount; i++) {
mDotSelectedX[i] = mDotSelectedLeftX[i - 1] + mArrowGap;
mDotSelectedLeftX[i] = mDotSelectedLeftX[i - 1] + mDotGap;
mDotSelectedRightX[i] = mDotSelectedX[i - 1] + mArrowGap;
}
mDotCenterY = top + mArrowRadius;
adjustDotPosition();
}
@VisibleForTesting
int getPageCount() {
return mPageCount;
}
@VisibleForTesting
int[] getDotSelectedX() {
return mDotSelectedX;
}
@VisibleForTesting
int[] getDotSelectedLeftX() {
return mDotSelectedLeftX;
}
@VisibleForTesting
int[] getDotSelectedRightX() {
return mDotSelectedRightX;
}
@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 = mDotSelectedLeftX[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 = mDotSelectedRightX[i];
}
}
public class Dot {
static final float LEFT = -1;
static final float RIGHT = 1;
float mAlpha;
@ColorInt
int mBgColor;
@ColorInt
int mFgColor;
float mTranslationX;
float mCenterX;
float mDiameter;
float mRadius;
float mArrowImageRadius;
float mDirection = RIGHT;
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);
}
@UsedByReflection
public float getAlpha() {
return mAlpha;
}
@UsedByReflection
public void setAlpha(float alpha) {
this.mAlpha = alpha;
adjustAlpha();
invalidate();
}
@UsedByReflection
public float getTranslationX() {
return mTranslationX;
}
@UsedByReflection
public void setTranslationX(float translationX) {
this.mTranslationX = translationX * mDirection;
invalidate();
}
@UsedByReflection
public float getDiameter() {
return mDiameter;
}
@UsedByReflection
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);
}
}
}
}