-
Notifications
You must be signed in to change notification settings - Fork 55
Drawables
Drawables encapsulate drawing into reusable components with support for state lists, tint, animation, and measuring. Drawables can be inflated from XML and interact with views a little bit.
class MyDrawable : Drawable() {
private val paint = Paint()
override fun draw(canvas: Canvas) {
// do the drawing here
}
override fun setAlpha(alpha: Int) {
paint.alpha = alpha
}
override fun getOpacity() = PixelFormat.TRANSLUCENT
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
}
}
To make the most simple custom drawable you need to extend the Drawable class and implement four methods. Two of them are interesting:
- The
getOpacity()
method should return the opacity required by this drawable to be supported by the drawing pipeline. For example, translucent drawables need to be blended with what's behind them (this operation is called alpha blending). Returning the right drawable's opacity mode helps Android to optimize drawing on older devices. This method is now deprecated and you can return any valid value. - The
draw(canvas)
method looks and works like theView.draw(canvas)
except that the drawn entity is much simpler. Implement the drawing here.
All drawables loaded from resources share the same state. Applying a change to one drawable will affect all instances with the shared state. This is an optimization used to reduce the memory needed to hold larger drawable data, like bitmaps. The shared state is held in constant state objects.
To make a drawable mutable you need to call the mutate()
method. To make the drawable support shared state you need to move the shared state to a constant state class, implement a constructor using that state, and the mutate()
method:
class SharedStateDrawable : Drawable {
private var mutated = false
private var state: SharedStateDrawableState
constructor(state: SharedStateDrawableState) : super() {
this.state = state
}
override fun getConstantState() = state
override fun mutate(): Drawable {
if (!mutated) {
state = SharedStateDrawableState(state)
mutated = true
}
return this
}
class SharedStateDrawableState(state: SharedStateDrawableState?) : ConstantState() {
override fun newDrawable() = SharedStateDrawable(this)
}
}
The constant state can hold any information the drawable wants to share. Every time the local state of this drawable is changed, you need to call mutate()
to make it mutable and use that mutable instance.
To actually use a shared state you also need to implement the process of drawable loading. That depends on the way you load your drawables and is pretty easy. When you load a drawable, cache its state. On the next time this drawable is requested, call state.newDrawable()
instead of loading it again.
If a view draws a drawable in a custom way (like CheckBox draws the check graphic), it needs to own that drawable and it needs to be the only owner. With a correctly set owner the drawable can refresh itself on state change and on animation frame. To connect a view and a drawable correctly you have to support the Drawable.Callback
interface. It's already implemented in the Drawable class and will be used if you call certain methods (for example invalidateSelf()
). For the custom view class, you need to implement a couple of things:
class CustomView extends View {
private Drawable drawable;
public void setDrawable(Drawable d) {
if (drawable != d) {
if (drawable != null) {
drawable.setCallback(null);
unscheduleDrawable(drawable);
}
drawable = d;
if (d != null) {
drawable = DrawableCompat.wrap(d);
d.setCallback(this);
d.setLayoutDirection(getLayoutDirection());
if (d.isStateful())
d.setState(getDrawableState());
d.setTint(getTint());
}
}
}
@Override
protected boolean verifyDrawable(@NonNull Drawable who) {
return super.verifyDrawable(who) || who == drawable;
}
}
The owner will validate that the view that called invalidate belongs to that view.
When setting a drawable, you can also set other things like its visibility or size. You also need to update drawable's state when the owner view changes. That applies to other things like tint or bounds too. With all of that you're ready to draw:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (drawable != null) {
final int scrollX = getScrollX();
final int scrollY = getScrollY();
if (scrollX == 0 && scrollY == 0) {
drawable.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
drawable.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
To make a custom drawable support states you have to implement these methods:
public boolean isStateful() = true
protected boolean onStateChange(int[] state) {
// did the drawable change?
}
States are usually set by the view that owns this drawable. On view state change the view checks, if the drawable is stateful, then calls drawable.setState(state)
and invalidates itself if needed. For example, for a checkable drawable, you need to add the following code to the owner view.
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (drawable != null && drawable.isStateful() && drawable.setState(getDrawableState()))
invalidateDrawable(drawable);
}
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (drawable != null) drawable.jumpToCurrentState();
}
public void setChecked(boolean state) {
if (this.checkedState != state) {
checkedState = state;
refreshDrawableState();
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
}
}