Android 屏幕适配总结

2019-11-23  本文已影响0人  有没有口罩给我一个
为什么需要屏幕适配?

所以系统中,最后不管你是使用dp还是还是sp最终还是会被转为px,所以我们适配的方案就有了,下面我们来看看一些概念。

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

而dpi是根据屏幕真实的分辨率和尺寸来计算的,每个设备都可能不一样的,我觉得dpi更像是作为一种标准出现。例如Android手机的:160dpi、320dpi、440dpi、480dpi,而density 在每个设备上都是固定的,dpi / 160 = density,屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度。

自定义像素适配

自定义像素适配就是以一个特定宽度和高度尺寸的设备为参考,实际上就以设计师给的设计图为参考,根据这个参考值换算出缩放比例,举个例子:

假设参考尺寸为720px*1280px,


自定义像素.png

我们想要的效果是360x360在720px*1280px的显示的UI元素是屏幕的一般,在1080x1920上显示的是540x540,但是在1080x1920上显示却只有1/3的宽度这并不是我么想要的效果,计算方法:

public class UiPx2PxScaleAdapt {

private static UiPx2PxScaleAdapt mUIAdaptUtil;

private static float DEFAULT_STANDARD_WIDTH = 375;//px
private static float DEFAULT_STANDARD_HEIGHT = 667;

//设计图参考尺寸
private static float standardWidth = DEFAULT_STANDARD_WIDTH;//px
private static float standardHeight = DEFAULT_STANDARD_HEIGHT;

//这里是屏幕显示宽高
private static int mDisplayWidth;
private static int mDisplayHeight;


private UiPx2PxScaleAdapt(Context context) {
    if (mDisplayWidth == 0 || mDisplayHeight == 0) {
        WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics metrics = new DisplayMetrics();
        manager.getDefaultDisplay().getMetrics(metrics);
        if (metrics.widthPixels > metrics.heightPixels) {//横屏
            mDisplayWidth = metrics.heightPixels;
            mDisplayHeight = metrics.widthPixels;
        } else {
            mDisplayWidth = metrics.widthPixels;
            mDisplayHeight = metrics.heightPixels - getStatusBarHeight(context);//为了精确一点呢,可以把状态栏高度减掉
        }
    }
}

public static UiPx2PxScaleAdapt adapt(Context context) {
    if (UiPx2PxScaleAdapt.mUIAdaptUtil == null) {
        synchronized (UiPx2PxScaleAdapt.class) {
            if (UiPx2PxScaleAdapt.mUIAdaptUtil == null) {
                UiPx2PxScaleAdapt.mUIAdaptUtil = new UiPx2PxScaleAdapt(context.getApplicationContext());

            }
        }
    }
    return mUIAdaptUtil;
}

public int getVerticalAdaptResult(int needValuePx) {
    return Math.round((needValuePx * getVerticalScale()));
}

public int getHorizontalAdaptResult(int needValuePx) {
    return Math.round(needValuePx * getHorizontalScale());
}

/**
 * 修改设计图参考尺寸
 *
 * @param standardWidth 设计图参考宽度 单位px
 */
public UiPx2PxScaleAdapt standardWidth(float standardWidth) {
    UiPx2PxScaleAdapt.standardWidth = standardWidth;
    return this;
}


/**
 * 修改设计图参考尺寸
 *
 * @param standardHeight 设计图参考高度 单位px
 */
public UiPx2PxScaleAdapt standardHeight(float standardHeight) {
    UiPx2PxScaleAdapt.standardHeight = standardHeight;
    return this;
}

/**
 * @return 获取水平方向的缩放比例
 */
public float getHorizontalScale() {
    return mDisplayWidth / standardWidth;
}

/**
 * @return 获取垂直方向的缩放比例
 */
public float getVerticalScale() {
    return mDisplayHeight / standardHeight;
}


private int getStatusBarHeight(Context context) {
    int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
    if (resId > 0) {
        return context.getResources().getDimensionPixelSize(resId);
    }
    return 0;
}

}

代码很简单,就是我们通过当前屏幕的宽和高,然后使用当前屏幕的宽和高除以参考尺寸得到缩放比例,然后在使用缩放比例乘以具体设置的尺寸。

<!--基本设计720,想要显示一半填写360px-->
<TextView
    android:layout_width="360px"
    android:layout_height="wrap_content"
    android:background="@color/colorAccent"
    android:text="@string/app_name" />

开发的时候只要按照设计图填写尺寸就行了最后封装成工具类。

