Drawable,从简化布局谈起

说到Drawable肯定会第一时间想到图片资源,可能有人会认为Drawable就是图片。Drawble从字面上说就是“可绘制的”,google文档的描述:“A Drawable is a general abstraction for “something that can be drawn.””,Drawable其实就是为那些”可以被绘制的’’提供的抽象类。

  • 哪些东西是”can be drawn”?
  • 绘制在哪里的?

一、Drawble是干嘛的?

看下android原生的Drawable和Canvas提供了的绘图的接口:android.graphics.drawable
android.graphics.Canvas。Canvas提供了一系列的接口去实现绘制想要的图形、图片、颜色或者其他something,Drawable就是为了这些”something”提供的。

二、如何自定义Drawable?如何简化布局?

Canvas和布局实现

相信一定见过类似这样的布局,在Android系统中如何把想要的图形绘制处理,一般有两种方式:

  • 把你想要的视图简单的定义到布局文件中,系统读取布局文件绘制视图
  • 把想要的图形通过canvas的绘图接口直接进行绘制
    在第二种方式中,你需要通过比如Drawable,比如自定义的View的onDraw()方法,或者调用Canvas的draw…()方法,而View的方式实际上也是Canvas绘制的方式,View也可以看做是对Canvas绘图接口的包装。而直接绘制到Canvas上的方式也单单是这些,在主线程上你可以通过调用View的invalidate()方法触发绘制,在onDraw()方法处理绘图的回调。另一种非主线程绘制的方式就是SurfaceView了。

通过Drawable绘制想要的图形是个不错的选择,Drawable是一个抽象类,draw()方法提供了绘制的Canvas,invalidateSelf()方法提供了重绘的可能。Drawable的抽象方法分别是:

  • **draw(Canvas canvas)**将想要的东西绘制到这个canvas对象上

  • setAlpha(int alpha) 为drawable指定一个alpha值,0代表全透明,255代表全不透明。

  • setColorFilter(ColorFilter cf) 为drawable指定一个颜色过滤器

  • getOpacity() 获取透明度

实现自定义的Drawable只需要继承Drawable.java,并且在draw方法里通过canvas绘制需要的图形就可以了,听起来很简单。无论是一组图片、一条曲线还是一个色块,所有你想要的图形都可通过canvas参数绘制上去。举个例子,如何使用Drawable简化上图所示的网格布局?假设你需要在网格布局里显示不同的图片,就可以把场景微信朋友群组的图标。当然你可以使用view布局来做,当然也可以使用canvas方式。根据Drawable添加的顺序依次确定每个图片的坐标位置,在draw()方法里根据每个图片的坐标位置和图片显示位置绘制到canvas上。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void draw(Canvas canvas) {
final int N = Math.min(mChildDrawable.size(), mRowCount * mColumnCount);
for (int i = 0; i < N; i++) {
ChildDrawable itemToDraw = mChildDrawable.get(i);
Drawable drawable = itemToDraw.mDrawable;
if (drawable != null) {
itemToDraw.mDrawable.setBounds(itemToDraw.mInsetL, itemToDraw.mInsetT, itemToDraw.mInsetR, itemToDraw.mInsetB);
drawable.draw(canvas);
}
}
}

三、让Drawable动起来怎么样?

  • 首先看下Drawable的Canvas从哪里来

一般使用ImageView显示drawable,看下ImageView的onDraw(Canvas canvas),可以看到Drawable在ImageView里的绘制发生在View的onDraw回调里,根据scaleType和padding确定绘制的方式和位置。

1
2
3
4
5
6
7
8
9
10
11
12
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
....

// mDrawMatrix根据ImageView的ScaleType来确定的
if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
mDrawable.draw(canvas);
} else {
......
mDrawable.draw(canvas);
}
}

View绘制Background Drawable在draw(Canvas canvas)方法,并在setBackgroundDrawable时会执行background.setCallback(this)操作。

1
2
3
4
5
6
7
8
9
10
11
// Step 1, draw the background, if needed
if (background != null) {
......
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
......
background.draw(canvas);
......
}
}
  • 再看Drawable如何重绘
    Drawable是依赖Canvas绘制的,查看Drawable源码发现调用invalidateSelf()方法需要获取Callback,在Callback是空的情况下无法重绘Drawable。那么在View里如何实现Drawable的重绘,显然需要先设置Drawable的重绘回调,View.java实现了Drawable.Callback接口,在View里调用setImageDrawable()方法里会首先设置当前View为Drawable的回调。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
    callback.invalidateDrawable(this);
    }
    }

    public static interface Callback {
    public void invalidateDrawable(Drawable who);
    public void scheduleDrawable(Drawable who, Runnable what, long when);
    public void unscheduleDrawable(Drawable who, Runnable what);
    }
    ImageView调用invalidate触发重绘,回调ImageView的onDraw()方法,完成Drawable的重绘。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Override
    public void invalidateDrawable(Drawable dr) {
    if (dr == mDrawable) {
    /* we invalidate the whole view in this case because it's very
    * hard to know where the drawable actually is. This is made
    * complicated because of the offsets and transformations that
    * can be applied. In theory we could get the drawable's bounds
    * and run them through the transformation and offsets, but this
    * is probably not worth the effort.
    */
    invalidate();
    } else {
    super.invalidateDrawable(dr);
    }
    }
    了解了Drawable绘制的画布和重绘的方法,如何实现动画的Drawable只需要按时重绘Drawable就可以了,用动画控制重绘效果。具体实现可以看https://github.com/Ivonhoe/FancyDrawable,现在看如何实现我在贝赛尔插值器里说过的BallsLine进度条效果,只需要在每次重绘的回调里动态的控制每个小球点的位置就可以了,需要的动画插值器参数可以参考Web代码

动态Drawable和GridDrawable

protected static final long FRAME_DURATION = 1000 / 60;

protected Runnable mUpdater = new Runnable() {
    @Override
    public void run() {
        onFrame();

        scheduleSelf(mUpdater, SystemClock.uptimeMillis() + FRAME_DURATION);
        invalidateSelf();
    }
};

@Override
public void start() {
    if (!isRunning()) {
        mIsRunning = true;

        // start
        onStart();
        scheduleSelf(mUpdater, SystemClock.uptimeMillis() + FRAME_DURATION);
        invalidateSelf();
    }
}

@Override
public void stop() {
    if (isRunning()) {
        mIsRunning = false;
        unscheduleSelf(mUpdater);

        //stop
        onStop();
    }
}

五、参考文档

http://developer.android.com/reference/android/graphics/drawable/Drawable.html

http://developer.android.com/guide/topics/graphics/2d-graphics.html

http://wiresareobsolete.com/2012/12/textdrawable-draw-some-text/

http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/

http://antoine-merle.com/blog/2013/11/12/make-your-progressbar-more-smooth/

六、Github

https://github.com/Ivonhoe/FancyDrawable

转载请标明出处病已blog https://ivonhoe.github.io/