android 最全屏幕适配方案-自定义实现谷歌百分比布局(包含一些大厂刘海屏适配)

为什么要屏幕适配?

因为Android设备的碎片化严重,导致app的界面元素在不同分辨率的设备屏幕尺寸上显示不一致;

为了让布局、布局组件、资源、用户界面流程,匹配不同设备屏幕尺寸;

常见屏幕适配方式:

布局适配

避免写死控件尺寸,使用warp_content\match_content;

灵活使用以下属性设置:

LinearLayout android:layout_weight="0.5"

RelativeLayout android:layout_centerInParent="true"....

ContraintLayout android:layout_constraintLeft_toLeftOf="parent"...

Percent-support-lib xxx:layout_widgetPercent="30%"

图片资源适配

.9图或者SVG图实现缩放

备用位图匹配不同分辨率

用户流程适配

根据业务逻辑执行不同的跳转逻辑

根据别名展示不同的界面

例如:手机和平板适配中就会采用此方式实现

限定符适配

分辨率限定符 drawable-hdpi、drawable-xhdpi,等

尺寸限定符 layout-small(小屏),layout-large(大屏),

最小宽度限定符 values-sw360dp,values-sw384dp,(数字表示设备像素密度)

屏幕方向限定符 layout-land(水平),layout-port(竖直)

市场刘海屏和水滴屏请参考各自官网适配详情

自定义View适配

原理:以一个特定尺寸设备屏幕分辨率为参考,在View的加载过程中,根据当前设备的实际屏幕分辨率和参考设备屏幕分辨率缩放比换算出View控件的目标像素,再作用到控件上;

实现步骤:

一、创建一个工具类:用来获取屏幕水平和垂直方向缩放比

public class Utils {

    private static Utils instance;

    private float mDisplayWidth;

    private float mDisplayHeigth;

    private float STANDARD_WIDTH = 720;

    private float STANDARD_HEIGHT = 1280;

    private Utils(Context context) {

        if (mDisplayWidth == 0 || mDisplayHeigth == 0) {

            // 获取屏幕实际宽高

            WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

            if (manager != null) {

                DisplayMetrics metrics = new DisplayMetrics();

                manager.getDefaultDisplay().getMetrics(metrics);

                if (metrics.widthPixels > metrics.heightPixels) {//横屏

                    mDisplayWidth = metrics.heightPixels;

                    mDisplayHeigth = metrics.widthPixels;

                } else {

                    mDisplayWidth = metrics.widthPixels;

                    mDisplayHeigth = metrics.heightPixels - getStatusBarHeight(context);

                }

            }

        }

    }

    public static Utils getInstance(Context context) {

        if (instance == null) {

            instance = new Utils(context.getApplicationContext());

        }

        return instance;

    }

    // 获取屏幕状态栏高度

    public int getStatusBarHeight(Context context) {

        int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");

        if (resId > 0) {

            return context.getResources().getDimensionPixelSize(resId);

        }

        return 0;

    }

    // 获取水平方向上的缩放比

    public float getHorizontalScale() {

        return mDisplayWidth / STANDARD_WIDTH;

    }

    // 获取垂直方向上的缩放比

    public float getVerticalScale() {

        return mDisplayHeigth / STANDARD_HEIGHT;

    }

}

二、在自定义View中测量时实际使用

public class MyView extends RelativeLayout {

    private boolean flag;// 用于标记,防止二次测量

    private float scaleX;

    private float scaleY;

    public MyView(Context context) {

        this(context, null);

    }

    public MyView(Context context, AttributeSet attrs) {

        this(context, attrs, 0);

    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

        //获取缩放比

        scaleX = Utils.getInstance(getContext()).getHorizontalScale();

        scaleY = Utils.getInstance(getContext()).getVerticalScale();

    }