public class UiAdaptCalPx2PxUtil {

public static void setTextSize(TextView view, int size) {
    int adaptResult = UiPx2PxScaleAdapt.adapt(view.getContext()).getVerticalAdaptResult(size);
    view.setTextSize(TypedValue.COMPLEX_UNIT_PX, adaptResult);
}

public static void setUIAdaptPx2Px(View dstView,
                                   int width,
                                   int height,
                                   int topMargin,
                                   int bottomMargin,
                                   int lefMargin,
                                   int rightMargin) {

    ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) dstView.getLayoutParams();

    if (width != ViewGroup.LayoutParams.MATCH_PARENT &&
            width != ViewGroup.LayoutParams.WRAP_CONTENT) {
        layoutParams.width =
                UiPx2PxScaleAdapt.adapt(dstView.getContext()).getHorizontalAdaptResult(width);
    } else {
        layoutParams.width = width;
    }


    if (height != ViewGroup.LayoutParams.MATCH_PARENT &&
            height != ViewGroup.LayoutParams.WRAP_CONTENT) {
        layoutParams.height =
                UiPx2PxScaleAdapt.adapt(dstView.getContext()).getVerticalAdaptResult(height);
    } else {
        layoutParams.height = height;
    }

    layoutParams.setMargins(
            UiPx2PxScaleAdapt.adapt(dstView.getContext()).getHorizontalAdaptResult(lefMargin),
            UiPx2PxScaleAdapt.adapt(dstView.getContext()).getVerticalAdaptResult(topMargin),
            UiPx2PxScaleAdapt.adapt(dstView.getContext()).getHorizontalAdaptResult(rightMargin),
            UiPx2PxScaleAdapt.adapt(dstView.getContext()).getVerticalAdaptResult(bottomMargin));

    dstView.setLayoutParams(layoutParams);

    dstView.setPadding(
            UiPx2PxScaleAdapt.adapt(dstView.getContext()).getHorizontalAdaptResult(dstView.getPaddingLeft()),
            UiPx2PxScaleAdapt.adapt(dstView.getContext()).getVerticalAdaptResult(dstView.getPaddingTop()),
            UiPx2PxScaleAdapt.adapt(dstView.getContext()).getHorizontalAdaptResult(dstView.getPaddingRight()),
            UiPx2PxScaleAdapt.adapt(dstView.getContext()).getVerticalAdaptResult(dstView.getPaddingBottom()));
}


/**
 * 在布局中指定尺寸的
 *
 * @param dstViews
 */
public static void setUIAdaptPx2PxLayout(View... dstViews) {
    if (dstViews.length <= 0) return;

    for (View dstView : dstViews) {
        ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) dstView.getLayoutParams();

        if (layoutParams == null) continue;

        setUIAdaptPx2Px(dstView, layoutParams.width, layoutParams.height,
                layoutParams.topMargin, layoutParams.bottomMargin,
                layoutParams.leftMargin, layoutParams.rightMargin
        );
    }
}

}

可能有人会说太麻烦了,那我要开始装逼了,根据上面介绍的,都是需要开发者手动去设置尺寸,那么有没有不用手动去设置呢?方案是人想出来的,一般我们开发者写布局UI都是无非就是会加载到DecorView的contentiew上不管是Dialog还是Activity或者Fragment,我们肯定能拿到content布局,或者DecorView的ContentView,那为何我们不通过content或者ContentView,遍历所有的child,从而修改布局尺寸呢?并且我们还可以把Activity或者Fragment抽取为BaseXXX,当我么加载布局时,做到无感自动完成适配?,但是这种方式有一个缺点就是,会把第三方库也给适配,会造成预想不到的效果,所以建议使用手动的方式。

 /**
 * 把适配放到Base中自动完成适配 指定的是开发者定义的布局的根布局
 *
 * @param contentView 或者开发者在xml中定义的根布局
 */
public static void setUIAdaptPx2PxContentView(View contentView) {
    if (contentView == null) return;
    if (contentView instanceof ViewGroup) {
        //修改ViewGroup本身的
        ViewGroup viewGroup = (ViewGroup) contentView;
        ViewGroup.MarginLayoutParams vplp = (ViewGroup.MarginLayoutParams) viewGroup.getLayoutParams();
        setUIAdaptPx2Px(viewGroup, vplp.width, vplp.height,
                vplp.topMargin, vplp.bottomMargin,
                vplp.leftMargin, vplp.rightMargin
        );

        // 修改child本身的
        int childCount = viewGroup.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = viewGroup.getChildAt(i);
            if (child instanceof ViewGroup) {
                setUIAdaptPx2PxContentView(child);
            } else {
                ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
                if (layoutParams == null) continue;
                setUIAdaptPx2Px(child, layoutParams.width, layoutParams.height,
                        layoutParams.topMargin, layoutParams.bottomMargin,
                        layoutParams.leftMargin, layoutParams.rightMargin
                );
            }
        }
    }
}

