Files
app_Settings/src/com/android/settings/widget/DotsPageIndicator.java
Noah Wang 085028d7ee Add dot page indicator to preview screen pager.
Change-Id: I4fa5aba28ad20be17bd5fa8d3c6a06d8a9a4a64a
2016-01-15 10:48:04 -08:00

919 lines
35 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (C) 2016 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.settings.widget;
import static android.view.animation.AnimationUtils.loadInterpolator;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Build;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Interpolator;
import com.android.settings.R;
import java.util.Arrays;
/**
* Custom pager indicator for use with a {@code ViewPager}.
*/
public class DotsPageIndicator extends View implements ViewPager.OnPageChangeListener {
public static final String TAG = DotsPageIndicator.class.getSimpleName();
// defaults
private static final int DEFAULT_DOT_SIZE = 8; // dp
private static final int DEFAULT_GAP = 12; // dp
private static final int DEFAULT_ANIM_DURATION = 400; // ms
private static final int DEFAULT_UNSELECTED_COLOUR = 0x80ffffff; // 50% white
private static final int DEFAULT_SELECTED_COLOUR = 0xffffffff; // 100% white
// constants
private static final float INVALID_FRACTION = -1f;
private static final float MINIMAL_REVEAL = 0.00001f;
// configurable attributes
private int dotDiameter;
private int gap;
private long animDuration;
private int unselectedColour;
private int selectedColour;
// derived from attributes
private float dotRadius;
private float halfDotRadius;
private long animHalfDuration;
private float dotTopY;
private float dotCenterY;
private float dotBottomY;
// ViewPager
private ViewPager viewPager;
private ViewPager.OnPageChangeListener pageChangeListener;
// state
private int pageCount;
private int currentPage;
private float selectedDotX;
private boolean selectedDotInPosition;
private float[] dotCenterX;
private float[] joiningFractions;
private float retreatingJoinX1;
private float retreatingJoinX2;
private float[] dotRevealFractions;
private boolean attachedState;
// drawing
private final Paint unselectedPaint;
private final Paint selectedPaint;
private final Path combinedUnselectedPath;
private final Path unselectedDotPath;
private final Path unselectedDotLeftPath;
private final Path unselectedDotRightPath;
private final RectF rectF;
// animation
private ValueAnimator moveAnimation;
private ValueAnimator[] joiningAnimations;
private AnimatorSet joiningAnimationSet;
private PendingRetreatAnimator retreatAnimation;
private PendingRevealAnimator[] revealAnimations;
private final Interpolator interpolator;
// working values for beziers
float endX1;
float endY1;
float endX2;
float endY2;
float controlX1;
float controlY1;
float controlX2;
float controlY2;
public DotsPageIndicator(Context context) {
this(context, null, 0);
}
public DotsPageIndicator(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DotsPageIndicator(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final int scaledDensity = (int) context.getResources().getDisplayMetrics().scaledDensity;
// Load attributes
final TypedArray typedArray = getContext().obtainStyledAttributes(
attrs, R.styleable.DotsPageIndicator, defStyle, 0);
dotDiameter = typedArray.getDimensionPixelSize(R.styleable.DotsPageIndicator_dotDiameter,
DEFAULT_DOT_SIZE * scaledDensity);
dotRadius = dotDiameter / 2;
halfDotRadius = dotRadius / 2;
gap = typedArray.getDimensionPixelSize(R.styleable.DotsPageIndicator_dotGap,
DEFAULT_GAP * scaledDensity);
animDuration = (long) typedArray.getInteger(R.styleable.DotsPageIndicator_animationDuration,
DEFAULT_ANIM_DURATION);
animHalfDuration = animDuration / 2;
unselectedColour = typedArray.getColor(R.styleable.DotsPageIndicator_pageIndicatorColor,
DEFAULT_UNSELECTED_COLOUR);
selectedColour = typedArray.getColor(R.styleable.DotsPageIndicator_currentPageIndicatorColor,
DEFAULT_SELECTED_COLOUR);
typedArray.recycle();
unselectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
unselectedPaint.setColor(unselectedColour);
selectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
selectedPaint.setColor(selectedColour);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
interpolator = loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
} else {
interpolator = loadInterpolator(context, android.R.anim.accelerate_decelerate_interpolator);
}
// create paths & rect now reuse & rewind later
combinedUnselectedPath = new Path();
unselectedDotPath = new Path();
unselectedDotLeftPath = new Path();
unselectedDotRightPath = new Path();
rectF = new RectF();
addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
attachedState = true;
}
@Override
public void onViewDetachedFromWindow(View v) {
attachedState = false;
}
});
}
public void setViewPager(ViewPager viewPager) {
this.viewPager = viewPager;
viewPager.setOnPageChangeListener(this);
setPageCount(viewPager.getAdapter().getCount());
viewPager.getAdapter().registerDataSetObserver(new DataSetObserver() {
@Override
public void onChanged() {
setPageCount(DotsPageIndicator.this.viewPager.getAdapter().getCount());
}
});
setCurrentPageImmediate();
}
/***
* As this class <b>must</b> act as the {@link ViewPager.OnPageChangeListener} for the ViewPager
* (as set by {@link #setViewPager(android.support.v4.view.ViewPager)}). Applications may set a
* listener here to be notified of the ViewPager events.
*
* @param onPageChangeListener
*/
public void setOnPageChangeListener(ViewPager.OnPageChangeListener onPageChangeListener) {
pageChangeListener = onPageChangeListener;
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// nothing to do just forward onward to any registered listener
if (pageChangeListener != null) {
pageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
}
@Override
public void onPageSelected(int position) {
if (attachedState) {
// this is the main event we're interested in!
setSelectedPage(position);
} else {
// when not attached, don't animate the move, just store immediately
setCurrentPageImmediate();
}
// forward onward to any registered listener
if (pageChangeListener != null) {
pageChangeListener.onPageSelected(position);
}
}
@Override
public void onPageScrollStateChanged(int state) {
// nothing to do just forward onward to any registered listener
if (pageChangeListener != null) {
pageChangeListener.onPageScrollStateChanged(state);
}
}
private void setPageCount(int pages) {
pageCount = pages;
calculateDotPositions();
resetState();
}
private void calculateDotPositions() {
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getWidth() - getPaddingRight();
int requiredWidth = getRequiredWidth();
float startLeft = left + ((right - left - requiredWidth) / 2) + dotRadius;
dotCenterX = new float[pageCount];
for (int i = 0; i < pageCount; i++) {
dotCenterX[i] = startLeft + i * (dotDiameter + gap);
}
// todo just top aligning for now… should make this smarter
dotTopY = top;
dotCenterY = top + dotRadius;
dotBottomY = top + dotDiameter;
setCurrentPageImmediate();
}
private void setCurrentPageImmediate() {
if (viewPager != null) {
currentPage = viewPager.getCurrentItem();
} else {
currentPage = 0;
}
if (pageCount > 0) {
selectedDotX = dotCenterX[currentPage];
}
}
private void resetState() {
if (pageCount > 0) {
joiningFractions = new float[pageCount - 1];
Arrays.fill(joiningFractions, 0f);
dotRevealFractions = new float[pageCount];
Arrays.fill(dotRevealFractions, 0f);
retreatingJoinX1 = INVALID_FRACTION;
retreatingJoinX2 = INVALID_FRACTION;
selectedDotInPosition = true;
}
}
@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;
default: // MeasureSpec.UNSPECIFIED
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;
default: // MeasureSpec.UNSPECIFIED
width = desiredWidth;
break;
}
setMeasuredDimension(width, height);
calculateDotPositions();
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
setMeasuredDimension(width, height);
calculateDotPositions();
}
@Override
public void clearAnimation() {
super.clearAnimation();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
cancelRunningAnimations();
}
}
private int getDesiredHeight() {
return getPaddingTop() + dotDiameter + getPaddingBottom();
}
private int getRequiredWidth() {
return pageCount * dotDiameter + (pageCount - 1) * gap;
}
private int getDesiredWidth() {
return getPaddingLeft() + getRequiredWidth() + getPaddingRight();
}
@Override
protected void onDraw(Canvas canvas) {
if (viewPager == null || pageCount == 0) {
return;
}
drawUnselected(canvas);
drawSelected(canvas);
}
private void drawUnselected(Canvas canvas) {
combinedUnselectedPath.rewind();
// draw any settled, revealing or joining dots
for (int page = 0; page < pageCount; page++) {
int nextXIndex = page == pageCount - 1 ? page : page + 1;
// todo Path.op should be supported in KitKat but causes the app to hang for Nexus 5.
// For now disabling for all pre-L devices.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Path unselectedPath = getUnselectedPath(page,
dotCenterX[page],
dotCenterX[nextXIndex],
page == pageCount - 1 ? INVALID_FRACTION : joiningFractions[page],
dotRevealFractions[page]);
combinedUnselectedPath.op(unselectedPath, Path.Op.UNION);
} else {
canvas.drawCircle(dotCenterX[page], dotCenterY, dotRadius, unselectedPaint);
}
}
// draw any retreating joins
if (retreatingJoinX1 != INVALID_FRACTION) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
combinedUnselectedPath.op(getRetreatingJoinPath(), Path.Op.UNION);
}
}
canvas.drawPath(combinedUnselectedPath, unselectedPaint);
}
/**
* Unselected dots can be in 6 states:
*
* #1 At rest
* #2 Joining neighbour, still separate
* #3 Joining neighbour, combined curved
* #4 Joining neighbour, combined straight
* #5 Join retreating
* #6 Dot re-showing / revealing
*
* It can also be in a combination of these states e.g. joining one neighbour while
* retreating from another. We therefore create a Path so that we can examine each
* dot pair separately and later take the union for these cases.
*
* This function returns a path for the given dot **and any action to it's right** e.g. joining
* or retreating from it's neighbour
*
* @param page
*/
private Path getUnselectedPath(int page,
float centerX,
float nextCenterX,
float joiningFraction,
float dotRevealFraction) {
unselectedDotPath.rewind();
if ((joiningFraction == 0f || joiningFraction == INVALID_FRACTION)
&& dotRevealFraction == 0f
&& !(page == currentPage && selectedDotInPosition == true)) {
// case #1 At rest
unselectedDotPath.addCircle(dotCenterX[page], dotCenterY, dotRadius, Path.Direction.CW);
}
if (joiningFraction > 0f && joiningFraction < 0.5f && retreatingJoinX1 == INVALID_FRACTION) {
// case #2 Joining neighbour, still separate
// start with the left dot
unselectedDotLeftPath.rewind();
// start at the bottom center
unselectedDotLeftPath.moveTo(centerX, dotBottomY);
// semi circle to the top center
rectF.set(centerX - dotRadius, dotTopY, centerX + dotRadius, dotBottomY);
unselectedDotLeftPath.arcTo(rectF, 90, 180, true);
// cubic to the right middle
endX1 = centerX + dotRadius + (joiningFraction * gap);
endY1 = dotCenterY;
controlX1 = centerX + halfDotRadius;
controlY1 = dotTopY;
controlX2 = endX1;
controlY2 = endY1 - halfDotRadius;
unselectedDotLeftPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX1, endY1);
// cubic back to the bottom center
endX2 = centerX;
endY2 = dotBottomY;
controlX1 = endX1;
controlY1 = endY1 + halfDotRadius;
controlX2 = centerX + halfDotRadius;
controlY2 = dotBottomY;
unselectedDotLeftPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX2, endY2);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
unselectedDotPath.op(unselectedDotLeftPath, Path.Op.UNION);
}
// now do the next dot to the right
unselectedDotRightPath.rewind();
// start at the bottom center
unselectedDotRightPath.moveTo(nextCenterX, dotBottomY);
// semi circle to the top center
rectF.set(nextCenterX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY);
unselectedDotRightPath.arcTo(rectF, 90, -180, true);
// cubic to the left middle
endX1 = nextCenterX - dotRadius - (joiningFraction * gap);
endY1 = dotCenterY;
controlX1 = nextCenterX - halfDotRadius;
controlY1 = dotTopY;
controlX2 = endX1;
controlY2 = endY1 - halfDotRadius;
unselectedDotRightPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX1, endY1);
// cubic back to the bottom center
endX2 = nextCenterX;
endY2 = dotBottomY;
controlX1 = endX1;
controlY1 = endY1 + halfDotRadius;
controlX2 = endX2 - halfDotRadius;
controlY2 = dotBottomY;
unselectedDotRightPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX2, endY2);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
unselectedDotPath.op(unselectedDotRightPath, Path.Op.UNION);
}
}
if (joiningFraction > 0.5f && joiningFraction < 1f && retreatingJoinX1 == INVALID_FRACTION) {
// case #3 Joining neighbour, combined curved
// start in the bottom left
unselectedDotPath.moveTo(centerX, dotBottomY);
// semi-circle to the top left
rectF.set(centerX - dotRadius, dotTopY, centerX + dotRadius, dotBottomY);
unselectedDotPath.arcTo(rectF, 90, 180, true);
// bezier to the middle top of the join
endX1 = centerX + dotRadius + (gap / 2);
endY1 = dotCenterY - (joiningFraction * dotRadius);
controlX1 = endX1 - (joiningFraction * dotRadius);
controlY1 = dotTopY;
controlX2 = endX1 - ((1 - joiningFraction) * dotRadius);
controlY2 = endY1;
unselectedDotPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX1, endY1);
// bezier to the top right of the join
endX2 = nextCenterX;
endY2 = dotTopY;
controlX1 = endX1 + ((1 - joiningFraction) * dotRadius);
controlY1 = endY1;
controlX2 = endX1 + (joiningFraction * dotRadius);
controlY2 = dotTopY;
unselectedDotPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX2, endY2);
// semi-circle to the bottom right
rectF.set(nextCenterX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY);
unselectedDotPath.arcTo(rectF, 270, 180, true);
// bezier to the middle bottom of the join
// endX1 stays the same
endY1 = dotCenterY + (joiningFraction * dotRadius);
controlX1 = endX1 + (joiningFraction * dotRadius);
controlY1 = dotBottomY;
controlX2 = endX1 + ((1 - joiningFraction) * dotRadius);
controlY2 = endY1;
unselectedDotPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX1, endY1);
// bezier back to the start point in the bottom left
endX2 = centerX;
endY2 = dotBottomY;
controlX1 = endX1 - ((1 - joiningFraction) * dotRadius);
controlY1 = endY1;
controlX2 = endX1 - (joiningFraction * dotRadius);
controlY2 = endY2;
unselectedDotPath.cubicTo(controlX1, controlY1, controlX2, controlY2, endX2, endY2);
}
if (joiningFraction == 1 && retreatingJoinX1 == INVALID_FRACTION) {
// case #4 Joining neighbour, combined straight
// technically we could use case 3 for this situation as well
// but assume that this is an optimization rather than faffing around with beziers
// just to draw a rounded rect
rectF.set(centerX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY);
unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius, Path.Direction.CW);
}
// case #5 is handled by #getRetreatingJoinPath()
// this is done separately so that we can have a single retreating path spanning
// multiple dots and therefore animate it's movement smoothly
if (dotRevealFraction > MINIMAL_REVEAL) {
// case #6 previously hidden dot revealing
unselectedDotPath.addCircle(centerX, dotCenterY, dotRevealFraction * dotRadius,
Path.Direction.CW);
}
return unselectedDotPath;
}
private Path getRetreatingJoinPath() {
unselectedDotPath.rewind();
rectF.set(retreatingJoinX1, dotTopY, retreatingJoinX2, dotBottomY);
unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius, Path.Direction.CW);
return unselectedDotPath;
}
private void drawSelected(Canvas canvas) {
canvas.drawCircle(selectedDotX, dotCenterY, dotRadius, selectedPaint);
}
private void setSelectedPage(int now) {
if (now == currentPage || pageCount == 0) {
return;
}
int was = currentPage;
currentPage = now;
// These animations are not supported in pre-JB versions.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
cancelRunningAnimations();
// create the anim to move the selected dot this animator will kick off
// retreat animations when it has moved 75% of the way.
// The retreat animation in turn will kick of reveal anims when the
// retreat has passed any dots to be revealed
final int steps = Math.abs(now - was);
moveAnimation = createMoveSelectedAnimator(dotCenterX[now], was, now, steps);
// create animators for joining the dots. This runs independently of the above and relies
// on good timing. Like comedy.
// if joining multiple dots, each dot after the first is delayed by 1/8 of the duration
joiningAnimations = new ValueAnimator[steps];
for (int i = 0; i < steps; i++) {
joiningAnimations[i] = createJoiningAnimator(now > was ? was + i : was - 1 - i,
i * (animDuration / 8L));
}
moveAnimation.start();
startJoiningAnimations();
} else {
setCurrentPageImmediate();
invalidate();
}
}
private ValueAnimator createMoveSelectedAnimator(final float moveTo, int was, int now,
int steps) {
// create the actual move animator
ValueAnimator moveSelected = ValueAnimator.ofFloat(selectedDotX, moveTo);
// also set up a pending retreat anim this starts when the move is 75% complete
retreatAnimation = new PendingRetreatAnimator(was, now, steps,
now > was
? new RightwardStartPredicate(moveTo - ((moveTo - selectedDotX) * 0.25f))
: new LeftwardStartPredicate(moveTo + ((selectedDotX - moveTo) * 0.25f)));
moveSelected.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// todo avoid autoboxing
selectedDotX = (Float) valueAnimator.getAnimatedValue();
retreatAnimation.startIfNecessary(selectedDotX);
postInvalidateOnAnimation();
}
});
moveSelected.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
// set a flag so that we continue to draw the unselected dot in the target position
// until the selected dot has finished moving into place
selectedDotInPosition = false;
}
@Override
public void onAnimationEnd(Animator animation) {
// set a flag when anim finishes so that we don't draw both selected & unselected
// page dots
selectedDotInPosition = true;
}
});
// slightly delay the start to give the joins a chance to run
// unless dot isn't in position yet then don't delay!
moveSelected.setStartDelay(selectedDotInPosition ? animDuration / 4L : 0L);
moveSelected.setDuration(animDuration * 3L / 4L);
moveSelected.setInterpolator(interpolator);
return moveSelected;
}
private ValueAnimator createJoiningAnimator(final int leftJoiningDot, final long startDelay) {
// animate the joining fraction for the given dot
ValueAnimator joining = ValueAnimator.ofFloat(0f, 1.0f);
joining.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
setJoiningFraction(leftJoiningDot, valueAnimator.getAnimatedFraction());
}
});
joining.setDuration(animHalfDuration);
joining.setStartDelay(startDelay);
joining.setInterpolator(interpolator);
return joining;
}
private void setJoiningFraction(int leftDot, float fraction) {
joiningFractions[leftDot] = fraction;
postInvalidateOnAnimation();
}
private void clearJoiningFractions() {
Arrays.fill(joiningFractions, 0f);
postInvalidateOnAnimation();
}
private void setDotRevealFraction(int dot, float fraction) {
dotRevealFractions[dot] = fraction;
postInvalidateOnAnimation();
}
private void cancelRunningAnimations() {
cancelMoveAnimation();
cancelJoiningAnimations();
cancelRetreatAnimation();
cancelRevealAnimations();
resetState();
}
private void cancelMoveAnimation() {
if (moveAnimation != null && moveAnimation.isRunning()) {
moveAnimation.cancel();
}
}
private void startJoiningAnimations() {
joiningAnimationSet = new AnimatorSet();
joiningAnimationSet.playTogether(joiningAnimations);
joiningAnimationSet.start();
}
private void cancelJoiningAnimations() {
if (joiningAnimationSet != null && joiningAnimationSet.isRunning()) {
joiningAnimationSet.cancel();
}
}
private void cancelRetreatAnimation() {
if (retreatAnimation != null && retreatAnimation.isRunning()) {
retreatAnimation.cancel();
}
}
private void cancelRevealAnimations() {
if (revealAnimations != null) {
for (PendingRevealAnimator reveal : revealAnimations) {
reveal.cancel();
}
}
}
int getUnselectedColour() {
return unselectedColour;
}
int getSelectedColour() {
return selectedColour;
}
float getDotCenterY() {
return dotCenterY;
}
float getDotCenterX(int page) {
return dotCenterX[page];
}
float getSelectedDotX() {
return selectedDotX;
}
int getCurrentPage() {
return currentPage;
}
/**
* A {@link android.animation.ValueAnimator} that starts once a given predicate returns true.
*/
public abstract class PendingStartAnimator extends ValueAnimator {
protected boolean hasStarted;
protected StartPredicate predicate;
public PendingStartAnimator(StartPredicate predicate) {
super();
this.predicate = predicate;
hasStarted = false;
}
public void startIfNecessary(float currentValue) {
if (!hasStarted && predicate.shouldStart(currentValue)) {
start();
hasStarted = true;
}
}
}
/**
* An Animator that shows and then shrinks a retreating join between the previous and newly
* selected pages. This also sets up some pending dot reveals to be started when the retreat
* has passed the dot to be revealed.
*/
public class PendingRetreatAnimator extends PendingStartAnimator {
public PendingRetreatAnimator(int was, int now, int steps, StartPredicate predicate) {
super(predicate);
setDuration(animHalfDuration);
setInterpolator(interpolator);
// work out the start/end values of the retreating join from the direction we're
// travelling in. Also look at the current selected dot position, i.e. we're moving on
// before a prior anim has finished.
final float initialX1 = now > was ? Math.min(dotCenterX[was], selectedDotX) - dotRadius
: dotCenterX[now] - dotRadius;
final float finalX1 = now > was ? dotCenterX[now] - dotRadius
: dotCenterX[now] - dotRadius;
final float initialX2 = now > was ? dotCenterX[now] + dotRadius
: Math.max(dotCenterX[was], selectedDotX) + dotRadius;
final float finalX2 = now > was ? dotCenterX[now] + dotRadius
: dotCenterX[now] + dotRadius;
revealAnimations = new PendingRevealAnimator[steps];
// hold on to the indexes of the dots that will be hidden by the retreat so that
// we can initialize their revealFraction's i.e. make sure they're hidden while the
// reveal animation runs
final int[] dotsToHide = new int[steps];
if (initialX1 != finalX1) { // rightward retreat
setFloatValues(initialX1, finalX1);
// create the reveal animations that will run when the retreat passes them
for (int i = 0; i < steps; i++) {
revealAnimations[i] = new PendingRevealAnimator(was + i,
new RightwardStartPredicate(dotCenterX[was + i]));
dotsToHide[i] = was + i;
}
addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// todo avoid autoboxing
retreatingJoinX1 = (Float) valueAnimator.getAnimatedValue();
postInvalidateOnAnimation();
// start any reveal animations if we've passed them
for (PendingRevealAnimator pendingReveal : revealAnimations) {
pendingReveal.startIfNecessary(retreatingJoinX1);
}
}
});
} else { // (initialX2 != finalX2) leftward retreat
setFloatValues(initialX2, finalX2);
// create the reveal animations that will run when the retreat passes them
for (int i = 0; i < steps; i++) {
revealAnimations[i] = new PendingRevealAnimator(was - i,
new LeftwardStartPredicate(dotCenterX[was - i]));
dotsToHide[i] = was - i;
}
addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// todo avoid autoboxing
retreatingJoinX2 = (Float) valueAnimator.getAnimatedValue();
postInvalidateOnAnimation();
// start any reveal animations if we've passed them
for (PendingRevealAnimator pendingReveal : revealAnimations) {
pendingReveal.startIfNecessary(retreatingJoinX2);
}
}
});
}
addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
cancelJoiningAnimations();
clearJoiningFractions();
// we need to set this so that the dots are hidden until the reveal anim runs
for (int dot : dotsToHide) {
setDotRevealFraction(dot, MINIMAL_REVEAL);
}
retreatingJoinX1 = initialX1;
retreatingJoinX2 = initialX2;
postInvalidateOnAnimation();
}
@Override
public void onAnimationEnd(Animator animation) {
retreatingJoinX1 = INVALID_FRACTION;
retreatingJoinX2 = INVALID_FRACTION;
postInvalidateOnAnimation();
}
});
}
}
/**
* An Animator that animates a given dot's revealFraction i.e. scales it up
*/
public class PendingRevealAnimator extends PendingStartAnimator {
private final int dot;
public PendingRevealAnimator(int dot, StartPredicate predicate) {
super(predicate);
this.dot = dot;
setFloatValues(MINIMAL_REVEAL, 1f);
setDuration(animHalfDuration);
setInterpolator(interpolator);
addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// todo avoid autoboxing
setDotRevealFraction(PendingRevealAnimator.this.dot,
(Float) valueAnimator.getAnimatedValue());
}
});
addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
setDotRevealFraction(PendingRevealAnimator.this.dot, 0f);
postInvalidateOnAnimation();
}
});
}
}
/**
* A predicate used to start an animation when a test passes
*/
public abstract class StartPredicate {
protected float thresholdValue;
public StartPredicate(float thresholdValue) {
this.thresholdValue = thresholdValue;
}
abstract boolean shouldStart(float currentValue);
}
/**
* A predicate used to start an animation when a given value is greater than a threshold
*/
public class RightwardStartPredicate extends StartPredicate {
public RightwardStartPredicate(float thresholdValue) {
super(thresholdValue);
}
boolean shouldStart(float currentValue) {
return currentValue > thresholdValue;
}
}
/**
* A predicate used to start an animation then a given value is less than a threshold
*/
public class LeftwardStartPredicate extends StartPredicate {
public LeftwardStartPredicate(float thresholdValue) {
super(thresholdValue);
}
boolean shouldStart(float currentValue) {
return currentValue < thresholdValue;
}
}
}