diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1c7bfff8b..db44ea235 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -87,5 +87,4 @@ play { dependencies { implementation(Libs.squidb) - implementation(Libs.ink_page_indicator) } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/intro/CustomPageIndicator.kt b/app/src/main/java/org/kiwix/kiwixmobile/intro/CustomPageIndicator.kt new file mode 100644 index 000000000..f178e70bc --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/intro/CustomPageIndicator.kt @@ -0,0 +1,812 @@ +/* + * Kiwix Android + * Copyright (c) 2022 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package org.kiwix.kiwixmobile.intro + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.database.DataSetObserver +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import android.graphics.RectF +import android.os.Parcel +import android.os.Parcelable +import android.util.AttributeSet +import android.view.View +import android.view.animation.Interpolator +import androidx.core.view.ViewCompat +import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import androidx.viewpager.widget.ViewPager +import androidx.viewpager.widget.ViewPager.OnPageChangeListener +import org.kiwix.kiwixmobile.R +import java.util.Arrays +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** + * Author DavidPacioianu + * Reference From + * https://github.com/DavidPacioianu/InkPageIndicator + * We refactor this java file to kotlin file + */ + +class CustomPageIndicator @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : View(context, attrs, defStyle), OnPageChangeListener, View.OnAttachStateChangeListener { + // configurable attributes + private val dotDiameter: Int + private val gap: Int + private val animDuration: Long + private val unselectedColour: Int + private val selectedColour: Int + + // derived from attributes + private val dotRadius: Float + private val halfDotRadius: Float + private val animHalfDuration: Long + private var dotTopY = 0f + private var dotCenterY = 0f + private var dotBottomY = 0f + + // ViewPager + private var viewPager: ViewPager? = null + + // state + private var pageCount = 0 + private var currentPage = 0 + private var previousPage = 0 + private var selectedDotX = 0f + private var selectedDotInPosition = false + private var dotCenterX: FloatArray? = null + private lateinit var joiningFractions: FloatArray + private var retreatingJoinX1 = 0f + private var retreatingJoinX2 = 0f + private lateinit var dotRevealFractions: FloatArray + private var attachedToWindow = false + private var pageChanging = false + + // drawing + private val unselectedPaint: Paint + private val selectedPaint: Paint + private val combinedUnselectedPath: Path + private val unselectedDotPath: Path + private val unselectedDotLeftPath: Path + private val unselectedDotRightPath: Path + private val rectF: RectF + + // animation + private var moveAnimation: ValueAnimator? = null + private val joiningAnimationSet: AnimatorSet? = null + private var retreatAnimation: PendingRetreatAnimator? = null + private lateinit var revealAnimations: Array + private val interpolator: Interpolator + + // working values for beziers + private var endX1 = 0f + private var endY1 = 0f + private var endX2 = 0f + private var endY2 = 0f + private var controlX1 = 0f + private var controlY1 = 0f + private var controlX2 = 0f + private var controlY2 = 0f + fun setViewPager(viewPager: ViewPager) { + this.viewPager = viewPager + viewPager.addOnPageChangeListener(this) + setPageCount(viewPager.adapter!!.count) + viewPager.adapter!!.registerDataSetObserver(object : DataSetObserver() { + override fun onChanged() { + setPageCount(this@CustomPageIndicator.viewPager!!.adapter!!.count) + } + }) + setCurrentPageImmediate() + } + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + if (attachedToWindow) { + var fraction = positionOffset + val currentPosition = if (pageChanging) previousPage else currentPage + var 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 = min(currentPosition, position) + } + } + setJoiningFraction(leftDotPosition, fraction) + } + } + + override fun onPageSelected(position: Int) { + if (attachedToWindow) { + // 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 fun onPageScrollStateChanged(state: Int) { + // nothing to do + } + + private fun setPageCount(pages: Int) { + pageCount = pages + resetState() + requestLayout() + } + + private fun calculateDotPositions(width: Int, height: Int) { + val left = paddingLeft + val top = paddingTop + val right = width - paddingRight + val bottom = height - paddingBottom + val requiredWidth = getRequiredWidth() + val startLeft = left + (right - left - requiredWidth) / 2 + dotRadius + dotCenterX = FloatArray(pageCount) + for (i in 0 until pageCount) { + dotCenterX!![i] = startLeft + i * (dotDiameter + gap) + } + // todo just top aligning for now… should make this smarter + dotTopY = top.toFloat() + dotCenterY = top + dotRadius + dotBottomY = (top + dotDiameter).toFloat() + setCurrentPageImmediate() + } + + private fun setCurrentPageImmediate() { + currentPage = if (viewPager != null) { + viewPager!!.currentItem + } else { + 0 + } + if (dotCenterX != null && dotCenterX!!.isNotEmpty() + ) { + if (moveAnimation == null || !moveAnimation!!.isStarted) + selectedDotX = dotCenterX!![currentPage] + } + } + + private fun resetState() { + joiningFractions = FloatArray(if (pageCount == 0) 0 else pageCount - 1) + Arrays.fill(joiningFractions, 0f) + dotRevealFractions = FloatArray(pageCount) + Arrays.fill(dotRevealFractions, 0f) + retreatingJoinX1 = INVALID_FRACTION + retreatingJoinX2 = INVALID_FRACTION + selectedDotInPosition = true + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val desiredHeight = getDesiredHeight() + val height: Int = when (MeasureSpec.getMode(heightMeasureSpec)) { + MeasureSpec.EXACTLY -> MeasureSpec.getSize(heightMeasureSpec) + MeasureSpec.AT_MOST -> min(desiredHeight, MeasureSpec.getSize(heightMeasureSpec)) + else -> desiredHeight + } + val desiredWidth = getDesiredWidth() + val width: Int = when (MeasureSpec.getMode(widthMeasureSpec)) { + MeasureSpec.EXACTLY -> MeasureSpec.getSize(widthMeasureSpec) + MeasureSpec.AT_MOST -> min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec)) + else -> desiredWidth + } + setMeasuredDimension(width, height) + calculateDotPositions(width, height) + } + + private fun getDesiredHeight(): Int = + paddingTop + dotDiameter + paddingBottom + + private fun getRequiredWidth(): Int = + pageCount * dotDiameter + (pageCount - 1) * gap + + private fun getDesiredWidth(): Int = + paddingLeft + getRequiredWidth() + paddingRight + + override fun onViewAttachedToWindow(view: View) { + attachedToWindow = true + } + + override fun onViewDetachedFromWindow(view: View) { + attachedToWindow = false + } + + override fun onDraw(canvas: Canvas) { + if (viewPager == null || pageCount == 0) return + drawUnselected(canvas) + drawSelected(canvas) + } + + private fun drawUnselected(canvas: Canvas) { + combinedUnselectedPath.rewind() + + // draw any settled, revealing or joining dots + for (page in 0 until pageCount) { + val nextXIndex = if (page == pageCount - 1) page else page + 1 + val unselectedPath = getUnselectedPath( + page, + dotCenterX!![page], + dotCenterX!![nextXIndex], + if (page == pageCount - 1) INVALID_FRACTION else joiningFractions[page], + dotRevealFractions[page] + ) + unselectedPath.addPath(combinedUnselectedPath) + combinedUnselectedPath.addPath(unselectedPath) + } + // draw any retreating joins + if (retreatingJoinX1 != INVALID_FRACTION) { + val retreatingJoinPath = retreatingJoinPath + combinedUnselectedPath.addPath(retreatingJoinPath) + } + canvas.drawPath(combinedUnselectedPath, unselectedPaint) + } + + private fun getUnselectedPath( + page: Int, + centerX: Float, + nextCenterX: Float, + joiningFraction: Float, + dotRevealFraction: Float + ): Path { + unselectedDotPath.rewind() + if ((joiningFraction == selectedFactor || joiningFraction == INVALID_FRACTION) + ) { + if (dotRevealFraction == selectedFactor && !(page == currentPage && selectedDotInPosition)) { + // case #1 – At rest + unselectedDotPath.addCircle(dotCenterX!![page], dotCenterY, dotRadius, Path.Direction.CW) + } + } + unselectedDotRightPath(centerX, joiningFraction, nextCenterX) + if (joiningFraction > smallUnSelectedFactor && + joiningFraction < unselectedFactor && + retreatingJoinX1 == INVALID_FRACTION + ) { + joinNeighbour(joiningFraction, centerX, nextCenterX) + } + if (joiningFraction == 1f && 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[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 fun joinNeighbour( + joiningFraction: Float, + centerX: Float, + nextCenterX: Float + ) { + // case #3 – Joining neighbour, combined curved + // adjust the fraction so that it goes from 0.3 -> 1 to produce a more realistic 'join' + val adjustedFraction = + (joiningFraction - zeroPointTwoFractionConst) * onePointTwoFiveFractionConst + // start in the bottom left + unselectedDotPath.moveTo(centerX, dotBottomY) + // semi-circle to the top left + rectF[centerX - dotRadius, dotTopY, centerX + dotRadius] = dotBottomY + unselectedDotPath.arcTo(rectF, startAngle, sweepAngle, 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[nextCenterX - dotRadius, dotTopY, nextCenterX + dotRadius] = dotBottomY + unselectedDotPath.arcTo( + rectF, + startAngle + sweepAngle, + sweepAngle, + 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 + ) + } + + private fun unselectedDotRightPath( + centerX: Float, + joiningFraction: Float, + nextCenterX: Float + ) { + if (joiningFraction > selectedFactor && + joiningFraction <= smallUnSelectedFactor && + 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[centerX - dotRadius, dotTopY, centerX + dotRadius] = dotBottomY + unselectedDotLeftPath.arcTo(rectF, startAngle, sweepAngle, 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.addPath(unselectedDotLeftPath) + // 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[nextCenterX - dotRadius, dotTopY, nextCenterX + dotRadius] = dotBottomY + unselectedDotRightPath.arcTo(rectF, startAngle, negativeSweepAngle, 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.addPath(unselectedDotRightPath) + } + } + + private val retreatingJoinPath: Path + get() { + unselectedDotPath.rewind() + rectF[retreatingJoinX1, dotTopY, retreatingJoinX2] = dotBottomY + unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius, Path.Direction.CW) + return unselectedDotPath + } + + private fun drawSelected(canvas: Canvas) { + canvas.drawCircle(selectedDotX, dotCenterY, dotRadius, selectedPaint) + } + + private fun setSelectedPage(now: Int) { + if (now == currentPage || dotCenterX == null || dotCenterX!!.size <= now) return + pageChanging = true + previousPage = currentPage + currentPage = now + val steps = abs(now - previousPage) + if (steps > 1) { + if (now > previousPage) { + for (i in 0 until steps) { + setJoiningFraction(previousPage + i, 1f) + } + } else { + for (i in -1 downTo -steps + 1) { + 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 fun createMoveSelectedAnimator( + moveTo: Float, + was: Int, + now: Int, + steps: Int + ): ValueAnimator { + + // create the actual move animator + val moveSelected = ValueAnimator.ofFloat(selectedDotX, moveTo) + + // also set up a pending retreat anim – this starts when the move is 75% complete + retreatAnimation = PendingRetreatAnimator( + was, now, steps, + if (now > was) + RightwardStartPredicate(moveTo - (moveTo - selectedDotX) * thresholdMultiplier) + else LeftwardStartPredicate( + moveTo + (selectedDotX - moveTo) * thresholdMultiplier + ) + ) + retreatAnimation!!.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + resetState() + pageChanging = false + } + }) + moveSelected.addUpdateListener { valueAnimator -> // todo avoid autoboxing + selectedDotX = valueAnimator.animatedValue as Float + retreatAnimation!!.startIfNecessary(selectedDotX) + ViewCompat.postInvalidateOnAnimation(this@CustomPageIndicator) + } + moveSelected.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + // 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 fun onAnimationEnd(animation: Animator) { + // 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.startDelay = if (selectedDotInPosition) animDuration / fourLong else zeroLong + moveSelected.duration = animDuration * threeLong / fourLong + moveSelected.interpolator = interpolator + return moveSelected + } + + private fun setJoiningFraction(leftDot: Int, fraction: Float) { + if (leftDot < joiningFractions.size) { + joiningFractions[leftDot] = fraction + ViewCompat.postInvalidateOnAnimation(this) + } + } + + private fun clearJoiningFractions() { + Arrays.fill(joiningFractions, 0f) + ViewCompat.postInvalidateOnAnimation(this) + } + + private fun setDotRevealFraction(dot: Int, fraction: Float) { + if (dot < dotRevealFractions.size) { + dotRevealFractions[dot] = fraction + } + ViewCompat.postInvalidateOnAnimation(this) + } + + private fun cancelJoiningAnimations() { + if (joiningAnimationSet != null && joiningAnimationSet.isRunning) { + joiningAnimationSet.cancel() + } + } + + /** + * A [ValueAnimator] that starts once a given predicate returns true. + */ + abstract inner class PendingStartAnimator(private var predicate: StartPredicate) : + ValueAnimator() { + private var hasStarted = false + fun startIfNecessary(currentValue: Float) { + if (!hasStarted && predicate.shouldStart(currentValue)) { + start() + hasStarted = true + } + } + } + + inner class PendingRetreatAnimator(was: Int, now: Int, steps: Int, predicate: StartPredicate) : + PendingStartAnimator(predicate) { + init { + duration = animHalfDuration + interpolator = 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. + val initialX1 = if (now > was) min( + dotCenterX!![was], + selectedDotX + ) - dotRadius else dotCenterX!![now] - dotRadius + val finalX1 = if (now > was) dotCenterX!![now] - dotRadius else dotCenterX!![now] - dotRadius + val initialX2 = if (now > was) dotCenterX!![now] + dotRadius else max( + dotCenterX!![was], selectedDotX + ) + dotRadius + val finalX2 = if (now > was) dotCenterX!![now] + dotRadius else dotCenterX!![now] + dotRadius + revealAnimations = arrayOfNulls(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 + val dotsToHide = IntArray(steps) + if (initialX1 != finalX1) { // rightward retreat + setFloatValues(initialX1, finalX1) + // create the reveal animations that will run when the retreat passes them + for (i in 0 until steps) { + revealAnimations[i] = PendingRevealAnimator( + was + i, + RightwardStartPredicate(dotCenterX!![was + i]) + ) + dotsToHide[i] = was + i + } + addUpdateListener { valueAnimator -> // todo avoid autoboxing + retreatingJoinX1 = valueAnimator.animatedValue as Float + ViewCompat.postInvalidateOnAnimation(this@CustomPageIndicator) + // start any reveal animations if we've passed them + for (pendingReveal in 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 (i in 0 until steps) { + revealAnimations[i] = PendingRevealAnimator( + was - i, + LeftwardStartPredicate(dotCenterX!![was - i]) + ) + dotsToHide[i] = was - i + } + addUpdateListener { valueAnimator -> // todo avoid autoboxing + retreatingJoinX2 = valueAnimator.animatedValue as Float + ViewCompat.postInvalidateOnAnimation(this@CustomPageIndicator) + // start any reveal animations if we've passed them + for (pendingReveal in revealAnimations) { + pendingReveal!!.startIfNecessary(retreatingJoinX2) + } + } + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + cancelJoiningAnimations() + clearJoiningFractions() + // we need to set this so that the dots are hidden until the reveal anim runs + for (dot in dotsToHide) { + setDotRevealFraction(dot, MINIMAL_REVEAL) + } + retreatingJoinX1 = initialX1 + retreatingJoinX2 = initialX2 + ViewCompat.postInvalidateOnAnimation(this@CustomPageIndicator) + } + + override fun onAnimationEnd(animation: Animator) { + retreatingJoinX1 = INVALID_FRACTION + retreatingJoinX2 = INVALID_FRACTION + ViewCompat.postInvalidateOnAnimation(this@CustomPageIndicator) + } + }) + } + } + + /** + * An Animator that animates a given dot's revealFraction i.e. scales it up + */ + inner class PendingRevealAnimator(dot: Int, predicate: StartPredicate) : + PendingStartAnimator(predicate) { + private val dot: Int + + init { + setFloatValues(MINIMAL_REVEAL, 1f) + this.dot = dot + duration = animHalfDuration + interpolator = interpolator + addUpdateListener { valueAnimator -> // todo avoid autoboxing + setDotRevealFraction( + this@PendingRevealAnimator.dot, + valueAnimator.animatedValue as Float + ) + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + setDotRevealFraction(this@PendingRevealAnimator.dot, 0f) + ViewCompat.postInvalidateOnAnimation(this@CustomPageIndicator) + } + }) + } + } + + /** + * A predicate used to start an animation when a test passes + */ + abstract inner class StartPredicate(protected var thresholdValue: Float) { + abstract fun shouldStart(currentValue: Float): Boolean + } + + /** + * A predicate used to start an animation when a given value is greater than a threshold + */ + inner class RightwardStartPredicate(thresholdValue: Float) : StartPredicate(thresholdValue) { + override fun shouldStart(currentValue: Float): Boolean = currentValue > thresholdValue + } + + /** + * A predicate used to start an animation then a given value is less than a threshold + */ + inner class LeftwardStartPredicate(thresholdValue: Float) : StartPredicate(thresholdValue) { + override fun shouldStart(currentValue: Float): Boolean = currentValue < thresholdValue + } + + public override fun onRestoreInstanceState(state: Parcelable) { + val savedState = state as SavedState + super.onRestoreInstanceState(savedState.superState) + currentPage = savedState.currentPage + requestLayout() + } + + public override fun onSaveInstanceState(): Parcelable? { + val superState = super.onSaveInstanceState() + val savedState = SavedState(superState) + savedState.currentPage = currentPage + return savedState + } + + internal class SavedState : BaseSavedState { + var currentPage = 0 + + constructor(superState: Parcelable?) : super(superState) + private constructor(data: Parcel) : super(data) { + currentPage = data.readInt() + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + super.writeToParcel(dest, flags) + dest.writeInt(currentPage) + } + + companion object { + @JvmField val CREATOR: Parcelable.Creator = + object : Parcelable.Creator { + override fun createFromParcel(`in`: Parcel): SavedState? = SavedState(`in`) + + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } + } + + companion object { + // defaults + private const val DEFAULT_DOT_SIZE = 8 // dp + private const val DEFAULT_GAP = 12 // dp + private const val DEFAULT_ANIM_DURATION = 400 // ms + private const val DEFAULT_UNSELECTED_COLOUR = -0x7f000001 // 50% white + private const val DEFAULT_SELECTED_COLOUR = -0x1 // 100% white + + // constants + private const val INVALID_FRACTION = -1f + private const val MINIMAL_REVEAL = 0.00001f + private const val zeroPointTwoFractionConst: Float = 0.2f + private const val onePointTwoFiveFractionConst: Float = 1.25f + private const val unselectedFactor: Float = 1f + private const val smallUnSelectedFactor: Float = 0.5f + private const val selectedFactor: Float = 0f + private const val threeLong: Long = 3L + private const val zeroLong: Long = 0L + private const val fourLong: Long = 4L + private const val thresholdMultiplier: Float = 0.25f + private const val negativeSweepAngle: Float = -180f + private const val sweepAngle: Float = 180f + private const val startAngle: Float = 90f + } + + init { + val density = context.resources.displayMetrics.density.toInt() + + // Load attributes + val a = getContext().obtainStyledAttributes( + attrs, R.styleable.CustomPageIndicator, defStyle, 0 + ) + dotDiameter = a.getDimensionPixelSize( + R.styleable.CustomPageIndicator_ipi_dotDiameter, + DEFAULT_DOT_SIZE * density + ) + dotRadius = (dotDiameter / 2).toFloat() + halfDotRadius = dotRadius / 2 + gap = a.getDimensionPixelSize( + R.styleable.CustomPageIndicator_ipi_dotGap, + DEFAULT_GAP * density + ) + animDuration = a.getInteger( + R.styleable.CustomPageIndicator_ipi_animationDuration, + DEFAULT_ANIM_DURATION + ).toLong() + animHalfDuration = animDuration / 2 + unselectedColour = a.getColor( + R.styleable.CustomPageIndicator_ipi_pageIndicatorColor, + DEFAULT_UNSELECTED_COLOUR + ) + selectedColour = a.getColor( + R.styleable.CustomPageIndicator_ipi_currentPageIndicatorColor, + DEFAULT_SELECTED_COLOUR + ) + a.recycle() + unselectedPaint = Paint(Paint.ANTI_ALIAS_FLAG) + unselectedPaint.color = unselectedColour + selectedPaint = Paint(Paint.ANTI_ALIAS_FLAG) + selectedPaint.color = selectedColour + interpolator = FastOutSlowInInterpolator() + + // create paths & rect now – reuse & rewind later + combinedUnselectedPath = Path() + unselectedDotPath = Path() + unselectedDotLeftPath = Path() + unselectedDotRightPath = Path() + rectF = RectF() + addOnAttachStateChangeListener(this) + } +} diff --git a/app/src/main/res/layout/fragment_intro.xml b/app/src/main/res/layout/fragment_intro.xml index ba9b9b357..7341c3ebd 100644 --- a/app/src/main/res/layout/fragment_intro.xml +++ b/app/src/main/res/layout/fragment_intro.xml @@ -25,7 +25,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> - + + + + + + + + + + diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index cd1d72a66..6d5176e10 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -288,9 +288,6 @@ object Libs { const val javax_annotation_api: String = "javax.annotation:javax.annotation-api:" + Versions.javax_annotation_api - const val ink_page_indicator: String = "com.pacioianu.david:ink-page-indicator:" + - Versions.ink_page_indicator - /** * http://github.com/square/leakcanary/ */ diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index c31fa64ad..93041465c 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -50,8 +50,6 @@ object Versions { const val javax_annotation_api: String = "1.3.2" - const val ink_page_indicator: String = "1.3.0" - const val leakcanary_android: String = "2.5" const val constraintlayout: String = "2.0.4"