安卓项目实战之与UI那点事:图片适配你必须要了解的知识点

1,mipmap和drawable的区别

在Android4.2以上的版本中,提供了对mipmaps的支持,如果你用Andorid Studio开发Android程序会发现Android Studio自动帮你创建了几个mipmaps文件夹,很多人每次新建一个工程的时候,总是先把mipmap删掉,新建几个不同dpi的drawable文件夹,才开始干别的。究竟mipmap和drawable有什么区别呢?对此疑问进行一次总结如下:

首先我们先来纠正一个错误:mipmap文件夹是用来替代drawable文件夹的?
经多方查询我们发现官网压根没说用mipmap文件夹替代drawable文件夹这样的话,而且根据官方对于mipmap文件夹的介绍我们可以得出以下结论:
就目前而言,mipmap文件夹仅仅是用来存放应用的启动图标和与缩放动画相关的图片的,AndroidStudio新建项目的ic_launcher.png都是默认放在mipmap文件夹下的,这样无论何种屏幕分辨率,系统都会选择最适合的分辨率的icon显示在主屏上。
而其他的图片资源等,还是按照以前方式,放在drawable文件夹下,如位图文件(PNG、JPEG、GIF),还有点九图片和XML文件(shape和selector等)。
扩展:
mipmap指的是一种纹理映射技术,是目前解决纹理分辨率与视点距离关系的最有效途径,mipmap是Android系统为了解决不同分辨率显示高清图标而采用的一个技术,来使图标保证清晰且适配各种屏幕。而mipmap也可以用在需要在动画中被缩放的图片,关于mipmap纹理映射技术的具体实现细节如果感兴趣请读者自行去查询资料了解。

2,mipmap应用启动图标适配

讲过上面的了解我们知道mipmap文件夹只是用来放置应用程序的启动图标icon的,仅此而已,并且系统对于mipmaps的支持也是从Android4.2以上版本才开始的,将icon放置在mipmap文件夹还可以让我们程序的launcher图标自动拥有跨设备密度展示的能力,比如说一台屏幕密度是xxhdpi的设备可以自动加载mipmap-xxxhdpi下的icon来作为应用程序的launcher图标,这样图标看上去就会更加细腻。
对于每种密度下的icon应该设计成什么尺寸其实Android也是给出了最佳建议,icon的尺寸最好不要随意设计,因为过低的分辨率会造成图标模糊,而过高的分辨率只会徒增APK大小。建议尺寸如下表所示:
在这里插入图片描述
然后我们引用mipmap的方式和之前引用drawable的方式是完全一致的,在资源中就使用@mipmap/res_id,在代码就使用R.mipmap.res_id即可。
建议:我们至少需要提供一个xxxhdpi类型的启动图标,因为Android会帮你自动缩小图标到对应的别的分辨率上(放大是会变模糊的),这样子可以节省些apk size。

3,drawable图片适配(重点)

在Android项目当中,drawable文件夹都是用来放置图片资源的,不管是jpg、png、还是9.png,都可以放在这里。除此之外,还有像selector这样的xml文件也是可以放在drawable文件夹下面的。
一般根据设备dpi(dpi是指每英寸的像素)的不同我们需要建立不同的drawable文件夹,如下图:
在这里插入图片描述
可以看到dpi可大致分为mdpi,hdpi,xhdpi,xxhdpi,xxxdpi文件夹,按照安卓官方的适配建议需要每个文件夹中都放置相对应的图片,这样一来一个图片就会有多个,有的人可能认为这样会增大工作量,只使用一套图放置一个文件夹,这样减轻UI人员和开发人员的工作,但是这样会造成另一个问题,就是会造成内存问题,接下来我们具体来看下是如何造成内存问题的:
首先我准备了一张270*480像素的图片:
在这里插入图片描述
将图片命名为android_logo.png,然后把它放在drawable-xxhdpi文件夹下面。为什么要放在这个文件夹下呢?是因为我的手机屏幕的密度就是xxhdpi的。那么怎么才能知道自己手机屏幕的密度呢?你可以使用如下方法先获取到屏幕的dpi值:

float xdpi = getResources().getDisplayMetrics().xdpi;
float ydpi = getResources().getDisplayMetrics().ydpi;

其中xdpi代表屏幕宽度的dpi值,ydpi代表屏幕高度的dpi值,通常这两个值都是近乎相等或者极其接近的,在我的手机上这两个值都约等于403。那么403又代表着什么意思呢?我们直接参考下面这个表格就知道了:
在这里插入图片描述
从表中可以看出,403dpi是处于320dpi到480dpi之间的,因此属于xxhdpi的范围。
图片放好了之后,下面我在布局文件中引用这张图片,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
   >

    <ImageView
        android:id="@+id/image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/android_logo"
        />

</LinearLayout>

在ImageView控件中指定加载android_logo这张图,并把ImageView控件的宽高都设置成wrap_content,这样图片有多大,我们的控件就会有多大。
现在运行一下程序,效果如下所示:
在这里插入图片描述
由于我的手机分辨率是10801920像素的,而这张图片的分辨率是270480像素的,刚好是手机分辨率的四分之一,因此从上图中也可以看出,android_logo图片的宽和高大概都占据了屏幕宽高的四分之一左右,大小基本是比较精准的。
关于手机屏幕的分辨率信息我们可以通过在Activity中调用如下代码来获取:

DisplayMetrics dm = new DisplayMetrics();
this.getWindowManager().getDefaultDisplay().getMetrics(dm);
int screenWidth = dm.widthPixels;
int screenHeight = dm.heightPixels;

接着我们尝试做点改变,将android_logo.png这张图移动到drawable-xhdpi文件夹下,注意不是复制一份到drawable-xhdpi文件夹下,而是将图片移动到drawable-xhdpi文件夹下,然后重新运行一下程序,效果如下图所示:
在这里插入图片描述
嗯?怎么感觉图片好像变大了一点,是错觉吗?
那么我们再将这张图移动到drawable-mdpi文件夹下试试,重新运行程序,效果如下图所示:
在这里插入图片描述
这次肯定不是错觉了,这实在是太明显了,图片被放大了!
那么为什么好端端的一张图片会被自动放大呢?而且这放大的比例是不是有点太过份了。其实不然,Android所做的这些缩放操作都是有它严格的规定和算法的。可能有不少做了很多年Android的朋友都没去留意过这些缩放的规则,因为这些细节太微小了,那么本篇的微技巧探索里面,我们就来把这些细节理理清楚。

首先解释一下图片为什么会被放大,当我们使用资源id来去引用一张图片时,Android会使用一些规则来去帮我们匹配最适合的图片。具体的查找规则如下:

  1. 先查找和屏幕密度最匹配的文件夹。比如上例中我的手机屏幕密度是xxhdpi,那么系统会优先去drawable-xxhdpi文件夹下去查找,如果有的话就使用,此时图片是不会被放大或者缩小的。
  2. 如果在最匹配的目录没有找到对应图片,就会向更高密度的目录查找,直到没有更高密度的目录。上例中更高密度的目录就是drawable-xxxhdpi文件夹了,然后发现这里也没有android_logo这张图,接下来会尝试再找更高密度的文件夹,发现没有更高密度的了。
  3. 如果一直往高密度目录均没有查找,Android就会查找drawable-nodpi目录。drawable-nodpi目录中的资源适用于所有密度的设备,不管当前屏幕的密度如何,系统都不会缩放此目录中的资源。因此,对于永远不希望系统缩放的资源,最简单的方法就是放在此目录中;同时,放在该目录中的资源最好不要再放到其他drawable目录下了,避免得到非预期的效果。
  4. 如果在drawable-nodpi目录也没有查找到,系统就会向比最匹配目录密度低的目录依次查找,直到没有更低密度的目录。例如,上例中最匹配目录是xxhdpi,更高密度的目录和nodpi目录查找不到后,就会依次查找drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。

总体匹配规则就是这样,比如说此时在屏幕密度为xxhdpi的设备上,系统优先去drawable-xxhdpi文件夹下去查找并没有找到,那么继续往更高密度找也没有找到,最后终于在drawable-mdpi文件夹下面找到android_logo这张图了,但是系统会认为你这张图是专门为低密度的设备所设计的,如果直接将这张图在当前的高密度设备上使用就有可能会出现像素过低的情况,于是系统自动帮我们做了这样一个放大操作,图片放大则像素增加,必然会引起内存占用量增加。
那么同样的道理,如果系统是在drawable-xxxhdpi文件夹下面找到这张图的话,它会认为这张图是为更高密度的设备所设计的,如果直接将这张图在当前设备上使用就有可能会出现像素过高的情况,于是会自动帮我们做一个缩小的操作,图片缩小则像素减少,内存占用量就会降低。
所以,我们可以尝试将android_logo这张图移动到drawable-xxxhdpi文件夹下面将会得到这样的结果:
在这里插入图片描述
可以看到,现在图片的宽和高都达到不手机屏幕的四分之一,说明图片确实是被缩小了。
接下来我们来看下在实际开发当中会遇到的场景:根据Android的开发建议,我们在准备图片资源时尽量应该给每种密度的设备都准备一套,这样程序的适配性就可以达到最好,但也存在问题,一是这种方式会增大安装包的大小;二是很多公司UI在出图时只会出一套。那么在这种情况下,我们应该将仅有的这一套图片资源放在哪个密度的文件夹下呢?
可以这样来分析,根据我们刚才所学的内容,如果将一张图片放在低密度文件夹下,那么在高密度设备上显示图片时就会被自动放大,而如果将一张图片放在高密度文件夹下,那么在低密度设备上显示图片时就会被自动缩小。那我们可以通过成本的方式来评估一下,一张原图片被缩小了之后显示其实并没有什么副作用,但是一张原图片被放大了之后显示就意味着要占用更多的内存了。因为图片被放大了,像素点也就变多了,而每个像素点都是要占用内存的。
内存的使用量可通过Android Monitor来查看,首先将android_logo.png图片移动到drawable-xxhdpi目录下,运行程序后我们通过Android Monitor来观察程序内存使用情况: (设备和目录级别相一致的最优情况)
在这里插入图片描述
可以看到,程序所占用的内存大概稳定在19.45M左右。然后将android_logo.png图片移动到drawable-mdpi目录下,重新运行程序,结果如下图所示: (低密度文件夹下图片在高密度设备上显示时会被放大)
在这里插入图片描述
现在涨到23.40M了,占用内存明显增加了,可以看到,仅仅一张图片的内存占用差别就已经在MB级别了。如果你将图片移动到drawable-ldpi目录下,你会发现占用内存会更高。图片放大的内存成本将是不得不考虑的一个重要因素了。

