布局适配
文章目录
首先要将布局适配的时候,我们要先讲一下Android中屏幕中的各种数据的关系
1.什么是屏幕尺寸、像素、分辨率和像素密度,密度无关像素?
- 屏幕尺寸: 我们通常用来形容机型几寸的屏幕,其实就是屏幕对角线的长度,1英寸=2.54厘米。
- 像素:用来标识像素点的个数单位。1px 代表一个显示设备中的一个像素点.
- 分辨率: 通常用来和清晰度有关,是横纵方向上存在的像素点数:如1080*1920。
- 像素密度(单位dpi):指的是每英寸上存在的存在的像素点数,比如: 在5寸、分辨率10801920的手 机上像素密度为 Math.sqrt(Math.pow(1080, 2) + Math.pow(1920, 2)) / 5 约为 440dip
(**实际计算中 ,因为存在导航栏,虚拟按键等原因。分辨率会低于10801920,如果需要具体数值可具体分析**) - 密度(density):基准比例,密度计算方式 dpi/160;
- 密度无关像素 ,即我们常说的dp或者说dip是一样的,以160dpi为基准。在160dpi设备上1dp=1px,在240dpi设备上1dp=1.5px,以此类推,它的大小是由操作系统根据手机屏幕像素密度动态渲染出来的,1dp 对应多少 px 在不同的设备上,可能是不一致的。计算方式 px=dpi/160*dp
2.Android中帮我们实现的适配(dp)?
我们首先要明白一点事情是 无论在开发时使用什么尺寸单位,最终都需要转为像素(PX)
其次Google为Android的内置了几个默认的 Dpi。为了解决这个问题, Android 中 ,在特定的分辨率下自动调用,也可以手动在配置文件中修改。
在使用dp做单位时候,系统通过当前dpi在不同分辨率上最终会计算成不同的px值,
比如 120dp在上述几种默认配置中对应的大小分别(假定120dp为宽度):
90px 120px 180px 240px 360px
在可视化界面中的120dp对应的大小在上述几种相同尺寸不同分辨率中相对应的会增加
因此在不同的分辨率的手机上可以做初步的适配。
当前Android手机的厂商越来越多,相对应的分辨率/dpi都存在越来越多种类,所以单纯的采用dp形式已经不能满足我们的适配规则。
2.目前流行的几种布局适配规则
1. 百分比适配
最早由Google方便开发者进行适配开发,主要依赖
implementation ‘com.android.support:percent:27.0.2’
这里引用两个其他作者的文章进行表述
正常使用方式:https://www.jianshu.com/p/0c2a8db91bda
hongyang大神的进阶版:
http://blog.youkuaiyun.com/lmj623565791/article/details/46767825;出自:【张鸿洋的博客】
但是百分比适配缺点是:麻烦!
比如UI设计图给的都是px dp等,百分比需要自行计算;Android studio语法检查中xml布局宽/高属性值不能为空等等等等
2. AutoUtils (使用px适配)
先介绍一下这种适配方式的原理和思路:
根据设计图的标注来讲 ,10801920在使用px的情况下,假定有一个TextView宽为540px,会占据屏幕的一半,如果在其他分辨率的屏幕上如果宽度依旧为540px的话,显示的位置肯定超过或者小于屏幕一半。如果能动态的对540px进行处理、更换到对应分辨率下的数值大小既可以完成适配。
以适配480800为例,540px的宽度在该设备上应该为:540*480/1080 = 240px,既设置240px的宽度即可完成适配。
核心的思路和算法就是
**新的宽(高)度 = 设计图宽(高)px*实际分辨率宽(高)/设计图分辨率宽(高)**
下面贴出该类的内容和该类的使用方式
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.util.TypedValue;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class AutoUtils {
public static int displayWidth;
public static int displayHeight;
private static int designWidth;
private static int designHeight;
private static double textPixelsRate;
public static void setSize(Activity act, boolean hasStatusBar, int designWidth, int designHeight) {
if (act == null || designWidth < 1 || designHeight < 1)
return;
Display display = act.getWindowManager().getDefaultDisplay();
int width = display.getWidth();
int height = display.getHeight();
if (hasStatusBar) {
height -= getStatusBarHeight(act);
}
AutoUtils.displayWidth = width;
AutoUtils.displayHeight = height;
AutoUtils.designWidth = designWidth;
AutoUtils.designHeight = designHeight;
double displayDiagonal = Math.sqrt(Math.pow(AutoUtils.displayWidth, 2) + Math.pow(AutoUtils.displayHeight, 2));
double designDiagonal = Math.sqrt(Math.pow(AutoUtils.designWidth, 2) + Math.pow(AutoUtils.designHeight, 2));
AutoUtils.textPixelsRate = displayDiagonal / designDiagonal;
}
public static int getStatusBarHeight(Context context) {
int result = 0;
try {
int resourceId = context.getResources().getIdentifier(
"status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
} catch (Resources.NotFoundException e) {
e.printStackTrace();
}
return result;
}
public static void auto(Activity act) {
if (act == null || displayWidth < 1 || displayHeight < 1)
return;
View view = act.getWindow().getDecorView();
auto(view);
}
public static void auto(View view) {
if (view == null || displayWidth < 1 || displayHeight < 1)
return;
AutoUtils.autoTextSize(view);
AutoUtils.autoSize(view);
AutoUtils.autoPadding(view);
AutoUtils.autoMargin(view);
if (view instanceof ViewGroup) {
auto((ViewGroup) view);
}
}
private static void auto(ViewGroup viewGroup) {
int count = viewGroup.getChildCount();
for (int i = 0; i < count; i++) {
View child = viewGroup.getChildAt(i);
if (child != null) {
auto(child);
}
}
}
public static void autoMargin(View view) {
if (!(view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams))
return;
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
if (lp == null)
return;
lp.leftMargin = getDisplayWidthValue(lp.leftMargin);
lp.topMargin = getDisplayHeightValue(lp.topMargin);
lp.rightMargin = getDisplayWidthValue(lp.rightMargin);
lp.bottomMargin = getDisplayHeightValue(lp.bottomMargin);
}
public static void autoPadding(View view) {
int l = view.getPaddingLeft();
int t = view.getPaddingTop();
int r = view.getPaddingRight();
int b = view.getPaddingBottom();
l = getDisplayWidthValue(l);
t = getDisplayHeightValue(t);
r = getDisplayWidthValue(r);
b = getDisplayHeightValue(b);
view.setPadding(l, t, r, b);
}
public static void autoSize(View view) {
ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp == null)
return;
boolean isSquare = false;
if (lp.width == lp.height) {
isSquare = true;
}
if (lp.width > 0) {
lp.width = getDisplayWidthValue(lp.width);
}
if (lp.height > 0) {
lp.height = getDisplayHeightValue(lp.height);
}
if (isSquare) {
if (lp.width > lp.height) {
lp.width = lp.height;
} else {
lp.height = lp.width;
}
}
}
public static void autoTextSize(View view) {
if (view instanceof TextView) {
double designPixels = ((TextView) view).getTextSize();
double displayPixels = textPixelsRate * designPixels;
((TextView) view).setIncludeFontPadding(false);
((TextView) view).setTextSize(TypedValue.COMPLEX_UNIT_PX, (float) displayPixels);
}
}
public static int getDisplayWidthValue(int designWidthValue) {
if (Math.abs(designWidthValue) < 2) {
return designWidthValue;
}
return designWidthValue * displayWidth / designWidth;
}
public static int getDisplayHeightValue(int designHeightValue) {
if (Math.abs(designHeightValue) < 2) {
return designHeightValue;
}
return designHeightValue * displayHeight / designHeight;
}
public static float getDisplayTextSize(float designTextSize) {
return (float) (AutoUtils.textPixelsRate * designTextSize);
}
}
使用方式:
在Acitivty/Fragment/Dialog的setContentView前使用
-
AutoUtils.setSize(this, false, 750, 1344); 对应参数分别为:
activity:用来获取当前设备的屏幕分辨率参数
hasStatusBar :设计图中是否包含状态栏,如果true,在计算中会去除状态栏高度
designWidth:设计图宽度
designHeight:设计图高度 -
Activity Fragment Dialog中
AutoUtils.auto(this)/AutoUtils.auto(view); 即可完成适配
优点:可全局统一,方便使用,
缺点:字体采用px后,不能适配Android系统的字体缩放大小;
在绘制前进行数据计算,会产生部分性能损耗
对自定义控件某些属性需要自行适配
3. SmallestWidth适配
1. 原理
该适配方式通过屏幕的最小宽度进行适配
px=dpi/160*dp
既
在Android 代码中最小宽度获取方式:
getResources().getConfiguration().smallestScreenWidthDp)
资源文件的适配方式如下
├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-sw320dp
│ ├── ├──values-sw360dp
│ ├── ├──values-sw400dp
│ ├── ├──values-sw411dp
│ ├── ├──values-sw480dp
│ ├── ├──...
│ ├── ├──values-sw600dp
│ ├── ├──values-sw640dp
在不同的宽度的资源文件中,获取R.dimen.(dp)对应不同的dp值,Android会默认筛选对应宽度的资源文件,如果资源文件中没有,则会自动向下匹配筛选,没有的时候会采用values中的默认值。
原理:根据一个基准的最小宽度分辨率,分别计算其他最小宽度下的1dp转化后对应的dp数值
直接上图:
通过对基准宽度下的数值按比例扩大/缩小,进行适配,达到适配的目的。
优点:
不会有任何性能的损耗
适配范围可自由控制,不会影响其他三方库
缺点:
需要添加对应的资源文件,会略微增加apk体积;
当没有该值的资源文件时候因为会产生误差;
2.快速生成value-sw()dp
- Android studio 添加插件:smallestWitdh
- 添加成功后:在Tools-smallestWitdh 或者 option+p 中打开
- 在界面中输入基准dp和需要生成的dp 、sp值,如果需要额外的宽度的文件通过add添加即可
4. 今日头条适配方案
通过修改系统的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) 来进行转换:
这里用到的DisplayMetrics正是从Resources中获得的。
再看看图片的decode,BitmapFactory#decodeResourceStream方法:
可见也是通过 DisplayMetrics 中的值来计算的。
当然还有些其他dp转换的场景,基本都是通过 DisplayMetrics 来计算的,这里不再详述。因此,想要满足上述需求,我们只需要修改 DisplayMetrics 中和 dp 转换相关的变量即可。
最终方案
下面假设设计图宽度是360dp,以宽维度来适配。
那么适配后的 density = 设备真实宽(单位px) / 360,接下来只需要把我们计算好的 density 在系统中修改下即可,代码实现如下:
// 系统的Density
private static float sNoncompatDensity;
// 系统的ScaledDensity
private static float sNoncompatScaledDensity;
public static void setCustomDensity(Activity activity, Application application) {
DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
if (sNoncompatDensity == 0) {
sNoncompatDensity = displayMetrics.density;
sNoncompatScaledDensity = displayMetrics.scaledDensity;
// 监听在系统设置中切换字体
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
sNoncompatScaledDensity=application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
// 此处以360dp的设计图作为例子
float targetDensity=displayMetrics.widthPixels/360;
float targetScaledDensity=targetDensity*(sNoncompatScaledDensity/sNoncompatDensity);
int targetDensityDpi= (int) (160 * targetDensity);
displayMetrics.density = targetDensity;
displayMetrics.scaledDensity = targetScaledDensity;
displayMetrics.densityDpi = targetDensityDpi;
DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
}
这样,宽度适配就已经完成啦,只需要在Activity中调用就行了,必须在setContentView()之前!
如果需要适配高度,今日头条指出只要按照同样的方法做高度适配就可以了!
今日头条的适配方式进阶版本
由JessYan开发者开源的项目。
在此就不做更多的描述,下面放上作者的地址和github链接
附上作者文章链接:https://www.jianshu.com/u/1d0c0bc634db
附上作者的github地址:https://github.com/JessYanCoding/AndroidAutoSize