How to override an android style's private attribute - android

I am trying to find a way to override a non-public attribute of an android style, more specifically an atttribute named itemColor of the Widget.FragmentBreadCrumbs style. This style affects the text color of the breadcumb in a PreferenceActivity when a preference fragment is being displayed on the right pane for large screens. It is used by the class FragmentBreadCrumbs.
My application uses a custom theme that extends Theme.Holo.Light and the theme breaks on API 23 so I am trying to find a workaround.
The aforementioned style sets a default value to itemColor of #null which is not overridden in the Holo theme while for example it is set to a valid value for the Material theme. As a result the title of the breadcrumb is not visible (see screenshot for API 19 and screenshot for API 23)
I guess what I am trying to do is to find a way that could change a private value of a theme similar to the way reflection can be used to modify the private field's value of a class. Alternatively the ContextThemeWrapper seems to be promising but I simple don't get how can I use it or even if it is applicable in my situtation.
What I need is that after FragmentBreadCrumbs class executes its constructor below the mTextColor attribute to not be #null (which I am guessing is 0) as is set by the Android theme configuration but to have a valid color value.
Do you think this is possible?
public FragmentBreadCrumbs(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.FragmentBreadCrumbs, defStyleAttr, defStyleRes);
mGravity = a.getInt(com.android.internal.R.styleable.FragmentBreadCrumbs_gravity,
DEFAULT_GRAVITY);
mLayoutResId = a.getResourceId(
com.android.internal.R.styleable.FragmentBreadCrumbs_itemLayout,
com.android.internal.R.layout.fragment_bread_crumb_item);
/* This is the value needed to be overridden */
mTextColor = a.getColor(
com.android.internal.R.styleable.FragmentBreadCrumbs_itemColor,
0);
a.recycle();
}

Unfortunately the toolchain will report an error if you try to use android:itemColor because this does not correspond to a public attribute name, so you cannot even make a style with this attribute.
The only thing I can think of is to change the text color via reflection just after the views have been constructed(/inflated). You would want to do this as early as possible, before the first time updateCrumbs() is run inside of FragmentBreadCrumbs. Perhaps you can override onCreate() of PreferenceActivity or onCreateView() of PreferenceFragment (whichever is applicable here) and do something like this:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentBreadCrumbs fbc = (FragmentBreadCrumbs) findViewById(...);
int color = ...;
FragmentBreadCrumbsUtils.setTextColor(fbc, color);
}
public class FragmentBreadCrumbsUtils {
private static final Field FRAGMENT_BREAD_CRUMBS_TEXT_COLOR = findField();
private static Field findField() {
try {
Field f = FragmentBreadCrumbs.class.getDeclaredField("mTextColor");
f.setAccessible(true);
return f;
} catch (Throwable t) {
// don't fail for any reason, just log it
Log.e("FragmentBreadCrumbsUtils",
"Couldn't find mTextColor field in FragmentBreadCrumbs",
t);
}
return null;
}
public static void setTextColor(FragmentBreadCrumbs fbc, int color) {
if (FRAGMENT_BREAD_CRUMBS_TEXT_COLOR == null) {
return; // can't do anything, we don't have the field
}
try {
FRAGMENT_BREAD_CRUMBS_TEXT_COLOR.set(fbc, color);
} catch (Throwable t) {
// don't fail for any reason, just log it
Log.e("FragmentBreadCrumbsUtils",
"Couldn't set mTextColor field in FragmentBreadCrumbs",
t);
}
}
}

Related

Is it possible to have different font for floating text and hint text of InputTextLayout

