博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
本文首发于此 博主:威威喵 | 博客主页:https://blog.csdn.net/smile_running
由于 Android 手机五花八门,手机厂商较多,所以导致的一个问题就是屏幕分辨率各有千秋,诸如:320*480、540*960、768*1280、1080*1920 等等,因为屏幕分辨率不同,导致一个难题就是如何将我们开发的应用适配这些分辨率,这就是本文要引出的一个屏幕适配的问题。
在早期的做法呢,由于 Android project 给我们提供了不同的 drawable res 文件,如 drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi 等等,分别对应不同的分辨率,从低分辨率到高分辨率。我们通过设计和制作不同的分辨率图片资源,放置在不同的 res 下,这样在 Android 系统加载时,会自动的去找对应 res 下的资源文件,这种方法可以做到适配效果。
可是,这样的话会导致一个比较严重的问题,因为每个 res 下都放置了图片资源文件,如果图片过多的话,会增大 apk 的体积,导致 apk 包过于庞大。还有一个是图片制作也比较费时,虽然是不同的分辨率的同一张图,修改起来也麻烦。
如今,这种做法渐渐的被放弃使用了,因为它的缺点比较明显,接下来,我们来看本文要讲的适配方案。
我们做过 Android 开发的都知道,要尽量的将控件的宽度设置成 wrap_content 或者 match_content,将大小设置为一个 dp 值,而不是具体的 px 值,这样能够有效的适配不同的分辨率。但是呢,难免一下控件需要占用屏幕的一个比例值,比如在 768 * 1280 的分辨率下,TextView 需要占一半效果,那么它的 width 就是 384 个 px;然而在 1080 * 1920 的分辨率下,这个 TextView 的 width 就变成了 540 个 px 了。
那么,要适配这样的一种方式,需要如何做呢?接下来,我们来一起看看本文的适配方案吧!
implementation 'com.android.support:percent:28.+'
接下来,我们要想让 TextView 占用屏幕的一半宽度,就可以通过设置一个百分比即可。布局文件代码如下:
为了更好的证明它能够适配所有的分辨率,我这里开启了两个不同分辨率的模拟器,一个是 768*1280,一个是 1080*1920 的分辨率,通过对比,可以看到它们的显示效果是一致的,如下图
这种方式是最简单的一种,通过引入 percent 控件,设置 layout_widthPercent 以及 layout_heightPercent 即可。
(1)屏幕分辨率:720 * 1280
(2)屏幕分辨率:1080 * 1920
若红色(TextView)的宽度占用屏幕的一半,要想进行适配,在 720 * 1280 的分辨率中,它的宽度是 360 px,而在 1080 * 1920 的分辨率中,它的宽度是 540 px
例如,当前的设计稿像素大小为 720 * 1280 ,其中 720px 是已知的设计稿宽度,而通过代码可以获取设备的宽度,根据比例公式计算可得:1080 / 720 * 360 = 540 px 。通过封装屏幕缩放比例工具类,可以计算出当前设备宽高与设计稿的一个比例值,代码如下:
package nd.no.xww.screenadapter;
import android.content.Context;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.view.WindowManager;
/**
* @author xww
* @desciption : 采用 px 来适配全屏幕
* @date 2020/1/18
* @time 19:10
*/
public class ScreenPixels {
private static ScreenPixels INSTANCE;
private Context context;
// design pixels on a prototype diagram(must float)
private static final float DESIGN_WIDTH = 1080f;
private static final float DESIGN_HEIGHT = 1920f;
private int screenWidth;
private int screenHeight;
private ScreenPixels(Context context) {
this.context = context;
final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (windowManager != null) {
DisplayMetrics displayMetrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
if (displayMetrics.widthPixels > displayMetrics.heightPixels) {
screenWidth = displayMetrics.heightPixels;
screenHeight = displayMetrics.widthPixels;
} else {
screenWidth = displayMetrics.widthPixels;
screenHeight = displayMetrics.heightPixels;
}
}
}
public static ScreenPixels getInstance(Context context) {
if (INSTANCE == null) {
INSTANCE = new ScreenPixels(context.getApplicationContext());
}
return INSTANCE;
}
private int getStatusBarHeight() {
Resources resources = context.getResources();
if (resources != null) {
int resId = resources.getIdentifier("status_bar_height", "dimen", "android");
return resources.getDimensionPixelSize(resId);
}
return 0;
}
public int getScreenWidth() {
return screenWidth;
}
public int getScreenHeight() {
return screenHeight;
}
public float getScaleWidth() {
return getScreenWidth() / DESIGN_WIDTH;
}
public float getScaleHeight() {
return getScreenHeight() / DESIGN_HEIGHT;
}
}
获取到这个像素缩放比例,然后通过对 View 的大小缩放,达到适配的目的。具体可以通过自定义 RelativeLayout 或其它 ViewGroup,覆盖 onMeasure() 方法,对每一个 childView 的width、height 以及 margin 进行缩放,代码如下:
package nd.no.xww.screenadapter;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
/**
* @author xww
* @desciption : 通过缩放比设置控件的宽高、缩进等
* @date 2020/1/18
* @time 19:48
*/
public class AdaptRelativeLayout extends RelativeLayout {
private static final String TAG = "AdaptRelativeLayout";
private boolean flag = true;
public AdaptRelativeLayout(Context context) {
super(context);
}
public AdaptRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AdaptRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (flag) { // just measure once
float scaleWidth = ScreenPixels.getInstance(getContext()).getScaleWidth();
float scaleHeight = ScreenPixels.getInstance(getContext()).getScaleHeight();
final int count = getChildCount();
LayoutParams params;
View childView;
for (int i = 0; i < count; i++) {
childView = getChildAt(i);
params = (LayoutParams) childView.getLayoutParams();
params.width = (int) (params.width * scaleWidth);
params.height = (int) (params.height * scaleHeight);
params.leftMargin = (int) (params.leftMargin * scaleWidth);
params.rightMargin = (int) (params.rightMargin * scaleWidth);
params.topMargin = (int) (params.topMargin * scaleHeight);
params.bottomMargin = (int) (params.bottomMargin * scaleHeight);
}
flag = false;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
这里的 onMeasure() 方法会进行测量两次,所以在缩放的时候,需要进行一个判断,如果重复测量的话,控件的大小将缩放两倍,会导致偏差。布局文件代码如下:
在布局文件中,我们应该写明当前控件的 px 值,相对于屏幕分辨率的一半。最后,我们的适配效果如下:
首先呢,通过获取 application 的 density、scaleDensity、densityApi,对当前 Activity density、scaleDensity、 进行缩放,以达到适配效果。代码如下:
package nd.no.xww.screenadapter;
import android.app.Activity;
import android.app.Application;
import android.content.ComponentCallbacks;
import android.content.res.Configuration;
import android.util.DisplayMetrics;
/**
* @author xww
* @desciption : 根据 dp 值来适配
* @date 2020/1/19
* @time 17:54
*/
public class ScreenDensity {
// 设计稿的屏幕宽度 dp 值
private static final float DESIGN_DENSITY = 360f;
private static float appScaleDensity;
public static void setDensity(Application application, Activity activity) {
// 获取 application 的 DisplayMetrics
DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
float appDensity = appDisplayMetrics.density;
appScaleDensity = appDisplayMetrics.scaledDensity;
//监听字体大小变化,重新获取变化后的 appScaleDensity,适配到应用中
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
// 获取 activity 的 DisplayMetrics
DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
// 计算缩放比例(设备屏幕宽度 / 设计稿宽度)
float targetDensity = appDisplayMetrics.widthPixels / DESIGN_DENSITY;
float targetScaleDensity = targetDensity * (appScaleDensity / appDensity);
int targetDensityApi = (int) (targetDensity * 160);
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaleDensity;
activityDisplayMetrics.densityDpi = targetDensityApi;
}
}
若没有字体大小适配的话,可以不必监听系统字体大小的变化。
这里有必要解释一下,density 表示屏幕的一个物理密度,scaleDensity 表示字体显示大小的一个密度值,通常情况下都是与 density 相等的,而 densityApi 则表示它相对于屏幕密度的一个比例值,就是 dots-per-inch。
注意:在 Activity 的 setContentView 之前进行设置 density
package nd.no.xww.screenadapter;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ScreenDensity.setDensity(getApplication(), this);
setContentView(R.layout.activity_main);
}
}
布局文件代码:
它的一个适配最终效果如下:
如果要设置全局的一个适配,可以有两种方式,一种是抽到 BaseActivity 中进行适配每一个 Activity。第二中方式是在自己的 Application 中,监听每一个 Activity 的生命周期回调情况,代码如下:
package nd.no.xww.screenadapter;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
/**
* @author xww
* @desciption :
* @date 2020/1/19
* @time 19:44
*/
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
ScreenDensity.setDensity(App.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) {
}
});
}
}
最后,在 xml 文件中记得换成我们自己的 App name 即可。
好了,如上提供的三种适配方案都能较好的解决当前屏幕适配问题,至于如何抉择,就看你的使用场景如何了。第一种方案比较简单,如果不是自定义 View 的话,在适配起来会简单很多,第二种比较适合在自定义 View 中做统一的大小处理,第三种是一个全局的适配方案,目前这种方法也在很多 App 中运用,所以比较推荐第三种。