ImageView源碼分析

美圖欣賞
冬季里的黃山
0. 基本特性
  1. src: 設置圖片資源
  2. scaleType: 圖形設定顯示效果,是裁剪,縮放,拉伸等(下面的configureBounds方法重點關注);
  3. cropToPadding: 保證padding空間區域不會有圖形內容,會被裁剪掉;
  4. tint: 圖片上色處理.
  5. adjustViewBounds: 這個屬性對imageView的實際寬高有很大的影響,他的目的是保證控件寬高比例和圖形drawable比例相同.在測量過程時會通過該屬性以及drawble寬高比來構建控件的實際大小;
  • 等等
1. 測量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    resolveUri();
    int w;
    int h;

    //真是圖片drawable的寬高比記錄
    float desiredAspect = 0.0f;

    //控件的寬高是否支持動態的調整以適配圖片的寬高比;
    boolean resizeWidth = false;
    boolean resizeHeight = false;   
    //imageview的寬,高測量規格
    final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

    if (mDrawable == null) {
        // If no drawable, its intrinsic size is 0.
        mDrawableWidth = -1;
        mDrawableHeight = -1;
        w = h = 0;
    } else {//如果有drawable;
        w = mDrawableWidth;
        h = mDrawableHeight;
        if (w <= 0) w = 1;
        if (h <= 0) h = 1;

        //如果設置了adjustViewBounds="true"
        if (mAdjustViewBounds) {
            //寬或者高設定的不是match_parent可以
            resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
            resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
            //記錄圖片draowable的寬高比
            desiredAspect = (float) w / (float) h;
        }
    }

    //控件的padding數值
    int pleft = mPaddingLeft;
    int pright = mPaddingRight;
    int ptop = mPaddingTop;
    int pbottom = mPaddingBottom;
    int widthSize;
    int heightSize;

    //寬,高支持調整, 看看怎么調整?
    if (resizeWidth || resizeHeight) {
        /* If we get here, it means we want to resize to match the
                drawables aspect ratio, and we have the freedom to change at
                least one dimension. 
            */

        //從這里看出,如果iv控件如果沒有設定adjustViewBounds="true",maxWith,maxHeight是不會生效的.
        //預先計算出iv的寬度,這里考慮了最大寬度;
        widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);

         //預先計算出iv的高度,這里考慮了最大高度;
        heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);
        
        //然而不止這樣,還要考慮drawable的寬高比,
        if (desiredAspect != 0.0f) {
            // 計算iv控件本身的寬,高比;
            float actualAspect = (float)(widthSize - pleft - pright) /
                (heightSize - ptop - pbottom);

            //如果寬,高比是不同的時候,進入A1
            if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {

                boolean done = false;

                //寬度是wrap的時候
                if (resizeWidth) {
                    //根據drawable的寬高比,以及當前的iv的預測量高度來計算現在的寬度;
                    int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) +
                        pleft + pright;

                    //當高度固定的時候.會在newWidth和mMaxWidth中再得出一個最小的;
                    if (!resizeHeight && !mAdjustViewBoundsCompat) {
                        widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
                    }

                    //當高度固定的時候,該條件會滿足;若不固定即寬高是actualAspect和desiredAspect
                    //一般是相同的,也不會進入進入A1,固不會來到這里.
                    if (newWidth <= widthSize) {
                        widthSize = newWidth;
                        //等比計算iv寬度之后就不會再計算高了,不需要重復的計算了;
                        done = true;
                    } 
                }

                // 如果寬沒有計算,即寬度是固定的,高度是wrap,那么就計算高的數值;原理和寬計算是一樣的.
                if (!done && resizeHeight) {
                    int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) +
                        ptop + pbottom;

                    // Allow the height to outgrow its original estimate if width is fixed.
                    if (!resizeWidth && !mAdjustViewBoundsCompat) {
                        heightSize = resolveAdjustedSize(newHeight, mMaxHeight,
                                                         heightMeasureSpec);
                    }

                    if (newHeight <= heightSize) {
                        heightSize = newHeight;
                    }
                }
            }
        }
    } else {
        
        //當不存在寬,高調整的時候,如果設置了adjustViewBounds="false"
        //計算方式就是根據drawable的寬,高,以及通常的測量方式來測量iv的大小了.
        w += pleft + pright;
        h += ptop + pbottom;

        w = Math.max(w, getSuggestedMinimumWidth());
        h = Math.max(h, getSuggestedMinimumHeight());

        widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
        heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
    }
    setMeasuredDimension(widthSize, heightSize);
}
  • 總結:測量有一些復雜,分為兩類.其一是普通的測量,根據padding, drawable的寬高,以及測量規格進行標準的圖形測量.其二,當圖形設置了adjustViewBounds=true這一個屬性,那么意圖是要讓圖形顯示的時候能夠保持原來的寬高比效果,必須要至少有一方的規格不是固定的,這樣就可以根據drawable的寬高比以及另外一方的fix尺寸進行比例縮放,進而讓圖形可以不變形地展示出來.測量就是實現了這樣的意圖.(注意: 圖形的最終是否變形,是否等比還是要看scaleType的屬性設定)
  • 尺寸的修訂:
private int resolveAdjustedSize(int desiredSize, int maxSize,
                                 int measureSpec) {
     int result = desiredSize;
     int specMode = MeasureSpec.getMode(measureSpec);
     int specSize =  MeasureSpec.getSize(measureSpec);
     switch (specMode) {
         case MeasureSpec.UNSPECIFIED:
             /* Parent says we can be as big as we want. Just don't be larger
                   than max size imposed on ourselves.
                */
             result = Math.min(desiredSize, maxSize);
             break;
         case MeasureSpec.AT_MOST:
             // Parent says we can be as big as we want, up to specSize. 
             // Don't be larger than specSize, and don't be larger than 
             // the max size imposed on ourselves.
             result = Math.min(Math.min(desiredSize, specSize), maxSize);
             break;
         case MeasureSpec.EXACTLY:
             // No choice. Do what we are told.
             result = specSize;
             break;
     }
     return result;
 }

  • 根據圖形內容drawable寬/高, 以及限制的最大值來計算出圖形控件最終可以使用的寬與高.drawable最終填入到該圖形區域中.
2. 圖形效果設定, 主要是根據設定scaleType來縮放或者裁剪圖片.

private void configureBounds() {
    if (mDrawable == null || !mHaveFrame) {
        return;
    }

    int dwidth = mDrawableWidth;
    int dheight = mDrawableHeight;

    int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
    int vheight = getHeight() - mPaddingTop - mPaddingBottom;

    boolean fits = (dwidth < 0 || vwidth == dwidth) &&
        (dheight < 0 || vheight == dheight);
    //如果是color類型的drawable或者fitxy,那么drawable圖形會填滿控件,不管是否拉伸;
    if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
        /* If the drawable has no intrinsic size, or we're told to
                scaletofit, then we just fill our entire view.
            */
        mDrawable.setBounds(0, 0, vwidth, vheight);
        mDrawMatrix = null;
    } else {
        // 將drawable的寬高設地為原始的圖形寬高,后面的縮放都是在這個基礎上做的變換
        mDrawable.setBounds(0, 0, dwidth, dheight);

        if (ScaleType.MATRIX == mScaleType) {
            //設定了matrix模式,就用他們設定的;
            if (mMatrix.isIdentity()) {
                mDrawMatrix = null;
            } else {
                mDrawMatrix = mMatrix;
            }
        } else if (fits) {
            // 沒有縮放,也沒有偏移
            mDrawMatrix = null;
        } else if (ScaleType.CENTER == mScaleType) {
            // 將Bitmap放在圖形控件的中心,不會去縮放drawable;不管大小如何
            mDrawMatrix = mMatrix;
            mDrawMatrix.setTranslate((int) ((vwidth - dwidth) * 0.5f + 0.5f),
                                     (int) ((vheight - dheight) * 0.5f + 0.5f));
        } else if (ScaleType.CENTER_CROP == mScaleType) {
            //CENTER_CROP,他會將圖形進行縮放,并且鋪滿整個iv空間居中顯示,至少一個方向保留所有內容
            //圖形的寬高比大和iv控件寬高比進行比對: 圖形的寬高比大,就以iv的高進行縮放;圖形的寬高比小,
            //就以iv的寬縮放.最后都圖形能鋪滿空間.
            mDrawMatrix = mMatrix;

            float scale;
            float dx = 0, dy = 0;
            //圖形的寬高比大,以iv的高為基準縮放,圖形的高內容保留,寬會丟失一部分圖形內容
            if (dwidth * vheight > vwidth * dheight) {
                scale = (float) vheight / (float) dheight; 
                dx = (vwidth - dwidth * scale) * 0.5f;
            } else {//圖形的寬高比小,以iv的寬為基準縮放,高內容會有部分的圖形丟失
                scale = (float) vwidth / (float) dwidth;
                dy = (vheight - dheight * scale) * 0.5f;
            }
            //縮放
            mDrawMatrix.setScale(scale, scale);
            //平移drawable中心到iv中心;
            mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
        } else if (ScaleType.CENTER_INSIDE == mScaleType) {
            //該模式會以完整展示為目的,圖形大于iv就縮放,小于就不做什么.
            //fitcenter大于iv和center_inside一樣,小于iv大小會放大的,這個不會放大!
            mDrawMatrix = mMatrix;
            float scale;
            float dx;
            float dy;

            //小了就不放大了
            if (dwidth <= vwidth && dheight <= vheight) {
                scale = 1.0f;
            } else {
                //大了就要去縮放一下了;
                scale = Math.min((float) vwidth / (float) dwidth,
                                 (float) vheight / (float) dheight);
            }

            dx = (int) ((vwidth - dwidth * scale) * 0.5f + 0.5f);
            dy = (int) ((vheight - dheight * scale) * 0.5f + 0.5f);
            //整合到matrix里面,后面組合到drawable中去;
            mDrawMatrix.setScale(scale, scale);
            mDrawMatrix.postTranslate(dx, dy);
        } else {
            // ScaleType.FIT_START || ScaleType.FIT_CENTER || ScaleType.FIT_END,
            //這三個模式的使用實現; 會等比縮放至填滿一邊,另外一邊一般會有空隙;
            mTempSrc.set(0, 0, dwidth, dheight);
            mTempDst.set(0, 0, vwidth, vheight);

            mDrawMatrix = mMatrix;
            mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
        }
    }
}
  • 總結一下:

    • FIT_START || FIT_CENTER<默認的> || FIT_END

      • 等比縮放,以至能夠填滿整個iv控件空間.
    • CENTER_INSIDE

      • 以完整展示圖片為目的,drawable圖形大于iv就縮放,小于就不做什么.fit_center小于會放大填滿
    • CENTER

      • 不會縮放,多大就是多大,圖形中心在iv控件中心
    • CENTER_CROP

      • 縮放, 在一個方向頂邊,一個方向放大后裁剪,中心顯示
    • FIT_XY

      • 填滿控件空間, 會出現拉扯變形現象.
    • MATRIX

      • 可以自定義矩陣變化,平移,縮放,旋轉等等效果.
3. 圖片設定效果的生效與繪制
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //沒有drwable不繪制
    if (mDrawable == null) {
        return; // couldn't resolve the URI
    }
    //color,shape一開始沒有寬高,經過設定之后是有寬高的;
    if (mDrawableWidth == 0 || mDrawableHeight == 0) {
        return;     // nothing to draw (empty bounds)
    }
    //如果沒有矩陣變化,沒有圖形設定就執行drawable的繪制了.
    if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
        mDrawable.draw(canvas);
    } else {

        int saveCount = canvas.getSaveCount();
        canvas.save();
        //裁剪和偏移的內容;當mCropToPadding為true的時候,會將padding區域的圖形內容都切去.
        // 不是padding區域本來就沒有圖形內容嗎?不是的,在有些場景下比如centerCrop類型.
        //圖形在放大情況下會填充到padding區間的,這個時候如果進行mCropToPadding,才可以讓
        //padding區域沒有圖形內容.
        if (mCropToPadding) {
            final int scrollX = mScrollX;
            final int scrollY = mScrollY;
            canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
                            scrollX + mRight - mLeft - mPaddingRight,
                            scrollY + mBottom - mTop - mPaddingBottom);
        }
        //畫布偏移。
        canvas.translate(mPaddingLeft, mPaddingTop);
        //這里是重要的組合前面的圖形變化設定,組合到canvas中去
        if (mDrawMatrix != null) {
            canvas.concat(mDrawMatrix);
        }
        //drawbale來繪制圖形;
        mDrawable.draw(canvas);
        canvas.restoreToCount(saveCount);
    }
}

