屏幕适配常见方式
布局适配
- 避免写死控件尺寸,使用match_parent、wrap_content
- LinearLayout的layout_weight和RelativeLayout
- ConstraintLayout 性能优于RelativeLayout
- Percent-support-lib layout_widthPercent
限定符适配
- 分辨率限定符 drawable-xhdpi drawable-hdpi
- 尺寸限定符 layout-small layout-large
- 最小宽度限定符 value-sw360dp value-sw480dp
- 屏幕方向限定符 layout-land layout-port
自定义View适配
根据UI设计标注的屏幕尺寸作为参考,在View的加载过程,根据当前设备的实际像素换算成目标像素,再作用到控件上。
public int getAdapterWidth(int width) {
return width * mDisplayWidth / DEFAULT_WIDTH;
}
public int getAdapterHeight(int height) {
return height * mDisplayHeight / DEFAULT_HEIGHT;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!isAdapted) {
int childCnt = getChildCount();
for (int i = 0; i < childCnt; i++) {
LayoutParams params = (LayoutParams) getChildAt(i).getLayoutParams();
params.width = ScreenUtils.getInstance(getContext()).getAdapterWidth(params.width);
params.height = ScreenUtils.getInstance(getContext()).getAdapterWidth(params.height);
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
百分比布局
原理和上面的自定义View的像素布局类似,通过自定义属性来计算View的尺寸。
Google已经支持百分比布局
implementation 'com.android.support:percent:29.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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">
<androidx.percentlayout.widget.PercentRelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%"
android:background="@android:color/holo_red_light"/>
</androidx.percentlayout.widget.PercentRelativeLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">
<TextView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="Hello World!"
app:layout_constraintWidth_percent=".5"
app:layout_constraintHeight_percent=".5"
android:background="@android:color/holo_red_light"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
注意:ConstraintLayout支持百分比布局,从1.1.X开始
修改系统density、scaleDensity、densityDpi
适用于APP已经有Pad或Phone版本,不想重新开发,适配Phone或Pad场景。
- px像素只说明了一个屏幕包含的点数有多少,但是点的大小不是确定的,同样是480*800,可能是手掌那么大,也可能是电影院屏幕那么大。
- densityDpi 屏幕的像素密度,即屏幕每英寸的像素点。例如,屏幕横向2英寸,480px,那么横向像素密度为480px/2=240dpi。
- density 逻辑上的屏幕密度,density = densityDpi/160。
- scaleDensity 字体的缩放密度,默认等于density。如果在手机设置中更改了字体大小,则不再等于density。
- dp 独立像素密度。dp = px/density + 0.5
- sp 缩放像素。随手机设置的字体大小而更改。sp = px/scaleDensity + 0.5
参考源码
/**
* Container for a dynamically typed data value. Primarily used with
* {@link android.content.res.Resources} for holding resource values.
*/
public class TypedValue {
/**
* Converts an unpacked complex data value holding a dimension to its final floating
* point value. The two parameters <var>unit</var> and <var>value</var>
* are as in {@link #TYPE_DIMENSION}.
*
* @param unit The unit to convert from.
* @param value The value to apply the unit to.
* @param metrics Current display metrics to use in the conversion --
* supplies display density and scaling information.
*
* @return The complex floating point value multiplied by the appropriate
* metrics depending on its unit.
*/
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;
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;
}
}
示例代码
public class DensityUtil {
/**
* 设计师标注的标准宽度,单位是DP
*/
private static final int STANDARD_WIDTH = 640;
private static float mDensity;
private static float mScaleDensity;
private static float mScreenWidth;
private DensityUtil() {
}
public static void setDensity(Activity activity) {
if (mDensity == 0) {
final Application application = activity.getApplication();
DisplayMetrics appDm = application.getResources().getDisplayMetrics();
mDensity = appDm.density;
mScaleDensity = appDm.scaledDensity;
// appDm.widthPixels始终都是短的边,不论是否设置了横竖屏
// 与通过WindowManager获取的不一样
mScreenWidth = appDm.widthPixels;
/**
* 监听设置中字体大小的更改
*/
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(@NonNull Configuration configuration) {
if (configuration != null && configuration.fontScale > 0) {
// 重新获取字体的缩放
mScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
// 会重新执行Activity的onCreate方法,即重新执行后面的设置代码
}
}
@Override
public void onLowMemory() {
}
});
}
// 通过标准屏幕宽dp和实际屏幕宽度px,计算将实际屏幕px换算成标准dp,对应的density值
float targetDensity = mScreenWidth / STANDARD_WIDTH;
// 通过新的density计算新的scaleDensity
float targetScaleDensity = mScaleDensity * (targetDensity / mDensity);
float densityDpi = targetDensity * 160;
DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
activityDm.density = targetDensity;
activityDm.scaledDensity = targetScaleDensity;
activityDm.densityDpi = (int) densityDpi;
}
}
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) {
DensityUtil.setDensity(activity);
}
......
});
}
}