A once-and-for-all screen adaptation scheme

Keywords: xml Android Google Fragment

Screen adaptation in Android is often a headache. Various screen resolutions bring us a lot of trouble in adapting. Google officially suggests that we use dp instead of px, but most of the designer's drawings are marked with px, which makes us feel uncomfortable when writing xml files. I believe that no programmer is willing to spend a lot of time writing xml files. In retrospect of the barbaric fit just touched Android, dividing the PX value of the design icon by two as the dp value directly goes to the layout file. Considering that there were still many 320 DPI devices at that time, testing some mainstream models is also appropriate. Of course, for more complex layouts, or for models that want to accommodate more resolution, this approach obviously makes no sense.

So is there any way to achieve a once-and-for-all screen adaptation? Copy the px value of the design drawing directly, no matter the width or height of the control or margin, simply copy it down. This solution at first glance violates Google's official recommendations, but let's think about why we need to use dp. Because DP can represent different pixel values in different dpi screens, the conversion formulas of DP and px are as follows:

px = dp * (dpi / 160)

In the 160 dpi screen, 1dp is 1px, while in the 320 dpi screen, 1dp is 2px. Obviously, the purpose of using dp is to make fixed values written in xml different on different dpi screens. The different embodiments here can be understood as different pixel values. Our requirements can be understood as the number written in xml such as 50, in 480 * 800, 720 * 1280 screen into different pixel values, but using dp system to automatically help us complete the conversion process, if we do our own conversion, of course, using PX is also possible. What we need to do is to write directly in xml according to the size of the design drawing, calculate the ratio of the size of the design drawing to the size of the screen, and convert all the numbers, so it must be adapted to the screen.

Direct code analysis:

    public static final int BASE_WIDTH=720;//Width and height of design drawings

    public static final int BASE_HEIGHT=1280;

    public static float mScale=1f;//Scaling ratio

    /**
     * Calculate scaling ratio
     */
    public static void initScaleValue(Context context){
        DisplayMetrics displayMetrics=context.getResources().getDisplayMetrics();
        mScale=(float) (displayMetrics.widthPixels)/BASE_WIDTH;
    }

Here we need to specify the size of the design drawing and calculate the ratio to the screen size. Usually, the initScaleValue() method can be invoked in the onCreate() method of the application.

 /**
     * Zoom in on any View
     */
    public static void initScaleView(View v) {
        if (v != null) {
            if (v instanceof ViewGroup) {
                scaleViewGroup((ViewGroup) v);
            } else {
                scaleView(v);
            }
        }
    }

initScaleView() is an externally provided method for scaling any View. This is how we adapt, including Activity Layout, Fragment Layout, Item Layout in the adapter. As long as View is passed in as a parameter, it can be scaled. Of course, we still need to do it once and for all. We don't need to write this method once for every layout file, as follows:

 ScaleUtils.initScale(ButterKnife.findById(this, android.R.id.content));

This is done in the onCreate() method of BaseActivity. Let's look at scaleViewGroup() and scaleView() methods in detail.

 /**
     * Loop through ViewGroup
     */
    private static void scaleViewGroup(ViewGroup viewGroup) {
        for (int i = 0; i < viewGroup.getChildCount(); i++) {
            View v = viewGroup.getChildAt(i);
            if (v instanceof ViewGroup) {
                scaleViewGroup((ViewGroup) v);
            } else {
                scaleView(v);
            }
        }
    }

    /**
     * Processing View
     */
    private static void scaleView(View v) {
        Object hasScaled = v.getTag(R.id.is_scale_tag);
        if (hasScaled instanceof Boolean) {
            if ((Boolean) hasScaled) return;
        }
        if (v instanceof TextView){
            processScaleTextView((TextView)v);
        }else {
            processScaleView(v);
        }
        v.setTag(R.id.is_scale_tag,true);
    }