那么经过上面一系列的分析,答案自然也就出来了,图片资源应该尽量放在高密度文件夹下,这样可以节省图片的内存开支,而UI在设计图片的时候也应该尽量面向高密度屏幕的设备来进行设计。由于目前的Android智能手机的屏幕基本都在1080p了,屏幕的dpi多数都处于320~480,为了更好地适配,同时为了节省内存成本,建议将切图放置在drawable-xxhdpi目录,同时建议UI针对该密度的设备设计切图。那么有的朋友可能会问了,不是还有更高密度的drawable-xxxhdpi吗?干吗不放在这里?这是因为,市面上480dpi到640dpi的设备实在是太少了,如果针对这种级别的屏幕密度来设计图片,图片在不缩放的情况下本身就已经很大了,基本也起不到节省内存开支的作用了。

4,UI切图px标注转dp

一款优秀app的产生,往往需要有一套精美华丽的UI设计图,诚然,UI仅仅只是个开始,有追求极致的前端工程师开发软件时尽可能地去贴近UI的设计才是重中之重。

我们知道,Android的尺寸单位一般采用dp或者sp,然而有时候我们遇到的UI设计图给的尺寸标注却是px的,这显然是给iOS画的UI。安卓设备的多样性决定了我们绝对不能将控件的尺寸大小直接设置为UI图上的px值。那该如何解决呢?愤愤不平地去找UI工程师出一套安卓的标注?条件允许的话你当然可以这样干,但其实我们还有另外一种快准不知道狠不狠的解决方案:px转dp。

我们知道px转dp的公式为:dp = px/density
上面density指是设备密度,有了设备密度,我们才可以将px转为dp。而Android系统也为我们提供了获取设备密度的方法:

context.getResources().getDisplayMetrics().density;

获取到了我们测试手机的设备密度之后,然后将UI图上标注的px去除以desity?
当然不是!!!,density值是获取了,但是请问UI图上的px值是按照你的手机来标注的吗?也就是说,我们必须要获取UI图在设计时基于的设备的设备密度(density)。

设备密度公式:density = PPI/160。
PPI是像素密度,公式:PPI = √(长度像素数² + 宽度像素数²) / 屏幕尺寸
上面PPI的公式不难理解,就是指每英寸屏幕有多少个像素点。比如iPhone6的PPI是326,1英寸屏幕有326个像素点。至于设备密度这个公式,PPI除以160,为什么是160而不是别的,这个不用太过于纠结。160是谷歌推荐的数值,这样转换为hdpi、xhdpi等的数值就比较妥当。

根据上面的公式我们就有了如下计算设备密度density的方法:

int width = 750;//屏幕宽度
int height = 1334;//屏幕高度
float screenInch = 4.7f;//屏幕尺寸
//设备密度公式
float density = (float) Math.sqrt(width * width + height * height) / screenInch / 160;

注意上面三个变量值是UI切图时所基于设备的取值。问UI工程师,问ta是以哪个尺寸为基准进行画图的。也有个神器叫PxCook能识别出UI图的设备型号基准,然后通过设备型号搜索出该设备是几寸屏。

一般在Android中px与dp的关系:

dp可以保证在不同屏幕像素密度的设备上显示相同的效果,而ui设计师给你的设计图是以px为单位的,Android开发则是使用dp作为单位的,那么我们需要进行转换:
在这里插入图片描述
在Android中,规定以160dpi(即屏幕分辨率为320x480)为基准:1dp=1px

例如我之前所在公司的UI切的图都是基于1280*720分辨率的设备切的图,所以对于他在图上所标出的px我都是直接除以2得到dp值然后来使用的。

dp,sp与px之间的转换工具类:

/**
 * dp,sp 和 px 转换的辅助类
 */
public class DisplayUtil {

    /**
     * 将px值转换为dip或dp值,保证尺寸大小不变
     * DisplayMetrics类中属性density
     */
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

    /**
     * 将dip或dp值转换为px值,保证尺寸大小不变
     * DisplayMetrics类中属性density
     */
    public static int dip2px(Context context, float dipValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }

    /**
     * 将px值转换为sp值,保证文字大小不变
     * DisplayMetrics类中属性scaledDensity
     */
    public static int px2sp(Context context, float pxValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValue / fontScale + 0.5f);
    }

    /**
     * 将sp值转换为px值,保证文字大小不变
     * DisplayMetrics类中属性scaledDensity
     */
    public static int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
}

猜你喜欢

转载自blog.csdn.net/gpf1320253667/article/details/84594945