Android 开发之 Drawable系列

本文详细探讨了Android开发中Drawable资源的多种类型及其应用,包括BitmapDrawable、NinePatchDrawable、LayerDrawable等,并阐述了如何通过配置属性如抗锯齿、过滤、颜色过滤、拉伸等优化Drawable的表现效果,以及如何利用Drawable实现动态背景、动画和交互效果。此外,文章还介绍了如何通过工厂模式简化Drawable资源的创建过程,旨在帮助开发者更高效地管理和使用Drawable资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Android开发中遇到很多地方需要drawable资源,而drawable的种类繁多,比如shape绘制纯色图片,.9.png图片适应拉伸。当我们充分了解drawable的子类后,就会发现,原来可以如此简单。

  • BitmapDrawable
  • NinePathDrawable
  • LayerDrawable
  • StateListDrawable
  • LevelDrawable
  • TransitionDrawable
  • Inset Drawable
  • ClipDrawable
  • ScaleDrawable
  • ShapeDrawable ….

BitmapDrawable

这里写图片描述

android:antialias:抗锯齿,canvas 绘图对位图进行了选择,会出现锯齿,通常解决办法是通过Paint mPaint=new Paint();调用 mPaint.setAntiAlias()方法。android:filter用于对位图进行过滤波处理。android:dither:图像的抖动处理,当每个颜色值以低于8位表示时,对应图像做抖动处理可以实现在可显示颜色总数比较低时还保持较好的显示效果.android:mipMap hasMipMap()这里的提一下drawable和mipMap的区别,android studio开发工具里面:原生图片放在mipmap 里面, drawable主要放一些放一些自定义形状和按钮变色之类的xml。google强烈建议使用mipmap装图片。把图片放到mipmaps可以提高系统渲染图片的速度,提高图片质量,减少GPU压力。android:tileMode平铺,xml实例如下:

<bitmap   
    xmlns:android="http://schemas.android.com/apk/res/android"   
    android:src="@drawable/test"   
    android:tileMode="repeat"   
    android:dither="true" /> 

代码调用如下:

Bitmap mBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher);  
BitmapDrawable mBitmapDrawable = new BitmapDrawable(mBitmap);  
mBitmapDrawable.setTileModeXY(TileMode.REPEAT , TileMode.REPEAT );  
mBitmapDrawable.setDither(true);  
mLayout.setBackgroundDrawable(mBitmapDrawable);

这里的TileMode类源自Shader类里面的枚举类型,源代码如下:

 public enum TileMode {
        /**
         * replicate the edge color if the shader draws outside of its
         * original bounds
         */
        CLAMP   (0),
        /**
         * repeat the shader's image horizontally and vertically
         */
        REPEAT  (1),
        /**
         * repeat the shader's image horizontally and vertically, alternating
         * mirror images so that adjacent images always seam
         */
        MIRROR  (2);

        TileMode(int nativeInt) {
            this.nativeInt = nativeInt;
        }
        final int nativeInt;
    }

BitmapDrawable构造函数

android.graphics.drawable
public class BitmapDrawable extends Drawable{

    // public static final int DENSITY_DEFAULT = DENSITY_MEDIUM=160;
    private int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;

