【Android_View】ImageView源碼簡析筆記(一)

【Android】ImageView源碼簡析

引文


這裏是【重要聲明】:
首先非常非常非常感謝您能閱讀這篇文章,重要的謝謝當然是說三遍。
【1】因爲Android系統中與View體系相關的內容較爲複雜龐大,而一篇文章根本不可能講述完所有的要點,因此文章中對於某些屬性細節僅爲介紹,而其實現以及具體的使用與分析等引用了【其它博主】的相應【博文】,藉此希望能略微免除讀者再額外搜索查閱的煩惱。在此對相應文章的作者表示感謝,由於沒有預先與相關博主進行溝通聯繫,如若侵犯了您的權益,請您聯繫我,立刻刪除相應的鏈接。再次表示感謝。
【2】文中有些方法內容的簡析在可能我們平時編程時基本不會調用,但知其然與知其所以然應該能幫助我們更好的理解ImageView。
【3】作者水平有限,文章會有很多Bug,希望小夥伴們多多批評,我將及時學習改正,我們大家共同學習。


ImageView是Android開發中常用的圖像顯示控件。網上有很多關於其某某具體屬性分析的文章,但是搜索來搜索去都沒有發現相應的總結。因此希望借這篇文章將其代碼簡要的分析一下。站在前人的肩膀上,也不能總是吃老本啊,總歸也是要拓展一下的。
本次源碼的分析建立在Android API 25的基礎上,畢竟截止到寫作時傳說中Android O(26)還只是beta_4版本啊。
我們先忽略它的直接與間接子類,只看繼承關係。首先,通過官網的解釋(如下圖):
這裏寫圖片描述

可以很清楚看到ImageView直接繼承於View。


一、內部成員變量及其枚舉類與構造器簡析

