mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-03 05:22:23 +00:00
301 lines
15 KiB
Kotlin
301 lines
15 KiB
Kotlin
package org.thoughtcrime.securesms.loki.views
|
|
|
|
import android.animation.ArgbEvaluator
|
|
import android.animation.FloatEvaluator
|
|
import android.animation.PointFEvaluator
|
|
import android.animation.ValueAnimator
|
|
import android.content.Context
|
|
import android.content.Context.VIBRATOR_SERVICE
|
|
import android.content.res.ColorStateList
|
|
import android.graphics.Color
|
|
import android.graphics.PointF
|
|
import android.graphics.drawable.GradientDrawable
|
|
import android.os.Build
|
|
import android.os.VibrationEffect
|
|
import android.os.VibrationEffect.DEFAULT_AMPLITUDE
|
|
import android.os.Vibrator
|
|
import android.util.AttributeSet
|
|
import android.view.Gravity
|
|
import android.view.MotionEvent
|
|
import android.widget.ImageView
|
|
import android.widget.RelativeLayout
|
|
import androidx.annotation.ColorRes
|
|
import androidx.annotation.DrawableRes
|
|
import network.loki.messenger.R
|
|
import org.thoughtcrime.securesms.loki.utilities.*
|
|
|
|
class NewConversationButtonSetView : RelativeLayout {
|
|
private var expandedButton: Button? = null
|
|
private var previousAction: Int? = null
|
|
private var isExpanded = false
|
|
var delegate: NewConversationButtonSetViewDelegate? = null
|
|
|
|
// region Convenience
|
|
private val sessionButtonExpandedPosition: PointF get() { return PointF(width.toFloat() / 2 - sessionButton.expandedSize / 2, 0.0f) }
|
|
private val closedGroupButtonExpandedPosition: PointF get() { return PointF(width.toFloat() - closedGroupButton.expandedSize, height.toFloat() - bottomMargin - closedGroupButton.expandedSize) }
|
|
private val openGroupButtonExpandedPosition: PointF get() { return PointF(0.0f, height.toFloat() - bottomMargin - openGroupButton.expandedSize) }
|
|
private val buttonRestPosition: PointF get() { return PointF(width.toFloat() / 2 - mainButton.expandedSize / 2, height.toFloat() - bottomMargin - mainButton.expandedSize) }
|
|
// endregion
|
|
|
|
// region Settings
|
|
private val minDragDistance by lazy { toPx(40, resources).toFloat() }
|
|
private val maxDragDistance by lazy { toPx(56, resources).toFloat() }
|
|
private val dragMargin by lazy { toPx(16, resources).toFloat() }
|
|
private val bottomMargin by lazy { resources.getDimension(R.dimen.new_conversation_button_bottom_offset) }
|
|
// endregion
|
|
|
|
// region Components
|
|
private val mainButton by lazy { Button(context, true, R.drawable.ic_plus) }
|
|
private val sessionButton by lazy { Button(context, false, R.drawable.ic_message) }
|
|
private val closedGroupButton by lazy { Button(context, false, R.drawable.ic_group) }
|
|
private val openGroupButton by lazy { Button(context, false, R.drawable.ic_globe) }
|
|
// endregion
|
|
|
|
// region Button
|
|
class Button : RelativeLayout {
|
|
@DrawableRes private var iconID = 0
|
|
private var isMain = false
|
|
|
|
companion object {
|
|
val animationDuration = 250.toLong()
|
|
}
|
|
|
|
val expandedSize by lazy { resources.getDimension(R.dimen.new_conversation_button_expanded_size) }
|
|
val collapsedSize by lazy { resources.getDimension(R.dimen.new_conversation_button_collapsed_size) }
|
|
private val expandedImageViewPosition by lazy { PointF(0.0f, 0.0f) }
|
|
private val collapsedImageViewPosition by lazy { PointF((expandedSize - collapsedSize) / 2, (expandedSize - collapsedSize) / 2) }
|
|
|
|
private val imageView by lazy {
|
|
val result = ImageView(context)
|
|
val size = collapsedSize.toInt()
|
|
result.layoutParams = LayoutParams(size, size)
|
|
result.setBackgroundResource(R.drawable.new_conversation_button_background)
|
|
val background = result.background as GradientDrawable
|
|
@ColorRes val backgroundColorID = if (isMain)
|
|
R.color.accent else
|
|
R.color.new_conversation_button_collapsed_background
|
|
background.color = ColorStateList.valueOf(resources.getColorWithID(backgroundColorID, context.theme))
|
|
result.scaleType = ImageView.ScaleType.CENTER
|
|
result.setImageResource(iconID)
|
|
|
|
result.imageTintList = if (isMain)
|
|
// Always use white icon for the main button.
|
|
ColorStateList.valueOf(resources.getColorWithID(android.R.color.white, context.theme))
|
|
else
|
|
ColorStateList.valueOf(resources.getColorWithID(R.color.text, context.theme))
|
|
|
|
result
|
|
}
|
|
|
|
constructor(context: Context) : super(context) { throw IllegalAccessException("Use Button(context:iconID:) instead.") }
|
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { throw IllegalAccessException("Use Button(context:iconID:) instead.") }
|
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { throw IllegalAccessException("Use Button(context:iconID:) instead.") }
|
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { throw IllegalAccessException("Use Button(context:iconID:) instead.") }
|
|
|
|
constructor(context: Context, isMain: Boolean, @DrawableRes iconID: Int) : super(context) {
|
|
this.iconID = iconID
|
|
this.isMain = isMain
|
|
disableClipping()
|
|
val size = resources.getDimension(R.dimen.new_conversation_button_expanded_size).toInt()
|
|
val layoutParams = LayoutParams(size, size)
|
|
this.layoutParams = layoutParams
|
|
addView(imageView)
|
|
imageView.x = collapsedImageViewPosition.x
|
|
imageView.y = collapsedImageViewPosition.y
|
|
gravity = Gravity.TOP or Gravity.LEFT // Intentionally not Gravity.START
|
|
}
|
|
|
|
fun expand() {
|
|
animateImageViewColorChange(R.color.new_conversation_button_collapsed_background, R.color.accent)
|
|
imageView.animateSizeChange(R.dimen.new_conversation_button_collapsed_size, R.dimen.new_conversation_button_expanded_size, animationDuration)
|
|
animateImageViewPositionChange(collapsedImageViewPosition, expandedImageViewPosition)
|
|
}
|
|
|
|
fun collapse() {
|
|
animateImageViewColorChange(R.color.accent, R.color.new_conversation_button_collapsed_background)
|
|
imageView.animateSizeChange(R.dimen.new_conversation_button_expanded_size, R.dimen.new_conversation_button_collapsed_size, animationDuration)
|
|
animateImageViewPositionChange(expandedImageViewPosition, collapsedImageViewPosition)
|
|
}
|
|
|
|
private fun animateImageViewColorChange(@ColorRes startColorID: Int, @ColorRes endColorID: Int) {
|
|
val drawable = imageView.background as GradientDrawable
|
|
val startColor = resources.getColorWithID(startColorID, context.theme)
|
|
val endColor = resources.getColorWithID(endColorID, context.theme)
|
|
val animation = ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor)
|
|
animation.duration = animationDuration
|
|
animation.addUpdateListener { animator ->
|
|
val color = animator.animatedValue as Int
|
|
drawable.color = ColorStateList.valueOf(color)
|
|
}
|
|
animation.start()
|
|
}
|
|
|
|
private fun animateImageViewPositionChange(startPosition: PointF, endPosition: PointF) {
|
|
val animation = ValueAnimator.ofObject(PointFEvaluator(), startPosition, endPosition)
|
|
animation.duration = animationDuration
|
|
animation.addUpdateListener { animator ->
|
|
val point = animator.animatedValue as PointF
|
|
imageView.x = point.x
|
|
imageView.y = point.y
|
|
}
|
|
animation.start()
|
|
}
|
|
|
|
fun animatePositionChange(startPosition: PointF, endPosition: PointF) {
|
|
val animation = ValueAnimator.ofObject(PointFEvaluator(), startPosition, endPosition)
|
|
animation.duration = animationDuration
|
|
animation.addUpdateListener { animator ->
|
|
val point = animator.animatedValue as PointF
|
|
x = point.x
|
|
y = point.y
|
|
}
|
|
animation.start()
|
|
}
|
|
|
|
fun animateAlphaChange(startAlpha: Float, endAlpha: Float) {
|
|
val animation = ValueAnimator.ofObject(FloatEvaluator(), startAlpha, endAlpha)
|
|
animation.duration = animationDuration
|
|
animation.addUpdateListener { animator ->
|
|
alpha = animator.animatedValue as Float
|
|
}
|
|
animation.start()
|
|
}
|
|
}
|
|
// endregion
|
|
|
|
// region Lifecycle
|
|
constructor(context: Context) : super(context) { setUpViewHierarchy() }
|
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { setUpViewHierarchy() }
|
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { setUpViewHierarchy() }
|
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { setUpViewHierarchy() }
|
|
|
|
private fun setUpViewHierarchy() {
|
|
// Set up session button
|
|
addView(sessionButton)
|
|
sessionButton.alpha = 0.0f
|
|
val sessionButtonLayoutParams = sessionButton.layoutParams as LayoutParams
|
|
sessionButtonLayoutParams.addRule(CENTER_IN_PARENT, TRUE)
|
|
sessionButtonLayoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE)
|
|
sessionButtonLayoutParams.bottomMargin = bottomMargin.toInt()
|
|
// Set up closed group button
|
|
addView(closedGroupButton)
|
|
closedGroupButton.alpha = 0.0f
|
|
val closedGroupButtonLayoutParams = closedGroupButton.layoutParams as LayoutParams
|
|
closedGroupButtonLayoutParams.addRule(CENTER_IN_PARENT, TRUE)
|
|
closedGroupButtonLayoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE)
|
|
closedGroupButtonLayoutParams.bottomMargin = bottomMargin.toInt()
|
|
// Set up open group button
|
|
addView(openGroupButton)
|
|
openGroupButton.alpha = 0.0f
|
|
val openGroupButtonLayoutParams = openGroupButton.layoutParams as LayoutParams
|
|
openGroupButtonLayoutParams.addRule(CENTER_IN_PARENT, TRUE)
|
|
openGroupButtonLayoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE)
|
|
openGroupButtonLayoutParams.bottomMargin = bottomMargin.toInt()
|
|
// Set up main button
|
|
addView(mainButton)
|
|
val mainButtonLayoutParams = mainButton.layoutParams as LayoutParams
|
|
mainButtonLayoutParams.addRule(CENTER_IN_PARENT, TRUE)
|
|
mainButtonLayoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE)
|
|
mainButtonLayoutParams.bottomMargin = bottomMargin.toInt()
|
|
}
|
|
// endregion
|
|
|
|
// region Interaction
|
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
val touch = PointF(event.x, event.y)
|
|
val allButtons = listOf( mainButton, sessionButton, closedGroupButton, openGroupButton )
|
|
val buttonsExcludingMainButton = listOf( sessionButton, closedGroupButton, openGroupButton )
|
|
if (allButtons.none { it.contains(touch) }) { return false }
|
|
when (event.action) {
|
|
MotionEvent.ACTION_DOWN -> {
|
|
if (isExpanded) {
|
|
if (mainButton.contains(touch)) { collapse() }
|
|
} else {
|
|
isExpanded = true
|
|
expand()
|
|
}
|
|
val vibrator = context.getSystemService(VIBRATOR_SERVICE) as Vibrator
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
vibrator.vibrate(VibrationEffect.createOneShot(50, DEFAULT_AMPLITUDE))
|
|
} else {
|
|
vibrator.vibrate(50)
|
|
}
|
|
}
|
|
MotionEvent.ACTION_MOVE -> {
|
|
mainButton.x = touch.x - mainButton.expandedSize / 2
|
|
mainButton.y = touch.y - mainButton.expandedSize / 2
|
|
mainButton.alpha = 1 - (PointF(mainButton.x, mainButton.y).distanceTo(buttonRestPosition) / maxDragDistance)
|
|
val buttonToExpand = buttonsExcludingMainButton.firstOrNull { button ->
|
|
var hasUserDraggedBeyondButton = false
|
|
if (button == openGroupButton && touch.isLeftOf(openGroupButton, dragMargin)) { hasUserDraggedBeyondButton = true }
|
|
if (button == sessionButton && touch.isAbove(sessionButton, dragMargin)) { hasUserDraggedBeyondButton = true }
|
|
if (button == closedGroupButton && touch.isRightOf(closedGroupButton, dragMargin)) { hasUserDraggedBeyondButton = true }
|
|
button.contains(touch) || hasUserDraggedBeyondButton
|
|
}
|
|
if (buttonToExpand != null) {
|
|
if (buttonToExpand == expandedButton) { return true }
|
|
expandedButton?.collapse()
|
|
buttonToExpand.expand()
|
|
this.expandedButton = buttonToExpand
|
|
} else {
|
|
expandedButton?.collapse()
|
|
this.expandedButton = null
|
|
}
|
|
}
|
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
val mainButtonCenter = PointF(width.toFloat() / 2, height.toFloat() - bottomMargin - mainButton.expandedSize / 2)
|
|
val distanceFromMainButtonCenter = touch.distanceTo(mainButtonCenter)
|
|
fun collapse() {
|
|
isExpanded = false
|
|
this.collapse()
|
|
}
|
|
if (distanceFromMainButtonCenter > (minDragDistance + mainButton.collapsedSize / 2)) {
|
|
if (sessionButton.contains(touch) || touch.isAbove(sessionButton, dragMargin)) { delegate?.createNewPrivateChat(); collapse() }
|
|
else if (closedGroupButton.contains(touch) || touch.isRightOf(closedGroupButton, dragMargin)) { delegate?.createNewClosedGroup(); collapse() }
|
|
else if (openGroupButton.contains(touch) || touch.isLeftOf(openGroupButton, dragMargin)) { delegate?.joinOpenGroup(); collapse() }
|
|
else { collapse() }
|
|
} else {
|
|
val currentPosition = PointF(mainButton.x, mainButton.y)
|
|
mainButton.animatePositionChange(currentPosition, buttonRestPosition)
|
|
val endAlpha = 1.0f
|
|
mainButton.animateAlphaChange(mainButton.alpha, endAlpha)
|
|
expandedButton?.collapse()
|
|
this.expandedButton = null
|
|
}
|
|
}
|
|
}
|
|
previousAction = event.action
|
|
return true
|
|
}
|
|
|
|
private fun expand() {
|
|
val buttonsExcludingMainButton = listOf( sessionButton, closedGroupButton, openGroupButton )
|
|
sessionButton.animatePositionChange(buttonRestPosition, sessionButtonExpandedPosition)
|
|
closedGroupButton.animatePositionChange(buttonRestPosition, closedGroupButtonExpandedPosition)
|
|
openGroupButton.animatePositionChange(buttonRestPosition, openGroupButtonExpandedPosition)
|
|
buttonsExcludingMainButton.forEach { it.animateAlphaChange(0.0f, 1.0f) }
|
|
postDelayed({ isExpanded = true }, Button.animationDuration)
|
|
}
|
|
|
|
private fun collapse() {
|
|
val allButtons = listOf( mainButton, sessionButton, closedGroupButton, openGroupButton )
|
|
allButtons.forEach {
|
|
val currentPosition = PointF(it.x, it.y)
|
|
it.animatePositionChange(currentPosition, buttonRestPosition)
|
|
val endAlpha = if (it == mainButton) 1.0f else 0.0f
|
|
it.animateAlphaChange(it.alpha, endAlpha)
|
|
}
|
|
postDelayed({ isExpanded = false }, Button.animationDuration)
|
|
}
|
|
// endregion
|
|
}
|
|
|
|
// region Delegate
|
|
interface NewConversationButtonSetViewDelegate {
|
|
|
|
fun joinOpenGroup()
|
|
fun createNewPrivateChat()
|
|
fun createNewClosedGroup()
|
|
}
|
|
// endregion
|