最近在学习网易的Android课程,网易的老师提供了网易云音乐的屏幕适配解决方案,主要有两种,17年前是采用自定义缩放布局,17年后是采用的是工具类发方案,现在这两种方案在网易云音乐中是同时存在的。互不影响。在对比dimen适配、density适配、百分比布局适配等各种适配方案之后,网易云音乐的这两种方案在我们的项目中都是非常简洁高效的。
通常所说的像素,就是CCD/CMOS上光电感应元件的数量,一个感光元件经过感光,光电信号转换,A/D转换等步骤以后,在输出的照片上就形成一个点,我们如果把影像放大数倍,会发现这些连续色调其实是由许多色彩相近的小方点所组成,这些小方点就是构成影像的最小单位“像素”(Pixel)。
手机在横向、纵向上的像素点数总和,一般描述成宽高 ,即横向像素点个数乘以纵向像素点个数。
手机对角线的物理尺寸,单位 英寸(inch),一英寸大约2.54cm,常见的尺寸有4.7寸、5寸、5.5寸、6寸。
每英寸长度上像素点个数。
例如每英寸内有160个像素点,则其像素密度为160dpi。
公式: 像素密度=像素/尺寸 (dpi=px/in)
每英寸长度上还有160个像素点,即称为标准屏幕像素密度(mdpi)。
手机真实像素密度与标准屏幕像素密度(160dpi)的比值。官方给出的0.75、1、1.5、2、3、4,即对应120dpi、160dpi、240dpi、320dpi、480dpi、640dpi
density-independent pixel,叫dp或dip,与终端上的实际物理像素点无关。可以保证在不同屏幕像素密度的设备上显示相同的效果,是安卓特有的长度单位。
scale-independent pixel,叫sp或sip,字体大小专用单位,可根据字体大小首选项进行缩放;
推荐使用12sp、14sp、18sp、22sp作为字体大小,不推荐使用奇数和小数,容易造成精度丢失,12sp以下字体太小。
适配交由系统根据手机分辨率自动读取不同的配置来完成,开发者无需手动处理任何细节。但坏处也很明显,由于对于各种分辨率,为了保证能最大精度的适配,我们要写一大堆的dimen文件,当然,直接用工具生成即可,主要问题是增大了安装包的大小。
通过动态修改手机的density来实现,但这种方式的缺陷是,有些厂商的手机不允许修改density的操作,再者,修改density一般是对宽度进行适配,而高度的适配则需要单独处理,否则有可能出现垂直方向显示不全的问题,这跟设备的屏幕比例有关,虽然可以通过加个ScrollView解决,但总体来说还是比较繁琐,效果也只是差强人意。
谷歌官方有提供了这套解决方案,在GitHub上可以找到,并且还有开发者对其进行了进一步封装完善。由于UI小姐姐给我们的效果图一般都是直接用像素(px)做单位来设计和标注的,所以如果要使用百分比布局,那对于大部分的元素我们都需要手动计算其横纵占比,这无疑增加了UI和开发之间沟通和实现成本。另外,我们所有的布局文件中用到的RelativeLayout、FrameLayout、LinearLayout等这些都必须替换成对应的百分比布局容器,对于自定义View也不太友好。
这种方式是通过继承布局控件,重写onMeasure()
方法,在此方法中通过当前屏幕分辨率与设计分辨率的横纵缩放比,对子控件的width、height、padding、margin等属性进行相应的缩放。以RelativeLayout为例:
public class UIRelativeLayout extends RelativeLayout {
private boolean flag=true;
public static final float STANDARD_WIDTH=1080f;
public static final float STANDARD_HEIGHT=1920f;
public UIRelativeLayout(Context context) {
super(context);
}
public UIRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public UIRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (flag) {
flag = false;
float scaleX = getHorizontalScaleValue();
float scaleY = getVerticalScaleValue();
int childCount = this.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = this.getChildAt(i);
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
layoutParams.width=(int) (layoutParams.width * scaleX);
layoutParams.height = (int) (layoutParams.height * scaleY);
layoutParams.leftMargin = (int) (layoutParams.leftMargin * scaleX);
layoutParams.rightMargin = (int) (layoutParams.rightMargin * scaleX);
layoutParams.topMargin = (int) (layoutParams.topMargin * scaleY);
layoutParams.bottomMargin = (int) (layoutParams.bottomMargin * scaleY);
}
}
}
public float getHorizontalScaleValue(){
return ((float)(displayMetricsWidth)) / STANDARD_WIDTH;
}
public float getVerticalScaleValue(){
return ((float)(displayMetricsHeight))/(STANDARD_HEIGHT-systemBarHeight);
}
}
自定义好布局文件之后,我们只需要在xml中是用我们自定义的这个类即可。这里需要注意的是,子控件的所有属性值必须使用像素作为单位。
这种方案其实是将自定义缩放布局的onMeasure()
中的计算部分单独封装,不再交由布局来处理,而是封装成单独的工具类,对外提供各种布局的适配接口。
工具类UIUtils.java
public class UIUtils {
private static UIUtils instance;
//标准宽高 以UI图为准
public static final float STANDARD_WIDTH = 1080f;
public static final float STANDARD_HEIGHT = 1920f;
public static float displayMetricsWidth;
public static float displayMetricsHeight;
public static float systemBarHeight;
public static UIUtils getInstance(Context context){
if (instance == null){
instance = new UIUtils(context);
}
return instance;
}
public static UIUtils getInstance(){
if (instance == null){
throw new RuntimeException("UiUtil应该先调用含有构造方法进行初始化");
}
return instance;
}
private UIUtils(Context context){
//计算缩放系数
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
if (displayMetricsWidth == 0.0f || displayMetricsHeight == 0.0f){
//获取设备的真实宽高
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
systemBarHeight = getSystemBarHeight(context);
//横屏
if (displayMetrics.widthPixels > displayMetrics.heightPixels){
displayMetricsWidth = (float)(displayMetrics.heightPixels);
displayMetricsHeight = (float)(displayMetrics.widthPixels-systemBarHeight);
}else {
//竖屏
displayMetricsWidth = (float)(displayMetrics.widthPixels);
displayMetricsHeight = (float)(displayMetrics.heightPixels-systemBarHeight);
}
}
}
/**
* 计算状态栏高度
* @param context context
* @return 高度
*/
private int getSystemBarHeight(Context context){
return getValue(context,"com.android.internal.R$dimen","system_bar_height",48);
}
private int getValue(Context context, String dimeClass, String system_bar_height, int defaultValue) {
// com.android.internal.R$dimen system_bar_height 状态栏的高度
try {
Class> clz=Class.forName(dimeClass);
Object object = clz.newInstance();
Field field=clz.getField(system_bar_height);
int id=Integer.parseInt(field.get(object).toString());
return context.getResources().getDimensionPixelSize(id);
} catch (Exception e) {
e.printStackTrace();
}
return defaultValue;
}
/**
* 获取适配后的宽度
* 例如传入100宽度,单位为像素,计算其在当前设备上应显示的像素宽度
*/
public int getWidth(int width) {
return Math.round((float)width * displayMetricsWidth / STANDARD_WIDTH);
}
/**
* 获取适配后的高度
*/
public int getHeight(int height) {
return Math.round((float)height * displayMetricsHeight / (STANDARD_HEIGHT-systemBarHeight));
}
}
在这里我们需要把状态栏高度考虑进去,这一点对于刘海屏适配是很重要,如果需要,还可以将底部的虚拟按键高度也考虑进去。
上述工具类只是完成了缩放系数相关的计算工作,我们还需要一个工具来来为各种布局提供适配接口。
各种布局接口类ViewCalculateUtil.java
public class ViewCalculateUtil {
/**
* RelativeLayout
* 根据屏幕的大小设置view的高度,间距
*/
public static void setViewLayoutParam(View view, int width, int height, int topMargin, int bottomMargin, int lefMargin, int rightMargin)
{
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
if (layoutParams != null)
{
if (width != RelativeLayout.LayoutParams.MATCH_PARENT && width != RelativeLayout.LayoutParams.WRAP_CONTENT && width != RelativeLayout.LayoutParams.FILL_PARENT)
{
layoutParams.width = UIUtils.getInstance().getWidth(width);
}
else
{
layoutParams.width = width;
}
if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT && height != RelativeLayout.LayoutParams.FILL_PARENT)
{
layoutParams.height = UIUtils.getInstance( ).getHeight(height);
}
else
{
layoutParams.height = height;
}
layoutParams.topMargin = UIUtils.getInstance( ).getHeight(topMargin);
layoutParams.bottomMargin = UIUtils.getInstance( ).getHeight(bottomMargin);
layoutParams.leftMargin = UIUtils.getInstance( ).getWidth(lefMargin);
layoutParams.rightMargin = UIUtils.getInstance( ).getWidth(rightMargin);
view.setLayoutParams(layoutParams);
}
}
/**
* 设置LinearLayout中 view的高度宽度
*
* @param view
* @param width
* @param height
*/
public static void setViewLinearLayoutParam(View view, int width, int height, int topMargin, int bottomMargin, int lefMargin,
int rightMargin)
{
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) view.getLayoutParams();
if (width != RelativeLayout.LayoutParams.MATCH_PARENT && width != RelativeLayout.LayoutParams.WRAP_CONTENT && width != RelativeLayout.LayoutParams.FILL_PARENT)
{
layoutParams.width = UIUtils.getInstance( ).getWidth(width);
}
else
{
layoutParams.width = width;
}
if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT && height != RelativeLayout.LayoutParams.FILL_PARENT)
{
layoutParams.height = UIUtils.getInstance( ).getHeight(height);
}
else
{
layoutParams.height = height;
}
layoutParams.topMargin = UIUtils.getInstance( ).getHeight(topMargin);
layoutParams.bottomMargin = UIUtils.getInstance( ).getHeight(bottomMargin);
layoutParams.leftMargin = UIUtils.getInstance( ).getWidth(lefMargin);
layoutParams.rightMargin = UIUtils.getInstance( ).getWidth(rightMargin);
view.setLayoutParams(layoutParams);
}
public static void setTextSize(TextView view, int size)
{
view.setTextSize(TypedValue.COMPLEX_UNIT_PX, UIUtils.getInstance().getHeight(size));
}
}
在这里我只封装了RelativeLayout
和LinearLayout
,其他的布局可以自行添加。
工具类的使用
UIUtils.getInstance(this.getApplicationContext());
setContentView(R.layout.activity_main);
tvText3 = findViewById(R.id.tvText3);
tvText4 = findViewById(R.id.tvText4);
ViewCalculateUtil.setViewLinearLayoutParam(tvText3, 540, 100, 0, 0, 0, 0);
ViewCalculateUtil.setViewLinearLayoutParam(tvText4, 1080, 100, 0, 0, 0, 0);
ViewCalculateUtil.setTextSize(tvText3,30);
适配的方法和原理,无非就是缩放。这两种方式的核心也是缩放。 采用这两种方式, 大到电视机,小到智能手表,我们都可以方便地将手机布局与UI设计图保持一致。无论是在手表还是手机还是电视上,我们看到的app页面都是一样的效果。