Skip to content
Advertisement

How to Overcome Aliasing Problem Caused By ConstraintLayout Guidelines?

I am trying to reconcile the following two things:

A) I want a precise, uniform, and clean UI with several identically sized buttons that correspond exactly to the underlying ‘grid cells’ — A UI that will look as similar as possible (proportionally to screen size) across as many Android devices as possible.

B) On Android, the screen dimensions (aspect ratio and actual pixel numbers) of the user’s device are unknown (to the app) until runtime.

My solution to this was to: (there is a code example below!)

1) Lock the app to portrait mode,

2) Do not define anything in static/absolute terms like dp,px, etc. and instead conceptualize a ‘basic unit of measure’ that is a function of screen height — 0.08% in my case — and base everything off of that.

3) Set horizontal guidelines within a ConstraintLayout whose positions are expressed as a percentage of parent (screen) height.

4) Make all buttons use this ‘basic unit’ as their height and width by setting their XML layout_constraintDimensionRatio attribute to “1:1” and using the guidelines above (see step 3),

5) Accomplish positioning and dimensions of all views by using constraints to either these guidelines, the parent’s bounds, or one additional vertical guideline at 50% of screen width.

The problem is that depending on the pixel height of the screen (whether it happens to be odd or even… or maybe other factors), the dimensions of a view/button, (and thus the paths drawn inside it) constrained between one pair of guidelines does not exactly match those of another view drawn between some other pair… even though the distance between both pairs of guidelines should be the same percentage of parent height. 🙂

Here is an example showing the Nexus 4 emulator:

enter image description here

At first I thought the problem was simply due to rounding ‘error’ during Android’s dimension calculations, but then why would the view not be square even though they are prescribed the 1:1 ratio attribute?

The only solutions I can think of would be:

A) To do the layout programatically instead of with XML… and set the guideline positions as exact pixel locations instead of percentages, and answer the question, “what is 0.08 x screen height?” myself… making the appropriate corrections to compensate for ‘indivisible’ screen heights.

B) Override onLayout() in the custom views and “force” their dimensions to be consistent… but then this would defeat the purpose of guidelines. 🙁

But I’m really hoping there is an easier solution than A or B.

(I know someone is going to suggest GridLayout, but it’s not an option, for a few reasons… one of which is that in GridLayout, views inside cells must be set to wrap_content… which means the paths they draw cannot be generated relative to parent at runtime).

Thanks for any other suggestions, though.

Code Example:

I whipped up a simple ‘minimal example’ below that should be easy to reconstruct in Android Studio. The logs will reveal the issue if it’s not immediately apparent.

XML layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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:id="@+id/rootView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <android.support.constraint.Guideline
        android:id="@+id/guidelineHorizontalTop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.08" />

    <android.support.constraint.Guideline
        android:id="@+id/guidelineHorizontalBottom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.92" />

    <android.support.constraint.Guideline
        android:id="@+id/guidelineHorizontalCenter1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.38" />

    <android.support.constraint.Guideline
        android:id="@+id/guidelineHorizontalCenter2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.46" />

    <android.support.constraint.Guideline
        android:id="@+id/guidelineHorizontalCenter3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.54" />

    <android.support.constraint.Guideline
        android:id="@+id/guidelineHorizontalCenter4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.62" />

    <com.example.boober.stack_aliasingproblem.CustomButton
        android:id="@+id/buttonTopLeft"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:tag="buttonTopLeft"
        app:layout_constraintBottom_toTopOf="@+id/guidelineHorizontalTop"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.example.boober.stack_aliasingproblem.CustomButton
        android:id="@+id/buttonTopRight"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:tag="buttonTopRight"
        app:layout_constraintBottom_toTopOf="@+id/guidelineHorizontalTop"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.example.boober.stack_aliasingproblem.CustomButton
        android:id="@+id/buttonBottomLeft"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:tag="buttonBottomLeft"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guidelineHorizontalBottom" />

    <com.example.boober.stack_aliasingproblem.CustomButton
        android:id="@+id/buttonBottomRight"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:tag="buttonBottomRight"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guidelineHorizontalBottom" />

    <com.example.boober.stack_aliasingproblem.CustomButton
        android:id="@+id/buttonMiddle"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:tag="buttonMiddle"
        app:layout_constraintBottom_toBottomOf="@id/guidelineHorizontalCenter3"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@id/guidelineHorizontalCenter2" />

    <com.example.boober.stack_aliasingproblem.CustomButton
        android:id="@+id/buttonMiddleTopLeft"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:tag="buttonMiddleTopLeft"
        app:layout_constraintBottom_toBottomOf="@id/guidelineHorizontalCenter2"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintEnd_toStartOf="@id/buttonMiddle"
        app:layout_constraintTop_toTopOf="@id/guidelineHorizontalCenter1" />

    <com.example.boober.stack_aliasingproblem.CustomButton
        android:id="@+id/buttonMiddleTopRight"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:tag="buttonMiddleTopRight"
        app:layout_constraintBottom_toBottomOf="@id/guidelineHorizontalCenter2"
        app:layout_constraintDimensionRatio="1:1"
        app:layout_constraintStart_toEndOf="@id/buttonMiddle"
        app:layout_constraintTop_toTopOf="@id/guidelineHorizontalCenter1" />
