I am trying to use drawable tinting using the following code in my RecyclerView
Drawable likeDrawable = ContextCompat.getDrawable(getActivity(), R.drawable.ic_thumb_up);
Drawable likeWrappedDrawable = DrawableCompat.wrap(likeDrawable);
DrawableCompat.setTint(likeWrappedDrawable,ContextCompat.getColor(getActivity(), android.R.color.white));
holder.ivLike.setImageDrawable(likeWrappedDrawable);
Now all of this is being done in the onBindViewHolder of the RecyclerView Adapter
I change this tint between three colors based on the state of that list item. This works all fine for Lolipop and above but below this version, the color of the list item is unpredictable. Sometimes it shows the right color but on refreshing the list sometimes it changes to some other color.
Anything im doing wrong here or the tinting thing on pre-lollipop is still unusable in this particular case?
Update
Including the code from my onBindViewHolder
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Drawable likeDrawable =
ContextCompat.getDrawable(getActivity(), R.drawable.ic_thumb_up);
Drawable likeWrappedDrawable = DrawableCompat.wrap(likeDrawable);
holder.tvLikeCount.setTextColor(ResUtil.getColor(R.color.light_font,
getActivity()));
DrawableCompat.setTint(likeWrappedDrawable,
ContextCompat.getColor(getActivity(), android.R.color.white));
if (tweetModel.isFavorited()) {
DrawableCompat.setTint(likeWrappedDrawable,
ContextCompat.getColor(getActivity(), android.R.color.holo_blue_light));
}
holder.ivLike.setImageDrawable(likeWrappedDrawable);
}
Call mutate() on the Drawable before calling setTint()
Drawable likeDrawable = ContextCompat.getDrawable(getActivity(), R.drawable.ic_thumb_up).mutate();
By default, all drawables instances loaded from the same resource share a common state; if you modify the state of one instance, all the other instances will receive the same modification.
http://developer.android.com/reference/android/graphics/drawable/Drawable.html#mutate()
TL;DR. If you're worried about efficiency, use TintedIconCache, a single class you can grab from this gist.
TintedIconCache cache = TintedIconCache.getInstance();
Drawable myTintedDrawable = cache.fetchTintedIcon(context, R.drawable.icon_01, R.color.color_01));
The need to do more than just .mutate()
As #Vladimir stated, you have to call drawable.mutate() to get an isolated state, otherwise any change made to your drawable properties would be reflected on all the other drawables sharing the same state.
This behavior is actually meant for good reasons, one of which is memory efficiency, and there are some scenarios where you'd want to keep that efficiency.
For instance, you might have a RecyclerView with items using the same drawable but tinted differently for each item according to some property (e.g. success, failure, warning, info, etc). If you're in that case, mutating your drawable for each item is not the best solution. You could think of creating all the possible drawables, with all the possible tints, but you might be creating useless onces. If you're in that case, I got you covered!
Enters TintedIconCache
A better solution is have some sort of drawable cache, where each uniquely tinted drawable is created only when needed, and then is cached efficiently for subsequent needs until you no longer need it or the system wants that occupied memory back.
I have already implemented this, in TintedIconCache. It's straightforward to use:
// Get an instance
TintedIconCache cache = TintedIconCache.getInstance();
// Will be fetched from the resources
Drawable backIcon = cache.fetchTintedIcon(context, R.drawable.icon, R.color.black));
// Will be fetched from the resources as well
Drawable bleuIcon = cache.fetchTintedIcon(context, R.drawable.icon, R.color.bleu));
// Will be fetched from the cache!!!
Drawable backIconTwo = cache.fetchTintedIcon(context, R.drawable.icon, R.color.back));
Consider looking at the gist to see how it works under the hood.
Related
If the following TextView, tinted_tv, is tinted with SetTintList() (option 1), the property BackgroundTintList remains null (even when evaluated in a UI posted runnable).
However, when tinted with BackgroundTintList (setBackgroundTintList()) (option 2), it (getBackgroundTintList()) does not.
Both options work as expected, so I'm not sure what the significance of their difference is, or the better one to use?
Layout
<TextView
android:id="#+id/tinted_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/white"
/>
Code
TextView tv = v.FindViewById<TextView>(Resource.Id.tinted_tv);
// option 1
tv.Background.SetTintList(Context.Resources.GetColorStateList(Resource.Color.color_state_list));
// option 2
tv.BackgroundTintList = Context.Resources.GetColorStateList(Resource.Color.color_state_list);
According to the Android.Views.View.BackgroundTintList Property documentation
Get method documentation[Android Documentation]
Return the tint applied to the background drawable, if specified.
Set method documentation[Android Documentation]
Applies a tint to the background drawable. Does not modify the current tint mode, which is PorterDuff+Mode by default. Subsequent calls to View.Background will automatically mutate the drawable and apply the specified tint and tint mode using Drawable.SetTintList(ColorStateList).
I would imagine that the Xamarin BackgroundTintList property getter/setter corresponds to Android's get/setBackgroundTintList() View methods. Does "raw" Android exhibit this same behavior (getBackgroundTintList() returns null following a call to setBackgroundTintList())?
View.BackgroundTintList is managed by the View and the tint is applied to the View.Background drawable when you call it. It's a layer of abstraction.
When you work directly with Drawable.TintList nobody knows or cares about it. That's why View.BackgroundTintList value remains unaffected.
View.BackgroundTintList has no precedence over View.Background.SetTintList. Whichever you call last wins.
On second look, there is one difference best described by a snippet from View source:
private void applyBackgroundTint() {
if (mBackground != null && mBackgroundTint != null) {
final TintInfo tintInfo = mBackgroundTint;
if (tintInfo.mHasTintList || tintInfo.mHasTintMode) {
mBackground = mBackground.mutate();
if (tintInfo.mHasTintList) {
mBackground.setTintList(tintInfo.mTintList);
}
if (tintInfo.mHasTintMode) {
mBackground.setTintMode(tintInfo.mTintMode);
}
// The drawable (or one of its children) may not have been
// stateful before applying the tint, so let's try again.
if (mBackground.isStateful()) {
mBackground.setState(getDrawableState());
}
}
}
}
Background drawable state is updated to state of view when you set View.BackgroundTintList, which is why you should choose this method when working with views.
When view state changes later background drawable state is updated regardless of which method you chose.
On top of View.BackgroundTintList there's also View.BackgroundTintMode which mirrors drawable API. Both of these values can be set by attributes in layout XML, something you can't do with standalone drawables. This approach may actually be used by the platform widgets.
in a listview, for each item it needs to show some image(drawable) for different item name. If the name are same they show same image. The drawable is specific and cost some time to make it (need remote resource).
So thought maybe cache the drawable and only build new one if it has not been cached.
saw some article "ANDROID DRAWABLE INSTANCES – DON’T SHARE THEM!"
http://loseyourmarbles.co/2013/09/android-drawable-instances-dont-share/,
and here also need to animate the drawable. But test seems not seeing bad behavior (maybe not tested enough)?
So anyone has had similar requirement and experienced whether one drawable instance could be used on multiple ImageView? Is this approach right way to speed up the listview?
Or can one drawable instance be used on multiple imageView?
HashMap<String, Drawable> mNameToDrawnable = new HashMap<>();
public Drawable getDrawable(Context context, String displayName){
Drawable cachedD = mNameToDrawnable.get(displayName);
if (cachedD != null) {
return cachedD;
}
Drawnable d = makeDrawable(displayName); //new Drawable(context.getResources());, etc ……
d.setDecodeDimensions(100, 100);
mNameToDrawnable.put(displayName, d);
return d;
}
If you want to use the cached drawable in your listview and you're worried about some issues with it you might want to call mutate() on your drawable. This might give you a better understanding : https://android-developers.googleblog.com/2009/05/drawable-mutations.html?m=1
I would like to understand, why I can't dynamically change the drawable resource from my imageView.
I have different use cases, and in function of these use cases I have to change my imageView. In my code I tried these solutions below, but I can't get my imageView refresh. It keeps the default drawable resource defined first. Here what I tried:
int imgRes = activity.getResources().getIdentifier("packageName:drawable/"+"my_drawable_name", null, null);
OR
int imgRes = activity.getResources().getIdentifier("drawable/"+"my_drawable_name", "drawable", activity.getPackageName());
imageIcon.setImageResource(imgRes);
imageIcon.invalidate();
And also:
imageIcon.setImageDrawable(null);
imageIcon.setImageDrawable(activity.getResources().getDrawable(R.drawable.my_drawable_name));
imageIcon.invalidate();
I'm in one adapter and I passed this ImageView from my activity to my adapter. And I'm doing this operation from this adapter which go the instance of my imageView.
So I tried to change this resource my_drawable_name but in both cases above, my imageView is never updated/refresh, and its image resource doesn't change. I have no error it just doesn't work. Am I doing something wrong ? What's the best practice to dynamically change a resource from an imageview by code?
No need to append drawable directory. It is the default behavior of android to get best suited drawable according to device.
Try using following code:
int imgRes = activity.getResources().getIdentifier("my_drawable_name", "drawable", activity.getPackageName());
try imageIcon.setImageResource(R.drawable.my_drawable_name);
I'm using the following (very common) code to change the checkbox image in my Android app.
mCheck = (CheckBox) findViewById(R.id.chkMine);
mCheck.setButtonDrawable(R.drawable.my_image);
I see many people asking for that. But I never see the second part:
How do I put BACK the original checkbox imagery, later in my code?
I hesitate to try to design all my own images (checked, unchecked, ghosted-checked, ghosted-unchecked, etc) because I need the original images that would normally appear on many different version of Android.
Maybe, initially save the default image with a (non-existing?) getButtonDrawable() call, and then reuse it later?
I would think it would be something as simple as calling setButtonDrawable() a 2nd time to "undo" my changes. Or is it?
Thanks.
As you already correctly mention yourself, unfortunately there is no getButtonDrawable() to get a reference to the drawable used before you replace it. Obviously you could copy Android's resources over to your local project and use those to reset the CheckBox's button, but that would mean you would have to take into account all the different styles from themes, let alone any changes device manufacturers might make to those resources. It's not impossible to go down this lane, but you'll find out it will be quite of a hassle for something that sounds to simple.
What you may want to consider is doing the following: query the resources for resource ids of the drawables that you need. That way you don't explicitly have to deal with different themes, as the lookups will do that. You can easily put this functionality in a dedicated method with just a few lines. Example:
private static int getDefaultCheckBoxButtonDrawableResourceId(Context context) {
// pre-Honeycomb has a different way of setting the CheckBox button drawable
if (Build.VERSION.SDK_INT <= 10) return Resources.getSystem().getIdentifier("btn_check", "drawable", "android");
// starting with Honeycomb, retrieve the theme-based indicator as CheckBox button drawable
TypedValue value = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.listChoiceIndicatorMultiple, value, true);
return value.resourceId;
}
Below a quick example of setting the button drawable to some custom image, and reset it afterwards. I'm simply toggling between the app icon and original button drawable whenever the check state changes.
CheckBox mCheckBox = (CheckBox) findViewById(R.id.checkbox);
mCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override public void onCheckedChanged(CompoundButton button, boolean isChecked) {
button.setButtonDrawable(isChecked ? R.drawable.icon : getDefaultCheckBoxButtonDrawableResourceId(StackOverflowActivity.this));
}
});
I think to retrieve the original images a call to setButtonDrawable() should work again.
But instead of your resource you'll reference to android's original resource.
mCheck = (CheckBox) findViewById(R.id.chkMine);
mCheck.setButtonDrawable(android.R.drawable.*);
Probably you've to lookup the file name on your own:
platforms > android-* > data > res > drawable-*
(source: accepted answer)
edit
Ha! Me knew me've seen that site before: Android R Drawables ^^v
(credits to Mef)
How Do You Force A NinePatchDrawable to release the BitMap bytes it parsed out of 'res'?
As an Android developer I face pressure to control memory utilization in my games.
I work to control memory utilization by releasing resources as quickly as I can after they are no longer used. To this end, I maintain a list of all loaded resources and purge / release them from memory as soon as I am done with them.
My application uses a number of different types of graphical resources
BitMap
BitMapDrawable
Drawable
NinePatchDrawable
How do I release these objects right now?
BitMap : I use the "recycle()" method
BitMapDrawable : I use the "getBitMap().recycle()" method
Drawable : I set these to null (not working)
NinePatchDrawable : I set these to null (not working)
What have you tried?
You cannot "getBitmap()" a NinePatchDrawable
You cannot convert a NinePatchDrawable to a BitMapDrawable (even if they are both Bitmap based Drawables)
There seems to be a way to parse the PNG yourself, feeding the bytes into NinePathDrawable yourself -- this might get me to a point where I can just "recycle()" the underlying BitMap myself, but that seems like I'm reinventing the wheel (http://code.google.com/p/android/issues/detail?id=13542)
My Current Rules:
Never use #drawable/ in XML
Never android:background in XML
Never android:src in XML
To be able to use: #drawable/, android:background, android:src in xml, you can always use custom classes.
So instead of
<ImageView android:src="#drawable/bg" />
you can use:
<com.myPackage.CustomImageView android:src="#drawable/bg" />
Than in the CustomImageView in the constructor you can get the references to your xml attributes:
private Drawable bg2;
private Drawable bg1;
public void CustomImageView(Context context, Attrs attrs)
{
super(context, attrs);
// Use this to get references to your xml attributes
int resourceId = attrs.getAttributeResourceValue("android", "src", 0);
bg1 = getResources().getDrawable(resourceId);
// Or for the 'background' attribute
resourceid = attrs.getAttributeResourceValue("android", "background", 0);
bg2 = getResources().getDrawable(resourceId);
// Now you can recycle() your 'bg' whenever you're done
}
This way, you can extract references from your xml. And recycle() them when you think it's appropriate
I too am frustrated by the outofmemory bug. My application was throwing an outofmemory error whenever the user went from one activity to another. Setting my drawables to null and calling System.gc() didn't work, neither did recycling my bitmapDrawables with getBitMap().recycle(). Android would continue to throw the outofmemory error with the first approach, and it would throw a canvas error message whenever it tried using a recycled bitmap with the second approach.
I took an even third approach. I set all views to null and the background to black. I do this cleanup in my onStop() method. This is the method that gets called as soon as the activity is no longer visible. The onDestroy() method might not get called. Also, if the cleanup is done in the onPause() method, users will get a black screen before moving onto the next screen.
To prevent a black screen from occurring if the user presses the back button on the device, I then reload the activity in the onRestart() method by calling the startActivity(getIntent()) and then finish() methods.
Note: it's not really necessary to change the background to black.