preface

When using ImageView and setting the width and height to wrAP_content and setting the bitmap, have you ever wondered how the size is calculated and how density relates to the final image size? With a rigorous attitude, I began to explore the source code interpretation of the road of no return.

process

The density of the test machine was 420. Let’s start by decoding a bitmap (IC_launcher size: 144 * 144) with the following code:

      val options = BitmapFactory.Options()
      val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options)
      Log.d("Bitmap", "{height: ${bitmap.height}  ---  width: ${bitmap.width}}")
Copy the code

{height: 126 — width: 126} We took a look inside decodeResource,

public static Bitmap decodeResource(Resources res, int id, Options opts) { validate(opts); Bitmap bm = null; InputStream is = null; try { final TypedValue value = new TypedValue(); is = res.openRawResource(id, value); bm = decodeResourceStream(res, value, is, null, opts); } catch (Exception e) { /* do nothing. If the exception happened on open, bm will be null. If it happened on close, bm is still valid. */ } finally { try { if (is ! = null) is.close(); } catch (IOException e) { // Ignore } } if (bm == null && opts ! = null && opts.inBitmap ! = null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } return bm; }Copy the code

Bitmap is created by decodeResourceStream, so let’s move on,

@Nullable public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value, @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) { validate(opts); if (opts == null) { opts = new Options(); } if (opts.inDensity == 0 && value ! = null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density ! = TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res ! = null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); }Copy the code

If options.inDensity = 0, the value of options will be assigned. InDensity refers to the density of the xhdPI file. InTargetDensity refers to the target density (DPI) of the mobile phone screen. In this experiment, the original density of the resource is 480 and the target density is 420. After the assignment, we move on.

@Nullable public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding, @Nullable Options opts) { // we don't throw in this case, thus allowing the caller to only check // the cache, and not force the image to be decoded. if (is == null) { return null; } validate(opts); Bitmap bm = null; Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap"); try { if (is instanceof AssetManager.AssetInputStream) { final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset(); bm = nativeDecodeAsset(asset, outPadding, opts); } else { bm = decodeStreamInternal(is, outPadding, opts); } if (bm == null && opts ! = null && opts.inBitmap ! = null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } setDensityFromOptions(bm, opts); } finally { Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS); } return bm; }Copy the code

What is done here is to call the native method for decoding, without looking further down. The original size is 144, the decoded size is 126, the inDensity is 480, and the inTargetDensity is 420. In other words, targetSize = rawSize * targetDensity/rawDensity, which is also very easy to understand, is to scale the image, the scaling basis is to adapt to the density of the current phone. Is it possible to change the size of the image decoder? Sure, here’s the code:

val options = BitmapFactory.Options() options.inTargetDensity = 480 val bitmap = BitmapFactory.decodeResource(resources,  R.mipmap.ic_launcher, options) Log.d("Bitmap", "{height: ${bitmap.height} --- width: ${bitmap.width}}")Copy the code

{height: 144 — width: 144}; height: 144 — width: 144}; We then modify the options, this time as follows:

      val options = BitmapFactory.Options()
      options.inDensity = 240
      options.inTargetDensity = 480
      val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options)
      Log.d("Bitmap", "{height: ${bitmap.height}  ---  width: ${bitmap.width}}")
Copy the code

If you do it in your head, you get 288. This time, we changed the density of image resources to affect the size generated by the decoding of bitmap. The size of the ImageView is the same as the size of the bitmap.

      val options = BitmapFactory.Options()
      val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options)
      Log.d("Bitmap", "{height: ${bitmap.height}  ---  width: ${bitmap.width}}")
      image_view.setImageBitmap(bitmap)
      image_view.viewTreeObserver.addOnPreDrawListener {
        Log.d("ImageView", "{height: ${image_view.height}  ---  width: ${image_view.width}}")
        true
      }
Copy the code

InTargetDensity = 480, imageView = 126, bitmap = 144 Take a look at the code. Start with setImageBitmap as follows:

    public void setImageBitmap(Bitmap bm) {
        // Hacky fix to force setImageDrawable to do a full setImageDrawable
        // instead of doing an object reference comparison
        mDrawable = null;
        if (mRecycleableBitmapDrawable == null) {
            mRecycleableBitmapDrawable = new BitmapDrawable(mContext.getResources(), bm);
        } else {
            mRecycleableBitmapDrawable.setBitmap(bm);
        }
        setImageDrawable(mRecycleableBitmapDrawable);
    }

Copy the code

You can see that the internal bitmap is actually loaded into the BitmapDrawable, so look further:

public void setImageDrawable(@Nullable Drawable drawable) { if (mDrawable ! = drawable) { mResource = 0; mUri = null; final int oldWidth = mDrawableWidth; final int oldHeight = mDrawableHeight; updateDrawable(drawable); if (oldWidth ! = mDrawableWidth || oldHeight ! = mDrawableHeight) { requestLayout(); } invalidate(); }}Copy the code

The key code is updateDrawable, in addition to the old width and height to determine whether to re-requestLayout. Look at the updateDrawable code,

private void updateDrawable(Drawable d) { if (d ! = mRecycleableBitmapDrawable && mRecycleableBitmapDrawable ! = null) { mRecycleableBitmapDrawable.setBitmap(null); } boolean sameDrawable = false; if (mDrawable ! = null) { sameDrawable = mDrawable == d; mDrawable.setCallback(null); unscheduleDrawable(mDrawable); if (! sCompatDrawableVisibilityDispatch && ! sameDrawable && isAttachedToWindow()) { mDrawable.setVisible(false, false); } } mDrawable = d; if (d ! = null) { d.setCallback(this); d.setLayoutDirection(getLayoutDirection()); if (d.isStateful()) { d.setState(getDrawableState()); } if (! sameDrawable || sCompatDrawableVisibilityDispatch) { final boolean visible = sCompatDrawableVisibilityDispatch ? getVisibility() == VISIBLE : isAttachedToWindow() && getWindowVisibility() == VISIBLE && isShown(); d.setVisible(visible, true); } d.setLevel(mLevel); mDrawableWidth = d.getIntrinsicWidth(); mDrawableHeight = d.getIntrinsicHeight(); applyImageTint(); applyColorMod(); configureBounds(); } else { mDrawableWidth = mDrawableHeight = -1; }}Copy the code

There are a couple of key things, one is the assignment of drawable, the other is

   mDrawableWidth = d.getIntrinsicWidth();
   mDrawableHeight = d.getIntrinsicHeight();
   configureBounds();
Copy the code

Assign the width and height of the drawable, and then resize the bound. The configureBounds method has a lot of code. Here’s the most important part.

        final int dwidth = mDrawableWidth;
        final int dheight = mDrawableHeight;
        mDrawable.setBounds(0, 0, dwidth, dheight);
Copy the code

The ImageView’s width and height are determined by the above d.goetintrinsicWidth () and d.goetintrinsicHeight (), so the key is to solve the problem with these two methods. Since the implementation class of drawable here is BitmapDrawable, the implementation method of BitmapDrawable needs to be viewed as follows

    @Override
    public int getIntrinsicWidth() {
        return mBitmapWidth;
    }

    @Override
    public int getIntrinsicHeight() {
        return mBitmapHeight;
    }
Copy the code

Okay, we’re close to victory, look at the mBitmapWidth assignment,

    private void computeBitmapSize() {
        final Bitmap bitmap = mBitmapState.mBitmap;
        if (bitmap != null) {
            mBitmapWidth = bitmap.getScaledWidth(mTargetDensity);
            mBitmapHeight = bitmap.getScaledHeight(mTargetDensity);
        } else {
            mBitmapWidth = mBitmapHeight = -1;
        }
    }
Copy the code

Keep smiling 😊, one step closer to the result,

  public int getScaledHeight(int targetDensity) {
        return scaleFromDensity(getHeight(), mDensity, targetDensity);
    }

    /**
     * @hide
     */
    static public int scaleFromDensity(int size, int sdensity, int tdensity) {
        if (sdensity == DENSITY_NONE || tdensity == DENSITY_NONE || sdensity == tdensity) {
            return size;
        }

        // Scale by tdensity / sdensity, rounding up.
        return ((size * tdensity) + (sdensity >> 1)) / sdensity;
    }
Copy the code

The bitmapDrawable drawn to the ImageView will scale the bitmap again. The scale is inDensity and targetDensity, but inDensity is the density of the bitmap. If options are not set, the density of the bitmap is the density of the image resource folder. In this case, the density of the bitmap is 480.

    state.mTargetDensity = Drawable.resolveDensity(r, 0);
    static int resolveDensity(@Nullable Resources r, int parentDensity) {
        final int densityDpi = r == null ? parentDensity : r.getDisplayMetrics().densityDpi;
        return densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
    }

Copy the code

Here it is clear that targetDensity is equal to the density of the device, i.e. 420. This is the same as the default bitmap scaling configuration. Although we changed the bitmap scaling configuration, it did not affect the bitmapDrawable configuration. So the size of the BitmapDrawable is 144 * 420/480 = 126. The size of the image can be changed by modifying the inDensity of the BitmapDrawable.

      val options = BitmapFactory.Options()
      options.inDensity = 240
      options.inTargetDensity = 480
      val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options)
      Log.d("Bitmap", "{height: ${bitmap.height}  ---  width: ${bitmap.width}}")
      image_view.setImageBitmap(bitmap)
      image_view.viewTreeObserver.addOnPreDrawListener {
        Log.d("ImageView", "{height: ${image_view.height}  ---  width: ${image_view.width}}")
        true
      }
Copy the code

Dang dang dang, elementary school math problem, the result is 256, because the denominator is reduced by half, so it’s equivalent to twice. The targetDensity code of BitmapDrawable can be modified.

     val options = BitmapFactory.Options()
      val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher, options)
      Log.d("Bitmap", "{height: ${bitmap.height}  ---  width: ${bitmap.width}}")

      val bitmapDrawable = BitmapDrawable(resources, bitmap)
      bitmapDrawable.setTargetDensity(480)
      image_view.setImageDrawable(bitmapDrawable)

      image_view.viewTreeObserver.addOnPreDrawListener {
        Log.d("ImageView", "{height: ${image_view.height}  ---  width: ${image_view.width}}")
        true
      }
Copy the code

What? You want results? It’s a simple question.


Okay, between you and me, it’s actually 144.

conclusion

  • For Bitmap, the size is equal to rawSize * targetDensity/rawDensity, where targetDensity is the targetDensity and rawDensity is the density of the original resource, If the density of image resources is too small, the decoded bitmap will be enlarged, resulting in an increase in memory. After all, the area after decoding will become larger. The amount of memory per unit area remains the same.
  • For ImageView, we know that even if we scale the Bitmap, the drawable in memory will be scaled again to fit the actual size. The scaling ratio can be controlled by modifying targetDensity and inDensity.
  • Ok, this time to share the end of this, like a thumbs-up, or we discuss.