Status Update
Comments
ch...@google.com <ch...@google.com> #2
private fun setAsyncText(textView: AppCompatTextView, text: String?) {
if (!text.isNullOrEmpty()) {
val textFuture = PrecomputedTextCompat.getTextFuture(text!!, textView.textMetricsParamsCompat, null)
textView.setTextFuture(textFuture) //Crash
}
}
at...@monzo.com <at...@monzo.com> #3
textView.setLayoutDirection(ViewCompat.getLayoutDirection(textView));
before :
setTextFuture(....)
ch...@google.com <ch...@google.com> #4
But I think it is better when set before this line
val textFuture = PrecomputedTextCompat.getTextFuture(text!!, textView.textMetricsParamsCompat, null)
Set android:layoutDirection="locale" or android:layoutDirection="inherit" for AppCompatTextView in the XML layout didn't this problem.
The new method btw re-set layoutDirector. Weird! This should handle in the AppcompatTextView.
private fun setAsyncText(textView: AppCompatTextView, text: String?) {
if (!text.isNullOrEmpty()) {
textView.layoutDirection = textView.layoutDirection
val textFuture = PrecomputedTextCompat.getTextFuture(text!!, textView.textMetricsParamsCompat, null)
textView.setTextFuture(textFuture) //Crash
}
}
he...@ataulm.com <he...@ataulm.com> #5
minSdk 21
targetSdk 28
Thanks #3 again.
at...@monzo.com <at...@monzo.com> #6
TextViewCompat.java:889
if (!param.equals(precomputed.getParams())) {
throw new IllegalArgumentException("Given text can not be applied to TextView.");
}
PrecomputedTextCompat.class:334
with : this.mTextDir != other.getTextDirection() == true
So,
TextDirection on TextView and TextDirection on Param is difference
maybe, it handled wrong or missing conditional on getTextDirectionHeuristic of TextViewCompat
at...@monzo.com <at...@monzo.com> #7
ch...@google.com <ch...@google.com> #8
But I think you should handle it in the AppcompatTextView. It is better. If a developer forgets testing with RTL. I think this is the nightmare(it is difficult to determine the bug) when they update their app on the Play Store.
I read this article, in the part databing he noted about set the direction
fun asyncText
..........
// first, set all measurement affecting properties of the text
// (size, locale, typeface, direction, etc)
But in the offical document isn't good (lack direction)
Anything layout related property changes, text size, typeface, letter spacing, etc after this method call will causes IllegalArgumentException during View measurement.
My view: Handling this in the AppcompatTextView is the best choice if you can do it.
Thanks!
he...@ataulm.com <he...@ataulm.com> #9
ki...@google.com <ki...@google.com> #10
he...@ataulm.com <he...@ataulm.com> #11
he...@ataulm.com <he...@ataulm.com> #12
Description shared by an external reporter:
================================
1.
Version used: androidx.appcompat:appcompat:1.1.0-alpha1
AppCompatTextView.setTextFuture
i found
2.
Non-fatal Exception: java.lang.IllegalArgumentException: Given text can not be applied to TextView.
at androidx.core.widget.TextViewCompat.retrieveField(Unknown Source:22)
at androidx.appcompat.widget.AppCompatTextView.consumeTextFutureAndSetBlocking(Unknown Source:15)
at androidx.appcompat.widget.AppCompatTextView.onMeasure(Unknown Source)
at com.bilibili.magicasakura.widgets.AppCompatTintTextView.onMeasure(Unknown Source)
at android.view.View.measure(View.java:22145)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6602)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1514)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:806)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:685)
at android.view.View.measure(View.java:22145)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6602)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:185)
at androidx.cardview.widget.CardView.onMeasure(Unknown Source:80)
at android.view.View.measure(View.java:22145)
at androidx.recyclerview.widget.RecyclerView$LayoutManager.measureChildWithMargins(Unknown Source:98)
at androidx.recyclerview.widget.LinearLayoutManager.generateDefaultLayoutParams(Unknown Source:60)
at androidx.recyclerview.widget.LinearLayoutManager.generateDefaultLayoutParams(Unknown Source:44)
at androidx.recyclerview.widget.LinearLayoutManager.findViewByPosition(Unknown Source:36)
at androidx.recyclerview.widget.LinearLayoutManager.setOrientation(Unknown Source:6)
at androidx.recyclerview.widget.RecyclerView.exceptionLabel(Unknown Source:39)
at androidx.recyclerview.widget.RecyclerView$ViewFlinger.run(Unknown Source:91)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:979)
at android.view.Choreographer.doCallbacks(Choreographer.java:791)
at android.view.Choreographer.doFrame(Choreographer.java:723)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:965)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6707)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886)
at de.robv.android.xposed.XposedBridge.main(XposedBridge.java:108)
device: meizu, Android 7 and 8
And you can see fabric detail
===========================================
Requesting team for initial inputs. Please let us know if this can be handled as a part of this bug else we will create a separate bug for this.
ch...@google.com <ch...@google.com> #13
al...@google.com <al...@google.com> #14
Alan - what's the expectation of arbitrary "nesting" of color state lists?
...
But what if that other selector (primary_60) had multiple entries for different states? Would that reference resolve to the default color?
I say we keep to the simple case, which is what the framework implicitly does: squash down to the default color.
Agreed, I think adding nesting to CsL would be going a bit too far.
ki...@google.com <ki...@google.com> #15
ch...@google.com <ch...@google.com> #16
LGTM to me overall
at...@monzo.com <at...@monzo.com> #17
I had a look into adding a callback so that CSLInflaterCompat could hook back into ResourcesCompat, if it needed to inflate another CSL, which has a cache of inflated CSLs.
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
interface ColorStateListCache {
@Nullable
ColorStateList getCachedColorStateList(Resources resources, @ColorRes int resId);
}
Since ResourcesCompat
is the only one with a CSL cache, we could pass a nullable cache to createFromXml
:
@NonNull
public static ColorStateList createFromXml(
@NonNull Resources r,
@NonNull XmlPullParser parser,
@Nullable Resources.Theme theme,
@Nullable ColorStateListCache cache
) throws XmlPullParserException, IOException {
final AttributeSet attrs = Xml.asAttributeSet(parser);
// ...
and createFromXmlInner
:
@NonNull
public static ColorStateList createFromXmlInner(
@NonNull Resources r,
@NonNull XmlPullParser parser,
@NonNull AttributeSet attrs,
@Nullable Resources.Theme theme,
@Nullable ColorStateListCache cache
) throws XmlPullParserException, IOException {
final String name = parser.getName();
// ...
Although these functions are public, this class is restricted to the library group, so only a few changes are required (and no public API changes):
ComplexColorCompat.inflate()
usescreateFromXmlInner
. This would pass anull
cache.TypedArrayUtils.getNamedColorStateList()
usescreateFromXml()
. This would pass anull
cache too.ResourcesCompat
usescreateFromXml()
. This passes a non-null cache:
@Nullable
private static ColorStateList inflateColorStateList(Resources resources, int resId,
@Nullable Theme theme) {
if (isColorInt(resources, resId)) {
// The resource is a color int, we can't handle it so return null
return null;
}
final XmlPullParser xml = resources.getXml(resId);
try {
return ColorStateListInflaterCompat.createFromXml(resources, xml, theme, cache);
} catch (Exception e) {
Log.e(TAG, "Failed to inflate ColorStateList, leaving it to the framework", e);
}
return null;
}
private static final ColorStateListCache cache = new ColorStateListCache() {
@Nullable
@Override
public ColorStateList getCachedColorStateList(Resources resources, int resId) {
return ResourcesCompat.getCachedColorStateList(resources, resId);
}
};
What do you think?
at...@monzo.com <at...@monzo.com> #18
*The logic in the private inflate
function gets pretty gnarly in Java though:
int resourceId = a.getResourceId(R.styleable.ColorStateListItem_android_color, -1);
int baseColor;
if (resourceId != -1 && !isColorInt(r, resourceId)) {
try {
if (cache != null) {
ColorStateList cachedCsl = cache.getCachedColorStateList(r, resourceId);
if (cachedCsl != null) {
baseColor = cachedCsl.getDefaultColor();
} else {
baseColor = createFromXml(r, r.getXml(resourceId),
theme).getDefaultColor();
}
} else {
baseColor = createFromXml(r, r.getXml(resourceId), theme).getDefaultColor();
}
} catch (Exception e) {
baseColor = a.getColor(R.styleable.ColorStateListItem_android_color,
Color.MAGENTA);
}
} else {
baseColor = a.getColor(R.styleable.ColorStateListItem_android_color, Color.MAGENTA);
}
ch...@google.com <ch...@google.com> #19
Hmmm, I say we keep to the simple solution which we spoke about in #8.
ki...@google.com <ki...@google.com> #20
The contributor CL has been merged and will be available in the next alphas of core and appcompat
ap...@google.com <ap...@google.com> #21
Branch: androidx-master-dev
commit e232f52b6f340b10ac5bed52d065068ac78e7b06
Author: Ataul Munim <hello@ataulm.com>
Date: Fri May 29 21:41:42 2020
Add CSL value support to ColorStateListInflaterCompat
Bug: 155579892
Test: ColorStateListInflaterCompatTest
Change-Id: I61efc84945a4ac1633b085b8fe1b83aca1a6cd4b
M appcompat/appcompat-resources/src/main/java/androidx/appcompat/content/res/AppCompatResources.java
M core/core/src/androidTest/java/androidx/core/content/res/ColorStateListInflaterCompatTest.java
A core/core/src/androidTest/res/color/color_state_list_secondary_text.xml
M core/core/src/androidTest/res/values/styles.xml
M core/core/src/main/java/androidx/core/content/ContextCompat.java
M core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java
M core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
ki...@google.com <ki...@google.com>
ki...@google.com <ki...@google.com> #22
Detected a regression that this CL introduced.
al...@google.com <al...@google.com> #23
P1 to block stable.
Looks like the CSL cache is keying resolved CSLs on Resources
without accounting for the Theme
against which they was resolved. It was always broken, we just never noticed.
Global cache keyed on Resources
:
private static final WeakHashMap<Resources, SparseArray<ColorStateListCacheEntry>>
sColorStateCaches = new WeakHashMap<>(0);
Resolved ColorStateList
and no mention of Theme
:
private static class ColorStateListCacheEntry {
final ColorStateList mValue;
final Configuration mConfiguration;
ColorStateListCacheEntry(@NonNull ColorStateList value,
@NonNull Configuration configuration) {
mValue = value;
mConfiguration = configuration;
}
}
ki...@google.com <ki...@google.com> #24
From Alan:
The base cache class is Resources.ThemeKey
. The old implementation in appcompat-resources
never ran into this issue since it never supported nested CSLs. However, now you can run into a cached CSL that was resolved against a Theme
, and then get the same CSL when you were asking for one that matches Resources
with no Theme
. So keying on Configuration
alone is not enough for caching.
A potential quick fix is to disable caching altogether (same as in framework). Or alternatively add something to the ColorStateListCacheEntry
that would respect the Theme
set on Resources
al...@google.com <al...@google.com> #25
Note that the platform's ResourcesImpl
keys the cache on Resources
, Theme
, and asset cookie:
final long key = (((long) value.assetCookie) << 32) | value.data;
final ConfigurationBoundResourceCache<ComplexColor> cache = mComplexColorCache;
ComplexColor complexColor = cache.getInstance(key, wrapper, theme);
Theme
doesn't implement hashCode
and its comparable ThemeKey
isn't public API, but you'd still get some benefits from keying off the Object
implementation of hashCode
.
al...@google.com <al...@google.com>
he...@ataulm.com <he...@ataulm.com> #26
What's the preferred approach? I can pick this up.
he...@ataulm.com <he...@ataulm.com> #27
I didn't understand when the Resources
might be different - is it this?
ResourcesCompat.getColorStateList(
context.resources,
R.color.my_csl,
ContextThemeWrapper(context, R.style.ThemeOverlay_EverythingDifferent).theme
)
If adding Theme
to ColorStateListCacheEntry
isn't good enough because the lack of a legit hashcode implementation means potential collisions, could we instead keep Resources
as the key in the global cache, but use theme?.resources ?: res
instead?
he...@ataulm.com <he...@ataulm.com> #28
The old implementation in appcompat-resources never ran into this issue since it never supported nested CSLs. However, now you can run into a cached CSL that was resolved against a Theme, and then get the same CSL when you were asking for one that matches Resources with no Theme.
This is the part I didn't understand. The nested CSL isn't resolved from the cache, it's always inflated, so I didn't follow why this is a problem now, but wasn't before.
he...@ataulm.com <he...@ataulm.com> #29
@Alan, is this still blocking?
al...@google.com <al...@google.com> #30
could we instead keep
Resources
as the key in the global cache, but usetheme?.resources ?: res
instead?
No, because many Theme
s will reference the same Resources
object. Theme
is a non-static inner class of Resources
and contains a reference to the Resources
object from which it was created.
@Alan, is this still blocking?
Not blocking, per se. We had to revert the CL. Lowering to P2 and considering this a FR now.
he...@ataulm.com <he...@ataulm.com> #32
I'm able to reproduce the bug and I understand what you mean now. It's a very common case, e.g. when using theme overlays:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Purple"
android:textColor="@color/csl_pointing_to_color_primary_which_is_purple" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Green"
android:textColor="@color/csl_pointing_to_color_primary_which_is_purple"
android:theme="@style/ThemeOverlay.GreenPrimary" />
We'd expect green text for the second one, but @color/csl_pointing_to_color_primary_which_is_purple
is cached, so it'll be purple. :+1:
I added a key that takes Theme
into account:
private static class ColorStateListCacheKey {
final Resources mResources;
@Nullable final Theme mTheme;
ColorStateListCacheKey(@NonNull Resources resources, @Nullable Theme theme) {
mResources = resources;
mTheme = theme;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ColorStateListCacheKey that = (ColorStateListCacheKey) o;
return mResources.equals(that.mResources) &&
Objects.equals(mTheme, that.mTheme);
}
@Override
public int hashCode() {
return Objects.hash(mResources, mTheme);
}
}
I don't think there would be any false positive matches anymore (the cache bug), but there could still be some false negatives (unnecessary inflation, duplicate values in cache). As you said this is what ResourcesImpl
does (*), is this a sufficient solution?
(*) I didn't understand the value of including the asset cookie in the key. Is that applicable for this case?
he...@ataulm.com <he...@ataulm.com> #33
Bump, I've got time to work on this this month. If I submit a CL with the above, reckon someone could take a look?
ki...@google.com <ki...@google.com> #34
Sure, if you have a CL with test covering the new paths, we'll be happy to get it in and see if it breaks any of the app tests that we have.
he...@ataulm.com <he...@ataulm.com> #35
CL here
I didn't add any new tests - ResourcesCompatTest
already covered the bug as I understood it, where R.color.complex_themed_selector
is loaded twice with distinct themes. I extended the coverage down to the minSdkVersion of the library (but could only test it on API 15).
ki...@google.com <ki...@google.com>
ap...@google.com <ap...@google.com> #36
Branch: androidx-main
commit 63f5fe8e647c9b5b704a33c34201dbc3bab946ef
Author: Ataul Munim <hello@ataulm.com>
Date: Sun Jul 26 10:05:33 2020
Add CSL value support to ColorStateListInflaterCompat
Bug: 155579892
Test: ColorStateListInflaterCompatTest verifies nested CSLs are loaded,
ResourcesCompatTest extends getThemedCsl coverage to all API versions
(tested down to API 15)
Change-Id: I2e4098d45173a443dda97a23923c02755e83acfb
M appcompat/appcompat-resources/src/main/java/androidx/appcompat/content/res/AppCompatResources.java
M core/core/src/androidTest/java/androidx/core/content/res/ColorStateListInflaterCompatTest.java
M core/core/src/androidTest/java/androidx/core/content/res/ResourcesCompatTest.java
A core/core/src/androidTest/res/color/color_state_list_secondary_text.xml
M core/core/src/androidTest/res/values/styles.xml
M core/core/src/main/java/androidx/core/content/ContextCompat.java
M core/core/src/main/java/androidx/core/content/res/ColorStateListInflaterCompat.java
M core/core/src/main/java/androidx/core/content/res/ResourcesCompat.java
Description
Issue
Steps to reproduce
colorPrimary
has a value@color/blue
(a plain hex color).@color/primary_60
is a single value CSL used to apply 60% alpha@color/text_tertiary
is a CSL with enabled/disabled states. It references@color/primary_60
for both states, applying a further 60% alpha for the disabled state.(This is a contrived use case, since we could define
@color/text_tertiary
in terms of?attr/colorPrimary
rather than@color/primary_60
.)The bug is that
AppCompatResources.getColorStateList
behaviour doesn't match the framework's resolution of CSLs.The same behavior is exhibited if using
android:textColor
or manually loading the@color/text_tertiary
withAppCompatResources
and usingsetTextColor(ColorStateList)
(AppCompatTextView
usesAppCompatResources
for this attribute under the hood).Screenshots
Attached screenshot comparing API 22 and API 29.