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

对应的密度如下:

android 面试题 谈谈屏幕适配_第1张图片

上面是讲了基本的概念, 下面谈谈如何去适配?

第一种方案:限定符适配

分辨率限定符  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();
        }
    }
}

上面其实就是百分比的实现原理了.这种实现方案缺点就是如果你使用了三方库的话就没办法了,所以不适应真实的项目中,而且就提供了

  • 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)