diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenConstraintLayout.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenConstraintLayout.kt deleted file mode 100644 index b98cdb817f7..00000000000 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenConstraintLayout.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.fsck.k9.ui.compose - -import android.content.Context -import android.util.AttributeSet -import android.widget.TextView -import androidx.constraintlayout.widget.ConstraintLayout - -/** - * Custom [ConstraintLayout] that returns an appropriate baseline value for our recipient token layout. - */ -class RecipientTokenConstraintLayout @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, -) : ConstraintLayout(context, attrs, defStyleAttr) { - private lateinit var textView: TextView - - override fun onFinishInflate() { - super.onFinishInflate() - textView = findViewById(android.R.id.text1) - } - - override fun getBaseline(): Int { - return textView.top + textView.baseline - } -} diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenLayout.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenLayout.kt new file mode 100644 index 00000000000..668466197e9 --- /dev/null +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenLayout.kt @@ -0,0 +1,72 @@ +package com.fsck.k9.ui.compose + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import com.fsck.k9.ui.R + +/** + * Custom layout for recipient tokens. + * + * Note: This layout is tightly coupled to recipient_token_item.xml + */ +class RecipientTokenLayout(context: Context, attrs: AttributeSet?) : ViewGroup(context, attrs) { + private lateinit var background: View + private lateinit var contactPicture: View + private lateinit var recipientName: View + private lateinit var cryptoStatus: View + + override fun onFinishInflate() { + super.onFinishInflate() + background = findViewById(R.id.background) + contactPicture = findViewById(R.id.contact_photo) + recipientName = findViewById(android.R.id.text1) + cryptoStatus = findViewById(R.id.crypto_status_container) + } + + // Return an appropriate baseline so the view is properly aligned with user-entered text in RecipientSelectView + override fun getBaseline(): Int { + return recipientName.top + recipientName.baseline + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + recipientName.measure(widthMeasureSpec, heightMeasureSpec) + cryptoStatus.measure(widthMeasureSpec, heightMeasureSpec) + + val height = recipientName.measuredHeight.coerceAtLeast(minimumHeight) + + val contactPictureWidth = height + val fixedWidthComponent = contactPictureWidth + cryptoStatus.measuredWidth + val desiredWidth = fixedWidthComponent + recipientName.measuredWidth + + if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) { + setMeasuredDimension(desiredWidth, height) + } else { + // Re-measure recipient name view with final width constraint + val width = desiredWidth.coerceAtMost(MeasureSpec.getSize(widthMeasureSpec)) + val recipientNameWidth = width - fixedWidthComponent + val recipientNameWidthMeasureSpec = MeasureSpec.makeMeasureSpec(recipientNameWidth, MeasureSpec.AT_MOST) + recipientName.measure(recipientNameWidthMeasureSpec, heightMeasureSpec) + + setMeasuredDimension(width, height) + } + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + val contactPictureSize = height + background.layout(contactPictureSize / 2, 0, width, height) + contactPicture.layout(0, 0, contactPictureSize, contactPictureSize) + + val recipientNameHeight = recipientName.measuredHeight + val recipientNameTop = (height - recipientNameHeight) / 2 + recipientName.layout( + contactPictureSize, + recipientNameTop, + contactPictureSize + recipientName.measuredWidth, + recipientNameTop + recipientNameHeight, + ) + + cryptoStatus.layout(width - cryptoStatus.measuredWidth, 0, width, cryptoStatus.measuredHeight) + } +} diff --git a/legacy/ui/legacy/src/main/res/layout/recipient_token_item.xml b/legacy/ui/legacy/src/main/res/layout/recipient_token_item.xml index 3ee1a30f30c..e9260afb43b 100644 --- a/legacy/ui/legacy/src/main/res/layout/recipient_token_item.xml +++ b/legacy/ui/legacy/src/main/res/layout/recipient_token_item.xml @@ -1,10 +1,11 @@ - - - - + > - + - + + + + + - + diff --git a/legacy/ui/legacy/src/main/res/values/themes.xml b/legacy/ui/legacy/src/main/res/values/themes.xml index b8b20323ec6..7b1b21eea46 100644 --- a/legacy/ui/legacy/src/main/res/values/themes.xml +++ b/legacy/ui/legacy/src/main/res/values/themes.xml @@ -9,6 +9,14 @@ ?android:attr/windowBackground ?attr/messageListRegularItemBackgroundColor ?attr/messageListRegularItemBackgroundColor + #ccc + #000 + #FF8800 + #CC0000 + #669900 + #336699 + #bbb + #888 diff --git a/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/compose/RecipientTokenLayoutTest.kt b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/compose/RecipientTokenLayoutTest.kt new file mode 100644 index 00000000000..61ccce1a5c5 --- /dev/null +++ b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/compose/RecipientTokenLayoutTest.kt @@ -0,0 +1,140 @@ +package com.fsck.k9.ui.compose + +import android.view.View +import android.view.View.MeasureSpec +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.fsck.k9.RobolectricTest +import com.fsck.k9.ui.R +import com.google.android.material.textview.MaterialTextView +import org.junit.Before +import org.junit.Test +import org.robolectric.Robolectric + +class RecipientTokenLayoutTest : RobolectricTest() { + private lateinit var activity: AppCompatActivity + + private lateinit var recipientTokenLayout: RecipientTokenLayout + + @Before + fun setUp() { + activity = Robolectric.buildActivity(AppCompatActivity::class.java).get() + activity.setTheme(R.style.Theme_Legacy_Test) + + recipientTokenLayout = + activity.layoutInflater.inflate(R.layout.recipient_token_item, null, false) as RecipientTokenLayout + } + + @Test + fun `measure with width constraint`() { + val maxWidth = 100 + recipientTokenLayout.recipientNameView.text = "recipient@domain.example" + + recipientTokenLayout.measure( + MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + ) + + assertThat(recipientTokenLayout.measuredWidth).isEqualTo(81) + assertThat(recipientTokenLayout.measuredHeight).isEqualTo(49) + } + + @Test + fun `respect max width when measuring`() { + val maxWidth = 70 + recipientTokenLayout.recipientNameView.text = "recipient@domain.example" + + recipientTokenLayout.measure( + MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + ) + + assertThat(recipientTokenLayout.measuredWidth).isEqualTo(maxWidth) + } + + @Test + fun `layout without reaching the maximum width`() { + val maxWidth = 100 + recipientTokenLayout.recipientNameView.text = "recipient@domain.example" + recipientTokenLayout.measure( + MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + ) + + recipientTokenLayout.layout(0, 0, recipientTokenLayout.measuredWidth, recipientTokenLayout.measuredHeight) + + assertThat(recipientTokenLayout.width).isEqualTo(81) + assertThat(recipientTokenLayout.height).isEqualTo(49) + + assertThat(recipientTokenLayout.contactPictureView.top).isEqualTo(0) + assertThat(recipientTokenLayout.contactPictureView.bottom).isEqualTo(49) + assertThat(recipientTokenLayout.contactPictureView.left).isEqualTo(0) + assertThat(recipientTokenLayout.contactPictureView.right).isEqualTo(49) + + assertThat(recipientTokenLayout.recipientNameView.top).isEqualTo(0) + assertThat(recipientTokenLayout.recipientNameView.bottom).isEqualTo(49) + assertThat(recipientTokenLayout.recipientNameView.left).isEqualTo(49) + assertThat(recipientTokenLayout.recipientNameView.right).isEqualTo(81) + + assertThat(recipientTokenLayout.cryptoStatusView.top).isEqualTo(0) + assertThat(recipientTokenLayout.cryptoStatusView.bottom).isEqualTo(0) + assertThat(recipientTokenLayout.cryptoStatusView.left).isEqualTo(81) + assertThat(recipientTokenLayout.cryptoStatusView.right).isEqualTo(81) + + assertThat(recipientTokenLayout.backgroundView.top).isEqualTo(0) + assertThat(recipientTokenLayout.backgroundView.bottom).isEqualTo(49) + assertThat(recipientTokenLayout.backgroundView.left).isEqualTo(24) + assertThat(recipientTokenLayout.backgroundView.right).isEqualTo(81) + } + + @Test + fun `layout with ellipsized text and crypto status indicator`() { + val maxWidth = 70 + recipientTokenLayout.recipientNameView.text = "recipient@domain.example" + recipientTokenLayout.cryptoStatusView.findViewById(R.id.contact_crypto_status_icon).isVisible = true + recipientTokenLayout.measure( + MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + ) + + recipientTokenLayout.layout(0, 0, recipientTokenLayout.measuredWidth, recipientTokenLayout.measuredHeight) + + assertThat(recipientTokenLayout.width).isEqualTo(70) + assertThat(recipientTokenLayout.height).isEqualTo(49) + + assertThat(recipientTokenLayout.contactPictureView.top).isEqualTo(0) + assertThat(recipientTokenLayout.contactPictureView.bottom).isEqualTo(49) + assertThat(recipientTokenLayout.contactPictureView.left).isEqualTo(0) + assertThat(recipientTokenLayout.contactPictureView.right).isEqualTo(49) + + assertThat(recipientTokenLayout.recipientNameView.top).isEqualTo(0) + assertThat(recipientTokenLayout.recipientNameView.bottom).isEqualTo(49) + assertThat(recipientTokenLayout.recipientNameView.left).isEqualTo(49) + assertThat(recipientTokenLayout.recipientNameView.right).isEqualTo(58) + + assertThat(recipientTokenLayout.cryptoStatusView.top).isEqualTo(0) + assertThat(recipientTokenLayout.cryptoStatusView.bottom).isEqualTo(12) + assertThat(recipientTokenLayout.cryptoStatusView.left).isEqualTo(58) + assertThat(recipientTokenLayout.cryptoStatusView.right).isEqualTo(70) + + assertThat(recipientTokenLayout.backgroundView.top).isEqualTo(0) + assertThat(recipientTokenLayout.backgroundView.bottom).isEqualTo(49) + assertThat(recipientTokenLayout.backgroundView.left).isEqualTo(24) + assertThat(recipientTokenLayout.backgroundView.right).isEqualTo(70) + } +} + +private val RecipientTokenLayout.backgroundView: View + get() = findViewById(R.id.background) + +private val RecipientTokenLayout.contactPictureView: View + get() = findViewById(R.id.contact_photo) + +private val RecipientTokenLayout.recipientNameView: MaterialTextView + get() = findViewById(android.R.id.text1) + +private val RecipientTokenLayout.cryptoStatusView: ViewGroup + get() = findViewById(R.id.crypto_status_container)