参考文档 :
在博客 【Android 屏幕适配】屏幕适配通用解决方案 ② ( 自定义组件解决方案 | 需要解决的问题 : 设计稿坐标数据转为屏幕真实坐标数据 | 实现步骤 ) 中 , 提出 如果要实现将 宽高为 720 x 1232 的设计稿 , 对应 手机屏幕中除 状态栏之外的 布局 , 需要完成如下操作 :
首先 , 要 获取到实际的设备屏幕数据 , 如 手机屏幕实际宽高 , 屏幕像素密度 DPI 等数据 ;
然后 , 计算实际设备的宽高 , 扣掉状态栏的高度 , 不同手机设备状态栏高度不同 , 然后再进行后续计算 ;
再后 , 给出一个 设计稿 与 屏幕实际有效像素值 的 换算比例 ;
最后 , 根据给出的比例 , 在 自定义组件的 onMeasure 方法 中 , 进 行动态换算 , 计算出在当前设备中每个组件的 实际坐标数据 ;
在博客 【Android 屏幕适配】屏幕适配通用解决方案 ③ ( 自定义组件解决方案 | 获取设备状态栏高度 | 获取设备屏幕数据 ) 中 , 完成了前两项工作 , 已经获取了实际的屏幕数据 , 状态栏高度 ;
在博客 【Android 屏幕适配】屏幕适配通用解决方案 ④ ( 自定义组件解决方案 | 计算设计稿与实际布局的比例系数 ) 中 , 计算了设计稿与实际布局的比例系数 ;
本篇博客中完成第四项工作 , 根据给出的比例 , 在 自定义组件的 onMeasure 方法 中 , 进 行动态换算 , 计算出在当前设备中每个组件的 实际坐标数据 ;
一、计算素材宽高与屏幕布局宽高比例系数
首先 , 获取自定义 ViewGroup 组件的子组件的个数 ;
// 获取该 ViewGroup 子组件个数
int childCount = this.getChildCount();
然后 , 获取 X 轴 和 Y 轴 方向上 , 设计稿 与 屏幕实际布局 缩放系数 ; 参考 【Android 屏幕适配】屏幕适配通用解决方案 ④ ( 自定义组件解决方案 | 计算设计稿与实际布局的比例系数 ) 博客 ;
// 获取 X 轴 设计稿 与 屏幕实际布局 缩放系数
float xScale = getXScaleValue();
// 获取 Y 轴 设计稿 与 屏幕实际布局 缩放系数
float yScale = getYScaleValue();
/**
* 获取设备实际宽度与参考宽度的比值
* @return
*/
public float getXScaleValue(){
return screenWidth / REFERENCE_WIDTH;
}
/**
* 获取沙设备实际高度与参考高度的比值
* @return
*/
public float getYScaleValue(){
return screenHeight / REFERENCE_HEIGHT;
}
最后 , 逐个遍历子组件 , 修改子组件的 LayoutParams 布局属性中的 宽高 , 左上右下边距位置 ;
// 逐个遍历子组件
for (int i = 0;i < childCount;i++){
// 获取子组件
View child = this.getChildAt(i);
// 获取子组件的布局属性
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
// 对子组件的 宽度 , 高度 , 左上右下的边距 进行换算
layoutParams.width = (int) (layoutParams.width * xScale);
layoutParams.height = (int) (layoutParams.height * yScale);
layoutParams.leftMargin = (int) (layoutParams.leftMargin * xScale);
layoutParams.topMargin = (int) (layoutParams.topMargin * yScale);
layoutParams.rightMargin = (int) (layoutParams.rightMargin * xScale);
layoutParams.bottomMargin = (int) (layoutParams.bottomMargin * yScale);
}
二、自定义组件完整代码
package kim.hsl;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.WindowManager;
import androidx.constraintlayout.widget.ConstraintLayout;
import java.lang.reflect.Field;
/**
* 屏幕适配解决方案
*/
public class ScreenAdaptLayout extends ConstraintLayout {
/**
* 自定义布局中的子组件坐标数据是否需要进行换算
*/
static boolean isCaculateFlag = true;
/**
* 屏幕适配参考宽度 / 设计稿宽度
*/
public final float REFERENCE_WIDTH = 720;
/**
* 屏幕适配参考高度 / 设计稿高度
* 这里注意标准高度是 1280, 去掉了状态栏后为 1232
*/
public final float REFERENCE_HEIGHT = 1232;
/**
* 设备布局实际宽度
*/
public float screenWidth;
/**
* 设备布局实际高度
*/
public float screenHeight;
/**
* 上下文对象
*/
public Context context;
public ScreenAdaptLayout(Context context) {
super(context);
this.context = context;
}
public ScreenAdaptLayout(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
}
public ScreenAdaptLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 初始化设备屏幕数据
initMetrics();
// isCaculateFlag 标志位, 保证该操作只执行一次
if(isCaculateFlag){
// 获取该 ViewGroup 子组件个数
int childCount = this.getChildCount();
// 获取 X 轴 设计稿 与 屏幕实际布局 缩放系数
float xScale = getXScaleValue();
// 获取 Y 轴 设计稿 与 屏幕实际布局 缩放系数
float yScale = getYScaleValue();
// 逐个遍历子组件
for (int i = 0;i < childCount;i++){
// 获取子组件
View child = this.getChildAt(i);
// 获取子组件的布局属性
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
// 对子组件的 宽度 , 高度 , 左上右下的边距 进行换算
layoutParams.width = (int) (layoutParams.width * xScale);
layoutParams.height = (int) (layoutParams.height * yScale);
layoutParams.leftMargin = (int) (layoutParams.leftMargin * xScale);
layoutParams.topMargin = (int) (layoutParams.topMargin * yScale);
layoutParams.rightMargin = (int) (layoutParams.rightMargin * xScale);
layoutParams.bottomMargin = (int) (layoutParams.bottomMargin * yScale);
}
isCaculateFlag = false;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
private void initMetrics(){
// 获取当前设备的屏幕信息
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
if(screenWidth == 0.0f || screenHeight == 0.0f){
//获取状态框信息
int statusBarHeight = getDimenValue(context,48);
// 在屏幕真实宽高上减去状态栏高度
if(displayMetrics.widthPixels > displayMetrics.heightPixels){
// 宽度大于高度说明是横屏状态, 状态栏在左侧或者右侧
this.screenWidth = displayMetrics.heightPixels;
this.screenHeight = displayMetrics.widthPixels - statusBarHeight;
}else{
// 高度大于宽度说明是竖屏状态, 状态栏在上侧
this.screenWidth = displayMetrics.widthPixels;
this.screenHeight = displayMetrics.heightPixels - statusBarHeight;
}
}
}
/**
* 获取设备实际宽度与参考宽度的比值
* @return
*/
public float getXScaleValue(){
return screenWidth / REFERENCE_WIDTH;
}
/**
* 获取沙设备实际高度与参考高度的比值
* @return
*/
public float getYScaleValue(){
return screenHeight / REFERENCE_HEIGHT;
}
/**
* 通过反射 com.android.internal.R$dimen 类, 获取其中的某些字段
* @param context 上下文对象
* @param defValue 如果没有成功获取指定字段, 这里返回一个默认值
* @return
*/
public int getDimenValue(Context context, int defValue) {
try {
// 反射 com.android.internal.R$dimen 类
Class<?> clazz = Class.forName("com.android.internal.R$dimen");
// 创建 com.android.internal.R$dimen 实例对象
Object instance = clazz.newInstance();
// 获取指定的字段, 这里用于获取 system_bar_height, 也就是系统状态栏高度
Field field = clazz.getField("system_bar_height");
// 获取字段的值
int fieldValue = (int) field.get(instance);
// 获取的字段值是资源 ID, 需要转为实际的像素值
return context.getResources().getDimensionPixelOffset(fieldValue);
} catch (Exception e) {
// 如果执行出现异常, 则返回默认值
return defValue;
}
}
}