Create MoreItemsLinearLayout.kt

MAILAND-1672
This commit is contained in:
Davide Farella 2021-06-11 15:38:00 +02:00
parent f930c1149e
commit 5ecd8ac0df
5 changed files with 337 additions and 6 deletions

View File

@ -0,0 +1,147 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.ui.layout
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import ch.protonmail.android.R
import ch.protonmail.android.testAndroidInstrumented.assertion.isGone
import ch.protonmail.android.util.ViewTest
import me.proton.core.test.kotlin.assertIs
import org.junit.Assert.assertEquals
import org.junit.runner.RunWith
import kotlin.test.Test
/**
* Test suite for [MoreItemsLinearLayout]
*/
@RunWith(AndroidJUnit4ClassRunner::class)
class MoreItemsLinearLayoutTest : ViewTest<MoreItemsLinearLayout>(
::MoreItemsLinearLayout,
width = VIEW_WIDTH
) {
@Test
fun viewsAreNeverAddedAfterMore() {
// given - when
testView.apply {
addView(ImageView(context))
addView(ImageView(context), -1)
addView(ImageView(context), 1)
addView(ImageView(context), 15)
}
// then
repeat(4) { index ->
assertIs<ImageView>(testView.getChildAt(index))
}
}
@Test
fun viewsAreInsertedAtTheRightIndex() {
// given
testView.addView(TextView(context))
// when
testView.addView(ImageView(context), 1)
// then
assertIs<ImageView>(testView.getChildAt(1))
}
@Test
fun addOnlyTheViewsThatCanFit() {
// given
val childParams = LinearLayout.LayoutParams(400, LinearLayout.LayoutParams.MATCH_PARENT)
// when
testView.apply {
orientation = LinearLayout.HORIZONTAL
repeat(5) {
addView(View(context), childParams)
}
}
// then
awaitCompletion()
assertEquals(5, testView.allChildCount)
assertEquals(2, testView.visibleChildCount)
assertEquals(3, testView.hiddenChildCount)
}
@Test
fun moreShowTheRightText() {
// given
val childParams = LinearLayout.LayoutParams(400, LinearLayout.LayoutParams.MATCH_PARENT)
// when
testView.apply {
orientation = LinearLayout.HORIZONTAL
repeat(5) {
addView(View(context), childParams)
}
}
// then
onMoreView().check(matches(withText("+3")))
}
@Test
fun moreIsHiddenIfAllTheViewsCanFit() {
// given
val childParams = LinearLayout.LayoutParams(400, LinearLayout.LayoutParams.MATCH_PARENT)
// when
testView.apply {
orientation = LinearLayout.HORIZONTAL
repeat(2) {
addView(View(context), childParams)
}
}
// then
onMoreView().check(isGone())
}
private fun onMoreView(): ViewInteraction =
onView(withId(R.id.more_items_ll_more_text_view))
private fun awaitCompletion() {
onTestView()
}
private companion object {
const val VIEW_WIDTH = 1000
}
}

View File