    public BitmapDrawable() {
        mBitmapState = new BitmapState((Bitmap) null);
    }
    public BitmapDrawable(Resources res) {
        mBitmapState = new BitmapState((Bitmap) null);
        mBitmapState.mTargetDensity = mTargetDensity;
    }
    public BitmapDrawable(Bitmap bitmap) {
        this(new BitmapState(bitmap), null);
    }
    public BitmapDrawable(Resources res, Bitmap bitmap) {
        this(new BitmapState(bitmap), res);
        mBitmapState.mTargetDensity = mTargetDensity;
    }
    public BitmapDrawable(String filepath) {
        this(new BitmapState(BitmapFactory.decodeFile(filepath)), null);
        if (mBitmapState.mBitmap == null) {
            android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + filepath);
        }
    }
    public BitmapDrawable(Resources res, String filepath) {
        this(new BitmapState(BitmapFactory.decodeFile(filepath)), null);
        mBitmapState.mTargetDensity = mTargetDensity;
        if (mBitmapState.mBitmap == null) {
            android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + filepath);
        }
    }
    public BitmapDrawable(java.io.InputStream is) {
        this(new BitmapState(BitmapFactory.decodeStream(is)), null);
        if (mBitmapState.mBitmap == null) {
            android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + is);
        }
    }
    public BitmapDrawable(Resources res, java.io.InputStream is) {
        this(new BitmapState(BitmapFactory.decodeStream(is)), null);
        mBitmapState.mTargetDensity = mTargetDensity;
        if (mBitmapState.mBitmap == null) {
            android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + is);
        }
    }
    /**
     * The one constructor to rule them all. This is called by all public
     * constructors to set the state and initialize local properties.
     */
    private BitmapDrawable(BitmapState state, Resources res) {
        mBitmapState = state;

        initializeWithState(mBitmapState, res);
    }

}

这里都调用了一个BitmapState的类,而BitmapState extends Drawable.ConstantState。重写其抽象方法

 public static abstract class ConstantState {
        /**
         * Create a new drawable without supplying resources the caller
         * is running in.  Note that using this means the density-dependent
         * drawables (like bitmaps) will not be able to update their target
         * density correctly. One should use {@link #newDrawable(Resources)}
         * instead to provide a resource.
         */
        public abstract Drawable newDrawable();

        /**
         * Create a new Drawable instance from its constant state.  This
         * must be implemented for drawables that change based on the target
         * density of their caller (that is depending on whether it is
         * in compatibility mode).
         */
        public Drawable newDrawable(Resources res) {
            return newDrawable();
        }

        /**
         * Create a new Drawable instance from its constant state. This must be
         * implemented for drawables that can have a theme applied.
         */
        public Drawable newDrawable(Resources res, Theme theme) {
            return newDrawable(null);
        }

        /**
         * Return a bit mask of configuration changes that will impact
         * this drawable (and thus require completely reloading it).
         */
        public abstract int getChangingConfigurations();

        /**
         * @return Total pixel count
         * @hide
         */
        public int addAtlasableBitmaps(Collection<Bitmap> atlasList) {
            return 0;
        }

        /** @hide */
        protected final boolean isAtlasable(Bitmap bitmap) {
            return bitmap != null && bitmap.getConfig() == Bitmap.Config.ARGB_8888;
        }

        /**
         * Return whether this constant state can have a theme applied.
         */
        public boolean canApplyTheme() {
            return false;
        }
    }

得到BitmapDrawable instance后,可动态设置attr, BitmapDrawable的在new的时候,重写Drawable的私有空方法,重写测量后设置Paint的shader

    /**
     * Override this in your subclass to change appearance if you vary based on
     * the bounds.
     */
    protected void onBoundsChange(Rect bounds) {}


    @Override
    protected void onBoundsChange(Rect bounds) {
        mDstRectAndInsetsDirty = true;

        final Shader shader = mBitmapState.mPaint.getShader();
        if (shader != null) {
            if (needMirroring()) {
                updateMirrorMatrix(bounds.right - bounds.left);
                shader.setLocalMatrix(mMirrorMatrix);
                mBitmapState.mPaint.setShader(shader);
            } else {
                if (mMirrorMatrix != null) {
                    mMirrorMatrix = null;
                    shader.setLocalMatrix(Matrix.IDENTITY_MATRIX);
                    mBitmapState.mPaint.setShader(shader);
                }
            }
        }
    }

getIntrinsicHeight()和getIntrinsicWidth()方法获取BitmapDrawable的宽高,即按照比例缩放后实际大小值。同时需要注意BitmapDrawable(Bitmap mBitmap)已经被弃用,如果延用该方法可能会出现问题mTargetDensity默认的是160,当mBitmap的density>160的时候,bitmap.getWidth() != bitmapDrawable.getIntrinsicWidth().解决办法是传入Resources或者调用setTargetDensity()方法,这样就没问题了。public 构造方法都调用自身的private构造方法,该方法内调用initializeWithState(mBitmapState, res)对mTargetDensity重新赋值,方法如下:

    /**
     * Initializes local dynamic properties from state. This should be called
     * after significant state changes, e.g. from the One True Constructor and
     * after inflating or applying a theme.
     */
    private void initializeWithState(BitmapState state, Resources res) {
        if (res != null) {
            mTargetDensity = res.getDisplayMetrics().densityDpi;
        } else {
            mTargetDensity = state.mTargetDensity;
        }

        mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
        computeBitmapSize();
    }

下面是一个代码调用实例

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        image=(ImageView)findViewById(R.id.image);
        image.setBackground(getBitmapDrawable());
    }

    private Drawable getBitmapDrawable() {

        BitmapDrawable mBitmapDrawable=new BitmapDrawable(getResources(),BitmapFactory.decodeResource(getResources(), R.drawable.anglebaby));

        mBitmapDrawable.setAlpha(1000);
        mBitmapDrawable.setAntiAlias(true);
        mBitmapDrawable.setDither(true);
        mBitmapDrawable.setFilterBitmap(true);
        mBitmapDrawable.setTileModeXY(TileMode.REPEAT,TileMode.REPEAT);
        mBitmapDrawable.setColorFilter(Color.parseColor("#40FF0000"), Mode.SRC_ATOP);

        return mBitmapDrawable;

    }

NinePatchDrawable该类与BitmapDrawable大同小异,NinePatchDrawable构造方法要传入NinePath和byte[],对指定区域进行屏幕的左右上下拉伸,也可以直接用sdk自带的工具制作.9图片。下面是代码调用实例:

private Drawable getNinePatchDrawable() {
        Bitmap mBitmap=BitmapFactory.decodeResource(getResources(), R.drawable.anglebabye);
        NinePatchDrawable mNinePatchDrawable=new NinePatchDrawable(getResources(), new NinePatch(mBitmap, mBitmap.getNinePatchChunk()));

        mNinePatchDrawable.setAlpha(1000);
        mNinePatchDrawable.setDither(true);
        mNinePatchDrawable.setFilterBitmap(true);
        mNinePatchDrawable.setTintMode(Mode.SRC_ATOP);//API 21
        mNinePatchDrawable.setColorFilter(Color.parseColor("#40FF0000"), Mode.SRC_ATOP);

        return mNinePatchDrawable;
    }

    /**
     * Private constructor that must received an already allocated native bitmap
     * int (pointer).
     */
    @SuppressWarnings({"UnusedDeclaration"}) // called from JNI
    Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
            boolean isMutable, boolean requestPremultiplied,
            byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
        if (nativeBitmap == 0) {
            throw new RuntimeException("internal error: native bitmap is 0");
        }

        mWidth = width;
        mHeight = height;
        mIsMutable = isMutable;
        mRequestPremultiplied = requestPremultiplied;
        mBuffer = buffer;

        // we delete this in our finalizer
        mNativeBitmap = nativeBitmap;

        mNinePatchChunk = ninePatchChunk;
        mNinePatchInsets = ninePatchInsets;
        if (density >= 0) {
            mDensity = density;
        }

        int nativeAllocationByteCount = buffer == null ? getByteCount() : 0;
        mFinalizer = new BitmapFinalizer(nativeBitmap, nativeAllocationByteCount);
    }
       ...mBitmap.getNinePatchChunk()>mNinePatchChunk对应这块的传值暂不涉及...

LayerDrawable

官方xml实例如下:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
      <bitmap android:src="@drawable/android_red"
        android:gravity="center" />
    </item>
    <item android:top="10dp" android:left="10dp">
      <bitmap android:src="@drawable/android_green"
        android:gravity="center" />
    </item>
    <item android:top="20dp" android:left="20dp">
      <bitmap android:src="@drawable/android_blue"
        android:gravity="center" />
    </item>
</layer-list>

<ImageView
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    android:src="@drawable/layers" />

LayerDrawable类继承Drawable 实现Drawable的Callback方法invalidateDrawable、 scheduleDrawable、unscheduleDrawable;先了解LayerDrawable的构造方法:

 public LayerDrawable(Drawable[] layers) {
        this(layers, null);
    }
 LayerDrawable(Drawable[] layers, LayerState state) {
        this(state, null);

        final int length = layers.length;
        final ChildDrawable[] r = new ChildDrawable[length];
        for (int i = 0; i < length; i++) {
            r[i] = new ChildDrawable();
            r[i].mDrawable = layers[i];
            layers[i].setCallback(this);
            mLayerState.mChildrenChangingConfigurations |= layers[i].getChangingConfigurations();
        }
        mLayerState.mNum = length;
        mLayerState.mChildren = r;

        ensurePadding();
    }
  LayerDrawable() {
        this((LayerState) null, null);
    }

  LayerDrawable(LayerState state, Resources res) {
        mLayerState = createConstantState(state, res);
        if (mLayerState.mNum > 0) {
            ensurePadding();
        }
    }

LayerState的Configuration重新赋值后,调用ensurePadding计算四个方向的padding值。inflate方法里面通过TypeArray获取自定义属性重新赋值

 /**
     * Initializes the constant state from the values in the typed array.
     */
    private void updateStateFromTypedArray(TypedArray a) {
        final LayerState state = mLayerState;

        // Account for any configuration changes.
        state.mChangingConfigurations |= a.getChangingConfigurations();

        // Extract the theme attributes, if any.
        state.mThemeAttrs = a.extractThemeAttrs();

        mOpacityOverride = a.getInt(R.styleable.LayerDrawable_opacity, mOpacityOverride);

        state.mAutoMirrored = a.getBoolean(R.styleable.LayerDrawable_autoMirrored,
                state.mAutoMirrored);
        state.mPaddingMode = a.getInteger(R.styleable.LayerDrawable_paddingMode,
                state.mPaddingMode);
    }

再调用inflateLayers方法,XmlPullParser解析后调用 updateLayerFromTypedArray(layer, a),获取xml的属性值,方法如下:

 private void updateLayerFromTypedArray(ChildDrawable layer, TypedArray a) {
        final LayerState state = mLayerState;

        // Account for any configuration changes.
        state.mChildrenChangingConfigurations |= a.getChangingConfigurations();

        // Extract the theme attributes, if any.
        layer.mThemeAttrs = a.extractThemeAttrs();

        layer.mInsetL = a.getDimensionPixelOffset(
                R.styleable.LayerDrawableItem_left, layer.mInsetL);
        layer.mInsetT = a.getDimensionPixelOffset(
                R.styleable.LayerDrawableItem_top, layer.mInsetT);
        layer.mInsetR = a.getDimensionPixelOffset(
                R.styleable.LayerDrawableItem_right, layer.mInsetR);
        layer.mInsetB = a.getDimensionPixelOffset(
                R.styleable.LayerDrawableItem_bottom, layer.mInsetB);
        layer.mId = a.getResourceId(R.styleable.LayerDrawableItem_id, layer.mId);

        final Drawable dr = a.getDrawable(R.styleable.LayerDrawableItem_drawable);
        if (dr != null) {
            layer.mDrawable = dr;
        }
    }

Drawable的padding值判断是否需要刷新,如果Padding值发生变化,就return true

/**
     * Refreshes the cached padding values for the specified child.
     *
     * @return true if the child's padding has changed
     */
    private boolean refreshChildPadding(int i, ChildDrawable r) {
        final Rect rect = mTmpRect;
        r.mDrawable.getPadding(rect);
        if (rect.left != mPaddingL[i] || rect.top != mPaddingT[i] ||
                rect.right != mPaddingR[i] || rect.bottom != mPaddingB[i]) {
            mPaddingL[i] = rect.left;
            mPaddingT[i] = rect.top;
            mPaddingR[i] = rect.right;
            mPaddingB[i] = rect.bottom;
            return true;
        }
        return false;
    }

onDraw方法把图片集合绘制到画布,其他方法都大同小异,暂不贴了。

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

代码调用LayerDrawable实例直接new 传入Drawable数组即可。不过需要注意:getResource.getDrawable(