天天看点

Android多分辨率适配原理

Android常用度量单位:

  1. px:是Pixel的缩写,也就是说像素
  2. inch:是指英寸,设备对角线的长度
  3. dpi:它表示每英寸上的像素点个数,也就是屏幕密度。例如手机分辨率为1920*1080,先利用勾股定理得其对角线的像素值为2202.91,再除以对角线的长度5,计算出440.582便是该设备的屏幕密度dpi。
  4. dp:android中常用的使用单位,不要与dpi混淆,下面会详细介绍dp

    ldpi、mdpi、hdpi、xhdpi、xxhdpi 这在android开发中非常常见,android根据dpi把设备分成多个级别,详细关系见下图:

    Android多分辨率适配原理
    可以通过代码获取设备dpi:
Resources resources=getResources();
    DisplayMetrics displayMetrics = resources.getDisplayMetrics();
    float density = displayMetrics.density;
    int dpi = displayMetrics.densityDpi;
           

假设当前设备dpi为480,density就为3。density是一个倍数关系,是当前设备dpi除以160所得。当设备dpi为160时,1px=1dp,所以dpi为480时 1dp=3px

图片加载:

通常为了适配以及防止图片失真,会在mdpi,xhdpi,xxhdpi这类文件夹下分别放入对应的尺寸的图。那么为什么需要这样放,如果把本应该放入xxhdpi的图放入mdpi会发生什么呢

经过测试会发现 假设你的手机显示类别为xxhdpi,图片尺寸100*100.把它放入xxhdpi文件中,然后通过下面代码获取图片宽高:

BitmapDrawable bitmapDrawable = (BitmapDrawable) imageview.getDrawable();
            if (null != bitmapDrawable) {
                Bitmap bitmap = bitmapDrawable.getBitmap();
                int width = bitmap.getWidth();
                int height = bitmap.getHeight();
            }
           

打印宽高会发现和图片原始尺寸一下是100*100.然后把图片移到mdpi中,运行后会发现图片被放大了3倍,变成了300*300。

从图片加载源码中找找关系,首先来看BitmapFactory中的的decodeResource()方法:

public static Bitmap decodeResource(Resources res, int id, Options 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) {

    } finally {
        try {
            if (is != null) is.close();
        } catch (IOException e) {

        }
    }

    if (bm == null && opts != null && opts.inBitmap != null) {
        throw new IllegalArgumentException("Problem decoding into existing bitmap");
    }
    return bm;
}
           

typevalue保存了和当前设备dpi最接近的图片所在文件夹的destinydpi。比如图片在xxhdpi中 desinydpi就是480.接着看decodeResourceStream方法:

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
        InputStream is, Rect pad, Options opts) {
    if (opts == null) {
        opts = new Options();
    }
    if (opts.inDensity ==  && 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 ==  && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }     
    return decodeStream(is, pad, opts);
}
           

value.density被赋值给opts.inDensity,保存了图片所在文件夹的dpi 如果放在xxhdpi中 那就是480

opts.inTargetDensity = res.getDisplayMetrics().densityDpi 可以看出inTargetDensity保存的当前设备的dpi。

接着往下看decodeStream方法:

public static Bitmap decodeStream(InputStream is, Rect outPadding, 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;
        }

        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;
    }
           

在该方法中会调用decodeStreamInternal();它又会继续调用nativeDecodeStream( ),该方法是native的;在BitmapFactory.cpp可见这个方法内部又调用了doDecode()它的核心源码如下:

static jobject doDecode(JNIEnv*env,SkStreamRewindable*stream,jobject padding,jobject options) {
......
    if (density !=  && targetDensity !=  && density != screenDensity) {
        scale = (float) targetDensity / density;
    }
......

if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + f);
scaledHeight = int(scaledHeight * scale + f);
}
if (willScale) {
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
......
SkPaint paint;
SkCanvas canvas(*outputBitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(decodingBitmap, f, f, &paint);
}
......
}
           

可以看到缩放比scale就等于opts.inTargetDensity/opts.inDensity,也就是屏幕dpi除以图片所在文件夹dpi,所以如果把原本在xxhdpi(480)文件夹中图片误放到mdpi(160)中 opts.inTargetDensity/opts.inDensity值就会增大,所以图片被放大到3倍。

正因为会自动选择缩放图片 我们只在xxhdpi中放了一套图 使用mdpi的手机来显示图片就会自动把图片缩小。所以现在很多开发者为了缩小apk大小会选择只在最大众的文件夹中放入一套图 比如xxhdpi。

如果不想让图片被缩放 可以试试把图片放到drawable-nodpi文件夹中哦。

继续阅读