FloatingActionMode - contextual action bar for Android

    Contextual actions with list items are widely used with Android applications. It is quite convenient to select several elements or all elements of the list and apply some action to all selected elements at once. Delete, for example.


    In Android applications, this can be used ActionMode, which allows you to display the available actions on selected elements on top Toolbar. There you can show the user how many elements are currently selected or other useful information. This is convenient and looks good, but in some cases, the information displayed on the screen itself Toolbarmay be important and would not be desirable to hide it. For example, there may be a name and photo of the user, a list of messages with which is displayed in the list. When highlighting some messages, it would be useful to see the name of the user to whom these messages are addressed.


    In this case, you can display the contextual actions panel with list items on top of the list itself without blocking it Toolbar. I will talk about creating such a contextual action panel in this article.


    Developed CustomView - a panel of contextual actions I called FloatingActionModeor simply FAM.


    Art
    FloatingActionMode during operation (Locked from below)


    Video - an example of working with FloatingActionMode (Fixed below)


    In the comments, it was pointed out that it may not be very convenient for the user to drag the panel around the screen, so it can be fixed at the bottom of the screen, as shown in the screenshots and video above. (To do this, specify the attributes android:layout_gravity="bottom"and app:fam_can_drag="false").


    At the same time, you can allow the user to move FAMaround the screen, as shown in the following screenshots and video.


    Art
    FloatingActionMode during work


    Video - an example of working with FloatingActionMode (Drag and Drop)


    It FAMdoesn’t have a default background, so you can use whatever you need. Also, the attribute can be used to create a shadow on devices with API> = 21android:translationZ="8dp"


    XML attributes


    To configure it FAMthrough a markup file, several special attributes are defined for it, which can also be changed programmatically:


    • fam_openeddetermines whether it will be FAMopen at creation. ( falsedefault)


    • fam_content_resthis LayoutResthat represents the content FAM(a few buttons, for example). ViewThe created from is fam_content_resadded FAMas a child View. Content can be changed programmatically while the application is running, so FAMan attribute can be specified android:animateLayoutChanges="true"for an animated change in content. (no content by default)


    • fam_can_closedetermines whether to FAMhave a button to close. ( truedefault)


    • fam_close_iconthese are the DrawableResclose buttons. (the default value is a cross)


    • fam_can_dragdetermines whether to FAMhave a button to drag and drop. ( truedefault)


    • fam_drag_iconthese are DrawableResdrag and drop buttons. (there is a default value)


    • fam_can_dismissdetermines whether FAMit closes if the user drags it horizontally far enough ( trueby default)


    • fam_dismiss_thresholdthis is the horizontal shift threshold starting from which it FAMwill be closed when the user releases fam_drag_button. That is, if ( getTranslationX/ getWidth)> dismissThreshold, then it FAMwill be closed. ( 0.4fdefault)


    • fam_minimize_directiondetermines the direction in which it will move FAMduring folding. This attribute can have the following values ​​( nearestdefault):


      • top- FAMwill move to the upper border of the parent (excluding indentation) during folding
      • bottom- FAMwill move to the lower border of the parent (excluding indentation) during folding
      • nearest- FAMwill move to the nearest (upper or lower) border of the parent (excluding indentation) during folding

    • fam_animation_durationdetermines the duration of the minimize / expand animation. ( 400ms by default)

    FAMalso has OnCloseListener, which allows you to perform a specific action when the FAMuser closes (deselect from list items, for example).


    Main actions


    The main actions with FAMare opening / closing and folding / unfolding. When it opens, it appears and unfolds, and when closed, it folds and disappears.


    The deployment is FAMaccompanied by animation, during which it moves from the top or bottom edge of the parent ViewGroup(this edge is set by the attribute fam_minimize_direction) to its position specified by the markup file. Animation is defined as follows:


    animate()
                .scaleY(1f)
                .scaleX(1f)
                .translationY(calculateArrangeTranslationY())
                .alpha(1f)

    When minimized, the animation is performed "in the opposite direction":


    animate()
                .scaleY(0.5f)
                .scaleX(0.5f)
                .translationY(calculateMinimizeTranslationY())
                .alpha(0.5f)

    The methods calculateArrangeTranslationY()and calculateMinimizeTranslationY()allow us translationYto calculate for the expanded and collapsed states, respectively, taking into account where the FAMuser dragged , the attribute fam_minimize_directionand indentation from the bottom and top, which will be discussed later.


    Close and drag


    For correct and beautiful work, it FAMhas buttons ( ImageView) with which the user can close the context action mode or drag vertically to another part of the screen (if it blocks the desired list item). It FAMcan also be closed by dragging it horizontally (swipe to dismiss).


    FAMrepresents LinearLayout, in which, when creating, buttons are added for closing ( fam_drag_button) and dragging ( fam_close_button). The ability to close / drag FAMcan be turned on / off while the application is running, therefore LinearLayout, the one containing these buttons has an attribute android:animateLayoutChanges="true".


    FAM markup

    The drag and drop mechanism is implemented with the help of OnTouchListenerwhich remembers the starting point of the touch and when moving sets translationXand translationYaccordingly touch. When the user releases the drag and drop button ( fam_drag_button), he FAMreturns to the initial horizontal position and, if the user dragged FAMfar enough horizontally, the method is called this@FloatingActionMode.close().


    Ontouchlistener
    fam_drag_button.setOnTouchListener(object : OnTouchListener {
                var prevTransitionY = 0f
                var startRawX = 0f
                var startRawY = 0f
                override fun onTouch(v: View, event: MotionEvent): Boolean {
                    if (!this@FloatingActionMode.canDrag) {
                        return false
                    }
                    val fractionX = Math.abs(event.rawX - startRawX) / this@FloatingActionMode.width
                    when (event.actionMasked) {
                        MotionEvent.ACTION_DOWN -> {
                            this@FloatingActionMode.fam_drag_button.isPressed = true
                            startRawX = event.rawX
                            startRawY = event.rawY
                            prevTransitionY = this@FloatingActionMode.translationY
                        }
                        MotionEvent.ACTION_MOVE -> {
                           this@FloatingActionMode.maximizeTranslationY =
                                    prevTransitionY + event.rawY - startRawY
                            translationX = event.rawX - startRawX
                            if (canDismiss) {
                                val alpha =
                                        if (fractionX < dismissThreshold)
                                            1.0f
                                        else
                                            Math.pow(1.0 - (fractionX - dismissThreshold)
                                                    / (1 - dismissThreshold), 4.0).toFloat()
                                this@FloatingActionMode.alpha = alpha
                            }
                        }
                        MotionEvent.ACTION_UP -> {
                            fam_drag_button.isPressed = false
                            this@FloatingActionMode.animate().translationX(0f)
                                    .duration = animationDuration
                            if (canDismiss && fractionX > dismissThreshold) {
                                this@FloatingActionMode.close()
                            }
                        }
                    }
                    return true
                }
            })

    Use in CoordinatorLayout


    It mentioned earlier, that the methods calculateArrangeTranslationY()and calculateMinimizeTranslationY()take into account the margins at the top and bottom to determine the correct position FAM. These paddings are calculated using FloatingActionModeBehavior- an extension CoordinatorLayout.Behaviorthat defines the top padding as height AppBarLayoutand the bottom padding as the height of the visible part Snackbar.SnackbarLayout.


    It also FloatingActionModeBehaviorallows you to FAMrespond to scrolling, folding when scrolling down and turning around when scrolling up (quick return pattern).


    FloatingActionModeBehavior
        open class FloatingActionModeBehavior
        @JvmOverloads constructor(context: Context? = null, attrs: AttributeSet? = null)
            : CoordinatorLayout.Behavior(context, attrs) {
            override fun layoutDependsOn(parent: CoordinatorLayout?,
                                         child: FloatingActionMode?, dependency: View?): Boolean {
                return dependency is AppBarLayout || dependency is Snackbar.SnackbarLayout
            }
            override fun onDependentViewChanged(parent: CoordinatorLayout,
                                                child: FloatingActionMode, dependency: View): Boolean {
                when (dependency) {
                    is AppBarLayout -> child.topOffset = dependency.bottom
                    is Snackbar.SnackbarLayout ->
                        child.bottomOffset = dependency.height - dependency.translationY.toInt()
                }
                return false
            }
            override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout?,
                                             child: FloatingActionMode?, directTargetChild: View?,
                                             target: View?, nestedScrollAxes: Int): Boolean {
                return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL
            }
            override fun onNestedScroll(coordinatorLayout: CoordinatorLayout,
                                        child: FloatingActionMode, target: View, dxConsumed: Int,
                                        dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {
                super.onNestedScroll(coordinatorLayout, child, target,
                        dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed)
                // FAM не должен реагировать на скроллинг своих дочерних View.
                var parent = target.parent
                while (parent != coordinatorLayout) {
                    if (parent == child) {
                        return
                    }
                    parent = parent.parent
                }
                if (dyConsumed > 0) {
                    child.minimize(true)
                } else if (dyConsumed < 0) {
                    child.maximize(true)
                }
            }
        }

    This FAMmay look like this in a markup file:


    
            ...
        

    Source


    Source code FloatingActionModeis available on GitHub ( library directory ). There is also a demo application using FAM( app directory ).


    Itself FloatingActionMode, as well as FloatingActionModeBehaviordefined as openclasses, so you can upgrade them as you need. Key methods are FloatingActionModealso defined as open.


    Thanks for attention. Happy coding!


    Also popular now: