Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4bf4103d authored by Amit Kumar's avatar Amit Kumar 💻
Browse files

Add custom folder container to enhance folder UX and remove bugs

parent f8b1d573
Loading
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import foundation.e.blisslauncher.features.test.TouchController;
public abstract class AbstractFloatingView extends LinearLayout implements TouchController {

    @IntDef(flag = true, value = {
        TYPE_FOLDER,
        TYPE_TASK_MENU,
        TYPE_OPTIONS_POPUP,
        TYPE_LISTENER
@@ -48,13 +49,15 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch
    public @interface FloatingViewType {
    }

    public static final int TYPE_FOLDER = 1 << 0;
    public static final int TYPE_LISTENER = 1 << 1;

    // Popups related to quickstep UI
    public static final int TYPE_TASK_MENU = 1 << 2;
    public static final int TYPE_OPTIONS_POPUP = 1 << 3;

    public static final int TYPE_ALL = TYPE_TASK_MENU | TYPE_OPTIONS_POPUP | TYPE_LISTENER;
    public static final int TYPE_ALL =
        TYPE_FOLDER | TYPE_TASK_MENU | TYPE_OPTIONS_POPUP | TYPE_LISTENER;

    public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_LISTENER;

+383 −0
Original line number Diff line number Diff line
package foundation.e.blisslauncher.core.customviews

import android.animation.AnimatorSet
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.view.FocusFinder
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewDebug.ExportedProperty
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.FolderPagerAdapter
import foundation.e.blisslauncher.features.test.Alarm
import foundation.e.blisslauncher.features.test.IconTextView
import foundation.e.blisslauncher.features.test.OnAlarmListener
import foundation.e.blisslauncher.features.test.TestActivity
import foundation.e.blisslauncher.features.test.VariantDeviceProfile
import foundation.e.blisslauncher.features.test.dragndrop.DragController
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

class Folder @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : AbstractFloatingView(context, attrs), DropTarget, DragController.DragListener,
    FolderTitleInput.OnBackKeyListener, FolderItem.FolderListener,
    OnEditorActionListener, DragSource {

    /**
     * Fraction of icon width which behave as scroll region.
     */
    private val ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f

    private val FOLDER_NAME_ANIMATION_DURATION = 633L

    private val REORDER_DELAY = 250L
    private val ON_EXIT_CLOSE_DELAY = 400L
    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
    lateinit var mInfo: FolderItem
    private val mCurrentAnimator: AnimatorSet? = null

    var mFolderIcon: IconTextView? = null

    lateinit var mContent: ViewPager
    lateinit var mFolderTitleInput: FolderTitleInput
    private lateinit var mPageIndicator: CircleIndicator

    // Cell ranks used for drag and drop
    var mTargetRank = 0
    var mPrevTargetRank = 0
    var mEmptyCellRank = 0

    var mState: Int = STATE_NONE

    private var mRearrangeOnClose = false
    var mItemsInvalidated = false
    private var mCurrentDragView: View? = null
    private var mIsExternalDrag = false
    private var mDragInProgress = false
    private val mDeleteFolderOnDropCompleted = false
    private val mSuppressFolderDeletion = false
    private var mItemAddedBackToSelfViaIcon = false

    var mFolderIconPivotX = 0f

    private var mIsEditingName = false

    @ExportedProperty(category = "launcher")
    private val mDestroyed = false

    init {
        setLocaleDependentFields(resources, false /* force */)
        mLauncher = 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.setOnEditorActionListener(this)
        mFolderTitleInput.setSelectAllOnFocus(true)
        mFolderTitleInput.inputType = mFolderTitleInput.inputType and
            InputType.TYPE_TEXT_FLAG_AUTO_CORRECT.inv() and
            InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS.inv() or
            InputType.TYPE_TEXT_FLAG_CAP_WORDS
        mFolderTitleInput.forceDisableSuggestions(true)
    }

    fun startDrag(v: View, options: DragOptions): Boolean {
        val tag = v.tag
        if (tag is LauncherItem) {
            val item: LauncherItem = tag as LauncherItem
            mEmptyCellRank = item.cell
            mCurrentDragView = v
            mDragController!!.addDragListener(this)
            mLauncher.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 {
        TODO("Not yet implemented")
    }

    override fun handleClose(animate: Boolean) {
    }

    override fun isOfType(type: Int): Boolean = type and TYPE_FOLDER != 0

    override fun onBackKey(): Boolean {
        // Convert to a string here to ensure that no other state associated with the text field
        // gets saved.
        val newTitle: String = mFolderTitleInput.text.toString()
        mInfo?.setTitle(newTitle)

        // Update database
        mLauncher.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)
        mIsEditingName = false
        return true
    }

    override fun onTitleChanged(title: CharSequence?) {
        TODO("Not yet implemented")
    }

    override fun onDragStart(dragObject: DropTarget.DragObject, options: DragOptions) {
        if (dragObject.dragSource != this) {
            return
        }

        mContent.removeItem(mCurrentDragView)
        if (dragObject.dragInfo is LauncherItem) {
            mItemsInvalidated = true
            SuppressInfoChanges().use { _ ->
                mInfo.remove(
                    dragObject.dragInfo as WorkspaceItemInfo,
                    true
                )
            }
        }
        mDragInProgress = true
        mItemAddedBackToSelfViaIcon = false
    }

    override fun onDragEnd() {
        if (mIsExternalDrag && mDragInProgress) {
            completeDragExit()
        }
        mDragInProgress = false
        mDragController?.removeDragListener(this)
    }

    fun isEditingName(): Boolean {
        return mIsEditingName
    }

    private fun startEditingFolderName() {
        post {
            mFolderTitleInput.hint = ""
            mIsEditingName = true
        }
    }

    override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
        if (actionId == EditorInfo.IME_ACTION_DONE) {
            mFolderTitleInput.dispatchBackKey()
            return true
        }
        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
        // the folder itself.
        requestFocus()
        super.onAttachedToWindow()
    }

    override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent?): Boolean =
        // When the folder gets focus, we don't want to announce the list of items.
        true

    override fun focusSearch(direction: Int): View? =
        // When the folder is focused, further focus search should be within the folder contents.
        FocusFinder.getInstance().findNextFocus(this, null, direction)

    /**
     * @return the FolderInfo object associated with this folder
     */
    fun getInfo(): FolderItem? {
        return mInfo
    }

    fun bind(info: FolderItem) {
        mInfo = info
        val children: MutableList<LauncherItem> = info.items.toMutableList()
        children.sortWith { lhs, rhs ->
            lhs.cell - rhs.cell
        }

        mItemsInvalidated = true
        updateTextViewFocus()
        mInfo.addListener(this)

        mFolderTitleInput.setText(mInfo.title)
        mFolderTitleInput.isCursorVisible = false

        val mDeviceProfile: VariantDeviceProfile = mLauncher.deviceProfile
        mContent?.adapter =
            FolderPagerAdapter(context, mInfo.items, mDeviceProfile)
        // We use same size for height and width as we want to look it like sqaure
        val height =
            mDeviceProfile.cellHeightPx * 3 + resources.getDimensionPixelSize(R.dimen.folder_padding)
        mContent?.layoutParams?.width =
            mDeviceProfile.cellHeightPx * 3 + resources.getDimensionPixelSize(R.dimen.folder_padding) * 2
        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 {
            if (getItemCount() <= 1) {
                replaceFolderWithFinalItem()
            }
        }
    }

    fun completeDragExit() {
        if (mIsOpen) {
            close(true)
            mRearrangeOnClose = true
        } else if (mState == STATE_ANIMATING) {
            mRearrangeOnClose = true
        } else {
            rearrangeChildren()
            clearDragInfo()
        }
    }

    private fun clearDragInfo() {
        mCurrentDragView = null
        mIsExternalDrag = false
    }

    /**
     * Rearranges the children based on their rank.
     */
    fun rearrangeChildren() {
        rearrangeChildren(-1)
    }

    /**
     * Rearranges the children based on their rank.
     * @param itemCount if greater than the total children count, empty spaces are left at the end,
     * otherwise it is ignored.
     */
    fun rearrangeChildren(itemCount: Int) {
        val views: ArrayList<View> = getItemsInReadingOrder()
        mContent.arrangeChildren(views, Math.max(itemCount, views.size))
        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")
    }

    override fun onDragOver(dragObject: DropTarget.DragObject?) {
        TODO("Not yet implemented")
    }

    var mOnExitAlarmListener: OnAlarmListener = OnAlarmListener { completeDragExit() }

    override fun onDragExit(d: DropTarget.DragObject) {
        // We only close the folder if this is a true drag exit, ie. not because
        // a drop has occurred above the folder.
        if (!d.dragComplete) {
            mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener)
            mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY)
        }
        mReorderAlarm.cancelAlarm()
    }

    override fun acceptDrop(dragObject: DropTarget.DragObject?): Boolean {
        TODO("Not yet implemented")
    }

    override fun prepareAccessibilityDrop() {
        TODO("Not yet implemented")
    }

    override fun onDropCompleted(target: View?, d: DropTarget.DragObject?, success: Boolean) {
    }

    override fun getHitRectRelativeToDragLayer(outRect: Rect?) {
        TODO("Not yet implemented")
    }

    companion object {
        const val STATE_NONE = -1
        const val STATE_SMALL = 0
        const val STATE_ANIMATING = 1
        const val STATE_OPEN = 2
    }

    /**
     * Temporary resource held while we don't want to handle info changes
     */
    inner class SuppressInfoChanges internal constructor() : AutoCloseable {
        override fun close() {
            mInfo.addListener(this@Folder)
            updateTextViewFocus()
        }

        init {
            mInfo.removeListener(this@Folder)
        }
    }
}
 No newline at end of file
+11 −3
Original line number Diff line number Diff line
@@ -31,12 +31,16 @@ import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.OvershootInterpolator;
import android.widget.GridLayout;
import android.widget.Toast;

import androidx.viewpager.widget.ViewPager;

import foundation.e.blisslauncher.BuildConfig;
import foundation.e.blisslauncher.R;
import foundation.e.blisslauncher.core.Utilities;
@@ -80,6 +84,7 @@ import foundation.e.blisslauncher.features.test.dragndrop.DropTarget;
import foundation.e.blisslauncher.features.test.dragndrop.SpringLoadedDragController;
import foundation.e.blisslauncher.features.test.graphics.DragPreviewProvider;
import foundation.e.blisslauncher.features.test.uninstall.UninstallHelper;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -89,6 +94,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;

