Drawable

来源:互联网 发布:棋院围棋软件 编辑:程序博客网 时间:2024/05/20 20:21
<View    android:id="@+id/view"    android:layout_width="200dp"    android:layout_height="200dp"    android:background="@android:color/holo_red_light" />

屏幕上会出现一个红色的正方形。

Drawable bg = findViewById(R.id.view).getBackground();
断点调试一下


bg is a ColorDrawable。


ColorDrawable 有点复杂,写一个简化的版本。

public class SimpleColorDrawable extends Drawable {    private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);    public SimpleColorDrawable(int color) {        mPaint.setColor(color);    }        @Override    public void draw(@NonNull Canvas canvas) {        canvas.drawRect(getBounds(), mPaint);    }    @Override    public void setAlpha(int alpha) {    }    @Override    public void setColorFilter(ColorFilter colorFilter) {    }    @Override    public int getOpacity() {        return PixelFormat.TRANSLUCENT;    }}

SimpleColorDrawable 也能正常工作。

 Drawable newBg = new SimpleColorDrawable(Color.BLUE); findViewById(R.id.view).setBackground(newBg);
这样就能看到蓝色的正方形了。


绘制 View 的过程中会绘制背景。

view.draw(canvas) -> view.drawBackground -> background.draw(canvas)

Drawable 不接受任何事件。

Drawable 有几个重要的方法。

setBounds 

设置绘制的区域,比如 SimpleColorDrawable 中的:

canvas.drawRect(getBounds(), mPaint);

(如果 bounds 大于 View 的区域会怎么样?不会绘制。)

setState 

Drawable 无法接受事件,但仍需要和用户交互,比如常见的按钮按下变色。

View 就成了用户和 Drawable 之间的媒介,具体过程等到 StateListDrawable 再分析。
setLevel

这个方法和 setState 很像,一个是留给用户用的交互入口,一个是留给我们写代码用的。

setCallback

稍后再说。


DrawableContainer

A helper class that contains several {@link Drawable}s and selects which one to use

    @Override    public void draw(Canvas canvas) {        if (mCurrDrawable != null) {            mCurrDrawable.draw(canvas);        }        if (mLastDrawable != null) {            mLastDrawable.draw(canvas);        }    }
它会引用多个 Drawable,最后绘制时会选择其中一个。


StateListDrawable 是 DrawableContainer 的子类。

设计意图:当 StateListDrawable 的 State 改变时,切换不同 Drawable 显示。

关键代码:

 public void addState(int[] stateSet, Drawable drawable) {        if (drawable != null) {            mStateListState.addStateSet(stateSet, drawable);            // in case the new state matches our current state...            onStateChange(getState());        }    }    @Override    public boolean isStateful() {        return true;    }    @Override    protected boolean onStateChange(int[] stateSet) {        final boolean changed = super.onStateChange(stateSet);        int idx = mStateListState.indexOfStateSet(stateSet);        if (DEBUG) android.util.Log.i(TAG, "onStateChange " + this + " states "                + Arrays.toString(stateSet) + " found " + idx);        if (idx < 0) {            idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);        }        return selectDrawable(idx) || changed;    }
当 View 的状态改变时会调用 drawableStateChanged

  protected void drawableStateChanged() {        final int[] state = getDrawableState();        boolean changed = false;        final Drawable bg = mBackground;        if (bg != null && bg.isStateful()) {            changed |= bg.setState(state);        }...}
所以 isStateful 方法需要重载。
setState 会调用 onStateChange,比较保存的 stateSet ,切换到合适的 drawable 后重画。


怎么切换 Drawable 的

 public boolean selectDrawable(int index) {        invalidateSelf();        return true;    }

 public void invalidateSelf() {        final Callback callback = getCallback();        if (callback != null) {            callback.invalidateDrawable(this);        }    }
View 在设置背景时,会执行如下代码:

background.setCallback(this);
而 view.invalidateDrawable

    @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();        }    }