</android.support.constraint.ConstraintLayout>

MainActivity.java:

public class MainActivity extends AppCompatActivity {

    CustomButton buttonTopLeft;
    CustomButton buttonTopRight;

    CustomButton buttonMiddle;
    CustomButton buttonMiddleTopLeft;
    CustomButton getButtonMiddleTopRight;

    CustomButton buttonBottomLeft;
    CustomButton buttonBottomRight;

    CustomButton[] arrayOfCustomButtons;

    ConstraintLayout rootView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        buttonTopLeft = findViewById(R.id.buttonTopLeft);
        buttonTopRight = findViewById(R.id.buttonTopRight);
        buttonBottomLeft = findViewById(R.id.buttonBottomLeft);
        buttonBottomRight = findViewById(R.id.buttonBottomRight);
        buttonMiddle = findViewById(R.id.buttonMiddle);
        buttonMiddleTopLeft = findViewById(R.id.buttonMiddleTopLeft);
        getButtonMiddleTopRight = findViewById(R.id.buttonMiddleTopRight);

        arrayOfCustomButtons = new CustomButton[]{buttonTopLeft, buttonTopRight, buttonBottomLeft,
                buttonBottomRight, buttonMiddle, buttonMiddleTopLeft, getButtonMiddleTopRight};
        rootView = findViewById(R.id.rootView);

        for (final CustomButton cb : arrayOfCustomButtons) {
            cb.setClickable(true);
            cb.post(new Runnable() {
                @Override
                public void run() {
                    Log.i("XXX", "width of: " + cb.getTag() + " is: "
                            + cb.getMeasuredWidth());
                }
            });
        }

        rootView.post(new Runnable() {
            @Override
            public void run() {
                Log.i("XXX", "height of rootView is: " + rootView.getMeasuredHeight());
            }
        });
    }

}

CustomButton.java:

public class CustomButton extends View {

    Path myOutlinePath;

    Paint myThinPaintBrush;
    Paint myThickPaintBrush;

    boolean isHighlighted = false;

    public CustomButton(Context context) {
        super(context);
        init();
    }
    public CustomButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        float measuredWidth = getMeasuredWidth();

        Log.i("XXX", "measured WIDTH Of " + this.getTag() + " is: " + measuredWidth);
        Log.i("XXX", "measured HEIGT Of " + this.getTag() + " is: " + getMeasuredHeight());
        Log.i("XXX", "n ");

        generateMyOutline(measuredWidth);

