本文首发于个人微信公众号:Myoung。
欢迎大家订阅,点赞、留言。

mipmap图片

如果做 App 开发的话,AS 会在项目的 res 目录下,创建几个以 mipmap 开头的文件夹:
根据 Android 官方描述:
  • Mipmap 仅仅用于存放 App 启动图标。
  • AS 提供了一个快捷工具 Image Asset Studio 用于生成图标。
  • Image Asset Studio会生成mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi五种尺寸的图标。

图标最好不要随意定义尺寸,分辨率过低会模糊,过高徒增APK包大小。各种密度下的图标建议尺寸为
密度
建议尺寸
mdpi
48*48
hdpi
72*72
xhdpi
96*96
xxhdpi
144*144
xxxhdpi
192*192
如果要上传到Google Play,还需要一张512*512的图片用于Google Play Store。


drawable图片

相关概念

  • dpi
每英寸点数,全称dots per inch。用来表示屏幕密度,即屏幕物理区域中的像素量。高密度屏幕比低密度屏幕在给定物理区域的像素要多。

  • dp
即dip,全称device independent pixel。设备独立像素,是一种虚拟像素单位,用于以密度无关方式表示布局维度或位置,以确保在不同密度的屏幕上正常显示UI。在160dpi的设备上,1dp=1px。

  • density
设备的逻辑密度,是dip的缩放因子。以160dpi的屏幕为基线,density=dpi/160。
getResources().getDisplayMetrics().density

  • sp
缩放独立像素,全称scale independent pixel。类似于dp,一般用于设置字体大小,可以根据用户设置的字体大小偏好来缩放。

六种通用密度

Android系统为了简化开发者为多种屏幕设计用户界面的方式,Android将实际屏幕尺寸和范围作了通用规定,称作“根据可用屏幕宽度管理屏幕尺寸的新技术”。

六种通用密度为:
密度
dpi范围
ldpi(低)
~120dpi
mdpi(中)
~160dpi
hdpi(高)
~240dpi
xhdpi(超高)
~320dpi
xxhdpi(超超高)
~480dpi
xxxhdpi(超超超高)
~640dpi
通用密度是以mdpi(中)为基线配置的,此基线基于第一代Android设备(T-Mobile G1)的屏幕配置。


Android系统适配原则

目的:
  • Android 为了更好地优化应用在不同屏幕密度下的用户体验。

提供的方法:
  • 在项目的res目录下可以创建 drawab-[density]目录。
  • density为6种通用密度名。
  • Android系统会依据特定的原则来查找各drawable目录下的图片。

开发者:
  • 在进行APP开发时,针对不同的屏幕密度,将图片放置于对应的drawable-[density]目录。

原则:

总结:
优先匹配最适合的图片 → 查找密度高的目录(升序)→ 查找nodpi目录 → 查找密度低的目录(降序)。


Android 在查找到图片后会根据当前设备的dpi对drawable-[density]目录中的图片进行缩放,那么什么情况下图片被放大,什么情况下图片被缩小呢?

图片的缩放

缩放规律

  • 匹配目录
符合当前设备dpi的drawable目录。比如:
    • dpi=320,匹配目录为drawable-xhdpi。
    • dpi=150,匹配目录为drawable-mdpi。

  • 图片的缩放规律
    • 如果图片所在目录为匹配目录,则图片会根据设备dpi做适当的缩放调整。
    • 如果图片所在目录dpi低于匹配目录,那么该图片被认为是为低密度设备需要的,现在要显示在高密度设备上,图片会被放大。
    • 如果图片所在目录dpi高于匹配目录,那么该图片被认为是为高密度设备需要的,现在要显示在低密度设备上,图片会被缩小。
    • 如果图片所在目录为drawable-nodpi,则无论设备dpi为多少,保留原图片大小,不进行缩放。

缩放倍数

  • 缩放倍数
以mdpi为基线,各密度目录下的放大倍数(即缩放因子density)如下:
密度
放大倍数
ldpi
0.75
mdpi
1.0
hdpi
1.5
xhdpi
2.0
xxhdpi
3.0
xxxhdpi
4.0

  • 更通用的缩放倍数计算公式
对于很多设备,其dpi并不刚好是六种通用密度最大dpi,这种情况下,各drawable-[density]目录下的图片放大倍数的计算公式:


验证

  • 验证缩放
设备:1080×1920 - 420dpi
  • 验证倍数
材料:在Sketch里简单绘制一张图,分别导出一倍图(1x)和三倍图(3x)。大小如下:

设备:1080×1920 - 420dpi
方法:分别把1倍图和3倍图置于drawable-mdpi和drawable-xxhdpi目录下
结果:

    • drawable-mdpi
scale = 420/160
1x:98 × scale = 98 × 420/160 = 257
3x:294 × scale = 294 × 420/160 = 772

    • drawable-xxhdpi
scale = 420/480
1x:98 × scale = 98 × 420/480 = 86
3x:294 × scale = 294 × 420/480 = 257

像素没有小数。
drawable-xxhdpi目录下,图片的宽高更接近于图片实际大小。


那么问题来了,Android上图片的缩放算法是怎样的呢?

Android图片压缩

质量压缩

更改图片的显示质量的一种压缩方式。

基本概念

  • 三原色:RGB
    • RGB通道
    • alpha通道

  • 位深
    • 8位:2^8 = 2^2(B) 2^3(G) 2^3(R) = 256
    • 16位:2^16 = 2^5(B) 2^6(G) 2^5(R) =  65536
    • 24位:2^24 = 2^8(B) 2^8(G) 2^8(R) =  16777216
    • 32位:alpha透明度 + 24位

位深越高,色彩越逼真。


压缩原理

不改变像素的前提下,通过改变位深来改变图片文件大小

    • 色彩逼真度下降,即失真(质量下降)。
    • 压缩比 1~100。
    • 压缩有三种格式:.JPEG、.PNG、.WEBP。

Android实现质量压缩:
//quality 为0~100,0表示最小体积,100表示最高质量,对应体积也是最大
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
其中,PNG无法压缩。

Android代码流程:
Bitmap(Java)→ Bitmap_compress(cpp)→ Skia 图形引擎 → 第三方库
    • libpng
    • libjpeg
    • libgif

再进一步的话,Android使用的其实是 Standard Huffman 算法。
    • Huffman table
    • optimize_coding

尺寸压缩

通过改变图片的像素尺寸来压缩图片的方式,即重新采样。
    • 上采样:方法图像
    • 下采样:缩小图像

Android中图像采用的两种方式:
    • 邻近采样(Nearest Neighbour Resampling)
    • 双线性采样(Bilinear Resampling)

邻近采样

邻近采样主要就是 inSampleSize 的使用:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
Bitmap compressBitmap = BitmapFactory.decodeFile("/sdcard/sample.png", options);

Google官方对 inSampleSize 的注释:
/** * If set to a value > 1, requests the decoder to subsample the original * image, returning a smaller image to save memory. The sample size is * the number of pixels in either dimension that correspond to a single * pixel in the decoded bitmap. For example, inSampleSize == 4 returns * an image that is 1/4 the width/height of the original, and 1/16 the * number of pixels. Any value <= 1 is treated the same as 1. Note: the * decoder uses a final value based on powers of 2, any other value will * be rounded down to the nearest power of 2. */
简单说,就是多个像素采样为一个像素。
举个栗子,如果 inSampleSize = 2,直接采样其中一个像素,另一个像素舍弃。

双线性采样

Android中双线性采样主要是通过 Matrix 来使用的:
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/sample.png");
Bitmap compress = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2, bitmap.getHeight()/2, true);

Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/sample.png");
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
bm = Bitmap.createBitmap(bitmap, 0, 0, bit.getWidth(), bit.getHeight(), matrix, true);

双线性采样内部使用的是双线性内插值算法

每个位置像素使用 2✖️2 的源像素按相对位置进行两个方向的线性插值,根据位置取对应的权重,计算后得到目标像素。

x方向:
y方向:
最终插值结果:


对比

  • 邻近采样
    • 简单粗暴,效率高
    • 失真较高
    • 因为是像素舍弃,会导致锯齿明显

  • 双线性采样
    • 计算量较大,效率相对低些
    • 保真相对较好一些
    • 有一定的抗锯齿能力
    • 具有低通滤波性质

其他算法

  • Bicubic Resampling
  • Lanczos Resampling