会直接重画自身,背景部分也自然会被重画了。

有时 Callback 也会不是 View ,稍后分析。


LevelListDrawable 也是 DrawableContainer 的子类,但这个类设计得有些费解,跳过了。


AnimationDrawable 也是 DrawableContainer 的子类。

适合实现那种转圈圈的效果,需要 12 张图片的那种。

不同于 StateListDrawable,它不需要和用户交互,预留了 start 和 stop 两个方法。

数据结构是,一个 duration 对应一个 drawable (StateListDrawable 是一个 StateSet 对应一个 Drawable)。

start 时会 setFrame 为第一帧。

   private void setFrame(int frame, boolean unschedule, boolean animate) {        selectDrawable(frame);            scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);    }
  public void scheduleSelf(@NonNull Runnable what, long when) {        final Callback callback = getCallback();        if (callback != null) {            callback.scheduleDrawable(this, what, when);        }    }
如果 callback 是 view

   public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {        if (verifyDrawable(who) && what != null) {            final long delay = when - SystemClock.uptimeMillis();            if (mAttachInfo != null) {                mAttachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(                        Choreographer.CALLBACK_ANIMATION, what, who,                        Choreographer.subtractFrameDelay(delay));            } else {                // Postpone the runnable until we know                // on which thread it needs to run.                getRunQueue().postDelayed(what, delay);            }        }    }
往队列里加一个 Runnable, duration 秒后执行。

这个 Runnable 就是显示下一帧。

 @Override    public void run() {        nextFrame(false);    }
这样就实现按 duration 不断切换 drawable 的效果了。


Drawable 的另一个子类,DrawableWrapper。

Drawable container with only one child element.

@Overridepublic void draw(@NonNull Canvas canvas) {    if (mDrawable != null) {        mDrawable.draw(canvas);    }}
和 DrawableContainer 一样,也不负责直接绘制。


ClipDrawable 是 DrawableWrapper 的子类。

设计意图:对绘制的 drawable 裁剪,只绘制一部分,常用于实现进度条。

  @Override    public void draw(Canvas canvas) {        final Drawable dr = getDrawable();        if (dr.getLevel() == 0) {            return;        }        final Rect r = mTmpRect;        final Rect bounds = getBounds();        final int level = getLevel();        int w = bounds.width();        final int iw = 0; //mState.mDrawable.getIntrinsicWidth();        if ((mState.mOrientation & HORIZONTAL) != 0) {            w -= (w - iw) * (MAX_LEVEL - level) / MAX_LEVEL;        }        int h = bounds.height();        final int ih = 0; //mState.mDrawable.getIntrinsicHeight();        if ((mState.mOrientation & VERTICAL) != 0) {            h -= (h - ih) * (MAX_LEVEL - level) / MAX_LEVEL;        }        final int layoutDirection = getLayoutDirection();        Gravity.apply(mState.mGravity, w, h, bounds, r, layoutDirection);        if (w > 0 && h > 0) {            canvas.save();            canvas.clipRect(r);            dr.draw(canvas);            canvas.restore();        }    }
ClipDrawable 有两个参数,orientation 决定垂直还是水平, gravity 决定左到右还是右到左(水平情况下)。
clipRect 也个 native 方法,没啥好研究的。


InsetDrawable是 DrawableWrapper 的子类。

它没重载 draw 方法,而是

@Override    protected void onBoundsChange(Rect bounds) {        final Rect r = mTmpRect;        r.set(bounds);        r.left += mState.mInsetLeft;        r.top += mState.mInsetTop;        r.right -= mState.mInsetRight;        r.bottom -= mState.mInsetBottom;        // Apply inset bounds to the wrapped drawable.        super.onBoundsChange(r);    }
就是压缩了所要绘制的 drawable 的 bounds,感觉没什么地方用的到。


剩下两个子类 RotateDrawable 和 ScaleDrawable 也大同小异。

当 callback 不是 view 
Drawable 比较神奇的地方就是可以一层套一层,因为都是 Drawable 嘛(递归思想还是常用的。)

ColorDrawable 套一个 ClipDrawable 显示 90%,再套一个 RotateDrawable 顺时针旋转 90 度。
那么问题来了,当你改变最内部 ColorDrawable 颜色时(虽然没这个方法。。),外部是如何知道它需要重画呢。

DrawableWrapper 是实现 Callback 接口的,

@Override    public void invalidateDrawable(@NonNull Drawable who) {        final Callback callback = getCallback();        if (callback != null) {            callback.invalidateDrawable(this);        }    } 
并且 setDrawable 时已经把内部 drawable 的 callback 设置为 DrawableWrapper

  public void setDrawable(@Nullable Drawable dr) {        if (mDrawable != null) {            mDrawable.setCallback(null);        }        mDrawable = dr;        if (dr != null) {            dr.setCallback(this);}...}
所以当 ColorDrawable invidateSelf 时,
   public void invalidateSelf() {        final Callback callback = getCallback();        if (callback != null) {            callback.invalidateDrawable(this);        }    }
colorDrawable.invalidateSelf -> clipDrawable.invalidateDrawble -> rotateDrawable.invalidateDrawable -> view.invalidate 

会完整的向上冒泡,直到 View 重画,然后在往下走一遍。(这和事件分发,还有 js 事件冒泡很像)

LayerDrawable 是 Drawable 的另一个子类。
A Drawable that manages an array of other Drawables. These are drawn in array
 order, so the element with the largest index will be drawn on top.
LayerDrawable 也引用多个 drawable,但它全部都绘制。

   @Override    public void draw(Canvas canvas) {        final ChildDrawable[] array = mLayerState.mChildren;        final int N = mLayerState.mNum;        for (int i = 0; i < N; i++) {            final Drawable dr = array[i].mDrawable;            if (dr != null) {                dr.draw(canvas);            }        }    }

TransitionDrawable 是 LayerDrawable 的子类

     Drawable d;        d = array[0].mDrawable;        if (crossFade) {            d.setAlpha(255 - alpha);        }        d.draw(canvas);        if (crossFade) {            d.setAlpha(0xFF);        }        if (alpha > 0) {            d = array[1].mDrawable;            d.setAlpha(alpha);            d.draw(canvas);            d.setAlpha(0xFF);        }
关键代码如上,就是根据根据时间流逝(0,duration)->(0,255),不断改变 alpha。

然后设置两个内部 drawable 的透明度 alpha 和 255-alpha,一起绘制。

关键是内部 drawable 要实现了 setAlpha 方法。


ShapeDrawable

把绘制工作交给了 Shape 类。

protected void onDraw(Shape shape, Canvas canvas, Paint paint) {        shape.draw(canvas, paint);    }


简单的 Shape

RectShape
        canvas.drawRect(mRect, paint);
ArcShape
        canvas.drawArc(rect(), mStart, mSweep, true, paint);
OvalShape
        canvas.drawOval(rect(), paint);

复杂的 Shape

RoundRectShape

 protected void onResize(float w, float h) {        super.onResize(w, h);        RectF r = rect();        mPath.reset();        if (mOuterRadii != null) {            mPath.addRoundRect(r, mOuterRadii, Path.Direction.CW);        } else {            mPath.addRect(r, Path.Direction.CW);        }        if (mInnerRect != null) {            mInnerRect.set(r.left + mInset.left, r.top + mInset.top,                           r.right - mInset.right, r.bottom - mInset.bottom);            if (mInnerRect.width() < w && mInnerRect.height() < h) {                if (mInnerRadii != null) {                    mPath.addRoundRect(mInnerRect, mInnerRadii, Path.Direction.CCW);                } else {                    mPath.addRect(mInnerRect, Path.Direction.CCW);                }            }        }    }
可以画出类似空心环的东西,用到 Path,注意下 CW 和 CCW 顺逆时针,以后应该会专门学学 Path。






0 0
原创粉丝点击