Why is my android custom view not square?
Asked Answered
B

6

14

I created a custom view to display a gameboard for a game I'm developing. The gameboard needs to be a square at all times. So with and height should be the same. I followed this tutorial to achieve this.

I added the following code to my custom view:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int width = getMeasuredWidth();
    int height = getMeasuredHeight();
    int widthWithoutPadding = width - getPaddingLeft() - getPaddingRight();
    int heigthWithoutPadding = height - getPaddingTop() - getPaddingBottom();

    int maxWidth = (int) (heigthWithoutPadding * RATIO);
    int maxHeight = (int) (widthWithoutPadding / RATIO);

    if (widthWithoutPadding > maxWidth) {
        width = maxWidth + getPaddingLeft() + getPaddingRight();
    } else {
        height = maxHeight + getPaddingTop() + getPaddingBottom();
    }

    setMeasuredDimension(width, height);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    Paint p = new Paint();
    p.setStyle(Paint.Style.STROKE);
    p.setStrokeWidth(3);
    canvas.drawRect(0, 0, getMeasuredWidth() - 1, getMeasuredHeight() - 1, p);
}

The layout with my custom view looks like this:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <view
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        class="com.peerkesoftware.nanograms.controls.GameBoard"
        android:id="@+id/view"
        android:background="@android:color/holo_purple"/>

</RelativeLayout>

The square that is drawn in the onDraw method is always a square. That works fine.

In the layout I add the custom view and give the view a background color. When I display it on a device in portrait mode everything works fine and the background color is filling up the square I draw in the onDraw method.

When I switch the device to landscape mode the square is still a square, but the area of the background color is bigger then that. It's has the same height, but has more width. But I have no idea why. The getMeasuredWidth() and getMeasuredHeight() methods return the correct values. How can it be that the view is still bigger then that?

Bove answered 21/6, 2013 at 6:11 Comment(2)
How did you define the custom view in your layout?Weak
With how much space it's the width bigger in landscape? Probably not the case, but you're using different layouts for portrait and landscape?Mastermind
V
7

For some reason your view doesn't work in RelativeLayout. For landscape orientation measurement ends up with following constraints (on my phone):

width = MeasureSpec: EXACTLY 404; height = MeasureSpec: AT_MOST 388

Because of this actual width ends up different than measured width (on my phone measured=388, actual=404). That's why you see background extending beyond your rect. You can check it yourself by adding some logging to onDraw and onMeasure.

Also, respecting measure mode doesn't help, measurement still ends with same result as above.

The solution is to use a different layout. LinearLayout and FrameLayout worked for me. Also it's a good idea to use actual view size in onDraw, with getWidth() and getHeight().

If you really need the RelativeLayout, you can add a child FrameLayout to hold your view like this:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <view
            android:id="@+id/view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            class="com.peerkesoftware.nanograms.controls.GameBoard"
            android:background="@android:color/holo_purple" />
    </FrameLayout>
</RelativeLayout>
Vogele answered 4/7, 2013 at 8:18 Comment(2)
Your solution works, but only if I set the width and height of the view to match_parent. The only problem I still have is that it's very difficult to position other views next to my custom view. With a RelativeLayout that would be easy.Bove
Never mind my last statement. I can position it very easy by replacing the FrameLayout with a LinearLayout.Bove
P
3

Subclass ImageView and override onMeasure and pass to

super.onMeasure(widthMeasureSpec, widthMeasureSpec);

the whole class:

public class SquaredImageView extends ImageView {

        public SquaredImageView(Context context) {
                super(context);
        }

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

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

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
                super.onMeasure(widthMeasureSpec, widthMeasureSpec);
        }

}

Edit: I tried with a normal view

public class CustomView extends View {

    public CustomView(Context context) {
        super(context);
        setBackgroundColor(Color.RED);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width = getMeasuredWidth();
    int heigth = getMeasuredHeight();

    int dimen = (width > heigth) ? heigth : width;

    setMeasuredDimension(dimen, dimen);
    }
}

and a TestActivity

public class TestActivity extends Activity {

    protected void onCreate(android.os.Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new CustomView(this));
    }

}

it draws a squared red rectangle on my mobile's screen

Pensive answered 27/6, 2013 at 7:21 Comment(3)
I don't see what the point is in extending ImageView instead of a normal View. I tried it for my own class, but the problem remains the same. The height en width are not the same (not a square) for the view in landscape mode.Bove
Mine was only an example. What class did you extend?Pensive
super.onMeasure(widthMeasureSpec, widthMeasureSpec) did the trick for me!Peso
V
0

This is my code, I'm using this code and it's working fine. Is not exacly to what you are looking for but may give you an idea. Based on my requirement height of image view should be as same length as width.

public class SquareImageView extends ImageView {

    private static final String TAG = "SquareImageView";

    public SquareImageView(Context context) {
        super(context);
    }

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

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

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //int h = this.getMeasuredHeight();
        int w = this.getMeasuredWidth();

        setMeasuredDimension(w, w);
    }
}

Defined in xml as:

<com.something.widget.SquareImageView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:id="@+id/ivProfilePicture"
                android:layout_alignParentLeft="true"
                android:layout_centerVertical="true"
                android:layout_alignParentTop="true"
                android:adjustViewBounds="true"
                android:scaleType="fitXY"
                android:background="@drawable/bg_curve_white_2"
                android:contentDescription="@string/general_content_description"
                android:src="@drawable/ic_default_logo"
                android:layout_marginTop="@dimen/side_margin"/>

And finally in Fragment it's something like:

SquareImageView ivProfilePicture = (SquareImageView) view.findViewById(R.id.ivProfilePicture);
Violence answered 21/6, 2013 at 8:47 Comment(1)
I haven't tested this yet, but I think this would have the same problem in landscape mode.Bove
A
0

I'm not sure what solution will serve the need, but code from the tutorial has fundamental design problem - it does not interpret MeasureSpec parameters of onMeasure() correctly. Answer in onMeasure custom view explanation explains correct interprestation of these parameters.

What happens in this example, onMeasure() is invoked twice for each occurrence of drawing for the custom view.

For the first call, both MeasureSpecs specify mode AT_MOST and give initial values without taking margins and padding into account. So, code from tutorial legitimately makes width and height to match.

For the second call, however, containing RelativeLayout takes margins and padding into account, and passes MeasureSpec for Width with EXACT mode, while Height is passed in AT_MOST mode.

For vertical screen, this works OK because Width got reduced by margins, and value for height is still height of the screen (also reduced by margins). So, during second call code legitimately reduces height to match now-smaller-width, and everything works properly.

For the horizontal screen orientation, however, "Width" is set to the value defined during first call, while Height is reduced by margins. When code tries to set Width to match Height, this setting operation does not work properly because Width mode is EXACT. As a result, properties of the view Width and MeasuredWidth end up with different values.

There are couple of simple ways to make code in question work: you can simply remove vertical margins:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/zero"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/zero"      
    tools:context=".MainActivity" >

Or, if margins are important, you can change containing layout to absolute:

<AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

In both cases it seems to make sense to change implementation of onMeasure() to respect EXACT mode (to log a problem at least).

Apothecary answered 1/7, 2013 at 17:44 Comment(2)
I suggest NOT to use AbsoluteLayout. It is deprecated since Android API level 3!Ruction
@LJoosse True, AbsoluteLayout is deprecated. But the question was "why width fails to match height in landscape orientation" and I was trying to illustrate the root cause of the problem. Without knowing actual requirements for screen layout it is hard to suggest specific solution.Apothecary
F
0

Your onMeasure() code is not straightforward. Change your code as follows & Set match_parent as width & height in xml (Actually it wont matter)

void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    int widthWithoutPadding = width - getPaddingLeft() - getPaddingRight();
    int heigthWithoutPadding = height - getPaddingTop() - getPaddingBottom();

    int maxWidth = (int) (heigthWithoutPadding * RATIO);
    int maxHeight = (int) (widthWithoutPadding / RATIO);

    if (widthWithoutPadding > maxWidth) {
        width = maxWidth + getPaddingLeft() + getPaddingRight();
    } else {
        height = maxHeight + getPaddingTop() + getPaddingBottom();
    }

    setMeasuredDimension(width, height);
}
Frazzle answered 3/7, 2013 at 7:26 Comment(0)
C
0

This simple code (similar to Blackbelt code with corrections) work for me in LinearLayout and FrameLayout:

protected int minNotZero(int valueA, int valueB)
{
    if (valueA != 0)
        if (valueB != 0)
            return Math.min(valueA, valueB);
        else
            return valueA;
    else
        return valueB;
}



@Override
protected void onMeasure(int measureSpecW, int measureSpecH)
{
    super.onMeasure(measureSpecW, measureSpecH);

    int width  = this.getMeasuredWidth();
    int heigth = this.getMeasuredHeight();

    int dimen = this.minNotZero(width, heigth);

    this.setMeasuredDimension(dimen, dimen);
}
Cuffs answered 15/8, 2020 at 9:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.