4. 實例說明:
8238c52d33b4a3ba0be03a613faad9c9.jpg
  • 假設是一張寬圖,寬/高 > 1. 當scaleType為centerCrop, padding=30dp, 我們實際的內容顯示是如何顯示的呢? 當paddingLeft=30dp的時候又是怎樣的效果呢?

    //圖形會居中顯示,無邊距。
    0. scaleType=centerCrop 
      <ImageView
            android:id="@+id/iv"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:src="@drawable/reba"
            android:scaleType="centerCrop"
            />
    //圖形上下有30內邊距,左右無內邊距,會居中顯示。
    1. scaleType=centerCrop, padding=30dp;   
      <ImageView
            android:id="@+id/iv"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:src="@drawable/reba"
            android:scaleType="centerCrop"
            android:padding="30dp"
            />
    
    //圖形的中心在控件的中心右15dp位置,不會居中顯示,也沒有內邊距。
    2.  scaleType=centerCrop, paddingLeft=30dp;      
      <ImageView
            android:id="@+id/iv"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:src="@drawable/reba"
            android:scaleType="centerCrop"
            android:paddingLeft="30dp"
            />
    
  • 0模式下:根據configureBounds中centerCrop的計算規則圖形的寬高比大,就以iv的高進行縮放, 所以高縮放到填滿高的空間,高不會裁剪,但是寬會有一些裁剪,然后根據dx = (vwidth - dwidth * scale) * 0.5f;的矩陣中心偏移可以得出偏移量剛好是圖形中心和控件中心的差值,然后經過mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));偏移,就將圖形矩陣的中心和控件中心重合在一起了。所以圖形就居中顯示.

  • 1模式下:同樣是和0一樣進行高縮放至填滿,因為有上下30的padding,所以只能縮放之后上下有30dp的空白,但是在這里經過矩陣偏移之后圖形的中心并不在控件的中心。假設經過縮放之后圖形的寬為200dp, 高為100dp:

    1590912302158.png
  • 該模式下, 當經過dx = (vwidth - dwidth * scale) * 0.5f;計算之后,dx = -80dp, 經過postTranslate偏移,也就是說圖形向中心會左移80, x中心為20dp了,并沒有與控件的中心(x = 50dp)對齊, 那么控件顯示中心是圖形的中心的右邊位置嗎?答案不是, 在的onDraw中還有一次畫布偏移,canvas.translate(mPaddingLeft, mPaddingTop);這里會將圖形的位置往右移30dp,所以就會和控件中心水平重合。圖形會居中顯示的呢,同時paddingTop的向下偏移30dp在垂直方向上也能對齊控件中心。最終的圖形效果是這樣子, 紅色區域為圖形的畫布內容。

    1590913188325.png
  • 在2模式下,只有paddingLeft=30dp, 圖形向右偏移30dp/2 = 15dp。來看下, 紅色為圖形drawable, 首先經過偏移之后, 圖形的中心在0點的右邊35dip之處。
1590914954462.png

然后經過canvas的右移paddingLeft, 30dp, 那么drawable的中心就在0點右側65dp處,所以圖形就在控件中心右側的15dp處而不是30dp處(控件中心在0點的右側50dp處)。


1590915351468.png

大概就這樣子吧,

5. 圖片的著色,濾色處理
  1. ColorStateList: 這個是對圖片進行著色的顏色集,底層是通過ColorFilter來實現的.區別是他可以提供一組色彩處理.他會讀取tint設置的顏色屬性值. 他會和PorterDuff.Mode組合使用.

    //讀取tint顏色到ColorStateList中
    ColorStateList mDrawableTintList = a.getColorStateList(R.styleable.ImageView_tint);
    .......
    
    //將顏色集設置到drawable中去;最后在onDraw方法中通過omDrawable.draw(canvas),生效圖形的著色處理.
    private void applyImageTint() {
        if (mDrawable != null && (mHasDrawableTint || mHasDrawableTintMode)) {
            mDrawable = mDrawable.mutate();
    
            if (mHasDrawableTint) {
                mDrawable.setTintList(mDrawableTintList);
            }
    
            if (mHasDrawableTintMode) {
                mDrawable.setTintMode(mDrawableTintMode);
            }
        }
    }
    
  2. colorFilter: 也是著色,會覆蓋ColorStateList的效果.圖片的黑白處理就可以用他來實現.

    //設置濾色效果
    public void setColorFilter(ColorFilter cf) {
        if (mColorFilter != cf) {
            mColorFilter = cf;
            mHasColorFilter = true;
            mColorMod = true;
            //顏色
            applyColorMod();
            invalidate();
        }
    }
    
    private void applyColorMod() {
        // Only mutate and apply when modifications have occurred. This should
        // not reset the mColorMod flag, since these filters need to be
        // re-applied if the Drawable is changed.
        if (mDrawable != null && mColorMod) {
            mDrawable = mDrawable.mutate();
            if (mHasColorFilter) {
                //將顏色filter設定到drawable中去
                mDrawable.setColorFilter(mColorFilter);
            }
            mDrawable.setXfermode(mXfermode);
            mDrawable.setAlpha(mAlpha * mViewAlphaScale >> 8);
        }
    }
    
    //實現圖片的灰白/黑白效果處理,一般是利用該策略來做的;
    ---- start ----
    Drawable mDrawable = iv.getDrawable();
    ColorMatrix cm = new ColorMatrix();
    cm.setSaturation(0);//灰白設定
    ColorMatrixColorFilter cf = new ColorMatrixColorFilter(cm);
    mDrawable.setColorFilter(cf);
    ---- end ----
    
    
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380