    @Override

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        if (!flag) {

            int childCount = getChildCount();

            if (childCount > 0) {

                for (int i = 0; i < childCount; i++) {

                    View child = getChildAt(i);

                    // 获取每个View的Params属性

                    LayoutParams params = (LayoutParams) child.getLayoutParams();

                    params.width = (int) (params.width * scaleX);

                    params.height = (int) (params.height * scaleY);

                    params.leftMargin = (int) (params.leftMargin * scaleX);

                    params.rightMargin = (int) (params.rightMargin * scaleX);

                    params.topMargin = (int) (params.topMargin * scaleY);

                    params.bottomMargin = (int) (params.bottomMargin * scaleY);

                }

            }

            flag = true;

        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

}

百分比布局适配

Android提供了Android-percent-support这个库,支持百分比布局,在一定程度上可以解决屏幕适配的问题

两种布局:

PercentRelativeLayout和PercentFrameLayout

PercentRelativeLayout继承RelativeLayout

PercentFrameLayout继承FrameLayout

官方使用

build.gradle添加:

implementation 'com.android.support:percent:28.0.0'

PercentFrameLayout

<?xml version="1.0" encoding="utf-8"?>

<android.support.percent.PercentFrameLayout

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    android:layout_width="match_parent"

    android:layout_height="match_parent">


    <ImageView

        android:layout_width="0dp"

        android:layout_height="0dp"

        app:layout_heightPercent="20%"

        app:layout_widthPercent="50%"

        android:layout_gravity="center"

        android:background="@mipmap/picture"/>


    <TextView

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:textSize="16sp"

        android:layout_gravity="center"

        android:text="孩子"

        android:gravity="center"/>


</android.support.percent.PercentFrameLayout>

根据UI的设计原型在不同的设备屏幕上显示效果都是一样的;

下面就是重点了,自己实现谷歌官方百分比布局

一、values文件夹下创建自定义属性attrs.xml

<?xml version="1.0" encoding="utf-8"?>

<resources>

    <declare-styleable name="PercentLayout">

        <attr name="widthPercent" format="float"></attr>

        <attr name="heightPercent" format="float"></attr>

        <attr name="marginLeftPercent" format="float"></attr>

        <attr name="marginRightPercent" format="float"></attr>

        <attr name="marginTopPercent" format="float"></attr>

        <attr name="marginBottomPercent" format="float"></attr>

    </declare-styleable>

</resources>

二、创建自定义布局,并解析自定义属性

public class PercentLayout extends RelativeLayout {

public PercentLayout(Context context) {

    this(context, null);

}

public PercentLayout(Context context, AttributeSet attrs) {

    this(context, attrs, 0);

}

public PercentLayout(Context context, AttributeSet attrs, int defStyleAttr) {

    super(context, attrs, defStyleAttr);

}

    public static class PercentParams extends RelativeLayout.LayoutParams {

        private float widthPercent;

        private float heightPercent;

        private float marginLeftPercent;

        private float marginRightPercent;

        private float marginTopPercent;

        private float marginBottomPercent;

        public PercentParams(Context c, AttributeSet attrs) {

            super(c, attrs);

            // 解析自定义属性

            TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.PercentLayout);

            widthPercent = (float) array.getFloat(R.styleable.PercentLayout_widthPercent, 0);

            heightPercent = (float) array.getFloat(R.styleable.PercentLayout_heightPercent, 0);

            marginLeftPercent = (float) array.getFloat(R.styleable.PercentLayout_marginLeftPercent, 0);

            marginRightPercent = (float) array.getFloat(R.styleable.PercentLayout_marginRightPercent, 0);

            marginTopPercent = (float) array.getFloat(R.styleable.PercentLayout_marginTopPercent, 0);

            marginBottomPercent = (float) array.getFloat(R.styleable.PercentLayout_marginBottomPercent, 0);

            array.recycle();

        }

    }

}

三、循环测量子view并设置子view的params值;

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    // 获取父容器宽高

    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    int heighSize = MeasureSpec.getSize(heightMeasureSpec);

    // 循环遍历子view并设置params

    int childCount = getChildCount();

    if (childCount > 0) {

        for (int i = 0; i < childCount; i++) {

            View child = getChildAt(i);

            LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();

            // 如果是自定义的百分比属性

            if (checkLayoutParams(layoutParams)) {

                PercentParams percentParams = (PercentParams) layoutParams;

                float widthPercent = percentParams.widthPercent;

                float heightPercent = percentParams.heightPercent;

                float marginLeftPercent = percentParams.marginLeftPercent;

                float marginRightPercent = percentParams.marginRightPercent;

                float marginTopPercent = percentParams.marginTopPercent;

                float marginBottomPercent = percentParams.marginBottomPercent;

                if (widthPercent > 0) layoutParams.width = (int) (widthSize * widthPercent);

                if (heightPercent > 0) layoutParams.height = (int) (heighSize * heightPercent);

                if (marginLeftPercent > 0)

                    layoutParams.leftMargin = (int) (widthSize * marginLeftPercent);

                if (marginRightPercent > 0)

                    layoutParams.rightMargin = (int) (widthSize * marginRightPercent);

                if (marginTopPercent > 0)

                    layoutParams.topMargin = (int) (heighSize * marginTopPercent);

                if (marginBottomPercent > 0)

                    layoutParams.bottomMargin = (int) (heighSize * marginBottomPercent);

            }

        }

    }

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

}

