Skip to content

Commit

Permalink
Use custom layout for recipient tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
cketti committed Jan 10, 2025
1 parent e8f2f0f commit a58be49
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 91 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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)
}
}
109 changes: 44 additions & 65 deletions legacy/ui/legacy/src/main/res/layout/recipient_token_item.xml
Original file line number Diff line number Diff line change
@@ -1,102 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<com.fsck.k9.ui.compose.RecipientTokenConstraintLayout
<com.fsck.k9.ui.compose.RecipientTokenLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="32dp"
>

<View
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?attr/contactTokenBackgroundColor"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/background_position_helper"
app:layout_constraintTop_toTopOf="parent"
/>

<com.fsck.k9.ui.compose.RecipientCircleImageView
android:id="@+id/contact_photo"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_height="match_parent"
android:contentDescription="@null"
android:minHeight="32dp"
app:layout_constraintBottom_toBottomOf="@android:id/text1"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@android:id/text1"
tools:src="@drawable/ic_account_circle"
/>

<View
android:id="@+id/background_position_helper"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/contact_photo"
app:layout_constraintEnd_toEndOf="@+id/contact_photo"
app:layout_constraintStart_toStartOf="@+id/contact_photo"
app:layout_constraintTop_toTopOf="@+id/contact_photo"
/>

<com.google.android.material.textview.MaterialTextView
android:id="@android:id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingTop="4dp"
android:paddingStart="0dp"
android:paddingEnd="14dp"
android:paddingBottom="4dp"
android:padding="4sp"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/contact_photo"
app:layout_constraintTop_toTopOf="parent"
tools:text="Jane Doe"
/>

<ImageView
android:id="@+id/contact_crypto_status_icon"
<FrameLayout
android:id="@+id/crypto_status_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_status_corner"
app:tint="?openpgp_black"
tools:visibility="gone"
/>
>

<ImageView
android:id="@+id/contact_crypto_status_icon_enabled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_status_corner"
app:tint="?openpgp_green"
tools:visibility="gone"
/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/contact_crypto_status_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:contentDescription="@null"
android:visibility="gone"
app:srcCompat="@drawable/ic_status_corner"
app:tint="?openpgp_black"
tools:visibility="visible"
/>

<ImageView
android:id="@+id/contact_crypto_status_icon_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_status_corner"
app:tint="?openpgp_red"
tools:visibility="gone"
/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/contact_crypto_status_icon_enabled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:contentDescription="@null"
android:visibility="gone"
app:srcCompat="@drawable/ic_status_corner"
app:tint="?openpgp_green"
tools:visibility="gone"
/>

<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/contact_crypto_status_icon_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:contentDescription="@null"
android:visibility="gone"
app:srcCompat="@drawable/ic_status_corner"
app:tint="?openpgp_red"
tools:visibility="gone"
/>

</FrameLayout>

</com.fsck.k9.ui.compose.RecipientTokenConstraintLayout>
</com.fsck.k9.ui.compose.RecipientTokenLayout>
Original file line number Diff line number Diff line change
@@ -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 = "[email protected]"

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 = "[email protected]"

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 = "[email protected]"
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 = "[email protected]"
recipientTokenLayout.cryptoStatusView.findViewById<View>(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)

0 comments on commit a58be49

Please sign in to comment.