如果你使用Kotlin,那么这样使用:

view.adapt().adaptLeftMargin(13).adaptRightMargin(11).adaptTextSize(11)

Kotlin拓展函数:

fun View.adapt(): AdaptBuilder {
   if (id == NO_ID) id = generateViewId()
    val tag = getTag(id)
   if (tag == null) {
       val builder = AdaptBuilder(this)
       setTag(this.id, builder)
      return builder
  }
    return tag as AdaptBuilder
}

//AdaptBuilder

class AdaptBuilder(private val adaptView: View) {
private val lp: ViewGroup.MarginLayoutParams = adaptView.layoutParams as ViewGroup.MarginLayoutParams

fun adaptWidth(@Px width: Int): AdaptBuilder {
    if (width != ViewGroup.LayoutParams.MATCH_PARENT && width != ViewGroup.LayoutParams.WRAP_CONTENT) {
        lp.width = UiPx2PxScaleAdapt.getHorizontalAdaptResult(width.toFloat())
    } else {
        lp.width = width
    }
    return this
}


fun adaptHeight(@Px height: Int): AdaptBuilder {
    if (height != ViewGroup.LayoutParams.MATCH_PARENT && height != ViewGroup.LayoutParams.WRAP_CONTENT) {
        lp.height = UiPx2PxScaleAdapt.getVerticalAdaptResult(height.toFloat())
    } else {
        lp.height = height
    }
    return this
}


fun adaptTopMargin(@Px topMargin: Int): AdaptBuilder {
    lp.setMargins(
        lp.leftMargin,
        UiPx2PxScaleAdapt.getVerticalAdaptResult(topMargin.toFloat()),
        lp.rightMargin,
        lp.bottomMargin
    )
    return this
}

fun adaptLeftMargin(@Px lefMargin: Int): AdaptBuilder {
    lp.setMargins(
        UiPx2PxScaleAdapt.getHorizontalAdaptResult(lefMargin.toFloat()),
        lp.topMargin,
        lp.rightMargin,
        lp.bottomMargin
    )
    return this
}

fun adaptBottomMargin(@Px bottomMargin: Int): AdaptBuilder {
    lp.setMargins(
        lp.leftMargin,
        lp.topMargin,
        lp.rightMargin,
        UiPx2PxScaleAdapt.getVerticalAdaptResult(bottomMargin.toFloat())
    )
    return this
}

fun adaptRightMargin(@Px rightMargin: Int): AdaptBuilder {
    lp.setMargins(
        lp.leftMargin,
        lp.topMargin,
        UiPx2PxScaleAdapt.getHorizontalAdaptResult(rightMargin.toFloat()),
        lp.bottomMargin
    )
    return this
}

fun adaptPaddingLeft(@Px setPaddingLeft: Int): AdaptBuilder {
    adaptView.setPadding(
        UiPx2PxScaleAdapt.getHorizontalAdaptResult(setPaddingLeft.toFloat()),
        adaptView.paddingTop, adaptView.paddingRight, adaptView.bottom
    )
    return this
}

fun adaptPaddingRight(@Px setPaddingRight: Int): AdaptBuilder {
    adaptView.setPadding(
        adaptView.left,
        adaptView.paddingTop,
        UiPx2PxScaleAdapt.getHorizontalAdaptResult(setPaddingRight.toFloat()),
        adaptView.bottom
    )
    return this
}

fun adaptPaddingTop(@Px setPaddingTop: Int): AdaptBuilder {
    adaptView.setPadding(
        adaptView.left,
        UiPx2PxScaleAdapt.getVerticalAdaptResult(setPaddingTop.toFloat()),
        adaptView.paddingRight,
        adaptView.bottom
    )
    return this
}

fun adaptPaddingBootom(@Px setPaddingBottom: Int): AdaptBuilder {
    adaptView.setPadding(
        adaptView.left,
        adaptView.paddingTop,
        adaptView.paddingRight,
        UiPx2PxScaleAdapt.getVerticalAdaptResult(setPaddingBottom.toFloat())
    )
    return this
}


/**
 *字体缩放比列:按照方向适配,比如:如果你想要在换行时准确,那么就是用水平方向的,否则垂直
 */
fun adaptTextSize(size: Float, isVertical: Boolean = false): AdaptBuilder {
    if (adaptView is TextView) {
        val adaptResult = UiPx2PxScaleAdapt.getVerticalAdaptResult(size)
        val adaptResult2 = UiPx2PxScaleAdapt.getHorizontalAdaptResult(size)
        adaptView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (if (isVertical) adaptResult else adaptResult2).toFloat())
    }
    return this
}


