Implement new conversation button redesign

This commit is contained in:
Niels Andriesse
2020-03-12 15:57:53 +11:00
committed by gmbnt
parent 8ff4688de1
commit f6a8cd93da
13 changed files with 393 additions and 71 deletions

View File

@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.loki.getColorWithID
import org.thoughtcrime.securesms.loki.redesign.utilities.push
import org.thoughtcrime.securesms.loki.redesign.utilities.show
import org.thoughtcrime.securesms.loki.redesign.views.ConversationView
import org.thoughtcrime.securesms.loki.redesign.views.NewConversationButtonSetViewDelegate
import org.thoughtcrime.securesms.loki.redesign.views.SeedReminderViewDelegate
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
@@ -41,7 +42,7 @@ import org.thoughtcrime.securesms.util.GroupUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import kotlin.math.abs
class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate {
class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate {
private lateinit var glide: GlideRequests
private val hexEncodedPublicKey: String
@@ -87,8 +88,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
profileButton.hexEncodedPublicKey = hexEncodedPublicKey
profileButton.update()
profileButton.setOnClickListener { openSettings() }
createClosedGroupButton.setOnClickListener { createClosedGroup() }
joinPublicChatButton.setOnClickListener { joinPublicChat() }
// Set up seed reminder view
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
@@ -125,8 +124,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
homeAdapter.changeCursor(null)
}
})
// Set up new conversation button
newConversationButton.setOnClickListener { createPrivateChat() }
newConversationButtonSet.delegate = this
// Set up typing observer
ApplicationContext.getInstance(this).typingStatusRepository.typingThreads.observe(this, Observer<Set<Long>> { threadIDs ->
val adapter = recyclerView.adapter as HomeAdapter
@@ -180,7 +178,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == CreateClosedGroupActivity.createNewPrivateChatResultCode) {
createPrivateChat()
createNewPrivateChat()
}
}
// endregion
@@ -215,17 +213,17 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
show(intent)
}
private fun createPrivateChat() {
override fun createNewPrivateChat() {
val intent = Intent(this, CreatePrivateChatActivity::class.java)
show(intent)
}
private fun createClosedGroup() {
override fun createNewClosedGroup() {
val intent = Intent(this, CreateClosedGroupActivity::class.java)
show(intent, true)
}
private fun joinPublicChat() {
override fun joinOpenGroup() {
val intent = Intent(this, JoinPublicChatActivity::class.java)
show(intent)
}

View File

@@ -0,0 +1,32 @@
package org.thoughtcrime.securesms.loki.redesign.utilities
import android.graphics.PointF
import android.view.View
fun PointF.distanceTo(other: PointF): Float {
return Math.sqrt(Math.pow(this.x.toDouble() - other.x.toDouble(), 2.toDouble()) + Math.pow(this.y.toDouble() - other.y.toDouble(), 2.toDouble())).toFloat()
}
fun PointF.isLeftOf(view: View, margin: Float = 0.0f): Boolean {
return isContainedVerticallyIn(view, margin) && x < view.hitRect.left
}
fun PointF.isAbove(view: View, margin: Float = 0.0f): Boolean {
return isContainedHorizontallyIn(view, margin) && y < view.hitRect.top
}
fun PointF.isRightOf(view: View, margin: Float = 0.0f): Boolean {
return isContainedVerticallyIn(view, margin) && x > view.hitRect.right
}
fun PointF.isBelow(view: View, margin: Float = 0.0f): Boolean {
return isContainedHorizontallyIn(view, margin) && y > view.hitRect.bottom
}
fun PointF.isContainedHorizontallyIn(view: View, margin: Float = 0.0f): Boolean {
return x >= view.hitRect.left - margin || x <= view.hitRect.right + margin
}
fun PointF.isContainedVerticallyIn(view: View, margin: Float = 0.0f): Boolean {
return y >= view.hitRect.top - margin || x <= view.hitRect.bottom + margin
}

View File

@@ -0,0 +1,9 @@
package org.thoughtcrime.securesms.loki.redesign.utilities
import android.view.ViewGroup
fun ViewGroup.disableClipping() {
clipToPadding = false
clipChildren = false
clipToOutline = false
}

View File

@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.loki.redesign.utilities
import android.graphics.PointF
import android.graphics.Rect
import android.view.View
fun View.contains(point: PointF): Boolean {
return hitRect.contains(point.x.toInt(), point.y.toInt())
}
val View.hitRect: Rect
get() {
val rect = Rect()
getHitRect(rect)
return rect
}

View File

@@ -0,0 +1,272 @@
package org.thoughtcrime.securesms.loki.redesign.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.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.support.annotation.ColorRes
import android.support.annotation.DimenRes
import android.support.annotation.DrawableRes
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.ImageView
import android.widget.RelativeLayout
import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.getColorWithID
import org.thoughtcrime.securesms.loki.redesign.utilities.*
import org.thoughtcrime.securesms.loki.toPx
class NewConversationButtonSetView : RelativeLayout {
private var expandedButton: Button? = null
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() - closedGroupButton.expandedSize) }
private val openGroupButtonExpandedPosition: PointF get() { return PointF(0.0f, height.toFloat() - openGroupButton.expandedSize) }
private val buttonRestPosition: PointF get() { return PointF(width.toFloat() / 2 - mainButton.expandedSize / 2, height.toFloat() - mainButton.expandedSize) }
// endregion
// region Settings
private val maxDragDistance by lazy { toPx(56, resources).toFloat() }
private val dragMargin by lazy { toPx(16, resources).toFloat() }
// 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
val colorID = if (isMain) R.color.accent else R.color.new_conversation_button_collapsed_background
background.color = ColorStateList.valueOf(resources.getColorWithID(colorID, context.theme))
result.scaleType = ImageView.ScaleType.CENTER
result.setImageResource(iconID)
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
}
fun expand() {
animateImageViewColorChange(R.color.new_conversation_button_collapsed_background, R.color.accent)
animateImageViewSizeChange(R.dimen.new_conversation_button_collapsed_size, R.dimen.new_conversation_button_expanded_size)
animateImageViewPositionChange(collapsedImageViewPosition, expandedImageViewPosition)
}
fun collapse() {
animateImageViewColorChange(R.color.accent, R.color.new_conversation_button_collapsed_background)
animateImageViewSizeChange(R.dimen.new_conversation_button_expanded_size, R.dimen.new_conversation_button_collapsed_size)
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 animateImageViewSizeChange(@DimenRes startSizeID: Int, @DimenRes endSizeID: Int) {
val layoutParams = imageView.layoutParams
val startSize = resources.getDimension(startSizeID)
val endSize = resources.getDimension(endSizeID)
val animation = ValueAnimator.ofObject(FloatEvaluator(), startSize, endSize)
animation.duration = animationDuration
animation.addUpdateListener { animator ->
val size = animator.animatedValue as Float
layoutParams.width = size.toInt()
layoutParams.height = size.toInt()
imageView.layoutParams = layoutParams
}
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() {
disableClipping()
// 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)
// 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)
// 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)
// Set up main button
addView(mainButton)
val mainButtonLayoutParams = mainButton.layoutParams as LayoutParams
mainButtonLayoutParams.addRule(CENTER_IN_PARENT, TRUE)
mainButtonLayoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE)
}
// endregion
// region Interaction
override fun onTouchEvent(event: MotionEvent): Boolean {
val touchX = event.x
val touchY = event.y
val touch = PointF(touchX, touchY)
val expandedButton = expandedButton
val buttonsExcludingMainButton = listOf( sessionButton, closedGroupButton, openGroupButton )
when (event.action) {
MotionEvent.ACTION_DOWN -> {
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)
}
sessionButton.animatePositionChange(buttonRestPosition, sessionButtonExpandedPosition)
closedGroupButton.animatePositionChange(buttonRestPosition, closedGroupButtonExpandedPosition)
openGroupButton.animatePositionChange(buttonRestPosition, openGroupButtonExpandedPosition)
buttonsExcludingMainButton.forEach { it.animateAlphaChange(0.0f, 1.0f) }
}
MotionEvent.ACTION_MOVE -> {
mainButton.x = touchX - mainButton.expandedSize / 2
mainButton.y = touchY - 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 -> {
expandedButton?.collapse()
this.expandedButton = null
val allButtons = listOf( mainButton ) + buttonsExcludingMainButton
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)
}
if (event.action == MotionEvent.ACTION_UP) {
if (openGroupButton.contains(touch) || touch.isLeftOf(openGroupButton, dragMargin)) { delegate?.joinOpenGroup() }
else if (sessionButton.contains(touch) || touch.isAbove(sessionButton, dragMargin)) { delegate?.createNewPrivateChat() }
else if (closedGroupButton.contains(touch) || touch.isRightOf(closedGroupButton, dragMargin)) { delegate?.createNewClosedGroup() }
}
}
}
return true
}
// endregion
}
// region Delegate
interface NewConversationButtonSetViewDelegate {
fun joinOpenGroup()
fun createNewPrivateChat()
fun createNewClosedGroup()
}
// endregion