美文网首页
ImageView源码分析

ImageView源码分析

作者: 凤鸣游子 | 来源:发表于2020-05-31 17:55 被阅读0次
    美图欣赏
    冬季里的黄山
    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 ----
      
      

    相关文章

      网友评论

          本文标题:ImageView源码分析

          本文链接:https://www.haomeiwen.com/subject/aydrzhtx.html