面试问你屏幕适配,那么你要知道为什么Android要做屏幕适配,因为Android是开源的, 各大厂商不仅可以对软件定制,还可以对硬件定制,这样就造成市场上不同分辨率的手机超多,现在估计得有几万或者几十万种,这就导致android设备的碎片化很严重。所以还是做ios很辛福啊,下面对一些概念弄清楚
屏幕尺寸:指屏幕的对角线的长度,单位是英寸,1英寸=2.54厘米
屏幕分辨率:是指横纵上的像素点 单位是px 1px = 1个像素点 一般是以纵向像素*横向像素 比如1920*1080 一个屏幕分辨率越高 显示效果就越好
屏幕像素密度:是指每英寸上的像素点数 单位是dpi 是dot per inch 的缩写,屏幕像素密度与屏幕尺寸以及屏幕分辨率有关
以Google的Nexus5为例,它的分辨率是1920*1080 它的屏幕尺寸是4.95inch 屏幕像素密度是445 这是怎么计算出来的呢?
1920*1920+1080*1080这值是4852800 然后开根号再除以4.95就得到是445.03175153177745
像素:构成图像的最小单位 美工或者设计师使用
dip:density independent pixels 是指密度 与像素无关以160dpi为基准,1dip = 1px 和dp一样
加入有二个设备 一个480*320 密度是160dpi. 另外一台是800*480像素密度是240dpi
比如你要在这二个屏幕上要TextView的宽度充满横屏除了使用match_parent还可以使用如下:
我们知道480*320 它的宽度是320px,它是以160dpi为基准的,1px = 1dip 那么它的宽度就是320px就可以 但是在800*480也就是说它的宽度是480px,该如何计算呢?这个也很简单,240/160=1/x; 求这x是多少1.5 相当于1dp = 1.5px 那么它的宽度就是320*1.5 其实这就是我们做屏幕适配使用到的核心技术,想要适配所有手机都是这么适配的。
我们在创建Android项目的时候 系统会帮助我们生成
drawable_mdpi
drawable_hdpi
drawable_xdpi
drawable_xxdpi
drawable_xxxdpi
对应的密度如下:
上面是讲了基本的概念, 下面谈谈如何去适配?
第一种方案:限定符适配
分辨率限定符 drawable-hdpi drawable-xdpi drawable-xxdpi
尺寸限定符layout-small layout-large
最小宽度限定符:values-sw360dp values-sw384dp
屏幕方向限定符:layout_port layout-land
这种方案几乎不用,除非在一些很小公司 做出来的app没啥人用, 大点的额公司肯定不用这套方案,比如我一张图片要放在不同的分辨率下 不但给美工同事添加了工作量,app打包后体积一定会增大,维护起来很麻烦。
第二种方案:自定义像素适配
这种适配目前是最好的,几乎能适配市面上所有的适配 当初在上面公司 交给test in 一个三方的测试公司, 测试了600多设备 都没出现问题,所以这种很靠谱
实现方案:以美工的设计尺寸为原始尺寸,根据不同设备的密度 计算出宽和高
代码如下:
public class UIAdapter {
private static volatile UIAdapter instance = null;
//设计师的参考尺寸
private static final float defaultWidth = 1080;
private static final float defaultHeight = 1920;
//屏幕的真实尺寸
private int screenWidth;
private int screenHeight;
private UIAdapter(){
}
public static UIAdapter getInstance(){
if(null==instance){
synchronized (UIAdapter.class){
if(null==instance){
instance = new UIAdapter();
}
}
}
return instance;
}
public void init(Context context) {
if(null==context){
return;
}
WindowManager wm = (WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(displayMetrics);
if(displayMetrics.widthPixels>displayMetrics.heightPixels){//横屏
screenWidth = displayMetrics.heightPixels;
screenHeight = displayMetrics.widthPixels;
}else{
screenWidth = displayMetrics.widthPixels;
screenHeight = displayMetrics.heightPixels-getStatusBarHeight(context);
}
}
/**
* 获取状态栏高度
* @param context
* @return
*/
public static int getStatusBarHeight(Context context) {
Resources resources = context.getResources();
int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
int height = resources.getDimensionPixelSize(resourceId);
return height;
}
public float scaleX(){
return screenWidth/defaultWidth;
}
public float scaleY(){
return screenHeight/defaultHeight;
}
public void scaleView(View v, int w, int h, int l, int t, int r, int b) {
if(v==null){
return;
}
w = (int) (w*scaleX());
h = (int) (h*scaleY());
l = (int) (l*scaleX());
t = (int) (t*scaleY());
r = (int) (r*scaleX());
b = (int) (b*scaleY());
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
if (params != null) {
params.width = w;
params.height = h;
params.setMargins(l, t, r, b);
}
}
}
记得在Application初始化下:
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
UIAdapter.getInstance().init(this);
}
}
使用:
UIAdapter.getInstance().scaleView(textview,540,200,0,0,0,0);
如果想显示屏幕的1/3的话就是360了宽度,是根据设计师给出来的宽度进行设置
第三种方案: 百分比适配
这是Google 提出来的一个解决适配方案,想要使用必须添加依赖:
implementation 'com.android.support:percent:28.0.0'
主要就二个类:
PercentRelativeLayout
PercentFrameLayout
主要属性如下:
app:layout_heightPercent:用百分比表示高度
app:layout_widthPercent:用百分比表示宽度
app:layout_marginPercent:用百分比表示View之间的间隔
app:layout_marginLeftPercent:用百分比表示左边间隔
app:layout_marginRight:用百分比表示右边间隔
app:layout_marginTopPercent:用百分比表示顶部间隔
app:layout_marginBottomPercent:用百分比表示底部间隔
app:layout_marginStartPercent:用百分比表示距离第一个View之间的距离
app:layout_marginEndPercent:用百分比表示距离最后一个View之间的距离
app:layout_aspectRatio:用百分比表示View的宽高比
简单的布局看看:
其实真实的项目中都没用过,那么它的实现原理是什么样的,因为现在面试不问你怎么使用,怎么使用它时初级工程师干的活,做了3到5年的人怎么去跟哪些刚毕业或者从事2年的比,那么这个时候比的就是内功了,怎么体现你比那些人牛逼呢?看PercentRelativeLayout的源码大概知道它怎么弄的,我们根据它的源代码返照写个,我们在分析view的加载流程中你的xml布局怎么生成对应的类文件,在这就不分析view的加载流程了,我们在Activity中写的setContentView()是调用了PhoneWindow中setContentView():
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
layoutResId就是我们的xml布局,看这段代码:
mLayoutInflater.inflate(layoutResID, mContentParent);
最终会调用LayoutInflater中的
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
方法里面有一段很关键的代码:
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
root变量可以看作是你布局中的根view
params = root.generateLayoutParams(attrs);
这个是获取ViewGroup.LayoutParams,如果你根view是RelativeLayout,那么LayoutParams类是干吗用的呢?进入到RelativeLayout中看看LayoutParams类
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
@ViewDebug.ExportedProperty(category = "layout", resolveId = true, indexMapping = {
@ViewDebug.IntToString(from = ABOVE, to = "above"),
@ViewDebug.IntToString(from = ALIGN_BASELINE, to = "alignBaseline"),
@ViewDebug.IntToString(from = ALIGN_BOTTOM, to = "alignBottom"),
@ViewDebug.IntToString(from = ALIGN_LEFT, to = "alignLeft"),
@ViewDebug.IntToString(from = ALIGN_PARENT_BOTTOM, to = "alignParentBottom"),
@ViewDebug.IntToString(from = ALIGN_PARENT_LEFT, to = "alignParentLeft"),
@ViewDebug.IntToString(from = ALIGN_PARENT_RIGHT, to = "alignParentRight"),
@ViewDebug.IntToString(from = ALIGN_PARENT_TOP, to = "alignParentTop"),
@ViewDebug.IntToString(from = ALIGN_RIGHT, to = "alignRight"),
@ViewDebug.IntToString(from = ALIGN_TOP, to = "alignTop"),
@ViewDebug.IntToString(from = BELOW, to = "below"),
@ViewDebug.IntToString(from = CENTER_HORIZONTAL, to = "centerHorizontal"),
@ViewDebug.IntToString(from = CENTER_IN_PARENT, to = "center"),
@ViewDebug.IntToString(from = CENTER_VERTICAL, to = "centerVertical"),
@ViewDebug.IntToString(from = LEFT_OF, to = "leftOf"),
@ViewDebug.IntToString(from = RIGHT_OF, to = "rightOf"),
@ViewDebug.IntToString(from = ALIGN_START, to = "alignStart"),
@ViewDebug.IntToString(from = ALIGN_END, to = "alignEnd"),
@ViewDebug.IntToString(from = ALIGN_PARENT_START, to = "alignParentStart"),
@ViewDebug.IntToString(from = ALIGN_PARENT_END, to = "alignParentEnd"),
@ViewDebug.IntToString(from = START_OF, to = "startOf"),
@ViewDebug.IntToString(from = END_OF, to = "endOf")
它的构造函数:
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.RelativeLayout_Layout);
final int targetSdkVersion = c.getApplicationInfo().targetSdkVersion;
mIsRtlCompatibilityMode = (targetSdkVersion < JELLY_BEAN_MR1 ||
!c.getApplicationInfo().hasRtlSupport());
final int[] rules = mRules;
//noinspection MismatchedReadAndWriteOfArray
final int[] initialRules = mInitialRules;
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignWithParentIfMissing:
alignWithParent = a.getBoolean(attr, false);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toLeftOf:
rules[LEFT_OF] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toRightOf:
rules[RIGHT_OF] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_above:
rules[ABOVE] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_below:
rules[BELOW] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignBaseline:
rules[ALIGN_BASELINE] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignLeft:
rules[ALIGN_LEFT] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignTop:
rules[ALIGN_TOP] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignRight:
rules[ALIGN_RIGHT] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignBottom:
rules[ALIGN_BOTTOM] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentLeft:
rules[ALIGN_PARENT_LEFT] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentTop:
rules[ALIGN_PARENT_TOP] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentRight:
rules[ALIGN_PARENT_RIGHT] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentBottom:
rules[ALIGN_PARENT_BOTTOM] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerInParent:
rules[CENTER_IN_PARENT] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerHorizontal:
rules[CENTER_HORIZONTAL] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerVertical:
rules[CENTER_VERTICAL] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toStartOf:
rules[START_OF] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toEndOf:
rules[END_OF] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignStart:
rules[ALIGN_START] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignEnd:
rules[ALIGN_END] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentStart:
rules[ALIGN_PARENT_START] = a.getBoolean(attr, false) ? TRUE : 0;
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentEnd:
rules[ALIGN_PARENT_END] = a.getBoolean(attr, false) ? TRUE : 0;
break;
}
}
你会发现你在xml中写的这些属性:
android:id="@+id/textview"
android:background="#f0f000"
app:layout_heightPercent="50%"
app:layout_widthPercent="50%"
android:text="Hello World!"
android:gravity="center"
都是通过LayoutParam加载进去的,那么我们就根据这个自己实现:首先定义一些属性,在values创建一个文件 attr
下面是实现自定义RelativeLayout
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;
}
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();
}
}
}
上面其实就是百分比的实现原理了.这种实现方案缺点就是如果你使用了三方库的话就没办法了,所以不适应真实的项目中,而且就提供了
第四种方案:修改density方案来实现屏幕适配,要修改三个值 分别是
density是指屏幕的密度 可以理解为Android系统内部针对某个尺寸它的分辨率 它的缩放比例, 这个缩放比例是指屏幕上每一寸有160个像素点,比如某个屏幕上达到320px,那么它的density就是2了,
scaleDensity:是指字体的缩放比例
densityDpi:指屏幕上每一英寸上像素点有多少个比如160 或者320
为什么能通过这三个值能达到修改从而适配呢?因为在Android不管你在xml设置了什么 最终都是转换成像素(px)显示,在Android系统类TypedValue类中:
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;
}
这个就是把其他单位转换成px,
density不同的设备它的值不一样,而且相同的分辨率下density也可能不一样,所以我们要对它进行调整处理,让density随着分辨率的变化而变化,但是这个也要设计师给出参考密度
代码如下:
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 Activity activity){
//获取当前app的屏幕显示信息
DisplayMetrics displayMetrics = activity.getApplication().getResources().getDisplayMetrics();
if (appDensity == 0){
//初始化赋值操作
appDensity = displayMetrics.density;
appScaleDensity = displayMetrics.scaledDensity;
//添加字体变化监听回调
activity.getApplication().registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
//字体发生更改,重新对scaleDensity进行赋值
if (newConfig != null && newConfig.fontScale > 0){
appScaleDensity = activity.getApplication().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的setContentView()前面调用,上面四种方案就是屏幕适配的几种方案了,我是选择了第二种项目中