Merge pull request #1245 from KryptKode/feat/bottom_cab

Feat/bottom cab
This commit is contained in:
Tibor Kaputa 2021-12-30 14:30:13 +01:00 committed by GitHub
commit 2cc4d857a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 750 additions and 45 deletions

View file

@ -1,13 +1,13 @@
package com.simplemobiletools.commons.adapters
import android.util.TypedValue
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.getTextSize
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.commons.views.bottomactionmenu.BottomActionMenuView
import kotlinx.android.synthetic.main.filepicker_favorite.view.*
class FilepickerFavoritesAdapter(
@ -35,7 +35,7 @@ class FilepickerFavoritesAdapter(
override fun getItemCount() = paths.size
override fun prepareActionMode(menu: Menu) {}
override fun onBottomActionMenuCreated(view: BottomActionMenuView) {}
override fun actionItemPressed(id: Int) {}
@ -47,8 +47,6 @@ class FilepickerFavoritesAdapter(
override fun getItemSelectionKey(position: Int) = paths[position].hashCode()
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
private fun setupView(view: View, path: String) {

View file

@ -3,7 +3,6 @@ package com.simplemobiletools.commons.adapters
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.util.TypedValue
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import com.bumptech.glide.Glide
@ -19,6 +18,7 @@ import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.getFilePlaceholderDrawables
import com.simplemobiletools.commons.models.FileDirItem
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.commons.views.bottomactionmenu.BottomActionMenuView
import kotlinx.android.synthetic.main.item_filepicker_list.view.*
import java.util.*
@ -55,7 +55,7 @@ class FilepickerItemsAdapter(
override fun getItemCount() = fileDirItems.size
override fun prepareActionMode(menu: Menu) {}
override fun onBottomActionMenuCreated(view: BottomActionMenuView) {}
override fun actionItemPressed(id: Int) {}
@ -67,8 +67,6 @@ class FilepickerItemsAdapter(
override fun getItemSelectionKey(position: Int) = fileDirItems[position].path.hashCode()
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onViewRecycled(holder: ViewHolder) {

View file

@ -1,6 +1,5 @@
package com.simplemobiletools.commons.adapters
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import com.simplemobiletools.commons.R
@ -10,6 +9,7 @@ import com.simplemobiletools.commons.extensions.deleteBlockedNumber
import com.simplemobiletools.commons.interfaces.RefreshRecyclerViewListener
import com.simplemobiletools.commons.models.BlockedNumber
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.commons.views.bottomactionmenu.BottomActionMenuView
import kotlinx.android.synthetic.main.item_manage_blocked_number.view.*
import java.util.*
@ -23,10 +23,8 @@ class ManageBlockedNumbersAdapter(
override fun getActionMenuId() = R.menu.cab_blocked_numbers
override fun prepareActionMode(menu: Menu) {
menu.apply {
findItem(R.id.cab_copy_number).isVisible = isOneItemSelected()
}
override fun onBottomActionMenuCreated(view: BottomActionMenuView) {
view.toggleItemVisibility(R.id.cab_copy_number, isOneItemSelected())
}
override fun actionItemPressed(id: Int) {
@ -44,8 +42,6 @@ class ManageBlockedNumbersAdapter(
override fun getItemKeyPosition(key: Int) = blockedNumbers.indexOfFirst { it.id.toInt() == key }
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_manage_blocked_number, parent)

View file

@ -16,6 +16,10 @@ import com.simplemobiletools.commons.extensions.getAdjustedPrimaryColor
import com.simplemobiletools.commons.extensions.getContrastColor
import com.simplemobiletools.commons.interfaces.MyActionModeCallback
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.commons.views.bottomactionmenu.BottomActionMenuView
import com.simplemobiletools.commons.views.bottomactionmenu.BottomActionMenuCallback
import com.simplemobiletools.commons.views.bottomactionmenu.BottomActionMenuItem
import com.simplemobiletools.commons.views.bottomactionmenu.BottomActionMenuPopup
import java.util.*
abstract class MyRecyclerViewAdapter(val activity: BaseSimpleActivity, val recyclerView: MyRecyclerView, val itemClick: (Any) -> Unit) :
@ -32,13 +36,15 @@ abstract class MyRecyclerViewAdapter(val activity: BaseSimpleActivity, val recyc
protected var selectedKeys = LinkedHashSet<Int>()
protected var positionOffset = 0
protected var actMode: ActionMode? = null
protected var contextCallback: BottomActionMenuCallback? = null
protected var contextPopup: BottomActionMenuPopup? = null
private var actBarTextView: TextView? = null
private var lastLongPressedItem = -1
abstract fun getActionMenuId(): Int
abstract fun prepareActionMode(menu: Menu)
abstract fun onBottomActionMenuCreated(view: BottomActionMenuView)
abstract fun actionItemPressed(id: Int)
@ -50,24 +56,27 @@ abstract class MyRecyclerViewAdapter(val activity: BaseSimpleActivity, val recyc
abstract fun getItemKeyPosition(key: Int): Int
abstract fun onActionModeCreated()
abstract fun onActionModeDestroyed()
protected fun isOneItemSelected() = selectedKeys.size == 1
init {
contextCallback = object : BottomActionMenuCallback {
override fun onViewCreated(view: BottomActionMenuView) {
onBottomActionMenuCreated(view)
}
override fun onItemClicked(item: BottomActionMenuItem) {
actionItemPressed(item.id)
}
}
actModeCallback = object : MyActionModeCallback() {
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
actionItemPressed(item.itemId)
return true
return false
}
override fun onCreateActionMode(actionMode: ActionMode, menu: Menu?): Boolean {
if (getActionMenuId() == 0) {
return true
}
isSelectable = true
actMode = actionMode
actBarTextView = layoutInflater.inflate(R.layout.actionbar_title, null) as TextView
@ -80,18 +89,16 @@ abstract class MyRecyclerViewAdapter(val activity: BaseSimpleActivity, val recyc
selectAll()
}
}
activity.menuInflater.inflate(getActionMenuId(), menu)
onActionModeCreated()
return true
}
override fun onPrepareActionMode(actionMode: ActionMode, menu: Menu): Boolean {
prepareActionMode(menu)
return true
return false
}
override fun onDestroyActionMode(actionMode: ActionMode) {
isSelectable = false
contextPopup?.dismiss()
(selectedKeys.clone() as HashSet<Int>).forEach {
val position = getItemKeyPosition(it)
if (position != -1) {
@ -143,6 +150,7 @@ abstract class MyRecyclerViewAdapter(val activity: BaseSimpleActivity, val recyc
if (oldTitle != newTitle) {
actBarTextView?.text = newTitle
actMode?.invalidate()
contextPopup?.invalidate()
}
}
@ -329,6 +337,7 @@ abstract class MyRecyclerViewAdapter(val activity: BaseSimpleActivity, val recyc
val currentPosition = adapterPosition - positionOffset
if (!actModeCallback.isSelectable) {
activity.startSupportActionMode(actModeCallback)
contextPopup = BottomActionMenuPopup(activity, getActionMenuId()).also { it.show(contextCallback) }
}
toggleItemSelection(true, currentPosition, true)

View file

@ -18,6 +18,10 @@ import com.simplemobiletools.commons.extensions.getAdjustedPrimaryColor
import com.simplemobiletools.commons.extensions.getContrastColor
import com.simplemobiletools.commons.interfaces.MyActionModeCallback
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.commons.views.bottomactionmenu.BottomActionMenuView
import com.simplemobiletools.commons.views.bottomactionmenu.BottomActionMenuCallback
import com.simplemobiletools.commons.views.bottomactionmenu.BottomActionMenuItem
import com.simplemobiletools.commons.views.bottomactionmenu.BottomActionMenuPopup
import java.util.*
abstract class MyRecyclerViewListAdapter<T>(
@ -39,13 +43,15 @@ abstract class MyRecyclerViewListAdapter<T>(
protected var selectedKeys = LinkedHashSet<Int>()
protected var positionOffset = 0
protected var actMode: ActionMode? = null
protected var contextCallback: BottomActionMenuCallback? = null
protected var contextPopup: BottomActionMenuPopup? = null
private var actBarTextView: TextView? = null
private var lastLongPressedItem = -1
abstract fun getActionMenuId(): Int
abstract fun prepareActionMode(menu: Menu)
abstract fun onBottomActionMenuCreated(view: BottomActionMenuView)
abstract fun actionItemPressed(id: Int)
@ -57,24 +63,27 @@ abstract class MyRecyclerViewListAdapter<T>(
abstract fun getItemKeyPosition(key: Int): Int
abstract fun onActionModeCreated()
abstract fun onActionModeDestroyed()
protected fun isOneItemSelected() = selectedKeys.size == 1
init {
contextCallback = object : BottomActionMenuCallback {
override fun onViewCreated(view: BottomActionMenuView) {
onBottomActionMenuCreated(view)
}
override fun onItemClicked(item: BottomActionMenuItem) {
actionItemPressed(item.id)
}
}
actModeCallback = object : MyActionModeCallback() {
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
actionItemPressed(item.itemId)
return true
return false
}
override fun onCreateActionMode(actionMode: ActionMode, menu: Menu?): Boolean {
if (getActionMenuId() == 0) {
return true
}
isSelectable = true
actMode = actionMode
actBarTextView = layoutInflater.inflate(R.layout.actionbar_title, null) as TextView
@ -87,18 +96,16 @@ abstract class MyRecyclerViewListAdapter<T>(
selectAll()
}
}
activity.menuInflater.inflate(getActionMenuId(), menu)
onActionModeCreated()
return true
}
override fun onPrepareActionMode(actionMode: ActionMode, menu: Menu): Boolean {
prepareActionMode(menu)
return true
return false
}
override fun onDestroyActionMode(actionMode: ActionMode) {
isSelectable = false
contextPopup?.dismiss()
(selectedKeys.clone() as HashSet<Int>).forEach {
val position = getItemKeyPosition(it)
if (position != -1) {
@ -150,6 +157,7 @@ abstract class MyRecyclerViewListAdapter<T>(
if (oldTitle != newTitle) {
actBarTextView?.text = newTitle
actMode?.invalidate()
contextPopup?.invalidate()
}
}
@ -336,6 +344,7 @@ abstract class MyRecyclerViewListAdapter<T>(
val currentPosition = adapterPosition - positionOffset
if (!actModeCallback.isSelectable) {
activity.startSupportActionMode(actModeCallback)
contextPopup = BottomActionMenuPopup(activity, getActionMenuId()).also { it.show(contextCallback) }
}
toggleItemSelection(true, currentPosition, true)

View file

@ -0,0 +1,7 @@
package com.simplemobiletools.commons.views.bottomactionmenu
interface BottomActionMenuCallback {
fun onItemClicked(item: BottomActionMenuItem){}
fun onViewCreated(view: BottomActionMenuView){}
fun onViewDestroyed(){}
}

View file

@ -0,0 +1,13 @@
package com.simplemobiletools.commons.views.bottomactionmenu
import android.view.View
import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
data class BottomActionMenuItem(
@IdRes val id: Int,
val title: String,
@DrawableRes val icon: Int = View.NO_ID,
val showAsAction: Boolean,
val isVisible: Boolean = true,
)

View file

@ -0,0 +1,253 @@
package com.simplemobiletools.commons.views.bottomactionmenu
import android.content.Context
import android.graphics.Rect
import android.view.*
import android.view.View.MeasureSpec
import android.widget.*
import androidx.core.content.ContextCompat
import androidx.core.widget.PopupWindowCompat
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.applyColorFilter
import com.simplemobiletools.commons.extensions.getAdjustedPrimaryColor
import com.simplemobiletools.commons.extensions.windowManager
import com.simplemobiletools.commons.helpers.isRPlus
import kotlinx.android.synthetic.main.item_action_mode_popup.view.*
import kotlin.math.ceil
class BottomActionMenuItemPopup(
private val context: Context,
private val items: List<BottomActionMenuItem>,
private val onSelect: (BottomActionMenuItem) -> Unit
) {
private val popup = PopupWindow(context, null, android.R.attr.popupMenuStyle)
private var anchorView: View? = null
private var dropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT
private var dropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT
private var dropDownVerticalOffset: Int = 0
private var dropDownHorizontalOffset: Int = 0
private val tempRect = Rect()
private val popupMinWidth: Int
private val popupPaddingBottom: Int
private val popupPaddingStart: Int
private val popupPaddingEnd: Int
private val popupPaddingTop: Int
private val dropDownGravity: Int = Gravity.TOP or Gravity.END
private val popupListAdapter = object : ArrayAdapter<BottomActionMenuItem>(context, R.layout.item_action_mode_popup, items) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var view = convertView
if (view == null) {
view = LayoutInflater.from(context).inflate(R.layout.item_action_mode_popup, parent, false)
}
val item = items[position]
view!!.cab_item.text = item.title
if (item.icon != View.NO_ID) {
val icon = ContextCompat.getDrawable(context, item.icon)
icon?.applyColorFilter(context.getAdjustedPrimaryColor())
view.cab_item.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
}
view.setOnClickListener {
onSelect.invoke(item)
popup.dismiss()
}
return view
}
}
init {
popup.isFocusable = true
popupMinWidth = context.resources.getDimensionPixelSize(R.dimen.cab_popup_menu_min_width)
popupPaddingStart = context.resources.getDimensionPixelSize(R.dimen.smaller_margin)
popupPaddingEnd = context.resources.getDimensionPixelSize(R.dimen.smaller_margin)
popupPaddingTop = context.resources.getDimensionPixelSize(R.dimen.smaller_margin)
popupPaddingBottom = context.resources.getDimensionPixelSize(R.dimen.smaller_margin)
}
fun show(anchorView: View) {
this.anchorView = anchorView
buildDropDown()
PopupWindowCompat.setWindowLayoutType(popup, WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL)
popup.isOutsideTouchable = true
if (popup.isShowing) {
popup.update(anchorView, dropDownHorizontalOffset, dropDownVerticalOffset, dropDownWidth, if (dropDownHeight < 0) -1 else dropDownHeight)
} else {
popup.width = dropDownWidth
popup.height = dropDownHeight
PopupWindowCompat.showAsDropDown(popup, anchorView, dropDownHorizontalOffset, dropDownVerticalOffset, dropDownGravity)
}
}
internal fun dismiss() {
popup.dismiss()
popup.contentView = null
}
internal fun setOnDismissListener(listener: (() -> Unit)?) {
if (listener != null) {
popup.setOnDismissListener { listener.invoke() }
} else {
popup.setOnDismissListener(null)
}
}
private fun buildDropDown() {
var otherHeights = 0
val dropDownList = ListView(context).apply {
adapter = popupListAdapter
isFocusable = true
divider = null
isFocusableInTouchMode = true
clipToPadding = false
isVerticalScrollBarEnabled = false
isHorizontalScrollBarEnabled = false
clipToOutline = true
setPaddingRelative(popupPaddingStart, popupPaddingTop, popupPaddingEnd, popupPaddingBottom)
}
val screenWidth = if (isRPlus()) {
context.windowManager.currentWindowMetrics.bounds.width()
} else {
context.windowManager.defaultDisplay.width
}
val width = measureMenuSizeAndGetWidth((0.8 * screenWidth).toInt())
updateContentWidth(width)
popup.contentView = dropDownList
// getMaxAvailableHeight() subtracts the padding, so we put it back
// to get the available height for the whole window.
val padding: Int
val popupBackground = popup.background
if (popupBackground != null) {
popupBackground.getPadding(tempRect)
padding = tempRect.top + tempRect.bottom
// If we don't have an explicit vertical offset, determine one from
// the window background so that content will line up.
dropDownVerticalOffset -= tempRect.top
} else {
tempRect.setEmpty()
padding = 0
}
if ((dropDownGravity and Gravity.BOTTOM) == Gravity.BOTTOM) {
dropDownVerticalOffset += anchorView!!.height
}
val maxHeight = popup.getMaxAvailableHeight(anchorView!!, dropDownVerticalOffset)
val listContent = measureHeightOfChildrenCompat(maxHeight - otherHeights)
if (listContent > 0) {
val listPadding = dropDownList.paddingTop + dropDownList.paddingBottom
otherHeights += padding + listPadding
}
dropDownHeight = listContent + otherHeights
dropDownList.layoutParams = ViewGroup.LayoutParams(dropDownWidth, dropDownHeight)
}
private fun updateContentWidth(width: Int) {
val popupBackground = popup.background
dropDownWidth = if (popupBackground != null) {
popupBackground.getPadding(tempRect)
tempRect.left + tempRect.right + width
} else {
width
}
}
/**
* @see androidx.appcompat.widget.DropDownListView.measureHeightOfChildrenCompat
*/
private fun measureHeightOfChildrenCompat(maxHeight: Int): Int {
val parent = FrameLayout(context)
val widthMeasureSpec = MeasureSpec.makeMeasureSpec(dropDownWidth, MeasureSpec.EXACTLY)
// Include the padding of the list
var returnedHeight = 0
val count = popupListAdapter.count
var child: View? = null
var viewType = 0
for (i in 0 until count) {
val positionType = popupListAdapter.getItemViewType(i)
if (positionType != viewType) {
child = null
viewType = positionType
}
child = popupListAdapter.getView(i, child, parent)
// Compute child height spec
val heightMeasureSpec: Int
var childLp: ViewGroup.LayoutParams? = child.layoutParams
if (childLp == null) {
childLp = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
child.layoutParams = childLp
}
heightMeasureSpec = if (childLp.height > 0) {
MeasureSpec.makeMeasureSpec(
childLp.height,
MeasureSpec.EXACTLY
)
} else {
MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
}
child.measure(widthMeasureSpec, heightMeasureSpec)
// Since this view was measured directly against the parent measure
// spec, we must measure it again before reuse.
child.forceLayout()
val marginLayoutParams = childLp as? ViewGroup.MarginLayoutParams
val topMargin = marginLayoutParams?.topMargin ?: 0
val bottomMargin = marginLayoutParams?.bottomMargin ?: 0
val verticalMargin = topMargin + bottomMargin
returnedHeight += child.measuredHeight + verticalMargin
if (returnedHeight >= maxHeight) {
// We went over, figure out which height to return. If returnedHeight >
// maxHeight, then the i'th position did not fit completely.
return maxHeight
}
}
// At this point, we went through the range of children, and they each
// completely fit, so return the returnedHeight
return returnedHeight
}
/**
* @see androidx.appcompat.view.menu.MenuPopup.measureIndividualMenuWidth
*/
private fun measureMenuSizeAndGetWidth(maxAllowedWidth: Int): Int {
val parent = FrameLayout(context)
var maxWidth = popupMinWidth
var itemView: View? = null
var itemType = 0
val widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
val heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
for (i in 0 until popupListAdapter.count) {
val positionType: Int = popupListAdapter.getItemViewType(i)
if (positionType != itemType) {
itemType = positionType
itemView = null
}
itemView = popupListAdapter.getView(i, itemView, parent)
itemView.measure(widthMeasureSpec, heightMeasureSpec)
val itemWidth = itemView.measuredWidth
if (itemWidth >= maxAllowedWidth) {
return maxAllowedWidth
} else if (itemWidth > maxWidth) {
maxWidth = itemWidth
}
}
return maxWidth
}
}

View file

@ -0,0 +1,79 @@
package com.simplemobiletools.commons.views.bottomactionmenu
import android.content.Context
import android.util.AttributeSet
import android.util.Xml
import android.view.MenuItem
import android.view.View
import com.simplemobiletools.commons.R
import org.xmlpull.v1.XmlPullParser
internal class BottomActionMenuParser(private val context: Context) {
companion object {
private const val NO_TEXT = ""
private const val MENU_TAG = "menu"
private const val MENU_ITEM_TAG = "item"
}
fun inflate(menuId: Int): List<BottomActionMenuItem> {
val parser = context.resources.getLayout(menuId)
parser.use {
val attrs = Xml.asAttributeSet(parser)
return readContextItems(parser, attrs)
}
}
private fun readContextItems(parser: XmlPullParser, attrs: AttributeSet): List<BottomActionMenuItem> {
val items = mutableListOf<BottomActionMenuItem>()
var eventType = parser.eventType
var tagName: String
// This loop will skip to the menu start tag
do {
if (eventType == XmlPullParser.START_TAG) {
tagName = parser.name
if (tagName == MENU_TAG) {
// Go to next tag
eventType = parser.next()
break
}
throw RuntimeException("Expecting menu, got $tagName")
}
eventType = parser.next()
} while (eventType != XmlPullParser.END_DOCUMENT)
var reachedEndOfMenu = false
while (!reachedEndOfMenu) {
tagName = parser.name
if (eventType == XmlPullParser.END_TAG) {
if (tagName == MENU_TAG) {
reachedEndOfMenu = true
}
}
if (eventType == XmlPullParser.START_TAG) {
when (tagName) {
MENU_ITEM_TAG -> items.add(readBottomActionMenuItem(parser, attrs))
}
}
eventType = parser.next()
}
return items
}
private fun readBottomActionMenuItem(parser: XmlPullParser, attrs: AttributeSet): BottomActionMenuItem {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomActionMenuItem)
val id = typedArray.getResourceId(R.styleable.BottomActionMenuItem_android_id, View.NO_ID)
val text = typedArray.getString(R.styleable.BottomActionMenuItem_android_title) ?: NO_TEXT
val iconId = typedArray.getResourceId(R.styleable.BottomActionMenuItem_android_icon, View.NO_ID)
val showAsAction = typedArray.getInt(R.styleable.BottomActionMenuItem_showAsAction, -1)
val visible = typedArray.getBoolean(R.styleable.BottomActionMenuItem_android_visible, true)
typedArray.recycle()
parser.require(XmlPullParser.START_TAG, null, MENU_ITEM_TAG)
return BottomActionMenuItem(id, text, iconId, showAsAction == MenuItem.SHOW_AS_ACTION_ALWAYS || showAsAction == MenuItem.SHOW_AS_ACTION_IF_ROOM, visible)
}
}

View file

@ -0,0 +1,74 @@
package com.simplemobiletools.commons.views.bottomactionmenu
import android.view.Gravity
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
import android.widget.PopupWindow
import androidx.annotation.MenuRes
import androidx.core.widget.PopupWindowCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.simplemobiletools.commons.activities.BaseSimpleActivity
class BottomActionMenuPopup(private val activity: BaseSimpleActivity, items: List<BottomActionMenuItem>) {
private val bottomActionMenuView = BottomActionMenuView(activity)
private val popup = PopupWindow(activity, null, android.R.attr.popupMenuStyle)
private var floatingActionButton: FloatingActionButton? = null
private var callback: BottomActionMenuCallback? = null
constructor(activity: BaseSimpleActivity, @MenuRes menuResId: Int) : this(activity, BottomActionMenuParser(activity).inflate(menuResId))
init {
popup.contentView = bottomActionMenuView
popup.width = ViewGroup.LayoutParams.MATCH_PARENT
popup.height = ViewGroup.LayoutParams.WRAP_CONTENT
popup.isOutsideTouchable = false
popup.setOnDismissListener {
callback?.onViewDestroyed()
floatingActionButton?.show()
}
PopupWindowCompat.setWindowLayoutType(popup, WindowManager.LayoutParams.TYPE_APPLICATION)
bottomActionMenuView.setup(items)
}
fun show(callback: BottomActionMenuCallback?, hideFab: Boolean = true) {
this.callback = callback
callback?.onViewCreated(bottomActionMenuView)
if (hideFab) {
floatingActionButton?.hide() ?: findFABAndHide()
}
bottomActionMenuView.setCallback(callback)
bottomActionMenuView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
popup.showAtLocation(bottomActionMenuView, Gravity.BOTTOM or Gravity.FILL_HORIZONTAL, 0, 0)
bottomActionMenuView.show()
}
fun dismiss() {
popup.dismiss()
}
private fun findFABAndHide() {
val parent = activity.findViewById<ViewGroup>(android.R.id.content)
findFab(parent)
floatingActionButton?.hide()
}
private fun findFab(parent: ViewGroup) {
val count = parent.childCount
for (i in 0 until count) {
val child = parent.getChildAt(i)
if (child is FloatingActionButton) {
floatingActionButton = child
break
} else if (child is ViewGroup) {
findFab(child)
}
}
}
fun invalidate() {
callback?.onViewCreated(bottomActionMenuView)
}
}

View file

@ -0,0 +1,228 @@
package com.simplemobiletools.commons.views.bottomactionmenu
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.TimeInterpolator
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewPropertyAnimator
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import androidx.annotation.MenuRes
import androidx.annotation.StringRes
import com.google.android.material.animation.AnimationUtils
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.isRPlus
class BottomActionMenuView : LinearLayout {
companion object {
private const val ENTER_ANIMATION_DURATION = 225
private const val EXIT_ANIMATION_DURATION = 175
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
private val inflater = LayoutInflater.from(context)
private val itemsLookup = LinkedHashMap<Int, BottomActionMenuItem>()
private val items: List<BottomActionMenuItem>
get() = itemsLookup.values.toList().sortedWith(compareByDescending<BottomActionMenuItem> { it.showAsAction }.thenBy { it.icon != View.NO_ID }).filter { it.isVisible }
private var currentAnimator: ViewPropertyAnimator? = null
private var callback: BottomActionMenuCallback? = null
init {
orientation = HORIZONTAL
elevation = 2f
setDefaultHeight()
}
private fun setDefaultHeight() {
val typedValue = TypedValue()
val defaultHeight = TypedValue.complexToDimensionPixelSize(typedValue.data, resources.displayMetrics)
minimumHeight = defaultHeight
}
fun setCallback(listener: BottomActionMenuCallback?) {
this.callback = listener
}
fun hide() {
slideDownToGone()
}
fun show() {
slideUpToVisible()
}
private fun slideUpToVisible() {
currentAnimator?.also {
it.cancel()
clearAnimation()
}
animateChildTo(0, ENTER_ANIMATION_DURATION.toLong(), AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR, true)
}
private fun slideDownToGone() {
currentAnimator?.also {
currentAnimator?.cancel()
clearAnimation()
}
animateChildTo(
height + (layoutParams as MarginLayoutParams).bottomMargin,
EXIT_ANIMATION_DURATION.toLong(),
AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR
)
}
private fun animateChildTo(targetY: Int, duration: Long, interpolator: TimeInterpolator, visible: Boolean = false) {
currentAnimator = animate()
.translationY(targetY.toFloat())
.setInterpolator(interpolator)
.setDuration(duration)
.setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
currentAnimator = null
beVisibleIf(visible)
}
})
}
fun createFromMenu(@MenuRes menuResId: Int) {
if (menuResId != View.NO_ID) {
val menuParser = BottomActionMenuParser(context)
val items = menuParser.inflate(menuResId)
setup(items)
}
}
fun setup(items: List<BottomActionMenuItem>) {
items.forEach { itemsLookup[it.id] = it }
init()
}
fun add(item: BottomActionMenuItem) {
setItem(item)
}
fun setItemShowAsAction(@IdRes itemId: Int, showAsAction: Boolean) {
val item = itemsLookup[itemId]
setItem(item?.copy(showAsAction = showAsAction))
}
fun changeItemIcon(@IdRes itemId: Int, @DrawableRes iconRes: Int) {
val item = itemsLookup[itemId]
setItem(item?.copy(icon = iconRes))
}
fun changeItemTitle(@IdRes itemId: Int, @StringRes stringRes: Int) {
changeItemTitle(itemId, context.getString(stringRes))
}
fun changeItemTitle(@IdRes itemId: Int, title: String) {
val item = itemsLookup[itemId]
setItem(item?.copy(title = title))
}
private fun setItem(item: BottomActionMenuItem?) {
item?.let {
itemsLookup[item.id] = item
init()
}
}
fun toggleItemVisibility(@IdRes itemId: Int, show: Boolean) {
val item = itemsLookup[itemId]
setItem(item?.copy(isVisible = show))
}
private fun init() {
removeAllViews()
val maxItemsBeforeOverflow = computeMaxItemsBeforeOverflow()
val allItems = items
for (i in allItems.indices) {
if (i <= maxItemsBeforeOverflow) {
drawNormalItem(allItems[i])
} else {
drawOverflowItem(allItems.slice(i until allItems.size))
break
}
}
}
private fun computeMaxItemsBeforeOverflow(): Int {
val itemsToShowAsAction = items.filter { it.showAsAction && it.icon != View.NO_ID }
val itemMinWidth = context.resources.getDimensionPixelSize(R.dimen.cab_item_min_width)
val totalActionWidth = (itemsToShowAsAction.size + 1) * itemMinWidth
val screenWidth = if (isRPlus()) {
context.windowManager.currentWindowMetrics.bounds.width()
} else {
context.windowManager.defaultDisplay.width
}
val result = if (screenWidth > totalActionWidth) {
itemsToShowAsAction.size
} else {
screenWidth / itemMinWidth
}
return result - 1
}
private fun drawNormalItem(item: BottomActionMenuItem) {
(inflater.inflate(R.layout.item_action_mode, this, false) as ImageView).apply {
setupItem(item)
setOnClickListener {
callback?.onItemClicked(item)
}
setOnLongClickListener {
context.toast(item.title)
true
}
addView(this)
}
}
private fun drawOverflowItem(overFlowItems: List<BottomActionMenuItem>) {
(inflater.inflate(R.layout.item_action_mode, this, false) as ImageView).apply {
setImageResource(R.drawable.ic_three_dots_vector)
val contentDesc = context.getString(R.string.more_info)
contentDescription = contentDesc
applyColorFilter(context.getAdjustedPrimaryColor())
val popup = getOverflowPopup(overFlowItems)
setOnClickListener {
popup.show(it)
}
setOnLongClickListener {
context.toast(contentDesc)
true
}
addView(this)
}
}
private fun ImageView.setupItem(item: BottomActionMenuItem) {
id = item.id
contentDescription = item.title
if (item.icon != View.NO_ID) {
setImageResource(item.icon)
}
beVisibleIf(item.isVisible)
applyColorFilter(context.getAdjustedPrimaryColor())
}
private fun getOverflowPopup(overFlowItems: List<BottomActionMenuItem>): BottomActionMenuItemPopup {
return BottomActionMenuItemPopup(context, overFlowItems) {
callback?.onItemClicked(it)
}
}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@null"
android:minWidth="@dimen/cab_item_min_width"
android:padding="@dimen/normal_margin"
tools:src="@drawable/ic_rename_vector"
tools:tint="@color/color_primary" />

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<com.simplemobiletools.commons.views.MyTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cab_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/listChoiceBackgroundIndicator"
android:contentDescription="@null"
android:drawablePadding="@dimen/medium_margin"
android:gravity="center_vertical"
android:padding="@dimen/normal_margin"
android:textSize="@dimen/bigger_text_size"
tools:drawableStart="@drawable/ic_rename_vector"
tools:drawableTint="@color/color_primary"
tools:text="@tools:sample/cities" />

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BottomActionMenuItem">
<attr name="android:id" />
<attr name="android:title" />
<attr name="android:icon" />
<attr name="android:visible" />
<attr name="showAsAction" />
</declare-styleable>
</resources>

View file

@ -20,6 +20,8 @@
<dimen name="colorpicker_color_width">70dp</dimen>
<dimen name="normal_icon_size">48dp</dimen>
<dimen name="fastscroll_width">8dp</dimen>
<dimen name="fastscroll_height">40dp</dimen>
<dimen name="fingerprint_icon_size">72dp</dimen>
<dimen name="fab_size">56dp</dimen>
<dimen name="secondary_fab_bottom_margin">76dp</dimen>
@ -48,4 +50,8 @@
<dimen name="big_text_size">18sp</dimen>
<dimen name="actionbar_text_size">20sp</dimen>
<dimen name="extra_big_text_size">22sp</dimen>
<dimen name="cab_popup_menu_max_width">336dp</dimen>
<dimen name="cab_popup_menu_min_width">168dp</dimen>
<dimen name="cab_item_min_width">85dp</dimen>
</resources>

View file

@ -1,6 +1,5 @@
package com.simplemobiletools.commons.samples.activities
import android.view.Menu
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
@ -15,6 +14,7 @@ import com.simplemobiletools.commons.interfaces.ItemTouchHelperContract
import com.simplemobiletools.commons.interfaces.StartReorderDragListener
import com.simplemobiletools.commons.samples.R
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.commons.views.bottomactionmenu.BottomActionMenuView
import kotlinx.android.synthetic.main.list_item.view.*
import java.util.*
@ -41,7 +41,7 @@ class StringsAdapter(
override fun getActionMenuId() = R.menu.cab_delete_only
override fun prepareActionMode(menu: Menu) {}
override fun onBottomActionMenuCreated(view: BottomActionMenuView) {}
override fun actionItemPressed(id: Int) {
if (selectedKeys.isEmpty()) {
@ -63,8 +63,6 @@ class StringsAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.list_item, parent)
override fun onActionModeCreated() {}
override fun onActionModeDestroyed() {
if (isChangingOrder) {
notifyDataSetChanged()