android 面试题 谈谈屏幕适配
面试问你屏幕适配,那么你要知道为什么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'
主要就二个类:
PercentRelativeLayoutPercentFrameLayout
主要属性如下:
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的宽高比
简单的布局看看:
<?xml version="1.0" encoding="utf-8"?>
其实真实的项目中都没用过,那么它的实现原理是什么样的,因为现在面试不问你怎么使用,怎么使用它时初级工程师干的活,做了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
<?xml version="1.0" encoding="utf-8"?>
下面是实现自定义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(); } }}
上面其实就是百分比的实现原理了.这种实现方案缺点就是如果你使用了三方库的话就没办法了,所以不适应真实的项目中,而且就提供了
- PercentRelativeLayout
- PercentFrameLayout
第四种方案:修改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()前面调用,上面四种方案就是屏幕适配的几种方案了,我是选择了第二种项目中
更多相关文章
- [Android] 一种粗暴快速的 Android 全屏幕适配方案
- android自定义adapter 滑动屏幕时 进度条显示混乱
- Android屏幕横竖屏切换和生命周期管理的详细总结
- Android屏幕锁定实例源码详解教程一
- android 固定横屏幕竖屏
- android 全屏幕显示以及竖屏显示
- android调节屏幕亮度
- android根据屏幕高度改变item占ListView高度