Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a58be498 authored by cketti's avatar cketti
Browse files

Use custom layout for recipient tokens

parent e8f2f0fd
Loading
Loading
Loading
Loading
+0 −26
Original line number Diff line number Diff line
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
    }
}
+72 −0
Original line number Diff line number Diff line
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)
    }
}
+44 −65
Original line number Diff line number Diff line
<?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
@@ -12,91 +13,69 @@
        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
    <FrameLayout
        android:id="@+id/crypto_status_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        >

        <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:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_status_corner"
            app:tint="?openpgp_black"
        tools:visibility="gone"
            tools:visibility="visible"
            />

    <ImageView
        <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:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_status_corner"
            app:tint="?openpgp_green"
            tools:visibility="gone"
            />

    <ImageView
        <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:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_status_corner"
            app:tint="?openpgp_red"
            tools:visibility="gone"
            />

</com.fsck.k9.ui.compose.RecipientTokenConstraintLayout>
    </FrameLayout>

</com.fsck.k9.ui.compose.RecipientTokenLayout>
+140 −0
Original line number Diff line number Diff line
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<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)