import org.jetbrains.annotations.NotNull;

public class LauncherPagedView extends PagedView<PageIndicatorDots> implements View.OnTouchListener,
@@ -1458,9 +1464,11 @@ public class LauncherPagedView extends PagedView<PageIndicatorDots> implements V
    public void onDragStart(
        DragObject dragObject, DragOptions options
    ) {
        if (mDragInfo != null && mDragInfo.getCell() != null) {
            CellLayout layout = (CellLayout) mDragInfo.getCell().getParent();
            layout.markCellsAsUnoccupiedForView(mDragInfo.getCell());
        if (mDragInfo != null) {
            ViewParent parent = mDragInfo.getCell().getParent();
            if (parent instanceof CellLayout) {
                ((CellLayout) parent).markCellsAsUnoccupiedForView(mDragInfo.getCell());
            }
        }

        if (mOutlineProvider != null) {
+2 −3
Original line number Diff line number Diff line
@@ -57,8 +57,7 @@ public class ItemLongClickListener {
        View v, TestActivity launcher, LauncherItem info,
        DragOptions dragOptions
    ) {
        //TODO: Enable when supporting folders
       /* if (info.container >= 0) {
        if (info.container >= 0) {
            Folder folder = Folder.getOpen(launcher);
            if (folder != null) {
                if (!folder.getItemsInReadingOrder().contains(v)) {
@@ -68,7 +67,7 @@ public class ItemLongClickListener {
                    return;
                }
            }
        }*/
        }

        CellLayout.CellInfo longClickCellInfo = new CellLayout.CellInfo(v, info);
        launcher.getLauncherPagedView().startDrag(longClickCellInfo, dragOptions);
+3 −0
Original line number Diff line number Diff line
@@ -24,6 +24,8 @@ import android.content.ContextWrapper;
import android.content.Intent;
import android.view.View.AccessibilityDelegate;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;

import foundation.e.blisslauncher.core.utils.ViewCache;
import java.lang.annotation.Retention;
import java.util.ArrayList;
@@ -87,6 +89,7 @@ public abstract class BaseActivity extends Activity {
        return mViewCache;
    }

    @NonNull
    public VariantDeviceProfile getDeviceProfile() {
        return mDeviceProfile;
    }
Loading