前一篇Android 进阶——高级UI必知必会之常用的屏幕适配完全攻略详解(七)总结了下通过Android 自动适配资源的特性通过限定符和资源别名进行屏幕适配的相关知识,不过并非唯一的思路,还可以在运行时通过代码动态进行适配,这篇文章将提供几种思路,相关系列文件链接如下:
自定义ViewGroup 进行屏幕适配的核心思想很简单,本质上来说屏幕适配就是对View的测量Measure流程进行干预,在对ViewTree进行测量前,选取一个分辨率作为基准(1080*1920比较主流),计算缩放比例,然后继承ViewGroup重写onMeasure方法,在布局里替换Android系统的原生ViewGroup,用自己写的ViewGroup包裹控件,并且在onMeasure方法里根据基准分辨率与目标分辨率计算缩放比例关系,再重新设置View的尺寸。简而言之就是在ViewGroup布局时中计算并通过测量,按照比例进行缩放设值。
自定义ViewGroup时要想干预View的测量Measure过程,最直接的方式就是直接重写onMeasure方法,通过遍历ViewGroup中的子View,并重新设置其对应的LayoutParams的值并更新。
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
/**
* 以像素为单位计算缩放系数进行适配
* @author cmo
*/
public class ScreenAdapterLayout extends RelativeLayout {
private boolean isMeasured =false;
public ScreenAdapterLayout(Context context) {
this(context,null);
}
public ScreenAdapterLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public ScreenAdapterLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 仅仅对其直接子View有效
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//防止两次测量
if (!isMeasured) {
isMeasured = true;
//获取横竖方向等比
float scaleX = ScreenHelper.getInstance(getContext()).getHorizontalScale();
float scaleY = ScreenHelper.getInstance(getContext()).getVerticalScale();
//获取直接子View总个数
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
//使用缩放比例参数重新计算其LayoutParams的值并更新
LayoutParams params = (LayoutParams) child.getLayoutParams();
//TODO 完善各种细节,比如说子View的Width为-1时等
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);
child.setPadding((int) (child.getPaddingLeft() * scaleX), (int) (child.getPaddingTop() * scaleY),
(int) (child.getPaddingRight() * scaleX), (int) (child.getPaddingBottom() * scaleY));
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
一个获取屏幕参数的工具类:
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.WindowManager;
/**
* @author cmo
*/
public class ScreenHelper {
private static final String STATUS_BAR_HEIGHT = "status_bar_height";
private static final String DIMEN = "dimen";
private static final String ANDROID = "android";
private static ScreenHelper screenHelper;
private Context mContext;
/**
* UI设计稿的基准宽高
*/
private static final float STANDARD_WIDTH = 1080;
private static final float STANDARD_HEIGHT = 1920;
/**
* 屏幕的真实宽高
*/
private int mDisplayWidth=0;
private int mDisplayHeight=0;
private int mStatusBarHeight=0;
private ScreenHelper(Context context) {
mContext = context;
if(mStatusBarHeight==0){
mStatusBarHeight=getStatusBarHeight();
}
if (mDisplayWidth == 0 || mDisplayHeight == 0) {
initScreenSize(context);
}
}
/**
* 初始化屏幕宽高 默认是没有虚拟按键的,如果有虚拟按键还需要减掉其高度
* @param context
*/
private void initScreenSize(Context context) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (windowManager != null) {
//宽高获取
DisplayMetrics displayMetrics = new DisplayMetrics();
//如果不是NavigationBar沉浸式(不包含NavigationBar)
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
// windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);//真实屏幕宽高
//判断当前的横竖屏
if (displayMetrics.widthPixels > displayMetrics.heightPixels) {
//横屏
mDisplayWidth = displayMetrics.heightPixels;
mDisplayHeight = displayMetrics.widthPixels - mStatusBarHeight;
} else {
//竖屏
mDisplayWidth = displayMetrics.widthPixels;
mDisplayHeight = displayMetrics.heightPixels - mStatusBarHeight;
}
}
}
public static ScreenHelper getInstance(Context context) {
if (screenHelper == null) {
screenHelper = new ScreenHelper(context);
}
return screenHelper;
}
/**
* 获取状态栏高度
* @return
*/
public int getStatusBarHeight() {
int resId = mContext.getResources().getIdentifier(STATUS_BAR_HEIGHT, DIMEN, ANDROID);
if (resId > 0) {
//获取具体的像素值
return mContext.getResources().getDimensionPixelSize(resId);
}
return 0;
}
/**
* 获取水平方向的缩放比例,通过屏幕与基准分辨率进行对比,下同
*/
public float getHorizontalScale() {
return mDisplayWidth / STANDARD_WIDTH;
}
/**
* 获取垂直方向的缩放比例
*/
public float getVerticalScale() {
return mDisplayHeight / (STANDARD_HEIGHT - mStatusBarHeight);
}
}
dp、sp转换为px的工具类
/**
* dp、sp转换为px的工具类
*/
public class DisplayUtil {
/**
* 将px值转换为dip或dp值,保证尺寸大小不变
* @param context
* @param pxValue
* @return
*/
public static int px2dip(Context context, float pxValue){
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* 将dip或dp值转换为px值,保证尺寸不变
* @param context
* @param dipValue
* @return
*/
public static int dip2px(Context context, float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
/**
* 将px值转换为sp值,保证文字大小不变
* @param context
* @param pxValue
* @return
*/
public static int px2sp(Context context, float pxValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
/**
* 将sp值转换为px值,保证文字大小不变
* @param context
* @param spValue
* @return
*/
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
使用时直接在原有的控件布局外套上一层这个自定义的ViewGroup即可:
百分比动态布局本质上来说核心思想与以像素为单位进行动态布局相同,区别在于View并不直接支持设置百分比的属性,而这些自定义属性,需要通过自定义ViewGroup.LayoutParams来传入。
res/values/attrs.xml
<resources>
<declare-styleable name="PercentLayout">
<attr name="widthPercent" format="fraction" />
<attr name="heightPercent" format="fraction" />
<attr name="marginLeftPercent" format="fraction" />
<attr name="marginRightPercent" format="fraction" />
<attr name="marginTopPercent" format="fraction" />
<attr name="marginBottomPercent" format="fraction" />
declare-styleable>
resources>
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
/**
* 根据百分比进行适配
* @author cmo
*/
public class PercentLayout extends RelativeLayout {
private boolean isMeasured=false;
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);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取父容器宽高
if (!isMeasured) {
isMeasured = true;
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//给子控件设置修改后的属性值
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
//获取子控件
View child = getChildAt(i);
//获取子控件LayoutParams
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
//判断子控件是否是百分比布局属性
if (checkLayoutParams(layoutParams)) {
PercentLayoutParams percentLayoutParams = (PercentLayoutParams) layoutParams;
float widthPercent = percentLayoutParams.widthPercent;
float heightPercent = percentLayoutParams.heightPercent;
float marginLeftPercent = percentLayoutParams.marginLeftPercent;
float marginRightPercent = percentLayoutParams.marginRightPercent;
float marginTopPercent = percentLayoutParams.marginTopPercent;
float marginBottomPercent = percentLayoutParams.marginBottomPercent;
if (widthPercent > 0) {
percentLayoutParams.width = (int) (widthSize * widthPercent);
}
if (heightPercent > 0) {
percentLayoutParams.height = (int) (heightSize * heightPercent);
}
if (marginLeftPercent > 0) {
percentLayoutParams.leftMargin = (int) (widthSize * marginLeftPercent);
}
if (marginRightPercent > 0) {
percentLayoutParams.rightMargin = (int) (widthSize * marginRightPercent);
}
if (marginTopPercent > 0) {
percentLayoutParams.topMargin = (int) (heightSize * marginTopPercent);
}
if (marginBottomPercent > 0) {
percentLayoutParams.bottomMargin = (int) (heightSize * marginBottomPercent);
}
}
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof PercentLayoutParams;
}
@Override
public PercentLayoutParams generateLayoutParams(AttributeSet attrs) {
return new PercentLayoutParams(getContext(), attrs);
}
/**
* 1、创建自定义属性
* 2、在容器中去创建一个静态内部类LayoutParams
* 3、在LayoutParams构造方法中获取自定义属性
* 4、onMeasure中给子控件设置修改后的属性值
*/
private static class PercentLayoutParams extends RelativeLayout.LayoutParams {
private float widthPercent;
private float heightPercent;
private float marginLeftPercent;
private float marginRightPercent;
private float marginTopPercent;
private float marginBottomPercent;
public PercentLayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
//3、在LayoutParams构造方法中获取自定义属性 解析自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PercentLayout);
widthPercent = typedArray.getFraction(R.styleable.PercentLayout_widthPercent, 1, 2, 0);
heightPercent = typedArray.getFraction(R.styleable.PercentLayout_heightPercent, 1, 2, 0);
marginLeftPercent = typedArray.getFraction(R.styleable.PercentLayout_marginLeftPercent, 1, 2, 0);
marginRightPercent = typedArray.getFraction(R.styleable.PercentLayout_marginRightPercent, 1, 2, 0);
marginTopPercent = typedArray.getFraction(R.styleable.PercentLayout_marginTopPercent, 1, 2, 0);
marginBottomPercent = typedArray.getFraction(R.styleable.PercentLayout_marginBottomPercent, 1, 2, 0);
typedArray.recycle();
}
}
}
使用百分比布局ViewGroup进行适配:
<com.crazymo.widget.PercentLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:crazymo="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#f00"
crazymo:heightPercent="5%"
crazymo:widthPercent="90%"
crazymo:marginLeftPercent="30%"
crazymo:marginRightPercent="30%"
crazymo:marginTopPercent="1%"
tools:ignore="MissingPrefix" />
com.crazymo.widget.PercentLayout>
像素密度为单位进行适配,核心思想与上面的有所不同,是通过与标准屏幕像素密度计算比例,然后在Activity生命周期方法中动态改变屏幕的density。
import android.app.Activity;
import android.app.Application;
import android.content.ComponentCallbacks;
import android.content.res.Configuration;
import android.util.DisplayMetrics;
/**
* 修改density,densityDpi值-直接更改系统内部对于目标尺寸而言的像素密度
* @author cmo
*/
public class DensityHelper {
/**
* 参考像素密度(dp)
*/
private static final float WIDTH = 360;
/**
* 表示屏幕密度
*/
private static float appDensity;
/**
* 字体缩放比例,默认为appDensity
*/
private static float appScaleDensity;
public static void setDensity(final Application application, Activity activity) {
//获取当前屏幕信息
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 scaledDensity,比如//1080/360=3;
float targetDensity = displayMetrics.widthPixels / WIDTH;
float targetScaleDensity = targetDensity * (appScaleDensity / appDensity);
int targetDensityDpi = (int) (targetDensity * 160);
//替换Activity的值
//px = dp * (dpi / 160)
DisplayMetrics dm = activity.getResources().getDisplayMetrics();
//(dpi/160) 后得到的值
dm.density = targetDensity;
dm.scaledDensity = targetScaleDensity;
dm.densityDpi = targetDensityDpi;
}
}
在Activity的onCreate方法中动态设置屏幕像素密度
或者在Application中通过监听ActivityLifecycleCallbacks:
public class MoApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
DensityHelper.setDensity(MoApplication.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) {
}
});
}
}
注意:不建议直接拿到项目中去使用,此文仅仅是分享核心思想,不完善和适配细节。
public class NotchActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_notch);
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if(getIsHasCutout()){
ScreenAdapterLayout layout = findViewById(R.id.layout);
layout.setPadding(0, StatusBarUtil.getStatusBarHeight(this), 0, 0);
}
}
}
刘海屏适配Demo源码传送门
关于刘海屏的适配来自网络的代码段,我只是进行了整理小结。