diff options
Diffstat (limited to 'core/src/main/kotlin/ca/allanwang/kau/widgets/InkPageIndicator.java')
-rw-r--r-- | core/src/main/kotlin/ca/allanwang/kau/widgets/InkPageIndicator.java | 859 |
1 files changed, 0 insertions, 859 deletions
diff --git a/core/src/main/kotlin/ca/allanwang/kau/widgets/InkPageIndicator.java b/core/src/main/kotlin/ca/allanwang/kau/widgets/InkPageIndicator.java deleted file mode 100644 index 78e915d..0000000 --- a/core/src/main/kotlin/ca/allanwang/kau/widgets/InkPageIndicator.java +++ /dev/null @@ -1,859 +0,0 @@ -/* - * Copyright 2015 Google Inc. - * - * 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 ca.allanwang.kau.widgets; - -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.support.annotation.ColorInt; -import android.support.v4.view.ViewPager; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.view.animation.Interpolator; - -import java.util.Arrays; - -import ca.allanwang.kau.R; -import ca.allanwang.kau.utils.AnimHolder; -import ca.allanwang.kau.utils.ColorUtilsKt; - -/** - * An ink inspired widget for indicating pages in a {@link ViewPager}. - */ -public class InkPageIndicator extends View implements ViewPager.OnPageChangeListener, - View.OnAttachStateChangeListener { - - // 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; - - public void setColour(@ColorInt int color) { - selectedColour = color; - unselectedColour = ColorUtilsKt.adjustAlpha(color, 0.5f); - selectedPaint.setColor(selectedColour); - unselectedPaint.setColor(unselectedColour); - } - - // 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; - - // state - private int pageCount; - private int currentPage; - private int previousPage; - private float selectedDotX; - private boolean selectedDotInPosition; - private float[] dotCenterX; - private float[] joiningFractions; - private float retreatingJoinX1; - private float retreatingJoinX2; - private float[] dotRevealFractions; - private boolean isAttachedToWindow; - private boolean pageChanging; - - // 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 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 InkPageIndicator(Context context) { - this(context, null, 0); - } - - public InkPageIndicator(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public InkPageIndicator(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - final int density = (int) context.getResources().getDisplayMetrics().density; - - // Load attributes - final TypedArray a = getContext().obtainStyledAttributes( - attrs, R.styleable.InkPageIndicator, defStyle, 0); - - dotDiameter = a.getDimensionPixelSize(R.styleable.InkPageIndicator_dotDiameter, - DEFAULT_DOT_SIZE * density); - dotRadius = dotDiameter / 2; - halfDotRadius = dotRadius / 2; - gap = a.getDimensionPixelSize(R.styleable.InkPageIndicator_dotGap, - DEFAULT_GAP * density); - animDuration = (long) a.getInteger(R.styleable.InkPageIndicator_animationDuration, - DEFAULT_ANIM_DURATION); - animHalfDuration = animDuration / 2; - unselectedColour = a.getColor(R.styleable.InkPageIndicator_pageIndicatorColor, - DEFAULT_UNSELECTED_COLOUR); - selectedColour = a.getColor(R.styleable.InkPageIndicator_currentPageIndicatorColor, - DEFAULT_SELECTED_COLOUR); - - a.recycle(); - - unselectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - unselectedPaint.setColor(unselectedColour); - selectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - selectedPaint.setColor(selectedColour); - interpolator = AnimHolder.INSTANCE.getFastOutSlowInInterpolator().invoke(context); - - // create paths & rect now – reuse & rewind later - combinedUnselectedPath = new Path(); - unselectedDotPath = new Path(); - unselectedDotLeftPath = new Path(); - unselectedDotRightPath = new Path(); - rectF = new RectF(); - - addOnAttachStateChangeListener(this); - } - - public void setViewPager(ViewPager viewPager) { - this.viewPager = viewPager; - viewPager.addOnPageChangeListener(this); - setPageCount(viewPager.getAdapter().getCount()); - viewPager.getAdapter().registerDataSetObserver(new DataSetObserver() { - @Override - public void onChanged() { - setPageCount(InkPageIndicator.this.viewPager.getAdapter().getCount()); - } - }); - setCurrentPageImmediate(); - } - - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - if (isAttachedToWindow) { - float fraction = positionOffset; - int currentPosition = pageChanging ? previousPage : currentPage; - int leftDotPosition = position; - // when swiping from #2 to #1 ViewPager reports position as 1 and a descending offset - // need to convert this into our left-dot-based 'coordinate space' - if (currentPosition != position) { - fraction = 1f - positionOffset; - - // if user scrolls completely to next page then the position param updates to that - // new page but we're not ready to switch our 'current' page yet so adjust for that - if (fraction == 1f) { - leftDotPosition = Math.min(currentPosition, position); - } - } - setJoiningFraction(leftDotPosition, fraction); - } - } - - @Override - public void onPageSelected(int position) { - if (isAttachedToWindow) { - // this is the main event we're interested in! - setSelectedPage(position); - } else { - // when not attached, don't animate the move, just store immediately - setCurrentPageImmediate(); - } - } - - @Override - public void onPageScrollStateChanged(int state) { - // nothing to do - } - - private void setPageCount(int pages) { - pageCount = pages; - resetState(); - requestLayout(); - } - - private void calculateDotPositions(int width, int height) { - int left = getPaddingLeft(); - int top = getPaddingTop(); - int right = width - getPaddingRight(); - int bottom = height - getPaddingBottom(); - - 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 (dotCenterX != null) { - selectedDotX = dotCenterX[currentPage]; - } - } - - private void resetState() { - 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; - 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); - calculateDotPositions(width, height); - } - - private int getDesiredHeight() { - return getPaddingTop() + dotDiameter + getPaddingBottom(); - } - - private int getRequiredWidth() { - return pageCount * dotDiameter + (pageCount - 1) * gap; - } - - private int getDesiredWidth() { - return getPaddingLeft() + getRequiredWidth() + getPaddingRight(); - } - - @Override - public void onViewAttachedToWindow(View view) { - isAttachedToWindow = true; - } - - @Override - public void onViewDetachedFromWindow(View view) { - isAttachedToWindow = false; - } - - @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; - combinedUnselectedPath.op(getUnselectedPath(page, - dotCenterX[page], - dotCenterX[nextXIndex], - page == pageCount - 1 ? INVALID_FRACTION : joiningFractions[page], - dotRevealFractions[page]), Path.Op.UNION); - } - // draw any retreating joins - if (retreatingJoinX1 != INVALID_FRACTION) { - combinedUnselectedPath.op(getRetreatingJoinPath(), Path.Op.UNION); - } - canvas.drawPath(combinedUnselectedPath, unselectedPaint); - } - - /** - * Unselected dots can be in 6 states: - * <p> - * #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 - * <p> - * 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. - * <p> - * 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 - * @return - */ - 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); - - 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); - unselectedDotPath.op(unselectedDotRightPath, Path.Op.UNION); - } - - if (joiningFraction > 0.5f && joiningFraction < 1f - && retreatingJoinX1 == INVALID_FRACTION) { - - // case #3 – Joining neighbour, combined curved - - // adjust the fraction so that it goes from 0.3 -> 1 to produce a more realistic 'join' - float adjustedFraction = (joiningFraction - 0.2f) * 1.25f; - - // 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 - (adjustedFraction * dotRadius); - controlX1 = endX1 - (adjustedFraction * dotRadius); - controlY1 = dotTopY; - controlX2 = endX1 - ((1 - adjustedFraction) * 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 - adjustedFraction) * dotRadius); - controlY1 = endY1; - controlX2 = endX1 + (adjustedFraction * 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 + (adjustedFraction * dotRadius); - controlX1 = endX1 + (adjustedFraction * dotRadius); - controlY1 = dotBottomY; - controlX2 = endX1 + ((1 - adjustedFraction) * 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 - adjustedFraction) * dotRadius); - controlY1 = endY1; - controlX2 = endX1 - (adjustedFraction * 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) return; - - pageChanging = true; - previousPage = currentPage; - currentPage = now; - final int steps = Math.abs(now - previousPage); - - if (steps > 1) { - if (now > previousPage) { - for (int i = 0; i < steps; i++) { - setJoiningFraction(previousPage + i, 1f); - } - } else { - for (int i = -1; i > -steps; i--) { - setJoiningFraction(previousPage + i, 1f); - } - } - } - - // 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 - moveAnimation = createMoveSelectedAnimator(dotCenterX[now], previousPage, now, steps); - moveAnimation.start(); - } - - 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))); - retreatAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - resetState(); - pageChanging = false; - } - }); - 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 void setJoiningFraction(int leftDot, float fraction) { - if (leftDot < joiningFractions.length) { - - if (leftDot == 1) { - Log.d("PageIndicator", "dot 1 fraction:\t" + 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 cancelJoiningAnimations() { - if (joiningAnimationSet != null && joiningAnimationSet.isRunning()) { - joiningAnimationSet.cancel(); - } - } - - /** - * A {@link 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 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 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 int dot; - - public PendingRevealAnimator(int dot, StartPredicate predicate) { - super(predicate); - setFloatValues(MINIMAL_REVEAL, 1f); - this.dot = dot; - 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; - } - } -} |