目录
前言
智能手机的用户分布在不同国家,且偏好各异,这就要求开发阶段兼容适配;由于各厂家生产出的安卓设备分别率不同、屏幕大小和风格也存在各异,如果手机的用户设备各异,仅用一张图片可能会出现拉伸变形模糊,影响用户体验,因此对应屏幕的兼容适配是重中之重;随着Google不断更新Android版本,每个版本的代码也有区别。Android适配技能日益成为开发者必不可少的一项专业技能。
本篇文章分别讲解了语言适配、屏幕适配、版本适配三个内容。
一、适配国家语言
语言适配有两种场景:一种是在手机系统里的“设置”选项中更改了系统的语言,影响的是整个手机内应用,包括系统应用和非系统应用。另一种,是用户可以手动切换某一款应用的内部语言风格,影响的范围仅仅是自身应用。
(1)手机系统语言适配
工程的根目录有个res/的目录,该目录下存放的是资源文件:如drawable、anim、layout、values。其中,value目录下存放/strings.xml,它用来设置项目中需要的字符串对象,可以理解为应用文本显示的内容。如下,默认的应用名称。
<resources>
<string name="app_name"> APP_NAME </string>
</resources>
value目录会根据手机语言的改变而加载不同String.xml。因此,在res/目录下创建多个values/strings.xml文件,且values目录需要改名,例如:
res/
values/ 默认
strings.xml
values-en/ 英文
strings.xml
values-es/ 西班牙
strings.xml
values-fr/ 法语
strings.xml
英语:/values-en/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="title">My Application</string>
<string name="hello_world">Hello World!</string>
</resources>
西班牙语:/values-es/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="title">Mi Aplicación</string>
<string name="hello_world">Hola Mundo!</string>
</resources>
法语:/values-fr/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="title">Mon Application</string>
<string name="hello_world">Bonjour le monde !</string>
</resources>
最后,我们就可以在代码中使用R.string.<string_name>语法来引用字符串资源就可以了。配置好了就不怕手机系统语言切换了,但是APP内部语言切换就得换一种实现方式了。
(2)应用切换语言
首先,通过Configuration(配置信息)这个类,获取应用当前的语言,然后修改该配置中的语言,最后更新配置,重启一下Activity才能生效。其次,还有一个比较重要的类Locale(地点),如下。
public final class Locale implements Cloneable, Serializable {
static private final Cache LOCALECACHE = new Cache();
/** 中文
*/
static public final Locale CHINESE = createConstant("zh", "");
/**英文
*/
static public final Locale ENGLISH = createConstant("en", "");
/** 法文
*/
static public final Locale FRENCH = createConstant("fr", "");
/** 德文
*/
static public final Locale GERMAN = createConstant("de", "");
/**印度文
*/
static public final Locale ITALIAN = createConstant("it", "");
/** 日文
*/
static public final Locale JAPANESE = createConstant("ja", "");
/** 韩文
*/
static public final Locale KOREAN = createConstant("ko", "");
.....
}
点击切换按钮,实现中英文语言切换。
@Override
public void onClick(View v) {
//1、获取当前语言
String current_language;
// 注:Locale.getDefault().getLanguage();该方法获取系统语言,对于应用内切换不适用。
Configuration config = getResources().getConfiguration();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){//api24 ,android 7.0
current_language = config.getLocales().toLanguageTags();
}else{
current_language = config.locale.getLanguage();
}
//2、切换中英语言
if (current_language.equals(Locale.CHINESE.getLanguage()) || current_language.equals("zh-CN") ){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
config.getLocales();
config.setLocale(Locale.ENGLISH);
}else{
config.locale = Locale.ENGLISH;
}
}else if (current_language.equals(Locale.ENGLISH.getLanguage())){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
config.getLocales();
config.setLocale(Locale.CHINESE);
}else{
config.locale = Locale.CHINESE;
}
}
//3、更新应用配置
getBaseContext().getResources().updateConfiguration(config,getBaseContext().getResources().getDisplayMetrics());
//4、重启Activity
recreate();
}
二、屏幕适配
为什么做屏幕适配?
目前市场上的安卓手机设备的分别率各不相同、屏幕大小和风格(9.0 刘海屏适配方案)也存在各异。例如,项目中仅用一张图片,在多款不同大小的手机上表现可能会出现拉伸变形模糊,影响用户体验。其次,Android设备是支持屏幕旋转功能的,如果不做适配那问题就大了,轻者布局错位重则生命周期重刷发生异常崩溃。这里归纳总结的屏幕适配有三个方向:图片适配、布局适配、旋转适配。
(1)图片适配
dp是一种基于屏幕密度的抽象单位,也叫虚拟像素,在不同的像素密度的设备上会自动适配。原理:我们知道px是像素单位,一款设备的屏幕上会散列着物理像素点,叫分别率。密度dpi表示在屏幕上每英寸的所占的像素点。规定当dpi为160时,1dp是等于1px的,从而实现dp可以自动适配的功能。
计算公式:px = dp * (dpi/160) ; dp = px/dpi/160;
手机设备的尺寸是☞屏幕对角线的长度,因此像素密度DPI的计算方式比较有趣:例如有个5寸且长宽比为4:3的手机设备。1寸 = 2.54厘米。知道了对角线长度和长宽比,就可以根据勾股定理很容易的计算出手机屏幕的长和宽了。
分辨率级别 | DPI | 比率 | 分别率(px) | dp 换算 px关系 |
ldpi(Low:低) | 120 | 0.75 | 240*320 | 1dp = 0.75px |
mdpi(Middle:中) | 160 | 1 | 320*480 | 1dp = 1px |
hdpi(High:高) | 240 | 1.5 | 480*800 | 1dp =1.5px |
xhdpi(超高,2倍图) | 320 | 2 | 1280*720 | 1dp = 2px |
xxhdpi(3倍图) | 480 | 3 | 1920*1080 | 1dp =3px |
xxxhdpi(4倍图) | 640 | 4 | 3840*2160 | 1dp =4px |
因此,我们可以根据dp与px的换算关系,让UI设计师给出计算后的多套图。例如,xhdpi 级别的设备画了一张200*200px的图,那么hdpi的150*150px、mdpi的100*100px、ldpi的75*75px....然后,将这些文件放入相应的drawable资源目录中,当引用
@drawable/image
时系统会根据屏幕的分辨率选择恰当的bitmap。
res/
drawable-xhdpi/
image.png
drawable-hdpi/
image.png
drawable-mdpi/
image.png
drawable-ldpi/
image.png
一般通过dp适配时,是在如上表中比较标准的dip和分别率的情况下,可以完美适配,但出现特殊屏幕时就会出现问题。举个例子,已知MIX2的屏幕尺寸是
5.5英寸
,分辨率是2160*1080象素,DPI
= resources.displayMetrics.densityDpi= 440。此时,设计人员给的UI设计图是按照分别率1280*720也就是2倍图,换算dp后宽高是640dp*375dp。而在MIX2设备上,屏幕的宽度是1080/(440/160)=392.7dp,也就是说手机设备屏幕比设计图要宽,这种情况下要显示满屏的欢迎页图片,就无法达到和设计图相同的效果。
当然,还会存在手机设备宽度不足375dp,那么就会出现图片超出屏幕的情况。因此用dp进行适配也是差强人意,下面是字节跳动的屏幕适配方案:获取屏幕参数信息,进行动态适配
object ScreenUtil {
fun adapterScreen(activity: Activity, targetDP: Int, isVertical: Boolean) {
//系统的屏幕尺寸
val systemDM = Resources.getSystem().displayMetrics
//app整体的屏幕尺寸
val appDM = activity.application.resources.displayMetrics
//activity的屏幕尺寸
val activityDM = activity.resources.displayMetrics
if (isVertical) {
// 适配屏幕的高度
activityDM.density = activityDM.heightPixels / targetDP.toFloat()
} else {
// 适配屏幕的宽度
activityDM.density = activityDM.widthPixels / targetDP.toFloat()
}
// 适配相应比例的字体大小
activityDM.scaledDensity = activityDM.density * (systemDM.scaledDensity / systemDM.density)
// 适配dpi
activityDM.densityDpi = (160 * activityDM.density).toInt()
}
fun resetScreen(activity: Activity) {
//系统的屏幕尺寸
val systemDM = Resources.getSystem().displayMetrics
//app整体的屏幕尺寸
val appDM = activity.application.resources.displayMetrics
//activity的屏幕尺寸
val activityDM = activity.resources.displayMetrics
activityDM.density = systemDM.density
activityDM.scaledDensity = systemDM.scaledDensity
activityDM.densityDpi = systemDM.densityDpi
appDM.density = systemDM.density
appDM.scaledDensity = systemDM.scaledDensity
appDM.densityDpi = systemDM.densityDpi
}
}
使用时需要注意以下几点:
- 尽量只在当前页面生效,包括activity,fragment,dialog,view,需要在setCOntentView()或者inflate之前调用这个方法,在结束onDestroy,onDismiss,onDetachWindow()的时候调用resetScreen()方法。
- 在页面中需要弹出toast和dialog的时候需要调用resetScreen,不然toast和dialog的页面大小和字体大小会被影响。
- 记住一点使用前调用adapterScreen,时候后调用resetScreen。
(2)、XML布局适配
Layout适配尺寸有4种:小(small),普通(normal),大(large),超大(xLarge)
我们在资源文件layout目录创建不同尺寸布局文件,系统会根据运行的设备屏幕尺寸,选择在与之对应的layout目录中加载layout。如下:
res/
layout(-normal)/默认
main.xml
layout-large/大
main.xml
layout-xlarge/超大
main.xml
layout-small/小
main.xml
注意:记得在AndroidManifest.xml文件中设置多分辨率支持!!!
<Supports-screens
android:largeScreens="true"
android:normalScreen="true"
android:anyDensity="true"
android:smalleScreen="true"/>
布局控件常用适配方法
- 尽量使用线性布局(LinearLayout)和相对布局(RelateLayout),尽量不使用绝对布局(AbsoluteLayout)和帧布局(FrameLayout)。
- 尽量使用wrap_content、mach_parent让view自适应或最大化,尽量不要写宽高的值。
- 使用线下布局的百分比weight权重时,要把宽度写成“0dp“,如果写成wrap_coent会使布局效果不佳等问题。
- 尽量使用android的Shape自定义view背景,这样会随之自适应。
- ImageView的ScaleType有五种方式(center,centerCrop,centerInside,fieCenter,fieXY),尽量使用fieCenter按比例扩大至view宽度,能取得较好适配和显示效果。
(3)、横竖屏适配
在AndroidMaifest.xml中activity中的属性 android:screenOrientation="",可以设置屏幕为固定横屏或竖屏。值有三种类型:属性landscape是横向,portrait是纵向,"sensor"是由物理的感应器来决定。
如果用户手动旋转设备,此时不仅要注意Activity会经过销毁到重建的问题,还要注意布局兼容问题。 适配横向屏幕,首先再创建一份layout-land布局文件,系统会根据运行的设备屏幕方向情况自动加载对应的layout。如下:
res/
layout-port/ 竖屏
main.xml
layout-land/ 横屏
main.xml
layout-large-land/ 也是可以与不同屏幕大小一起使用
main.xml
也可以只在layout文件夹下创建不同xml布局,通过Configuration().orientation来判断当前是横屏landscape还是竖屏portrait,然后加载相对应的布局文件即可。
重建问题:
如果不需要重新走一遍Activity的生命周期,则在AndroidManifest.xml中activity标签下设置android: configChanges="orientation|keybordHidden|screenSize",这样的话就不会重复调用activity的生命周期方法,切换时只会调用 onConfigurationChanged(Configuration newconfig)方法。
如果一切让它销毁在重建,只不过这过程中把需重要的值保存起来。重建后在取出来就行了。
//onResume之后调用,onPause()之前执行
@Override
protected void onSaveInstanceState(Bundle outBundle) {
super.onSaveInstanceState(outBundle);
outBundle.putBoolean("RoadChange", mChange);
}
//onResume之前调用 ,onStart之后执行
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mChange = savedInstanceState.getBoolean("RoadChange");
}
三、适配不同系统版本
新的Android版本会为我们的app提供更棒的API,但我们的app仍应支持旧版本的Android,直到更多的设备升级到新版本为止。Android 5.0、6.0、7.0、8.0、9.0 、10.0新特性,DownloadManager踩坑记
首先,在项目清单文件中指定最小和目标API级别。具体来说,<uses-sdk>元素中的minSdkVersion和targetSdkVersion 属性,标明在设计和测试app时,最低兼容API的级别和最高适用的API级别(这个最高的级别是需要通过我们的测试的)。例如:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ... >
<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="15" />
...
</manifest>
其次,是在代码中判断检查版本信息。Android在Build常量类中提供了对每一个版本的唯一代号,在我们的app中使用这些代号可以建立条件,保证依赖于高级别的API的代码,只会在这些API在当前系统中可用时,才会执行。
private void setUpActionBar() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
//
}
}