Commit 10dac382 authored by Amit Kumar's avatar Amit Kumar 💻
Browse files

Separate folder logic from launcher activity

parent 4bf4103d
Pipeline #151609 passed with stage
in 8 minutes and 20 seconds
......@@ -85,7 +85,7 @@ class ShaderBlurDrawable internal constructor(private val blurWallpaperProvider:
if (canvasOffset > 0)
canvas.translate(-canvasOffset, 0f)
}
override fun setAlpha(alpha: Int) {
blurAlpha = alpha
blurPaint.alpha = alpha
......
package foundation.e.blisslauncher.core.customviews
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.graphics.Rect
import android.text.InputType
import android.text.Selection
import android.util.AttributeSet
import android.util.Log
import android.view.FocusFinder
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewDebug.ExportedProperty
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import android.widget.TextView.OnEditorActionListener
import androidx.viewpager.widget.ViewPager
import foundation.e.blisslauncher.R
import foundation.e.blisslauncher.core.DeviceProfile
import foundation.e.blisslauncher.core.database.model.FolderItem
import foundation.e.blisslauncher.core.database.model.LauncherItem
import foundation.e.blisslauncher.features.folder.FolderAnimationManager
import foundation.e.blisslauncher.features.folder.FolderIcon
import foundation.e.blisslauncher.features.folder.FolderPagerAdapter
import foundation.e.blisslauncher.features.folder.FolderViewPager
import foundation.e.blisslauncher.features.test.Alarm
import foundation.e.blisslauncher.features.test.IconTextView
import foundation.e.blisslauncher.features.test.BaseDragLayer
import foundation.e.blisslauncher.features.test.CellLayout
import foundation.e.blisslauncher.features.test.OnAlarmListener
import foundation.e.blisslauncher.features.test.TestActivity
import foundation.e.blisslauncher.features.test.VariantDeviceProfile
......@@ -32,15 +40,15 @@ import foundation.e.blisslauncher.features.test.dragndrop.DragLayer
import foundation.e.blisslauncher.features.test.dragndrop.DragOptions
import foundation.e.blisslauncher.features.test.dragndrop.DragSource
import foundation.e.blisslauncher.features.test.dragndrop.DropTarget
import me.relex.circleindicator.CircleIndicator
import java.util.ArrayList
import java.util.Collections
import java.util.Comparator
import kotlinx.android.synthetic.main.activity_test.*
import me.relex.circleindicator.CircleIndicator
class Folder @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : AbstractFloatingView(context, attrs), DropTarget, DragController.DragListener,
FolderTitleInput.OnBackKeyListener, FolderItem.FolderListener,
context: Context,
attrs: AttributeSet? = null
) : AbstractFloatingView(context, attrs), DragController.DragListener,
FolderTitleInput.OnBackKeyListener, FolderItem.FolderListener, View.OnFocusChangeListener,
OnEditorActionListener, DragSource {
/**
......@@ -55,19 +63,18 @@ class Folder @JvmOverloads constructor(
private val sTempRect = Rect()
private val MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10
private var sDefaultFolderName: String? = null
private val mReorderAlarm: Alarm = Alarm()
private val mOnExitAlarm: Alarm = Alarm()
val mItemsInReadingOrder = ArrayList<View>()
protected val mLauncher: TestActivity
protected var mDragController: DragController? = null
val launcher: TestActivity
var dragController: DragController? = null
lateinit var mInfo: FolderItem
private val mCurrentAnimator: AnimatorSet? = null
private var mCurrentAnimator: AnimatorSet? = null
var mFolderIcon: IconTextView? = null
var folderIcon: FolderIcon? = null
lateinit var mContent: ViewPager
lateinit var mContent: FolderViewPager
lateinit var mFolderTitleInput: FolderTitleInput
private lateinit var mPageIndicator: CircleIndicator
......@@ -83,8 +90,8 @@ class Folder @JvmOverloads constructor(
private var mCurrentDragView: View? = null
private var mIsExternalDrag = false
private var mDragInProgress = false
private val mDeleteFolderOnDropCompleted = false
private val mSuppressFolderDeletion = false
private var mDeleteFolderOnDropCompleted = false
private var mSuppressFolderDeletion = false
private var mItemAddedBackToSelfViaIcon = false
var mFolderIconPivotX = 0f
......@@ -92,23 +99,22 @@ class Folder @JvmOverloads constructor(
private var mIsEditingName = false
@ExportedProperty(category = "launcher")
private val mDestroyed = false
private var mDestroyed = false
init {
setLocaleDependentFields(resources, false /* force */)
mLauncher = TestActivity.getLauncher(context)
launcher = TestActivity.getLauncher(context)
isFocusableInTouchMode = true
}
override fun onFinishInflate() {
super.onFinishInflate()
mContent = findViewById(R.id.folder_apps)
mContent.setFolder(this)
mPageIndicator = findViewById(R.id.indicator)
mFolderTitleInput = findViewById(R.id.folder_title)
mFolderTitleInput.setOnBackKeyListener(this)
mFolderTitleInput.setOnFocusChangeListener(this)
mFolderTitleInput.onFocusChangeListener = this
mFolderTitleInput.setOnEditorActionListener(this)
mFolderTitleInput.setSelectAllOnFocus(true)
mFolderTitleInput.inputType = mFolderTitleInput.inputType and
......@@ -124,23 +130,102 @@ class Folder @JvmOverloads constructor(
val item: LauncherItem = tag as LauncherItem
mEmptyCellRank = item.cell
mCurrentDragView = v
mDragController!!.addDragListener(this)
mLauncher.getLauncherPagedView().beginDragShared(v, this, options)
dragController!!.addDragListener(this)
launcher.getLauncherPagedView().beginDragShared(v, this, options)
}
return true
}
fun setLocaleDependentFields(res: Resources, force: Boolean) {
if (sDefaultFolderName == null || force) {
sDefaultFolderName = res.getString(R.string.untitled)
override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
ev?.let {
if (it.action == MotionEvent.ACTION_DOWN) {
val dl: DragLayer = launcher.dragLayer
if (isEditingName()) {
if (!dl.isEventOverView(mFolderTitleInput, ev)) {
mFolderTitleInput.dispatchBackKey()
return true
}
return false
} else if (!dl.isEventOverView(this, ev)) {
close(true)
return true
}
}
}
return false
}
override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
TODO("Not yet implemented")
override fun handleClose(animate: Boolean) {
mIsOpen = false
if (!animate && mCurrentAnimator != null && mCurrentAnimator!!.isRunning) {
mCurrentAnimator?.cancel()
}
if (isEditingName()) {
mFolderTitleInput.dispatchBackKey()
}
if (folderIcon != null) {
// mFolderIcon.clearLeaveBehindIfExists()
}
if (animate) {
animateClosed()
} else {
closeComplete(false)
}
}
override fun handleClose(animate: Boolean) {
private fun animateClosed() {
val a = FolderAnimationManager(this, false /* isOpening */).animator
a.play(ObjectAnimator.ofFloat(launcher.getLauncherPagedView(), View.ALPHA, 1f))
.with(ObjectAnimator.ofFloat(launcher.hotseat, View.ALPHA, 1f))
.with(
ObjectAnimator.ofFloat(
launcher.getLauncherPagedView().pageIndicator,
View.ALPHA,
1f
)
)
a.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
closeComplete(true)
}
})
startAnimation(a)
}
private fun closeComplete(wasAnimated: Boolean) {
// TODO: Clear all active animations.
(this.parent as DragLayer?)?.removeView(this)
clearFocus()
folderIcon?.apply {
launcher.getLauncherPagedView().alpha = 1f
launcher.getLauncherPagedView().pageIndicator.alpha = 1f
launcher.hotseat.alpha = 1f
if (wasAnimated) {
if (this.hasDot()) {
this.animateDotScale(0f, 1f)
}
}
}
if (mRearrangeOnClose) {
rearrangeChildren()
mRearrangeOnClose = false
}
if (getItemCount() <= 1) {
if (!mDragInProgress && !mSuppressFolderDeletion) {
replaceFolderWithFinalItem()
} else if (mDragInProgress) {
mDeleteFolderOnDropCompleted = true
}
}
mSuppressFolderDeletion = false
clearDragInfo()
mState = STATE_NONE
mContent.currentItem = 0
}
override fun isOfType(type: Int): Boolean = type and TYPE_FOLDER != 0
......@@ -152,19 +237,61 @@ class Folder @JvmOverloads constructor(
mInfo?.setTitle(newTitle)
// Update database
mLauncher.getLauncherPagedView().updateDatabase()
launcher.getLauncherPagedView().updateDatabase()
// This ensures that focus is gained every time the field is clicked, which selects all
// the text and brings up the soft keyboard if necessary.
mFolderTitleInput.clearFocus()
Selection.setSelection(mFolderTitleInput.getText(), 0, 0)
Selection.setSelection(mFolderTitleInput.text, 0, 0)
mIsEditingName = false
return true
}
override fun onTitleChanged(title: CharSequence?) {
TODO("Not yet implemented")
// This is used so the item doesn't immediately appear in the folder when added. In one case
// we need to create the illusion that the item isn't added back to the folder yet, to
// to correspond to the animation of the icon back into the folder. This is
fun hideItem(info: LauncherItem) {
getViewForInfo(info).apply {
visibility = INVISIBLE
}
}
fun showItem(info: LauncherItem) {
getViewForInfo(info)?.apply {
visibility = VISIBLE
}
}
override fun onAdd(item: LauncherItem) {
}
override fun onTitleChanged(title: CharSequence?) {}
override fun onRemove(item: LauncherItem) {
mItemsInvalidated = true
val v: View? = getViewForInfo(item)
mContent.adapter?.notifyDataSetChanged()
if (mState == STATE_ANIMATING) {
mRearrangeOnClose = true
} else {
rearrangeChildren()
}
if (getItemCount() <= 1) {
if (mIsOpen) {
close(true)
} else {
replaceFolderWithFinalItem()
}
}
}
private fun getViewForInfo(item: LauncherItem): View? {
return mContent.iterateOverItems { info, _, _ -> info === item }
}
override fun onItemsChanged(animate: Boolean) {
updateTextViewFocus()
}
override fun onDragStart(dragObject: DropTarget.DragObject, options: DragOptions) {
......@@ -177,7 +304,7 @@ class Folder @JvmOverloads constructor(
mItemsInvalidated = true
SuppressInfoChanges().use { _ ->
mInfo.remove(
dragObject.dragInfo as WorkspaceItemInfo,
dragObject.dragInfo as LauncherItem,
true
)
}
......@@ -191,7 +318,7 @@ class Folder @JvmOverloads constructor(
completeDragExit()
}
mDragInProgress = false
mDragController?.removeDragListener(this)
dragController?.removeDragListener(this)
}
fun isEditingName(): Boolean {
......@@ -213,12 +340,6 @@ class Folder @JvmOverloads constructor(
return false
}
fun getFolderIcon() = mFolderIcon
fun setFolderIcon(icon: IconTextView) {
mFolderIcon = icon
}
override fun onAttachedToWindow() {
// requestFocus() causes the focus onto the folder itself, which doesn't cause visual
// effect but the next arrow key can start the keyboard focus inside of the folder, not
......@@ -256,28 +377,135 @@ class Folder @JvmOverloads constructor(
mFolderTitleInput.setText(mInfo.title)
mFolderTitleInput.isCursorVisible = false
val mDeviceProfile: VariantDeviceProfile = mLauncher.deviceProfile
mContent?.adapter =
val mDeviceProfile: VariantDeviceProfile = launcher.deviceProfile
mContent.adapter =
FolderPagerAdapter(context, mInfo.items, mDeviceProfile)
// We use same size for height and width as we want to look it like sqaure
// We use same size for height and width as we want to look it like square
val height =
mDeviceProfile.cellHeightPx * 3 + resources.getDimensionPixelSize(R.dimen.folder_padding)
mContent?.layoutParams?.width =
mContent.layoutParams?.width =
mDeviceProfile.cellHeightPx * 3 + resources.getDimensionPixelSize(R.dimen.folder_padding) * 2
mContent?.layoutParams?.height =
mContent.layoutParams?.height =
(mDeviceProfile.cellHeightPx + mDeviceProfile.iconDrawablePaddingPx * 2) * 3 + resources.getDimensionPixelSize(
R.dimen.folder_padding
) * 2
mPageIndicator.setViewPager(mContent)
// In case any children didn't come across during loading, clean up the folder accordingly
mFolderIcon?.post {
folderIcon?.post {
if (getItemCount() <= 1) {
replaceFolderWithFinalItem()
}
}
}
private fun startAnimation(a: AnimatorSet) {
if (mCurrentAnimator != null && mCurrentAnimator!!.isRunning) {
mCurrentAnimator?.cancel()
}
val workspace: LauncherPagedView = launcher.getLauncherPagedView()
val currentCellLayout: CellLayout =
workspace.getChildAt(workspace.currentPage) as CellLayout
val useHardware = shouldUseHardwareLayerForAnimation(currentCellLayout)
val wasHardwareAccelerated: Boolean = currentCellLayout.isHardwareLayerEnabled()
a.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
if (useHardware) {
currentCellLayout.enableHardwareLayer(true)
}
mState = STATE_ANIMATING
mCurrentAnimator = a
}
override fun onAnimationEnd(animation: Animator) {
if (useHardware) {
currentCellLayout.enableHardwareLayer(wasHardwareAccelerated)
}
mCurrentAnimator = null
}
})
a.start()
}
private fun shouldUseHardwareLayerForAnimation(currentCellLayout: CellLayout): Boolean {
var folderCount = 0
for (i in currentCellLayout.childCount - 1 downTo 0) {
val child: View = currentCellLayout.getChildAt(i)
if (child is FolderIcon) ++folderCount
}
return folderCount >= MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION
}
/**
* Opens the user folder described by the specified tag. The opening of the folder
* is animated relative to the specified View. If the View is null, no animation
* is played.
*/
fun animateOpen() {
val openFolder = getOpen(launcher)
if (openFolder != null && openFolder !== this) {
// Close any open folder before opening a folder.
openFolder.close(true)
}
mIsOpen = true
val dragLayer = launcher.dragLayer
// Just verify that the folder hasn't already been added to the DragLayer.
// There was a one-off crash where the folder had a parent already.
if (parent == null) {
dragLayer.addView(this, BaseDragLayer.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
} else {
Log.e(
TAG,
"Opening folder (" + this + ") which already has a parent:" +
parent
)
}
// mContent.completePendingPageChanges()
if (!mDragInProgress) {
// Open on the first page.
mContent.currentItem = 0
}
// This is set to true in close(), but isn't reset to false until onDropCompleted(). This
// leads to an inconsistent state if you drag out of the folder and drag back in without
// dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice.
mDeleteFolderOnDropCompleted = false
// centerAboutIcon()
val anim: AnimatorSet = FolderAnimationManager(this, true /* isOpening */).animator
anim.play(ObjectAnimator.ofFloat(launcher.getLauncherPagedView(), View.ALPHA, 0f))
.with(ObjectAnimator.ofFloat(launcher.hotseat, View.ALPHA, 0f))
.with(
ObjectAnimator.ofFloat(
launcher.getLauncherPagedView().pageIndicator,
View.ALPHA,
0f
)
)
anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
}
override fun onAnimationEnd(animation: Animator) {
mState = STATE_OPEN
launcher.getLauncherPagedView().alpha = 0f
launcher.hotseat.alpha = 0f
launcher.getLauncherPagedView().pageIndicator.alpha = 0f
}
override fun onAnimationCancel(animation: Animator?) {
launcher.getLauncherPagedView().alpha = 1f
launcher.hotseat.alpha = 1f
launcher.getLauncherPagedView().pageIndicator.alpha = 1f
}
})
startAnimation(anim)
// Make sure the folder picks up the last drag move even if the finger doesn't move.
if (dragController!!.isDragging) {
dragController!!.forceTouchMove()
}
}
fun completeDragExit() {
if (mIsOpen) {
close(true)
......@@ -308,63 +536,150 @@ class Folder @JvmOverloads constructor(
* otherwise it is ignored.
*/
fun rearrangeChildren(itemCount: Int) {
val views: ArrayList<View> = getItemsInReadingOrder()
mContent.arrangeChildren(views, Math.max(itemCount, views.size))
mContent.adapter?.notifyDataSetChanged()
mItemsInvalidated = true
}
fun getItemCount(): Int {
return mContent.getItemCount()
}
override fun isDropEnabled(): Boolean {
TODO("Not yet implemented")
}
override fun onDrop(dragObject: DropTarget.DragObject?, options: DragOptions?) {
TODO("Not yet implemented")
}
override fun onDragEnter(dragObject: DropTarget.DragObject?) {
TODO("Not yet implemented")
fun isDestroyed(): Boolean {
return mDestroyed
}
private fun replaceFolderWithFinalItem() {
// Add the last remaining child to the workspace in place of the folder
val onCompleteRunnable = Runnable {
val itemCount: Int = mInfo.items.size
if (itemCount <= 1) {
var finalItem: LauncherItem? = null
if (itemCount == 1) {
// Move the item from the folder to the workspace, in the position of the
// folder
finalItem = mInfo.items.removeAt(0)
finalItem?.apply {
cell = mInfo.cell
screenId = mInfo.screenId
container = mInfo.container
}
}
// Remove the folder
launcher.getLauncherPagedView().removeItem(folderIcon, mInfo /* deleteFromDb */)
if (finalItem != null) {
// We add the child after removing the folder to prevent both from existing
// at the same time in the CellLayout. We need to add the new item with
// addInScreenFromBind() to ensure that hotseat items are placed correctly.
launcher.getLauncherPagedView().bindItems(listOf(finalItem), true)
}
launcher.getLauncherPagedView().updateDatabase()
}
}
onCompleteRunnable.run()
mDestroyed = true
}
override fun onDragOver(dragObject: DropTarget.DragObject?) {
TODO("Not yet implemented")
fun getItemCount(): Int {
Log.i(TAG, "getItemCount: " + mContent.getItemCount() + " " + mInfo.items.size)
return mInfo.items.size
}
// This method keeps track of the first and last item in the folder for the purposes
// of keyboard focus
fun updateTextViewFocus() {
val firstChild: View? = mContent.getFirstItem()
val lastChild: View? = mContent.getLastItem()
if (firstChild != null && lastChild != null) {
mFolderTitleInput.nextFocusDownId = lastChild.id
mFolderTitleInput.nextFocusRightId = lastChild.id
mFolderTitleInput.nextFocusLeftId = lastChild.id
mFolderTitleInput.nextFocusUpId = lastChild.id
// Hitting TAB from the folder name wraps around to the first item on the current
// folder page, and hitting SHIFT+TAB from that item wraps back to the folder name.
mFolderTitleInput.nextFocusForwardId = firstChild.id
// When clicking off the folder when editing the name, this Folder gains focus. When
// pressing an arrow key from that state, give the focus to the first item.
this.nextFocusDownId = firstChild.id
this.nextFocusRightId = firstChild.id
this.nextFocusLeftId = firstChild.id
this.nextFocusUpId = firstChild.id
// When pressing shift+tab in the above state, give the focus to the last item.
setOnKeyListener { _, keyCode, event ->
val isShiftPlusTab = keyCode == KeyEvent.KEYCODE_TAB &&
event.hasModifiers(KeyEvent.META_SHIFT_ON)
if (isShiftPlusTab && this@Folder.isFocused) {
lastChild.requestFocus()
} else false
}
}
}