Currently, I would like to have a bold effect on my floating text of InputTextLayout. Here's what I'm doing
this.usernameTextInputLayout.setTypeface(Utils.ROBOTO_BOLD_TYPE_FACE);
It works as expected. The floating text (Username) has became bold.
However, this will create another undesired effect for me. The hint text will become bold as well.
You can compare the above 2 images. Please take note, for comparison purpose, I just leave passwordTextInputLayout as it is.
Is it possible to have different fonts, for floating text and hint text of InputTextLayout?
As you know, TextInputLayout uses a private helper class to handle the hint text stylings and animations. This class - CollapsingTextHelper - maintains separate typefaces for its collapsed and expanded states. We just need to set the right one, which we'll do using reflection.
I usually package these kinds of functionalities into custom subclasses, so I'll do the same here. If you don't want to use a subclass, the reflection stuff could be easily pulled into some simple methods you can put in your Activity or utility class.
public class CustomTextInputLayout extends TextInputLayout {
private Object collapsingTextHelper;
private Method setCollapsedTypefaceMethod;
private Method setExpandedTypefaceMethod;
public CustomTextInputLayout(Context context) {
this(context, null);
}
public CustomTextInputLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomTextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
try {
Field cthField = TextInputLayout.class
.getDeclaredField("mCollapsingTextHelper");
cthField.setAccessible(true);
collapsingTextHelper = cthField.get(this);
setCollapsedTypefaceMethod = collapsingTextHelper
.getClass().getDeclaredMethod("setCollapsedTypeface", Typeface.class);
setCollapsedTypefaceMethod.setAccessible(true);
setExpandedTypefaceMethod = collapsingTextHelper
.getClass().getDeclaredMethod("setExpandedTypeface", Typeface.class);
setExpandedTypefaceMethod.setAccessible(true);
}
catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException e) {
collapsingTextHelper = null;
setCollapsedTypefaceMethod = null;
setExpandedTypefaceMethod = null;
e.printStackTrace();
}
}
public void setCollapsedTypeface(Typeface typeface) {
if (collapsingTextHelper == null) {
return;
}
try {
setCollapsedTypefaceMethod.invoke(collapsingTextHelper, typeface);
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
}
}
public void setExpandedTypeface(Typeface typeface) {
if (collapsingTextHelper == null) {
return;
}
try {
setExpandedTypefaceMethod.invoke(collapsingTextHelper, typeface);
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
Somewhat counterintuitively, TextInputLayout's collapsed state is when the hint is the floating label above the EditText. Its expanded state is when the hint is in the "normal" position, inside the EditText. Methods to set the typeface for both states are given above.
This is a drop-in replacement for TextInputLayout, and you can use it in your layouts just as you would that. For example:
<com.mycompany.myapp.CustomTextInputLayout
android:id="#+id/username_til"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:hintTextAppearance="#style/TextLabel">
<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:hint="Username" />
</com.mycompany.myapp.CustomTextInputLayout>
In your code, to set the typeface of the floating text hint:
CustomTextInputLayout usernameTextInputLayout =
(CustomTextInputLayout) findViewById(R.id.username_til);
usernameTextInputLayout.setCollapsedTypeface(Utils.ROBOTO_BOLD_TYPE_FACE);
The CollapsingTextHelper methods used above were added in version 23.1.0 of the support library. If you're using a previous version, or you're getting a NoSuchMethodException for some other reason, the original version of my answer that directly sets the typeface fields should work, no matter the version.

Android AppCompat 23.1.0 Tint Compound Drawable

I was using the method below to properly tint compound drawables with android.support.design 23.0.1 . Now that they released 23.1.0 it doesn't work anymore on api LVL16, all my drawables are black.
Anyone has a suggestion ?
private void setCompoundColor(TextView view) {
Drawable drawable = view.getCompoundDrawables()[0];
Drawable wrap = DrawableCompat.wrap(drawable);
DrawableCompat.setTint(wrap, ContextCompat.getColor(this, R.color.primaryLighter2));
DrawableCompat.setTintMode(wrap, PorterDuff.Mode.SRC_IN);
wrap = wrap.mutate();
view.setCompoundDrawablesRelativeWithIntrinsicBounds(wrap, null, null, null);
}
Thanks.
I faced the same problem last week, and it turns out in the AppCompatTextView v23.1.0, compound drawables are automatically tinted.
Here is the solution I found, with more explications on why I did this below. Its not very clean but at least it enables you to tint your compound drawables !
SOLUTION
Put this code in a helper class or in your custom TextView/Button :
/**
* The app compat text view automatically sets the compound drawable tints for a static array of drawables ids.
* If the drawable id is not in the list, the lib apply a null tint, removing the custom tint set before.
* There is no way to change this (private attributes/classes, only set in the constructor...)
*
* #param object the object on which to disable default tinting.
*/
public static void removeDefaultTinting(Object object) {
try {
// Get the text helper field.
Field mTextHelperField = object.getClass().getSuperclass().getDeclaredField("mTextHelper");
mTextHelperField.setAccessible(true);
// Get the text helper object instance.
final Object mTextHelper = mTextHelperField.get(object);
if (mTextHelper != null) {
// Apply tint to all private attributes. See AppCompat source code for usage of theses attributes.
setObjectFieldToNull(mTextHelper, "mDrawableStartTint");
setObjectFieldToNull(mTextHelper, "mDrawableEndTint");
setObjectFieldToNull(mTextHelper, "mDrawableLeftTint");
setObjectFieldToNull(mTextHelper, "mDrawableTopTint");
setObjectFieldToNull(mTextHelper, "mDrawableRightTint");
setObjectFieldToNull(mTextHelper, "mDrawableBottomTint");
}
} catch (NoSuchFieldException e) {
// If it doesn't work, we can do nothing else. The icons will be white, we will see it.
e.printStackTrace();
} catch (IllegalAccessException e) {
// If it doesn't work, we can do nothing else. The icons will be white, we will see it.
e.printStackTrace();
}
}
/**
* Set the field of an object to null.
*
* #param object the TextHelper object (class is not accessible...).
* #param fieldName the name of the tint field.
*/
private static void setObjectFieldToNull(Object object, String fieldName) {
try {
Field tintField;
// Try to get field from class or super class (depends on the implementation).
try {
tintField = object.getClass().getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
tintField = object.getClass().getSuperclass().getDeclaredField(fieldName);
}
tintField.setAccessible(true);
tintField.set(object, null);
} catch (NoSuchFieldException e) {
// If it doesn't work, we can do nothing else. The icons will be white, we will see it.
e.printStackTrace();
} catch (IllegalAccessException e) {
// If it doesn't work, we can do nothing else. The icons will be white, we will see it.
e.printStackTrace();
}
}
Then you can call removeDefaultTinting(this); on each constructor of your class extending AppCompatTextView or AppCompatButton. For example :
public MyCustomTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
removeDefaultTinting(this);
}
With this, code working with v23.0.1 should work on v23.1.0.
I am not satisfied by the use of reflection to change attributes in the AppCompat lib, but this is the only way I found to use tinting on compound drawables with v23.1.0. Hopefully someone will find a better solution, or compound drawable tinting will be added to the AppCompat public methods.
UPDATE
I found another simpler solution : this bug occurs only if you set compound drawables using xml. Do not set them in xml, then set them in your code and it will work. The faulty code being in the constructor, setting drawables after it has been called is not affected.
EXPLICATIONS
In AppCompatTextView constructor, a text helper is initialized :
mTextHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper.applyCompoundDrawablesTints();
In the TextHelper loadFromAttributes function, a tint list is created for each compound drawable. As you can see, mDrawableXXXTint.mHasTintList is always set to true. mDrawableXXXTint.mTintList is the tint color that will be applied, and is only get from hardcoded values of AppCompat. For your custom drawables, it will always be null. So you end up with a tint having a null "tint list".
TypedArray a = context.obtainStyledAttributes(attrs, VIEW_ATTRS, defStyleAttr, 0);
final int ap = a.getResourceId(0, -1);
// Now read the compound drawable and grab any tints
if (a.hasValue(1)) {
mDrawableLeftTint = new TintInfo();
mDrawableLeftTint.mHasTintList = true;
mDrawableLeftTint.mTintList = tintManager.getTintList(a.getResourceId(1, 0));
}
if (a.hasValue(2)) {
mDrawableTopTint = new TintInfo();
mDrawableTopTint.mHasTintList = true;
mDrawableTopTint.mTintList = tintManager.getTintList(a.getResourceId(2, 0));
}
...
The problem is that this tint is applied in the constructor, and each time a drawable is set or changed :
#Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (mBackgroundTintHelper != null) {
mBackgroundTintHelper.applySupportBackgroundTint();
}
if (mTextHelper != null) {
mTextHelper.applyCompoundDrawablesTints();
}
}
So if you apply a tint to a compound drawable, and then call a super method such as view.setCompoundDrawablesRelativeWithIntrinsicBounds, the text helper will apply its null tint to your drawable, removing everything you've done...
Finally, here is the function applying the tint :
final void applyCompoundDrawableTint(Drawable drawable, TintInfo info) {
if (drawable != null && info != null) {
TintManager.tintDrawable(drawable, info, mView.getDrawableState());
}
}
The TintInfo in parameters is the mDrawableXXXTint attribute of the texthelper class. As you can see, if it is null, no tint is applied. Setting all drawable tint attributes to null prevents AppCompat from applying its tint, and enables you to do wathever you want with the drawables.
I didn't find a clean way of blocking this behavior or getting it to apply the tint I want. All attributes are private, with no getters.
You can try something like this
ContextCompat.getDrawable(context, R.drawable.cool_icon)?.apply {
setTint(ContextCompat.getColor(context, R.color.red))
}

