diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/activities/HiddenIconsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/activities/HiddenIconsActivity.kt index c45dc41..6c036a5 100644 --- a/app/src/main/kotlin/com/simplemobiletools/launcher/activities/HiddenIconsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/activities/HiddenIconsActivity.kt @@ -5,6 +5,7 @@ import android.content.pm.PackageManager import android.os.Bundle import com.simplemobiletools.commons.extensions.beVisibleIf import com.simplemobiletools.commons.extensions.normalizeString +import com.simplemobiletools.commons.extensions.viewBinding import com.simplemobiletools.commons.helpers.NavigationIcon import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener @@ -17,12 +18,11 @@ import com.simplemobiletools.launcher.extensions.hiddenIconsDB import com.simplemobiletools.launcher.models.HiddenIcon class HiddenIconsActivity : SimpleActivity(), RefreshRecyclerViewListener { - private lateinit var binding: ActivityHiddenIconsBinding + private val binding by viewBinding(ActivityHiddenIconsBinding::inflate) override fun onCreate(savedInstanceState: Bundle?) { isMaterialActivity = true super.onCreate(savedInstanceState) - binding = ActivityHiddenIconsBinding.inflate(layoutInflater) setContentView(binding.root) updateIcons() diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/activities/MainActivity.kt index 327ca4e..88b971a 100644 --- a/app/src/main/kotlin/com/simplemobiletools/launcher/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/activities/MainActivity.kt @@ -46,6 +46,7 @@ import com.simplemobiletools.launcher.extensions.* import com.simplemobiletools.launcher.fragments.MyFragment import com.simplemobiletools.launcher.helpers.* import com.simplemobiletools.launcher.interfaces.FlingListener +import com.simplemobiletools.launcher.interfaces.ItemMenuListener import com.simplemobiletools.launcher.models.AppLauncher import com.simplemobiletools.launcher.models.HiddenIcon import com.simplemobiletools.launcher.models.HomeScreenGridItem @@ -72,7 +73,7 @@ class MainActivity : SimpleActivity(), FlingListener { private var wasJustPaused: Boolean = false private lateinit var mDetector: GestureDetectorCompat - private lateinit var binding: ActivityMainBinding + private val binding by viewBinding(ActivityMainBinding::inflate) companion object { private var mLastUpEvent = 0L @@ -84,7 +85,6 @@ class MainActivity : SimpleActivity(), FlingListener { useDynamicTheme = false super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) appLaunched(BuildConfig.APPLICATION_ID) @@ -132,79 +132,6 @@ class MainActivity : SimpleActivity(), FlingListener { } } - private fun handleIntentAction(intent: Intent) { - if (intent.action == LauncherApps.ACTION_CONFIRM_PIN_SHORTCUT) { - val launcherApps = applicationContext.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val item = launcherApps.getPinItemRequest(intent) - if (item.shortcutInfo == null) { - return - } - - ensureBackgroundThread { - val shortcutId = item.shortcutInfo?.id!! - val label = item.shortcutInfo?.shortLabel?.toString() ?: item.shortcutInfo?.longLabel?.toString() ?: "" - val icon = launcherApps.getShortcutIconDrawable(item.shortcutInfo!!, resources.displayMetrics.densityDpi) - val (page, rect) = findFirstEmptyCell() - val gridItem = HomeScreenGridItem( - null, - rect.left, - rect.top, - rect.right, - rect.bottom, - page, - item.shortcutInfo!!.`package`, - "", - label, - ITEM_TYPE_SHORTCUT, - "", - -1, - shortcutId, - icon.toBitmap(), - false, - icon - ) - - runOnUiThread { - binding.homeScreenGrid.root.skipToPage(page) - } - // delay showing the shortcut both to let the user see adding it in realtime and hackily avoid concurrent modification exception at HomeScreenGrid - Thread.sleep(2000) - - try { - item.accept() - binding.homeScreenGrid.root.storeAndShowGridItem(gridItem) - } catch (ignored: IllegalStateException) { - } - } - } - } - - private fun findFirstEmptyCell(): Pair { - val gridItems = homeScreenGridItemsDB.getAllItems() as ArrayList - val maxPage = gridItems.map { it.page }.max() - val occupiedCells = ArrayList>() - gridItems.forEach { item -> - for (xCell in item.left..item.right) { - for (yCell in item.top..item.bottom) { - occupiedCells.add(Triple(item.page, xCell, yCell)) - } - } - } - - for (page in 0 until maxPage) { - for (checkedYCell in 0 until config.homeColumnCount) { - for (checkedXCell in 0 until config.homeRowCount - 1) { - val wantedCell = Triple(page, checkedXCell, checkedYCell) - if (!occupiedCells.contains(wantedCell)) { - return Pair(page, Rect(wantedCell.second, wantedCell.third, wantedCell.second, wantedCell.third)) - } - } - } - } - - return Pair(maxPage + 1, Rect(0, 0, 0, 0)) - } - override fun onStart() { super.onStart() binding.homeScreenGrid.root.appWidgetHost.startListening() @@ -256,12 +183,13 @@ class MainActivity : SimpleActivity(), FlingListener { newRowCount = config.homeRowCount, newColumnCount = config.homeColumnCount ) + binding.homeScreenGrid.root.updateColors() binding.allAppsFragment.root.onResume() } override fun onStop() { super.onStop() - binding.homeScreenGrid.root.appWidgetHost?.stopListening() + binding.homeScreenGrid.root.appWidgetHost.stopListening() wasJustPaused = false } @@ -351,7 +279,7 @@ class MainActivity : SimpleActivity(), FlingListener { hasFingerMoved(event) } - if (mLongPressedIcon != null && mOpenPopupMenu != null && hasFingerMoved) { + if (mLongPressedIcon != null && (mOpenPopupMenu != null) && hasFingerMoved) { mOpenPopupMenu?.dismiss() mOpenPopupMenu = null binding.homeScreenGrid.root.itemDraggingStarted(mLongPressedIcon!!) @@ -422,6 +350,80 @@ class MainActivity : SimpleActivity(), FlingListener { return true } + private fun handleIntentAction(intent: Intent) { + if (intent.action == LauncherApps.ACTION_CONFIRM_PIN_SHORTCUT) { + val launcherApps = applicationContext.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val item = launcherApps.getPinItemRequest(intent) + if (item.shortcutInfo == null) { + return + } + + ensureBackgroundThread { + val shortcutId = item.shortcutInfo?.id!! + val label = item.shortcutInfo?.shortLabel?.toString() ?: item.shortcutInfo?.longLabel?.toString() ?: "" + val icon = launcherApps.getShortcutIconDrawable(item.shortcutInfo!!, resources.displayMetrics.densityDpi) + val (page, rect) = findFirstEmptyCell() + val gridItem = HomeScreenGridItem( + null, + rect.left, + rect.top, + rect.right, + rect.bottom, + page, + item.shortcutInfo!!.`package`, + "", + label, + ITEM_TYPE_SHORTCUT, + "", + -1, + shortcutId, + icon.toBitmap(), + false, + null, + icon + ) + + runOnUiThread { + binding.homeScreenGrid.root.skipToPage(page) + } + // delay showing the shortcut both to let the user see adding it in realtime and hackily avoid concurrent modification exception at HomeScreenGrid + Thread.sleep(2000) + + try { + item.accept() + binding.homeScreenGrid.root.storeAndShowGridItem(gridItem) + } catch (ignored: IllegalStateException) { + } + } + } + } + + private fun findFirstEmptyCell(): Pair { + val gridItems = homeScreenGridItemsDB.getAllItems() as ArrayList + val maxPage = gridItems.map { it.page }.max() + val occupiedCells = ArrayList>() + gridItems.forEach { item -> + for (xCell in item.left..item.right) { + for (yCell in item.top..item.bottom) { + occupiedCells.add(Triple(item.page, xCell, yCell)) + } + } + } + + for (page in 0 until maxPage) { + for (checkedYCell in 0 until config.homeColumnCount) { + for (checkedXCell in 0 until config.homeRowCount - 1) { + val wantedCell = Triple(page, checkedXCell, checkedYCell) + if (!occupiedCells.contains(wantedCell)) { + return Pair(page, Rect(wantedCell.second, wantedCell.third, wantedCell.second, wantedCell.third)) + } + } + } + } + + return Pair(maxPage + 1, Rect(0, 0, 0, 0)) + } + // some devices ACTION_MOVE keeps triggering for the whole long press duration, but we are interested in real moves only, when coords change private fun hasFingerMoved(event: MotionEvent) = mTouchDownX != -1 && mTouchDownY != -1 && ((Math.abs(mTouchDownX - event.x) > mMoveGestureThreshold) || (Math.abs(mTouchDownY - event.y) > mMoveGestureThreshold)) @@ -524,6 +526,9 @@ class MainActivity : SimpleActivity(), FlingListener { if (clickedGridItem != null) { performItemClick(clickedGridItem) } + if (clickedGridItem?.type != ITEM_TYPE_FOLDER) { + binding.homeScreenGrid.root.closeFolder(redraw = true) + } } fun closeAppDrawer(delayed: Boolean = false) { @@ -559,20 +564,26 @@ class MainActivity : SimpleActivity(), FlingListener { } private fun performItemClick(clickedGridItem: HomeScreenGridItem) { - if (clickedGridItem.type == ITEM_TYPE_ICON) { - launchApp(clickedGridItem.packageName, clickedGridItem.activityName) - } else if (clickedGridItem.type == ITEM_TYPE_SHORTCUT) { - val id = clickedGridItem.shortcutId - val packageName = clickedGridItem.packageName - val userHandle = android.os.Process.myUserHandle() - val shortcutBounds = binding.homeScreenGrid.root.getClickableRect(clickedGridItem) - val launcherApps = applicationContext.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - launcherApps.startShortcut(packageName, id, shortcutBounds, null, userHandle) + when (clickedGridItem.type) { + ITEM_TYPE_ICON -> launchApp(clickedGridItem.packageName, clickedGridItem.activityName) + ITEM_TYPE_FOLDER -> openFolder(clickedGridItem) + ITEM_TYPE_SHORTCUT -> { + val id = clickedGridItem.shortcutId + val packageName = clickedGridItem.packageName + val userHandle = android.os.Process.myUserHandle() + val shortcutBounds = binding.homeScreenGrid.root.getClickableRect(clickedGridItem) + val launcherApps = applicationContext.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + launcherApps.startShortcut(packageName, id, shortcutBounds, null, userHandle) + } } } + private fun openFolder(folder: HomeScreenGridItem) { + binding.homeScreenGrid.root.openFolder(folder) + } + private fun performItemLongClick(x: Float, clickedGridItem: HomeScreenGridItem) { - if (clickedGridItem.type == ITEM_TYPE_ICON || clickedGridItem.type == ITEM_TYPE_SHORTCUT) { + if (clickedGridItem.type == ITEM_TYPE_ICON || clickedGridItem.type == ITEM_TYPE_SHORTCUT || clickedGridItem.type == ITEM_TYPE_FOLDER) { binding.mainHolder.performHapticFeedback() } @@ -595,7 +606,7 @@ class MainActivity : SimpleActivity(), FlingListener { binding.homeScreenPopupMenuAnchor.y = anchorY if (mOpenPopupMenu == null) { - mOpenPopupMenu = handleGridItemPopupMenu(binding.homeScreenPopupMenuAnchor, gridItem, isOnAllAppsFragment) + mOpenPopupMenu = handleGridItemPopupMenu(binding.homeScreenPopupMenuAnchor, gridItem, isOnAllAppsFragment, menuListener) } } @@ -623,52 +634,6 @@ class MainActivity : SimpleActivity(), FlingListener { } } - private fun handleGridItemPopupMenu(anchorView: View, gridItem: HomeScreenGridItem, isOnAllAppsFragment: Boolean): PopupMenu { - - val contextTheme = ContextThemeWrapper(this, getPopupMenuTheme()) - return PopupMenu(contextTheme, anchorView, Gravity.TOP or Gravity.END).apply { - if (isQPlus()) { - setForceShowIcon(true) - } - - inflate(R.menu.menu_app_icon) - menu.findItem(R.id.rename).isVisible = gridItem.type == ITEM_TYPE_ICON && !isOnAllAppsFragment - menu.findItem(R.id.hide_icon).isVisible = gridItem.type == ITEM_TYPE_ICON && isOnAllAppsFragment - menu.findItem(R.id.resize).isVisible = gridItem.type == ITEM_TYPE_WIDGET - menu.findItem(R.id.app_info).isVisible = gridItem.type == ITEM_TYPE_ICON - menu.findItem(R.id.uninstall).isVisible = gridItem.type == ITEM_TYPE_ICON && canAppBeUninstalled(gridItem.packageName) - menu.findItem(R.id.remove).isVisible = !isOnAllAppsFragment - setOnMenuItemClickListener { item -> - resetFragmentTouches() - when (item.itemId) { - R.id.hide_icon -> hideIcon(gridItem) - R.id.rename -> renameItem(gridItem) - R.id.resize -> binding.homeScreenGrid.root.widgetLongPressed(gridItem) - R.id.app_info -> launchAppInfo(gridItem.packageName) - R.id.remove -> binding.homeScreenGrid.root.removeAppIcon(gridItem) - R.id.uninstall -> uninstallApp(gridItem.packageName) - } - true - } - - setOnDismissListener { - mOpenPopupMenu = null - resetFragmentTouches() - } - - var visibleMenuItems = 0 - for (item in menu.iterator()) { - if (item.isVisible) { - visibleMenuItems++ - } - } - val yOffset = resources.getDimension(R.dimen.long_press_anchor_button_offset_y) * (visibleMenuItems - 1) - anchorView.y -= yOffset - - show() - } - } - private fun resetFragmentTouches() { binding.widgetsFragment.root.apply { touchDownY = -1 @@ -714,6 +679,53 @@ class MainActivity : SimpleActivity(), FlingListener { } } + val menuListener: ItemMenuListener = object : ItemMenuListener { + override fun onAnyClick() { + resetFragmentTouches() + } + + override fun hide(gridItem: HomeScreenGridItem) { + hideIcon(gridItem) + } + + override fun rename(gridItem: HomeScreenGridItem) { + renameItem(gridItem) + } + + override fun resize(gridItem: HomeScreenGridItem) { + binding.homeScreenGrid.root.widgetLongPressed(gridItem) + } + + override fun appInfo(gridItem: HomeScreenGridItem) { + launchAppInfo(gridItem.packageName) + } + + override fun remove(gridItem: HomeScreenGridItem) { + binding.homeScreenGrid.root.removeAppIcon(gridItem) + } + + override fun uninstall(gridItem: HomeScreenGridItem) { + uninstallApp(gridItem.packageName) + } + + override fun onDismiss() { + mOpenPopupMenu = null + resetFragmentTouches() + } + + override fun beforeShow(menu: Menu) { + var visibleMenuItems = 0 + for (item in menu.iterator()) { + if (item.isVisible) { + visibleMenuItems++ + } + } + val yOffset = resources.getDimension(R.dimen.long_press_anchor_button_offset_y) * (visibleMenuItems - 1) + binding.homeScreenPopupMenuAnchor.y -= yOffset + } + } + + private class MyGestureListener(private val flingListener: FlingListener) : GestureDetector.SimpleOnGestureListener() { override fun onSingleTapUp(event: MotionEvent): Boolean { (flingListener as MainActivity).homeScreenClicked(event.x, event.y) @@ -857,7 +869,8 @@ class MainActivity : SimpleActivity(), FlingListener { -1, "", null, - true + true, + null ) homeScreenGridItems.add(dialerIcon) } @@ -883,7 +896,8 @@ class MainActivity : SimpleActivity(), FlingListener { -1, "", null, - true + true, + null ) homeScreenGridItems.add(SMSMessengerIcon) } @@ -911,7 +925,8 @@ class MainActivity : SimpleActivity(), FlingListener { -1, "", null, - true + true, + null ) homeScreenGridItems.add(browserIcon) } @@ -938,7 +953,8 @@ class MainActivity : SimpleActivity(), FlingListener { -1, "", null, - true + true, + null ) homeScreenGridItems.add(storeIcon) } @@ -967,7 +983,8 @@ class MainActivity : SimpleActivity(), FlingListener { -1, "", null, - true + true, + null ) homeScreenGridItems.add(cameraIcon) } diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/activities/SettingsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/activities/SettingsActivity.kt index 98f993a..eadbc65 100644 --- a/app/src/main/kotlin/com/simplemobiletools/launcher/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/activities/SettingsActivity.kt @@ -21,11 +21,10 @@ import kotlin.system.exitProcess class SettingsActivity : SimpleActivity() { - private lateinit var binding: ActivitySettingsBinding + private val binding by viewBinding(ActivitySettingsBinding::inflate) override fun onCreate(savedInstanceState: Bundle?) { isMaterialActivity = true super.onCreate(savedInstanceState) - binding = ActivitySettingsBinding.inflate(layoutInflater) setContentView(binding.root) updateMaterialActivityViews(binding.settingsCoordinator, binding.settingsHolder, useTransparentNavigation = true, useTopSearchMenu = false) diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/adapters/FolderIconsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/adapters/FolderIconsAdapter.kt new file mode 100644 index 0000000..353b304 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/adapters/FolderIconsAdapter.kt @@ -0,0 +1,109 @@ +package com.simplemobiletools.launcher.adapters + +import android.view.Menu +import android.view.View +import android.view.ViewGroup +import androidx.core.view.iterator +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter +import com.simplemobiletools.commons.views.MyRecyclerView +import com.simplemobiletools.launcher.R +import com.simplemobiletools.launcher.activities.MainActivity +import com.simplemobiletools.launcher.databinding.ItemLauncherLabelBinding +import com.simplemobiletools.launcher.extensions.handleGridItemPopupMenu +import com.simplemobiletools.launcher.interfaces.ItemMenuListenerAdapter +import com.simplemobiletools.launcher.models.HomeScreenGridItem + +class FolderIconsAdapter( + activity: BaseSimpleActivity, var items: MutableList, private val iconPadding: Int, + recyclerView: MyRecyclerView, itemClick: (Any) -> Unit +) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) { + + override fun getActionMenuId() = 0 + + override fun actionItemPressed(id: Int) {} + + override fun getSelectableItemCount() = itemCount + + override fun getIsItemSelectable(position: Int) = false + + override fun getItemSelectionKey(position: Int) = items.getOrNull(position)?.id?.toInt() + + override fun getItemKeyPosition(key: Int) = items.indexOfFirst { it.id?.toInt() == key } + + override fun onActionModeCreated() {} + + override fun onActionModeDestroyed() {} + + override fun prepareActionMode(menu: Menu) {} + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return createViewHolder(ItemLauncherLabelBinding.inflate(layoutInflater, parent, false).root) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + setupView(holder.itemView, item) + bindViewHolder(holder) + } + + override fun getItemCount() = items.size + + private fun removeItem(item: HomeScreenGridItem) { + val position = items.indexOfFirst { it.id == item.id } + items.removeAt(position) + notifyItemRemoved(position) + } + + private fun setupView(view: View, item: HomeScreenGridItem) { + ItemLauncherLabelBinding.bind(view).apply { + launcherLabel.text = item.title + launcherLabel.setTextColor(textColor) + launcherIcon.setPadding(iconPadding, iconPadding, iconPadding, 0) + launcherIcon.setImageDrawable(item.drawable) + + val mainListener = (activity as? MainActivity)?.menuListener + + root.setOnClickListener { itemClick(item) } + root.setOnLongClickListener { + popupAnchor.y = launcherIcon.y + activity.handleGridItemPopupMenu(popupAnchor, item, false, object : ItemMenuListenerAdapter() { + override fun appInfo(gridItem: HomeScreenGridItem) { + mainListener?.appInfo(gridItem) + } + + override fun remove(gridItem: HomeScreenGridItem) { + mainListener?.remove(gridItem) + removeItem(gridItem) + } + + override fun uninstall(gridItem: HomeScreenGridItem) { + mainListener?.uninstall(gridItem) + } + + override fun rename(gridItem: HomeScreenGridItem) { + mainListener?.rename(gridItem) + } + + override fun beforeShow(menu: Menu) { + var visibleMenuItems = 0 + for (menuItem in menu.iterator()) { + if (menuItem.isVisible) { + visibleMenuItems++ + } + } + val yOffset = resources.getDimension(R.dimen.long_press_anchor_button_offset_y) * (visibleMenuItems - 1) + popupAnchor.y -= yOffset + } + }) + true + } + } + } + + fun updateItems(items: List) { + this.items.clear() + this.items.addAll(items) + notifyDataSetChanged() + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/databases/AppsDatabase.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/databases/AppsDatabase.kt index 095ea3b..64b4c47 100644 --- a/app/src/main/kotlin/com/simplemobiletools/launcher/databases/AppsDatabase.kt +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/databases/AppsDatabase.kt @@ -68,8 +68,8 @@ abstract class AppsDatabase : RoomDatabase() { private val MIGRATION_4_5 = object : Migration(4, 5) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE `home_screen_grid_items_new` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `left` INTEGER NOT NULL, `top` INTEGER NOT NULL, `right` INTEGER NOT NULL, `bottom` INTEGER NOT NULL, `page` INTEGER NOT NULL, `package_name` TEXT NOT NULL, `activity_name` TEXT NOT NULL, `title` TEXT NOT NULL, `type` INTEGER NOT NULL, `class_name` TEXT NOT NULL, `widget_id` INTEGER NOT NULL, `shortcut_id` TEXT NOT NULL, `icon` BLOB, `docked` INTEGER NOT NULL DEFAULT 0)") - database.execSQL("INSERT INTO `home_screen_grid_items_new` (`id`, `left`, `top`, `right`, `bottom`, `page`, `package_name`, `activity_name`, `title`, `type`, `class_name`, `widget_id`, `shortcut_id`, `icon`, `docked`) SELECT `id`, `left`, `top`, `right`, `bottom`, 0 as `page`, `package_name`, `activity_name`, `title`, `type`, `class_name`, `widget_id`, `shortcut_id`, `icon`, CASE WHEN `type` != 1 AND `top` = 5 THEN 1 ELSE 0 END AS `docked` FROM `home_screen_grid_items` WHERE `intent` IS NULL OR `intent` = ''") + database.execSQL("CREATE TABLE `home_screen_grid_items_new` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `left` INTEGER NOT NULL, `top` INTEGER NOT NULL, `right` INTEGER NOT NULL, `bottom` INTEGER NOT NULL, `page` INTEGER NOT NULL, `package_name` TEXT NOT NULL, `activity_name` TEXT NOT NULL, `title` TEXT NOT NULL, `type` INTEGER NOT NULL, `class_name` TEXT NOT NULL, `widget_id` INTEGER NOT NULL, `shortcut_id` TEXT NOT NULL, `icon` BLOB, `docked` INTEGER NOT NULL DEFAULT 0, `parent_id` INTEGER)") + database.execSQL("INSERT INTO `home_screen_grid_items_new` (`id`, `left`, `top`, `right`, `bottom`, `page`, `package_name`, `activity_name`, `title`, `type`, `class_name`, `widget_id`, `shortcut_id`, `icon`, `docked`, `parent_id`) SELECT `id`, `left`, `top`, `right`, `bottom`, 0 as `page`, `package_name`, `activity_name`, `title`, `type`, `class_name`, `widget_id`, `shortcut_id`, `icon`, CASE WHEN `type` != 1 AND `top` = 5 THEN 1 ELSE 0 END AS `docked`, NULL AS `parent_id` FROM `home_screen_grid_items` WHERE `intent` IS NULL OR `intent` = ''") database.execSQL("DROP TABLE `home_screen_grid_items`") database.execSQL("ALTER TABLE `home_screen_grid_items_new` RENAME TO `home_screen_grid_items`") database.execSQL("CREATE UNIQUE INDEX `index_home_screen_grid_items_id` ON `home_screen_grid_items` (`id`)") diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/extensions/Activity.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/extensions/Activity.kt index be02efb..316fd2e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/launcher/extensions/Activity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/extensions/Activity.kt @@ -6,9 +6,21 @@ import android.content.Intent import android.content.pm.ApplicationInfo import android.net.Uri import android.provider.Settings +import android.view.ContextThemeWrapper +import android.view.Gravity +import android.view.View +import android.widget.PopupMenu +import com.simplemobiletools.commons.extensions.getPopupMenuTheme import com.simplemobiletools.commons.extensions.showErrorToast +import com.simplemobiletools.commons.helpers.isQPlus +import com.simplemobiletools.launcher.R import com.simplemobiletools.launcher.activities.SettingsActivity +import com.simplemobiletools.launcher.helpers.ITEM_TYPE_FOLDER +import com.simplemobiletools.launcher.helpers.ITEM_TYPE_ICON +import com.simplemobiletools.launcher.helpers.ITEM_TYPE_WIDGET import com.simplemobiletools.launcher.helpers.UNINSTALL_APP_REQUEST_CODE +import com.simplemobiletools.launcher.interfaces.ItemMenuListener +import com.simplemobiletools.launcher.models.HomeScreenGridItem fun Activity.launchApp(packageName: String, activityName: String) { // if this is true, launch the app settings @@ -56,3 +68,40 @@ fun Activity.uninstallApp(packageName: String) { startActivityForResult(this, UNINSTALL_APP_REQUEST_CODE) } } + +fun Activity.handleGridItemPopupMenu(anchorView: View, gridItem: HomeScreenGridItem, isOnAllAppsFragment: Boolean, listener: ItemMenuListener): PopupMenu { + val contextTheme = ContextThemeWrapper(this, getPopupMenuTheme()) + return PopupMenu(contextTheme, anchorView, Gravity.TOP or Gravity.END).apply { + if (isQPlus()) { + setForceShowIcon(true) + } + + inflate(R.menu.menu_app_icon) + menu.findItem(R.id.rename).isVisible = (gridItem.type == ITEM_TYPE_ICON || gridItem.type == ITEM_TYPE_FOLDER) && !isOnAllAppsFragment + menu.findItem(R.id.hide_icon).isVisible = gridItem.type == ITEM_TYPE_ICON && isOnAllAppsFragment + menu.findItem(R.id.resize).isVisible = gridItem.type == ITEM_TYPE_WIDGET + menu.findItem(R.id.app_info).isVisible = gridItem.type == ITEM_TYPE_ICON + menu.findItem(R.id.uninstall).isVisible = gridItem.type == ITEM_TYPE_ICON && canAppBeUninstalled(gridItem.packageName) + menu.findItem(R.id.remove).isVisible = !isOnAllAppsFragment + setOnMenuItemClickListener { item -> + listener.onAnyClick() + when (item.itemId) { + R.id.hide_icon -> listener.hide(gridItem) + R.id.rename -> listener.rename(gridItem) + R.id.resize -> listener.resize(gridItem) + R.id.app_info -> listener.appInfo(gridItem) + R.id.remove -> listener.remove(gridItem) + R.id.uninstall -> listener.uninstall(gridItem) + } + true + } + + setOnDismissListener { + listener.onDismiss() + } + + listener.beforeShow(menu) + + show() + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/fragments/AllAppsFragment.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/fragments/AllAppsFragment.kt index b8c37f3..afb667e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/launcher/fragments/AllAppsFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/fragments/AllAppsFragment.kt @@ -207,6 +207,7 @@ class AllAppsFragment(context: Context, attributeSet: AttributeSet) : MyFragment "", null, false, + null, appLauncher.drawable ) diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/fragments/WidgetsFragment.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/fragments/WidgetsFragment.kt index bd855c0..d637565 100644 --- a/app/src/main/kotlin/com/simplemobiletools/launcher/fragments/WidgetsFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/fragments/WidgetsFragment.kt @@ -267,6 +267,7 @@ class WidgetsFragment(context: Context, attributeSet: AttributeSet) : MyFragment "", null, false, + null, appWidget.widgetPreviewImage, appWidget.providerInfo, appWidget.activityInfo, diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/helpers/Constants.kt index 65e7bcd..e201a3f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/launcher/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/helpers/Constants.kt @@ -27,6 +27,7 @@ const val REQUEST_CREATE_SHORTCUT = 53 const val ITEM_TYPE_ICON = 0 const val ITEM_TYPE_WIDGET = 1 const val ITEM_TYPE_SHORTCUT = 2 +const val ITEM_TYPE_FOLDER = 3 const val WIDGET_HOST_ID = 12345 const val MAX_CLICK_DURATION = 150 diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/interfaces/HomeScreenGridItemsDao.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/interfaces/HomeScreenGridItemsDao.kt index 592e895..1e6fcaf 100644 --- a/app/src/main/kotlin/com/simplemobiletools/launcher/interfaces/HomeScreenGridItemsDao.kt +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/interfaces/HomeScreenGridItemsDao.kt @@ -1,9 +1,6 @@ package com.simplemobiletools.launcher.interfaces -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.* import com.simplemobiletools.launcher.models.HomeScreenGridItem @Dao @@ -11,6 +8,9 @@ interface HomeScreenGridItemsDao { @Query("SELECT * FROM home_screen_grid_items") fun getAllItems(): List + @Query("SELECT * FROM home_screen_grid_items WHERE parent_id = :folderId") + fun getFolderItems(folderId: Long): List + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(item: HomeScreenGridItem): Long @@ -23,12 +23,33 @@ interface HomeScreenGridItemsDao { @Query("UPDATE home_screen_grid_items SET title = :title WHERE id = :id") fun updateItemTitle(title: String, id: Long): Int - @Query("UPDATE home_screen_grid_items SET `left` = :left, `top` = :top, `right` = :right, `bottom` = :bottom, `page` = :page, `docked` = :docked WHERE id = :id") - fun updateItemPosition(left: Int, top: Int, right: Int, bottom: Int, page: Int, docked: Boolean, id: Long) + @Query("UPDATE home_screen_grid_items SET `left` = :left, `top` = :top, `right` = :right, `bottom` = :bottom, `page` = :page, `docked` = :docked , `parent_id` = :parentId WHERE id = :id") + fun updateItemPosition(left: Int, top: Int, right: Int, bottom: Int, page: Int, docked: Boolean, parentId: Long?, id: Long) @Query("DELETE FROM home_screen_grid_items WHERE id = :id") - fun deleteById(id: Long) + fun deleteItemById(id: Long) + + @Query("DELETE FROM home_screen_grid_items WHERE parent_id = :id") + fun deleteItemsWithParentId(id: Long) + + @Transaction + fun deleteById(id: Long) { + deleteItemById(id) + deleteItemsWithParentId(id) + } @Query("DELETE FROM home_screen_grid_items WHERE package_name = :packageName") - fun deleteByPackageName(packageName: String) + fun deleteItemByPackageName(packageName: String) + + @Query("DELETE FROM home_screen_grid_items WHERE parent_id IN (SELECT id FROM home_screen_grid_items WHERE package_name = :packageName)") + fun deleteItemsByParentPackageName(packageName: String) + + @Query("UPDATE home_screen_grid_items SET `left` = `left` + :shiftBy WHERE parent_id == :folderId AND `left` > :shiftFrom AND id != :excludingId") + fun shiftFolderItems(folderId: Long, shiftFrom: Int, shiftBy: Int, excludingId: Long? = null) + + @Transaction + fun deleteByPackageName(packageName: String) { + deleteItemByPackageName(packageName) + deleteItemsByParentPackageName(packageName) + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/interfaces/ItemMenuListener.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/interfaces/ItemMenuListener.kt new file mode 100644 index 0000000..4f0a324 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/interfaces/ItemMenuListener.kt @@ -0,0 +1,28 @@ +package com.simplemobiletools.launcher.interfaces + +import android.view.Menu +import com.simplemobiletools.launcher.models.HomeScreenGridItem + +interface ItemMenuListener { + fun onAnyClick() + fun hide(gridItem: HomeScreenGridItem) + fun rename(gridItem: HomeScreenGridItem) + fun resize(gridItem: HomeScreenGridItem) + fun appInfo(gridItem: HomeScreenGridItem) + fun remove(gridItem: HomeScreenGridItem) + fun uninstall(gridItem: HomeScreenGridItem) + fun onDismiss() + fun beforeShow(menu: Menu) +} + +abstract class ItemMenuListenerAdapter : ItemMenuListener { + override fun onAnyClick() = Unit + override fun hide(gridItem: HomeScreenGridItem) = Unit + override fun rename(gridItem: HomeScreenGridItem) = Unit + override fun resize(gridItem: HomeScreenGridItem) = Unit + override fun appInfo(gridItem: HomeScreenGridItem) = Unit + override fun remove(gridItem: HomeScreenGridItem) = Unit + override fun uninstall(gridItem: HomeScreenGridItem) = Unit + override fun onDismiss() = Unit + override fun beforeShow(menu: Menu) = Unit +} diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/models/HomeScreenGridItem.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/models/HomeScreenGridItem.kt index 7f75d41..8071ddb 100644 --- a/app/src/main/kotlin/com/simplemobiletools/launcher/models/HomeScreenGridItem.kt +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/models/HomeScreenGridItem.kt @@ -3,6 +3,7 @@ package com.simplemobiletools.launcher.models import android.appwidget.AppWidgetProviderInfo import android.content.pm.ActivityInfo import android.graphics.Bitmap +import android.graphics.Point import android.graphics.drawable.Drawable import androidx.room.* import com.simplemobiletools.launcher.helpers.ITEM_TYPE_ICON @@ -25,6 +26,7 @@ data class HomeScreenGridItem( @ColumnInfo(name = "shortcut_id") var shortcutId: String, // used at pinned shortcuts at startLauncher call @ColumnInfo(name = "icon") var icon: Bitmap? = null, // store images of pinned shortcuts, those cannot be retrieved after creating @ColumnInfo(name = "docked") var docked: Boolean = false, // special flag, meaning that page, top and bottom don't matter for this item, it is always at the bottom of the screen + @ColumnInfo(name = "parent_id") var parentId: Long? = null, // id of folder this item is in (if it is in any) @Ignore var drawable: Drawable? = null, @Ignore var providerInfo: AppWidgetProviderInfo? = null, // used at widgets @@ -32,7 +34,11 @@ data class HomeScreenGridItem( @Ignore var widthCells: Int = 1, @Ignore var heightCells: Int = 1 ) { - constructor() : this(null, -1, -1, -1, -1, 0, "", "", "", ITEM_TYPE_ICON, "", -1, "", null, false, null, null, null, 1, 1) + companion object { + const val FOLDER_MAX_CAPACITY = 16 + } + + constructor() : this(null, -1, -1, -1, -1, 0, "", "", "", ITEM_TYPE_ICON, "", -1, "", null, false, null, null, null, null, 1, 1) fun getWidthInCells() = if (right == -1 || left == -1) { widthCells @@ -63,4 +69,6 @@ data class HomeScreenGridItem( } fun getItemIdentifier() = "$packageName/$activityName" + + fun getTopLeft() = Point(left, top) } diff --git a/app/src/main/kotlin/com/simplemobiletools/launcher/views/HomeScreenGrid.kt b/app/src/main/kotlin/com/simplemobiletools/launcher/views/HomeScreenGrid.kt index c4a96d5..7892f0e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/launcher/views/HomeScreenGrid.kt +++ b/app/src/main/kotlin/com/simplemobiletools/launcher/views/HomeScreenGrid.kt @@ -10,6 +10,7 @@ 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 @@ -20,16 +21,18 @@ import android.util.AttributeSet import android.util.Size import android.util.SizeF import android.view.View +import android.view.animation.DecelerateInterpolator 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.ensureBackgroundThread -import com.simplemobiletools.commons.helpers.isSPlus +import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.launcher.R import com.simplemobiletools.launcher.activities.MainActivity import com.simplemobiletools.launcher.databinding.HomeScreenGridBinding @@ -46,22 +49,27 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel private lateinit var binding: HomeScreenGridBinding private var columnCount = context.config.homeColumnCount private var rowCount = context.config.homeRowCount - private var cellXCoords = ArrayList(columnCount) - private var cellYCoords = ArrayList(rowCount) + private var pageIndicatorsYPos = 0 + private val cells = mutableMapOf() + private var dockCellY = 0 var cellWidth = 0 var cellHeight = 0 - private var extraXMargin = 0 - private var extraYMargin = 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 @@ -74,14 +82,19 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel getWidth = { width }, getHandler = { handler }, getNextPageBound = { right - sideMargins.right - cellWidth / 2 }, - getPrevPageBound = { left + sideMargins.left + cellWidth / 2 } + getPrevPageBound = { left + sideMargins.left + cellWidth / 2 }, + pageChangeStarted = { 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() - private var gridCenters = ArrayList>() + private var gridCenters = ArrayList() private var draggedItemCurrentCoords = Pair(-1, -1) private var widgetViews = ArrayList() @@ -101,6 +114,17 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel 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) @@ -115,6 +139,22 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel 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 @@ -138,6 +178,8 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel gridItems.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) @@ -159,8 +201,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel if (columnCount != newColumnCount || rowCount != newRowCount) { rowCount = newRowCount columnCount = newColumnCount - cellXCoords = ArrayList(columnCount) - cellYCoords = ArrayList(rowCount) + cells.clear() gridCenters.clear() iconMargin = (context.resources.getDimension(R.dimen.icon_side_margin) * 5 / columnCount).toInt() redrawWidgets = true @@ -168,6 +209,13 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel } } + 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) @@ -199,8 +247,17 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel fun itemDraggingStarted(draggedGridItem: HomeScreenGridItem) { draggedItem = draggedGridItem + + if (draggedGridItem.type == ITEM_TYPE_WIDGET) { + closeFolder() + } + if (draggedItem!!.drawable == null) { - draggedItem!!.drawable = context.getDrawableForPackageName(draggedGridItem.packageName) + if (draggedItem?.type == ITEM_TYPE_FOLDER) { + draggedItem!!.drawable = draggedGridItem.toFolder().generateDrawable() + } else { + draggedItem!!.drawable = context.getDrawableForPackageName(draggedGridItem.packageName) + } } redrawGrid() @@ -211,7 +268,21 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel return } - if (draggedItemCurrentCoords.first == -1 && draggedItemCurrentCoords.second == -1) { + 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) { @@ -223,6 +294,37 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel } 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() } @@ -239,6 +341,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel pager.itemMovementStopped() when (draggedItem!!.type) { + ITEM_TYPE_FOLDER -> moveItem() ITEM_TYPE_ICON, ITEM_TYPE_SHORTCUT -> addAppIconOrShortcut() ITEM_TYPE_WIDGET -> addWidget() } @@ -274,7 +377,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel } updateWidgetPositionAndSize(widgetView, item) ensureBackgroundThread { - context.homeScreenGridItemsDB.updateItemPosition(item.left, item.top, item.right, item.bottom, item.page, false, item.id!!) + context.homeScreenGridItemsDB.updateItemPosition(item.left, item.top, item.right, item.bottom, item.page, false, null, item.id!!) } } @@ -299,92 +402,52 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel resizedWidget = null } - private fun addAppIconOrShortcut() { + private fun moveItem() { + val draggedHomeGridItem = gridItems.firstOrNull { it.id == draggedItem?.id } val center = gridCenters.minBy { - Math.abs(it.first - draggedItemCurrentCoords.first + sideMargins.left) + Math.abs(it.second - draggedItemCurrentCoords.second + sideMargins.top) + 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.first - val yIndex = gridCells.second + val xIndex = gridCells.x + val yIndex = gridCells.y // check if the destination cell is empty - var areAllCellsEmpty = true + var isDroppingPositionValid = true val wantedCell = Pair(xIndex, yIndex) - gridItems.filter { pager.isItemOnCurrentPage(it) || it.docked }.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) { - areAllCellsEmpty = false - return@forEach + // 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 (areAllCellsEmpty) { - val draggedHomeGridItem = gridItems.firstOrNull { it.id == draggedItem?.id } + if (isDroppingPositionValid) { + draggedHomeGridItem?.apply { + left = xIndex + top = yIndex + right = xIndex + bottom = yIndex + page = pager.getCurrentPage() + docked = yIndex == rowCount - 1 - // we are moving an existing home screen item from one place to another - if (draggedHomeGridItem != null) { - draggedHomeGridItem.apply { - 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, 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, - yIndex, - pager.getCurrentPage(), - draggedItem!!.packageName, - draggedItem!!.activityName, - draggedItem!!.title, - draggedItem!!.type, - "", - -1, - "", - draggedItem!!.icon, - yIndex == rowCount - 1, - 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 { + context.homeScreenGridItemsDB.updateItemPosition(left, top, right, bottom, page, docked, parentId, id!!) } } - } else { - performHapticFeedback() redrawIcons = true } } @@ -396,6 +459,239 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel } } + 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 + 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) + } + } + } + } 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 @@ -405,7 +701,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel private fun addWidget() { val center = gridCenters.minBy { - Math.abs(it.first - draggedItemCurrentCoords.first + sideMargins.left) + Math.abs(it.second - draggedItemCurrentCoords.second + sideMargins.top) + Math.abs(it.x - draggedItemCurrentCoords.first + sideMargins.left) + Math.abs(it.y - draggedItemCurrentCoords.second + sideMargins.top) } val gridCells = getClosestGridCells(center) @@ -419,7 +715,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel } var areAllCellsEmpty = true - gridItems.filter { it.id != draggedItem?.id && (pager.isItemOnCurrentPage(it) || it.docked) }.forEach { item -> + 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) @@ -458,13 +754,15 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel widgetItem.bottom, pager.getCurrentPage(), false, + null, widgetItem.id!! ) val widgetView = widgetViews.firstOrNull { it.tag == widgetItem.widgetId } if (widgetView != null && !widgetItem.outOfBounds()) { post { - widgetView.x = calculateWidgetX(widgetItem.left) - widgetView.y = calculateWidgetY(widgetItem.top) + val widgetPos = calculateWidgetPos(widgetItem.getTopLeft()) + widgetView.x = widgetPos.x.toFloat() + widgetView.y = widgetPos.y.toFloat() widgetView.beVisible() } } @@ -551,9 +849,9 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel private fun updateWidgetPositionAndSize(widgetView: AppWidgetHostView, item: HomeScreenGridItem): Size { val currentViewPosition = pager.getCurrentViewPositionInFullPageSpace() * width.toFloat() - val x = calculateWidgetX(item.left) + width * item.page - currentViewPosition - widgetView.x = x - widgetView.y = calculateWidgetY(item.top) + 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 @@ -573,21 +871,17 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel return Size(widgetWidth, widgetHeight) } - private fun calculateWidgetX(leftCell: Int) = cellXCoords[leftCell] + sideMargins.left.toFloat() + extraXMargin - - private fun calculateWidgetY(topCell: Int) = cellYCoords[topCell] + sideMargins.top.toFloat() + extraYMargin + 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: Pair): Pair? { - cellXCoords.forEachIndexed { xIndex, xCell -> - cellYCoords.forEachIndexed { yIndex, yCell -> - if (xCell + cellWidth / 2 == center.first && yCell + cellHeight / 2 == center.second) { - return Pair(xIndex, yIndex) - } - } - } - - return null + private fun getClosestGridCells(center: Point): Point? { + return cells.entries.firstOrNull { (_, cell) -> center.x == cell.centerX() && center.y == cell.centerY() }?.key } private fun redrawGrid() { @@ -604,83 +898,60 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel @SuppressLint("DrawAllocation") override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - if (cellXCoords.isEmpty()) { + if (cells.isEmpty()) { fillCellSizes() } val currentXFactor = pager.getXFactorForCurrentPage() val lastXFactor = pager.getXFactorForLastPage() - fun handleItemDrawing(item: HomeScreenGridItem, xFactor: Float) { - if (item.id != draggedItem?.id) { - val drawableX = cellXCoords[item.left] + iconMargin + extraXMargin + sideMargins.left + (width * xFactor).toInt() - - if (item.docked) { - val drawableY = cellYCoords[rowCount - 1] + cellHeight - iconMargin - iconSize + sideMargins.top - - item.drawable!!.setBounds(drawableX, drawableY, drawableX + iconSize, drawableY + iconSize) - } else { - val drawableY = cellYCoords[item.top] + iconMargin + extraYMargin + sideMargins.top - item.drawable!!.setBounds(drawableX, drawableY, drawableX + iconSize, drawableY + iconSize) - - if (item.id != draggedItem?.id && item.title.isNotEmpty()) { - val textX = cellXCoords[item.left].toFloat() + labelSideMargin + sideMargins.left + width * xFactor - val textY = cellYCoords[item.top].toFloat() + iconSize + iconMargin + extraYMargin + labelSideMargin + sideMargins.top - val staticLayout = StaticLayout.Builder - .obtain(item.title, 0, item.title.length, textPaint, cellWidth - 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) + 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.drawable != null && it.type == ITEM_TYPE_ICON || it.type == ITEM_TYPE_SHORTCUT) && pager.isItemOnCurrentPage(it) && !it.docked } + gridItems.filter { it.isSingleCellType() && pager.isItemOnCurrentPage(it) && !it.docked && it.parentId == null } .forEach { item -> if (item.outOfBounds()) { return@forEach } - handleItemDrawing(item, currentXFactor) - } - gridItems.filter { (it.drawable != null && it.type == ITEM_TYPE_ICON || it.type == ITEM_TYPE_SHORTCUT) && it.docked }.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 + } - handleItemDrawing(item, 0f) - } + handleMainGridItemDrawing(item, 0f) + } if (pager.isAnimatingPageChange()) { - gridItems.filter { (it.drawable != null && it.type == ITEM_TYPE_ICON || it.type == ITEM_TYPE_SHORTCUT) && pager.isItemOnLastPage(it) && !it.docked } + gridItems.filter { it.isSingleCellType() && pager.isItemOnLastPage(it) && !it.docked && it.parentId == null } .forEach { item -> if (item.outOfBounds()) { return@forEach } - handleItemDrawing(item, lastXFactor) + handleMainGridItemDrawing(item, lastXFactor) } } if (pager.isSwiped()) { gridItems.filter { - (it.drawable != null && it.type == ITEM_TYPE_ICON || it.type == ITEM_TYPE_SHORTCUT) + it.isSingleCellType() && pager.isItemInSwipeRange(it) && !it.docked + && it.parentId == null }.forEach { item -> if (item.outOfBounds()) { return@forEach } - handleItemDrawing(item, lastXFactor) + handleMainGridItemDrawing(item, lastXFactor) } } @@ -703,7 +974,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel val usableWidth = getFakeWidth() val pageIndicatorsStart = (usableWidth - pageIndicatorsRequiredWidth) / 2 + sideMargins.left var currentPageIndicatorLeft = pageIndicatorsStart - val pageIndicatorY = cellYCoords[rowCount - 1].toFloat() + sideMargins.top + extraYMargin + iconMargin + val pageIndicatorY = pageIndicatorsYPos.toFloat() + sideMargins.top + iconMargin val pageIndicatorStep = pageIndicatorRadius * 2 + pageIndicatorMargin emptyPageIndicatorPaint.alpha = pager.getPageChangeIndicatorsAlpha() // Draw empty page indicators @@ -718,23 +989,79 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel canvas.drawCircle(currentIndicatorPosition + pageIndicatorRadius, pageIndicatorY, pageIndicatorRadius, currentPageIndicatorPaint) } - 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 { - abs(it.first - draggedItemCurrentCoords.first + sideMargins.left) + abs(it.second - draggedItemCurrentCoords.second + sideMargins.top) + 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) } - val gridCells = getClosestGridCells(center) - if (gridCells != null) { - val shadowX = cellXCoords[gridCells.first] + iconMargin + iconSize / 2f + extraXMargin + sideMargins.left - val shadowY = if (gridCells.second == rowCount - 1) { - cellYCoords[gridCells.second] + cellHeight - iconMargin - iconSize / 2f - } else { - cellYCoords[gridCells.second] + iconMargin + iconSize / 2f + extraYMargin - } + sideMargins.top + items.forEach { item -> + val itemRect = folder.getItemRect(item) +// canvas.drawRect(itemRect, contrastTextPaint) + canvas.drawItemInCell(item, itemRect) + } + } + } - canvas.drawCircle(shadowX, shadowY, iconSize / 2f, dragShadowCirclePaint) + 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 @@ -746,14 +1073,15 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel // 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) + 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 leftSide = calculateWidgetX(widgetRect.left) - val topSide = calculateWidgetY(widgetRect.top) + 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) @@ -780,30 +1108,34 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel } private fun fillCellSizes() { - cellWidth = getFakeWidth() / context.config.homeColumnCount - cellHeight = getFakeHeight() / context.config.homeRowCount - extraXMargin = if (cellWidth > cellHeight) { + cellWidth = getFakeWidth() / columnCount + cellHeight = getFakeHeight() / rowCount + val extraXMargin = if (cellWidth > cellHeight) { (cellWidth - cellHeight) / 2 } else { 0 } - extraYMargin = if (cellHeight > cellWidth) { + val extraYMargin = if (cellHeight > cellWidth) { (cellHeight - cellWidth) / 2 } else { 0 } iconSize = min(cellWidth, cellHeight) - 2 * iconMargin - for (i in 0 until context.config.homeColumnCount) { - cellXCoords.add(i, i * cellWidth) - } - - for (i in 0 until context.config.homeRowCount) { - cellYCoords.add(i, i * cellHeight) - } - - cellXCoords.forEach { x -> - cellYCoords.forEach { y -> - gridCenters.add(Pair(x + cellWidth / 2, y + cellHeight / 2)) + 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 + } } } } @@ -812,6 +1144,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel widgetViews.forEach { it.ignoreTouches = true } + closeFolder(true) } fun fragmentCollapsed() { @@ -822,23 +1155,34 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel // get the clickable area around the icon, it includes text too fun getClickableRect(item: HomeScreenGridItem): Rect { - if (cellXCoords.isEmpty()) { + if (cells.isEmpty()) { fillCellSizes() } - val clickableLeft = cellXCoords[item.left] + sideMargins.left + extraXMargin - val clickableTop = if (item.docked) { - cellYCoords[item.getDockAdjustedTop(rowCount)] + cellHeight - iconSize - iconMargin + 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 { - cellYCoords[item.top] - iconMargin + extraYMargin - } + sideMargins.top + 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: Pair): Rect { - val left = item.first - floor((draggedItem!!.getWidthInCells() - 1) / 2.0).toInt() - val rect = Rect(left, item.second, left + draggedItem!!.getWidthInCells() - 1, item.second + draggedItem!!.getHeightInCells() - 1) + 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 @@ -861,19 +1205,29 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel } fun isClickingGridItem(x: Int, y: Int): HomeScreenGridItem? { - for (gridItem in gridItems.filter { it.page == pager.getCurrentPage() || it.docked }) { + 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.type == ITEM_TYPE_ICON || gridItem.type == ITEM_TYPE_SHORTCUT) { + 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 left = calculateWidgetX(gridItem.left) - val top = calculateWidgetY(gridItem.top) + 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 @@ -895,7 +1249,7 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel } private fun HomeScreenGridItem.outOfBounds(): Boolean { - return (left >= cellXCoords.size || right >= cellXCoords.size || (!docked && (top >= cellYCoords.size - 1 || bottom >= cellYCoords.size - 1))) + return (left >= columnCount || right >= columnCount || (!docked && (top >= rowCount - 1 || bottom >= rowCount - 1))) } private inner class HomeScreenGridTouchHelper(host: View) : ExploreByTouchHelper(host) { @@ -981,6 +1335,280 @@ class HomeScreenGrid(context: Context, attrs: AttributeSet, defStyle: Int) : Rel 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.filterVisibleOnCurrentPageOnly() = filter { it.visibleOnCurrentPage() } + + private fun HomeScreenGridItem.visibleOnCurrentPage() = (pager.isItemOnCurrentPage(this) || docked) && parentId == null + + private fun HomeScreenGridItem.isSingleCellType() = (drawable != null && 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 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 items = getItems() + val itemsCount = getItems().count() + 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() + it.drawable?.setBounds(drawableX, drawableY, drawableX + scaledIconSize.toInt(), drawableY + scaledIconSize.toInt()) + it.drawable?.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> { + 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 { + 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 + } } /** @@ -994,6 +1622,7 @@ private class AnimatedGridPager( private val getHandler: () -> Handler, private val getNextPageBound: () -> Int, private val getPrevPageBound: () -> Int, + private val pageChangeStarted: () -> Unit ) { companion object { @@ -1093,6 +1722,7 @@ private class AnimatedGridPager( if (currentPage < getMaxPage() && diffX > 0f || currentPage > 0 && diffX < 0f) { pageChangeSwipedPercentage = (-diffX / getWidth().toFloat()).coerceIn(-1f, 1f) + pageChangeStarted() redrawGrid() } } @@ -1240,6 +1870,7 @@ private class AnimatedGridPager( private fun handlePageChange(redraw: Boolean = false) { pageChangeEnabled = false pageChangeIndicatorsAlpha = 0f + pageChangeStarted() val startingAt = 1 - abs(pageChangeSwipedPercentage) pageChangeSwipedPercentage = 0f getHandler().removeCallbacks(startFadingIndicators) @@ -1267,3 +1898,4 @@ private class AnimatedGridPager( } } } + diff --git a/app/src/main/res/layout/dialog_folder_icons.xml b/app/src/main/res/layout/dialog_folder_icons.xml new file mode 100644 index 0000000..3e30ac8 --- /dev/null +++ b/app/src/main/res/layout/dialog_folder_icons.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/item_launcher_label.xml b/app/src/main/res/layout/item_launcher_label.xml index 5d8a910..431a2c4 100644 --- a/app/src/main/res/layout/item_launcher_label.xml +++ b/app/src/main/res/layout/item_launcher_label.xml @@ -27,4 +27,10 @@ android:maxLines="2" android:textSize="@dimen/smaller_text_size" /> + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 18d61a4..d8beb5c 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -9,4 +9,6 @@ 6dp 1dp 6dp + 200dp + 500dp