parent
f930c1149e
commit
5ecd8ac0df
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"/>
|
||||
|
||||
|
|
Loading…
Reference in New Issue