最近在玩Android看到一篇文章一种极低成本的Android屏幕适配方式。细细阅读发现,其适配原理主要是根据dp/sp与px的转换,而dp/sp与px的转换又与DisplayMetrics的density相关,所以可以通过改变DisplayMetrics的density,scaledDensity和densityDpi的值来适配不同分辨率机型。这其中是怎么做到的呢,本篇博文将会从源码的角度来分析。
在开始分析之前,我们需要了解一些概念,如:
在说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等。
转换公式
源码路径: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.
*
* 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解析