Merge pull request #113 from esensar/improvement/page-swiping

Improve page change behavior
This commit is contained in:
Tibor Kaputa 2023-08-23 20:42:31 +02:00 committed by GitHub
commit a1b459f396
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 343 additions and 165 deletions

View File

@ -60,6 +60,8 @@ class MainActivity : SimpleActivity(), FlingListener {
private var mMoveGestureThreshold = 0 private var mMoveGestureThreshold = 0
private var mIgnoreUpEvent = false private var mIgnoreUpEvent = false
private var mIgnoreMoveEvents = false private var mIgnoreMoveEvents = false
private var mIgnoreXMoveEvents = false
private var mIgnoreYMoveEvents = false
private var mLongPressedIcon: HomeScreenGridItem? = null private var mLongPressedIcon: HomeScreenGridItem? = null
private var mOpenPopupMenu: PopupMenu? = null private var mOpenPopupMenu: PopupMenu? = null
private var mCachedLaunchers = ArrayList<AppLauncher>() private var mCachedLaunchers = ArrayList<AppLauncher>()
@ -362,7 +364,10 @@ class MainActivity : SimpleActivity(), FlingListener {
if (hasFingerMoved && !mIgnoreMoveEvents) { if (hasFingerMoved && !mIgnoreMoveEvents) {
val diffY = mTouchDownY - event.y val diffY = mTouchDownY - event.y
val diffX = mTouchDownX - event.x
if (abs(diffY) > abs(diffX) && !mIgnoreYMoveEvents) {
mIgnoreXMoveEvents = true
if (isWidgetsFragmentExpanded()) { if (isWidgetsFragmentExpanded()) {
val newY = mWidgetsFragmentY - diffY val newY = mWidgetsFragmentY - diffY
binding.widgetsFragment.root.y = Math.min(Math.max(0f, newY), mScreenHeight.toFloat()) binding.widgetsFragment.root.y = Math.min(Math.max(0f, newY), mScreenHeight.toFloat())
@ -370,6 +375,10 @@ class MainActivity : SimpleActivity(), FlingListener {
val newY = mAllAppsFragmentY - diffY val newY = mAllAppsFragmentY - diffY
binding.allAppsFragment.root.y = Math.min(Math.max(0f, newY), mScreenHeight.toFloat()) binding.allAppsFragment.root.y = Math.min(Math.max(0f, newY), mScreenHeight.toFloat())
} }
} else if (abs(diffX) > abs(diffY) && !mIgnoreXMoveEvents) {
mIgnoreYMoveEvents = true
binding.homeScreenGrid.root.setSwipeMovement(diffX)
}
} }
mLastTouchCoords = Pair(event.x, event.y) mLastTouchCoords = Pair(event.x, event.y)
@ -386,6 +395,7 @@ class MainActivity : SimpleActivity(), FlingListener {
binding.homeScreenGrid.root.itemDraggingStopped() binding.homeScreenGrid.root.itemDraggingStopped()
if (!mIgnoreUpEvent) { if (!mIgnoreUpEvent) {
if (!mIgnoreYMoveEvents) {
if (binding.allAppsFragment.root.y < mScreenHeight * 0.5) { if (binding.allAppsFragment.root.y < mScreenHeight * 0.5) {
showFragment(binding.allAppsFragment) showFragment(binding.allAppsFragment)
} else if (isAllAppsFragmentExpanded()) { } else if (isAllAppsFragmentExpanded()) {
@ -398,6 +408,14 @@ class MainActivity : SimpleActivity(), FlingListener {
hideFragment(binding.widgetsFragment) hideFragment(binding.widgetsFragment)
} }
} }
if (!mIgnoreXMoveEvents) {
binding.homeScreenGrid.root.finalizeSwipe()
}
}
mIgnoreXMoveEvents = false
mIgnoreYMoveEvents = false
} }
} }
@ -731,6 +749,10 @@ class MainActivity : SimpleActivity(), FlingListener {
} }
override fun onFlingUp() { override fun onFlingUp() {
if (mIgnoreYMoveEvents) {
return
}
if (!isWidgetsFragmentExpanded()) { if (!isWidgetsFragmentExpanded()) {
mIgnoreUpEvent = true mIgnoreUpEvent = true
showFragment(binding.allAppsFragment) showFragment(binding.allAppsFragment)
@ -739,6 +761,10 @@ class MainActivity : SimpleActivity(), FlingListener {
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
override fun onFlingDown() { override fun onFlingDown() {
if (mIgnoreYMoveEvents) {
return
}
mIgnoreUpEvent = true mIgnoreUpEvent = true
if (isAllAppsFragmentExpanded()) { if (isAllAppsFragmentExpanded()) {
hideFragment(binding.allAppsFragment) hideFragment(binding.allAppsFragment)
@ -753,11 +779,19 @@ class MainActivity : SimpleActivity(), FlingListener {
} }
override fun onFlingRight() { override fun onFlingRight() {
if (mIgnoreXMoveEvents) {
return
}
mIgnoreUpEvent = true mIgnoreUpEvent = true
binding.homeScreenGrid.root.prevPage(redraw = true) binding.homeScreenGrid.root.prevPage(redraw = true)
} }
override fun onFlingLeft() { override fun onFlingLeft() {
if (mIgnoreXMoveEvents) {
return
}
mIgnoreUpEvent = true mIgnoreUpEvent = true
binding.homeScreenGrid.root.nextPage(redraw = true) binding.homeScreenGrid.root.nextPage(redraw = true)
} }

View File

@ -11,6 +11,7 @@ import android.content.Context
import android.graphics.* import android.graphics.*
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.text.Layout import android.text.Layout
import android.text.StaticLayout import android.text.StaticLayout
import android.text.TextPaint import android.text.TextPaint
@ -37,10 +38,7 @@ import com.simplemobiletools.launcher.extensions.getDrawableForPackageName
import com.simplemobiletools.launcher.extensions.homeScreenGridItemsDB import com.simplemobiletools.launcher.extensions.homeScreenGridItemsDB
import com.simplemobiletools.launcher.helpers.* import com.simplemobiletools.launcher.helpers.*
import com.simplemobiletools.launcher.models.HomeScreenGridItem import com.simplemobiletools.launcher.models.HomeScreenGridItem
import kotlin.math.abs import kotlin.math.*
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min
class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : RelativeLayout(context, attrs, defStyle) { class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : RelativeLayout(context, attrs, defStyle) {
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0)
@ -70,13 +68,14 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
private var redrawWidgets = false private var redrawWidgets = false
private var iconSize = 0 private var iconSize = 0
private var lastPage = 0 private val pager = AnimatedGridPager(
private var currentPage = 0 getMaxPage = ::getMaxPage,
private var pageChangeLastArea = PageChangeArea.MIDDLE redrawGrid = ::redrawGrid,
private var pageChangeLastAreaEntryTime = 0L getWidth = { width },
private var pageChangeAnimLeftPercentage = 0f getHandler = { handler },
private var pageChangeEnabled = true getNextPageBound = { right - sideMargins.right - cellWidth / 2 },
private var pageChangeIndicatorsAlpha = 0f getPrevPageBound = { left + sideMargins.left + cellWidth / 2 }
)
// apply fake margins at the home screen. Real ones would cause the icons be cut at dragging at screen sides // apply fake margins at the home screen. Real ones would cause the icons be cut at dragging at screen sides
var sideMargins = Rect() var sideMargins = Rect()
@ -92,26 +91,6 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
var itemClickListener: ((HomeScreenGridItem) -> Unit)? = null var itemClickListener: ((HomeScreenGridItem) -> Unit)? = null
var itemLongClickListener: ((HomeScreenGridItem) -> Unit)? = null var itemLongClickListener: ((HomeScreenGridItem) -> Unit)? = null
private val checkAndExecuteDelayedPageChange: Runnable = Runnable {
if (System.currentTimeMillis() - pageChangeLastAreaEntryTime > PAGE_CHANGE_HOLD_THRESHOLD) {
when (pageChangeLastArea) {
PageChangeArea.RIGHT -> nextOrAdditionalPage(true)
PageChangeArea.LEFT -> prevPage(true)
else -> clearPageChangeFlags()
}
}
}
private val startFadingIndicators: Runnable = Runnable {
ValueAnimator.ofFloat(1f, 0f)
.apply {
addUpdateListener {
pageChangeIndicatorsAlpha = it.animatedValue as Float
redrawGrid()
}
start()
}
}
init { init {
ViewCompat.setAccessibilityDelegate(this, HomeScreenGridTouchHelper(this)) ViewCompat.setAccessibilityDelegate(this, HomeScreenGridTouchHelper(this))
@ -197,7 +176,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
} }
gridItems.removeIf { it.id == item.id } gridItems.removeIf { it.id == item.id }
if (currentPage > getMaxPage()) { if (pager.isOutsideOfPageRange()) {
post { post {
prevPage() prevPage()
} }
@ -232,9 +211,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
return return
} }
pageChangeIndicatorsAlpha = 1f if (draggedItemCurrentCoords.first == -1 && draggedItemCurrentCoords.second == -1) {
removeCallbacks(startFadingIndicators)
if (draggedItemCurrentCoords.first == -1 && draggedItemCurrentCoords.second == -1 && draggedItem != null) {
if (draggedItem!!.type == ITEM_TYPE_WIDGET) { if (draggedItem!!.type == ITEM_TYPE_WIDGET) {
val draggedWidgetView = widgetViews.firstOrNull { it.tag == draggedItem?.widgetId } val draggedWidgetView = widgetViews.firstOrNull { it.tag == draggedItem?.widgetId }
if (draggedWidgetView != null) { if (draggedWidgetView != null) {
@ -246,47 +223,10 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
} }
draggedItemCurrentCoords = Pair(x, y) draggedItemCurrentCoords = Pair(x, y)
if (x > right - sideMargins.right - cellWidth / 2) { pager.handleItemMovement(x, y)
doWithPageChangeDelay(PageChangeArea.RIGHT) {
nextOrAdditionalPage()
}
} else if (x < left + sideMargins.left + cellWidth / 2) {
doWithPageChangeDelay(PageChangeArea.LEFT) {
prevPage()
}
} else {
clearPageChangeFlags()
}
redrawGrid() redrawGrid()
} }
private fun clearPageChangeFlags() {
pageChangeLastArea = PageChangeArea.MIDDLE
pageChangeLastAreaEntryTime = 0
removeCallbacks(checkAndExecuteDelayedPageChange)
}
private fun schedulePageChange() {
pageChangeLastAreaEntryTime = System.currentTimeMillis()
postDelayed(checkAndExecuteDelayedPageChange, PAGE_CHANGE_HOLD_THRESHOLD)
}
private fun scheduleIndicatorsFade() {
pageChangeIndicatorsAlpha = 1f
postDelayed(startFadingIndicators, PAGE_INDICATORS_FADE_DELAY)
}
private fun doWithPageChangeDelay(needed: PageChangeArea, pageChangeFunction: () -> Boolean) {
if (pageChangeLastArea != needed) {
pageChangeLastArea = needed
schedulePageChange()
} else if (System.currentTimeMillis() - pageChangeLastAreaEntryTime > PAGE_CHANGE_HOLD_THRESHOLD) {
if (pageChangeFunction()) {
clearPageChangeFlags()
}
}
}
// figure out at which cell was the item dropped, if it is empty // figure out at which cell was the item dropped, if it is empty
fun itemDraggingStopped() { fun itemDraggingStopped() {
widgetViews.forEach { widgetViews.forEach {
@ -297,7 +237,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
return return
} }
scheduleIndicatorsFade() pager.itemMovementStopped()
when (draggedItem!!.type) { when (draggedItem!!.type) {
ITEM_TYPE_ICON, ITEM_TYPE_SHORTCUT -> addAppIconOrShortcut() ITEM_TYPE_ICON, ITEM_TYPE_SHORTCUT -> addAppIconOrShortcut()
ITEM_TYPE_WIDGET -> addWidget() ITEM_TYPE_WIDGET -> addWidget()
@ -373,7 +313,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
// check if the destination cell is empty // check if the destination cell is empty
var areAllCellsEmpty = true var areAllCellsEmpty = true
val wantedCell = Pair(xIndex, yIndex) val wantedCell = Pair(xIndex, yIndex)
gridItems.filter { it.page == currentPage || it.docked }.forEach { item -> gridItems.filter { pager.isItemOnCurrentPage(it) || it.docked }.forEach { item ->
for (xCell in item.left..item.right) { for (xCell in item.left..item.right) {
for (yCell in item.getDockAdjustedTop(rowCount)..item.getDockAdjustedBottom(rowCount)) { for (yCell in item.getDockAdjustedTop(rowCount)..item.getDockAdjustedBottom(rowCount)) {
val cell = Pair(xCell, yCell) val cell = Pair(xCell, yCell)
@ -396,7 +336,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
top = yIndex top = yIndex
right = xIndex right = xIndex
bottom = yIndex bottom = yIndex
page = currentPage page = pager.getCurrentPage()
docked = yIndex == rowCount - 1 docked = yIndex == rowCount - 1
ensureBackgroundThread { ensureBackgroundThread {
@ -412,7 +352,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
yIndex, yIndex,
xIndex, xIndex,
yIndex, yIndex,
currentPage, pager.getCurrentPage(),
draggedItem!!.packageName, draggedItem!!.packageName,
draggedItem!!.activityName, draggedItem!!.activityName,
draggedItem!!.title, draggedItem!!.title,
@ -479,7 +419,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
} }
var areAllCellsEmpty = true var areAllCellsEmpty = true
gridItems.filter { it.id != draggedItem?.id && (it.page == currentPage || it.docked) }.forEach { item -> gridItems.filter { it.id != draggedItem?.id && (pager.isItemOnCurrentPage(it) || it.docked) }.forEach { item ->
for (xCell in item.left..item.right) { for (xCell in item.left..item.right) {
for (yCell in item.getDockAdjustedTop(rowCount)..item.getDockAdjustedBottom(rowCount)) { for (yCell in item.getDockAdjustedTop(rowCount)..item.getDockAdjustedBottom(rowCount)) {
val cell = Pair(xCell, yCell) val cell = Pair(xCell, yCell)
@ -499,7 +439,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
top = widgetRect.top top = widgetRect.top
right = widgetRect.right right = widgetRect.right
bottom = widgetRect.bottom bottom = widgetRect.bottom
page = currentPage page = pager.getCurrentPage()
} }
ensureBackgroundThread { ensureBackgroundThread {
@ -516,7 +456,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
widgetItem.top, widgetItem.top,
widgetItem.right, widgetItem.right,
widgetItem.bottom, widgetItem.bottom,
currentPage, pager.getCurrentPage(),
false, false,
widgetItem.id!! widgetItem.id!!
) )
@ -575,7 +515,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
removeItemFromHomeScreen(item) removeItemFromHomeScreen(item)
} }
if (currentPage > getMaxPage()) { if (pager.isOutsideOfPageRange()) {
prevPage(redraw = true) prevPage(redraw = true)
} }
} }
@ -610,25 +550,8 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
} }
private fun updateWidgetPositionAndSize(widgetView: AppWidgetHostView, item: HomeScreenGridItem): Size { private fun updateWidgetPositionAndSize(widgetView: AppWidgetHostView, item: HomeScreenGridItem): Size {
var x = calculateWidgetX(item.left) + width * item.page - width * lastPage val currentViewPosition = pager.getCurrentViewPositionInFullPageSpace() * width.toFloat()
if (pageChangeAnimLeftPercentage > 0f && pageChangeAnimLeftPercentage < 1f && (item.page == currentPage || item.page == lastPage)) { val x = calculateWidgetX(item.left) + width * item.page - currentViewPosition
val xFactor = if (currentPage > lastPage) {
pageChangeAnimLeftPercentage
} else {
-pageChangeAnimLeftPercentage
}
val lastXFactor = if (currentPage > lastPage) {
pageChangeAnimLeftPercentage - 1
} else {
1 - pageChangeAnimLeftPercentage
}
if (item.page == currentPage) {
x += width * xFactor
}
if (item.page == lastPage) {
x += width * lastXFactor
}
}
widgetView.x = x widgetView.x = x
widgetView.y = calculateWidgetY(item.top) widgetView.y = calculateWidgetY(item.top)
val widgetWidth = item.getWidthInCells() * cellWidth val widgetWidth = item.getWidthInCells() * cellWidth
@ -685,16 +608,8 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
fillCellSizes() fillCellSizes()
} }
val currentXFactor = if (currentPage > lastPage) { val currentXFactor = pager.getXFactorForCurrentPage()
pageChangeAnimLeftPercentage val lastXFactor = pager.getXFactorForLastPage()
} else {
-pageChangeAnimLeftPercentage
}
val lastXFactor = if (currentPage > lastPage) {
pageChangeAnimLeftPercentage - 1
} else {
1 - pageChangeAnimLeftPercentage
}
fun handleItemDrawing(item: HomeScreenGridItem, xFactor: Float) { fun handleItemDrawing(item: HomeScreenGridItem, xFactor: Float) {
if (item.id != draggedItem?.id) { if (item.id != draggedItem?.id) {
@ -729,7 +644,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
} }
} }
gridItems.filter { (it.drawable != null && it.type == ITEM_TYPE_ICON || it.type == ITEM_TYPE_SHORTCUT) && it.page == currentPage && !it.docked } gridItems.filter { (it.drawable != null && it.type == ITEM_TYPE_ICON || it.type == ITEM_TYPE_SHORTCUT) && pager.isItemOnCurrentPage(it) && !it.docked }
.forEach { item -> .forEach { item ->
if (item.outOfBounds()) { if (item.outOfBounds()) {
return@forEach return@forEach
@ -744,8 +659,8 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
handleItemDrawing(item, 0f) handleItemDrawing(item, 0f)
} }
if (pageChangeAnimLeftPercentage > 0f) { if (pager.isAnimatingPageChange()) {
gridItems.filter { (it.drawable != null && it.type == ITEM_TYPE_ICON || it.type == ITEM_TYPE_SHORTCUT) && it.page == lastPage && !it.docked } gridItems.filter { (it.drawable != null && it.type == ITEM_TYPE_ICON || it.type == ITEM_TYPE_SHORTCUT) && pager.isItemOnLastPage(it) && !it.docked }
.forEach { item -> .forEach { item ->
if (item.outOfBounds()) { if (item.outOfBounds()) {
return@forEach return@forEach
@ -755,6 +670,20 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
} }
} }
if (pager.isSwiped()) {
gridItems.filter {
(it.drawable != null && it.type == ITEM_TYPE_ICON || it.type == ITEM_TYPE_SHORTCUT)
&& pager.isItemInSwipeRange(it)
&& !it.docked
}.forEach { item ->
if (item.outOfBounds()) {
return@forEach
}
handleItemDrawing(item, lastXFactor)
}
}
if (isFirstDraw) { if (isFirstDraw) {
gridItems.filter { it.type == ITEM_TYPE_WIDGET && !it.outOfBounds() }.forEach { item -> gridItems.filter { it.type == ITEM_TYPE_WIDGET && !it.outOfBounds() }.forEach { item ->
bindWidget(item, true) bindWidget(item, true)
@ -768,19 +697,15 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
} }
// Only draw page indicators when there is a need for it // Only draw page indicators when there is a need for it
if (pageChangeAnimLeftPercentage > 0f || pageChangeIndicatorsAlpha != 0f) { if (pager.shouldDisplayPageChangeIndicator()) {
val pageCount = max(getMaxPage(), currentPage) + 1 val pageCount = pager.getPageCount()
val pageIndicatorsRequiredWidth = pageCount * pageIndicatorRadius * 2 + pageCount * (pageIndicatorMargin - 1) val pageIndicatorsRequiredWidth = pageCount * pageIndicatorRadius * 2 + pageCount * (pageIndicatorMargin - 1)
val usableWidth = getFakeWidth() val usableWidth = getFakeWidth()
val pageIndicatorsStart = (usableWidth - pageIndicatorsRequiredWidth) / 2 + sideMargins.left val pageIndicatorsStart = (usableWidth - pageIndicatorsRequiredWidth) / 2 + sideMargins.left
var currentPageIndicatorLeft = pageIndicatorsStart var currentPageIndicatorLeft = pageIndicatorsStart
val pageIndicatorY = cellYCoords[rowCount - 1].toFloat() + sideMargins.top + extraYMargin + iconMargin val pageIndicatorY = cellYCoords[rowCount - 1].toFloat() + sideMargins.top + extraYMargin + iconMargin
val pageIndicatorStep = pageIndicatorRadius * 2 + pageIndicatorMargin val pageIndicatorStep = pageIndicatorRadius * 2 + pageIndicatorMargin
if (pageChangeIndicatorsAlpha != 0f) { emptyPageIndicatorPaint.alpha = pager.getPageChangeIndicatorsAlpha()
emptyPageIndicatorPaint.alpha = (pageChangeIndicatorsAlpha * 255.0f).toInt()
} else {
emptyPageIndicatorPaint.alpha = 255
}
// Draw empty page indicators // Draw empty page indicators
for (page in 0 until pageCount) { for (page in 0 until pageCount) {
canvas.drawCircle(currentPageIndicatorLeft + pageIndicatorRadius, pageIndicatorY, pageIndicatorRadius, emptyPageIndicatorPaint) canvas.drawCircle(currentPageIndicatorLeft + pageIndicatorRadius, pageIndicatorY, pageIndicatorRadius, emptyPageIndicatorPaint)
@ -788,14 +713,8 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
} }
// Draw current page indicator on exact position // Draw current page indicator on exact position
val currentIndicatorRangeStart = pageIndicatorsStart + lastPage * pageIndicatorStep val currentIndicatorPosition = pageIndicatorsStart + pager.getCurrentViewPositionInFullPageSpace() * pageIndicatorStep
val currentIndicatorRangeEnd = pageIndicatorsStart + currentPage * pageIndicatorStep currentPageIndicatorPaint.alpha = pager.getPageChangeIndicatorsAlpha()
val currentIndicatorPosition = MathUtils.lerp(currentIndicatorRangeStart, currentIndicatorRangeEnd, 1 - pageChangeAnimLeftPercentage)
if (pageChangeIndicatorsAlpha != 0f) {
currentPageIndicatorPaint.alpha = (pageChangeIndicatorsAlpha * 255.0f).toInt()
} else {
currentPageIndicatorPaint.alpha = 255
}
canvas.drawCircle(currentIndicatorPosition + pageIndicatorRadius, pageIndicatorY, pageIndicatorRadius, currentPageIndicatorPaint) canvas.drawCircle(currentIndicatorPosition + pageIndicatorRadius, pageIndicatorY, pageIndicatorRadius, currentPageIndicatorPaint)
} }
@ -942,7 +861,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
} }
fun isClickingGridItem(x: Int, y: Int): HomeScreenGridItem? { fun isClickingGridItem(x: Int, y: Int): HomeScreenGridItem? {
for (gridItem in gridItems.filter { it.page == currentPage || it.docked }) { for (gridItem in gridItems.filter { it.page == pager.getCurrentPage() || it.docked }) {
if (gridItem.outOfBounds()) { if (gridItem.outOfBounds()) {
continue continue
} }
@ -1038,15 +957,187 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
private fun getMaxPage() = gridItems.filter { !it.docked && !it.outOfBounds() }.maxOfOrNull { it.page } ?: 0 private fun getMaxPage() = gridItems.filter { !it.docked && !it.outOfBounds() }.maxOfOrNull { it.page } ?: 0
private fun nextOrAdditionalPage(redraw: Boolean = false): Boolean { fun nextPage(redraw: Boolean = false): Boolean {
if (currentPage < getMaxPage() + 1 && pageChangeEnabled) { return pager.nextPage(redraw)
lastPage = currentPage
currentPage++
handlePageChange(redraw)
return true
} }
return false fun prevPage(redraw: Boolean = false): Boolean {
return pager.prevPage(redraw)
}
fun skipToPage(targetPage: Int): Boolean {
return pager.skipToPage(targetPage)
}
fun getCurrentIconSize(): Int = iconSize
fun setSwipeMovement(diffX: Float) {
if (draggedItem == null) {
pager.setSwipeMovement(diffX)
}
}
fun finalizeSwipe() {
pager.finalizeSwipe()
}
}
/**
* Helper class responsible for managing current page and providing utilities for animating page changes,
* as well as partial dragigng between pages
*/
private class AnimatedGridPager(
private val getMaxPage: () -> Int,
private val redrawGrid: () -> Unit,
private val getWidth: () -> Int,
private val getHandler: () -> Handler,
private val getNextPageBound: () -> Int,
private val getPrevPageBound: () -> Int,
) {
companion object {
private const val PAGE_CHANGE_HOLD_THRESHOLD = 500L
private const val PAGE_INDICATORS_FADE_DELAY = PAGE_CHANGE_HOLD_THRESHOLD + 300L
private enum class PageChangeArea {
LEFT,
MIDDLE,
RIGHT
}
}
private var lastPage = 0
private var currentPage = 0
private var pageChangeLastArea = PageChangeArea.MIDDLE
private var pageChangeLastAreaEntryTime = 0L
private var pageChangeAnimLeftPercentage = 0f
private var pageChangeEnabled = true
private var pageChangeIndicatorsAlpha = 0f
private var pageChangeSwipedPercentage = 0f
fun getCurrentPage() = currentPage
fun isItemOnCurrentPage(item: HomeScreenGridItem) = item.page == currentPage
fun isItemOnLastPage(item: HomeScreenGridItem) = item.page == lastPage
fun getPageCount() = max(getMaxPage(), currentPage) + 1
fun isOutsideOfPageRange() = currentPage > getMaxPage()
fun isItemInSwipeRange(item: HomeScreenGridItem) =
((pageChangeSwipedPercentage > 0f && item.page == currentPage - 1) || (pageChangeSwipedPercentage < 0f && item.page == currentPage + 1))
fun isSwiped() = abs(pageChangeSwipedPercentage) > 0f
fun isAnimatingPageChange() = pageChangeAnimLeftPercentage > 0f
fun shouldDisplayPageChangeIndicator() = isSwiped() || isAnimatingPageChange() || pageChangeIndicatorsAlpha > 0f
fun getPageChangeIndicatorsAlpha() = if (pageChangeIndicatorsAlpha != 0f) {
(pageChangeIndicatorsAlpha * 255.0f).toInt()
} else {
255
}
fun getXFactorForCurrentPage(): Float {
return if (abs(pageChangeSwipedPercentage) > 0f) {
pageChangeSwipedPercentage
} else {
if (currentPage > lastPage) {
pageChangeAnimLeftPercentage
} else {
-pageChangeAnimLeftPercentage
}
}
}
fun getXFactorForLastPage(): Float {
return if (abs(pageChangeSwipedPercentage) > 0f) {
(1 - abs(pageChangeSwipedPercentage)) * -sign(pageChangeSwipedPercentage)
} else {
if (currentPage > lastPage) {
pageChangeAnimLeftPercentage - 1
} else {
1 - pageChangeAnimLeftPercentage
}
}
}
fun getCurrentViewPositionInFullPageSpace(): Float {
val rangeStart = lastPage.toFloat()
val rangeEndPage = if (abs(pageChangeSwipedPercentage) > 0f) {
if (pageChangeSwipedPercentage > 0f) {
currentPage - 1
} else {
currentPage + 1
}
} else {
currentPage
}
val rangeEnd = rangeEndPage.toFloat()
val lerpAmount = if (pageChangeAnimLeftPercentage > 0f) {
1 - pageChangeAnimLeftPercentage
} else {
abs(pageChangeSwipedPercentage)
}
return MathUtils.lerp(rangeStart, rangeEnd, lerpAmount)
}
fun setSwipeMovement(diffX: Float) {
if (!pageChangeEnabled) {
return
}
if (currentPage < getMaxPage() && diffX > 0f || currentPage > 0 && diffX < 0f) {
pageChangeSwipedPercentage = (-diffX / getWidth().toFloat()).coerceIn(-1f, 1f)
redrawGrid()
}
}
fun finalizeSwipe() {
if (abs(pageChangeSwipedPercentage) == 0f) {
return
}
if (abs(pageChangeSwipedPercentage) > 0.5f) {
lastPage = currentPage
currentPage = if (pageChangeSwipedPercentage > 0f) {
currentPage - 1
} else {
currentPage + 1
}
handlePageChange(true)
} else {
lastPage = if (pageChangeSwipedPercentage > 0f) {
currentPage - 1
} else {
currentPage + 1
}
pageChangeSwipedPercentage = sign(pageChangeSwipedPercentage) * (1 - abs(pageChangeSwipedPercentage))
handlePageChange(true)
}
}
fun handleItemMovement(x: Int, y: Int) {
showIndicators()
if (x > getNextPageBound()) {
doWithPageChangeDelay(PageChangeArea.RIGHT) {
nextOrAdditionalPage()
}
} else if (x < getPrevPageBound()) {
doWithPageChangeDelay(PageChangeArea.LEFT) {
prevPage()
}
} else {
clearPageChangeFlags()
}
}
fun itemMovementStopped() {
scheduleIndicatorsFade()
} }
fun nextPage(redraw: Boolean = false): Boolean { fun nextPage(redraw: Boolean = false): Boolean {
@ -1082,16 +1173,80 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
return false return false
} }
fun getCurrentIconSize(): Int = iconSize private val checkAndExecuteDelayedPageChange: Runnable = Runnable {
if (System.currentTimeMillis() - pageChangeLastAreaEntryTime > PAGE_CHANGE_HOLD_THRESHOLD) {
when (pageChangeLastArea) {
PageChangeArea.RIGHT -> nextOrAdditionalPage(true)
PageChangeArea.LEFT -> prevPage(true)
else -> clearPageChangeFlags()
}
}
}
private val startFadingIndicators: Runnable = Runnable {
ValueAnimator.ofFloat(1f, 0f)
.apply {
addUpdateListener {
pageChangeIndicatorsAlpha = it.animatedValue as Float
redrawGrid()
}
start()
}
}
private fun showIndicators() {
pageChangeIndicatorsAlpha = 1f
getHandler().removeCallbacks(startFadingIndicators)
}
private fun clearPageChangeFlags() {
pageChangeLastArea = PageChangeArea.MIDDLE
pageChangeLastAreaEntryTime = 0
getHandler().removeCallbacks(checkAndExecuteDelayedPageChange)
}
private fun schedulePageChange() {
pageChangeLastAreaEntryTime = System.currentTimeMillis()
getHandler().postDelayed(checkAndExecuteDelayedPageChange, PAGE_CHANGE_HOLD_THRESHOLD)
}
private fun scheduleIndicatorsFade() {
pageChangeIndicatorsAlpha = 1f
getHandler().postDelayed(startFadingIndicators, PAGE_INDICATORS_FADE_DELAY)
}
private fun doWithPageChangeDelay(needed: PageChangeArea, pageChangeFunction: () -> Boolean) {
if (pageChangeLastArea != needed) {
pageChangeLastArea = needed
schedulePageChange()
} else if (System.currentTimeMillis() - pageChangeLastAreaEntryTime > PAGE_CHANGE_HOLD_THRESHOLD) {
if (pageChangeFunction()) {
clearPageChangeFlags()
}
}
}
private fun nextOrAdditionalPage(redraw: Boolean = false): Boolean {
if (currentPage < getMaxPage() + 1 && pageChangeEnabled) {
lastPage = currentPage
currentPage++
handlePageChange(redraw)
return true
}
return false
}
private fun handlePageChange(redraw: Boolean = false) { private fun handlePageChange(redraw: Boolean = false) {
pageChangeEnabled = false pageChangeEnabled = false
pageChangeIndicatorsAlpha = 0f pageChangeIndicatorsAlpha = 0f
removeCallbacks(startFadingIndicators) val startingAt = 1 - abs(pageChangeSwipedPercentage)
pageChangeSwipedPercentage = 0f
getHandler().removeCallbacks(startFadingIndicators)
if (redraw) { if (redraw) {
redrawGrid() redrawGrid()
} }
ValueAnimator.ofFloat(1f, 0f) ValueAnimator.ofFloat(startingAt, 0f)
.apply { .apply {
addUpdateListener { addUpdateListener {
pageChangeAnimLeftPercentage = it.animatedValue as Float pageChangeAnimLeftPercentage = it.animatedValue as Float
@ -1103,7 +1258,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
pageChangeAnimLeftPercentage = 0f pageChangeAnimLeftPercentage = 0f
pageChangeEnabled = true pageChangeEnabled = true
lastPage = currentPage lastPage = currentPage
schedulePageChange() clearPageChangeFlags()
scheduleIndicatorsFade() scheduleIndicatorsFade()
redrawGrid() redrawGrid()
} }
@ -1111,15 +1266,4 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel
start() start()
} }
} }
companion object {
private const val PAGE_CHANGE_HOLD_THRESHOLD = 500L
private const val PAGE_INDICATORS_FADE_DELAY = PAGE_CHANGE_HOLD_THRESHOLD + 300L
private enum class PageChangeArea {
LEFT,
MIDDLE,
RIGHT
}
}
} }