private boolean checkLayoutParams(LayoutParams params) {

    return params instanceof PercentParams;

}

// 这里一定要重写此方法,并返回自定义的PercentParams

@Override

public LayoutParams generateLayoutParams(AttributeSet attrs) {

    return new PercentParams(getContext(), attrs);

}

四、使用自定义百分比布局

<?xml version="1.0" encoding="utf-8"?>

<com.xxx.uidemo.PercentLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    tools:context=".MainActivity">

    <TextView

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:background="@color/colorPrimary"

        android:text="百分比控件"

        app:heightPercent="0.5"

        app:widthPercent="0.5"

        tools:ignore="MissingPrefix" />

</com.xxx.uidemo.PercentLayout>

修改像素密度

修改density(屏幕密度)、scaleDensity(字体缩放比例)、densityDpi(每英寸像素点个数)值-----直接更改系统内部对于目标尺寸而言的像素密度;

原理:布局中不管是dp、sp、pt最终都是通过系统中上面三个参数转化为相应的px值,只要统一了上面三个值,那么不管什么设备屏幕都能够适配;

源码转化如下:

/frameworks/base/core/java/android/util/TypedValue.java


实现步骤:

一、创建修改Density的工具类

public class Density {

    private static final float WIDTH = 320;//参考设备的宽,单位dp;

    private static float appDensity;// 表示屏幕密度;

    private static float appScaleDensity;// 表示字体缩放比例;默认和appDensity一致

    public static void setDensity(final Application application, Activity activity) {

        // 获取当前app的屏幕显示信息;

        final DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();

        if (appDensity == 0 || appScaleDensity == 0) {

            // 初始化

            appDensity = displayMetrics.density;

            appScaleDensity = displayMetrics.scaledDensity;

            // 当系统字体大小发生变化后,需要重新对appScaleDensity进行赋值;

            // 监听系统字体变化回调

            application.registerComponentCallbacks(new ComponentCallbacks() {

                @Override

                public void onConfigurationChanged(Configuration configuration) {

                    // 字体发生变化后,重新赋值

                    if (configuration != null && configuration.fontScale > 0) {

                        appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;

                    }

                }

                @Override

                public void onLowMemory() {

                }

            });

        }

        // 通过参考设备的宽,计算目标值density、scaleDensity、densityDpi

        float targetDensity = displayMetrics.widthPixels / WIDTH;// 1080/360 = 3.0

        float targetScaleDensity = targetDensity * (appScaleDensity / appDensity);

        int targetDensityDpi = (int) (targetDensity * 160);

        // 替换当前activity的density、scaleDensity、densityDpi值

        DisplayMetrics dm = activity.getResources().getDisplayMetrics();

        dm.density = targetDensity;

        dm.scaledDensity = targetScaleDensity;

        dm.densityDpi = targetDensityDpi;

    }

}

二、在Activity中使用

public class MainActivity extends AppCompatActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        // 必须在setContentView方法之前执行

        Density.setDensity(getApplication(), this);

        setContentView(R.layout.activity_test);

    }

}

布局文件

<?xml version="1.0" encoding="utf-8"?>

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    android:layout_width="match_parent"

    android:layout_height="match_parent">

    <TextView

        android:id="@+id/text"

        android:layout_width="160dp"

        android:layout_height="160dp"

        android:background="@color/colorAccent"

        android:text="Hello World!"

        app:layout_constraintLeft_toLeftOf="parent"

        app:layout_constraintTop_toTopOf="parent" />

    <TextView

        android:id="@+id/text1"

        android:layout_width="160dp"

        android:layout_height="160dp"

        android:background="@color/colorAccent"

        android:text="Hello World!"

        app:layout_constraintLeft_toRightOf="@id/text"

        app:layout_constraintTop_toBottomOf="@id/text" />

</android.support.constraint.ConstraintLayout>

三、当有多个界面时的用法

a、抽取基类BaseActivity,在onCreate方法中调用Density.setDensity(getApplication(), this);

