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 
      
    //图形上下有30内边距,左右无内边距,会居中显示。
    1. scaleType=centerCrop, padding=30dp;   
      
    
    //图形的中心在控件的中心右15dp位置,不会居中显示,也没有内边距。
    2.  scaleType=centerCrop, 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 ----
    
    

你可能感兴趣的:(ImageView源码分析)