/**
 * 如果是已经完成了加载并绘制了,那么需要调用此方法重新测量绘制
 * 如果没有完成加载可调用可不用
 */
fun buildAdapt(): ViewGroup.MarginLayoutParams {
    adaptView.layoutParams = lp
    return lp
}

}

小结

大家知道我们在代码中填写的尺寸,无非就是width、height、margin和pandding,所以在代码中可以使用这个工具类完成,而使用自定义像素适配的好处就是非常完美的适配所有的尺寸,能够满足同时适配水平和垂直两个方向,而目前是,面上基本就是一个方向上适配,如果你使用Kotlin,就非常的简单了。

修改系统density,densityDpi适配

以前我不知道居然还能修改系统density,densityDpi适配,根据前面的介绍的前置知识,我们知道,不管你在布局填写dp和是sp最终还是转为px,计算公式: px = density * dp, density = dpi / 160, px = dp * (dpi / 160),而density 的意思就是 1 dp 占当前设备多少像素,而屏幕的总 dp 宽度在不同的设备上是会变化的,但是我们在布局中填写的 dp 值却是固定不变的,所以我们能不能根据px = density * dp公式,把dp看成固定设计图的尺寸,那么 density = px/ dp,如果在屏幕的尺寸和我们,即:当前设备屏幕总宽度(单位为px)/ 设计图总宽度(单位为 dp) = density,就是想前面讲的根据:当前设备屏幕总宽度(单位为px) / 设计图总宽度 (单位为 px) * 具体的值(单位为 px) = px是一样的,比如:

验证的结果就是宽度刚好是屏幕的一半,具体看看那代码:

public class Density {
private static float WIDTH = 375; //参考设备的宽,单位是dp 1440dp / 2 = 187.5dp  居中
private static float appDensity = 0f;
private static float appScaleDensity = 0f;
private static int appDensityDpi = 0;

public static void adaptDensity(Application app, Activity activity) {
    //获取当前app的屏幕显示信息
    DisplayMetrics appMetrics = app.getResources().getDisplayMetrics();
    if (appDensity == 0) {
        appDensity = appMetrics.density;
        appScaleDensity = appMetrics.scaledDensity;
        appDensityDpi = appMetrics.densityDpi;
    }

    Log.e("TAG", appMetrics.widthPixels + "  " + appMetrics.density);

    //计算目标值density, scaleDensity, densityDpi
    float targetDensity = appMetrics.widthPixels / WIDTH;
    // 默认 density 和 scaledDensity 相等
    float targetScaleDensity = targetDensity * (appScaleDensity / appDensity);
    //dpi = density * 160
    int targetDensityDpi = (int) (targetDensity * 160);

    Log.e("TAG", "" + targetDensity);


    //替换Activity的density, scaleDensity, densityDpi
    DisplayMetrics displayMetrics = activity.getResources().getDisplayMetrics();
    displayMetrics.density = targetDensity;
    displayMetrics.scaledDensity = targetScaleDensity;
    displayMetrics.densityDpi = targetDensityDpi;

    Log.e("TAG", displayMetrics.widthPixels + "  " + displayMetrics.density);

}


/**
 * 为了解决对第三方库的适配的影响,我们可以取消Density适配,然后使用自定义像素适配
 * <p>
 * 取消density适配
 *
 * @param activity
 */
public static void cancelAdaptDensity(Activity activity) {
    DisplayMetrics displayMetrics = activity.getResources().getDisplayMetrics();
    displayMetrics.density = appDensity;
    displayMetrics.scaledDensity = appScaleDensity;
    displayMetrics.densityDpi = appDensityDpi;
}
}

这个方案的优点是侵入性和成本低,但是缺点就是,这种修改是全局性的,但凡有一些第三方库的参考尺寸和我们的不一致,那么这个方案就会失效,为了解决对第三方库的适配的影响,我们是可以取消Density适配,然后结合前面讲的自定义像素适配,所以前面我说了没有完美的适配方案,只有更适合,我们项目中就使用两种适配方案结合使用。

总结
上一篇下一篇

猜你喜欢

热点阅读