Android 进阶——高级UI必知必会之统一可绘制概念Drawable详解(四)

引言

前面文章我们学了Canvas、Paint等绘制知识了解绘制流程的基本要素以及核心流程,事实上绘制不仅仅只是在Canvas上进行,Android 因此还抽象了在Canvas基础上的Drawable,那么Drawable是一个Bitmap么?

相关文章链接如下:

一、Drawable概述

Drawable是一个抽象的概念,表示一个可绘制的对象,可以通过Resource类的getDrawable(int id,int theme)获取对应的Drawable对象,可以在Canvas上进行绘制的顶级抽象概念。在Android中Drawable可能是一张位图(BitmapDrawable),可能是一个图形(ShapeDrawable),也可能是一个图层(LayerDrawable)等等,正如源码显示Drawable是一个抽象类,通常在开发中不直接使用,往往都是使用它的派生类或者自定义的Drawable子类,Android中已经实现了派生类有:ClipDrawable, ColorDrawable, DrawableContainer, GradientDrawable, InsetDrawable, LayerDrawable, NinePatchDrawable, PictureDrawable, RotateDrawable, ScaleDrawable, ShapeDrawable,AnimationDrawable, LevelListDrawable, PaintDrawable, StateListDrawable, TransitionDrawable,每一种子类就是Drawable在Android里的体现形式。我们根据画图的需求,创建相应的可绘制对象(Drawable子类),就可以将这个可绘制对象当作一块“画布”,在其上面操作可绘制对象,并最终将这种可绘制对象显示在画布上Drawable#draw(Canvas canvas)),也有点类似于“内存画布“,相当于是把Drawable绘制到屏幕上。

二、Drawable系设计思想浅析

从源码角度上分析Drawable 只是提供了一个统一的顶层概念以及一些共有的操作API,并不负责任何涉及到绘制的具体工作,针对不同类型的Drawable交由对应的子类去重写draw方法去真正实现绘制工作,其实与View系的设计有点类似(把draw的操作延迟到子类),在进行绘制时在子View的onDraw方法中被调用。不过Drawable并不属于View,所以不能接收任何事件,自然也不能直接与用户交互,为了与当前正在绘制的内容进行交互Drawable 定义了一些通用机制,除了Callback还有setLevel方法等(通过setLevel方法改变mLevel值来实现动态回调onLevelChanged),当然还有缓存机制,当你新生成一个Drawable的时候,就会将Drawable的ConstantState从Drawable中取出,然后放入你Cache池中。

public abstract class Drawable {
	...
    private static final Rect ZERO_BOUNDS_RECT = new Rect();
    static final PorterDuff.Mode DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN;
    private int[] mStateSet = StateSet.WILD_CARD;
    private int mLevel = 0;
    private Rect mBounds = ZERO_BOUNDS_RECT;  // lazily becomes a new Rect()
    private WeakReference<Callback> mCallback = null;

	//Drawable的绘制效果的核心方法,在setBounds方法指定的矩形区域内进行绘制
    public abstract void draw(@NonNull Canvas canvas);
	
	/**
     * 用于指定Drawable实例绘制的位置和大小。所有的Drawable实例都会生成请求的尺寸,为当前Drawable实例设置一个矩形范围,
     * 在draw方法调用时候,Drawable实例将被绘制到这个矩形范围内。
     */
    public void setBounds(int left, int top, int right, int bottom) {
        Rect oldBounds = mBounds;

        if (oldBounds == ZERO_BOUNDS_RECT) {
            oldBounds = mBounds = new Rect();
        }

        if (oldBounds.left != left || oldBounds.top != top ||
                oldBounds.right != right || oldBounds.bottom != bottom) {
            if (!oldBounds.isEmpty()) {
                // first invalidate the previous bounds
                invalidateSelf();
            }
            mBounds.set(left, top, right, bottom);
            onBoundsChange(mBounds);
        }
    }
    public void setBounds(@NonNull Rect bounds) {
        setBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
    }

	//将当前Drawable实例通过setBounds设置的绘制范围拷贝到客户端提供的Rect实例中返回
    public final void copyBounds(@NonNull Rect bounds) {
        bounds.set(mBounds);
    }
    public final Rect copyBounds() {
        return new Rect(mBounds);
    }
    /**
     *返回当前Drawable实例的矩形绘制范围,所以如果是需要一个拷贝的矩形范围,
     *应该调用copyBounds来代替,而且调用getBounds时不能修改返回的矩形,因为这会影响Drawable实例。
     */
    @NonNull
    public final Rect getBounds() {
        if (mBounds == ZERO_BOUNDS_RECT) {
            mBounds = new Rect();
        }

        return mBounds;
    }

    /**
     *当设置为true,则该Drawable实例在缩放或者旋转时候将对它关联的bitmap进行滤波过滤。可以提升旋转时的绘制效果。
     *如果该Drawable实例未使用bitmap,这个方法无作用。
     */
    public void setFilterBitmap(boolean filter) {}
    public boolean isFilterBitmap() {
        return false;
    }

	//一个回调接口,用于调度和执行Drawable实例的动画。比如实现自定义的动画Drawable时就需要实现这个接口。
    public interface Callback {
        //Drawable实例被重绘时候调用。在当前Drawable实例位置的View实例需要重绘,或者至少部分重绘。
        void invalidateDrawable(@NonNull Drawable who);

        //一个Drawable实例可以调用这个方法预先安排动画的下一帧,也可以通过Handler.postAtTime实现。
        void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when);

		//一个Drawable实例可以调用这个方法取消之前安排的某一帧。
        也可以通过Handler.removeCallbacks实现。
        void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);
    }
    

    public void invalidateSelf() {
        final Callback callback = getCallback();
        if (callback != null) {
            callback.invalidateDrawable(this);
        }
    }

    public void scheduleSelf(@NonNull Runnable what, long when) {
        final Callback callback = getCallback();
        if (callback != null) {
            callback.scheduleDrawable(this, what, when);
        }
    }

    public void unscheduleSelf(@NonNull Runnable what) {
        final Callback callback = getCallback();
        if (callback != null) {
            callback.unscheduleDrawable(this, what);
        }
    }
	//获取当前Drawable实例的布局方向。
    public @View.ResolvedLayoutDir int getLayoutDirection() {
        return mLayoutDirection;
    }
	
	//设置当前Drawable实例的布局方向。
    public final boolean setLayoutDirection(@View.ResolvedLayoutDir int layoutDirection) {
        if (mLayoutDirection != layoutDirection) {
            //如果当前Drawable布局方向和layoutDirection不一致,
            //则修改布局方向为layoutDirection,然后执行onLayoutDirectionChanged
            mLayoutDirection = layoutDirection;
            return onLayoutDirectionChanged(layoutDirection);
        }
        return false;
    }
	
	//当调用setLayoutDirection方法,Drawable布局方向发生变化后调用
    public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
        return false;
    }

	// 设置Drawable实例的透明度。(0:完全透明;255:完全不透明)
    public abstract void setAlpha(@IntRange(from=0,to=255) int alpha);

    /**
     *为当前Drawable实例设置颜色滤镜
     */
    public abstract void setColorFilter(@Nullable ColorFilter colorFilter);
    /**
	 *为当前Drawable实例设置滤镜效果
     */
    public void setColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode) {
        setColorFilter(new PorterDuffColorFilter(color, mode));
    }
	
	//为当前Drawable实例着色
    public void setTint(@ColorInt int tintColor) {
        setTintList(ColorStateList.valueOf(tintColor));
    }
    
    //根据ColorStateList对当前Drawable实例进行着色,空方法是交由子类去实现的
    public void setTintList(@Nullable ColorStateList tint) {}

	// 设置当前Drawable实例着色的混合过滤模式
    public void setTintMode(@NonNull PorterDuff.Mode tintMode) {}

	//设置当前Drawable实例热点区域的中心点坐标
    public void setHotspot(float x, float y) {}

	//为当前Drawable实例设置一个状态值集合。当现有状态和stateSet不同时候,触发onStateChange(stateSet)方法。
    public boolean setState(@NonNull final int[] stateSet) {
        if (!Arrays.equals(mStateSet, stateSet)) {
            mStateSet = stateSet;
            return onStateChange(stateSet);
        }
        return false;
    }
	...
    /**
    *将当前Drawable实例的padding值作为参数设置为Rect实例padding
    *的边界值。如果当前实例有padding值,返回true,否则返回false;
    *当返回false,则Recti实例padding的边界值都设置为0;
    */
    public boolean getPadding(@NonNull Rect padding) {
        padding.set(0, 0, 0, 0);
        return false;
    }
    //仅仅是一个标记值的作用,每调用一次就改变mLevel的值,两次值不一样时就会触发onLevelChange回调
    public final boolean setLevel(@IntRange(from=0,to=10000) int level) {
        if (mLevel != level) {
            mLevel = level;
            return onLevelChange(level);
        }
        return false;
    }
    //当通过调用{@link #setLevel}值改变mLevel值时就会触发这个回调方法
    protected boolean onLevelChange(int level) {
        return false;
    }
    ...
}

当需要使用ImageView绘制Bitmap时就会调用到BitmapDrawable中的draw方法,把要绘制的Bitmap绘制到Canvas上,而Drawable相当于是起到了一个“容器工具”的作用,把不同类型的图形、图像的绘制统一起来。

    @Override
    public void draw(Canvas canvas) {
        final Bitmap bitmap = mBitmapState.mBitmap;
        if (bitmap == null) {
            return;
        }

        final BitmapState state = mBitmapState;
        final Paint paint = state.mPaint;
        if (state.mRebuildShader) {
            final Shader.TileMode tmx = state.mTileModeX;
            final Shader.TileMode tmy = state.mTileModeY;
            if (tmx == null && tmy == null) {
                paint.setShader(null);
            } else {
                paint.setShader(new BitmapShader(bitmap,
                        tmx == null ? Shader.TileMode.CLAMP : tmx,
                        tmy == null ? Shader.TileMode.CLAMP : tmy));
            }

            state.mRebuildShader = false;
        }

        final int restoreAlpha;
        if (state.mBaseAlpha != 1.0f) {
            final Paint p = getPaint();
            restoreAlpha = p.getAlpha();
            p.setAlpha((int) (restoreAlpha * state.mBaseAlpha + 0.5f));
        } else {
            restoreAlpha = -1;
        }

        final boolean clearColorFilter;
        if (mTintFilter != null && paint.getColorFilter() == null) {
            paint.setColorFilter(mTintFilter);
            clearColorFilter = true;
        } else {
            clearColorFilter = false;
        }

        updateDstRectAndInsetsIfDirty();
        final Shader shader = paint.getShader();
        final boolean needMirroring = needMirroring();
        if (shader == null) {
            if (needMirroring) {
                canvas.save();
                // Mirror the bitmap
                canvas.translate(mDstRect.right - mDstRect.left, 0);
                canvas.scale(-1.0f, 1.0f);
            }

            canvas.drawBitmap(bitmap, null, mDstRect, paint);

            if (needMirroring) {
                canvas.restore();
            }
        } else {
            updateShaderMatrix(bitmap, paint, shader, needMirroring);
            canvas.drawRect(mDstRect, paint);
        }

        if (clearColorFilter) {
            paint.setColorFilter(null);
        }

        if (restoreAlpha >= 0) {
            paint.setAlpha(restoreAlpha);
        }
    }

三、自定义Drawable的简单实例

继承Drawable实现自定义比例的抠图效果。

package com.dn_alan.myapplication;

import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.Gravity;

/**
 * @author cmo
 */
public class MoClipDrawable extends Drawable {

    private final Rect mTmpRect = new Rect();
    private Drawable srcDrawable;

    public MoClipDrawable(Drawable src) {
        srcDrawable = src;
    }

    @Override
    public void draw(Canvas canvas){
        //得到当前自身Drawable的矩形区域
        Rect bounds=getBounds();
        Rect targetRect=new Rect();
        int w = bounds.width();
        int h=bounds.height()-2;
        int ratio=-1;
        int gravity = ratio < 0 ? Gravity.LEFT : Gravity.RIGHT;
                //从一个已有的bounds矩形边界范围中抠出一个矩形r
                Gravity.apply(
                        gravity,//从左边还是右边开始抠
                        w/2,//目标矩形的宽
                        h, //目标矩形的高
                        bounds, //被抠出来的rect
                        targetRect);//目标rect
        canvas.save();
        canvas.clipRect(targetRect);//切割
        srcDrawable.draw(canvas);//画
        canvas.restore();//恢复之前保存的画布
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        // 定好Drawable图片的宽高---边界bounds
        srcDrawable.setBounds(bounds);
    }

    @Override
    public int getIntrinsicWidth() {
        //得到Drawable的实际宽度
        return srcDrawable.getIntrinsicWidth();
    }

    @Override
    public int getIntrinsicHeight() {
        //得到Drawable的实际高度
        return srcDrawable.getIntrinsicHeight();
    }

    @Override
    protected boolean onLevelChange(int level) {
        // 当设置level的时候回调---提醒自己重新绘制
        invalidateSelf();
        return true;
    }

    @Override
    public void setAlpha(int i) {
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
    }

    @Override
    public int getOpacity() {
        return PixelFormat.UNKNOWN;
    }
}

Drawable不能单独使用必须要配置到View上才有效果

import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.widget.ImageView;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private ImageView iv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        iv=findViewById(R.id.iv_love_left);
        ///获取Drawable实例
        MoClipDrawable drawable=new MoClipDrawable(getResources().getDrawable(R.mipmap.mn));
        ///把Drawable设置到View上
        iv.setImageDrawable(drawable);
    }
}

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/102883547