Android设备碎片化,导致app的界面元素在不同屏幕尺寸上显示不一致。屏幕适配的目的是为了让布局,布局组件,资源,用户界面流程,匹配不同的屏幕尺寸。
屏幕适配的方法主要分为四种来讲,
1. 自定义像素匹配。
由于同样的分辨率(1080×1920), 但是像素密度很可能不一样。导致即使用dp,一样会有屏幕适配的问题。差异还是很明显的。
所以采用自定义像素匹配。也就是分辨率缩放的方式。
目前有两种方式。
一种是继承系统现有的layout,Relativelatout,Linearlayout等。然后重写onMeasure()方法
还有一种就是采用原始的方法。为每一个布局控件都用像素设置width, height,以及margin.
首先我们准备一个分辨率缩放的类。
public class UIUtils {
// 工具类
private static UIUtils instance ;
// ios 标准
public static final float STANDARD_WIDTH=1080f;
public static final float STANDARD_HEIGHT=1920f;
//实际设备信息 赋值 他是不知道横竖 1080 1920
// width 1920 高度 1080
public static float displayMetricsWidth;
public static float displayMetricsHeight;
public static float systemBarHeight;
// applicaiton
public static UIUtils getInstance(Context context){
if(instance==null){
instance=new UIUtils(context);
}
return instance;
}
public static UIUtils notityInstance(Context context){
instance=new UIUtils(context);
return instance;
}
// activity
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.0f || displayMetricsHeight==0.0f){
//在这里得到设备的真实值
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
systemBarHeight=getSystemBarHeight(context);
//横屏
if(displayMetrics.widthPixels>displayMetrics.heightPixels){
this.displayMetricsWidth=(float)(displayMetrics.heightPixels);
this.displayMetricsHeight=(float)(displayMetrics.widthPixels-systemBarHeight);
}else {
// 竖屏
this.displayMetricsWidth=(float)(displayMetrics.widthPixels);
this.displayMetricsHeight=(float)(displayMetrics.heightPixels-systemBarHeight);
}
// 状态栏
}
}
//
public float getHorizontalScaleValue(){
return ((float)(displayMetricsWidth)) / STANDARD_WIDTH;
}
public float getVerticalScaleValue(){
return ((float)(displayMetricsHeight))/(STANDARD_HEIGHT-systemBarHeight);
}
private int getSystemBarHeight(Context context){
return getValue(context,"com.android.internal.R$dimen","system_bar_height",48);
}
public int getWidth(int width) {
return Math.round((float)width * this.displayMetricsWidth / STANDARD_WIDTH);
}
public int getHeight(int height) {
return Math.round((float)height * this.displayMetricsHeight / (STANDARD_HEIGHT-systemBarHeight));
}
private int getValue(Context context, String dimeClass, String system_bar_height, int defaultValue) {
// com.android.internal.R$dimen system_bar_height 状态栏的高度
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;
}
}
往往美工给出的设计图是基于像素的,这样我们就已美工设计的屏幕大小为基准,也就是STANDARD_WIDTH和
STANDARD_HEIGHT.
然后取出当前手机的像素分辨来进行缩放. 还要注意状态栏的高度,美工的设计是否包含状态栏的
,以此减去状态栏的高度.
还有屏幕横竖屏改变的时候, 要调用下notifyInstance()因为此时横竖颠倒了.
以下是重写RelativLayout的onMeasure()的方式:
public class UIRelativeLayout extends RelativeLayout {
private boolean flag=true;
public UIRelativeLayout(Context context) {
super(context);
}
public UIRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public UIRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasure
if (flag) {//避免二次测量导致值被缩放两次
flag = false;
float scaleX = UIUtils.getInstance(getContext()).getHorizontalScaleValue();
float scaleY = UIUtils.getInstance(getContext()).getVerticalScaleValue();
int childCount = this.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = this.getChildAt(i);
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
layoutParams.width=(int) (layoutParams.width * scaleX);
layoutParams.height = (int) (layoutParams.height * scaleY);
layoutParams.leftMargin = (int) (layoutParams.leftMargin * scaleX);
layoutParams.rightMargin = (int) (layoutParams.rightMargin * scaleX);
layoutParams.topMargin = (int) (layoutParams.topMargin * scaleY);
layoutParams.bottomMargin = (int) (layoutParams.bottomMargin * scaleY);
}
}
}
}
容器里的每一个view的width,height和margin,都缩放一下
在xml上直接用美工设计的px设置就可以了
用这种方式有一个缺点, 如果子view是系统的viewgroup,那么是无效的.
这里就引申出另外一种采用原始的方法,简单且粗暴,
public class ViewCalculateUtil {
// 界面元素 进行赋值 开发者来说是麻烦的
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 && width != RelativeLayout.LayoutParams.FILL_PARENT)
{
layoutParams.width = UIUtils.getInstance().getWidth(width);
}
else
{
layoutParams.width = width;
}
if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT && height != RelativeLayout.LayoutParams.FILL_PARENT)
{
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);
}
else
{
}
}
public static void setTextSize(TextView view, int size)
{
view.setTextSize(TypedValue.COMPLEX_UNIT_PX, UIUtils.getInstance().getHeight(size));
}
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 && width != RelativeLayout.LayoutParams.FILL_PARENT)
{
layoutParams.width = UIUtils.getInstance( ).getWidth(width);
}
else
{
layoutParams.width = width;
}
if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT && height != RelativeLayout.LayoutParams.FILL_PARENT)
{
layoutParams.height = UIUtils.getInstance( ).getHeight(height);
}
else
{
layoutParams.height = height;
}
view.setLayoutParams(layoutParams);
}
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 && width != RelativeLayout.LayoutParams.FILL_PARENT)
{
layoutParams.width = UIUtils.getInstance( ).getWidth(width);
}
else
{
layoutParams.width = width;
}
if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT && height != RelativeLayout.LayoutParams.FILL_PARENT)
{
layoutParams.height = UIUtils.getInstance( ).getHeight(height);
}
else
{
layoutParams.height = height;
}
view.setLayoutParams(layoutParams);
}
/**
* 设置LinearLayout中 view的高度宽度
*
* @param view
* @param width
* @param height
*/
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 && width != RelativeLayout.LayoutParams.FILL_PARENT)
{
layoutParams.width = UIUtils.getInstance( ).getWidth(width);
}
else
{
layoutParams.width = width;
}
if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT && height != RelativeLayout.LayoutParams.FILL_PARENT)
{
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);
}
}
直接在代码里调用ViewCalculateUtil.setViewLinearLayoutParam(), 设置view的大小,以及margin,padding.
这种方式虽然很烦人,效率很差。但是反而是最有效的, 大厂里面都是用这种方式的.
2. 百分比匹配。
google提供一种百分比适配的方式,但是只能解决某些case.
Percent-support-lib
比如xxx:layout_widthPercent="30%"
3. 修改系统density, density适配。
修改density, scaleDensity, densityDpi值-直接更改系统内部对于目标尺寸而言的像素密度
density:一英寸160个像素, density就是1,一英寸320个像素density就是2
scaleDensity: 字体的缩放比例, 默认值和density一样.
densityDpi: 一英寸有多少像素
在TypedValue类中有一个方法,最后大尺寸单位, 无论是dp,sp, 还是pt等,最后都是px.
public static float applyDimension(int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity; //这里注意到题目外的问题。字体不要用dp, 不然
在手机设置里缩放字体的时候,就无效了
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;
}
density适配和像素适配的原理是一样的
public class Density {
private static final float WIDTH = 320;//参考设备的宽,单位是dp 320 / 2 = 160
private static float appDensity;//表示屏幕密度
private static float appScaleDensity; //字体缩放比例,默认appDensity
public static void setDensity(final Application application, Activity activity){
//获取当前app的屏幕显示信息
DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
if (appDensity == 0){
//初始化赋值操作
appDensity = displayMetrics.density;
appScaleDensity = displayMetrics.scaledDensity;
//添加字体变化监听回调
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
//字体发生更改,重新对scaleDensity进行赋值
if (newConfig != null && newConfig.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;
}
}
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
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 savedInstanceState) {
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 outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
});
}
}
在BaseActivity或者Application里面定义一下就可以使用了, 相对于像素适配,是不是简单的不得了? 但是天不遂人愿.有些变态的手机不允许修改density!
4. 刘海屏匹配
Android官方9.0刘海屏适配策略
如果非全屏模式(有状态栏),则app不受刘海屏的影响,刘海屏的高就是状态栏的高
如果是全屏模式,app未适配刘海屏,系统会对界面做特殊处理,竖屏向下移动,横屏向右移动
看一下注解就明白了。
public class DisplayCutoutActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//1.设置全屏
requestWindowFeature(Window.FEATURE_NO_TITLE);
Window window = getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
//华为, 小米,oppo
//1.判断手机厂商, 2,判断手机是否刘海, 3,设置是否让内容区域延伸进刘海 4,设置控件是否避开刘海区域 5, 获取刘海的高度
//判断手机是否是刘海屏
boolean hasDisplayCutout = hasDisplayCutout(window);
if (hasDisplayCutout){
//2.让内容区域延伸进刘海
WindowManager.LayoutParams params = window.getAttributes();
/**
* * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 全屏模式,内容下移,非全屏不受影响
* * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 允许内容去延伸进刘海区
* * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 不允许内容延伸进刘海区
*/
params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
window.setAttributes(params);
//3.设置成沉浸式
int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
int visibility = window.getDecorView().getSystemUiVisibility();
visibility |= flags; //追加沉浸式设置
window.getDecorView().setSystemUiVisibility(visibility);
}
setContentView(R.layout.activity_main);
// Button button = findViewById(R.id.button);
// RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) button.getLayoutParams();
// layoutParams.topMargin = heightForDisplayCutout();
// button.setLayoutParams(layoutParams);
RelativeLayout layout = findViewById(R.id.container);
layout.setPadding(layout.getPaddingLeft(), heightForDisplayCutout(), layout.getPaddingRight(), layout.getPaddingBottom());
}
private boolean hasDisplayCutout(Window window) {
DisplayCutout displayCutout;
View rootView = window.getDecorView();
WindowInsets insets = rootView.getRootWindowInsets();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && insets != null){
displayCutout = insets.getDisplayCutout();
if (displayCutout != null){
if (displayCutout.getBoundingRects() != null && displayCutout.getBoundingRects().size() > 0 && displayCutout.getSafeInsetTop() > 0){
return true;
}
}
}
return true; //因为模拟器原因,这里设置成true
}
//通常情况下,刘海的高就是状态栏的高
public int heightForDisplayCutout(){
int resID = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resID > 0){
return getResources().getDimensionPixelSize(resID);
}
return 96;
}
}
public class Utils {
/**
* 是否刘海
* @param context
* @return
*/
public static boolean hasNotchInScreen(Context context) {
boolean ret = false;
try {
ClassLoader cl = context.getClassLoader();
Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
ret = (boolean) get.invoke(HwNotchSizeUtil);
} catch (ClassNotFoundException e) {
Log.e("test", "hasNotchInScreen ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("test", "hasNotchInScreen NoSuchMethodException");
} catch (Exception e) {
Log.e("test", "hasNotchInScreen Exception");
}
return ret;
}
/**
* 获取刘海尺寸:width、height,int[0]值为刘海宽度 int[1]值为刘海高度。
* @param context
* @return
*/
public static int[] getNotchSize(Context context) {
int[] ret = new int[]{0, 0};
try {
ClassLoader cl = context.getClassLoader();
Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
Method get = HwNotchSizeUtil.getMethod("getNotchSize");
ret = (int[]) get.invoke(HwNotchSizeUtil);
} catch (ClassNotFoundException e) {
Log.e("test", "getNotchSize ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("test", "getNotchSize NoSuchMethodException");
} catch (Exception e) {
Log.e("test", "getNotchSize Exception");
}
return ret;
}
/**
* 设置使用刘海区域
* @param window
*/
public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
if (window == null) {
return;
}
try {
WindowManager.LayoutParams layoutParams = window.getAttributes();
Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
Constructor con=layoutParamsExCls.getConstructor(WindowManager.LayoutParams.class);
Object layoutParamsExObj=con.newInstance(layoutParams);
Method method=layoutParamsExCls.getMethod("addHwFlags", int.class);
method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
} catch (Exception e) {
Log.e("test", "other Exception");
}
}
/*刘海屏全屏显示FLAG*/
public static final int FLAG_NOTCH_SUPPORT = 0x00010000;
/**
* 设置应用窗口在华为刘海屏手机不使用刘海
*
* @param window 应用页面window对象
*/
public static void setNotFullScreenWindowLayoutInDisplayCutout(Window window) {
if (window == null) {
return;
}
try {
WindowManager.LayoutParams layoutParams = window.getAttributes();
Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
Constructor con = layoutParamsExCls.getConstructor(WindowManager.LayoutParams.class);
Object layoutParamsExObj = con.newInstance(layoutParams);
Method method = layoutParamsExCls.getMethod("clearHwFlags", int.class);
method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
} catch (Exception e) {
Log.e("test", "hw clear notch screen flag api error");
}
}
/*********
* 1、声明全屏显示。
*
* 2、适配沉浸式状态栏,避免状态栏部分显示应用具体内容。
*
* 3、如果应用可横排显示,避免应用两侧的重要内容被遮挡。
*/
/********************
* 判断该 OPPO 手机是否为刘海屏手机
* @param context
* @return
*/
public static boolean hasNotchInOppo(Context context) {
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
/**
* 刘海高度和状态栏的高度是一致的
* @param context
* @return
*/
public static int getStatusBarHeight(Context context) {
int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resId > 0){
return context.getResources().getDimensionPixelSize(resId);
}
return 0;
}
/**
* Vivo判断是否有刘海, Vivo的刘海高度小于等于状态栏高度
*/
public static final int VIVO_NOTCH = 0x00000020;//是否有刘海
public static final int VIVO_FILLET = 0x00000008;//是否有圆角
public static boolean hasNotchAtVivo(Context context) {
boolean ret = false;
try {
ClassLoader classLoader = context.getClassLoader();
Class FtFeature = classLoader.loadClass("android.util.FtFeature");
Method method = FtFeature.getMethod("isFeatureSupport", int.class);
ret = (boolean) method.invoke(FtFeature, VIVO_NOTCH);
} catch (ClassNotFoundException e) {
Log.e("Notch", "hasNotchAtVivo ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("Notch", "hasNotchAtVivo NoSuchMethodException");
} catch (Exception e) {
Log.e("Notch", "hasNotchAtVivo Exception");
} finally {
return ret;
}
}
}
其他手机厂商(华为,小米,oppo,vivo)适配
华为:https://devcenter-test.huawei.com/consumer/cn/devservice/doc/50114
小米:https://dev.mi.com/console/doc/detail?pId=1293
Oppo:https://open.oppomobile.com/service/message/detail?id=61876
Vivo:https://dev.vivo.com.cn/documentCenter/doc/103
其他适配方式还有, 限定符适配之类,由于很少使用,就不说开了。