推荐Android两种屏幕适配方案

简介:

前言

在Android开发中,由于Android碎片化严重,屏幕分辨率千奇百怪,而想要在各种分辨率的设备上显示基本一致的效果,适配成本越来越高。虽然Android官方提供了dp单位来适配,但其在各种奇怪分辨率下表现却不尽如人意,因此下面探索一种简单且低侵入的适配方式。本文将推荐两种屏幕适配方案,大家可以根据实际情况使用。

传统dp适配方式的缺点

android中的dp在渲染前会将dp转为px,计算公式:

  • px = density * dp;

  • density = dpi / 160;

  • px = dp * (dpi / 160);

而dpi是根据屏幕真实的分辨率和尺寸来计算的,每个设备都可能不一样的。

屏幕尺寸、分辨率、像素密度三者关系

通常情况下,一部手机的分辨率是宽x高,屏幕大小是以寸为单位,那么三者的关系是:

ea9c82d71c0bf2ca4c4645fefab9c847f83706d6

举个例子:屏幕分辨率为:1920*1080,屏幕尺寸为5吋的话,那么dpi为440。

3997958d1423dde78d200c7ba0770c22c56842b1

这样会存在什么问题呢?

假设我们UI设计图是按屏幕宽度为360dp来设计的,那么在上述设备上,屏幕宽度其实为1080/(440/160)=392.7dp,也就是屏幕是比设计图要宽的。这种情况下, 即使使用dp也是无法在不同设备上显示为同样效果的。 同时还存在部分设备屏幕宽度不足360dp,这时就会导致按360dp宽度来开发实际显示不全的情况。

而且上述屏幕尺寸、分辨率和像素密度的关系,很多设备并没有按此规则来实现, 因此dpi的值非常乱,没有规律可循,从而导致使用dp适配效果差强人意。

今日头条轻量级适配方案

梳理需求

首先来梳理下我们的需求,一般我们设计图都是以固定的尺寸来设计的。比如以分辨率1920px 1080px来设计,以density为3来标注,也就是屏幕其实是640dp 360dp。如果我们想在所有设备上显示完全一致,其实是不现实的,因为屏幕高宽比不是固定的,16:9、4:3甚至其他宽高比层出不穷,宽高比不同,显示完全一致就不可能了。但是通常下,我们只需要以宽或高一个维度去适配,比如我们Feed是上下滑动的,只需要保证在所有设备中宽的维度上显示一致即可,再比如一个不支持上下滑动的页面,那么需要保证在高这个维度上都显示一致,尤其不能存在某些设备上显示不全的情况。同时考虑到现在基本都是以dp为单位去做的适配,如果新的方案不支持dp,那么迁移成本也非常高。

因此,总结下大致需求如下:

支持以宽或者高一个维度去适配,保持该维度上和设计图一致;

支持dp和sp单位,控制迁移成本到最小。

找兼容突破口

从dp和px的转换公式 :px = dp * density

可以看出,如果设计图宽为360dp,想要保证在所有设备计算得出的px值都正好是屏幕宽度的话,我们只能修改 density 的值。

通过阅读源码,我们可以得知,density 是 DisplayMetrics 中的成员变量,而 DisplayMetrics 实例通过 Resources#getDisplayMetrics 可以获得,而Resouces通过Activity或者Application的Context获得。

先来熟悉下 DisplayMetrics 中和适配相关的几个变量:

DisplayMetrics#density 就是上述的density

DisplayMetrics#densityDpi 就是上述的dpi

DisplayMetrics#scaledDensity 字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值

那么是不是所有的dp和px的转换都是通过 DisplayMetrics 中相关的值来计算的呢?

首先来看看布局文件中dp的转换,最终都是调用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 来进行转换:


/**
* Converts an unpacked complex data value holding a dimension to its final floating 
* point value. The two parameters <var>unit</var> and <var>value</var>
* are as in {@link #TYPE_DIMENSION}.
* @param unit The unit to convert from.
* @param value The value to apply the unit to.
* @param metrics Current display metrics to use in the conversion -- 
* supplies display density and scaling information.
* @return The complex floating point value multiplied by the appropriate 
* metrics depending on its unit. 
*/
public static float applyDimension(int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
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;
}

这里用到的DisplayMetrics正是从Resources中获得的。

再看看图片的decode,BitmapFactory#decodeResourceStream方法:


/**
* Decode a new Bitmap from an InputStream. This InputStream was obtained from
* resources, which we pass to be able to scale the bitmap accordingly.
* @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
* is {@link android.graphics.Bitmap.Config#HARDWARE}
* and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
* is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
* function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
*/
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
validate(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);
}

可见也是通过 DisplayMetrics 中的值来计算的。

当然还有些其他dp转换的场景,基本都是通过 DisplayMetrics 来计算的,这里不再详述。因此,想要满足上述需求,我们只需要修改 DisplayMetrics 中和 dp 转换相关的变量即可。

最终方案

下面假设设计图宽度是360dp,以宽维度来适配。

那么适配后的 density = 设备真实宽(单位px) / 360,接下来只需要把我们计算好的 density 在系统中修改下即可,代码实现如下:


/**
* @param activity
* @param application
*/
private void setCustomDensity(@NonNull Activity activity, @NonNull Application application) {

 //application
final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();

//计算宽为360dp 同理可以设置高为640dp的根据实际情况
final float targetDensity = appDisplayMetrics.widthPixels / 360;
final int targetDensityDpi = (int) (targetDensity * 160);

appDisplayMetrics.density = appDisplayMetrics.scaledDensity = targetDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;

//activity
final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();

activityDisplayMetrics.density = appDisplayMetrics.scaledDensity = targetDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
}

同时在 Activity#onCreate 方法中调用下。代码比较简单,也没有涉及到系统非公开api的调用,因此理论上不会影响app稳定性。

于是修改后上线灰度测试了一版,稳定性符合预期,没有收到由此带来的crash,但是收到了很多字体过小的反馈:

d0493fd3ee69db27f5d4f260d00d1bd1ba3fe180

原因是在上面的适配中,我们忽略了DisplayMetrics#scaledDensity的特殊性,将DisplayMetrics#scaledDensity和DisplayMetrics#density设置为同样的值,从而某些用户在系统中修改了字体大小失效了,但是我们还不能直接用原始的scaledDensity,直接用的话可能导致某些文字超过显示区域,因此我们可以通过计算之前scaledDensity和density的比获得现在的scaledDensity,方式如下:

final float targetScaledDensity = targetDensity * (appDisplayMetrics.scaledDensity / appDisplayMetrics.density);

但是测试后发现另外一个问题,就是如果在系统设置中切换字体,再返回应用,字体并没有变化。于是还得监听下字体切换,调用 Application#registerComponentCallbacks 注册下 onConfigurationChanged 监听即可。


private static float sRoncompatDennsity;
private static float sRoncompatScaledDensity;

 private void setCustomDensity(@NonNull Activity activity, final @NonNull Application application) {

//application
final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();

if (sRoncompatDennsity == 0) {
sRoncompatDennsity = appDisplayMetrics.density;
sRoncompatScaledDensity = appDisplayMetrics.scaledDensity;
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
sRoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}

@Override
public void onLowMemory() {

}
});
}

//计算宽为360dp 同理可以设置高为640dp的根据实际情况
final float targetDensity = appDisplayMetrics.widthPixels / 360;
final float targetScaledDensity = targetDensity * (sRoncompatScaledDensity / sRoncompatDennsity);
final int targetDensityDpi = (int) (targetDensity * 160);

appDisplayMetrics.density = targetDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;
appDisplayMetrics.scaledDensity = targetScaledDensity;

//activity
final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();

activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
}

适配之后对比 2e7b253ec9ad67456da955810fd3b4f8ff54ba56

另外一种适配方案 (AndroidScreenAdaptation)

AndroidScreenAdaptation gihub地址

本库特点

完全不用改变自己的布局编写习惯,你原先是怎么写布局,就怎么写布局.不用去继承适配类,不用在最外层包裹适配布局,不用新建茫茫多的分辨率适配文件夹,不要求强制使用px为单位,支持代码动态添加view适配,可以实时预览布局,满足旋转和分屏适配,全面屏或带虚拟按键手机适配也没问题.

效果展示

a62932bbff91b989383234c06a4a15091c04ff81
快速开始 添加依赖

implementation ‘me.yatoooon:screenadaptation:1.1.1’

初始化工具类

(1.)创建自己的application继承Application


public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
ScreenAdapterTools.init(this);
}

注:旋转适配,如果应用屏幕固定了某个方向不旋转的话(比如qq和微信),下面可不写.


@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
ScreenAdapterTools.getInstance().reset(this);
}

(2.)在AndroidManifest.xml文件中声明使用你自己创建的application并且添加meta-data数据,例子上标明了这些数据的代表的意义


<application
android:name=".App"
.....
<meta-data
android:name="designwidth"
android:value="1080" /> //设计图的宽,单位是像素,推荐用markman测量,量出来如果是750px那么请尽量去找ui设计师要一份android的设计图.
<meta-data
android:name="designdpi"
android:value="480" /> //设计图对应的标准dpi,根据下面的那张图找到对应的dpi,比如1080就对应480dpi,如果拿到的是其他宽度的设计图,那么选择一个相近的dpi就好了
<meta-data
android:name="fontsize"
android:value="1.0" /> //全局字体的大小倍数,有时候老板会觉得你的所有的字小了或者大了,你总不能一个一个去改吧
<meta-data
android:name="unit"
android:value="px" /> //你的布局里面用的是px这就写px,你的布局里面用的是dp这就写dp,要统一,不要一会儿px一会儿dp,字体也用px或者dp,不要用sp,微信qq用的肯定不是sp.
</application>

宽 240 320 480 720 1080 1440
DPI等级 LDPI MDPI HDPI XHDPI XXHDPI XXXHDPI
DPI数值 120 160 240 320 480 640

开始使用

(1.)在Activity中,找到setcontentview(R.layout.xxxxxx)


