HorizontalPageIndicator.kt
/*
* Copyright 2023 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 androidx.wear.compose.materialcore
import androidx.annotation.RestrictTo
import kotlin.math.abs
/**
* Represents an internal state of pageIndicator. This state is responsible for keeping and
* recalculating alpha and size parameters of each indicator, and selected indicators as well.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class PagesState(
val totalPages: Int,
val pagesOnScreen: Int
) {
// Sizes and alphas of first and last indicators on the screen. Used to show that there're more
// pages on the left or on the right, and also for smooth transitions
private var firstAlpha = 1f
private var lastAlpha = 0f
private var firstSize = 1f
private var secondSize = 1f
private var lastSize = 1f
private var lastButOneSize = 1f
private var smoothProgress = 0f
// An offset in pages, basically meaning how many pages are hidden to the left.
private var hiddenPagesToTheLeft = 0
// A default size of spacers - invisible items to the left and to the right of
// visible indicators, used for smooth transitions
// Current visible position on the screen.
var visibleDotIndex = 0
private set
// A size of a left spacer used for smooth transitions
val leftSpacerSizeRatio
get() = 1 - smoothProgress
// A size of a right spacer used for smooth transitions
val rightSpacerSizeRatio
get() = smoothProgress
/**
* Depending on the page index, return an alpha for this indicator
*
* @param page Page index
* @return An alpha of page index- in range 0..1
*/
fun alpha(page: Int): Float =
when (page) {
0 -> firstAlpha
pagesOnScreen -> lastAlpha
else -> 1f
}
/**
* Depending on the page index, return a size ratio for this indicator
*
* @param page Page index
* @return An size ratio for page index - in range 0..1
*/
fun sizeRatio(page: Int): Float =
when (page) {
0 -> firstSize
1 -> secondSize
pagesOnScreen - 1 -> lastButOneSize
pagesOnScreen -> lastSize
else -> 1f
}
/**
* Returns a value in the range 0..1 where 0 is unselected state, and 1 is selected.
* Used to show a smooth transition between page indicator items.
*/
fun calculateSelectedRatio(targetPage: Int, offset: Float): Float =
(1 - abs(visibleDotIndex + offset - targetPage)).coerceAtLeast(0f)
// Main function responsible for recalculation of all parameters regarding
// to the [selectedPage] and [offset]
fun recalculateState(selectedPage: Int, offset: Float) {
val pageWithOffset = selectedPage + offset
// Calculating offsetInPages relating to the [selectedPage].
// For example, for [selectedPage] = 4 we will see this picture :
// O O O O X o. [offsetInPages] will be 0.
// But when [selectedPage] will be incremented to 5, it will be seen as
// o O O O X o, with [offsetInPages] = 1
if (selectedPage > hiddenPagesToTheLeft + pagesOnScreen - 2) {
// Set an offset as a difference between current page and pages on the screen,
// except if this is not the last page - then offsetInPages is not changed
hiddenPagesToTheLeft = (selectedPage - (pagesOnScreen - 2))
.coerceAtMost(totalPages - pagesOnScreen)
} else if (pageWithOffset <= hiddenPagesToTheLeft) {
hiddenPagesToTheLeft = (selectedPage - 1).coerceAtLeast(0)
}
// Condition for scrolling to the right. A smooth scroll to the right is only triggered
// when we have more than 2 pages to the right, and currently we're on the right edge.
// For example -> o O O O X o -> a small "o" shows that there're more pages to the right
val scrolledToTheRight = pageWithOffset > hiddenPagesToTheLeft + pagesOnScreen - 2 &&
pageWithOffset < totalPages - 2
// Condition for scrolling to the left. A smooth scroll to the left is only triggered
// when we have more than 2 pages to the left, and currently we're on the left edge.
// For example -> o X O O O o -> a small "o" shows that there're more pages to the left
val scrolledToTheLeft = pageWithOffset > 1 && pageWithOffset < hiddenPagesToTheLeft + 1
smoothProgress = if (scrolledToTheLeft || scrolledToTheRight) offset else 0f
// Calculating exact parameters for border indicators like [firstAlpha], [lastSize], etc.
firstAlpha = 1 - smoothProgress
lastAlpha = smoothProgress
secondSize = 1 - 0.5f * smoothProgress
// Depending on offsetInPages we'll either show a shrinked first indicator, or full-size
firstSize = if (hiddenPagesToTheLeft == 0 ||
hiddenPagesToTheLeft == 1 && scrolledToTheLeft
) {
1 - smoothProgress
} else {
0.5f * (1 - smoothProgress)
}
// Depending on offsetInPages and other parameters, we'll either show a shrinked
// last indicator, or full-size
lastSize =
if (hiddenPagesToTheLeft == totalPages - pagesOnScreen - 1 && scrolledToTheRight ||
hiddenPagesToTheLeft == totalPages - pagesOnScreen && scrolledToTheLeft
) {
smoothProgress
} else {
0.5f * smoothProgress
}
lastButOneSize = if (scrolledToTheRight || scrolledToTheLeft) {
0.5f * (1 + smoothProgress)
} else if (hiddenPagesToTheLeft < totalPages - pagesOnScreen) 0.5f else 1f
// A visibleDot represents a currently selected page on the screen
// As we scroll to the left, we add an invisible indicator to the left, shifting all other
// indicators to the right. The shift is only possible when a visibleDot = 1,
// thus we have to leave it at 1 as we always add a positive offset
visibleDotIndex = if (scrolledToTheLeft) 1
else selectedPage - hiddenPagesToTheLeft
}
}