美文网首页
Android 屏幕适配总结

Android 屏幕适配总结

作者: 有没有口罩给我一个 | 来源:发表于2019-11-23 01:05 被阅读0次
    为什么需要屏幕适配?
    • 我们为什么需要屏幕适配,而不是屏幕越大应该显示更多的内容呢?不仅是我会有这样的疑问,我相信很多人和我一样有同样的疑,由于在Android开发中,由于Android设备碎片化非常严重,屏幕分辨率不计其数,而想要在各种分辨率的设备上保持UI元素一致的效果,具体表示就是:
      1、屏幕尺寸的碎片化,市面上有720x1280、1080x1920等等。
      2、屏幕的密度,有可能就是屏幕尺寸一样但是它的屏幕的密度是不一样的;
      3、国内厂商比如OPPO、华为等,我们需要去适配的刘海屏和水滴屏等等;

    • 市面上有多各种适配方案,比如谷歌推荐的dp、今日头条、百分比和最小宽度适配等方案,我以前使用最小宽度适配方案,现在不是了,这段时间也在调研适配方案,最后总结出了,一句话,没有最好的适配方案,只有合适的自己方案,我们现在项目就用了多种方案结合使用,感觉效果不错,所以就有了这篇文章,下面来看看一些前置知识。

      ###android.util.TypedValue
      public static float applyDimension(int unit, float value,
                                       DisplayMetrics metrics) {
        switch (unit) {
            case COMPLEX_UNIT_PX:// px = px
                return value;
            case COMPLEX_UNIT_DIP://px = density * dp
                return value * metrics.density;
            case COMPLEX_UNIT_SP:// px = density * dp    默认情况下scaledDensity = density
                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;
      

      }

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

    android中的dp在渲染前会将dp转为px,计算公式:
    • px = density * dp;
    • density = dpi / 160;
    • px = dp * (dpi / 160);

    而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的宽度这并不是我么想要的效果,计算方法:

    • 假如你当前屏幕宽度是720px上显示在一半: 720px / 720px * 360px = 360px;
    • 假如你当前屏幕宽度是1080px上显示在一半: 1080px / 720px * 360px = 540px;
    • 假如你当前屏幕宽度是1920px上显示在一半: 1920px / 720px * 360px = 690px;
    • 假如你当前屏幕宽度是1440px上显示在一半: 1440px / 720px * 360px = 720px;
    • 设计稿的值永远按照720px标准,它会自行缩放,怎么计算,首先我们应该要知道参考尺寸和当前屏幕的尺寸,代码如下:
    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是一样的,比如:

    • 假如你当前屏幕宽度是720px,设计图是720dp,要显示的效果是360dp: 720px / 720pdp = 1.0,最终的显示的效果由公式:px = density * dp,1.0 x 360 = 360px

    • 假如你当前屏幕宽度是1080px,设计图是720dp,要显示的效果是360dp: 1080px / 720pdp = 1.5,最终的显示的效果由公式:px = density * dp,1.5 x 360 = 540px

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

    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适配,然后结合前面讲的自定义像素适配,所以前面我说了没有完美的适配方案,只有更适合,我们项目中就使用两种适配方案结合使用。

    总结
    • 1、自定义像素目前我觉得应该是最好的适配方式,能够满足同时适配水平和垂直两个方向,同时不会受一些第三方库影响,缺点就是开发者会写更多的代码,但是我觉得也不算缺点;
    • 2、修改系统density这种适配方案,只能同时适配(水平或垂直)方向,有点是侵入和适配成本低,缺点就是受第三方库影响;
    • 3、百分比布局适配方案,就是根据父容器尺寸作为参考,在View的加载过程中,根据父容器的实际尺寸换算出目标尺寸,在作用到View上,这种方案侵入性强,在适配中还要开发者自行计算比例,成本太高,不能精确的做到是适配,比如我想这个Button距离左边20dp,那百分比就做不到,百分比就是通过父容器的width和height然后剩余指定的百分比,并不能作为任何场景的适配。,感兴趣的可以看着片文章屏幕适配?不如手写个百分比布局
    • 4、最小宽度适配方案,这种方案缺点就是侵入性强,会有引入大量的适配资源,增大app体积;
      最后我想说的是没有完美的适配方案,只有更适合的:

    相关文章

      网友评论

          本文标题:Android 屏幕适配总结

          本文链接:https://www.haomeiwen.com/subject/fuzvictx.html