@ -23,6 +23,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.matcher.ViewMatchers.withId
@ -33,18 +34,22 @@ import org.junit.Rule
import kotlin.test.BeforeTest
open class ViewTest<V : View>(
private val buildView: (Context) -> V
private val buildView: (Context) -> V,
private val width: Int = FrameLayout.LayoutParams.WRAP_CONTENT,
private val height: Int = FrameLayout.LayoutParams.WRAP_CONTENT
) {
@get:Rule
val activityScenarioRule = viewScenarioRule()
lateinit var testView: V
protected lateinit var testView: V
protected val context: Context get() = testView.context
@BeforeTest
fun setupView() {
open fun setupView() {
activityScenarioRule.scenario.onActivity { activity ->
testView = activity.setView(buildView)
val params = FrameLayout.LayoutParams(width, height)
testView = activity.setView(buildView, params)
testView.id = TEST_VIEW_ID
}
}

View File

@ -34,9 +34,9 @@ class ViewTestActivity : AppCompatActivity() {
setContentView(frameLayout)
}
fun <V : View> setView(buildView: (Context) -> V): V {
fun <V : View> setView(buildView: (Context) -> V, params: FrameLayout.LayoutParams): V {
val view = buildView(this)
frameLayout.addView(view)
frameLayout.addView(view, params)
return view
}
}

View File

@ -0,0 +1,178 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonMail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.ui.layout
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.StyleRes
import androidx.core.view.isVisible
import ch.protonmail.android.R
/**
* A [LinearLayout] that will show items as much as they can fit, then show a "+N" text
*/
@SuppressWarnings("TooManyFunctions")
open // Android's View overrides
class MoreItemsLinearLayout @JvmOverloads constructor (
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
@StyleRes defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
public operator fun ViewGroup.iterator(): MutableIterator<View> = object : MutableIterator<View> {
private var index = 0
override fun hasNext() = index < allChildCount
override fun next() = getChildAt(index++) ?: throw IndexOutOfBoundsException()
override fun remove() = removeViewAt(--index)
}
val allChildren: Sequence<View> get() = object : Sequence<View> {
override fun iterator() = this@MoreItemsLinearLayout.iterator()
}
// Created for shadow the homonymous extension function in ViewGroup.kt
@Deprecated("Use allChildren", ReplaceWith("allChildren"))
val children: Sequence<View> get() = allChildren
@Suppress("DEPRECATION")
val allChildCount get() = childCount - 1
val visibleChildCount get() = allChildren.filter { it.isVisible }.toList().size
val hiddenChildCount get() = allChildren.filterNot { it.isVisible }.toList().size
val moreTextView = TextView(context).apply {
id = R.id.more_items_ll_more_text_view
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
setTextAppearance(R.style.Proton_Text_Caption)
}
init {
@Suppress("LeakingThis")
addView(moreTextView)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// Find out how big everyone wants to be
measureChildren(widthMeasureSpec, heightMeasureSpec)
// Measure this layout
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var availableRelevantSize = getRelevantSizeFor(this)
// show or hide children
var limitReached = false
for ((index, child) in allChildren.toList().withIndex()) {
if (limitReached) {
// We already reached the limit, just set invisible
child.isVisible = false
continue
}
// Update "more" text and calculate spaces
moreTextView.apply {
text = "+${allChildCount - index}"
isVisible = true
}
measureChild(moreTextView, widthMeasureSpec, heightMeasureSpec)
val effectiveAvailableSize = availableRelevantSize - getRelevantSizeFor(moreTextView)
val relevantSize = getRelevantSizeFor(child)
if (relevantSize > effectiveAvailableSize) {
// Children can't fit anymore
child.isVisible = false
limitReached = true
} else {
// We can fit this child
child.isVisible = true
availableRelevantSize -= relevantSize
}
}
moreTextView.isVisible = limitReached
}
// region add
override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) {
val fixedIndex = if (index in itemsRange()) index else allChildCount
super.addView(child, fixedIndex, params)
}
override fun addViewInLayout(
child: View,
index: Int,
params: ViewGroup.LayoutParams?,
preventRequestLayout: Boolean
): Boolean {
val fixedIndex = if (index in itemsRange()) index else allChildCount
return super.addViewInLayout(child, fixedIndex, params, preventRequestLayout)
}
// endregion
// region remove
override fun removeView(view: View) {
if (indexOfChild(view) !in itemsRange()) return
super.removeView(view)
}
override fun removeViewAt(index: Int) {
if (index !in itemsRange()) return
super.removeViewAt(index)
}
override fun removeViews(start: Int, count: Int) {
val fixedCount = count.coerceAtMost(allChildCount - start)
super.removeViews(start, fixedCount)
}
override fun removeViewInLayout(view: View) {
if (indexOfChild(view) !in itemsRange()) return
super.removeViewInLayout(view)
}
// endregion
// region remove all
override fun removeAllViewsInLayout() {
removeViewsInLayout(0, allChildCount)
}
override fun removeViewsInLayout(start: Int, count: Int) {
val fixedCount = count.coerceAtMost(allChildCount - start)
super.removeViewsInLayout(start, fixedCount)
}
// endregion
@Deprecated(
"Use allChildCount, visibleChildCount or hiddenChildCount",
ReplaceWith("allChildCount")
)
override fun getChildCount() = super.getChildCount()
private fun itemsRange() = 0 until allChildCount
private fun getRelevantSizeFor(measuredView: View): Int {
return if (orientation == HORIZONTAL) measuredView.measuredWidth
else measuredView.measuredHeight
}
}

View File

@ -20,6 +20,7 @@
<resources>
<item name="composer_attachments_recycler_view" type="id"/>
<item name="more_items_ll_more_text_view" type="id"/>
<item name="multi_line_label_recycler_view" type="id"/>
<item name="side_drawer_recycler_view" type="id"/>