@@ -0,0 +1,918 @@
/*
* 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 ;
}
}
}