Android屏幕适配原理解析

最近在玩Android看到一篇文章一种极低成本的Android屏幕适配方式。细细阅读发现,其适配原理主要是根据dp/sp与px的转换,而dp/sp与px的转换又与DisplayMetrics的density相关,所以可以通过改变DisplayMetrics的density,scaledDensity和densityDpi的值来适配不同分辨率机型。这其中是怎么做到的呢,本篇博文将会从源码的角度来分析。

在开始分析之前,我们需要了解一些概念,如:

  • DisplayMetrics:是Android屏幕显示的信息描述,如尺寸size,密度desity,字体缩放值font scaling
  • Resources:是应用的资源管理类(High level api),可以调用应用中的color,string,drawable等资源
  • AssetManager:是可以直接访问应用的raw资源(Low level api)
  • BitmapFactory: Bitmap工厂类,可以根据多种来源创建Bitmap。

在说Android适配原理之前,我们先来了解一些基础概念。

一、基础概念

1. dip/dp

是Density independent pixel的缩写,指的是抽象意义上的像素。跟设备的屏幕密度有关系。它是Android里的一个单位,dip和dp是一样的。

2. sp

scale-independent pixel,安卓开发用的字体大小单位。

3.px

想像把屏幕放大再放大,对!看到的那一个个小点或者小方块就是像素了。

4.dpi

是dot per inch的缩写,就是每英寸的像素数,也叫做屏幕密度。这个值越大,屏幕就越清晰。iPhone5S的dpi是326; Samsung Note3 的dpi是386

5.分辨率

是指屏幕上垂直方向和水平方向上的像素个数。比如iPhone5S的分辨率是1136*640;Samsung Note3的分辨率是1920*1080;

6.屏幕尺寸(screen size)

就是我们平常讲的手机屏幕大小,是屏幕的对角线长度,一般讲的大小单位都是英寸。在api版本13之前(3.2),屏幕被分成四大组:small,normal,large,xlarge。但是在13往后,可以支持更加精确的屏幕区分:sw600dp,sw720dp,w600dp等。

二、dp/sp与px转换原理分析

转换公式

源码路径:frameworks/base/core/java/android/util/TypedValue.java

    public static float applyDimension(int unit, float value,
                                       DisplayMetrics metrics)
    {
        switch (unit) {
        case COMPLEX_UNIT_PX:
            return value;
        case COMPLEX_UNIT_DIP://1.dp转换为px
            return value * metrics.density;
        case COMPLEX_UNIT_SP://2.sp转换为px
            return value * metrics.scaledDensity;
        case COMPLEX_UNIT_PT:
            return value * metrics.xdpi * (1.0f/72);
        case COMPLEX_UNIT_IN:
            return value * metrics.xdpi;
        case COMPLEX_UNIT_MM:
            return value * metrics.xdpi * (1.0f/25.4f);
        }
        return 0;
    }

由上知px = dp * metrics.density和px = sp * metrics.scaledDensity。根据google官方建议,我们主要都是用dp和sp,而这两个单位,最后都会转化为px(像素),在不同的设备中,决定px转化的大小是metrics.density和metrics.scaledDensity,所以这里我们具体来看看这两个变量,具体我们来看看DisplayMetrics源码:

源码路径:frameworks/base/core/java/android/util/DisplayMetrics.java


    public class DisplayMetrics {

    public static final int DENSITY_LOW = 120;

    public static final int DENSITY_MEDIUM = 160;

    public static final int DENSITY_TV = 213;

    public static final int DENSITY_HIGH = 240;

    public static final int DENSITY_280 = 280;

    public static final int DENSITY_XHIGH = 320;

    public static final int DENSITY_360 = 360;

    public static final int DENSITY_400 = 400;

    public static final int DENSITY_420 = 420;

    public static final int DENSITY_XXHIGH = 480;

    public static final int DENSITY_560 = 560;

    public static final int DENSITY_XXXHIGH = 640;

    public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;

    public static final float DENSITY_DEFAULT_SCALE = 1.0f / DENSITY_DEFAULT;

    /**
     * The device's density.
     * @hide because eventually this should be able to change while
     * running, so shouldn't be a constant.
     * @deprecated There is no longer a static density; you can find the
     * density for a display in {@link #densityDpi}.
     */
    @Deprecated
    public static int DENSITY_DEVICE = getDeviceDensity();

    /**
     * The absolute width of the display in pixels.
     */
    public int widthPixels;
    /**
     * The absolute height of the display in pixels.
     */
    public int heightPixels;
    /**
     * The logical density of the display.  This is a scaling factor for the
     * Density Independent Pixel unit, where one DIP is one pixel on an
     * approximately 160 dpi screen (for example a 240x320, 1.5"x2" screen), 
     * providing the baseline of the system's display. Thus on a 160dpi screen 
     * this density value will be 1; on a 120 dpi screen it would be .75; etc.
     *  
     * <p>This value does not exactly follow the real screen size (as given by 
     * {@link #xdpi} and {@link #ydpi}, but rather is used to scale the size of
     * the overall UI in steps based on gross changes in the display dpi.  For 
     * example, a 240x320 screen will have a density of 1 even if its width is 
     * 1.8", 1.3", etc. However, if the screen resolution is increased to 
     * 320x480 but the screen size remained 1.5"x2" then the density would be 
     * increased (probably to 1.5).
     *
     * @see #DENSITY_DEFAULT
     */
    public float density;
    /**
     * The screen density expressed as dots-per-inch.  May be either
     * {@link #DENSITY_LOW}, {@link #DENSITY_MEDIUM}, or {@link #DENSITY_HIGH}.
     */
    public int densityDpi;
    /**
     * A scaling factor for fonts displayed on the display.  This is the same
     * as {@link #density}, except that it may be adjusted in smaller
     * increments at runtime based on a user preference for the font size.
     */
    public float scaledDensity;

    ......

    public void setToDefaults() {
        widthPixels = 0;
        heightPixels = 0;
        density =  DENSITY_DEVICE / (float) DENSITY_DEFAULT;//1.desity的赋值
        densityDpi =  DENSITY_DEVICE;
        scaledDensity = density;
        xdpi = DENSITY_DEVICE;
        ydpi = DENSITY_DEVICE;
        ......
    }

    ......

    private static int getDeviceDensity() {
        // qemu.sf.lcd_density can be used to override ro.sf.lcd_density
        // when running in the emulator, allowing for dynamic configurations.
        // The reason for this is that ro.sf.lcd_density is write-once and is
        // set by the init process when it parses build.prop before anything else.
        return SystemProperties.getInt("qemu.sf.lcd_density",
                SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
    }
}

由上面的代码知,默认情况下,metrics.density和metrics.scaledDensity是相等的,并且有metrics.density = DENSITY_DEVICE / (float) DENSITY_DEFAULT,其中DENSITY_DEVICE = getDeviceDensity(),DENSITY_DEFAULT = DENSITY_MEDIUM = 160,我们来看看一下获取设备Density方法getDeviceDensity():

    private static int getDeviceDensity() {
        // qemu.sf.lcd_density can be used to override ro.sf.lcd_density
        // when running in the emulator, allowing for dynamic configurations.
        // The reason for this is that ro.sf.lcd_density is write-once and is
        // set by the init process when it parses build.prop before anything else.
        return SystemProperties.getInt("qemu.sf.lcd_density",
                SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
    }

此方法通过调用原生方法SystemProperties.getInt(“qemu.sf.lcd_density”,SystemProperties.getInt(“ro.sf.lcd_density”, DENSITY_DEFAULT))从而获得设备Density,通过研究分析知,这里是调用底层C的代码,我们继续来看:

源码路径:android\external\qemu\android文件夹下的hw-lcd.c和hw-lcd.h


void hwLcd_setBootProperty(int density)
{
    char  temp[8];

    /* Map density to one of our five bucket values.
       The TV density is a bit particular (and not actually a bucket
       value) so we do only exact match on it.
    */
    if (density != LCD_DENSITY_TVDPI) {
        if (density < (LCD_DENSITY_LDPI + LCD_DENSITY_MDPI)/2)
            density = LCD_DENSITY_LDPI;
        else if (density < (LCD_DENSITY_MDPI + LCD_DENSITY_HDPI)/2)
            density = LCD_DENSITY_MDPI;
        else if (density < (LCD_DENSITY_HDPI + LCD_DENSITY_XHDPI)/2)
            density = LCD_DENSITY_HDPI;
        else
            density = LCD_DENSITY_XHDPI;
    }
    snprintf(temp, sizeof temp, "%d", density);
    boot_property_add("qemu.sf.lcd_density", temp);
}

此方法主要就是向设备添加参数为”qemu.sf.lcd_density”的值,然后通过SystemProperties.getInt(“qemu.sf.lcd_density”,”“),就可以获取到此值。通过此方法,设备会返回系统规定好的值,其中LCD_DENSITY_LDPI为120,LCD_DENSITY_MDPI为160,LCD_DENSITY_HDPI为240,LCD_DENSITY_XHDPI为320等,通过ppi公式算出的值desityDpi不是最终的desityDpi,为了统一,为了drawable-ldpi,drawable-mdpi,drawable-hdpi,drawable-xhdpi,drawable-xxhdpi等图片资源获取,这里系统做了一下处理,以保证不同的设备返回的值在相对应的区间范围。

三、图片资源加载原理分析

加载本地资源图片方法有getDrawable()和decodeResource(Resources res, int id),我们先来分析第一个方法,我们知道getDrawable()是Resources类中的方法,所以我们来看看此类

1.getDrawable()方法
源码路径:frameworks/base/core/java/android/content/res/Resource.java

public class Resources {

    public Drawable getDrawable(int id) throws NotFoundException {
        synchronized (mTmpValue) {
            TypedValue value = mTmpValue;
            getValue(id, value, true);
            return loadDrawable(value, id);
        }
    }

 Drawable loadDrawable(TypedValue value, int id)
            throws NotFoundException {

                ......

                if (file.endsWith(".xml")) {//xml中获取图片
                    try {
                        XmlResourceParser rp = loadXmlResourceParser(
                                file, id, value.assetCookie, "drawable");
                        dr = Drawable.createFromXml(this, rp);
                        rp.close();
                    } catch (Exception e) {
                        NotFoundException rnf = new NotFoundException(
                            "File " + file + " from drawable resource ID #0x"
                            + Integer.toHexString(id));
                        rnf.initCause(e);
                        throw rnf;
                    }

                } else {//代码中获取图片
                    try {
                        InputStream is = mAssets.openNonAsset(
                                value.assetCookie, file, AssetManager.ACCESS_STREAMING);
        //                System.out.println("Opened file " + file + ": " + is);
                        dr = Drawable.createFromResourceStream(this, value, is,
                                file, null);//核心代码
                        is.close();
        //                System.out.println("Created stream: " + dr);
                    } catch (Exception e) {
                        NotFoundException rnf = new NotFoundException(
                            "File " + file + " from drawable resource ID #0x"
                            + Integer.toHexString(id));
                        rnf.initCause(e);
                        throw rnf;
                    }
                }
            }
        }
        ......

        return dr;
    }
}

我们主要来看从代码中获取图片,我们继续来看看核心代码Drawable.createFromResourceStream(this, value, is,file, null):

    public static Drawable createFromResourceStream(Resources res, TypedValue value,
            InputStream is, String srcName, BitmapFactory.Options opts) {
        .......
        if (opts == null) opts = new BitmapFactory.Options();
        opts.inScreenDensity = res != null
                ? res.getDisplayMetrics().noncompatDensityDpi : DisplayMetrics.DENSITY_DEVICE;
        Bitmap  bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts);//核心代码
        if (bm != null) {
            byte[] np = bm.getNinePatchChunk();
            if (np == null || !NinePatch.isNinePatchChunk(np)) {
                np = null;
                pad = null;
            }

            final Rect opticalInsets = new Rect();
            bm.getOpticalInsets(opticalInsets);
            return drawableFromBitmap(res, bm, np, pad, opticalInsets, srcName);
        }
        return null;
    }

由上我们,继续来看看 BitmapFactory.decodeResourceStream(res, value, is, pad, opts)方法:

    public static Bitmap decodeResourceStream(Resources res, TypedValue value,
            InputStream is, Rect pad, Options 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;//Android设备的densityDpi
        }

        return decodeStream(is, pad, opts);
    }

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

由上知,当传入Android设备的相关密度后,最后调用了原生方法nativeDecodeAsset()从而获取本地相关资源图片。

2.decodeResource(Resources res, int id)方法
此方法主要是BitmapFactory中的方法,所以我们主要来看此类


    public static Bitmap decodeResource(Resources res, int id) {
        return decodeResource(res, id, null);
    }

    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) {
            /*  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;
    }

   public static Bitmap decodeResourceStream(Resources res, TypedValue value,
            InputStream is, Rect pad, Options 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);
    }

由上易发现,最后也都是调用了原生方法nativeDecodeAsset()从而获取本地相关资源图片。

上面两种方法获取应用资源图片,其中都传入了Android的densityDpi密度值,然后再通过原生返回相关图片。为什么要传入Android设备的密度值,因为为了适配多个屏幕,这里就涉及到了图片资源的缩放。我们知道Android项目有多个图片文件夹,如drawable-ldpi,drawable-mdpi,drawable-hdpi,drawable-xhdpi,drawable-xxhdpi等,其对应的设备密度为120,160,240,320,480等。

通过实际Demo测试,一张分辨率为60x60的图片,如果放在drawable-xhdpi中,测试机的密度值为480,在其测试机上显示的图片分辨率为90x90,其缩放比值为480/320=1.5;如果在测试机密度为240,在其测试机上的显示图片分辨率为45x45,其缩放比值为240/320 = 0.75;

由此我们知道,不同文件夹下的图片,在高密度的手机上是放大,在低密度的手机上是缩小

最后附上一种极低成本的Android屏幕适配方式解决方式的源码:

/**
 * Describe: 屏幕适配方案
 *
 * 1.设计以1080*1920(px)为标准,换成dp为360*640(dp)
 * 2.其他分辨率按宽为360dp为标准,density = displayWidth/360,保证所有机型宽都能铺满屏幕
 *
 * Created by AwenZeng on 2018/6/14.
 */
public class AutoScreenUtils {

    private static float originalScaledDensity;

    private static final int DEFAULT_STANDARD = 360;//默认标准

    public static void AdjustDensity(final Application application) {
        final DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
        final float originalDensity = displayMetrics.density;
        originalScaledDensity = displayMetrics.scaledDensity;
        application.registerComponentCallbacks(new ComponentCallbacks() {
            @Override
            public void onConfigurationChanged(Configuration newConfig) {
                if (newConfig != null && newConfig.fontScale > 0) {
                    originalScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                }
            }
            @Override
            public void onLowMemory() {
            }
        });

        float targetDensity = (float)displayMetrics.widthPixels / DEFAULT_STANDARD;
        float targetScaledDensity = targetDensity * (originalScaledDensity / originalDensity);
        int targetDensityDpi = (int) (160 * targetDensity);
        displayMetrics.density = targetDensity;
        displayMetrics.scaledDensity = targetScaledDensity;
        displayMetrics.densityDpi = targetDensityDpi;

        DisplayMetrics activityDisplayMetrics = application.getResources().getDisplayMetrics();
        activityDisplayMetrics.density = targetDensity;
        activityDisplayMetrics.scaledDensity = targetScaledDensity;
        activityDisplayMetrics.densityDpi = targetDensityDpi;

        application.registerActivityLifecycleCallbacks(new CreateActivityLifecycle() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                float targetDensity = (float)displayMetrics.widthPixels / DEFAULT_STANDARD;
                float targetScaledDensity = targetDensity * (originalScaledDensity / originalDensity);
                int targetDensityDpi = (int) (160 * targetDensity);
                displayMetrics.density = targetDensity;
                displayMetrics.scaledDensity = targetScaledDensity;
                displayMetrics.densityDpi = targetDensityDpi;

                DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
                activityDisplayMetrics.density = targetDensity;
                activityDisplayMetrics.scaledDensity = targetScaledDensity;
                activityDisplayMetrics.densityDpi = targetDensityDpi;
            }
        });

    }

    private static abstract class CreateActivityLifecycle implements Application.ActivityLifecycleCallbacks {

        @Override
        public void onActivityStarted(Activity activity) {

        }

        @Override
        public void onActivityResumed(Activity activity) {

        }

        @Override
        public void onActivityPaused(Activity activity) {

        }

        @Override
        public void onActivityStopped(Activity activity) {

        }

        @Override
        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

        }

        @Override
        public void onActivityDestroyed(Activity activity) {

        }
    }

}

注:源码采用android-4.1.1_r1版本,建议下载源码然后自己走一遍流程,这样更能加深理解。

四、参考文档

一种极低成本的Android屏幕适配方式

dpi 、 dip 、分辨率、屏幕尺寸、px、density 关系以及换算

UI之支持多屏幕

Android屏幕适配及DisplayMetrics解析

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页