Android 屏幕适配
常见方案
限定符适配
尺寸限定符
限定符 | 描述 |
---|---|
layout-small | 小屏幕 |
layout-normal | 基准屏幕 |
layout-large | 大屏幕 |
layout-xlarge | 超大屏幕 |
屏幕方向限定符
限定符 | 描述 |
---|---|
layout-land | 横向 |
layout-port | 纵向 |
分辨率限定符
限定符 | 描述 |
---|---|
drawable-ldpi | <= 120dpi |
drawable-mdpi | <= 160dpi |
drawable-hdpi | <= 240dpi |
drawable-xhdpi | <= 320dpi |
drawable-xxhdpi | <= 480dpi |
drawable-xxxhdpi | <= 640dpi |
drawable-nodpi | 适用于所有密度的资源。这些是与密度无关的资源。无论当前屏幕的密度是多少,系统都不会缩放以此限定符标记的资源。 |
drawable-tvdpi | 适用于密度介于 mdpi 和 hdpi 之间的屏幕(约 213dpi)的资源。这不属于“主要”密度组。它主要用于电视,而大多数应用都不需要它。对于大多数应用而言,提供 mdpi 和 hdpi 资源便已足够,系统将视情况对其进行缩放。如果您发现有必要提供 tvdpi 资源,应按一个系数来确定其大小,即 1.33*mdpi。例如,如果某张图片在 mdpi 屏幕上的大小为 100px x 100px,那么它在 tvdpi 屏幕上的大小应该为 133px x 133px。 |
其他限定符
名称 | 限定符 | 描述 |
---|---|---|
最小宽度限定符 | swdp 例如sw600dp, sw720dp | 屏幕的最小尺寸,就是屏幕可用区域的最小尺寸,是指屏幕可用高度或宽度的最小值(你可以默认是屏幕的最小宽度).你能用这个限定符确保,无论屏幕方向如何,这个限定符修饰下的布局需要的屏幕最小尺寸是Ndp. 例如,如果你的布局在运行时需要的最小屏幕宽度是600dp,则你可以利用这个限定符创建布局资源目录res/layout-sw600dp.只有当屏幕的最小宽度或最小高度是600dp时,系统才会使用这些布局文件或者资源文件.最小屏幕宽度是固定设备的特有屏幕尺寸,当屏幕方向发生变化时,设备的最小宽度值不变.设备的最小宽度值要考虑屏幕的尺寸和系统UI.例如,如果在屏幕上有一些系统持久化UI元素,则系统的最小宽度值要比实现的屏幕尺寸小一些,因为这些系统的UI元素你的应用是无法使用到的.当你使用之前的广义限定符是,你可以定义连续的一系列限定符.用最小宽度来决定广义屏幕尺寸是有意义的,是因为宽度是影响你UI设计的关键因素.UI在竖直方向上会经常滚动,但是在水平方向上往往是固定的布局.可见不论是适配手机或者平板,宽度往往都是布局的关键因素.因此,你需要关心你手机上的最小宽度值. |
屏幕可用宽度 | wdp Examples: w720p w1024p | 指定资源使用时需要的最小宽度.当屏幕方向发生变化时,系统会调整这个值,使其始终为你UI显示的宽度. 这个属性经常被用来判断当前是否需要显示多屏布局,因为哪怕用户当前正在使用平板,你也可能不希望用户在平板竖屏时显示多个屏幕的布局样式.这时,你就可以使用这个限定符来标明你布局需要的最小宽度 |
屏幕可用高度 | hdp Examples: h720dp h1024dp | 标明资源使用时需要的最小高度.当屏幕发生旋转时,系统会自动选择当前大的一方作为高度值 |
.9 图或者 SVG 图实现缩放适配
.9 图为 Android 支持的一种可以自由伸缩的图片,可参考官网描述。这里提供一个制作 .9 的链接
而 SVG (多密度矢量图形) 在 Android 4.4 开始支持。可参考官网描述。
AndroidStudio 为我们提供了许多默认的 SVG 图,可以在 drawable 中 新建一个 Vector Asset 来创建一个新的 SVG 资源
非常规方案
自定义View-像素适配
以一个特定尺寸(px)作为标准值,在 View 加载过程中,根据当前设备的实际像素尺寸和标准尺寸计算出相应的比例值,在作用于控件上。
可以先预览一下效果,以 720x1280 为基准,宽高比都占屏幕的一半。
具体实现如下
像素适配工具类:
/**
* Author silence.
* Time:2019-08-28.
* Desc:像素适配工具
*/
public final class PixelUtils {
//设计稿标准尺寸
private static final float STANDARD_WIDTH = 720;
private static final float STANDARD_HEIGHT = 1280;
private int mDisplayWidth;
private int mDisplayHeight;
private PixelUtils(Context context){
if (mDisplayWidth == 0 || mDisplayHeight == 0){
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (windowManager != null){
DisplayMetrics displayMetrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
if (displayMetrics.widthPixels > displayMetrics.heightPixels){//横屏
mDisplayWidth = displayMetrics.heightPixels;
mDisplayHeight = displayMetrics.widthPixels;
} else {//竖屏
mDisplayWidth = displayMetrics.widthPixels;
mDisplayHeight = displayMetrics.heightPixels - getStatusBarHeight(context);
}
}
}
}
private int getStatusBarHeight(Context context){
int resID = context.getResources().getIdentifier("status_bar_height","dimen","android");
if (resID > 0){
return context.getResources().getDimensionPixelSize(resID);
}
return 0;
}
public static float getHorizontalScale(Context context){
return getInstance(context).mDisplayWidth / STANDARD_WIDTH;
}
public static float getVerticalScale(Context context){
return getInstance(context).mDisplayHeight / STANDARD_HEIGHT;
}
private static PixelUtils utils;
private static PixelUtils getInstance(Context context){
if (utils == null){
utils = new PixelUtils(context);
}
return utils;
}
}
自定义适配布局:
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
/**
* Author silence.
* Time:2019-08-28.
* Desc:
*/
public class ScreenAdapterRelativeLayout extends RelativeLayout {
private boolean hasAdjust;//防止多次调整
public ScreenAdapterRelativeLayout(Context context) {
super(context);
}
public ScreenAdapterRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScreenAdapterRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!hasAdjust) {
float scaleX = PixelUtils.getHorizontalScale(getContext());
float scaleY = PixelUtils.getVerticalScale(getContext());
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
RelativeLayout.LayoutParams params = (LayoutParams) child.getLayoutParams();
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);
}
hasAdjust = true;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
相应的布局文件:
<com.wuba.demo.screenadapter.ScreenAdapterRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="360px"
android:layout_height="640px"
android:contentDescription="@string/app_name"
android:background="@color/colorPrimary"/>
</com.wuba.demo.screenadapter.ScreenAdapterRelativeLayout>
像素密度
根据特定的标准尺寸(dp)和系统尺寸计算相应的比例,然后修改当前界面的 Density 、 scaleDensity 、 densityDpi 像素密度
可以先预览一下效果,以 320dp 为基准,宽高比都占屏幕的一半。
在 AndroidStudio 中预览的效果
在手机上运行效果:
具体实现如下
import android.app.Activity;
import android.app.Application;
import android.content.ComponentCallbacks;
import android.content.res.Configuration;
import android.util.DisplayMetrics;
/**
* Author silence.
* Time:2019-08-28.
* Desc:像素密度适配工具
*/
public final class DensityUtils {
private static final float WIDTH = 320;//基准尺寸(dp)
private static float appDensity;//屏幕密度
private static float appScaleDensity;//字体缩放比例,默认为 appDensity
public static void setDensity(final Application application, Activity activity){
DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
//获取当前 app 的显示信息
if (appDensity == 0){
appDensity = displayMetrics.density;
appScaleDensity = displayMetrics.scaledDensity;
}
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
//字体发生变化时,更新 appScaleDensity
if (newConfig != null && newConfig.fontScale > 0){
appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
//计算目标值
float targetDensity = displayMetrics.widthPixels / WIDTH;// eg: 1080 / 360 = 3
float targetScaleDensity = targetDensity * (appScaleDensity / appDensity);
int targetDensityDpi = (int) (targetDensity * 160);
//设置目标值
displayMetrics = activity.getResources().getDisplayMetrics();
displayMetrics.density = targetDensity;
displayMetrics.scaledDensity = targetScaleDensity;
displayMetrics.densityDpi = targetDensityDpi;
}
}
百分比布局
以父容器尺寸为标准,在 View 加载过程中,根据当前父容器的实际尺寸计算目标尺寸,在作用于 View 上。谷歌推出过一个 百分比布局,我们此处简略实现下,下面是预览效果。
具体代码实现,首先在 values 中定义相关属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PercentLayout">
<attr name="widthPercent" format="float"/>
<attr name="heightPercent" format="float"/>
</declare-styleable>
</resources>
在自定义 百分比布局 PercentLayout
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;
import com.wuba.demo.R;
/**
* Author silence.
* Time:2019-08-28.
* Desc:
*/
public class PercentLayout extends RelativeLayout {
public PercentLayout(Context context) {
super(context);
}
public PercentLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PercentLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
ViewGroup.LayoutParams params = child.getLayoutParams();
if (checkLayoutParams(params)){
LayoutParams lp = (LayoutParams) params;
float widthPercent = lp.widthPercent;
float heightPercent = lp.heightPercent;
if (widthPercent > 0){
params.width = (int) (widthSize * widthPercent);
}
if (heightPercent > 0){
params.height = (int) (heightSize * heightPercent);
}
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
public RelativeLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(),attrs);
}
public static class LayoutParams extends RelativeLayout.LayoutParams{
private float widthPercent;
private float heightPercent;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
//解析我们自定义的属性
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.PercentLayout);
widthPercent = a.getFloat(R.styleable.PercentLayout_widthPercent,0);
heightPercent = a.getFloat(R.styleable.PercentLayout_heightPercent,0);
a.recycle();
}
}
}
最后就是在 xml 中使用了
<com.wuba.demo.screenadapter.PercentLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent">
<ImageView
android:id="@+id/top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:widthPercent="0.5"
app:heightPercent="0.5"
android:contentDescription="@string/app_name"
android:background="@color/colorPrimary"/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/top"
app:widthPercent="0.5"
app:heightPercent="0.5"
android:layout_below="@id/top"
android:contentDescription="@string/app_name"
android:background="@color/colorPrimary"/>
</com.wuba.demo.screenadapter.PercentLayout>