为什么要屏幕适配?
因为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