/*
* Copyright (c) 2015 PocketHub
*
* 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.github.pockethub.android.ui.view;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import com.github.pockethub.android.R;
import com.github.pockethub.android.ui.ViewPager;
public class DotPageIndicator extends View implements android.support.v4.view.ViewPager.OnPageChangeListener {
private static final Paint PAINT_SELECTED = new Paint();
private static final Paint PAINT_NOT_SELECTED = new Paint();
/**
* Radius of a dot
*/
private float dotRadius;
/**
* Spacing between dots, can be set by app:spacing in the xml
*/
private float dotSpacing;
/**
* The width without the padding, set in onSizedChanged
*/
private float currentWidth;
/**
* The height without the padding, set in onSizedChanged
*/
private float currentHeight;
/**
* The ViewPager which is currently set
*/
private ViewPager viewPager;
/**
* If the drawing values needs to change (Only if size changed)
*/
private boolean updateValues;
/**
* Number of dots per row
*/
private int dotColumns;
/**
* Number of rows in the view
*/
private int dotRows;
/**
* The standard alpha from the selected dot color
*/
private int dotAlpha;
/**
* Fields for when the dot is moving
*/
private float amount;
private int currentPos;
private int nextPos;
/**
* Instantiates the view and it's values
* @param context
* @param attrs
*/
public DotPageIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
if (isInEditMode()) {
return;
}
Resources res = getResources();
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.DotPageIndicator,
0, 0);
try {
PAINT_SELECTED.setColor(a.getColor(R.styleable.DotPageIndicator_dotColorSelected,
getColor(res, android.R.color.secondary_text_light , context.getTheme())));
PAINT_NOT_SELECTED.setColor(a.getColor(R.styleable.DotPageIndicator_dotColor,
getColor(res, android.R.color.secondary_text_dark , context.getTheme())));
dotSpacing = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, res.getDisplayMetrics());
dotRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, res.getDisplayMetrics());
dotRadius = a.getDimensionPixelSize(R.styleable.DotPageIndicator_dotRadius, (int) dotRadius);
dotSpacing = a.getDimensionPixelSize(R.styleable.DotPageIndicator_dotSpacing, (int) dotSpacing);
}finally {
a.recycle();
}
dotAlpha = PAINT_SELECTED.getAlpha();
}
public void setDotColor(@ColorInt int color) {
PAINT_NOT_SELECTED.setColor(color);
}
public void setSelectedDotColor(@ColorInt int color) {
PAINT_SELECTED.setColor(color);
}
/**
* Sets up the size of the view and saves it for further use
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if(viewPager != null) {
createOnDrawValues();
}
int desiredWidth = (int) getWidthOfDots(dotColumns);
int desiredHeight = (int) getHeightOfDots(dotRows);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthSize);
} else {
width = desiredWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredHeight, heightSize);
} else {
height = desiredHeight;
}
setMeasuredDimension(width, height);
float horiPadding = getPaddingLeft() + getPaddingRight();
float vertPadding = getPaddingBottom() + getPaddingTop();
currentWidth = width - horiPadding;
currentHeight = height - vertPadding;
updateValues = true;
}
/**
* Updates values for drawing
*/
private void createOnDrawValues(){
int pageAmount = getNumberOfPages();
float w = getWidthOfDots(pageAmount);
dotRows = 1;
dotColumns = pageAmount;
if (currentWidth > 0 && w > currentWidth) {
for (int i = 1; i < pageAmount; i++) {
float tempWidth = currentWidth - getWidthOfDots(i);
if (tempWidth <= 0) {
dotRows = (int) Math.ceil(pageAmount / i - 1);
dotColumns = i - 1;
break;
}
}
if(getHeightOfDots(dotRows) > currentHeight) {
throw new IllegalStateException("Can't fit DotPageIndicator");
}
}
updateValues = false;
}
/**
* Draws the dots
*
* @param canvas The canvas to draw on
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (updateValues) {
createOnDrawValues();
}
for (int i = 0; i < dotRows; i++) {
int dotDiff = getNumberOfPages() - ((i + 1) * dotColumns);
int dots = dotDiff < 0 ? dotDiff + dotColumns : dotColumns;
float rowCenterY = (i * dotRadius * 2) + (i * dotSpacing) + dotRadius;
drawRow(canvas, dots, currentWidth, rowCenterY);
}
drawSelectedDot(canvas);
}
/**
* Draw the selected dot and when it moves
*
* @param canvas The canvas to draw on
*/
private void drawSelectedDot(Canvas canvas) {
float distance = (dotRadius * 2 + dotSpacing) * amount;
float fromPosX = getDotCenterX(currentPos);
float fromPosY = getDotCenterY(currentPos);
float toPosX = getDotCenterX(nextPos);
float toPosY = getDotCenterY(nextPos);
if (toPosY != fromPosY) {
PAINT_SELECTED.setAlpha(Math.round(dotAlpha * amount));
canvas.drawCircle(toPosX - ((dotRadius * 2 + dotSpacing) - distance), toPosY, dotRadius, PAINT_SELECTED);
PAINT_SELECTED.setAlpha(Math.round(dotAlpha * (1 - amount)));
}
canvas.drawCircle(fromPosX + distance, fromPosY, dotRadius, PAINT_SELECTED);
PAINT_SELECTED.setAlpha(dotAlpha);
}
/**
* Draws a row of dots
*
* @param canvas The canvas to draw on
* @param columns Number of dots in the row
* @param w Width of the view to draw on
* @param y Y position of the row (center)
*/
private void drawRow(Canvas canvas, int columns, float w, float y) {
float center = w / 2;
float start = center - getWidthOfDots(columns / 2);
start -= columns % 2 == 0 ? dotSpacing / 2 : dotRadius + dotSpacing;
for (int i = 0; i < columns; i++) {
canvas.drawCircle(start + dotRadius, y, dotRadius, PAINT_NOT_SELECTED);
start += (dotRadius * 2) + dotSpacing;
}
}
/**
* A helper method for getting colors from resource id
*
* @param res A reference to resources
* @param id Resource id to the color
* @param theme A theme reference
* @return The color specified
*/
private int getColor(Resources res, int id, Resources.Theme theme){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return res.getColor(id, theme);
} else {
return res.getColor(id);
}
}
/**
* Gets the center x position of the dot in position {@param index}
*
* @param index The dots index
* @return The x position
*/
private float getDotCenterX(int index) {
int column = index % dotColumns;
int row = (index - column) / dotColumns;
int columns = dotColumns;
if(dotColumns * (row + 1) > getNumberOfPages()) {
columns = dotColumns - ((dotColumns * (row + 1)) - getNumberOfPages());
}
float center = currentWidth / 2;
float start = center - getWidthOfDots(columns / 2);
start -= columns % 2 == 0 ? dotSpacing / 2 : dotRadius + dotSpacing;
start += ((dotRadius * 2) + dotSpacing) * column;
return start + dotRadius;
}
/**
* Gets the center y position of the dot in position {@param index}
*
* @param index The dots index
* @return The y position
*/
private float getDotCenterY(int index) {
int column = index % dotColumns;
int row = (index - column) / dotColumns;
return (row * dotRadius * 2) + (row * dotSpacing) + dotRadius;
}
/**
* Gets the width of some dots
*
* @param amount The amount of dots to measure
* @return The calculated width
*/
private float getWidthOfDots(int amount) {
return (amount * dotRadius * 2) + ((amount - 1) * dotSpacing);
}
/**
* Gets the height of some rows of dots
*
* @param rows The rows of dots to measure
* @return The calculated height
*/
private float getHeightOfDots(int rows){
return (rows * dotRadius * 2) + ((rows-1) * dotSpacing);
}
/**
* The amount of pages in the {@link ViewPager}
*
* @return Number of pages
*/
private int getNumberOfPages() {
return viewPager != null ? viewPager.getAdapter().getCount(): -1;
}
/**
* Sets the viewpager to use
*
* @param pager ViewPager
*/
public void setViewPager(ViewPager pager) {
if (pager == null) {
return;
}
if(viewPager != null) {
viewPager.removeOnPageChangeListener(this);
}
viewPager = pager;
pager.addOnPageChangeListener(this);
}
/**
* Animates the dot
*
* @param position
* @param positionOffset
* @param positionOffsetPixels
*/
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
amount = positionOffset;
currentPos = positionOffsetPixels >= 0 ? position : position + 1;
nextPos = positionOffsetPixels >= 0 ? position + 1 : position;
invalidate();
}
@Override
public void onPageSelected(int position) {
//Not used
}
/**
* Sets current item
*
* @param state
*/
@Override
public void onPageScrollStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_IDLE || state == ViewPager.SCROLL_STATE_SETTLING) {
currentPos = viewPager.getCurrentItem();
}
}
}