最近在学习网易云课堂的安卓开发课程时,网易的老师提供了网易云音乐的屏幕适配终极解决方案,在看过了dimen适配、density适配、百分比布局适配、自定义缩放布局适配等各种适配方案之后,这套方案的简单、暴力、直接的程度让人瞠目结舌。
屏幕适配一直是每一个Android开发者都曾为之蛋疼的问题,Android机型千千万,各种屏幕分辨率,大到电视机,小到手表,当然,光是各种手机的分辨率都已经让我们头大。
我一开始使用过dimen适配,这种方案的好处是适配交由系统根据手机分辨率自动读取不同的配置来完成,开发者无需手动处理任何细节。但坏处也很明显,由于对于各种分辨率,为了保证能最大精度的适配,我们要写一大堆的dimen文件,当然,直接用工具生成即可,主要问题是增大了安装包的大小。
后来接触了density适配,通过动态修改手机的density来实现,但这种方式的缺陷是,有些厂商的手机不允许修改density的操作,再者,修改density一般是对宽度进行适配,而高度的适配则需要单独处理,否则有可能出现垂直方向显示不全的问题,这跟设备的屏幕比例有关,虽然可以通过加个ScrollView解决,但总体来说还是比较繁琐,效果也只是差强人意。
再来是百分比布局适配,谷歌官方也有提供了这套解决方案,在GitHub上可以找到,并且还有开发者对其进行了进一步封装完善。由于UI小姐姐给我们的效果图一般都是直接用像素(px)做单位来设计和标注的,所以如果要使用百分比布局,那对于大部分的元素我们都需要手动计算其横纵占比,这无疑增加了UI和开发之间沟通和实现成本。另外,我们所有的布局文件中用到的RelativeLayout、FrameLayout、LinearLayout等这些都必须替换成对应的百分比布局容器,对于自定义View也不太友好。
而关于自定义缩放布局,和百分比布局的原理其实差不多,只不过它不需要自定义属性,只是在onMeasure方法中通过当前屏幕分辨率与设计分辨率的横纵缩放比,对子控件的width、height、padding、margin等属性进行相应的缩放。需要注意的是,子控件的所有属性值必须使用像素作为单位。但这种方案和上面百分比布局面临着同样的问题。
最简单的方案,其实也是最老实,没有任何花里胡哨的东西。我们知道,适配最求的最终效果就是UI一致性,无论是在手表还是手机还是电视上,我们的app页面都是一样的效果。
这种方案其实是将自定义缩放布局的onMeasure中的计算部分单独封装,不再交由布局来处理,而是封装成单独的工具类,对外提供各种布局的适配接口。
接下来上代码:
UIUtils.java
public class UIUtils {
/**
* 标准值 正常情况下应该保存在配置文件中
* 设计参考的分辨率,由UI小姐姐提供
*/
private static final float STANDARD_WIDTH = 1080f;
private static final float STANDARD_HEIGHT = 1920f;
/**
* 实际设备信息
*/
public float displayMetricsWidth;
public float displayMetricsHeight;
/**
* 状态栏高度
*/
private int stateBarHeight;
public int getStateBarHeight() {
return stateBarHeight;
}
private static UIUtils instance ;
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 || displayMetricsHeight == 0){
// 在这里得到设备的真实值
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
int 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);
}
stateBarHeight = getSystemBarHeight(context);
}
}
/**
* 获取水平缩放系数
*/
public float getHorizontalScaleValue(){
return (displayMetricsWidth)/STANDARD_WIDTH;
}
/**
* 获取垂直缩放系数
*/
public float getVerticalScaleValue(){
return displayMetricsHeight/(STANDARD_HEIGHT-stateBarHeight);
}
/**
* 计算状态框的高度
*/
private int getSystemBarHeight(Context context){
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
int height = context.getResources().getDimensionPixelSize(resourceId);
if (height != -1) {
return height;
}
return getValue(context,"com.android.internal.R$dimen","system_bar_height",48);
}
/**
* 通过resourceId获取状态栏高度失败后,尝试使用反射来获取
*/
private int getValue(Context context, String dimeClass, String system_bar_height, int defaultValue) {
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;
}
/**
* 获取适配后的宽度
* 例如传入200宽度,单位为像素,计算其在当前设备上应显示的像素宽度
*/
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-stateBarHeight));
}
}
需要注意的就是要把状态栏高度考虑进去,这一点对于刘海屏适配极其重要,如有必要,还需将底部的虚拟按键高度也考虑进去,读者可以自行完善。
上述工具类只是完成了缩放系数相关的计算工作,我们还需要一个工具来来为各种布局提供适配接口。
ViewCalclateUtil.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)
{
layoutParams.width = UIUtils.getInstance().getWidth(width);
}
else
{
layoutParams.width = width;
}
if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT)
{
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);
}
}
/**
* FrameLayout
*/
public static void setViewFrameLayoutParam(View view, int width, int height, int topMargin, int bottomMargin, int lefMargin, int rightMargin) {
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) view.getLayoutParams();
if (width != RelativeLayout.LayoutParams.MATCH_PARENT && width != RelativeLayout.LayoutParams.WRAP_CONTENT)
{
layoutParams.width = UIUtils.getInstance().getWidth(width);
}
else
{
layoutParams.width = width;
}
if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT)
{
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);
}
/**
* 设置view的padding
*/
public static void setViewPadding(View view, int topPadding, int bottomPadding, int leftPadding, int rightPadding) {
view.setPadding(UIUtils.getInstance().getWidth(leftPadding),
UIUtils.getInstance().getHeight(topPadding),
UIUtils.getInstance().getWidth(rightPadding),
UIUtils.getInstance().getHeight(bottomPadding));
}
/**
* 设置字号
*/
public static void setTextSize(TextView view, int size) {
view.setTextSize(TypedValue.COMPLEX_UNIT_PX, UIUtils.getInstance().getHeight(size));
}
/**
* 设置LinearLayout中 view的高度宽度
*/
public static void setViewLinearLayoutParam(View view, int width, int height) {
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) view.getLayoutParams();
if (width != RelativeLayout.LayoutParams.MATCH_PARENT && width != RelativeLayout.LayoutParams.WRAP_CONTENT)
{
layoutParams.width = UIUtils.getInstance().getWidth(width);
}
else
{
layoutParams.width = width;
}
if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT)
{
layoutParams.height = UIUtils.getInstance().getHeight(height);
}
else
{
layoutParams.height = height;
}
view.setLayoutParams(layoutParams);
}
/**
* ViewGroup,通用
*/
public static void setViewGroupLayoutParam(View view, int width, int height) {
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (width != RelativeLayout.LayoutParams.MATCH_PARENT && width != RelativeLayout.LayoutParams.WRAP_CONTENT)
{
layoutParams.width = UIUtils.getInstance().getWidth(width);
}
else
{
layoutParams.width = width;
}
if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT)
{
layoutParams.height = UIUtils.getInstance().getHeight(height);
}
else
{
layoutParams.height = height;
}
view.setLayoutParams(layoutParams);
}
/**
* 设置LinearLayout中 view的高度宽度
*/
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)
{
layoutParams.width = UIUtils.getInstance().getWidth(width);
}
else
{
layoutParams.width = width;
}
if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT)
{
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);
}
}
几个方法都很简单,只是进行简单的乘法计算。同样的,读者也可以对其进行完善和优化,例如,当前的宽高都是从外部传入的,方法调用时就比较冗长,所以我们可以直接在写布局文件时就用像素做单位来设置所有的属性,然后在适配接口中直接读取对应的值进行缩放。
使用起来极其简单,传入待修改的View及对应的UI设计参考值即可。
ViewCalculateUtil.setViewLayoutParam(mToolbar, 1080, 168 + UIUtils.getInstance().getStateBarHeight(), 0, 0, 0, 0);
ViewCalculateUtil.setViewLayoutParam(mLvHeaderDetail, 1080, 380, 72, 0, 52, 0);
ViewCalculateUtil.setViewLinearLayoutParam(mHeaderImageItem,380,380);
适配这个问题我们总想着用各种复杂的手段去实现,什么修改系统属性啊,自定义适配布局啊等等,而我们都知道适配的方法和原理,无非就是缩放。为了让别人用的时候看起来高大尚,实际上只是把问题弄复杂了,从而滋生了各种各样的麻烦。所以,我们舍弃掉花里胡哨的东西,我们要做的顶多就是写个循环遍历子节点,设置一下每个子节点的宽高就完事儿了。最朴实的往往最简单好用。