As you can see, scaleViewGroup() is ultimately called scaleView(), because ViewGroup is still made up of a series of Views. We just need to iterate through the child nodes of ViewGroup. Once we find that the child node is still ViewGroup, we call scaleViewGroup() recursively until all the child views in ViewGroup call the scaleView() method. In the scaleView() method, we first need to determine whether the View has been scaled to avoid repeated scaling. Then for TextView, we need special processing, not only to scale the size of the control, margin, etc., but also to adapt its font size. Let's first look at the handling of TextView.

  /**
     * Processing TextView
     */
    private static void processScaleTextView(TextView view) {
       if (view==null)return;
        processScaleView(view);
        processScaleTextSize(view);
    }

 /**
     * Scale TextView font size
     */
    private static void processScaleTextSize(TextView view) {
        if (view==null)return;
        Object hasScaled=view.getTag(R.id.is_scale_font_size_tag);
        if (hasScaled instanceof Boolean){
            if ((Boolean)hasScaled)return;
        }
        float size=view.getTextSize();
        size*=mScale;
        view.setTextSize(TypedValue.COMPLEX_UNIT_PX,size);
    }

For Textview, the first step is to call processScaleView() method to zoom the control, and then call processScaleTextSize() to zoom the font size. Note that you also need to determine whether the zoom has been done. Then take a look at how the processScaleView() method scales the control.

    private static void processScaleView(View v) {
        if (v == null) return;

        int left = getScalePxValue(v.getPaddingLeft());
        int top = getScalePxValue(v.getPaddingTop());
        int right = getScalePxValue(v.getPaddingRight());
        int bottom = getScalePxValue(v.getPaddingBottom());

        v.setPadding(left, top, right, bottom);

        if (v instanceof TextView) {
            Drawable[] drawables = ((TextView) v).getCompoundDrawables();
            setCompoundDrawablesWithIntrinsicBounds((TextView) v, drawables[0], drawables[1], drawables[2], drawables[3]);
            ((TextView) v).setCompoundDrawablePadding(getScalePxValue(((TextView) v).getCompoundDrawablePadding()));
        }

        v.setLayoutParams(scaleParams(v.getLayoutParams()));
    }

First, the padding of the control is scaled, and the getScalePxValue() method changes the original value according to mScale.

public static int getScalePxValue(int value) {
        if (value <= 4) {
            return value;
        }
        return (int) Math.ceil(mScale * value);
    }

If it's TextView, it also scales the four Drawable s up and down the TextView.

private static void setCompoundDrawablesWithIntrinsicBounds(TextView view,
                                                                Drawable left, Drawable top, Drawable right, Drawable bottom) {

        if (left != null) {
            scaleBoundsDrawable(left);
        }

        if (right != null) {
            scaleBoundsDrawable(right);
        }

        if (top != null) {
            scaleBoundsDrawable(top);
        }

        if (bottom != null) {
            scaleBoundsDrawable(bottom);
        }

        view.setCompoundDrawables(left, top, right, bottom);
    }

public static Drawable scaleBoundsDrawable(Drawable drawable) {
        drawable.setBounds(0, 0,
                getScalePxValue(drawable.getIntrinsicWidth()),
                getScalePxValue(drawable.getIntrinsicHeight()));
        return drawable;
    }

Finally, the scaleParams method, which scales the width and margin of the control itself, is required for any View.

public static ViewGroup.LayoutParams scaleParams(
            ViewGroup.LayoutParams params) {
        if (params == null) {
            throw new RuntimeException("params not's null");
        }
        if (params.width > 0) {
            params.width = getScalePxValue(params.width);
        }

        if (params.height > 0) {
            params.height = getScalePxValue(params.height);
        }

        if (params instanceof ViewGroup.MarginLayoutParams) {
            ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params;
            lp.topMargin = getScalePxValue(lp.topMargin);
            lp.leftMargin = getScalePxValue(lp.leftMargin);
            lp.bottomMargin = getScalePxValue(lp.bottomMargin);
            lp.rightMargin = getScalePxValue(lp.rightMargin);
        }

        return params;
    }

This will solve the problem of screen adaptation once and for all. When we get the design drawings, ask about the size of the design drawings, just copy the designer's label!

If you have any questions, please join us for discussion: 261386924

Posted by FrozNic on Mon, 01 Apr 2019 22:21:30 -0700