-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use custom layout for recipient tokens
- Loading branch information
Showing
4 changed files
with
256 additions
and
91 deletions.
There are no files selected for viewing
26 changes: 0 additions & 26 deletions
26
legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenConstraintLayout.kt
This file was deleted.
Oops, something went wrong.
72 changes: 72 additions & 0 deletions
72
legacy/ui/legacy/src/main/java/com/fsck/k9/ui/compose/RecipientTokenLayout.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
109
legacy/ui/legacy/src/main/res/layout/recipient_token_item.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
140 changes: 140 additions & 0 deletions
140
legacy/ui/legacy/src/test/java/com/fsck/k9/ui/compose/RecipientTokenLayoutTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |