1961 lines
78 KiB
Kotlin
1961 lines
78 KiB
Kotlin
package com.simplemobiletools.launcher.views
|
|
|
|
import android.animation.Animator
|
|
import android.animation.AnimatorListenerAdapter
|
|
import android.animation.ValueAnimator
|
|
import android.annotation.SuppressLint
|
|
import android.appwidget.AppWidgetHostView
|
|
import android.appwidget.AppWidgetManager
|
|
import android.appwidget.AppWidgetProviderInfo
|
|
import android.content.Context
|
|
import android.graphics.*
|
|
import android.graphics.drawable.BitmapDrawable
|
|
import android.graphics.drawable.Drawable
|
|
import android.os.Bundle
|
|
import android.os.Handler
|
|
import android.text.Layout
|
|
import android.text.StaticLayout
|
|
import android.text.TextPaint
|
|
import android.text.TextUtils
|
|
import android.util.AttributeSet
|
|
import android.util.Size
|
|
import android.util.SizeF
|
|
import android.view.View
|
|
import android.view.animation.DecelerateInterpolator
|
|
import android.view.animation.OvershootInterpolator
|
|
import android.widget.RelativeLayout
|
|
import androidx.core.graphics.drawable.toBitmap
|
|
import androidx.core.graphics.drawable.toDrawable
|
|
import androidx.core.graphics.withScale
|
|
import androidx.core.graphics.withTranslation
|
|
import androidx.core.view.ViewCompat
|
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
|
import androidx.customview.widget.ExploreByTouchHelper
|
|
import com.google.android.material.math.MathUtils
|
|
import com.simplemobiletools.commons.extensions.*
|
|
import com.simplemobiletools.commons.helpers.*
|
|
import com.simplemobiletools.launcher.R
|
|
import com.simplemobiletools.launcher.activities.MainActivity
|
|
import com.simplemobiletools.launcher.databinding.HomeScreenGridBinding
|
|
import com.simplemobiletools.launcher.extensions.config
|
|
import com.simplemobiletools.launcher.extensions.getDrawableForPackageName
|
|
import com.simplemobiletools.launcher.extensions.homeScreenGridItemsDB
|
|
import com.simplemobiletools.launcher.helpers.*
|
|
import com.simplemobiletools.launcher.models.HomeScreenGridItem
|
|
import kotlinx.collections.immutable.toImmutableList
|
|
import kotlin.math.*
|
|
|
|
class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : RelativeLayout(context, attrs, defStyle) {
|
|
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0)
|
|
|
|
private lateinit var binding: HomeScreenGridBinding
|
|
private var columnCount = context.config.homeColumnCount
|
|
private var rowCount = context.config.homeRowCount
|
|
private var pageIndicatorsYPos = 0
|
|
private val cells = mutableMapOf<Point, Rect>()
|
|
private var dockCellY = 0
|
|
var cellWidth = 0
|
|
var cellHeight = 0
|
|
|
|
private var iconMargin = (context.resources.getDimension(R.dimen.icon_side_margin) * 5 / columnCount).toInt()
|
|
private var labelSideMargin = context.resources.getDimension(com.simplemobiletools.commons.R.dimen.small_margin).toInt()
|
|
private var roundedCornerRadius = context.resources.getDimension(com.simplemobiletools.commons.R.dimen.activity_margin)
|
|
private var folderPadding = context.resources.getDimension(com.simplemobiletools.commons.R.dimen.medium_margin)
|
|
private var pageIndicatorRadius = context.resources.getDimension(R.dimen.page_indicator_dot_radius)
|
|
private var pageIndicatorMargin = context.resources.getDimension(R.dimen.page_indicator_margin)
|
|
private var textPaint: TextPaint
|
|
private var contrastTextPaint: TextPaint
|
|
private var folderTitleTextPaint: TextPaint
|
|
private var dragShadowCirclePaint: Paint
|
|
private var emptyPageIndicatorPaint: Paint
|
|
private var currentPageIndicatorPaint: Paint
|
|
private var folderBackgroundPaint: Paint
|
|
private var folderIconBackgroundPaint: Paint
|
|
private var folderIconBorderPaint: Paint
|
|
private var draggedItem: HomeScreenGridItem? = null
|
|
private var resizedWidget: HomeScreenGridItem? = null
|
|
private var isFirstDraw = true
|
|
private var redrawWidgets = false
|
|
private var iconSize = 0
|
|
|
|
private val pager = AnimatedGridPager(
|
|
getMaxPage = ::getMaxPage,
|
|
redrawGrid = ::redrawGrid,
|
|
getWidth = { width },
|
|
getHandler = { handler },
|
|
getNextPageBound = { right - sideMargins.right - cellWidth / 2 },
|
|
getPrevPageBound = { left + sideMargins.left + cellWidth / 2 },
|
|
pageChangeStarted = {
|
|
widgetViews.forEach { it.resetTouches() }
|
|
closeFolder()
|
|
}
|
|
)
|
|
|
|
private var currentlyOpenFolder: HomeScreenFolder? = null
|
|
private var draggingLeftFolderAt: Long? = null
|
|
private var draggingEnteredNewFolderAt: Long? = null
|
|
|
|
// apply fake margins at the home screen. Real ones would cause the icons be cut at dragging at screen sides
|
|
var sideMargins = Rect()
|
|
|
|
private var gridItems = ArrayList<HomeScreenGridItem>()
|
|
private var gridCenters = ArrayList<Point>()
|
|
private var draggedItemCurrentCoords = Pair(-1, -1)
|
|
private var widgetViews = ArrayList<MyAppWidgetHostView>()
|
|
|
|
val appWidgetHost = MyAppWidgetHost(context, WIDGET_HOST_ID)
|
|
private val appWidgetManager = AppWidgetManager.getInstance(context)
|
|
|
|
var itemClickListener: ((HomeScreenGridItem) -> Unit)? = null
|
|
var itemLongClickListener: ((HomeScreenGridItem) -> Unit)? = null
|
|
|
|
|
|
init {
|
|
ViewCompat.setAccessibilityDelegate(this, HomeScreenGridTouchHelper(this))
|
|
|
|
textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
color = Color.WHITE
|
|
textSize = context.resources.getDimension(com.simplemobiletools.commons.R.dimen.smaller_text_size)
|
|
setShadowLayer(2f, 0f, 0f, Color.BLACK)
|
|
}
|
|
|
|
contrastTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
color = context.getProperTextColor()
|
|
textSize = context.resources.getDimension(com.simplemobiletools.commons.R.dimen.smaller_text_size)
|
|
setShadowLayer(2f, 0f, 0f, context.getProperTextColor().getContrastColor())
|
|
}
|
|
|
|
folderTitleTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
color = context.getProperTextColor()
|
|
textSize = context.resources.getDimension(com.simplemobiletools.commons.R.dimen.medium_text_size)
|
|
}
|
|
|
|
dragShadowCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
color = context.resources.getColor(com.simplemobiletools.commons.R.color.hint_white)
|
|
strokeWidth = context.resources.getDimension(com.simplemobiletools.commons.R.dimen.small_margin)
|
|
style = Paint.Style.STROKE
|
|
}
|
|
|
|
emptyPageIndicatorPaint = Paint(dragShadowCirclePaint).apply {
|
|
strokeWidth = context.resources.getDimension(R.dimen.page_indicator_stroke_width)
|
|
}
|
|
currentPageIndicatorPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
color = context.resources.getColor(android.R.color.white)
|
|
style = Paint.Style.FILL
|
|
}
|
|
|
|
folderBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
color = context.getProperBackgroundColor()
|
|
style = Paint.Style.FILL
|
|
}
|
|
|
|
folderIconBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
color = context.getProperBackgroundColor().adjustAlpha(MEDIUM_ALPHA)
|
|
style = Paint.Style.FILL
|
|
}
|
|
|
|
folderIconBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
color = context.getProperBackgroundColor().adjustAlpha(HIGHER_ALPHA)
|
|
strokeWidth = context.resources.getDimension(R.dimen.page_indicator_stroke_width) * 5
|
|
style = Paint.Style.STROKE
|
|
}
|
|
|
|
val sideMargin = context.resources.getDimension(com.simplemobiletools.commons.R.dimen.normal_margin).toInt()
|
|
sideMargins.apply {
|
|
top = context.statusBarHeight
|
|
bottom = context.navigationBarHeight
|
|
left = sideMargin
|
|
right = sideMargin
|
|
}
|
|
|
|
fetchGridItems()
|
|
}
|
|
|
|
override fun onFinishInflate() {
|
|
super.onFinishInflate()
|
|
binding = HomeScreenGridBinding.bind(this)
|
|
}
|
|
|
|
fun fetchGridItems() {
|
|
ensureBackgroundThread {
|
|
val providers = appWidgetManager.installedProviders
|
|
gridItems = context.homeScreenGridItemsDB.getAllItems() as ArrayList<HomeScreenGridItem>
|
|
gridItems.toImmutableList().forEach { item ->
|
|
if (item.type == ITEM_TYPE_ICON) {
|
|
item.drawable = context.getDrawableForPackageName(item.packageName)
|
|
} else if (item.type == ITEM_TYPE_FOLDER) {
|
|
item.drawable = item.toFolder().generateDrawable()
|
|
} else if (item.type == ITEM_TYPE_SHORTCUT) {
|
|
if (item.icon != null) {
|
|
item.drawable = BitmapDrawable(item.icon)
|
|
} else {
|
|
ensureBackgroundThread {
|
|
context.homeScreenGridItemsDB.deleteById(item.id!!)
|
|
}
|
|
}
|
|
}
|
|
|
|
item.providerInfo = providers.firstOrNull { it.provider.className == item.className }
|
|
}
|
|
|
|
redrawGrid()
|
|
}
|
|
}
|
|
|
|
fun resizeGrid(newRowCount: Int, newColumnCount: Int) {
|
|
if (columnCount != newColumnCount || rowCount != newRowCount) {
|
|
rowCount = newRowCount
|
|
columnCount = newColumnCount
|
|
cells.clear()
|
|
gridCenters.clear()
|
|
iconMargin = (context.resources.getDimension(R.dimen.icon_side_margin) * 5 / columnCount).toInt()
|
|
redrawWidgets = true
|
|
redrawGrid()
|
|
}
|
|
}
|
|
|
|
fun updateColors() {
|
|
folderTitleTextPaint.color = context.getProperTextColor()
|
|
contrastTextPaint.color = context.getProperTextColor()
|
|
contrastTextPaint.setShadowLayer(2f, 0f, 0f, context.getProperTextColor().getContrastColor())
|
|
folderBackgroundPaint.color = context.getProperBackgroundColor()
|
|
}
|
|
|
|
fun removeAppIcon(item: HomeScreenGridItem) {
|
|
ensureBackgroundThread {
|
|
removeItemFromHomeScreen(item)
|
|
post {
|
|
removeView(widgetViews.firstOrNull { it.tag == item.widgetId })
|
|
}
|
|
|
|
gridItems.removeIf { it.id == item.id }
|
|
if (pager.isOutsideOfPageRange()) {
|
|
post {
|
|
prevPage()
|
|
}
|
|
}
|
|
redrawGrid()
|
|
}
|
|
}
|
|
|
|
private fun removeItemFromHomeScreen(item: HomeScreenGridItem) {
|
|
ensureBackgroundThread {
|
|
if (item.id != null) {
|
|
context.homeScreenGridItemsDB.deleteById(item.id!!)
|
|
}
|
|
|
|
if (item.type == ITEM_TYPE_WIDGET) {
|
|
appWidgetHost.deleteAppWidgetId(item.widgetId)
|
|
}
|
|
|
|
if (item.page != 0 && gridItems.none { it.page == item.page && it.id != item.id && it.parentId == null }) {
|
|
deletePage(item.page)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun itemDraggingStarted(draggedGridItem: HomeScreenGridItem) {
|
|
draggedItem = draggedGridItem
|
|
|
|
if (draggedGridItem.type == ITEM_TYPE_WIDGET) {
|
|
closeFolder()
|
|
}
|
|
|
|
if (draggedItem!!.drawable == null) {
|
|
if (draggedItem?.type == ITEM_TYPE_FOLDER) {
|
|
draggedItem!!.drawable = draggedGridItem.toFolder().generateDrawable()
|
|
} else {
|
|
draggedItem!!.drawable = context.getDrawableForPackageName(draggedGridItem.packageName)
|
|
}
|
|
}
|
|
|
|
redrawGrid()
|
|
}
|
|
|
|
fun draggedItemMoved(x: Int, y: Int) {
|
|
if (draggedItem == null) {
|
|
return
|
|
}
|
|
|
|
currentlyOpenFolder?.also { folder ->
|
|
if (folder.getDrawingRect().contains(x.toFloat(), y.toFloat())) {
|
|
draggingLeftFolderAt = null
|
|
} else {
|
|
draggingLeftFolderAt.also {
|
|
if (it == null) {
|
|
draggingLeftFolderAt = System.currentTimeMillis()
|
|
} else if (System.currentTimeMillis() - it > FOLDER_CLOSE_HOLD_THRESHOLD) {
|
|
closeFolder()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (draggedItemCurrentCoords.first == -1 && draggedItemCurrentCoords.second == -1 && draggedItem != null) {
|
|
if (draggedItem!!.type == ITEM_TYPE_WIDGET) {
|
|
val draggedWidgetView = widgetViews.firstOrNull { it.tag == draggedItem?.widgetId }
|
|
if (draggedWidgetView != null) {
|
|
draggedWidgetView.buildDrawingCache()
|
|
draggedItem!!.drawable = Bitmap.createBitmap(draggedWidgetView.drawingCache).toDrawable(context.resources)
|
|
draggedWidgetView.beGone()
|
|
}
|
|
}
|
|
}
|
|
|
|
draggedItemCurrentCoords = Pair(x, y)
|
|
|
|
if (draggedItem?.type != ITEM_TYPE_FOLDER && draggedItem?.type != ITEM_TYPE_WIDGET) {
|
|
val center = gridCenters.minBy {
|
|
abs(it.x - draggedItemCurrentCoords.first + sideMargins.left) + abs(it.y - draggedItemCurrentCoords.second + sideMargins.top)
|
|
}
|
|
val coveredCell = getClosestGridCells(center)
|
|
if (coveredCell != null) {
|
|
val coveredFolder = gridItems.firstOrNull { it.type == ITEM_TYPE_FOLDER && it.left == coveredCell.x && it.top == coveredCell.y }
|
|
if (coveredFolder != null && coveredFolder.id != draggedItem?.id && currentlyOpenFolder == null) {
|
|
draggingEnteredNewFolderAt.also {
|
|
if (it == null) {
|
|
draggingEnteredNewFolderAt = System.currentTimeMillis()
|
|
} else if (System.currentTimeMillis() - it > FOLDER_OPEN_HOLD_THRESHOLD) {
|
|
if (coveredFolder.toFolder().getItems()
|
|
.count() >= HomeScreenGridItem.FOLDER_MAX_CAPACITY && draggedItem?.parentId != coveredFolder.id
|
|
) {
|
|
performHapticFeedback()
|
|
draggingEnteredNewFolderAt = null
|
|
} else {
|
|
openFolder(coveredFolder)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
draggingEnteredNewFolderAt = null
|
|
}
|
|
} else {
|
|
draggingEnteredNewFolderAt = null
|
|
}
|
|
}
|
|
|
|
pager.handleItemMovement(x, y)
|
|
redrawGrid()
|
|
}
|
|
|
|
// figure out at which cell was the item dropped, if it is empty
|
|
fun itemDraggingStopped() {
|
|
widgetViews.forEach {
|
|
it.hasLongPressed = false
|
|
}
|
|
|
|
if (draggedItem == null) {
|
|
return
|
|
}
|
|
|
|
pager.itemMovementStopped()
|
|
when (draggedItem!!.type) {
|
|
ITEM_TYPE_FOLDER -> moveItem()
|
|
ITEM_TYPE_ICON, ITEM_TYPE_SHORTCUT -> addAppIconOrShortcut()
|
|
ITEM_TYPE_WIDGET -> addWidget()
|
|
}
|
|
}
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
fun widgetLongPressed(item: HomeScreenGridItem) {
|
|
resizedWidget = item
|
|
redrawGrid()
|
|
|
|
val widgetView = widgetViews.firstOrNull { it.tag == resizedWidget!!.widgetId }
|
|
binding.resizeFrame.beGone()
|
|
if (widgetView != null) {
|
|
val viewX = widgetView.x.toInt()
|
|
val viewY = widgetView.y.toInt()
|
|
val frameRect = Rect(viewX, viewY, viewX + widgetView.width, viewY + widgetView.height)
|
|
val otherGridItems = gridItems.filterVisibleOnCurrentPageOnly().filter { it.widgetId != item.widgetId }.toMutableList() as ArrayList<HomeScreenGridItem>
|
|
binding.resizeFrame.updateFrameCoords(frameRect, cellWidth, cellHeight, sideMargins, item, otherGridItems)
|
|
binding.resizeFrame.beVisible()
|
|
binding.resizeFrame.z = 1f // make sure the frame isnt behind the widget itself
|
|
binding.resizeFrame.onClickListener = {
|
|
hideResizeLines()
|
|
}
|
|
|
|
binding.resizeFrame.onResizeListener = { cellsRect ->
|
|
item.left = cellsRect.left
|
|
item.top = cellsRect.top
|
|
item.right = cellsRect.right
|
|
item.bottom = if (cellsRect.bottom > rowCount - 2) {
|
|
rowCount - 2
|
|
} else {
|
|
cellsRect.bottom
|
|
}
|
|
updateWidgetPositionAndSize(widgetView, item)
|
|
ensureBackgroundThread {
|
|
context.homeScreenGridItemsDB.updateItemPosition(item.left, item.top, item.right, item.bottom, item.page, false, null, item.id!!)
|
|
}
|
|
}
|
|
|
|
widgetView.ignoreTouches = true
|
|
widgetView.setOnTouchListener { v, event ->
|
|
binding.resizeFrame.onTouchEvent(event)
|
|
return@setOnTouchListener true
|
|
}
|
|
}
|
|
}
|
|
|
|
fun hideResizeLines() {
|
|
if (resizedWidget == null) {
|
|
return
|
|
}
|
|
|
|
binding.resizeFrame.beGone()
|
|
widgetViews.firstOrNull { it.tag == resizedWidget!!.widgetId }?.apply {
|
|
ignoreTouches = false
|
|
setOnTouchListener(null)
|
|
}
|
|
resizedWidget = null
|
|
}
|
|
|
|
private fun moveItem() {
|
|
val draggedHomeGridItem = gridItems.firstOrNull { it.id == draggedItem?.id }
|
|
val center = gridCenters.minBy {
|
|
abs(it.x - draggedItemCurrentCoords.first + sideMargins.left) + abs(it.y - draggedItemCurrentCoords.second + sideMargins.top)
|
|
}
|
|
|
|
var redrawIcons = false
|
|
val gridCells = getClosestGridCells(center)
|
|
if (gridCells != null) {
|
|
val xIndex = gridCells.x
|
|
val yIndex = gridCells.y
|
|
|
|
// check if the destination cell is empty
|
|
var isDroppingPositionValid = true
|
|
val wantedCell = Pair(xIndex, yIndex)
|
|
// No moving folder into the dock
|
|
if (draggedHomeGridItem?.type == ITEM_TYPE_FOLDER && yIndex == rowCount - 1) {
|
|
isDroppingPositionValid = false
|
|
} else {
|
|
gridItems.filterVisibleOnCurrentPageOnly().forEach { item ->
|
|
for (xCell in item.left..item.right) {
|
|
for (yCell in item.getDockAdjustedTop(rowCount)..item.getDockAdjustedBottom(rowCount)) {
|
|
val cell = Pair(xCell, yCell)
|
|
val isAnyCellOccupied = wantedCell == cell
|
|
if (isAnyCellOccupied) {
|
|
isDroppingPositionValid = false
|
|
return@forEach
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isDroppingPositionValid) {
|
|
draggedHomeGridItem?.apply {
|
|
val oldPage = page
|
|
left = xIndex
|
|
top = yIndex
|
|
right = xIndex
|
|
bottom = yIndex
|
|
page = pager.getCurrentPage()
|
|
docked = yIndex == rowCount - 1
|
|
|
|
ensureBackgroundThread {
|
|
context.homeScreenGridItemsDB.updateItemPosition(left, top, right, bottom, page, docked, parentId, id!!)
|
|
|
|
if (page != oldPage && oldPage != 0) {
|
|
if (gridItems.none { it.page == oldPage && it.parentId == null }) {
|
|
deletePage(oldPage)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
redrawIcons = true
|
|
}
|
|
}
|
|
|
|
draggedItem = null
|
|
draggedItemCurrentCoords = Pair(-1, -1)
|
|
if (redrawIcons) {
|
|
redrawGrid()
|
|
}
|
|
}
|
|
|
|
private fun addAppIconOrShortcut() {
|
|
var isDroppingPositionValid: Boolean = false
|
|
var potentialParent: HomeScreenGridItem? = null
|
|
var xIndex: Int? = null
|
|
var yIndex: Int? = null
|
|
var redrawIcons = false
|
|
|
|
val folder = currentlyOpenFolder
|
|
if (folder != null && folder.getItemsDrawingRect().contains(
|
|
(draggedItemCurrentCoords.first).toFloat(),
|
|
(draggedItemCurrentCoords.second).toFloat()
|
|
)
|
|
) {
|
|
val center = folder.getItemsGridCenters().minBy {
|
|
abs(it.second - draggedItemCurrentCoords.first + sideMargins.left) + abs(it.third - draggedItemCurrentCoords.second + sideMargins.top)
|
|
}
|
|
isDroppingPositionValid = true
|
|
potentialParent = folder.item
|
|
xIndex = center.first
|
|
yIndex = 0
|
|
redrawIcons = true
|
|
} else {
|
|
val center = gridCenters.minBy {
|
|
Math.abs(it.x - draggedItemCurrentCoords.first + sideMargins.left) + Math.abs(it.y - draggedItemCurrentCoords.second + sideMargins.top)
|
|
}
|
|
|
|
val gridCells = getClosestGridCells(center)
|
|
if (gridCells != null) {
|
|
xIndex = gridCells.x
|
|
yIndex = gridCells.y
|
|
|
|
// check if the destination cell is empty or a folder
|
|
isDroppingPositionValid = true
|
|
val wantedCell = Pair(xIndex, yIndex)
|
|
gridItems.filterVisibleOnCurrentPageOnly().filter { it.id != draggedItem?.id }.forEach { item ->
|
|
for (xCell in item.left..item.right) {
|
|
for (yCell in item.getDockAdjustedTop(rowCount)..item.getDockAdjustedBottom(rowCount)) {
|
|
val cell = Pair(xCell, yCell)
|
|
val isAnyCellOccupied = wantedCell == cell
|
|
if (isAnyCellOccupied) {
|
|
if (item.type != ITEM_TYPE_WIDGET && !item.docked) {
|
|
potentialParent = item
|
|
} else {
|
|
isDroppingPositionValid = false
|
|
}
|
|
return@forEach
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isDroppingPositionValid) {
|
|
val draggedHomeGridItem = gridItems.firstOrNull { it.id == draggedItem?.id }
|
|
|
|
if (potentialParent != null) {
|
|
if (potentialParent?.type == ITEM_TYPE_FOLDER) {
|
|
addAppIconOrShortcut(
|
|
draggedHomeGridItem,
|
|
xIndex!!,
|
|
yIndex!!,
|
|
potentialParent?.id,
|
|
toFolderEnd = potentialParent != currentlyOpenFolder?.item
|
|
)
|
|
} else {
|
|
val parentItem = potentialParent!!.copy(
|
|
type = ITEM_TYPE_FOLDER,
|
|
id = null,
|
|
title = resources.getString(com.simplemobiletools.commons.R.string.folder)
|
|
)
|
|
ensureBackgroundThread {
|
|
val newId = context.homeScreenGridItemsDB.insert(parentItem)
|
|
parentItem.id = newId
|
|
potentialParent?.apply {
|
|
parentId = newId
|
|
left = 0
|
|
context.homeScreenGridItemsDB.updateItemPosition(left, top, right, bottom, page, docked, newId, id!!)
|
|
}
|
|
(context as? MainActivity)?.runOnUiThread {
|
|
gridItems.add(parentItem)
|
|
addAppIconOrShortcut(draggedHomeGridItem, xIndex!!, yIndex!!, newId)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
} else {
|
|
addAppIconOrShortcut(draggedHomeGridItem, xIndex!!, yIndex!!)
|
|
return
|
|
}
|
|
} else {
|
|
performHapticFeedback()
|
|
redrawIcons = true
|
|
}
|
|
|
|
draggedItem = null
|
|
draggedItemCurrentCoords = Pair(-1, -1)
|
|
if (redrawIcons) {
|
|
redrawGrid()
|
|
}
|
|
}
|
|
|
|
private fun addAppIconOrShortcut(
|
|
draggedHomeGridItem: HomeScreenGridItem?,
|
|
xIndex: Int,
|
|
yIndex: Int,
|
|
newParentId: Long? = null,
|
|
toFolderEnd: Boolean = true
|
|
) {
|
|
if (newParentId != null && newParentId != draggedHomeGridItem?.parentId) {
|
|
gridItems.firstOrNull { it.id == newParentId }?.also {
|
|
if (it.toFolder().getItems().count() >= HomeScreenGridItem.FOLDER_MAX_CAPACITY) {
|
|
performHapticFeedback()
|
|
draggedItem = null
|
|
draggedItemCurrentCoords = Pair(-1, -1)
|
|
redrawGrid()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
val finalXIndex = if (newParentId != null) {
|
|
if (toFolderEnd) {
|
|
gridItems.firstOrNull { it.id == newParentId }?.toFolder()?.getItems()?.maxOf { it.left + 1 } ?: 0
|
|
} else {
|
|
min(xIndex, gridItems.firstOrNull { it.id == newParentId }?.toFolder()?.getItems()?.maxOf {
|
|
if (draggedHomeGridItem?.parentId == newParentId) {
|
|
it.left
|
|
} else {
|
|
it.left + 1
|
|
}
|
|
} ?: 0)
|
|
}
|
|
} else {
|
|
xIndex
|
|
}
|
|
// we are moving an existing home screen item from one place to another
|
|
if (draggedHomeGridItem != null) {
|
|
draggedHomeGridItem.apply {
|
|
val oldParentId = parentId
|
|
val oldLeft = left
|
|
val oldPage = page
|
|
left = finalXIndex
|
|
top = yIndex
|
|
right = finalXIndex
|
|
bottom = yIndex
|
|
page = pager.getCurrentPage()
|
|
docked = yIndex == rowCount - 1
|
|
parentId = newParentId
|
|
|
|
val oldParent = gridItems.firstOrNull { it.id == oldParentId }
|
|
val deleteOldParent = if (oldParent?.toFolder()?.getItems()?.isEmpty() == true) {
|
|
gridItems.remove(oldParent)
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
|
|
ensureBackgroundThread {
|
|
context.homeScreenGridItemsDB.updateItemPosition(left, top, right, bottom, page, docked, newParentId, id!!)
|
|
if (deleteOldParent && oldParentId != null) {
|
|
context.homeScreenGridItemsDB.deleteById(oldParentId)
|
|
} else if (oldParentId != null && gridItems.none { it.parentId == oldParentId && it.left == oldLeft }) {
|
|
gridItems.filter { it.parentId == oldParentId && it.left > oldLeft && it.id != id }.forEach {
|
|
it.left -= 1
|
|
}
|
|
context.homeScreenGridItemsDB.shiftFolderItems(oldParentId, oldLeft, -1, id)
|
|
}
|
|
|
|
if (newParentId != null && gridItems.any { it.parentId == newParentId && it.left == left } && (newParentId != oldParentId || left != oldLeft)) {
|
|
gridItems.filter { it.parentId == newParentId && it.left >= left && it.id != id }.forEach {
|
|
it.left += 1
|
|
}
|
|
|
|
context.homeScreenGridItemsDB.shiftFolderItems(newParentId, left - 1, +1, id)
|
|
}
|
|
|
|
if (page != oldPage && oldPage != 0) {
|
|
if (gridItems.none { it.page == oldPage && it.parentId == null }) {
|
|
deletePage(oldPage)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (draggedItem != null) {
|
|
// we are dragging a new item at the home screen from the All Apps fragment
|
|
val newHomeScreenGridItem = HomeScreenGridItem(
|
|
null,
|
|
finalXIndex,
|
|
yIndex,
|
|
finalXIndex,
|
|
yIndex,
|
|
pager.getCurrentPage(),
|
|
draggedItem!!.packageName,
|
|
draggedItem!!.activityName,
|
|
draggedItem!!.title,
|
|
draggedItem!!.type,
|
|
"",
|
|
-1,
|
|
"",
|
|
draggedItem!!.icon,
|
|
yIndex == rowCount - 1,
|
|
newParentId,
|
|
draggedItem!!.drawable,
|
|
draggedItem!!.providerInfo,
|
|
draggedItem!!.activityInfo
|
|
)
|
|
|
|
if (newHomeScreenGridItem.type == ITEM_TYPE_ICON) {
|
|
ensureBackgroundThread {
|
|
storeAndShowGridItem(newHomeScreenGridItem)
|
|
}
|
|
} else if (newHomeScreenGridItem.type == ITEM_TYPE_SHORTCUT) {
|
|
(context as? MainActivity)?.handleShorcutCreation(newHomeScreenGridItem.activityInfo!!) { shortcutId, label, icon ->
|
|
ensureBackgroundThread {
|
|
newHomeScreenGridItem.shortcutId = shortcutId
|
|
newHomeScreenGridItem.title = label
|
|
newHomeScreenGridItem.icon = icon.toBitmap()
|
|
newHomeScreenGridItem.drawable = icon
|
|
storeAndShowGridItem(newHomeScreenGridItem)
|
|
}
|
|
}
|
|
}
|
|
|
|
ensureBackgroundThread {
|
|
if (newParentId != null && gridItems.any { it.parentId == newParentId && it.left == finalXIndex }) {
|
|
gridItems.filter { it.parentId == newParentId && it.left >= finalXIndex }.forEach {
|
|
it.left += 1
|
|
}
|
|
|
|
context.homeScreenGridItemsDB.shiftFolderItems(newParentId, left - 1, +1)
|
|
}
|
|
}
|
|
}
|
|
|
|
draggedItem = null
|
|
draggedItemCurrentCoords = Pair(-1, -1)
|
|
redrawGrid()
|
|
}
|
|
|
|
fun storeAndShowGridItem(item: HomeScreenGridItem) {
|
|
val newId = context.homeScreenGridItemsDB.insert(item)
|
|
item.id = newId
|
|
gridItems.add(item)
|
|
redrawGrid()
|
|
}
|
|
|
|
private fun addWidget() {
|
|
val center = gridCenters.minBy {
|
|
Math.abs(it.x - draggedItemCurrentCoords.first + sideMargins.left) + Math.abs(it.y - draggedItemCurrentCoords.second + sideMargins.top)
|
|
}
|
|
|
|
val gridCells = getClosestGridCells(center)
|
|
if (gridCells != null) {
|
|
val widgetRect = getWidgetOccupiedRect(gridCells)
|
|
val widgetTargetCells = ArrayList<Pair<Int, Int>>()
|
|
for (xCell in widgetRect.left..widgetRect.right) {
|
|
for (yCell in widgetRect.top..widgetRect.bottom) {
|
|
widgetTargetCells.add(Pair(xCell, yCell))
|
|
}
|
|
}
|
|
|
|
var areAllCellsEmpty = true
|
|
gridItems.filterVisibleOnCurrentPageOnly().filter { it.id != draggedItem?.id }.forEach { item ->
|
|
for (xCell in item.left..item.right) {
|
|
for (yCell in item.getDockAdjustedTop(rowCount)..item.getDockAdjustedBottom(rowCount)) {
|
|
val cell = Pair(xCell, yCell)
|
|
val isAnyCellOccupied = widgetTargetCells.contains(cell)
|
|
if (isAnyCellOccupied) {
|
|
areAllCellsEmpty = false
|
|
return@forEach
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (areAllCellsEmpty) {
|
|
val widgetItem = draggedItem!!.copy()
|
|
val oldPage = widgetItem.page
|
|
widgetItem.apply {
|
|
left = widgetRect.left
|
|
top = widgetRect.top
|
|
right = widgetRect.right
|
|
bottom = widgetRect.bottom
|
|
page = pager.getCurrentPage()
|
|
}
|
|
|
|
ensureBackgroundThread {
|
|
// store the new widget at creating it, else just move the existing one
|
|
if (widgetItem.id == null) {
|
|
val itemId = context.homeScreenGridItemsDB.insert(widgetItem)
|
|
widgetItem.id = itemId
|
|
post {
|
|
bindWidget(widgetItem, false)
|
|
}
|
|
} else {
|
|
context.homeScreenGridItemsDB.updateItemPosition(
|
|
widgetItem.left,
|
|
widgetItem.top,
|
|
widgetItem.right,
|
|
widgetItem.bottom,
|
|
pager.getCurrentPage(),
|
|
false,
|
|
null,
|
|
widgetItem.id!!
|
|
)
|
|
val widgetView = widgetViews.firstOrNull { it.tag == widgetItem.widgetId }
|
|
if (widgetView != null && !widgetItem.outOfBounds()) {
|
|
post {
|
|
val widgetPos = calculateWidgetPos(widgetItem.getTopLeft())
|
|
widgetView.x = widgetPos.x.toFloat()
|
|
widgetView.y = widgetPos.y.toFloat()
|
|
widgetView.beVisible()
|
|
}
|
|
}
|
|
|
|
gridItems.firstOrNull { it.id == widgetItem.id }?.apply {
|
|
left = widgetItem.left
|
|
right = widgetItem.right
|
|
top = widgetItem.top
|
|
bottom = widgetItem.bottom
|
|
page = pager.getCurrentPage()
|
|
}
|
|
|
|
|
|
if (widgetItem.page != oldPage && oldPage != 0) {
|
|
if (gridItems.none { it.page == oldPage && it.parentId == null }) {
|
|
deletePage(oldPage)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
performHapticFeedback()
|
|
widgetViews.firstOrNull { it.tag == draggedItem?.widgetId }?.apply {
|
|
post {
|
|
beVisible()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
draggedItem = null
|
|
draggedItemCurrentCoords = Pair(-1, -1)
|
|
redrawGrid()
|
|
}
|
|
|
|
private fun bindWidget(item: HomeScreenGridItem, isInitialDrawAfterLaunch: Boolean) {
|
|
val activity = context as MainActivity
|
|
val appWidgetProviderInfo = item.providerInfo ?: appWidgetManager!!.installedProviders.firstOrNull { it.provider.className == item.className }
|
|
if (appWidgetProviderInfo != null) {
|
|
val appWidgetId = appWidgetHost.allocateAppWidgetId()
|
|
activity.handleWidgetBinding(appWidgetManager, appWidgetId, appWidgetProviderInfo) { canBind ->
|
|
if (canBind) {
|
|
if (appWidgetProviderInfo.configure != null && !isInitialDrawAfterLaunch) {
|
|
activity.handleWidgetConfigureScreen(appWidgetHost, appWidgetId) { success ->
|
|
if (success) {
|
|
placeAppWidget(appWidgetId, appWidgetProviderInfo, item)
|
|
} else {
|
|
removeItemFromHomeScreen(item)
|
|
}
|
|
}
|
|
} else {
|
|
placeAppWidget(appWidgetId, appWidgetProviderInfo, item)
|
|
}
|
|
} else {
|
|
removeItemFromHomeScreen(item)
|
|
}
|
|
|
|
if (pager.isOutsideOfPageRange()) {
|
|
prevPage(redraw = true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun placeAppWidget(appWidgetId: Int, appWidgetProviderInfo: AppWidgetProviderInfo, item: HomeScreenGridItem) {
|
|
item.widgetId = appWidgetId
|
|
// we have to pass the base context here, else there will be errors with the themes
|
|
val widgetView = appWidgetHost.createView((context as MainActivity).baseContext, appWidgetId, appWidgetProviderInfo) as MyAppWidgetHostView
|
|
widgetView.tag = appWidgetId
|
|
widgetView.setAppWidget(appWidgetId, appWidgetProviderInfo)
|
|
widgetView.longPressListener = { x, y ->
|
|
val activity = context as? MainActivity
|
|
if (activity?.isAllAppsFragmentExpanded() == false) {
|
|
activity.showHomeIconMenu(x, widgetView.y, item, false)
|
|
performHapticFeedback()
|
|
}
|
|
}
|
|
|
|
widgetView.onIgnoreInterceptedListener = {
|
|
hideResizeLines()
|
|
}
|
|
|
|
val widgetSize = updateWidgetPositionAndSize(widgetView, item)
|
|
addView(widgetView, widgetSize.width, widgetSize.height)
|
|
widgetViews.add(widgetView)
|
|
|
|
// remove the drawable so that it gets refreshed on long press
|
|
item.drawable = null
|
|
// Delete existing windget if it has already been loaded to the list
|
|
gridItems.removeIf { it.id == item.id }
|
|
gridItems.add(item)
|
|
}
|
|
|
|
private fun updateWidgetPositionAndSize(widgetView: AppWidgetHostView, item: HomeScreenGridItem): Size {
|
|
val currentViewPosition = pager.getCurrentViewPositionInFullPageSpace() * width.toFloat()
|
|
val widgetPos = calculateWidgetPos(item.getTopLeft())
|
|
widgetView.x = widgetPos.x + width * item.page - currentViewPosition
|
|
widgetView.y = widgetPos.y.toFloat()
|
|
val widgetWidth = item.getWidthInCells() * cellWidth
|
|
val widgetHeight = item.getHeightInCells() * cellHeight
|
|
|
|
val density = context.resources.displayMetrics.density
|
|
val widgetDpWidth = (widgetWidth / density).toInt()
|
|
val widgetDpHeight = (widgetHeight / density).toInt()
|
|
|
|
if (isSPlus()) {
|
|
val sizes = listOf(SizeF(widgetDpWidth.toFloat(), widgetDpHeight.toFloat()))
|
|
widgetView.updateAppWidgetSize(Bundle(), sizes)
|
|
} else {
|
|
widgetView.updateAppWidgetSize(Bundle(), widgetDpWidth, widgetDpHeight, widgetDpWidth, widgetDpHeight)
|
|
}
|
|
|
|
widgetView.layoutParams?.width = widgetWidth
|
|
widgetView.layoutParams?.height = widgetHeight
|
|
return Size(widgetWidth, widgetHeight)
|
|
}
|
|
|
|
private fun calculateWidgetPos(topLeft: Point): Point {
|
|
val cell = cells[topLeft]!!
|
|
return Point(
|
|
cell.left + sideMargins.left,
|
|
cell.top + sideMargins.top
|
|
)
|
|
}
|
|
|
|
// convert stuff like 102x192 to grid cells like 0x1
|
|
private fun getClosestGridCells(center: Point): Point? {
|
|
return cells.entries.firstOrNull { (_, cell) -> center.x == cell.centerX() && center.y == cell.centerY() }?.key
|
|
}
|
|
|
|
private fun redrawGrid() {
|
|
post {
|
|
setWillNotDraw(false)
|
|
invalidate()
|
|
binding.drawingArea.invalidate()
|
|
}
|
|
}
|
|
|
|
private fun getFakeWidth() = width - sideMargins.left - sideMargins.right
|
|
|
|
private fun getFakeHeight() = height - sideMargins.top - sideMargins.bottom
|
|
|
|
fun drawInto(canvas: Canvas) {
|
|
if (cells.isEmpty()) {
|
|
fillCellSizes()
|
|
}
|
|
|
|
val currentXFactor = pager.getXFactorForCurrentPage()
|
|
val lastXFactor = pager.getXFactorForLastPage()
|
|
|
|
fun handleMainGridItemDrawing(item: HomeScreenGridItem, xFactor: Float) {
|
|
val offsetX = sideMargins.left + (this@HomeScreenGrid.width * xFactor).toInt()
|
|
val offsetY = sideMargins.top
|
|
cells[item.getTopLeft()]!!.withOffset(offsetX, offsetY) {
|
|
canvas.drawItemInCell(item, this)
|
|
}
|
|
}
|
|
|
|
gridItems.filter { it.isSingleCellType() && pager.isItemOnCurrentPage(it) && !it.docked && it.parentId == null }
|
|
.forEach { item ->
|
|
if (item.outOfBounds()) {
|
|
return@forEach
|
|
}
|
|
|
|
handleMainGridItemDrawing(item, currentXFactor)
|
|
}
|
|
gridItems.filter { it.isSingleCellType() && it.docked && it.parentId == null }
|
|
.forEach { item ->
|
|
if (item.outOfBounds()) {
|
|
return@forEach
|
|
}
|
|
|
|
handleMainGridItemDrawing(item, 0f)
|
|
}
|
|
if (pager.isAnimatingPageChange()) {
|
|
gridItems.filter { it.isSingleCellType() && pager.isItemOnLastPage(it) && !it.docked && it.parentId == null }
|
|
.forEach { item ->
|
|
if (item.outOfBounds()) {
|
|
return@forEach
|
|
}
|
|
|
|
handleMainGridItemDrawing(item, lastXFactor)
|
|
}
|
|
}
|
|
|
|
if (pager.isSwiped()) {
|
|
gridItems.filter {
|
|
it.isSingleCellType()
|
|
&& pager.isItemInSwipeRange(it)
|
|
&& !it.docked
|
|
&& it.parentId == null
|
|
}.forEach { item ->
|
|
if (item.outOfBounds()) {
|
|
return@forEach
|
|
}
|
|
|
|
handleMainGridItemDrawing(item, lastXFactor)
|
|
}
|
|
}
|
|
|
|
if (isFirstDraw) {
|
|
gridItems.filter { it.type == ITEM_TYPE_WIDGET && !it.outOfBounds() }.forEach { item ->
|
|
bindWidget(item, true)
|
|
}
|
|
} else {
|
|
gridItems.filter { it.type == ITEM_TYPE_WIDGET && !it.outOfBounds() }.forEach { item ->
|
|
widgetViews.firstOrNull { it.tag == item.widgetId }?.also {
|
|
updateWidgetPositionAndSize(it, item)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only draw page indicators when there is a need for it
|
|
if (pager.shouldDisplayPageChangeIndicator()) {
|
|
val pageCount = pager.getPageCount()
|
|
val pageIndicatorsRequiredWidth = pageCount * pageIndicatorRadius * 2 + pageCount * (pageIndicatorMargin - 1)
|
|
val usableWidth = getFakeWidth()
|
|
val pageIndicatorsStart = (usableWidth - pageIndicatorsRequiredWidth) / 2 + sideMargins.left
|
|
var currentPageIndicatorLeft = pageIndicatorsStart
|
|
val pageIndicatorY = pageIndicatorsYPos.toFloat() + sideMargins.top + iconMargin
|
|
val pageIndicatorStep = pageIndicatorRadius * 2 + pageIndicatorMargin
|
|
emptyPageIndicatorPaint.alpha = pager.getPageChangeIndicatorsAlpha()
|
|
// Draw empty page indicators
|
|
for (page in 0 until pageCount) {
|
|
canvas.drawCircle(currentPageIndicatorLeft + pageIndicatorRadius, pageIndicatorY, pageIndicatorRadius, emptyPageIndicatorPaint)
|
|
currentPageIndicatorLeft += pageIndicatorStep
|
|
}
|
|
|
|
// Draw current page indicator on exact position
|
|
val currentIndicatorPosition = pageIndicatorsStart + pager.getCurrentViewPositionInFullPageSpace() * pageIndicatorStep
|
|
currentPageIndicatorPaint.alpha = pager.getPageChangeIndicatorsAlpha()
|
|
canvas.drawCircle(currentIndicatorPosition + pageIndicatorRadius, pageIndicatorY, pageIndicatorRadius, currentPageIndicatorPaint)
|
|
}
|
|
|
|
val folder = currentlyOpenFolder
|
|
if (folder != null && folder.getItems().isNotEmpty()) {
|
|
val items = folder.getItems()
|
|
val folderRect = folder.getDrawingRect()
|
|
|
|
val currentViewPosition = pager.getCurrentViewPositionInFullPageSpace() * width.toFloat()
|
|
val rectOffset = width * folder.item.page - currentViewPosition
|
|
folderRect.offset(rectOffset, 0f)
|
|
|
|
canvas.withScale(folder.scale, folder.scale, folderRect.centerX(), folderRect.centerY()) {
|
|
canvas.drawRoundRect(folderRect, roundedCornerRadius / folder.scale, roundedCornerRadius / folder.scale, folderBackgroundPaint)
|
|
val textX = folderRect.left + folderPadding
|
|
val textY = folderRect.top + folderPadding
|
|
val staticLayout = StaticLayout.Builder
|
|
.obtain(
|
|
folder.item.title,
|
|
0,
|
|
folder.item.title.length,
|
|
folderTitleTextPaint,
|
|
(folderRect.width() - 2 * folderPadding * folder.scale).toInt()
|
|
)
|
|
.setMaxLines(1)
|
|
.setEllipsize(TextUtils.TruncateAt.END)
|
|
.setAlignment(Layout.Alignment.ALIGN_CENTER)
|
|
.build()
|
|
|
|
withTranslation(textX, textY) {
|
|
staticLayout.draw(canvas)
|
|
}
|
|
|
|
items.forEach { item ->
|
|
val itemRect = folder.getItemRect(item)
|
|
canvas.drawItemInCell(item, itemRect)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (draggedItem != null && draggedItemCurrentCoords.first != -1 && draggedItemCurrentCoords.second != -1) {
|
|
if (draggedItem!!.isSingleCellType()) {
|
|
if (folder != null && folder.getItemsDrawingRect().contains(
|
|
(draggedItemCurrentCoords.first).toFloat(),
|
|
(draggedItemCurrentCoords.second).toFloat()
|
|
)
|
|
) {
|
|
|
|
val center = folder.getItemsGridCenters().minBy {
|
|
abs(it.second - draggedItemCurrentCoords.first + sideMargins.left) + abs(it.third - draggedItemCurrentCoords.second + sideMargins.top)
|
|
}
|
|
val cellSize = folder.getCellSize()
|
|
|
|
val shadowX = center.second - cellSize / 2 + iconMargin + iconSize / 2f
|
|
val shadowY = center.third - cellSize / 2 + iconMargin + iconSize / 2
|
|
|
|
canvas.drawCircle(shadowX, shadowY.toFloat(), iconSize / 2f, dragShadowCirclePaint)
|
|
} else {
|
|
// draw a circle under the current cell
|
|
val center = gridCenters.minBy {
|
|
abs(it.x - draggedItemCurrentCoords.first + sideMargins.left) + abs(it.y - draggedItemCurrentCoords.second + sideMargins.top)
|
|
}
|
|
|
|
val gridCells = getClosestGridCells(center)
|
|
if (gridCells != null) {
|
|
val cell = cells[gridCells]!!
|
|
val shadowX = cell.left + iconMargin + iconSize / 2f + sideMargins.left
|
|
val shadowY = if (gridCells.y == rowCount - 1) {
|
|
cellHeight - iconMargin - iconSize / 2f
|
|
} else {
|
|
iconMargin + iconSize / 2f
|
|
} + sideMargins.top + cell.top
|
|
|
|
canvas.drawCircle(shadowX, shadowY, iconSize / 2f, dragShadowCirclePaint)
|
|
}
|
|
}
|
|
|
|
// show the app icon itself at dragging, move it above the finger a bit to make it visible
|
|
val drawableX = (draggedItemCurrentCoords.first - iconSize / 1.5f).toInt()
|
|
val drawableY = (draggedItemCurrentCoords.second - iconSize / 1.2f).toInt()
|
|
draggedItem!!.drawable?.setBounds(drawableX, drawableY, drawableX + iconSize, drawableY + iconSize)
|
|
draggedItem!!.drawable?.draw(canvas)
|
|
} else if (draggedItem!!.type == ITEM_TYPE_WIDGET) {
|
|
// at first draw we are loading the widget from the database at some exact spot, not dragging it
|
|
if (!isFirstDraw) {
|
|
val center = gridCenters.minBy {
|
|
Math.abs(it.x - draggedItemCurrentCoords.first + sideMargins.left) + Math.abs(it.y - draggedItemCurrentCoords.second + sideMargins.top)
|
|
}
|
|
|
|
val gridCells = getClosestGridCells(center)
|
|
if (gridCells != null) {
|
|
val widgetRect = getWidgetOccupiedRect(gridCells)
|
|
val widgetPos = calculateWidgetPos(Point(widgetRect.left, widgetRect.top))
|
|
val leftSide = widgetPos.x.toFloat()
|
|
val topSide = widgetPos.y.toFloat()
|
|
val rightSide = leftSide + draggedItem!!.getWidthInCells() * cellWidth
|
|
val bottomSide = topSide + draggedItem!!.getHeightInCells() * cellHeight
|
|
canvas.drawRoundRect(leftSide, topSide, rightSide, bottomSide, roundedCornerRadius, roundedCornerRadius, dragShadowCirclePaint)
|
|
}
|
|
|
|
// show the widget preview itself at dragging
|
|
draggedItem!!.drawable?.also { drawable ->
|
|
val aspectRatio = drawable.minimumHeight / drawable.minimumWidth.toFloat()
|
|
val drawableX = (draggedItemCurrentCoords.first - drawable.minimumWidth / 2f).toInt()
|
|
val drawableY = (draggedItemCurrentCoords.second - drawable.minimumHeight / 3f).toInt()
|
|
val drawableWidth = draggedItem!!.getWidthInCells() * cellWidth - iconMargin * (draggedItem!!.getWidthInCells() - 1)
|
|
drawable.setBounds(
|
|
drawableX,
|
|
drawableY,
|
|
drawableX + drawableWidth,
|
|
(drawableY + drawableWidth * aspectRatio).toInt()
|
|
)
|
|
drawable.draw(canvas)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
isFirstDraw = false
|
|
}
|
|
|
|
private fun fillCellSizes() {
|
|
cellWidth = getFakeWidth() / columnCount
|
|
cellHeight = getFakeHeight() / rowCount
|
|
val extraXMargin = if (cellWidth > cellHeight) {
|
|
(cellWidth - cellHeight) / 2
|
|
} else {
|
|
0
|
|
}
|
|
val extraYMargin = if (cellHeight > cellWidth) {
|
|
(cellHeight - cellWidth) / 2
|
|
} else {
|
|
0
|
|
}
|
|
iconSize = min(cellWidth, cellHeight) - 2 * iconMargin
|
|
pageIndicatorsYPos = (rowCount - 1) * cellHeight + extraYMargin
|
|
for (i in 0 until columnCount) {
|
|
for (j in 0 until rowCount) {
|
|
val yMarginToAdd = if (j == rowCount - 1) 0 else extraYMargin
|
|
val rect = Rect(
|
|
i * cellWidth + extraXMargin,
|
|
j * cellHeight + yMarginToAdd,
|
|
(i + 1) * cellWidth - extraXMargin,
|
|
(j + 1) * cellHeight - yMarginToAdd,
|
|
)
|
|
cells[Point(i, j)] = rect
|
|
gridCenters.add(Point(rect.centerX(), rect.centerY()))
|
|
if (j == rowCount - 1) {
|
|
dockCellY = j * cellHeight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun fragmentExpanded() {
|
|
widgetViews.forEach {
|
|
it.ignoreTouches = true
|
|
}
|
|
closeFolder(true)
|
|
}
|
|
|
|
fun fragmentCollapsed() {
|
|
widgetViews.forEach {
|
|
it.ignoreTouches = false
|
|
}
|
|
}
|
|
|
|
// get the clickable area around the icon, it includes text too
|
|
fun getClickableRect(item: HomeScreenGridItem): Rect {
|
|
if (cells.isEmpty()) {
|
|
fillCellSizes()
|
|
}
|
|
|
|
val folder = currentlyOpenFolder
|
|
val clickableLeft: Int
|
|
val clickableTop: Int
|
|
if (folder != null && item.parentId == folder.item.id) {
|
|
val itemRect = folder.getItemRect(item)
|
|
clickableLeft = itemRect.left
|
|
clickableTop = itemRect.top - iconMargin
|
|
} else {
|
|
val cell = cells[item.getTopLeft()]!!
|
|
clickableLeft = cell.left + sideMargins.left
|
|
clickableTop = if (item.docked) {
|
|
dockCellY + cellHeight - iconSize - iconMargin
|
|
} else {
|
|
cell.top - iconMargin
|
|
} + sideMargins.top
|
|
}
|
|
|
|
return Rect(clickableLeft, clickableTop, clickableLeft + iconSize + 2 * iconMargin, clickableTop + iconSize + 2 * iconMargin)
|
|
}
|
|
|
|
// drag the center of the widget, not the top left corner
|
|
private fun getWidgetOccupiedRect(item: Point): Rect {
|
|
val left = item.x - floor((draggedItem!!.getWidthInCells() - 1) / 2.0).toInt()
|
|
val rect = Rect(left, item.y, left + draggedItem!!.getWidthInCells() - 1, item.y + draggedItem!!.getHeightInCells() - 1)
|
|
if (rect.left < 0) {
|
|
rect.right -= rect.left
|
|
rect.left = 0
|
|
} else if (rect.right > columnCount - 1) {
|
|
val diff = rect.right - columnCount + 1
|
|
rect.right -= diff
|
|
rect.left -= diff
|
|
}
|
|
|
|
if (rect.top < 0) {
|
|
rect.bottom -= rect.top
|
|
rect.top = 0
|
|
} else if (rect.bottom > rowCount - 2) {
|
|
val diff = rect.bottom - rowCount + 2
|
|
rect.bottom -= diff
|
|
rect.top -= diff
|
|
}
|
|
|
|
return rect
|
|
}
|
|
|
|
fun isClickingGridItem(x: Int, y: Int): HomeScreenGridItem? {
|
|
if (pager.isAnimatingPageChange() || pager.isSwiped()) {
|
|
return null
|
|
}
|
|
|
|
currentlyOpenFolder?.also { folder ->
|
|
folder.getItems().forEach { gridItem ->
|
|
val rect = getClickableRect(gridItem)
|
|
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
|
return gridItem
|
|
}
|
|
}
|
|
}
|
|
|
|
for (gridItem in gridItems.filterVisibleOnCurrentPageOnly()) {
|
|
if (gridItem.outOfBounds()) {
|
|
continue
|
|
}
|
|
|
|
if (gridItem.isSingleCellType()) {
|
|
val rect = getClickableRect(gridItem)
|
|
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
|
return gridItem
|
|
}
|
|
} else if (gridItem.type == ITEM_TYPE_WIDGET) {
|
|
val widgetPos = calculateWidgetPos(gridItem.getTopLeft())
|
|
val left = widgetPos.x.toFloat()
|
|
val top = widgetPos.y.toFloat()
|
|
val right = left + gridItem.getWidthInCells() * cellWidth
|
|
val bottom = top + gridItem.getHeightInCells() * cellHeight
|
|
|
|
if (x >= left && x <= right && y >= top && y <= bottom) {
|
|
return gridItem
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
fun intoViewSpaceCoords(screenSpaceX: Float, screenSpaceY: Float): Pair<Float, Float> {
|
|
val viewLocation = IntArray(2)
|
|
getLocationOnScreen(viewLocation)
|
|
val x = screenSpaceX - viewLocation[0]
|
|
val y = screenSpaceY - viewLocation[1]
|
|
return Pair(x, y)
|
|
}
|
|
|
|
private fun HomeScreenGridItem.outOfBounds(): Boolean {
|
|
return (left >= columnCount || right >= columnCount || (!docked && (top >= rowCount - 1 || bottom >= rowCount - 1)))
|
|
}
|
|
|
|
private inner class HomeScreenGridTouchHelper(host: View) : ExploreByTouchHelper(host) {
|
|
override fun getVirtualViewAt(x: Float, y: Float): Int {
|
|
val item = isClickingGridItem(x.toInt(), y.toInt())
|
|
|
|
return if (item != null) {
|
|
item.id?.toInt() ?: INVALID_ID
|
|
} else {
|
|
INVALID_ID
|
|
}
|
|
}
|
|
|
|
override fun getVisibleVirtualViews(virtualViewIds: MutableList<Int>?) {
|
|
val sorted = gridItems.filterVisibleOnCurrentPageOnly().sortedBy {
|
|
it.getDockAdjustedTop(rowCount) * 100 + it.left
|
|
}
|
|
sorted.forEachIndexed { index, homeScreenGridItem ->
|
|
virtualViewIds?.add(index, homeScreenGridItem.id?.toInt() ?: index)
|
|
}
|
|
}
|
|
|
|
override fun onPopulateNodeForVirtualView(virtualViewId: Int, node: AccessibilityNodeInfoCompat) {
|
|
val item = gridItems.firstOrNull { it.id?.toInt() == virtualViewId } ?: throw IllegalArgumentException("Unknown id")
|
|
|
|
node.text = item.title
|
|
|
|
val viewLocation = IntArray(2)
|
|
getLocationOnScreen(viewLocation)
|
|
|
|
val viewBounds = getClickableRect(item)
|
|
val onScreenBounds = Rect(viewBounds)
|
|
onScreenBounds.offset(viewLocation[0], viewLocation[1])
|
|
node.setBoundsInScreen(onScreenBounds)
|
|
node.setBoundsInParent(viewBounds)
|
|
|
|
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK)
|
|
node.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK)
|
|
node.setParent(this@HomeScreenGrid)
|
|
}
|
|
|
|
override fun onPerformActionForVirtualView(virtualViewId: Int, action: Int, arguments: Bundle?): Boolean {
|
|
val item = gridItems.firstOrNull { it.id?.toInt() == virtualViewId } ?: throw IllegalArgumentException("Unknown id")
|
|
when (action) {
|
|
AccessibilityNodeInfoCompat.ACTION_CLICK -> itemClickListener?.apply {
|
|
invoke(item)
|
|
return true
|
|
}
|
|
|
|
AccessibilityNodeInfoCompat.ACTION_LONG_CLICK -> itemLongClickListener?.apply {
|
|
invoke(item)
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
private fun deletePage(page: Int) {
|
|
gridItems.filter { it.page > page }.forEach {
|
|
it.page -= 1
|
|
}
|
|
context.homeScreenGridItemsDB.shiftPage(page, -1)
|
|
|
|
if (pager.isOutsideOfPageRange()) {
|
|
post {
|
|
prevPage()
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getMaxPage() = gridItems.filter { !it.docked && !it.outOfBounds() }.maxOfOrNull { it.page } ?: 0
|
|
|
|
fun nextPage(redraw: Boolean = false): Boolean {
|
|
return pager.nextPage(redraw)
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
fun openFolder(folder: HomeScreenGridItem) {
|
|
if (currentlyOpenFolder == null) {
|
|
currentlyOpenFolder = folder.toFolder(animateOpening = true)
|
|
redrawGrid()
|
|
} else if (currentlyOpenFolder?.item?.id != folder.id) {
|
|
closeFolder()
|
|
}
|
|
}
|
|
|
|
fun closeFolder(redraw: Boolean = false) {
|
|
currentlyOpenFolder?.animateClosing {
|
|
currentlyOpenFolder = null
|
|
if (redraw) {
|
|
redrawGrid()
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun Canvas.drawItemInCell(item: HomeScreenGridItem, cell: Rect) {
|
|
if (item.id != draggedItem?.id) {
|
|
val drawableX = cell.left + iconMargin
|
|
|
|
val drawable = if (item.type == ITEM_TYPE_FOLDER) {
|
|
item.toFolder().generateDrawable()
|
|
} else {
|
|
item.drawable
|
|
}
|
|
|
|
if (item.docked) {
|
|
val drawableY = dockCellY + cellHeight - iconMargin - iconSize + sideMargins.top
|
|
|
|
drawable?.setBounds(drawableX, drawableY, drawableX + iconSize, drawableY + iconSize)
|
|
} else {
|
|
val drawableY = cell.top + iconMargin
|
|
drawable?.setBounds(drawableX, drawableY, drawableX + iconSize, drawableY + iconSize)
|
|
|
|
if (item.id != draggedItem?.id && item.title.isNotEmpty()) {
|
|
val textX = cell.left.toFloat() + labelSideMargin
|
|
val textY = cell.top.toFloat() + iconSize + iconMargin + labelSideMargin
|
|
val textPaintToUse = if (item.parentId == null) {
|
|
textPaint
|
|
} else {
|
|
contrastTextPaint
|
|
}
|
|
val staticLayout = StaticLayout.Builder
|
|
.obtain(item.title, 0, item.title.length, textPaintToUse, cellWidth - 2 * labelSideMargin)
|
|
.setMaxLines(2)
|
|
.setEllipsize(TextUtils.TruncateAt.END)
|
|
.setAlignment(Layout.Alignment.ALIGN_CENTER)
|
|
.build()
|
|
|
|
withTranslation(textX, textY) {
|
|
staticLayout.draw(this)
|
|
}
|
|
}
|
|
}
|
|
|
|
drawable?.draw(this)
|
|
}
|
|
}
|
|
|
|
private fun Rect.withOffset(offsetX: Int, offsetY: Int, block: Rect.() -> Unit) {
|
|
offset(offsetX, offsetY)
|
|
try {
|
|
block()
|
|
} finally {
|
|
offset(-offsetX, -offsetY)
|
|
}
|
|
}
|
|
|
|
private fun ArrayList<HomeScreenGridItem>.filterVisibleOnCurrentPageOnly() = filter { it.visibleOnCurrentPage() }
|
|
|
|
private fun HomeScreenGridItem.visibleOnCurrentPage() = (pager.isItemOnCurrentPage(this) || docked) && parentId == null
|
|
|
|
private fun HomeScreenGridItem.isSingleCellType() = (type == ITEM_TYPE_ICON || type == ITEM_TYPE_SHORTCUT || type == ITEM_TYPE_FOLDER)
|
|
|
|
private fun HomeScreenGridItem.toFolder(animateOpening: Boolean = false) = HomeScreenFolder(this, animateOpening)
|
|
|
|
private inner class HomeScreenFolder(
|
|
val item: HomeScreenGridItem,
|
|
animateOpening: Boolean
|
|
) {
|
|
var scale: Float = 1f
|
|
private var closing = false
|
|
|
|
init {
|
|
if (animateOpening) {
|
|
scale = 0f
|
|
post {
|
|
ValueAnimator.ofFloat(0f, 1f)
|
|
.apply {
|
|
interpolator = DecelerateInterpolator()
|
|
addUpdateListener {
|
|
scale = it.animatedValue as Float
|
|
redrawGrid()
|
|
}
|
|
addListener(object : AnimatorListenerAdapter() {
|
|
override fun onAnimationEnd(animation: Animator) {
|
|
super.onAnimationEnd(animation)
|
|
scale = 1f
|
|
redrawGrid()
|
|
}
|
|
})
|
|
duration = FOLDER_ANIMATION_DURATION
|
|
start()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun getItems() =
|
|
gridItems.filter { it.isSingleCellType() && it.parentId == item.id }
|
|
|
|
fun generateDrawable(): Drawable? {
|
|
if (iconSize == 0) {
|
|
return null
|
|
}
|
|
|
|
val items = getItems()
|
|
val itemsCount = getItems().count()
|
|
|
|
if (itemsCount == 0) {
|
|
return null
|
|
}
|
|
|
|
val bitmap = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888)
|
|
val canvas = Canvas(bitmap)
|
|
val circlePath = Path().apply { addCircle((iconSize / 2).toFloat(), (iconSize / 2).toFloat(), (iconSize / 2).toFloat(), Path.Direction.CCW) }
|
|
canvas.clipPath(circlePath)
|
|
canvas.drawPaint(folderIconBackgroundPaint)
|
|
val folderColumnCount = ceil(sqrt(itemsCount.toDouble())).roundToInt()
|
|
val folderRowCount = ceil(itemsCount.toFloat() / folderColumnCount).roundToInt()
|
|
val scaledCellSize = (iconSize.toFloat() / folderColumnCount)
|
|
val scaledGap = scaledCellSize / 5f
|
|
val scaledIconSize = (iconSize - (folderColumnCount + 1) * scaledGap) / folderColumnCount
|
|
val extraYMargin = if (folderRowCount < folderColumnCount) (scaledIconSize + scaledGap) / 2 else 0f
|
|
items.forEach {
|
|
val (row, column) = getItemPosition(it)
|
|
val drawableX = (scaledGap + column * scaledIconSize + column * scaledGap).toInt()
|
|
val drawableY = (extraYMargin + scaledGap + row * scaledIconSize + row * scaledGap).toInt()
|
|
val newDrawable = it.drawable?.constantState?.newDrawable()?.mutate()
|
|
newDrawable?.setBounds(drawableX, drawableY, drawableX + scaledIconSize.toInt(), drawableY + scaledIconSize.toInt())
|
|
newDrawable?.draw(canvas)
|
|
}
|
|
canvas.drawPath(circlePath, folderIconBorderPaint)
|
|
return BitmapDrawable(resources, bitmap)
|
|
}
|
|
|
|
fun getDrawingRect(): RectF {
|
|
val count = getItems().count()
|
|
if (count == 0) {
|
|
return RectF(0f, 0f, 0f, 0f)
|
|
}
|
|
val columnsCount = ceil(sqrt(count.toDouble())).toInt()
|
|
val rowsCount = ceil(count.toFloat() / columnsCount).toInt()
|
|
val cellSize = getCellSize()
|
|
val gap = getGapSize()
|
|
val yGap = gap + textPaint.textSize + 2 * labelSideMargin
|
|
val cell = cells[item.getTopLeft()]!!
|
|
val centerX = sideMargins.left + cell.centerX()
|
|
val centerY = sideMargins.top + cell.centerY()
|
|
val folderDialogWidth = columnsCount * cellSize + 2 * folderPadding + (columnsCount - 1) * gap
|
|
val folderDialogHeight = rowsCount * cellSize + 3 * folderPadding + folderTitleTextPaint.textSize + rowsCount * yGap
|
|
var folderDialogTop = centerY - folderDialogHeight / 2
|
|
var folderDialogLeft = centerX - folderDialogWidth / 2
|
|
|
|
if (folderDialogLeft < left + sideMargins.left) {
|
|
folderDialogLeft += left + sideMargins.left - folderDialogLeft
|
|
}
|
|
if (folderDialogLeft + folderDialogWidth > right - sideMargins.right) {
|
|
folderDialogLeft -= folderDialogLeft + folderDialogWidth - (right - sideMargins.right)
|
|
}
|
|
if (folderDialogTop < top + sideMargins.top) {
|
|
folderDialogTop += top + sideMargins.top - folderDialogTop
|
|
}
|
|
if (folderDialogTop + folderDialogHeight > bottom - sideMargins.bottom) {
|
|
folderDialogTop -= folderDialogTop + folderDialogHeight - (bottom - sideMargins.bottom)
|
|
}
|
|
|
|
return RectF(folderDialogLeft, folderDialogTop, folderDialogLeft + folderDialogWidth, folderDialogTop + folderDialogHeight)
|
|
}
|
|
|
|
fun getItemsDrawingRect(): RectF {
|
|
val folderRect = getDrawingRect()
|
|
return RectF(
|
|
folderRect.left + folderPadding,
|
|
folderRect.top + folderPadding * 2 + folderTitleTextPaint.textSize,
|
|
folderRect.right - folderPadding,
|
|
folderRect.bottom - folderPadding
|
|
)
|
|
}
|
|
|
|
fun getItemsGridCenters(): List<Triple<Int, Int, Int>> {
|
|
val count = getItems().count()
|
|
val columnsCount = ceil(sqrt(count.toDouble())).roundToInt()
|
|
val rowsCount = ceil(count.toFloat() / columnsCount).roundToInt()
|
|
val folderItemsRect = getItemsDrawingRect()
|
|
val cellSize = getCellSize()
|
|
val gap = getGapSize()
|
|
val yGap = gap + textPaint.textSize + 2 * labelSideMargin
|
|
return (0 until columnsCount * rowsCount)
|
|
.toList()
|
|
.map { Pair(it % columnsCount, it / columnsCount) }
|
|
.mapIndexed { index, (x, y) ->
|
|
Triple(
|
|
index,
|
|
(folderItemsRect.left + x * cellSize + x * gap + cellSize / 2).toInt(),
|
|
(folderItemsRect.top + y * cellSize + y * yGap + cellSize / 2).toInt()
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun getItemPosition(item: HomeScreenGridItem): Pair<Int, Int> {
|
|
val count = getItems().count()
|
|
val columnsCount = ceil(sqrt(count.toDouble())).roundToInt()
|
|
val column = item.left % columnsCount
|
|
val row = item.left / columnsCount
|
|
return Pair(row, column)
|
|
}
|
|
|
|
fun getItemRect(item: HomeScreenGridItem): Rect {
|
|
val (row, column) = getItemPosition(item)
|
|
val itemsRect = getItemsDrawingRect()
|
|
val cellSize = getCellSize()
|
|
val gapSize = getGapSize()
|
|
val yGapSize = gapSize + textPaint.textSize + 2 * labelSideMargin
|
|
val left = (itemsRect.left + column * cellSize + column * gapSize).roundToInt()
|
|
val top = (itemsRect.top + row * cellSize + row * yGapSize).roundToInt()
|
|
return Rect(
|
|
left,
|
|
top,
|
|
left + cellSize,
|
|
top + cellSize
|
|
)
|
|
}
|
|
|
|
fun animateClosing(callback: () -> Unit) {
|
|
post {
|
|
if (closing) {
|
|
return@post
|
|
}
|
|
closing = true
|
|
ValueAnimator.ofFloat(scale, 0.2f)
|
|
.apply {
|
|
interpolator = DecelerateInterpolator()
|
|
addUpdateListener {
|
|
scale = it.animatedValue as Float
|
|
redrawGrid()
|
|
}
|
|
addListener(object : AnimatorListenerAdapter() {
|
|
override fun onAnimationEnd(animation: Animator) {
|
|
super.onAnimationEnd(animation)
|
|
scale = 0.2f
|
|
callback()
|
|
}
|
|
})
|
|
duration = (FOLDER_ANIMATION_DURATION * (max(0f, scale - 0.2f))).toLong()
|
|
start()
|
|
}
|
|
}
|
|
}
|
|
|
|
fun getCellSize(): Int = min(cellWidth, cellHeight)
|
|
|
|
private fun getGapSize(): Float {
|
|
val cellSize = getCellSize()
|
|
return if (cellSize == cellWidth) {
|
|
0f
|
|
} else {
|
|
cellSize / 5f
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val FOLDER_OPEN_HOLD_THRESHOLD = 500L
|
|
private const val FOLDER_CLOSE_HOLD_THRESHOLD = 300L
|
|
private const val FOLDER_ANIMATION_DURATION = 200L
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
private val pageChangeStarted: () -> Unit
|
|
) {
|
|
|
|
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)
|
|
pageChangeStarted()
|
|
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 {
|
|
if (currentPage < getMaxPage() && pageChangeEnabled) {
|
|
lastPage = currentPage
|
|
currentPage++
|
|
handlePageChange(redraw)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
fun prevPage(redraw: Boolean = false): Boolean {
|
|
if (currentPage > 0 && pageChangeEnabled) {
|
|
lastPage = currentPage
|
|
currentPage--
|
|
handlePageChange(redraw)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
fun skipToPage(targetPage: Int): Boolean {
|
|
if (currentPage != targetPage && targetPage < getMaxPage() + 1) {
|
|
lastPage = currentPage
|
|
currentPage = targetPage
|
|
handlePageChange()
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
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) {
|
|
pageChangeEnabled = false
|
|
pageChangeIndicatorsAlpha = 0f
|
|
pageChangeStarted()
|
|
val startingAt = 1 - abs(pageChangeSwipedPercentage)
|
|
pageChangeSwipedPercentage = 0f
|
|
getHandler().removeCallbacks(startFadingIndicators)
|
|
pageChangeStarted()
|
|
if (redraw) {
|
|
redrawGrid()
|
|
}
|
|
ValueAnimator.ofFloat(startingAt, 0f)
|
|
.apply {
|
|
interpolator = OvershootInterpolator(1f)
|
|
addUpdateListener {
|
|
if (it.animatedValue != 0f) {
|
|
pageChangeAnimLeftPercentage = it.animatedValue as Float
|
|
redrawGrid()
|
|
}
|
|
}
|
|
addListener(object : AnimatorListenerAdapter() {
|
|
override fun onAnimationEnd(animation: Animator) {
|
|
super.onAnimationEnd(animation)
|
|
pageChangeAnimLeftPercentage = 0f
|
|
pageChangeEnabled = true
|
|
lastPage = currentPage
|
|
clearPageChangeFlags()
|
|
scheduleIndicatorsFade()
|
|
redrawGrid()
|
|
}
|
|
})
|
|
start()
|
|
}
|
|
}
|
|
}
|
|
|