        myThinPaintBrush.setStrokeWidth(measuredWidth/12);
        myThickPaintBrush.setStrokeWidth(measuredWidth/6);

    }

    private void generateMyOutline(float W) {

        Path path = new Path();

        path.moveTo(0,0);
        path.lineTo(W, 0);
        path.lineTo(W, W);
        path.lineTo(0, W);
        path.lineTo(0,0);

        myOutlinePath = path;

    }

    private void init() {

        myOutlinePath = new Path();

        myThinPaintBrush = new Paint();
        myThinPaintBrush.setAntiAlias(false); // setting this to true does not solve the problem.
        myThinPaintBrush.setStyle(Paint.Style.STROKE);
        myThinPaintBrush.setStrokeCap(Paint.Cap.ROUND);

        myThickPaintBrush = new Paint();
        myThickPaintBrush.setAntiAlias(false);
        myThickPaintBrush.setStyle(Paint.Style.STROKE);
        myThickPaintBrush.setStrokeCap(Paint.Cap.ROUND);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if (this.isClickable()) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    isHighlighted = true;
                    invalidate();
                    break;

                case MotionEvent.ACTION_UP:
                    isHighlighted = false;
                    invalidate();
                    break;

                case MotionEvent.ACTION_CANCEL:
                    isHighlighted = false;
                    invalidate();
                    break;
            }
        }
        return super.onTouchEvent(event);

    }

    @Override
    protected void onDraw(Canvas canvas) {

        canvas.drawPath(myOutlinePath, myThinPaintBrush);
        if (isHighlighted) {
            canvas.drawPath(myOutlinePath, myThickPaintBrush);
        }
        super.onDraw(canvas);

    }

}

Advertisement

Answer

I would go for the middle ground: Use your XML layout as is and make adjustments programmatically to the guideline positions. The following code converts percentage guidelines to fixed position guidelines by computing a new layout height that is a multiple of 8% of the height of the initial layout.

All sizes are computed correctly except for the bottom squares that tend to be larger. This can be easily corrected based upon your actual requirements (more important to be at the bottom or a certain distance from the other squares, for instance.)

MainActivity.jav

public class MainActivity extends AppCompatActivity {

    CustomButton buttonTopLeft;
    CustomButton buttonTopRight;

    CustomButton buttonMiddle;
    CustomButton buttonMiddleTopLeft;
    CustomButton getButtonMiddleTopRight;

    CustomButton buttonBottomLeft;
    CustomButton buttonBottomRight;

    CustomButton[] arrayOfCustomButtons;

    ConstraintLayout rootView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        buttonTopLeft = findViewById(R.id.buttonTopLeft);
        buttonTopRight = findViewById(R.id.buttonTopRight);
        buttonBottomLeft = findViewById(R.id.buttonBottomLeft);
        buttonBottomRight = findViewById(R.id.buttonBottomRight);
        buttonMiddle = findViewById(R.id.buttonMiddle);
        buttonMiddleTopLeft = findViewById(R.id.buttonMiddleTopLeft);
        getButtonMiddleTopRight = findViewById(R.id.buttonMiddleTopRight);

        rootView = findViewById(R.id.rootView);

        rootView.post(new Runnable() {
            @Override
            public void run() {
                int rootViewHeight = rootView.getMeasuredHeight();
                Log.i("XXX", "height of rootView is: " + rootViewHeight);
                int segHeight = (int) (rootViewHeight * 0.08f);
                adjustGuideline(R.id.guidelineHorizontalTop, segHeight);
                adjustGuideline(R.id.guidelineHorizontalCenter1, segHeight);
                adjustGuideline(R.id.guidelineHorizontalCenter2, segHeight);
                adjustGuideline(R.id.guidelineHorizontalCenter3, segHeight);
                adjustGuideline(R.id.guidelineHorizontalCenter4, segHeight);
                adjustGuideline(R.id.guidelineHorizontalBottom, segHeight);

                arrayOfCustomButtons = new CustomButton[]{buttonTopLeft, buttonTopRight, buttonBottomLeft,
                    buttonBottomRight, buttonMiddle, buttonMiddleTopLeft, getButtonMiddleTopRight};
                rootView = findViewById(R.id.rootView);
                for (final CustomButton cb : arrayOfCustomButtons) {
                    cb.setClickable(true);
                    cb.post(new Runnable() {
                        @Override
                        public void run() {
                            Log.i("MainActivity", "<<<< width of: " + cb.getTag() + " is: "
                                + cb.getMeasuredWidth());
                        }
                    });
                }
            }
        });
    }

    private void adjustGuideline(int guideLineId, int segHeight) {
        Guideline gl = (Guideline) findViewById(guideLineId);
        ConstraintLayout.LayoutParams lp = ((ConstraintLayout.LayoutParams) gl.getLayoutParams());
        gl.setGuidelineBegin((int) (segHeight * lp.guidePercent / 0.08f));
        gl.setGuidelinePercent(-1f);
    }
}
User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement