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);
    }
}

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章