1、其成員變量(與相應註釋)主要如下:

    private Uri mUri; 
    private int mResource = 0;
    //【ImageView】中利用【矩陣】來處理縮放等操作
    private Matrix mMatrix;
    //縮放類型
    private ScaleType mScaleType;
    private boolean mHaveFrame = false;
    private boolean mAdjustViewBounds = false;
    private int mMaxWidth = Integer.MAX_VALUE;
    private int mMaxHeight = Integer.MAX_VALUE;

    //以下變量與顯示的Drawable有關
    private ColorFilter mColorFilter = null; //色彩濾鏡
    private boolean mHasColorFilter = false;//是否含有色彩濾鏡
    private Xfermode mXfermode;//圖像混合模式
    private int mAlpha = 255;//透明度
    private final int mViewAlphaScale = 256;
    private boolean mColorMod = false;

    private Drawable mDrawable = null;
    private BitmapDrawable mRecycleableBitmapDrawable = null;
    //注意tint:爲圖片設置渲染顏色,單獨設置時,會覆蓋掉原有背景圖片
    private ColorStateList mDrawableTintList = null;//着色列表
    private PorterDuff.Mode mDrawableTintMode = null;//圖像(渲染)混合
    private boolean mHasDrawableTint = false;
    private boolean mHasDrawableTintMode = false;

    private int[] mState = null;//圖像當前的狀態
    private boolean mMergeState = false;
    private int mLevel = 0;
    [一篇關於Level的博文](http://blog.csdn.net/zhangphil/article/details/48936209)
    private int mDrawableWidth;
    private int mDrawableHeight;
    private Matrix mDrawMatrix = null;//【圖像】矩陣

    // 以下兩個RectF同樣在圖像縮放時調用,具體見下文
    private final RectF mTempSrc = new RectF();
    private final RectF mTempDst = new RectF();

    //如果爲真,會剪切圖片以適應內邊距的大小,與ImageView的paddingXXX屬性配合使用,前提是:該屬性爲true,其餘的設置纔會起作用
    private boolean mCropToPadding;//是否【裁剪】圖片適應【內邊距】

    private int mBaseline = -1;//視圖內,基線的偏移量
    private boolean mBaselineAlignBottom = false;

    //兼容性模式依賴於程序的targetSdkVersion
    private static boolean sCompatDone;

  //對於老版本的APP,AdjustViewBounds behavior將處於兼容模式 
    private static boolean sCompatAdjustViewBounds;

//是否在從stream中創建對象時傳遞資源
    private static boolean sCompatUseCorrectStreamDensity;

// 當前版本小於24時,初始化此變量爲true否則爲false,與是否可見(visable)有關
    private static boolean sCompatDrawableVisibilityDispatch;

2、重要的內部枚舉類

說到ImageView的成員變量,不得不說其內部一個重要的枚舉類 ===> ScaleType。

看名字就可以直截了當地知道Scale(比例)Type(類型)。這個類是用來調整圖片的比例(縮放)類型,決定了圖片在View上顯示時的樣子,如進行何種比例的縮放,及顯示圖片的整體還是部分
其內部共有8個變量。分別是:

屬性名稱 作用
matrix 用矩陣來繪製(從左上角起始的矩陣區域)
fitXY 把圖片不按比例擴大/縮小到View的大小顯示(確保圖片會完整顯示,並充滿View)
fitStart 把圖片按比例擴大/縮小到View的寬度,顯示在View的上部分位置(圖片會完整顯示)
fitCenter 把圖片按比例擴大/縮小到View的寬度,居中顯示(圖片會完整顯示)
fitEnd 把圖片按比例擴大/縮小到View的寬度,顯示在View的下部分位置(圖片會完整顯示)
center 按圖片的原來size居中顯示,當圖片寬超過View的寬,則截取圖片的居中部分顯示,當圖片寬小於View的寬,則圖片居中顯示
centerCrop 按比例擴大/縮小圖片的size居中顯示,使得圖片的高等於View的高,使得圖片寬等於或大於View的寬
centerInside 將圖片的內容完整居中顯示,使得圖片按比例縮小或原來的大小(圖片比View小時)使得圖片寬等於或小於View的寬 (圖片會完整顯示)

閱讀源碼可以發現,縮放類型主要在調用setScaleType()方法時設定。

 public void setScaleType(ScaleType scaleType) {
        if (scaleType == null) {
            throw new NullPointerException();
        }
        if (mScaleType != scaleType) {
        //根據傳入的值,設置當前的縮放類型
        mScaleType = scaleType;
        //View中的方法。緩存機制,(imageview在此跳過)避免浪費內存
        setWillNotCacheDrawing(mScaleType == ScaleType.CENTER); 
        //請求父View重新發起measure,layout,draw等流程進行重繪         
        requestLayout(); 
        //使整個視圖無效,重新繪製。(注意只能在UI線程調用)          
        invalidate();      
     }   
 }

3、ImageView構造器

在構造器中執行的第一個方法是initImageView(),源碼如下:

private void initImageView() {
        mMatrix = new Matrix();
        mScaleType = ScaleType.FIT_CENTER;

        if (!sCompatDone) {
            final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
            sCompatAdjustViewBounds = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
            sCompatUseCorrectStreamDensity = targetSdkVersion > Build.VERSION_CODES.M;
            sCompatDrawableVisibilityDispatch = targetSdkVersion < Build.VERSION_CODES.N;
            sCompatDone = true;
        }
    }

閱讀代碼可以發現,與名稱一致,這個方法主要執行部分初始化操作。諸如圖像(縮放)矩陣的初始化以及初始縮放類型的設置,很明顯,初始縮放類型爲【ScaleType.FIT_CENTER】,即完整地居中顯示圖像。然後進行了部分兼容性適配方面的任務如設置目標SDK版本等。

接下來,通過獲取資源數組TypedArray,進行相應的操作。

//獲取資源數組
final TypedArray a = context.obtainStyledAttributes(attrs,    R.styleable.ImageView, defStyleAttr, defStyleRes);
//獲取資源中的初始Drawable
final Drawable d = a.getDrawable(R.styleable.ImageView_src);
        if (d != null) {
        //將獲取的Drawable,設置爲ImageView的顯示內容。
            setImageDrawable(d);
        }
//初始化基線
mBaselineAlignBottom = a.getBoolean(R.styleable.ImageView_baselineAlignBottom, false);

mBaseline = a.getDimensionPixelSize(R.styleable.ImageView_baseline, -1);

//初始化邊界的自適應      
setAdjustViewBounds(a.getBoolean(R.styleable.ImageView_adjustViewBounds, false));

//初始化最大寬、高  
     setMaxWidth(a.getDimensionPixelSize(R.styleable.ImageView_maxWidth, Integer.MAX_VALUE));
        setMaxHeight(a.getDimensionPixelSize(R.styleable.ImageView_maxHeight, Integer.MAX_VALUE));

//初始化縮放類型
final int index = a.getInt(R.styleable.ImageView_scaleType, -1);
        if (index >= 0) {
            setScaleType(sScaleTypeArray[index]);
        }

[Android中Tint屬簡介](http://blog.csdn.net/zhuoxiuwu/article/details/50976337) 

if (a.hasValue(R.styleable.ImageView_tint)) {
            mDrawableTintList = a.getColorStateList(R.styleable.ImageView_tint);
            mHasDrawableTint = true;
            mDrawableTintMode = PorterDuff.Mode.SRC_ATOP;
            mHasDrawableTintMode = true;
        }

        if (a.hasValue(R.styleable.ImageView_tintMode)) {
            mDrawableTintMode = Drawable.parseTintMode(a.getInt(
                    R.styleable.ImageView_tintMode, -1), mDrawableTintMode);
            mHasDrawableTintMode = true;
        }
        //初始化ImageView中Drawable的着色
        applyImageTint();
        //初始化透明度
        final int alpha = a.getInt(R.styleable.ImageView_drawableAlpha, 255);
        if (alpha != 255) {
            setImageAlpha(alpha);
        }

        mCropToPadding = a.getBoolean(
                R.styleable.ImageView_cropToPadding, false);

        //☆☆☆特別注意:資源數組必須在結束使用後釋放
        a.recycle();

其中在applyImageTint()方法中,設置Drawable的圖像【着色】屬性。
(Tint 這個屬性是Android L以後添加的,附上一篇關於Tint的解析博文

  private void applyImageTint() {
        if (mDrawable != null && (mHasDrawableTint || mHasDrawableTintMode)) {
            mDrawable = mDrawable.mutate();

            if (mHasDrawableTint) {
                mDrawable.setTintList(mDrawableTintList);
            }

            if (mHasDrawableTintMode) {
                mDrawable.setTintMode(mDrawableTintMode);
            }
            //Drawable在應用着色之前可能沒有完成狀態的設置,這裏進行重新設置、
            if (mDrawable.isStateful()) {
                mDrawable.setState(getDrawableState());
            }
        }
    }

上述即爲ImageView的構造器簡析。


二、部分重寫的方法簡析

1、有關View的測量、佈局與繪製的方法

ps:見【Android_View】ImageView源碼簡析筆記(二)

2、其它方法(一)

1)Drawable確認

 @Override
    protected boolean verifyDrawable(@NonNull Drawable dr) {
        return mDrawable == dr || super.verifyDrawable(dr);
    }

這個方法是重寫了View中方法,原始註釋翻譯爲:如果您的視圖子類要顯示自己的Drawable對象,那麼它應該覆蓋重寫此函數,併爲其顯示的任何Drawable返回true。 這樣可以安排這些可繪製的動畫。
我們看一下View中的原始方法:

 @CallSuper
    protected boolean verifyDrawable(@NonNull Drawable who) {
//我們應該避免驗證與滾動條【scroll bar】有關的內容,以免陷入無效的循環。
        return who == mBackground || (mForegroundInfo != null && mForegroundInfo.mDrawable == who);
    }

按照原方法的註釋,這個方法用來驗證當前顯示的Drawable是否是我們希望ImageView顯示的圖像,如果是則返回true,否則返回調用父類的相應方法後的結果(很明顯,此處的調用方爲繼承View的子類,而父類指的是View本身)。

2)Drawable狀態切換

 @Override
    public void jumpDrawablesToCurrentState() {
        super.jumpDrawablesToCurrentState();
        if (mDrawable != null) mDrawable.jumpToCurrentState();
    }

該方法調用了Drawable中的jumpToCurrentState(),代碼如下:

/**
     * If this Drawable does transition animations between states, ask that it immediately jump to the current state and skip any active animations.
     */
    public void jumpToCurrentState() {
    }

可以發現,方法體內並沒有相應的執行語句,註釋的意思爲:如果當前的Drawable在兩個狀態轉換中執行了相應的動畫,則請求立刻跳轉到指定的狀態並略過動畫。

3)重繪選中的Drawable

