553 lines
23 KiB
Kotlin

package com.simplemobiletools.launcher.views
import android.annotation.SuppressLint
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.BitmapDrawable
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextUtils
import android.util.AttributeSet
import android.widget.RelativeLayout
import com.simplemobiletools.commons.extensions.navigationBarHeight
import com.simplemobiletools.commons.extensions.performHapticFeedback
import com.simplemobiletools.commons.extensions.statusBarHeight
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.launcher.R
import com.simplemobiletools.launcher.activities.MainActivity
import com.simplemobiletools.launcher.extensions.getDrawableForPackageName
import com.simplemobiletools.launcher.extensions.homeScreenGridItemsDB
import com.simplemobiletools.launcher.helpers.*
import com.simplemobiletools.launcher.models.HomeScreenGridItem
class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : RelativeLayout(context, attrs, defStyle) {
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0)
private var iconMargin = context.resources.getDimension(R.dimen.icon_side_margin).toInt()
private var labelSideMargin = context.resources.getDimension(R.dimen.small_margin).toInt()
private var roundedCornerRadius = context.resources.getDimension(R.dimen.activity_margin)
private var textPaint: TextPaint
private var dragShadowCirclePaint: Paint
private var draggedItem: HomeScreenGridItem? = null
private var isFirstDraw = true
// let's use a 6x5 grid for now with 1 special row at the bottom, prefilled with default apps
private var rowXCoords = ArrayList<Int>(COLUMN_COUNT)
private var rowYCoords = ArrayList<Int>(ROW_COUNT)
private var rowWidth = 0
private var rowHeight = 0
private var iconSize = 0
// apply fake margins at the home screen. Real ones would cause the icons be cut at dragging at screen sides
private var sideMargins = Rect()
private var gridItems = ArrayList<HomeScreenGridItem>()
private var gridCenters = ArrayList<Pair<Int, Int>>()
private var draggedItemCurrentCoords = Pair(-1, -1)
private var widgetViews = ArrayList<MyAppWidgetHostView>()
val appWidgetHost = MyAppWidgetHost(context, WIDGET_HOST_ID)
private val appWidgetManager = AppWidgetManager.getInstance(context)
init {
textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = context.resources.getDimension(R.dimen.smaller_text_size)
setShadowLayer(.5f, 0f, 0f, Color.BLACK)
}
dragShadowCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.resources.getColor(R.color.light_grey_stroke)
strokeWidth = context.resources.getDimension(R.dimen.small_margin)
style = Paint.Style.STROKE
}
val sideMargin = context.resources.getDimension(R.dimen.normal_margin).toInt()
sideMargins.apply {
top = context.statusBarHeight
bottom = context.navigationBarHeight
left = sideMargin
right = sideMargin
}
fetchGridItems()
}
fun fetchGridItems() {
ensureBackgroundThread {
gridItems = context.homeScreenGridItemsDB.getAllItems() as ArrayList<HomeScreenGridItem>
gridItems.forEach { item ->
if (item.type == ITEM_TYPE_ICON) {
item.drawable = context.getDrawableForPackageName(item.packageName)
}
}
redrawGrid()
}
}
fun removeAppIcon(item: HomeScreenGridItem) {
ensureBackgroundThread {
removeItemFromHomeScreen(item)
post {
removeView(widgetViews.firstOrNull { it.tag == item.widgetId })
}
gridItems.removeIf { it.id == item.id }
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)
}
}
}
fun itemDraggingStarted(draggedGridItem: HomeScreenGridItem) {
draggedItem = draggedGridItem
if (draggedItem!!.drawable == null) {
draggedItem!!.drawable = context.getDrawableForPackageName(draggedGridItem.packageName)
}
redrawGrid()
}
fun draggedItemMoved(x: Int, y: Int) {
if (draggedItem == null) {
return
}
draggedItemCurrentCoords = Pair(x, y)
redrawGrid()
}
// figure out at which cell was the item dropped, if it is empty
fun itemDraggingStopped() {
if (draggedItem == null) {
return
}
when (draggedItem!!.type) {
ITEM_TYPE_ICON -> addAppIcon()
ITEM_TYPE_WIDGET -> addWidget()
ITEM_TYPE_SHORTCUT -> {
// replace this with real shortcut handling
draggedItem = null
redrawGrid()
}
}
}
private fun addAppIcon() {
val center = gridCenters.minBy {
Math.abs(it.first - draggedItemCurrentCoords.first + sideMargins.left) + Math.abs(it.second - draggedItemCurrentCoords.second + sideMargins.top)
}
var redrawIcons = false
val gridCells = getClosestGridCells(center)
if (gridCells != null) {
val xIndex = gridCells.first
val yIndex = gridCells.second
// check if the destination cell is empty
var areAllCellsEmpty = true
val wantedCell = Pair(xIndex, yIndex)
gridItems.forEach { item ->
for (xCell in item.left until item.right) {
for (yCell in item.top until item.bottom) {
val cell = Pair(xCell, yCell)
val isAnyCellOccupied = wantedCell == cell
if (isAnyCellOccupied) {
areAllCellsEmpty = false
return@forEach
}
}
}
}
if (areAllCellsEmpty) {
val draggedHomeGridItem = gridItems.firstOrNull { it.id == draggedItem?.id }
// we are moving an existing home screen item from one place to another
if (draggedHomeGridItem != null) {
draggedHomeGridItem.apply {
left = xIndex
top = yIndex
right = xIndex + 1
bottom = yIndex + 1
ensureBackgroundThread {
context.homeScreenGridItemsDB.updateItemPosition(left, top, right, bottom, id!!)
}
}
redrawIcons = true
} else if (draggedItem != null) {
// we are dragging a new item at the home screen from the All Apps fragment
val newHomeScreenGridItem = HomeScreenGridItem(
null,
xIndex,
yIndex,
xIndex + 1,
yIndex + 1,
1,
1,
draggedItem!!.packageName,
draggedItem!!.title,
draggedItem!!.type,
"",
-1,
draggedItem!!.drawable
)
ensureBackgroundThread {
val newId = context.homeScreenGridItemsDB.insert(newHomeScreenGridItem)
newHomeScreenGridItem.id = newId
gridItems.add(newHomeScreenGridItem)
redrawGrid()
}
}
} else {
performHapticFeedback()
redrawIcons = true
}
}
draggedItem = null
draggedItemCurrentCoords = Pair(-1, -1)
if (redrawIcons) {
redrawGrid()
}
}
private fun addWidget() {
val center = gridCenters.minBy {
Math.abs(it.first - draggedItemCurrentCoords.first + sideMargins.left) + Math.abs(it.second - 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 until widgetRect.right) {
for (yCell in widgetRect.top until widgetRect.bottom) {
widgetTargetCells.add(Pair(xCell, yCell))
}
}
var areAllCellsEmpty = true
gridItems.filter { it.id != draggedItem?.id }.forEach { item ->
for (xCell in item.left until item.right) {
for (yCell in item.top until item.bottom) {
val cell = Pair(xCell, yCell)
val isAnyCellOccupied = widgetTargetCells.contains(cell)
if (isAnyCellOccupied) {
areAllCellsEmpty = false
return@forEach
}
}
}
}
if (areAllCellsEmpty) {
val widgetItem = draggedItem!!.copy()
widgetItem.apply {
left = widgetRect.left
top = widgetRect.top
right = widgetRect.right
bottom = widgetRect.bottom
}
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, widgetItem.id!!)
val widgetView = widgetViews.firstOrNull { it.tag == widgetItem.widgetId }
if (widgetView != null) {
widgetView.x = calculateWidgetX(widgetItem.left)
widgetView.y = calculateWidgetY(widgetItem.top)
}
gridItems.firstOrNull { it.id == widgetItem.id }?.apply {
left = widgetItem.left
right = widgetItem.right
top = widgetItem.top
bottom = widgetItem.bottom
}
}
}
} else {
performHapticFeedback()
}
}
draggedItem = null
draggedItemCurrentCoords = Pair(-1, -1)
redrawGrid()
}
private fun bindWidget(item: HomeScreenGridItem, isInitialDrawAfterLaunch: Boolean) {
val activity = context as MainActivity
val infoList = appWidgetManager!!.installedProviders
val appWidgetProviderInfo = infoList.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)
}
}
}
}
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 yOffset = resources.getDimension(R.dimen.home_long_press_anchor_offset_y)
(context as? MainActivity)?.showHomeIconMenu(x, widgetView.y - yOffset, item, false)
val gridItem = gridItems.firstOrNull { it.widgetId == appWidgetId }
if (gridItem != null) {
widgetView.buildDrawingCache()
gridItem.drawable = BitmapDrawable(widgetView.drawingCache)
}
}
widgetView.x = calculateWidgetX(item.left)
widgetView.y = calculateWidgetY(item.top)
val widgetWidth = item.widthCells * rowWidth
val widgetHeight = item.heightCells * rowHeight
addView(widgetView, widgetWidth, widgetHeight)
widgetViews.add(widgetView)
// remove the drawable so that it gets refreshed on long press
item.drawable = null
gridItems.add(item)
}
private fun calculateWidgetX(leftCell: Int) = leftCell * rowWidth + sideMargins.left.toFloat()
private fun calculateWidgetY(topCell: Int) = topCell * rowHeight + sideMargins.top.toFloat()
// convert stuff like 102x192 to grid cells like 0x1
private fun getClosestGridCells(center: Pair<Int, Int>): Pair<Int, Int>? {
rowXCoords.forEachIndexed { xIndex, xCell ->
rowYCoords.forEachIndexed { yIndex, yCell ->
if (xCell + rowWidth / 2 == center.first && yCell + rowHeight / 2 == center.second) {
return Pair(xIndex, yIndex)
}
}
}
return null
}
private fun redrawGrid() {
post {
setWillNotDraw(false)
invalidate()
}
}
private fun getFakeWidth() = width - sideMargins.left - sideMargins.right
private fun getFakeHeight() = height - sideMargins.top - sideMargins.bottom
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (rowXCoords.isEmpty()) {
rowWidth = getFakeWidth() / COLUMN_COUNT
rowHeight = getFakeHeight() / ROW_COUNT
iconSize = rowWidth - 2 * iconMargin
for (i in 0 until COLUMN_COUNT) {
rowXCoords.add(i, i * rowWidth)
}
for (i in 0 until ROW_COUNT) {
rowYCoords.add(i, i * rowHeight)
}
rowXCoords.forEach { x ->
rowYCoords.forEach { y ->
gridCenters.add(Pair(x + rowWidth / 2, y + rowHeight / 2))
}
}
}
gridItems.filter { it.drawable != null && it.type == ITEM_TYPE_ICON }.forEach { item ->
if (item.id != draggedItem?.id) {
val drawableX = rowXCoords[item.left] + iconMargin + sideMargins.left
// icons at the bottom are drawn at the bottom of the grid and they have no label
if (item.top == ROW_COUNT - 1) {
val drawableY = rowYCoords[item.top] + rowHeight - iconSize - iconMargin * 2 + sideMargins.top
item.drawable!!.setBounds(drawableX, drawableY, drawableX + iconSize, drawableY + iconSize)
} else {
val drawableY = rowYCoords[item.top] + iconSize / 2 + sideMargins.top
item.drawable!!.setBounds(drawableX, drawableY, drawableX + iconSize, drawableY + iconSize)
if (item.id != draggedItem?.id && item.title.isNotEmpty()) {
val textX = rowXCoords[item.left].toFloat() + labelSideMargin + sideMargins.left
val textY = rowYCoords[item.top] + iconSize * 1.5f + labelSideMargin + sideMargins.top
val staticLayout = StaticLayout.Builder
.obtain(item.title, 0, item.title.length, textPaint, rowWidth - 2 * labelSideMargin)
.setMaxLines(2)
.setEllipsize(TextUtils.TruncateAt.END)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.build()
canvas.save()
canvas.translate(textX, textY)
staticLayout.draw(canvas)
canvas.restore()
}
}
item.drawable!!.draw(canvas)
}
}
if (isFirstDraw) {
gridItems.filter { it.type == ITEM_TYPE_WIDGET }.forEach { item ->
bindWidget(item, true)
}
}
if (draggedItem != null && draggedItemCurrentCoords.first != -1 && draggedItemCurrentCoords.second != -1) {
if (draggedItem!!.type == ITEM_TYPE_ICON || draggedItem!!.type == ITEM_TYPE_SHORTCUT) {
// draw a circle under the current cell
val center = gridCenters.minBy {
Math.abs(it.first - draggedItemCurrentCoords.first + sideMargins.left) + Math.abs(it.second - draggedItemCurrentCoords.second + sideMargins.top)
}
val gridCells = getClosestGridCells(center)
if (gridCells != null) {
val shadowX = rowXCoords[gridCells.first] + iconMargin.toFloat() + iconSize / 2 + sideMargins.left
val shadowY = if (gridCells.second == ROW_COUNT - 1) {
rowYCoords[gridCells.second] + rowHeight - iconSize / 2 - iconMargin * 2
} else {
rowYCoords[gridCells.second] + iconSize
} + sideMargins.top
canvas.drawCircle(shadowX, shadowY.toFloat(), 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.first - draggedItemCurrentCoords.first + sideMargins.left) + Math.abs(it.second - draggedItemCurrentCoords.second + sideMargins.top)
}
val gridCells = getClosestGridCells(center)
if (gridCells != null) {
val widgetRect = getWidgetOccupiedRect(gridCells)
val leftSide = widgetRect.left * rowWidth + sideMargins.left + iconMargin.toFloat()
val topSide = widgetRect.top * rowHeight + sideMargins.top + iconMargin.toFloat()
val rightSide = leftSide + draggedItem!!.widthCells * rowWidth - sideMargins.right - iconMargin.toFloat()
val bottomSide = topSide + draggedItem!!.heightCells * rowHeight - sideMargins.top
canvas.drawRoundRect(leftSide, topSide, rightSide, bottomSide, roundedCornerRadius, roundedCornerRadius, dragShadowCirclePaint)
}
// show the widget preview itself at dragging
val drawable = draggedItem!!.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!!.widthCells * rowWidth - iconMargin * (draggedItem!!.widthCells - 1)
drawable.setBounds(
drawableX,
drawableY,
drawableX + drawableWidth,
(drawableY + drawableWidth * aspectRatio).toInt()
)
drawable.draw(canvas)
}
}
}
isFirstDraw = false
}
// get the clickable area around the icon, it includes text too
private fun getClickableRect(item: HomeScreenGridItem): Rect {
val clickableLeft = item.left * rowWidth + sideMargins.left
val clickableTop = rowYCoords[item.top] + iconSize / 3 + sideMargins.top
return Rect(clickableLeft, clickableTop, clickableLeft + rowWidth, clickableTop + iconSize * 2)
}
// drag the center of the widget, not the top left corner
private fun getWidgetOccupiedRect(item: Pair<Int, Int>): Rect {
val left = item.first - Math.floor((draggedItem!!.widthCells - 1) / 2.0).toInt()
val rect = Rect(left, item.second, left + draggedItem!!.widthCells, item.second + draggedItem!!.heightCells)
if (rect.left < 0) {
rect.right -= rect.left
rect.left = 0
} else if (rect.right > COLUMN_COUNT) {
val diff = rect.right - COLUMN_COUNT
rect.right -= diff
rect.left -= diff
}
// do not allow placing widgets at the bottom row, that is for pinned default apps
if (rect.bottom >= ROW_COUNT) {
val diff = rect.bottom - ROW_COUNT + 1
rect.bottom -= diff
rect.top -= diff
}
return rect
}
fun isClickingGridItem(x: Int, y: Int): HomeScreenGridItem? {
for (gridItem in gridItems) {
val rect = getClickableRect(gridItem)
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
return gridItem
}
}
return null
}
}