适配问题所在:
问题出现的原因在于屏幕宽度的不确定
在wrap_content和固定宽度的时候需要考虑应用 在不同屏幕宽度情况下的布局显示效果,使用wrap_content和固定宽度有可能在小屏手机中出现显示不全、大屏手机留有空白位置的情况。
有下面几种情况需要考虑布局宽度排版的问题:
不同尺寸的屏幕大小、横竖屏的切换(刘海屏-cutout、虚拟按键和状态栏的高度影响)
解决方案:
1. 今日头条适配方案:
原理:通过DisplayMetrics类,重新计算设置准确的density、dpi等屏幕参数,然后通过控制宽度按固定dp来达到不同宽度的屏幕显示一样的布局效果。
优点:不用提供多套资源,入侵性小
缺点:
实现:
public class FixedDpTool { private static float mRawDensity = 0; private static float mRawScaleDensity = 0; /**屏幕宽度dp值, 单位dp*/ private static int widthDp = 360; public static int getWidthDp() { return widthDp; } public static void setWidthDp(int widthDp) { FixedDpTool.widthDp = widthDp; } public static final void setDensity(final Activity activity){ //获取Application的DisplayMetrics, 得到原始的density和scaleDensity final DisplayMetrics appMetrics = activity.getApplication().getResources().getDisplayMetrics(); //初始化,记录原始的density, scaleDensity if(mRawDensity== 0){ mRawDensity = appMetrics.density; mRawScaleDensity = appMetrics.scaledDensity; activity.getApplication().registerComponentCallbacks(new ComponentCallbacks() { @Override public void onConfigurationChanged(Configuration newConfig) { //字体配置有变化 if(newConfig!= null&& newConfig.fontScale> 0){ mRawScaleDensity = activity.getApplication().getResources().getDisplayMetrics().scaledDensity; } } @Override public void onLowMemory() { } }); } //计算目标的density和scaleDensity //这里的appMetrics.widthPixels在竖屏时,这样计算没有问题,但是在横屏时涉及虚拟按键的时候这个appMetrics.widthPixels值不一定准确 //在某些机型上,竖屏时的高度appMetrics.heightPixels不包含虚拟按键的高度,横屏时则是appMetrics.widthPixels值不包含虚拟按键长度 //所以这里需要判断宽度的分辨率时正确值 float targetDensity = appMetrics.widthPixels / widthDp; float targetScaleDensity = (targetDensity / mRawDensity) * mRawScaleDensity; float targetDpi = targetDensity * 160; //设置Activity的DisplayMetrics DisplayMetrics actMetrics = activity.getResources().getDisplayMetrics(); actMetrics.density = targetDensity; actMetrics.densityDpi = (int) targetDpi; actMetrics.scaledDensity = targetScaleDensity; } }
在应用Application、每个Activity的onCreate方法中都需要调用setDensity方法重新设置DisplayMetrics类实例中的变量。
有几个问题需要注意:
***在存在虚拟按键的时候,DisplayMetrics类中的除了width、height的值会受虚拟按键的影响,width、height中如果那一边存在虚拟按键,屏幕实际的像素值应该是width或height+虚拟按键的长度。所以最好使用其他方法来获取屏幕实际的分辨率
如:
反射:
private static void getDisPlaySize(Activity act){ int width = 0, height = 0; final DisplayMetrics metrics = new DisplayMetrics(); Display display = act.getWindowManager().getDefaultDisplay(); Method mGetRawH = null, mGetRawW = null; try { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { display.getRealMetrics(metrics); width = metrics.widthPixels; height = metrics.heightPixels; } else { mGetRawH = Display.class.getMethod("getRawHeight"); mGetRawW = Display.class.getMethod("getRawWidth"); try { width = (Integer) mGetRawW.invoke(display); height = (Integer) mGetRawH.invoke(display); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } } catch (NoSuchMethodException e3) { e3.printStackTrace(); } }
或者
public static int getDpi(Context context){ int dpi = 0; WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = windowManager.getDefaultDisplay(); DisplayMetrics displayMetrics = new DisplayMetrics(); @SuppressWarnings("rawtypes") Class c; try { c = Class.forName("android.view.Display"); @SuppressWarnings("unchecked") Method method = c.getMethod("getRealMetrics",DisplayMetrics.class); method.invoke(display, displayMetrics); dpi=displayMetrics.heightPixels; }catch(Exception e){ e.printStackTrace(); } return dpi; }
API17及以上可以使用:
private static void getScreen(Activity act){ Point point = new Point(); //getRealSize方法是在API17才增加的 act.getWindowManager().getDefaultDisplay().getRealSize(point); //y表示竖屏时的高度,x表示竖屏时的宽度 int screenHeight = point.y; int screenWidth = point.x; Log.i(TAG, "getScreen: height:"+ screenHeight+ ", width:"+ screenWidth); }
2.SmallestWidth,sw配置限定符:
原理:根据屏幕的宽度最小dp值来提供多套资源,用于在不同尺寸屏幕上显示,每套资源根据宽度最小dp和屏幕的实际宽度px值;
按sw配置限定符的匹配规则,如果在只有sw360dp的备用资源时,设备宽度在360dp~369dp宽度范围内就会使用sw360dp的资源,这样可能会导致369+dp的设备会有一些空白留下,但是10dp左右的宽度对显示的影响不是很大,如果需要要求高的话可以每种dp值提供一套资源用于使用。
优点:适配适用范围更广,稳定性好
缺点:需要根据固定dp和实际像素宽度来计算每dp有几个像素点,生成多套资源时会增加apk体积。
网上找到的生成dp资源的工程
https://github.com/ladingwu/dimens_sw
3.百分比布局:
原理:使用官方(或者自己实现)的百分比布局来进行排版,在宽度设置特定值时不使用实际dp、px定义宽度,而是计算占用宽度的百分比数值来进行设置。
优点:不需要再布局中显式定于固定的宽度
缺点:应用范围不广,比较适用简单的布局排版,涉及代码控制的View控件使用时比较麻烦;如果刚开始开发的话,适用SmallestWidth方案会更好,但是在开发过程中需要进行适配的话则是今日头条的方案会更合适;至于百分比布局可以在按需求来应用。
涉及知识点:
- 屏幕相关的单位以及计算公式
单位:
dp、px、分辨率、屏幕大小(英寸)、dpi(像素密度)、sp
sp : 同dp相似,还会受系统字体大小设置影响
px:物理像素单位,1px表示屏幕上的一个像素点;
dp:密度无关像素;1dp等于 160 dpi 屏幕上的一个物理像素
分辨率:屏幕的宽像素点个数*高像素点个数;如1080*1794表示屏幕宽有1080个像素点,高有1794个像素点
英寸:物理长度单位,用来描述屏幕的大小,单位(in), 1in=2.54cm;
dpi(dots-per-inch):屏幕像素密度,描述打印像素密度(也叫打印分辨率),不是实际;Android的屏幕密度分组有6个(对应的drawable文件夹):
低 | ldpii | ~120dpi | 最少尺寸dp |
中 | mdpi | ~160dpi |
|
高 | hdpi | ~240dpi |
|
超高 | xhdpi | ~320dpi |
|
超超高 | xxhdpi | ~480dpi |
|
超超超高 | xxxhdpi | ~640dpi |
|
ppi(Pixels per inch):每英寸的像素个数
--我们通常来说的像素密度可以用dpi和ppi表示,单细分来说dpi用于描述打印机的单位,ppi则是用于描述屏幕真实像素密度。
屏幕大小:一般说到屏幕大小说的是屏幕对象先的长度,如5寸屏幕说的就是屏幕对角线长5英寸,通常都是厂商推出的时候在参数说明里面,无法通过系统获得(因为无法获取精确的dpi值,无法通过公式换算出屏幕大小—英寸长度)。
公式:
下面公式里面的160是因为dp的定义,1dp=1px的时候屏幕的像素密度是160dpi,以这个160为基准计算1dp实际占几个px(像素)。
首先知道屏幕像素密度(dpi)的计算公式:
公式中的width是屏幕宽的像素个数(单位:px),height是屏幕高的像素个数(单位:px)
像素、dpi、dp之间的转换公式
px= dp*(dpi/160)
px= density* dp
density=dpi/160
屏幕总dp的计算公式就是: dp= px/(dpi/160)
观察公式可以知道, dpi、dp、density这些都是通过屏幕分辨率和屏幕尺寸的到的
但是,Android系统并没有获取屏幕物理尺寸大小的直接方法,只提供了获取准确分辨率的方法(在有虚拟按键Bottom布局的时候,不同的方法获取分辨率时,可能会有不一样)。
Android系统对资源的缩放规则:
官方文档上的默认资源列表和资源配置限定符列表
默认资源目录列表:
目录 | 资源类型 |
animator/ | 用于定义属性动画的 XML 文件。 |
anim/ | 定义渐变动画的 XML 文件。(属性动画也可以保存在此目录中,但是为了区分这两种类型,属性动画首选 animator/ 目录。) |
color/ | 用于定义颜色状态列表的 XML 文件。请参阅颜色状态列表资源 |
drawable/ | 位图文件(.png、.9.png、.jpg、.gif)或编译为以下可绘制对象资源子类型的 XML 文件:
请参阅 可绘制对象资源。 |
mipmap/ | 适用于不同启动器图标密度的可绘制对象文件。如需了解有关使用 mipmap/ 文件夹管理启动器图标的详细信息,请参阅管理项目概览。 |
layout/ | 用于定义用户界面布局的 XML 文件。 请参阅布局资源。 |
menu/ | 用于定义应用菜单(如选项菜单、上下文菜单或子菜单)的 XML 文件。请参阅菜单资源。 |
raw/ | 要以原始形式保存的任意文件。要使用原始 InputStream 打开这些资源,请使用资源 ID(即 R.raw.filename)调用 Resources.openRawResource()。 但是,如需访问原始文件名和文件层次结构,则可以考虑将某些资源保存在 assets/ 目录下(而不是 res/raw/)。assets/ 中的文件没有资源 ID,因此您只能使用 AssetManager 读取这些文件。 |
values/ | 包含字符串、整型数和颜色等简单值的 XML 文件。 其他 res/ 子目录中的 XML 资源文件是根据 XML 文件名定义单个资源,而 values/ 目录中的文件可描述多个资源。对于此目录中的文件,<resources> 元素的每个子元素均定义一个资源。例如,<string> 元素创建 R.string 资源,<color> 元素创建 R.color 资源。 由于每个资源均用其自己的 XML 元素定义,因此您可以根据自己的需要命名文件,并将不同的资源类型放在一个文件中。但是,为了清晰起见,您可能需要将独特的资源类型放在不同的文件中。 例如,对于可在此目录中创建的资源,下面给出了相应的文件名约定: |
xml/ | 可以在运行时通过调用 Resources.getXML() 读取的任意 XML 文件。各种 XML 配置文件(如可搜索配置)都必须保存在此处。 |
配置限定符列表:
配置 | 限定符值 | 说明 |
MCC 和 MNC | 示例: | 移动国家代码 (MCC),(可选)后跟设备 SIM 卡中的移动网络代码 (MNC)。例如,mcc310 是指美国的任一运营商,mcc310-mnc004 是指美国的 Verizon 公司,mcc208-mnc00 是指法国的 Orange 公司。 如果设备使用无线电连接(GSM 手机),则 MCC 和 MNC 值来自 SIM 卡。 也可以单独使用 MCC(例如,将国家/地区特定的合法资源包括在应用中)。如果只需根据语言指定,则改用“语言和区域”限定符(稍后进行介绍)。 如果决定使用 MCC 和 MNC 限定符,请谨慎执行此操作并测试限定符是否按预期工作。 |
语言和区域 | 示例: | 语言通过由两个字母组成的 ISO 639-1 语言代码定义,可以选择后跟两个字母组成的 ISO 3166-1-alpha-2 区域码(前带小写字母“r”)。 这些代码不区分大小写;r 前缀用于区分区域码。 不能单独指定区域。 如果用户更改系统设置中的语言,它有可能在应用生命周期中发生改变。 如需了解这会在运行期间给应用带来哪些影响,请参阅处理运行时变更。 有关针对其他语言本地化应用的完整指南,请参阅本地化。 另请参阅 locale 配置字段,该字段表示当前的语言区域。 |
布局方向 | ldrtl | 应用的布局方向。ldrtl 是指“布局方向从右到左”。ldltr 是指“布局方向从左到右”,这是默认的隐式值。 它适用于布局、图片或值等任何资源。 例如,若要针对阿拉伯语提供某种特定布局,并针对任何其他“从右到左”语言(如波斯语或希伯来语)提供某种通用布局,则可编码如下: res/ layout/ main.xml (Default layout) layout-ar/ main.xml (Specific layout for Arabic) layout-ldrtl/ main.xml (Any "right-to-left" language, except for Arabic, because the "ar" language qualifier has a higher precedence.) 注:要为应用启用从右到左的布局功能,必须将 supportsRtl 设置为 "true",并将 targetSdkVersion 设置为 17 或更高版本。 此项为 API 级别 17 中新增配置。 |
smallestWidth | sw<N>dp | 屏幕的基本尺寸,由可用屏幕区域的最小尺寸指定。 具体来说,设备的 smallestWidth 是屏幕可用高度和宽度的最小尺寸(您也可以将其视为屏幕的“最小可能宽度”)。无论屏幕的当前方向如何,您均可使用此限定符确保应用 UI 的可用宽度至少为 <N>dp。 例如,如果布局要求屏幕区域的最小尺寸始终至少为 600dp,则可使用此限定符创建布局资源 res/layout-sw600dp/。仅当可用屏幕的最小尺寸至少为 600dp 时,系统才会使用这些资源,而不考虑 600dp 所代表的边是用户所认为的高度还是宽度。smallestWidth 是设备的固定屏幕尺寸特性;设备的 smallestWidth 不会随屏幕方向的变化而改变。 设备的 smallestWidth 将屏幕装饰元素和系统 UI 考虑在内。例如,如果设备的屏幕上有一些永久性 UI 元素占据沿 smallestWidth 轴的空间,则系统会声明 smallestWidth 小于实际屏幕尺寸,因为这些屏幕像素不适用于您的 UI。 因此,使用的值应该是布局所需要的实际最小尺寸(通常,无论屏幕的当前方向如何,此值都是布局支持的“最小宽度”)。 以下是一些可用于普通屏幕尺寸的值:
应用为多个资源目录提供不同的 smallestWidth 限定符值时,系统会使用最接近(但未超出)设备 smallestWidth 的值。 此项为 API 级别 13 中新增配置。 另请参阅 android:requiresSmallestWidthDp 属性和 smallestScreenWidthDp 配置字段,前者声明与应用兼容的最小 smallestWidth;后者存放设备的 smallestWidth 值。 如需了解有关设计不同屏幕和使用此限定符的详细信息,请参阅支持多种屏幕开发者指南。 |
可用宽度 | w<N>dp | 指定资源应该使用的最小可用屏幕宽度,以 dp 为单位,由 <N> 值定义。在横向和纵向之间切换时,为了匹配当前实际宽度,此配置值也会随之发生变化。 应用为多个资源目录提供不同的此配置值时,系统会使用最接近(但未超出)设备当前屏幕宽度的值。 此处的值考虑到了屏幕装饰元素,因此如果设备显示屏的左边缘或右边缘上有一些永久性 UI 元素,考虑到这些 UI 元素,它会使用小于实际屏幕尺寸的宽度值,这样会减少应用的可用空间。 此项为 API 级别 13 中新增配置。 另请参阅 screenWidthDp 配置字段,该字段存放当前屏幕宽度。 如需了解有关设计不同屏幕和使用此限定符的详细信息,请参阅支持多种屏幕开发者指南。 |
可用高度 | h<N>dp | 指定资源应该使用的最小可用屏幕高度,以“dp”为单位,由 <N> 值定义。 在横向和纵向之间切换时,为了匹配当前实际高度,此配置值也会随之发生变化。 应用为多个资源目录提供不同的此配置值时,系统会使用最接近(但未超出)设备当前屏幕高度的值。 此处的值考虑到了屏幕装饰元素,因此如果设备显示屏的上边缘或下边缘有一些永久性 UI 元素,考虑到这些 UI 元素,同时为减少应用的可用空间,它会使用小于实际屏幕尺寸的高度值。 非固定的屏幕装饰元素(例如,全屏时可隐藏的手机状态栏)并不在考虑范围内,标题栏或操作栏等窗口装饰也不在考虑范围内,因此应用必须准备好处理稍小于其所指定值的空间。 此项为 API 级别 13 中新增配置。 另请参阅 screenHeightDp 配置字段,该字段存放当前屏幕宽度。 如需了解有关设计不同屏幕和使用此限定符的详细信息,请参阅支持多种屏幕开发者指南。 |
屏幕尺寸 | small |
注:使用尺寸限定符并不表示资源仅适用于该尺寸的屏幕。 如果没有为备用资源提供最符合当前设备配置的限定符,则系统可能使用其中最匹配的资源。 注意:如果所有资源均使用大于当前屏幕的尺寸限定符,则系统不会使用这些资源,并且应用在运行时将会崩溃(例如,如果所有布局资源均用 xlarge 限定符标记,但设备是标准尺寸的屏幕)。 此项为 API 级别 4 中新增配置。 如需了解详细信息,请参阅支持多种屏幕。 另请参阅 screenLayout 配置字段,该字段表示屏幕是小尺寸、标准尺寸还是大尺寸。 |
屏幕纵横比 | long |
此项为 API 级别 4 中新增配置。 它完全基于屏幕的纵横比(宽屏较宽),而与屏幕方向无关。 另请参阅 screenLayout 配置字段,该字段指示屏幕是否为宽屏。 |
圆形屏幕 | round |
此项为 API 级别 23 中新增配置。 另请参阅 isScreenRound() 配置方法,其指示屏幕是否为宽屏。 |
屏幕方向 | port |
如果用户旋转屏幕,它有可能在应用生命周期中发生改变。 如需了解这会在运行期间给应用带来哪些影响,请参阅处理运行时变更。 另请参阅 orientation 配置字段,该字段指示当前的设备方向。 |
UI 模式 | car |
此项为 API 级别 8 中新增配置,API 13 中新增电视配置,API 20 中新增手表配置。 如需了解应用在设备插入手机座或从中移除时的响应方式,请阅读确定并监控插接状态和类型。 如果用户将设备放入手机座中,它有可能在应用生命周期中发生改变。 可以使用 UiModeManager 启用或禁用其中某些模式。如需了解这会在运行期间给应用带来哪些影响,请参阅处理运行时变更。 |
夜间模式 | night |
此项为 API 级别 8 中新增配置。 如果夜间模式停留在自动模式(默认),它有可能在应用生命周期中发生改变。在这种情况下,该模式会根据当天的时间进行调整。 可以使用 UiModeManager 启用或禁用此模式。如需了解这会在运行期间给应用带来哪些影响,请参阅处理运行时变更。 |
屏幕像素密度 (dpi) | ldpi |
六个主要密度之间的缩放比为 3:4:6:8:12:16(忽略 tvdpi 密度)。因此,9x9 (ldpi) 位图相当于 12x12 (mdpi)、18x18 (hdpi)、24x24 (xhdpi) 位图,依此类推。 如果您认为图像资源在电视或其他某些设备上呈现的效果不够好,而想尝试使用 tvdpi 资源,则缩放比例为 1.33*mdpi。例如,mdpi 屏幕的 100px x 100px 图像应该相当于 tvdpi 的133px x 133px。 注:使用密度限定符并不表示资源仅适用于该密度的屏幕。 如果没有为备用资源提供最符合当前设备配置的限定符,则系统可能使用其中最匹配的资源。 如需了解有关如何处理不同屏幕密度以及 Android 如何缩放位图以适应当前密度的详细信息,请参阅支持多种屏幕。 |
触摸屏类型 | notouch |
另请参阅 touchscreen 配置字段,该字段指示设备上的触摸屏类型。 |
键盘可用性 | keysexposed |
如果提供了 keysexposed 资源,但未提供 keyssoft 资源,那么只要系统已经启用软键盘,就会使用 keysexposed 资源,而不考虑键盘是否可见。 如果用户打开硬键盘,它有可能在应用生命周期中发生改变。 如需了解这会在运行期间给应用带来哪些影响,请参阅处理运行时变更。 另请参阅配置字段 hardKeyboardHidden 和 keyboardHidden,这两个字段分别指示硬键盘的可见性和任何一种键盘(包括软键盘)的可见性。 |
主要文本输入法 | nokeys |
另请参阅 keyboard 配置字段,该字段指示可用的主要文本输入法。 |
导航键可用性 | navexposed |
如果用户显示导航键,它有可能在应用生命周期中发生改变。 如需了解这会在运行期间给应用带来哪些影响,请参阅处理运行时变更。 另请参阅 navigationHidden 配置字段,该字段指示导航键是否处于隐藏状态。 |
主要非触摸导航方法 | nonav |
另请参阅 navigation 配置字段,该字段指示可用的导航方法类型。 |
平台版本(API 级别) | 示例: | 设备支持的 API 级别。例如,v1 对应于 API 级别 1(带有 Android 1.0 或更高版本系统的设备),v4 对应于 API 级别 4(带有 Android 1.6 或更高版本系统的设备)。如需了解有关这些值的详细信息,请参阅 Android API 级别文档。 |