Add new bottom actionbar to chat list.

This commit is contained in:
Greyson Parrelli
2021-10-21 10:55:33 -04:00
parent 2167522f7d
commit f533a898f5
23 changed files with 335 additions and 199 deletions

View File

@@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.components.menu
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
/**
* Represents an action to be rendered via [SignalContextMenu] or [SignalBottomActionBar]
*/
data class ActionItem(
@DrawableRes val iconRes: Int,
@StringRes val titleRes: Int,
val action: Runnable
)

View File

@@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.components.menu
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.ViewUtil
/**
* A bar that displays a set of action buttons. Intended as a replacement for ActionModes, this gives you a simple interface to add a bunch of actions, and
* the bar itself will handle putting things in the overflow and whatnot.
*
* Overflow items are rendered in a [SignalContextMenu].
*/
class SignalBottomActionBar(context: Context, attributeSet: AttributeSet) : LinearLayout(context, attributeSet) {
val items: MutableList<ActionItem> = mutableListOf()
init {
orientation = HORIZONTAL
setBackgroundResource(R.drawable.signal_bottom_action_bar_background)
if (Build.VERSION.SDK_INT >= 21) {
elevation = 20f
}
}
fun setItems(items: List<ActionItem>) {
this.items.clear()
this.items.addAll(items)
present(this.items)
}
private fun present(items: List<ActionItem>) {
if (width == 0) {
post { present(items) }
return
}
val widthDp: Float = ViewUtil.pxToDp(width.toFloat())
val minButtonWidthDp = 70
val maxButtons: Int = (widthDp / minButtonWidthDp).toInt()
val usableButtonCount = when {
items.size <= maxButtons -> items.size
else -> maxButtons - 1
}
val renderableItems: List<ActionItem> = items.subList(0, usableButtonCount)
val overflowItems: List<ActionItem> = if (renderableItems.size < items.size) items.subList(usableButtonCount, items.size) else emptyList()
removeAllViews()
renderableItems.forEach { item ->
val view: View = LayoutInflater.from(context).inflate(R.layout.signal_bottom_action_bar_item, this, false)
addView(view)
bindItem(view, item)
}
if (overflowItems.isNotEmpty()) {
val view: View = LayoutInflater.from(context).inflate(R.layout.signal_bottom_action_bar_item, this, false)
addView(view)
bindItem(
view,
ActionItem(
iconRes = R.drawable.ic_more_horiz_24,
titleRes = R.string.SignalBottomActionBar_more,
action = {
SignalContextMenu.Builder(view, parent as ViewGroup)
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.END)
.offsetY(ViewUtil.dpToPx(8))
.show(overflowItems)
}
)
)
}
}
private fun bindItem(view: View, item: ActionItem) {
val icon: ImageView = view.findViewById(R.id.signal_bottom_action_bar_item_icon)
val title: TextView = view.findViewById(R.id.signal_bottom_action_bar_item_title)
icon.setImageResource(item.iconRes)
title.setText(item.titleRes)
view.setOnClickListener { item.action.run() }
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components
package org.thoughtcrime.securesms.components.menu
import android.content.Context
import android.graphics.Rect
import android.os.Build
import android.view.LayoutInflater
import android.view.View
@@ -8,8 +9,6 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.PopupWindow
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -17,6 +16,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.MappingAdapter
import org.thoughtcrime.securesms.util.MappingModel
import org.thoughtcrime.securesms.util.MappingViewHolder
import org.thoughtcrime.securesms.util.ViewUtil
/**
* A custom context menu that will show next to an anchor view and display several options. Basically a PopupMenu with custom UI and positioning rules.
@@ -27,10 +27,11 @@ import org.thoughtcrime.securesms.util.MappingViewHolder
*/
class SignalContextMenu private constructor(
val anchor: View,
val container: View,
val items: List<Item>,
val container: ViewGroup,
val items: List<ActionItem>,
val baseOffsetX: Int = 0,
val baseOffsetY: Int = 0,
val horizontalPosition: HorizontalPosition = HorizontalPosition.START,
val onDismiss: Runnable? = null
) : PopupWindow(
LayoutInflater.from(anchor.context).inflate(R.layout.signal_context_menu, null),
@@ -77,8 +78,14 @@ class SignalContextMenu private constructor(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
val menuBottomBound = anchor.y + anchor.height + contentView.measuredHeight + baseOffsetY
val menuTopBound = anchor.y - contentView.measuredHeight - baseOffsetY
val anchorRect = Rect(anchor.left, anchor.top, anchor.right, anchor.bottom).also {
if (anchor.parent != container) {
container.offsetDescendantRectToMyCoords(anchor, it)
}
}
val menuBottomBound = anchorRect.bottom + contentView.measuredHeight + baseOffsetY
val menuTopBound = anchorRect.top - contentView.measuredHeight - baseOffsetY
val screenBottomBound = container.height
val screenTopBound = container.y
@@ -88,16 +95,33 @@ class SignalContextMenu private constructor(
if (menuBottomBound < screenBottomBound) {
offsetY = baseOffsetY
} else if (menuTopBound > screenTopBound) {
offsetY = -(anchor.height + contentView.measuredHeight + baseOffsetY)
offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
mappingAdapter.submitList(items.reversed().toAdapterItems())
} else {
offsetY = -((anchor.height / 2) + (contentView.measuredHeight / 2) + baseOffsetY)
offsetY = -((anchorRect.height() / 2) + (contentView.measuredHeight / 2) + baseOffsetY)
}
showAsDropDown(anchor, baseOffsetX, offsetY)
val offsetX: Int = when (horizontalPosition) {
HorizontalPosition.START -> {
if (ViewUtil.isLtr(context)) {
baseOffsetX
} else {
-(baseOffsetX + contentView.measuredWidth)
}
}
HorizontalPosition.END -> {
if (ViewUtil.isLtr(context)) {
-(baseOffsetX + contentView.measuredWidth - anchorRect.width())
} else {
baseOffsetX - anchorRect.width()
}
}
}
showAsDropDown(anchor, offsetX, offsetY)
}
private fun List<Item>.toAdapterItems(): List<DisplayItem> {
private fun List<ActionItem>.toAdapterItems(): List<DisplayItem> {
return this.mapIndexed { index, item ->
val displayType: DisplayType = when {
this.size == 1 -> DisplayType.ONLY
@@ -110,14 +134,8 @@ class SignalContextMenu private constructor(
}
}
data class Item(
@DrawableRes val iconRes: Int,
@StringRes val titleRes: Int,
val action: Runnable
)
private data class DisplayItem(
val item: Item,
val item: ActionItem,
val displayType: DisplayType
) : MappingModel<DisplayItem> {
override fun areItemsTheSame(newItem: DisplayItem): Boolean {
@@ -129,7 +147,7 @@ class SignalContextMenu private constructor(
}
}
enum class DisplayType {
private enum class DisplayType {
TOP, BOTTOM, MIDDLE, ONLY
}
@@ -162,18 +180,23 @@ class SignalContextMenu private constructor(
}
}
enum class HorizontalPosition {
START, END
}
/**
* @param anchor The view to put the pop-up on
* @param container A parent of [anchor] that represents the acceptable boundaries of the popup
*/
class Builder(
val anchor: View,
val container: View
val container: ViewGroup
) {
var onDismiss: Runnable? = null
var offsetX: Int = 0
var offsetY: Int = 0
var offsetX = 0
var offsetY = 0
var horizontalPosition = HorizontalPosition.START
fun onDismiss(onDismiss: Runnable): Builder {
this.onDismiss = onDismiss
@@ -190,13 +213,19 @@ class SignalContextMenu private constructor(
return this
}
fun show(items: List<Item>) {
fun preferredHorizontalPosition(horizontalPosition: HorizontalPosition): Builder {
this.horizontalPosition = horizontalPosition
return this
}
fun show(items: List<ActionItem>) {
SignalContextMenu(
anchor = anchor,
container = container,
items = items,
baseOffsetX = offsetX,
baseOffsetY = offsetY,
horizontalPosition = horizontalPosition,
onDismiss = onDismiss
).show()
}