mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Add new bottom actionbar to chat list.
This commit is contained in:
@@ -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
|
||||
)
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user