Android高级开发工程师-屏幕适配解决方案

975 阅读3分钟

屏幕适配常见方式

布局适配

  • 避免写死控件尺寸,使用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);
            }
            ......
        });
    }
}