public class MainActivity extends AppCompatActivity {

 @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_dp);
//ScreenAdapterTools.getInstance().reset(this);//如果希望android7.0分屏也适配的话,加上这句
//在setContentView();后面加上适配语句
ScreenAdapterTools.getInstance().loadView(getWindow().getDecorView());

}
}

(2.)在Fragment或recyclerview,listview或gridview,viewpager,自定义view等等等,只要能找到布局填充器(自定义view完全是代码绘制的没有用布局填充器怎么办?往下看)


public class TestFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.test_px, container, false);
//拿到布局填充器返回的view后
ScreenAdapterTools.getInstance().loadView(view);
return view;
}
}

注: 1.自定义view的话,在 ScreenAdapterTools.getInstance().loadView(view); 外面包裹一层判断如下,不然在使用自定义view编写布局文件时预览xml会有问题!但不影响真机运行效果.


if (!isInEditMode()) {
ScreenAdapterTools.getInstance().loadView(view);
}

2.完全代码绘制的自定义view怎么办? 比如说我绘制了个半径为100dp的圆,在代码里找到获取半径属性值circleRadius的地方

circleRadius = ScreenAdapterTools.getInstance().loadCustomAttrValue(circleRadius);

(3.)现在打开你的布局文件,并且打开预览,点击预览上部的小手机图标选择和你设计图匹配的模拟器,然后就可以按照设计图测量并编写布局文件,测量和编写的单位用px还是dp取决于你清单文件中的meta_data中unit填写的值,暂时只支持宽 高 padding layout_margin 字体大小 这几个属性(如果有minmaxWidthHeight这种属性值,适配时…loadView(…view,new CustomConversion()),如果有其他需要的属性值,请自行继承IConversion和AbsLoadViewHelper编写),布局文件完成后,你看到的预览是什么样,各种真机运行出来就是什么样.

原理

那些长篇大论的文章我也不想提,想必读者已经在别处看疯了,知道几个最简单的概念用起来就可以了

px是分辨率的单位 比如现在主流手机分辨率1080*1920.

dp是安卓开发专有的单位 在 不同的手机下 1dp = 不同的 px.

sp是字体大小(前面清单文件中要求字体也用dp或者px),sp随系统字体大小变化而变化,但据我观察,像微信qq这些app的字体是不随系统显示字体大小变化的.

本库是按照设计图的宽度的值(单位px)和对应标准dpi来适配的(手机实际宽度相对于设计图增加或减少,高度同比例(这的比例是宽度增加或减少的比例)增加或减少),所有的布局控件都按这个比例(手机实际宽度/设计图宽度)来适配,在不同的分辨率,不同ppi(手机屏幕密度,又称为dpi),不同最小宽度(有的人喜欢去调开发者选项下面的最小宽度,主流手机默认为360dp)的手机下都做到了适配。


原文发布时间为:2018-08-30

本文作者:Gxinyu

本文来自云栖社区合作伙伴“终端研发部”,了解相关信息可以关注“终端研发部”。

相关文章
|
2月前
|
JavaScript Android开发
使用贝叶斯曲线滑动安卓屏幕(autojsPro7)
使用贝叶斯曲线滑动安卓屏幕(autojsPro7)
119 0
|
2月前
|
XML Java Android开发
Android Studio App开发之捕获屏幕的变更事件实战(包括竖屏与横屏切换,回到桌面与切换到任务列表)
Android Studio App开发之捕获屏幕的变更事件实战(包括竖屏与横屏切换,回到桌面与切换到任务列表)
86 0
|
10月前
|
编解码 开发工具 Android开发
Android平台RTSP轻量级服务|RTMP推送摄像头或屏幕之音频接口设计
好多开发者在做Android平台录像或者RTSP轻量级服务、RTMP推送相关模块时,对需要设计哪些常用接口会心存疑惑,本文主要以大牛直播SDK(官方)为例,简单介绍下Android平台直播推送SDK所有音频相关的接口,感兴趣的开发者可以看看。
|
2月前
|
编解码 Java Android开发
Android技能树 — 屏幕适配小结,2024年最新android开发环境搭建教程
Android技能树 — 屏幕适配小结,2024年最新android开发环境搭建教程
|
2月前
|
开发工具 Android开发
rk平台Android12屏幕永不休眠
rk平台Android12屏幕永不休眠
43 1
|
2月前
|
Shell Android开发
ADB更改Android设备屏幕显示方向
ADB更改Android设备屏幕显示方向
115 5
|
2月前
|
编解码 Android开发
Android 屏幕适配
Android 屏幕适配
50 4
|
2月前
|
Android开发
获取Android设备屏幕大小
获取Android设备屏幕大小
33 3
|
2月前
|
XML Java Android开发
Android系统 添加动态控制屏幕方向、强制APP横竖屏方向
Android系统 添加动态控制屏幕方向、强制APP横竖屏方向
142 1
|
2月前
|
Android开发
Android 新建一个lunch项(全志方案)
Android 新建一个lunch项(全志方案)
40 0