@Override
    public void invalidateDrawable(@NonNull Drawable dr) {
        if (dr == mDrawable) {
            if (dr != null) {
                // 更新緩存信息
                final int w = dr.getIntrinsicWidth();
                final int h = dr.getIntrinsicHeight();
                if (w != mDrawableWidth || h != mDrawableHeight) {
                    mDrawableWidth = w;
                    mDrawableHeight = h;
                   // 更新依賴於邊界信息的矩陣
                    configureBounds();
                }
            }
//在這種情況下,我們可以使得整個視圖無效,因爲很難知道drawable實際上在哪裏。這是因爲應用的偏移和變換操作等使之變得複雜。理論上,我們可以得到drawable的邊界等,並通過轉換和補償等來操作它們,但這些並不值得我們這麼做。
            invalidate();
        } else {
            super.invalidateDrawable(dr);
        }
    }

這個方法主要用來重新繪製Drawable。我們知道,視圖的繪製需要完成測量、佈局、繪製三個步驟,很明顯requestLayout()方法的調用是必須執行上述三個步驟的,而invalidate()方法只能在Draw流程中起作用,而不會完成先前的測量與佈局流程。

由於ImageView中invalidate()的實現主要調用View的相應方法,因此我們直接看View.invalidate():

 /**
 This is where the invalidate() work actually happens. A full invalidate() causes the drawing cache to be invalidated, but this function can be called with invalidateCache set to false to skip that invalidation step for cases that do not need it (for example, a component that remains at the same dimensions with the same content).
*/
    void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }

根據註釋我們可以知道,在視圖重繪實際中起作用的就是上述方法。在調用該方法時,如果傳入true,則進行完整的重繪,也就是使得繪圖緩存無效;而傳入false,則會保留住緩存內容。
而通過ImageView的源碼,我們可以發現,此處傳入了true,即完全重繪。

 public void invalidate() {
        invalidate(true);
    }

此方法主要在drawableStateChanged()中調用。

@Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();

        final Drawable drawable = mDrawable;
        if (drawable != null && drawable.isStateful()
                && drawable.setState(getDrawableState())) {
            invalidateDrawable(drawable);
        }
    }

附上一篇與【視圖重繪】有關的博文

4)判斷是否有重疊

@Override
    public boolean hasOverlappingRendering() {
       return (getBackground() != null &&    getBackground().getCurrent() != null);
    }

自定義 View 時重寫 hasOverlappingRendering ()指定 View 是否有 Overlapping 的情況,提高渲染性能。

上述getBackground()實質調用了View中的方法,返回對象亦爲Drawable。

  /**
     * Gets the background drawable
     * @return The drawable used as the background for this view, if any.
     * @see #setBackground(Drawable)
     * @attr ref android.R.styleable#View_background
     */
    public Drawable getBackground() {
        return mBackground;
    }

5)輔助功能

/** @hide */
    @Override
    public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
        super.onPopulateAccessibilityEventInternal(event);
        final CharSequence contentDescription = getContentDescription();
        if (!TextUtils.isEmpty(contentDescription)) {
            event.getText().add(contentDescription);
        }
    }

附一篇文章,有關於Android輔助功能

6)獲取與設置 是否適應邊界

public boolean getAdjustViewBounds() {
        return mAdjustViewBounds;
    }
 @android.view.RemotableViewMethod
    public void setAdjustViewBounds(boolean adjustViewBounds) {
        mAdjustViewBounds = adjustViewBounds;
        if (adjustViewBounds) {
            setScaleType(ScaleType.FIT_CENTER);
        }
    }

很明顯,如果希望ImageView調整其邊界以保持drawable的寬高比,則將將其設置爲true。
可以看到,如果傳入true值,會將縮放狀態設置爲ScaleType.FIT_CENTER

7)獲取 與 設置 視圖的最大寬度MaxHeight

 @android.view.RemotableViewMethod
    public void setMaxWidth(int maxWidth) {
        mMaxWidth = maxWidth;
    }
    public int getMaxHeight() {
        return mMaxHeight;
    }

對上述方法,官方的註釋爲:
爲此視圖提供最大寬度的可選參數。 只有{@link #setAdjustViewBounds(boolean)}設置爲true時纔有效。
要將圖像設置爲最大爲100 x 100,同時保留原始寬高比,請執行以下操作:
1)將adjustViewBounds設置爲true
2)將maxWidth和maxHeight設置爲100
3)將高度和寬度佈局參數設置爲WRAP_CONTENT。
(最大高度MaxHeight 的操作與之類似)

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