布局适配:
- 避免写死控件尺寸,使用wrap_content、match_parent
- LinearLayout使用layout_weight
- RelativeLayout使用centerInParent等
- 使用ContraintLayout,类似RelativeLayout,比RelativeLayout性能好
- 使用Percent-support-lib,layout_widthPercent="30%"等
图片资源适配:
- .9图或则SVG图实现缩放
- 备用位图匹配不同分辨率
限定符适配:
- 分辨率限定符drawable-hdpi、drawable-xdpi、...
- 尺寸限定符layout-small、layout-large(不如在phone和pad上显示不同的布局)
- 最小宽度限定符values-sw360dp、values-sw384dp、...
- 屏幕方向限定符layout-land、layout-port
如果对适配要求比较高,限定符适配就不能满足需求,举个例子,假设我们有这样的需求:显示宽度为屏幕一半的一张图片。
先说下Android布局中单位的基本概念:
px:像素,平常所说的1920×1080就是像素数量,也就是1920px×1080px,代表手机高度上有1920个像素点,宽度上有1080个像素点
dpi:每英寸多少像素,也就是说同分辨率的手机也会存在dpi不同的情况
dp:官方叙述为当屏幕每英寸有160个像素时(也就是160dpi),dp与px等价的。那如果每英寸240个像素呢?1dp—>1240/160=1.5px,即1dp与1.5px等价了。
综上:dpi = 像素/尺寸, px=dpi/160dp
然后说上面的问题,直接用px肯定不行,换成dp能处理大多数情况,但是有些情况还是显示不正确。比如宽度都为1080px的屏幕,但是因为尺寸不同dpi分别是160和240,当把图片宽度设置为540dp时,那么在dpi为160的屏幕上显示是540px,也就是屏幕的一半,但是在dpi为240的屏幕上,根据上述算法,显示为540*(240/160)px,所以在屏幕宽度为1080px的屏幕上显示并不是屏幕的一半(dpi越大,显示图片越宽)。这样满足不了我们需求。
所以适配还是需要手撸,常见的有:自定义像素适配、百分比布局适配、修改像素密度适配。
1. 自定义像素适配
以一个特定宽度尺寸的设备为参考,在View的加载过程中根据当前设备的实际像素换算出目标像素,再作用在控件上。
首先获取写一个工具类获取设计稿和当前手机屏幕的缩放比例,这里采用单例的Utils:
public class Utils {
private static Utils utils;
//这里是设计稿参考宽高
private static final float STANDARD_WIDTH = 1080;
private static final float STANDARD_HEIGHT = 1920;
//这里是屏幕显示宽高
private int mDisplayWidth;
private int mDisplayHeight;
private Utils(Context context){
//获取屏幕的宽高
if(mDisplayWidth == 0 || mDisplayHeight == 0){
WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (manager != null){
DisplayMetrics displayMetrics = new DisplayMetrics();
manager.getDefaultDisplay().getMetrics(displayMetrics);
if (displayMetrics.widthPixels > displayMetrics.heightPixels){
//横屏
mDisplayWidth = displayMetrics.heightPixels;
mDisplayHeight = displayMetrics.widthPixels;
}else{
mDisplayWidth = displayMetrics.widthPixels;
mDisplayHeight = displayMetrics.heightPixels - getStatusBarHeight(context);
}
}
}
}
public 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 Utils getInstance(Context context){
if (utils == null){
utils = new Utils(context.getApplicationContext());
}
return utils;
}
//获取水平方向的缩放比例
public float getHorizontalScale(){
return mDisplayWidth / STANDARD_WIDTH;
}
//获取垂直方向的缩放比例
public float getVerticalScale(){
return mDisplayHeight / STANDARD_HEIGHT;
}
}
自定义一个RelativeLayout:
public class ScreenAdapterLayout extends RelativeLayout {
// 防止重复调用
private boolean flag;
public ScreenAdapterLayout(Context context) {
super(context);
}
public ScreenAdapterLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScreenAdapterLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!flag){
//获取横、纵向缩放比
float scaleX = Utils.getInstance(getContext()).getHorizontalScale();//
float scaleY = Utils.getInstance(getContext()).getVerticalScale();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
//重新设置子View的布局属性
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);
}
flag = true;
}
// 计算完成后再进行测量
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
之后我们的布局文件都要用这个自定义的RelativeLayout包裹,当前我们还需要自定义LinearLayout等,就能实现适配,注意的是单位要用px,就是设计稿上的px值:
<?xml version="1.0" encoding="utf-8"?>
<com.netease.screenadapter.pixel.ScreenAdapterLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="540px"
android:layout_height="540px"
android:layout_marginLeft="10px"
android:text="Hello World!"
android:background="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.netease.screenadapter.pixel.ScreenAdapterLayout>
完事!
2. 百分比布局适配
用Google的Percent-support-lib就可以,这里不说使用,说下实现。
首先肯定要自定义属性,让控件可以设置百分比,在attrs里添加:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PercentLayout">
<attr name="widthPercent" format="float" />
<attr name="heightPercent" format="float" />
<attr name="marginLeftPercent" format="float" />
<attr name="marginRightPercent" format="float" />
<attr name="marginTopPercent" format="float" />
<attr name="marginBottomPercent" format="float" />
</declare-styleable>
</resources>
这些属性肯定要解析并使用,具体的解析过程可以在RelativeLayout或者LinearLayout的源码中查看它们的特有属性是怎么处理的。LayoutInflater的源码中可以看出View的布局属性,都是在父容器中创建的(源码分析就不贴出了,主要的方法就是调用了父容器的generateLayoutParams()方法),所以直接自定义Layout去获取去这些属性就可以了。这里直接贴出处理代码:
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 count = getChildCount();
for (int i = 0; i < count; 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;
float marginLeftPercent = lp.marginLeftPercent;
float marginRightPercent= lp.marginRightPercent;
float marginTopPercent= lp.marginTopPercent;
float marginBottomPercent = lp.marginBottomPercent;
if (widthPercent > 0){
params.width = (int) (widthSize * widthPercent);
}
if (heightPercent > 0){
params.height = (int) (heightSize * heightPercent);
}
if (marginLeftPercent > 0){
((LayoutParams) params).leftMargin = (int) (widthSize * marginLeftPercent);
}
if (marginRightPercent > 0){
((LayoutParams) params).rightMargin = (int) (widthSize * marginRightPercent);
}
if (marginTopPercent > 0){
((LayoutParams) params).topMargin = (int) (heightSize * marginTopPercent);
}
if (marginBottomPercent > 0){
((LayoutParams) params).bottomMargin = (int) (heightSize * marginBottomPercent);
}
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs){
return new LayoutParams(getContext(), attrs);
}
public static class LayoutParams extends RelativeLayout.LayoutParams{
private float widthPercent;
private float heightPercent;
private float marginLeftPercent;
private float marginRightPercent;
private float marginTopPercent;
private float marginBottomPercent;
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);
marginLeftPercent = a.getFloat(R.styleable.PercentLayout_marginLeftPercent, 0);
marginRightPercent = a.getFloat(R.styleable.PercentLayout_marginRightPercent, 0);
marginTopPercent = a.getFloat(R.styleable.PercentLayout_marginTopPercent, 0);
marginBottomPercent = a.getFloat(R.styleable.PercentLayout_marginBottomPercent, 0);
a.recycle();
}
}
}
然后我们布局的时候,用自定的Layout包裹就行:
<?xml version="1.0" encoding="utf-8"?>
<com.netease.screenadapter.percentlayout.PercentLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="宽50%;高75%"
android:background="#f00"
app:widthPercent="0.5"
app:heightPercent="0.75"
app:marginLeftPercent="0.5"/>
</com.netease.screenadapter.percentlayout.PercentLayout>
完事!
总结下自定义属性解析:
- 在attrs里创建自定义属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PercentLayout">
<attr name="widthPercent" format="float" />
...
</declare-styleable>
</resources>
- 创建自定义Layout,比如:
public class PercentLayout extends RelativeLayout
- 在自定义Layout中创建静态内部类LayoutParams继承自该Layout. LayoutParams并实现构造方法,在其构造方法中用obtainStyledAttributes去解析这些自定义属性:
public static class LayoutParams extends RelativeLayout.LayoutParams
private float widthPercent;
...
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);
...
a.recycle();
}
}
- 重写自定义Layout的generateLayoutParams()方法,使用我们自定义的LayoutParams:
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs){
return new LayoutParams(getContext(), attrs);
}
- 重写checkLayoutParams,模仿ViewGroup中的代码,可写可不写。用于获取LayoutParams时的类型判断,也可以直接用p instanceof LayoutParams去判断:
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
- 使用:
if (checkLayoutParams(params)){
LayoutParams lp = (LayoutParams)params;
float widthPercent = lp.widthPercent;
...
}
3. 修改像素密度适配
修改density、scaleDensity,densityDpi的值,直接更改系统内部对于目标尺寸的像素密度。
density:屏幕密度,系统针对某一尺寸的分辨率缩放比例(某一尺寸是指每寸有160px的屏幕,上面也有提到过),假设某个屏幕每英寸有320px,那么此时density为2
scaleDensity:字体缩放比例,默认情况下和density一样
densityDpi:每英寸像素的,比如刚才说的160或320,可以通过屏幕尺寸和分辨率算出来
为什么修改这些值能达到屏幕适配?
TypeValue源码中有这样一段:
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;
}
这段代码说明我们不管在XML里设置什么单位(sp、dp、px),最终都会转换成px设置到屏幕上,而转换过程的计算方式就用到了density、scaledDensity。
为什么修改density,不使用系统的density?
因为相同分辨率的屏幕,因为尺寸不同,density也会不同,例子上面提到过。
原理完事直接贴代码:
新建一个Density类,提供setDensity()方法:
public class Density {
private static final float WIDTH = 320;//参考设备的宽,单位是dp 320 / 2 = 160
private static float appDensity;//表示屏幕密度
private static float appScaleDensity; //字体缩放比例,默认appDensity
public static void setDensity(final Application application, Activity activity){
//获取当前app的屏幕显示信息
DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
if (appDensity == 0){
//初始化赋值操作
appDensity = displayMetrics.density;
appScaleDensity = displayMetrics.scaledDensity;
//添加字体变化监听回调
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
//字体发生更改,重新对scaleDensity进行赋值
if (newConfig != null && newConfig.fontScale > 0){
appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
//计算目标值density, scaleDensity, densityDpi
float targetDensity = displayMetrics.widthPixels / WIDTH; // 1080 / 360 = 3.0
float targetScaleDensity = targetDensity * (appScaleDensity / appDensity);
int targetDensityDpi = (int) (targetDensity * 160);
//替换Activity的density, scaleDensity, densityDpi
DisplayMetrics dm = activity.getResources().getDisplayMetrics();
dm.density = targetDensity;
dm.scaledDensity = targetScaleDensity;
dm.densityDpi = targetDensityDpi;
}
}
然后在每个Activity里调用Density.setDensity(getApplication(),this)设置就可以了,当然可以在BaseActivity里调用。但是最好的解决方式是在Application的registerActivityLifecycleCallbacks()里设置:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
Density.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) {
}
});
}
}
完事!!!
网友评论