session-android/app/src/main/java/org/thoughtcrime/securesms/home/NewConversationButtonSetVie...

387 lines
20 KiB
Kotlin

package org.thoughtcrime.securesms.home
import android.animation.FloatEvaluator
import android.animation.PointFEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.PointF
import android.graphics.Typeface
import android.os.Build
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import network.loki.messenger.R
import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.NewConversationButtonImageView
import org.thoughtcrime.securesms.util.UiModeUtilities
import org.thoughtcrime.securesms.util.animateSizeChange
import org.thoughtcrime.securesms.util.contains
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.distanceTo
import org.thoughtcrime.securesms.util.getColorWithID
import org.thoughtcrime.securesms.util.isAbove
import org.thoughtcrime.securesms.util.isLeftOf
import org.thoughtcrime.securesms.util.isRightOf
import org.thoughtcrime.securesms.util.toPx
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 - sessionButton.shadowMargin, 0.0f) }
private val sessionTooltipExpandedPosition: PointF get() {
val x = sessionButtonExpandedPosition.x + sessionButton.width / 2 - sessionTooltip.width / 2
val y = sessionButtonExpandedPosition.y + sessionButton.height - sessionTooltip.height / 2
return PointF(x, y)
}
private val closedGroupButtonExpandedPosition: PointF get() { return PointF(width.toFloat() - closedGroupButton.expandedSize - 2 * closedGroupButton.shadowMargin, height.toFloat() - bottomMargin - closedGroupButton.expandedSize - 2 * closedGroupButton.shadowMargin) }
private val closedGroupTooltipExpandedPosition: PointF get() {
val x = closedGroupButtonExpandedPosition.x + closedGroupButton.width / 2 - closedGroupTooltip.width / 2
val y = closedGroupButtonExpandedPosition.y + closedGroupButton.height - closedGroupTooltip.height / 2
return PointF(x, y)
}
private val openGroupButtonExpandedPosition: PointF get() { return PointF(0.0f, height.toFloat() - bottomMargin - openGroupButton.expandedSize - 2 * openGroupButton.shadowMargin) }
private val openGroupTooltipExpandedPosition: PointF get() {
val x = openGroupButtonExpandedPosition.x + openGroupButton.width / 2 - openGroupTooltip.width / 2
val y = openGroupButtonExpandedPosition.y + openGroupButton.height - openGroupTooltip.height / 2
return PointF(x, y)
}
private val buttonRestPosition: PointF get() { return PointF(width.toFloat() / 2 - mainButton.expandedSize / 2 - mainButton.shadowMargin, height.toFloat() - bottomMargin - mainButton.expandedSize - 2 * mainButton.shadowMargin) }
private fun tooltipRestPosition(viewWidth: Int): PointF {
return PointF(width.toFloat() / 2 - viewWidth / 2, height.toFloat() - bottomMargin)
}
// 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 sessionTooltip by lazy {
TextView(context).apply {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
typeface = Typeface.DEFAULT_BOLD
setText(R.string.NewConversationButton_SessionTooltip)
isAllCaps = true
}
}
private val closedGroupButton by lazy { Button(context, false, R.drawable.ic_group) }
private val closedGroupTooltip by lazy {
TextView(context).apply {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
typeface = Typeface.DEFAULT_BOLD
setText(R.string.NewConversationButton_ClosedGroupTooltip)
isAllCaps = true
}
}
private val openGroupButton by lazy { Button(context, false, R.drawable.ic_globe) }
private val openGroupTooltip by lazy {
TextView(context).apply {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
typeface = Typeface.DEFAULT_BOLD
setText(R.string.NewConversationButton_OpenGroupTooltip)
isAllCaps = true
}
}
// endregion
// region Button
class Button : RelativeLayout {
@DrawableRes private var iconID = 0
private var isMain = false
fun getIconID() = iconID
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) }
val shadowMargin by lazy { toPx(6, resources).toFloat() }
private val expandedImageViewPosition by lazy { PointF(shadowMargin, shadowMargin) }
private val collapsedImageViewPosition by lazy { PointF(shadowMargin + (expandedSize - collapsedSize) / 2, shadowMargin + (expandedSize - collapsedSize) / 2) }
private val imageView by lazy {
val result = NewConversationButtonImageView(context)
val size = collapsedSize.toInt()
result.layoutParams = LayoutParams(size, size)
result.setBackgroundResource(R.drawable.new_conversation_button_background)
@ColorRes val backgroundColorID = if (isMain) R.color.accent else R.color.new_conversation_button_collapsed_background
@ColorRes val shadowColorID = if (isMain) {
R.color.new_conversation_button_shadow
} else {
if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
}
result.mainColor = resources.getColorWithID(backgroundColorID, context.theme)
result.sessionShadowColor = resources.getColorWithID(shadowColorID, context.theme)
result.scaleType = ImageView.ScaleType.CENTER
result.setImageResource(iconID)
result.imageTintList = if (isMain) {
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() + 2 * shadowMargin.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() {
GlowViewUtilities.animateColorChange(context, imageView, R.color.new_conversation_button_collapsed_background, R.color.accent)
@ColorRes val startShadowColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
GlowViewUtilities.animateShadowColorChange(context, imageView, startShadowColorID, R.color.new_conversation_button_shadow)
imageView.animateSizeChange(R.dimen.new_conversation_button_collapsed_size, R.dimen.new_conversation_button_expanded_size, animationDuration)
animateImageViewPositionChange(collapsedImageViewPosition, expandedImageViewPosition)
}
fun collapse() {
GlowViewUtilities.animateColorChange(context, imageView, R.color.accent, R.color.new_conversation_button_collapsed_background)
@ColorRes val endShadowColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
GlowViewUtilities.animateShadowColorChange(context, imageView, R.color.new_conversation_button_shadow, endShadowColorID)
imageView.animateSizeChange(R.dimen.new_conversation_button_expanded_size, R.dimen.new_conversation_button_collapsed_size, animationDuration)
animateImageViewPositionChange(expandedImageViewPosition, collapsedImageViewPosition)
}
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() {
disableClipping()
isHapticFeedbackEnabled = true
// Set up session button
addView(sessionButton)
addView(sessionTooltip)
sessionButton.alpha = 0.0f
sessionTooltip.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)
addView(closedGroupTooltip)
closedGroupButton.alpha = 0.0f
closedGroupTooltip.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)
addView(openGroupTooltip)
openGroupButton.alpha = 0.0f
openGroupTooltip.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()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
} else {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
}
}
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 )
val allTooltips = listOf(sessionTooltip, closedGroupTooltip, openGroupTooltip)
sessionButton.animatePositionChange(buttonRestPosition, sessionButtonExpandedPosition)
sessionTooltip.animatePositionChange(tooltipRestPosition(sessionTooltip.width), sessionTooltipExpandedPosition)
closedGroupButton.animatePositionChange(buttonRestPosition, closedGroupButtonExpandedPosition)
closedGroupTooltip.animatePositionChange(tooltipRestPosition(closedGroupTooltip.width), closedGroupTooltipExpandedPosition)
openGroupButton.animatePositionChange(buttonRestPosition, openGroupButtonExpandedPosition)
openGroupTooltip.animatePositionChange(tooltipRestPosition(openGroupTooltip.width), openGroupTooltipExpandedPosition)
buttonsExcludingMainButton.forEach { it.animateAlphaChange(0.0f, 1.0f) }
allTooltips.forEach { it.animateAlphaChange(0.0f, 1.0f) }
postDelayed({ isExpanded = true }, Button.animationDuration)
}
private fun collapse() {
val allButtons = listOf( mainButton, sessionButton, closedGroupButton, openGroupButton )
val allTooltips = listOf(sessionTooltip, closedGroupTooltip, openGroupTooltip)
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)
}
allTooltips.forEach {
it.animateAlphaChange(1.0f, 0.0f)
it.animatePositionChange(PointF(it.x, it.y), tooltipRestPosition(it.width))
}
postDelayed({ isExpanded = false }, Button.animationDuration)
}
// endregion
fun View.animatePositionChange(startPosition: PointF, endPosition: PointF) {
val animation = ValueAnimator.ofObject(PointFEvaluator(), startPosition, endPosition)
animation.duration = Button.animationDuration
animation.addUpdateListener { animator ->
val point = animator.animatedValue as PointF
x = point.x
y = point.y
}
animation.start()
}
fun View.animateAlphaChange(startAlpha: Float, endAlpha: Float) {
val animation = ValueAnimator.ofObject(FloatEvaluator(), startAlpha, endAlpha)
animation.duration = Button.animationDuration
animation.addUpdateListener { animator ->
alpha = animator.animatedValue as Float
}
animation.start()
}
}
// region Delegate
interface NewConversationButtonSetViewDelegate {
fun joinOpenGroup()
fun createNewPrivateChat()
fun createNewClosedGroup()
}
// endregion