diff --git a/res/drawable/ic_group.xml b/res/drawable/ic_group.xml
new file mode 100644
index 0000000000..2b77ad4c47
--- /dev/null
+++ b/res/drawable/ic_group.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/res/drawable/ic_message.xml b/res/drawable/ic_message.xml
new file mode 100644
index 0000000000..77b88ec345
--- /dev/null
+++ b/res/drawable/ic_message.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/res/drawable/ic_plus.xml b/res/drawable/ic_plus.xml
new file mode 100644
index 0000000000..e57a46a646
--- /dev/null
+++ b/res/drawable/ic_plus.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/res/drawable/new_conversation_button_background.xml b/res/drawable/new_conversation_button_background.xml
index be48173893..4de519558a 100644
--- a/res/drawable/new_conversation_button_background.xml
+++ b/res/drawable/new_conversation_button_background.xml
@@ -1,7 +1,4 @@
-
-
-
\ No newline at end of file
+ android:shape="oval" />
\ No newline at end of file
diff --git a/res/drawable/new_conversation_button_foreground.xml b/res/drawable/new_conversation_button_foreground.xml
deleted file mode 100644
index 44bb8f9675..0000000000
--- a/res/drawable/new_conversation_button_foreground.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/res/layout/activity_home.xml b/res/layout/activity_home.xml
index 28e927d94d..df7e4be5c2 100644
--- a/res/layout/activity_home.xml
+++ b/res/layout/activity_home.xml
@@ -40,28 +40,6 @@
android:layout_centerVertical="true"
android:layout_marginLeft="64dp" />
-
-
-
-
-
-
-
-
@@ -80,36 +58,13 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
-
-
-
-
-
-
-
-
-
+ android:layout_marginBottom="@dimen/new_conversation_button_bottom_offset" />
diff --git a/res/values/colors.xml b/res/values/colors.xml
index b62e73a5c3..958e2a8bd1 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -25,6 +25,7 @@
#222325
#3F4146
#99FFFFFF
+ #1F1F1F
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index a4ec3dff18..3ceaa3aefd 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -22,7 +22,8 @@
14dp
1dp
1dp
- 56dp
+ 60dp
+ 72dp
36dp
8dp
224dp
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/HomeActivity.kt
index f50167493b..af3df9afdb 100644
--- a/src/org/thoughtcrime/securesms/loki/redesign/activities/HomeActivity.kt
+++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/HomeActivity.kt
@@ -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> { 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)
}
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/utilities/PointFUtilities.kt b/src/org/thoughtcrime/securesms/loki/redesign/utilities/PointFUtilities.kt
new file mode 100644
index 0000000000..ef3bd13468
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/redesign/utilities/PointFUtilities.kt
@@ -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
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/utilities/ViewGroupUtilities.kt b/src/org/thoughtcrime/securesms/loki/redesign/utilities/ViewGroupUtilities.kt
new file mode 100644
index 0000000000..8f99bf1381
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/redesign/utilities/ViewGroupUtilities.kt
@@ -0,0 +1,9 @@
+package org.thoughtcrime.securesms.loki.redesign.utilities
+
+import android.view.ViewGroup
+
+fun ViewGroup.disableClipping() {
+ clipToPadding = false
+ clipChildren = false
+ clipToOutline = false
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/utilities/ViewUtilities.kt b/src/org/thoughtcrime/securesms/loki/redesign/utilities/ViewUtilities.kt
new file mode 100644
index 0000000000..ec1242699c
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/redesign/utilities/ViewUtilities.kt
@@ -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
+ }
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/views/NewConversationButtonSetView.kt b/src/org/thoughtcrime/securesms/loki/redesign/views/NewConversationButtonSetView.kt
new file mode 100644
index 0000000000..7cb6c76a06
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/redesign/views/NewConversationButtonSetView.kt
@@ -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
\ No newline at end of file