Merged PR 239416: PeoplePicker: Center count span + bug fixes

This PR introduces a new `CenterVerticalSpan` class to handle centering the `CountSpan`.  It's inspired by Android's `SuperscriptSpan`.

### Bug fixes:
1. In the process of testing this change across devices, I discovered a bug in my Nexus 4, api 19 emulator, where we crash and get an `IllegalAccessError` due to generic type issues when retrieving persona spans. I put in a fix for that using a [reified type](https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters).
2. The count span string was not updating correctly on rotation. I reverted the change that caused this and added back a todo to revisit the original bug.

### Before (CountSpan is too low):
![original.png](https://onedrive.visualstudio.com/4dcbf0bc-c3cd-49c8-a7c3-ec1924691d9b/_apis/git/repositories/32fa6338-45ea-42a0-aca0-484938e1962a/pullRequests/239416/attachments/original.png)
### After (CountSpan is centered):
![count_span_centered_new.png](https://onedrive.visualstudio.com/4dcbf0bc-c3cd-49c8-a7c3-ec1924691d9b/_apis/git/repositories/32fa6338-45ea-42a0-aca0-484938e1962a/pullRequests/239416/attachments/count_span_centered_new.png)

Related work items: #693617
This commit is contained in:
Emily Lynam 2019-03-12 17:19:06 +00:00
Родитель 63d37158db
Коммит d3f2d0ed87
3 изменённых файлов: 76 добавлений и 29 удалений

Просмотреть файл

@ -0,0 +1,30 @@
/**
* Copyright © 2019 Microsoft Corporation. All rights reserved.
*/
package com.microsoft.officeuifabric.peoplepicker
import android.graphics.Rect
import android.text.TextPaint
import android.text.style.MetricAffectingSpan
/**
* [CenterVerticalSpan] shifts the baseline of a substring to the center of the text paint bounds.
* This class comes in handy when you have a substring that is taller or shorter than the rest of your text
* and you need to center it vertically. It compares the [substringBounds] of your substring to the text paint bounds
* and shifts the baseline accordingly.
*/
internal class CenterVerticalSpan(private val substringBounds: Rect) : MetricAffectingSpan() {
override fun updateDrawState(tp: TextPaint) {
shiftBaselineToCenter(tp)
}
override fun updateMeasureState(tp: TextPaint) {
shiftBaselineToCenter(tp)
}
private fun shiftBaselineToCenter(tp: TextPaint) {
val topDifference = tp.fontMetrics.top - substringBounds.top
tp.baselineShift += (topDifference / 2).toInt()
}
}

Просмотреть файл

@ -7,7 +7,7 @@ package com.microsoft.officeuifabric.peoplepicker
import android.content.ClipData
import android.content.ClipDescription
import android.content.Context
import android.content.res.Configuration
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.os.Build
@ -38,6 +38,7 @@ import com.microsoft.officeuifabric.peoplepicker.PeoplePickerView.PersonaChipCli
import com.microsoft.officeuifabric.persona.IPersona
import com.microsoft.officeuifabric.persona.PersonaChipView
import com.microsoft.officeuifabric.persona.setPersona
import com.microsoft.officeuifabric.util.getTextSize
import com.tokenautocomplete.CountSpan
import com.tokenautocomplete.TokenCompleteTextView
@ -55,7 +56,7 @@ import com.tokenautocomplete.TokenCompleteTextView
* TODO Known issues:
* - Using backspace to delete a selected token does not work if other text is entered in the input;
* [TokenCompleteTextView] overrides [onCreateInputConnection] which blocks our ability to control this functionality.
* - [CountSpan] text should be centered
* - Persona spans do not resize dynamically when the layout changes.
*/
internal class PeoplePickerTextView : TokenCompleteTextView<IPersona> {
companion object {
@ -177,27 +178,12 @@ internal class PeoplePickerTextView : TokenCompleteTextView<IPersona> {
return TokenImageSpan(getViewForObject(obj), obj, maxTextWidth().toInt() - countSpanWidth)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// To ensure persona chips are correctly laid out when orientation changes, redraw them by re-adding them.
val personas = objects
removeObjects(personas)
addObjects(personas)
}
override fun performCollapse(hasFocus: Boolean) {
if (getOriginalCountSpan() == null)
removeCountSpanText()
super.performCollapse(hasFocus)
// Remove viewPadding for single line to fix jittery virtual view bounds in ExploreByTouch.
if (!hasFocus())
setPadding(0, 0, 0, 0)
else
setPadding(0, resources.getDimension(R.dimen.uifabric_people_picker_text_view_padding).toInt(), 0, resources.getDimension(R.dimen.uifabric_people_picker_text_view_padding).toInt())
updateCountSpanStyle()
}
@ -309,13 +295,29 @@ internal class PeoplePickerTextView : TokenCompleteTextView<IPersona> {
val originalCountSpan = getOriginalCountSpan() ?: return
text.removeSpan(originalCountSpan)
val replacementCountSpan = SpannableString(originalCountSpan.text)
// Set the TextAppearance of the count span
replacementCountSpan.setSpan(
TextAppearanceSpan(context, R.style.TextAppearance_UIFabric_PeoplePickerCountSpan),
0,
replacementCountSpan.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
// Center the count span
val replacementCountSpanPaint = Paint()
val replacementCountSpanBounds = Rect()
replacementCountSpanPaint.textSize = context.getTextSize(R.style.TextAppearance_UIFabric_PeoplePickerCountSpan)
replacementCountSpanPaint.getTextBounds(replacementCountSpan.toString(), 0, replacementCountSpan.length, replacementCountSpanBounds)
replacementCountSpan.setSpan(
CenterVerticalSpan(replacementCountSpanBounds),
0,
replacementCountSpan.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
text.replace(countSpanStart, countSpanEnd, replacementCountSpan)
}
@ -345,14 +347,14 @@ internal class PeoplePickerTextView : TokenCompleteTextView<IPersona> {
movementMethod = blockedMovementMethod
}
private fun getPersonaSpans(start: Int = 0, end: Int = text.length): Array<TokenCompleteTextView<IPersona>.TokenImageSpan> =
text.getSpans(start, end, TokenImageSpan::class.java) as Array<TokenCompleteTextView<IPersona>.TokenImageSpan>
private inline fun <reified T> getPersonaSpans(start: Int = 0, end: Int = text.length): Array<T> =
text.getSpans(start, end, TokenImageSpan::class.java) as Array<T>
private fun getOriginalCountSpan(): CountSpan? =
text.getSpans(0, text.length, CountSpan::class.java).firstOrNull()
private fun getSpanForPersona(persona: Any): TokenImageSpan? =
getPersonaSpans().firstOrNull { it.token === persona }
getPersonaSpans<TokenCompleteTextView<IPersona>.TokenImageSpan>().firstOrNull { it.token === persona }
// Token listener
@ -458,7 +460,7 @@ internal class PeoplePickerTextView : TokenCompleteTextView<IPersona> {
if (offset == -1)
return null
return getPersonaSpans(offset, offset).firstOrNull()
return getPersonaSpans<TokenCompleteTextView<IPersona>.TokenImageSpan>(offset, offset).firstOrNull()
}
private fun addPersonaFromDragEvent(event: DragEvent): Boolean {
@ -484,9 +486,6 @@ internal class PeoplePickerTextView : TokenCompleteTextView<IPersona> {
}
override fun onDown(event: MotionEvent): Boolean {
if (!isFocused)
requestFocus()
val touchedPersonaSpan = getPersonaSpanAt(event.x, event.y) ?: return true
if (allowPersonaChipDragAndDrop)
initialTouchedPersonaSpan = touchedPersonaSpan
@ -495,13 +494,16 @@ internal class PeoplePickerTextView : TokenCompleteTextView<IPersona> {
}
override fun onSingleTapUp(event: MotionEvent): Boolean {
val touchedPersonaSpan = getPersonaSpanAt(event.x, event.y) ?: return true
if (isFocused && initialTouchedPersonaSpan == touchedPersonaSpan) {
val touchedPersonaSpan = getPersonaSpanAt(event.x, event.y)
if (isFocused && initialTouchedPersonaSpan == touchedPersonaSpan && touchedPersonaSpan != null) {
if (isPersonaChipClickable(touchedPersonaSpan.token))
personaChipClickListener?.onClick(touchedPersonaSpan.token)
touchedPersonaSpan.onClick()
}
if (!isFocused)
requestFocus()
initialTouchedPersonaSpan = null
return true
}
@ -587,7 +589,7 @@ internal class PeoplePickerTextView : TokenCompleteTextView<IPersona> {
layout.getPrimaryHorizontal(start).toInt() - extraSpaceForLegibility,
layout.getLineTop(line),
layout.getPrimaryHorizontal(end).toInt() + extraSpaceForLegibility,
if (getPersonaSpans().isEmpty()) bottom else layout.getLineBottom(line)
if (getPersonaSpans<TokenCompleteTextView<IPersona>.TokenImageSpan>().isEmpty()) bottom else layout.getLineBottom(line)
)
bounds.offset(paddingLeft, paddingTop)
return bounds
@ -652,7 +654,7 @@ internal class PeoplePickerTextView : TokenCompleteTextView<IPersona> {
val offset = getOffsetForPosition(x, y)
if (offset != -1) {
val personaSpan = getPersonaSpans(offset, offset).firstOrNull()
val personaSpan = getPersonaSpans<TokenCompleteTextView<IPersona>.TokenImageSpan>(offset, offset).firstOrNull()
if (personaSpan != null && positionIsInsidePersonaBounds(x, y, personaSpan) && isFocused)
return objects.indexOf(personaSpan.token)
else if (searchConstraint.isNotEmpty() && positionIsInsideSearchConstraintBounds(x, y))
@ -770,7 +772,7 @@ internal class PeoplePickerTextView : TokenCompleteTextView<IPersona> {
private fun onPersonaSpanAccessibilityClick(personaSpan: TokenImageSpan) {
val persona = personaSpan.token
val personaSpanIndex = getPersonaSpans().indexOf(personaSpan)
val personaSpanIndex = getPersonaSpans<TokenCompleteTextView<IPersona>.TokenImageSpan>().indexOf(personaSpan)
when (personaChipClickStyle) {
PeoplePickerPersonaChipClickStyle.Select, PeoplePickerPersonaChipClickStyle.SelectDeselect -> {
if (selectedPersona != null && selectedPersona == persona) {

Просмотреть файл

@ -0,0 +1,15 @@
/**
* Copyright © 2019 Microsoft Corporation. All rights reserved.
*/
package com.microsoft.officeuifabric.util
import android.content.Context
import android.support.annotation.StyleRes
fun Context.getTextSize(@StyleRes textAppearanceResourceId: Int): Float {
val textAttributes = obtainStyledAttributes(textAppearanceResourceId, intArrayOf(android.R.attr.textSize))
val textSize = textAttributes.getDimension(0, -1f)
textAttributes.recycle()
return textSize
}