Merge "[Ongoing Call Chip] UI tweaks: Don't have the chip width change each second, and hide the time if it's too large for the space available." into sc-dev

This commit is contained in:
Caitlin Cassidy
2021-05-06 14:24:03 +00:00
committed by Android (Google) Code Review
3 changed files with 251 additions and 4 deletions

View File

@@ -29,18 +29,17 @@
android:src="@*android:drawable/ic_phone"
android:layout_width="@dimen/ongoing_call_chip_icon_size"
android:layout_height="@dimen/ongoing_call_chip_icon_size"
android:paddingEnd="@dimen/ongoing_call_chip_icon_text_padding"
android:tint="?android:attr/colorPrimary"
/>
<!-- TODO(b/183229367): The text in this view isn't quite centered within the chip. -->
<!-- TODO(b/183229367): This text view's width shouldn't change as the time increases. -->
<Chronometer
<com.android.systemui.statusbar.phone.ongoingcall.OngoingCallChronometer
android:id="@+id/ongoing_call_chip_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:gravity="center"
android:gravity="center|start"
android:paddingStart="@dimen/ongoing_call_chip_icon_text_padding"
android:textAppearance="@android:style/TextAppearance.Material.Small"
android:fontFamily="@*android:string/config_headlineFontFamily"
android:textColor="?android:attr/colorPrimary"

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.phone.ongoingcall
import android.content.Context
import android.util.AttributeSet
import android.widget.Chronometer
/**
* A [Chronometer] specifically for the ongoing call chip in the status bar.
*
* This class handles:
* 1) Setting the text width. If we used a basic WRAP_CONTENT for width, the chip width would
* change slightly each second because the width of each number is slightly different.
*
* Instead, we save the largest number width seen so far and ensure that the chip is at least
* that wide. This means the chip may get larger over time (e.g. in the transition from 59:59
* to 1:00:00), but never smaller.
*
* 2) Hiding the text if the time gets too long for the space available. Once the text has been
* hidden, it remains hidden for the duration of the call.
*
* Note that if the text was too big in portrait mode, resulting in the text being hidden, then the
* text will also be hidden in landscape (even if there is enough space for it in landscape).
*/
class OngoingCallChronometer @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : Chronometer(context, attrs, defStyle) {
// Minimum width that the text view can be. Corresponds with the largest number width seen so
// far.
var minimumTextWidth: Int = 0
// True if the text is too long for the space available, so the text should be hidden.
var shouldHideText: Boolean = false
override fun setBase(base: Long) {
// These variables may have changed during the previous call, so re-set them before the new
// call starts.
minimumTextWidth = 0
shouldHideText = false
super.setBase(base)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
if (shouldHideText) {
setMeasuredDimension(0, 0)
return
}
// Evaluate how wide the text *wants* to be if it had unlimited space.
super.onMeasure(
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
heightMeasureSpec)
val desiredTextWidth = measuredWidth
// Evaluate how wide the text *can* be based on the enforced constraints
val enforcedTextWidth = resolveSize(desiredTextWidth, widthMeasureSpec)
if (desiredTextWidth > enforcedTextWidth) {
shouldHideText = true
setMeasuredDimension(0, 0)
} else {
// It's possible that the current text could fit in a smaller width, but we don't want
// the chip to change size every second. Instead, keep it at the minimum required width.
minimumTextWidth = desiredTextWidth.coerceAtLeast(minimumTextWidth)
setMeasuredDimension(minimumTextWidth, MeasureSpec.getSize(heightMeasureSpec))
}
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.phone.ongoingcall
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
private const val TEXT_VIEW_MAX_WIDTH = 400
// When a [Chronometer] is created, it starts off with "00:00" as its text.
private const val INITIAL_TEXT = "00:00"
private const val LARGE_TEXT = "00:000"
private const val XL_TEXT = "00:0000"
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class OngoingCallChronometerTest : SysuiTestCase() {
private lateinit var textView: OngoingCallChronometer
private lateinit var doesNotFitText: String
@Before
fun setUp() {
allowTestableLooperAsMainThread()
TestableLooper.get(this).runWithLooper {
val chipView = LayoutInflater.from(mContext)
.inflate(R.layout.ongoing_call_chip, null) as LinearLayout
textView = chipView.findViewById(R.id.ongoing_call_chip_time)!!
measureTextView()
calculateDoesNotFixText()
}
}
@Test
fun verifyTextSizes() {
val initialTextLength = textView.paint.measureText(INITIAL_TEXT)
val largeTextLength = textView.paint.measureText(LARGE_TEXT)
val xlTextLength = textView.paint.measureText(XL_TEXT)
// Assert that our test text sizes do what we expect them to do in the rest of the tests.
assertThat(initialTextLength).isLessThan(TEXT_VIEW_MAX_WIDTH)
assertThat(largeTextLength).isLessThan(TEXT_VIEW_MAX_WIDTH)
assertThat(xlTextLength).isLessThan(TEXT_VIEW_MAX_WIDTH)
assertThat(textView.paint.measureText(doesNotFitText)).isGreaterThan(TEXT_VIEW_MAX_WIDTH)
assertThat(largeTextLength).isGreaterThan(initialTextLength)
assertThat(xlTextLength).isGreaterThan(largeTextLength)
}
@Test
fun onMeasure_initialTextFitsInSpace_textDisplayed() {
assertThat(textView.measuredWidth).isGreaterThan(0)
}
@Test
fun onMeasure_newTextLargerThanPreviousText_widthGetsLarger() {
val initialTextLength = textView.measuredWidth
setTextAndMeasure(LARGE_TEXT)
assertThat(textView.measuredWidth).isGreaterThan(initialTextLength)
}
@Test
fun onMeasure_newTextSmallerThanPreviousText_widthDoesNotGetSmaller() {
setTextAndMeasure(XL_TEXT)
val xlWidth = textView.measuredWidth
setTextAndMeasure(LARGE_TEXT)
assertThat(textView.measuredWidth).isEqualTo(xlWidth)
}
@Test
fun onMeasure_textDoesNotFit_textHidden() {
setTextAndMeasure(doesNotFitText)
assertThat(textView.measuredWidth).isEqualTo(0)
}
@Test
fun onMeasure_newTextFitsButPreviousTextDidNot_textHidden() {
setTextAndMeasure(doesNotFitText)
setTextAndMeasure(LARGE_TEXT)
assertThat(textView.measuredWidth).isEqualTo(0)
}
@Test
fun resetBase_hadLongerTextThenSetBaseThenShorterText_widthIsShort() {
setTextAndMeasure(XL_TEXT)
val xlWidth = textView.measuredWidth
textView.base = 0L
setTextAndMeasure(INITIAL_TEXT)
assertThat(textView.measuredWidth).isLessThan(xlWidth)
assertThat(textView.measuredWidth).isGreaterThan(0)
}
@Test
fun setBase_wasHidingTextThenSetBaseThenShorterText_textShown() {
setTextAndMeasure(doesNotFitText)
textView.base = 0L
setTextAndMeasure(INITIAL_TEXT)
assertThat(textView.measuredWidth).isGreaterThan(0)
}
private fun setTextAndMeasure(text: String) {
textView.text = text
measureTextView()
}
private fun measureTextView() {
textView.measure(
View.MeasureSpec.makeMeasureSpec(TEXT_VIEW_MAX_WIDTH, View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
}
/**
* Calculates what [doesNotFitText] should be. Needs to be done dynamically because different
* devices have different densities, which means the textView can fit different amounts of
* characters.
*/
private fun calculateDoesNotFixText() {
var currentText = XL_TEXT + "0"
while (textView.paint.measureText(currentText) <= TEXT_VIEW_MAX_WIDTH) {
currentText += "0"
}
doesNotFitText = currentText
}
}