关于Drawable的思考

432 阅读3分钟

本文已参与[新人创作礼]活动,一起开启掘金创作之路。

首先要搞清楚官方为什么要设计出Drawable,而且还允许我们进行自定义。

这个问题可以通过阅读View源码找到答案。View都有一个drawable属性。在draw(canvas)方法里面我们可以看到调用了drawable.draw(canvas)方法,可以看到将画布对象传了进去。谷哥官方也就是通过这种方式将绘制背景的操作单独提取了出来。

class View{
    public void draw(Canvas canvas) {
        drawBackground(canvas);
    }
    private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) 
            return;
        setBackgroundBounds();
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }
}

所以单独设计一个Drawable的目的在我看来是为了让代码不杂揉在view里面。比如你要给view设置一个背景,改变view的形状。有了画布canvas我们就能干任何事情,drawable就是帮助我们在展示我们想要展示的东西之前帮我们做一些不是那么主要的事情。


上面讲了我对drawable的理解,现在就要实际使用一下他了。

在使用前有个我自认为很重要的知识点需要讲下,为什么在自定义Drawable里面调用invalidateSelf()方法没有效果出现?答:因为你的自定义Drawable没有继承Drawable.Callback接口,然后实现方法invalidateDrawable(Drawble who)。我们来看下View类。可以看到他继承了Drawable.Callback,然后实现了invalidateDrawable方法,在里面调用了invalidate()实现了页面刷新。

public class View implements Drawable.Callback{
    @Override
    public void invalidateDrawable(@NonNull Drawable drawable) {
        if (verifyDrawable(drawable)) {
            final Rect dirty = drawable.getDirtyBounds();
            final int scrollX = mScrollX;
            final int scrollY = mScrollY;

            invalidate(dirty.left + scrollX, dirty.top + scrollY,
                    dirty.right + scrollX, dirty.bottom + scrollY);
            rebuildOutline();
        }
    }
}

然后我们在View的setBackgroundDrawable方法找到他们给Drawable设置了回调:

    void setBackgroundDrawable(Drawable background){
        background.setCallback(this);
    }

所以我们在写自定义Drawable的时候一定要继承Drawable.Callback接口,实现invalidateDrawable方法。套路如下:不然回调不到上层view方法从而调用不到invalidate方法。

@Override
public void invalidateDrawable(Drawable who) {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}

关于图片的具体大小我们可以重写getIntrinsicWidth()和getIntrinsicHeight来设置图片大小值。默认是-1。

现在实现一个点击图片,图片渐变的效果。

绘制思路:对画布上面进行裁切,先绘制正常的图片,然后对画布进行下面裁切,绘制有蒙板的颜色。蒙板的颜色也就是设置colorFilter属性。

override fun draw(canvas: Canvas) {
    if (mDrawable == null) return
    var rect = bounds
    var curOffset = mPercent * rect.height()
    canvas.save()
    canvas.clipRect(rect.left.toFloat(),rect.top.toFloat(),rect.width().toFloat(),rect.height() - curOffset)
    mDrawable.colorFilter = mDefaultColorFilter
    mDrawable.draw(canvas)
    canvas.restore()
    canvas.save()
    canvas.clipRect(rect.left.toFloat(), rect.height() - curOffset, rect.width().toFloat(), rect.height().toFloat());
    mDrawable.colorFilter= mPercentColorFilter
    mDrawable.draw(canvas)
    canvas.restore()
}

总知关于drawable的自定义还需要多练习,我们现在知道了他其实就是帮我们在自定义view的时候把一些操作从view里面提取出来,防止view过于臃肿。基于这个思想你就可以自行改造了。

学习过程遇到的一个好的博客推荐


中途添加的知识点:

调用方法:view.setBackgroundResource(id)可能会触发requestlayout方法。在不知不觉的时候造成了性能浪费。
源码解析:

public void setBackgroundResource(@DrawableRes int resid) {
    Drawable d = null;
    if (resid != 0) {
        d = mContext.getDrawable(resid);
    }
    setBackground(d);
}
public void setBackground(Drawable background) {
    //noinspection deprecation
    setBackgroundDrawable(background);
}
 
public void setBackgroundDrawable(Drawable background) {
    if (background == mBackground) {
        return;
    }
    boolean requestLayout = false;
    if (background != null) {
            if (mBackground == null
                || mBackground.getMinimumHeight() != background.getMinimumHeight()
                || mBackground.getMinimumWidth() != background.getMinimumWidth()) {
            requestLayout = true;
        }
        mBackground = background;
        background.setCallback(this);
        if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
            mPrivateFlags &= ~PFLAG_SKIP_DRAW;
            requestLayout = true;
        }
    } else {
        mBackground = null
        requestLayout = true;
    }

    if (requestLayout) {
        requestLayout();
    }
}
 

可以看到当我们设置的新图片和老图片的大小不相等的时候就会认为需要重新布局,所以解决办法就是我们可以手动设置图片的大小相等用来避免这个不必要出现的资源浪费。


新学到的知识:制作涟漪效果可以使用RippleDrawable.

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="#FF0000" >

    <item android:id="@android:id/mask"
         android:drawable="@android:color/white" />

</ripple>