b、实现application实现类,在onCreate方法中监听app所有Activity生命周期并在Activity生命周期onCreate中调用Density.setDensity(getApplication(), this);如下所示

public class App extends Application {

    @Override

    public void onCreate() {

        super.onCreate();

        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {

            @Override

            public void onActivityCreated(Activity activity, Bundle bundle) {

                Density.setDensity(App.this,activity);

            }

            @Override

            public void onActivityStarted(Activity activity) {

            }

            @Override

            public void onActivityResumed(Activity activity) {

            }

            @Override

            public void onActivityPaused(Activity activity) {

            }

            @Override

            public void onActivityStopped(Activity activity) {

            }

            @Override

            public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {

            }

            @Override

            public void onActivityDestroyed(Activity activity) {

            }

        });

    }

}

刘海屏适配

Android官方9.0刘海屏适配策略

如果时非全屏模式(有状态栏),则app不受刘海屏影响,刘海屏的高就是状态栏高度;

全屏模式,app未适配刘海屏,系统会对界面做特殊处理,竖屏下内容区域下移,横屏下内容区域右移;

所以说适配刘海屏只是在全屏模式下做适配

适配原理:首先app界面设置全屏模式,然后设置内容区域延伸到刘海区;

代码实现逻辑:

public class TestActivity extends AppCompatActivity {

    @Override

    protected void onCreate(@Nullable Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        // 1、设置全屏模式

        requestWindowFeature(Window.FEATURE_NO_TITLE);

        Window window = getWindow();

        window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

        // 2、判断手机是否是刘海屏

        boolean hasDisplayCutout = hasDisplayCutout(window);

        if (hasDisplayCutout) {

            // 3、设置内容区域延伸至刘海区域

            WindowManager.LayoutParams layoutParams = window.getAttributes();

            /*

            public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT = 0;//默认模式,全屏模式下,内容区域向下移动,非全屏不受影响

            public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER = 2;// 不管是否全屏,内容区域不能延伸至刘海区

            public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES = 1;//允许内容区域延伸至刘海区

            * */

            layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;

            window.setAttributes(layoutParams);

            // 4、设置沉浸式

            int flag = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;

            int visibility = window.getDecorView().getSystemUiVisibility();

            visibility |= flag;

            window.getDecorView().setSystemUiVisibility(visibility);

        }

        setContentView(R.layout.activity_test);

    }

    private boolean hasDisplayCutout(Window window) {

        DisplayCutout displayCutout;

        View rootView = window.getDecorView();

        WindowInsets windowInsets = rootView.getRootWindowInsets();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && windowInsets != null) {

            displayCutout = windowInsets.getDisplayCutout();

            if (displayCutout != null) {

                if (displayCutout.getBoundingRects() != null && displayCutout.getBoundingRects().size() > 0 && displayCutout.getSafeInsetTop() > 0) {

                    return true;

                }

            }

        }

        return true;//这里由于使用的是模拟器,暂时设置为true;

    }

}

第一次运行未成功,发现原因,需要将AppTheme中添加如下属性:

<item name="windowNoTitle">true</item>

运行结果截图:


这里备注一下怎么给模拟器设置刘海屏模式:

开发者选项中设置:


真实开发时处理逻辑:

1、设置全屏模式

2、判断手机厂商

3、判断手机是否支持刘海屏

4、设置让内容区域延伸至刘海屏区域

5、获取刘海屏高度

6、如果有控件被刘海屏遮挡的情况下,特殊处理,让此控件向下移动,移动高度就是刘海屏高度;

如遇到如下情况,需要5、6步骤处理,下面的button被遮挡了


此时处理方式:沟通交互设计师,不让button显示在这里,或者让button下移一个刘海屏高度的位置

一般刘海屏高度就是状态栏高度;

// 获取屏幕状态栏高度

public int getStatusBarHeight(Context context) {

    int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");

    if (resId > 0) {

        return context.getResources().getDimensionPixelSize(resId);

    }

    return 0;

}

然后给button设置marginTop属性或者为button父布局设置paddingTop属性;

整理了各大厂商刘海屏的适配文档,请参考

华为:https://devcentertest.huawei.com/consumer/cn/devservice/doc/50114

小米:https://dev.mi.com/console/doc/detail?pId=1341

oppo:https://open.oppomobile.com/service/message/detail?id=61876

Vivo:https://dev.vivo.com.cn/documentCenter/doc/103

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容