How to get the style attribute of a TextView

I am creating a Custom TextView class MTextView . Inside the constructor i want to know the value of style attrib of the textview so that I can set different typefaces depending on whether style is set to bold or not. But there is no getStyle() function ? What to do?
public class MTextView extends TextView{
public MTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if(style.equals(TypeFace.bold)) //how to get style?
setTypeface(Typeface.createFromAsset(getContext().getAssets(),"rc.ttf"));
}
}
You can get the textStyle from the TextView's getTypeface() instance method.
int style = getTypeface().getStyle();
If no textStyle has been specified (i.e. you want to support normal textStyle) then getTypeface() can return null.
In the case where it is not null it may be best to assume textStyle is implicitly set to normal.
Use this code:
if (attrs != null) {
try {
int style = attrs.getAttributeIntValue(
"http://schemas.android.com/apk/res/android",
"textStyle",
Typeface.NORMAL);
setTypeface(Typeface.createFromAsset(
getContext().getAssets(),
"rc.ttf"),
style);
}
catch (Exception e) {
Log.e(TAG, e.getMessage());
}
}

How to Get EditText maxLength setting in code

I would like to see the maxLength of an EditText at run time to be able to make a text display decision.
Is that possible?
Here is a description of what I wan't to do.
I have a ListView with many rows and each row have an EditText and a TextView.
I've made a subclass of ArrayAdapter to be able to feed the String that I want to place in the EditText of each row.
I have set android:maxLength="12" in the XML file.
I want to display a number in that EditText field, but if the number I want to display has more than android:maxLength="12" I want to display an "error message" instead.
And I would prefer not to hard code that 12 in my subclass of ArrayAdapter.
There is probably a simple solution, but I haven't found it yet.
(android first time...)
Only limited parameters have their getters, so I don't think you can read it .
So write length (Say 12) in values folder and use it in xml layout and arrayAdapter .
Now its not hard-coded .
1)Create integer.xml in values *
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="integer" name="max_length">12</item>
</resources>
2)In layout
<TextView android:id="#+id/tv"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:maxLength="#integer/max_length"
/>
3) in ArrayAdapter :
int maxLength = getResources().getInteger(R.integer.max_length);
This should work:
editText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(12) });
From api 21 you can do it like that:
for (InputFilter filter : mEditText.getFilters()) {
if (filter instanceof InputFilter.LengthFilter) {
((InputFilter.LengthFilter) filter).getMax());
}
}
I hope this helps someone.
extend the edit text and retrieve the value from the attributeset in the constructor.
public class MyEditText extends EditText {
public static final String XML_NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android";
private int mMaxLength;
public MyEditText(Context context) {
super(context, null);
}
public MyEditText(Context context, AttributeSet attrs) {
super(context, attrs);
mMaxLength = attrs.getAttributeIntValue(XML_NAMESPACE_ANDROID, "maxLength", -1);
}
Kotlin one line solution - returns max length or null if not set
view.filters.filterIsInstance<InputFilter.LengthFilter>().firstOrNull()?.max
As extension:
val TextView.maxLength: Int?
get() = filters.filterIsInstance<InputFilter.LengthFilter>().firstOrNull()?.max
You can get the Field value using the Reflection API.
Why You Shouldn't Do It
Just about everyone would advocate against it (including me) because:
It's slow
It's implementation-dependant
It's not intended to be accessed (obviously)
As of now, looking at the source code (Android API 19), the implementation depends on an
InputFilter.LengthFilter which is set in the constructor as:
if (maxlength >= 0) {
setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
} else {
setFilters(NO_FILTERS);
}
where maxLength is the Integer you're interested in finding, parsed from the xml attribute (android:maxLength="#integer/max_length").
This InputFilter.LengthFilter has only one field (private int mMax) and no accessor method.
How It Can Be Done
Declare a static method in a relevant utility class accepting a TextView and returning an int.
Iterate over each InputFilter set on the TextView and find one belonging to the InputFilter.LengthFilter implementation.
Access, get and return the mMax field's value using Reflection.
This would give you something like this:
import java.lang.reflect.Field;
// [...]
public static int getMaxLengthForTextView(TextView textView)
{
int maxLength = -1;
for (InputFilter filter : textView.getFilters()) {
if (filter instanceof InputFilter.LengthFilter) {
try {
Field maxLengthField = filter.getClass().getDeclaredField("mMax");
maxLengthField.setAccessible(true);
if (maxLengthField.isAccessible()) {
maxLength = maxLengthField.getInt(filter);
}
} catch (IllegalAccessException e) {
Log.w(filter.getClass().getName(), e);
} catch (IllegalArgumentException e) {
Log.w(filter.getClass().getName(), e);
} catch (NoSuchFieldException e) {
Log.w(filter.getClass().getName(), e);
} // if an Exception is thrown, Log it and return -1
}
}
return maxLength;
}
As mentioned earlier, this will break if the implementation that sets the maximum length of the TextView changes. You will be notified of this change when the method starts throwing. Even then, the method still returns -1, which you should be handling as unlimited length.
Kind of complicated, but I don't know of any other approach. I hope it works (not tested):
XmlResourceParser parser = getResources().getLayout(R.layout.theLayout);
String namespace = "http://schemas.android.com/apk/res/android";
int maxLength = parser.getAttributeIntValue(namespace, "maxLength", 12);

Android preferences summary default color?

I have installed my app in a real phone, and even though in the emulator all the texts of the
preferences summaries seem to be in the same color, in the real phone the color is different (some kind of blue... but I guess it depends on the phone's model).
How can I set this color to my custom preference component?
(I have implemented my own seek bar, and its summary text color is different from all the other components text color...).
Thanks!
Preference pUpdate = findPreference("sys_setting_update");
pUpdate.setSummary(Html.fromHtml("<font color=\"#B0C4DE\">This is content</font>"));
use Html.fromHtml("<font color=\"#B0C4DE\">This is content</font>") to setSummary
I found these: android:textAppearance="?android:attr/textAppearanceLarge"
and android:textAppearance="?android:attr/textAppearanceSmall"
seem to do the trick.
I have figured out a way to retrieve the default color used by the Android device your application is running in. It is a bit tricky and requieres that you retrieve the color being shown from another Preference Summary View of your activity and store it in runtime.
Then you can use the same color code in other Views of other preferences, assuring that you will allways get the same color code Android assigned to the standard preferences. Here is how I did it:
My preferences activity has a normal CheckBoxPreference that I use to activate or deactivate a service. I have extended CheckBoxPreference as follows, so my extension retrieves in rutime the default color Android finally gave to the summary of that CheckBoxPreference:
public class MyCheckBoxPreference extends android.preference.CheckBoxPreference {
private static int sSummaryColor = Color.WHITE;
private static boolean sInitialized = false;
public MyCheckBoxPreference(Context context) {
super(context);
}
public MyCheckBoxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyCheckBoxPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public void onBindView(View view) {
super.onBindView(view);
if (!sInitialized) {
sSummaryColor = getSummaryColor(view);
sInitialized = true;
}
}
private int getSummaryColor(View view) {
int color = Color.WHITE;
// Gets the color android gave to the summary by default
TextView summaryView = (TextView) view.findViewById(android.R.id.summary);
if (summaryView != null) {
ColorStateList list = summaryView.getTextColors();
if (list != null) {
color = list.getDefaultColor();
}
}
return color;
}
public static int getSummaryColor() {
return sSummaryColor;
}
}
In my preferences.xml I instantiate that preference as MyCheckBoxPreference instead of just CheckBoxPreference:
<org.yourpackage.MyCheckBoxPreference
android:title="#string/preference_title_activate"
android:defaultValue="false"
android:summary="#string/preference_summary_activate_off"
android:summaryOff="#string/preference_summary_activate_off"
android:key="preference_activate">
</org.yourpackage.MyCheckBoxPreference>
The MyCheckBoxPreference has to be instantiated once before retrieving the summary color with MyCheckBoxPreference.getSummaryColor().
Now you can set the color of other customized preferences from onBindView(View):
public class MyCustmizedPreference extends Preference {
public MyCustmizedPreference (Context context) {
super(context);
setLayoutResource(R.layout.my_customized_preference);
}
#Override
public void onBindView(View view) {
super.onBindView(view);
TextView summaryView = (TextView) view.findViewById(android.R.id.summary);
if (summaryView != null) {
summaryView.setTextColor(MyCheckBoxPreference.getSummaryColor());
}
}
}
It actually works under Samsung Galaxy S. I have also tested that it doesn't break anything under the emulator.
The Samsung Galaxy S phones have their own Preference layout with the text color specified for the Summary line. Even though a TextAppearance.Small is specified the textColor attribute of the layout is overriding the text appearance.
I don't think this is possible. I am able to change the background color and the title text color, but not the summary color.
Background:
getListView().setBackgroundColor(Color.TRANSPARENT);
Title text:
Preference yourpreference = findPreference("yourpreference");
TextView tv = (TextView)yourpreference.getView(null, getListView());
tv.setTextColor(...);
Sorry I couldn't help more...
I had the same problem and I've been experimenting with my custom seekbar-preference's style. Finally these lines in onCreateView method of seekBarPreference.java show preference's summary with default text color:
TextView summaryText = new TextView(getContext());
summaryText.setText(getSummary());
summaryText.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
I use it on preference_screen.xml:
<com.asdasf.SeekBarPreferencias
android:key="#string/pref_seekBar_distance_key"
android:id="#+id/mySeekBarPreference"
android:title="#string/pref_seekBar_distance_title"
android:summary="#string/pref_seekBar_distance_summary"
android:max="50"
android:defaultValue="12"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
I hope it will be useful...(and that I have written well my first answer)
Regard!

Categories

Resources