Resizing rotated child view in custom ViewGroup subclass

1.5k Views Asked by At

Background

I have a custom ViewGroup subclass that rotates and mirrors its child view. The purpose for this is to correctly display traditional Mongolian text.

I could put anything is this ViewGroup, but for my current project I am putting an EditText in it. (I was never successful in just rotating and mirroring the EditText directly. However, wrapping it in this custom view group does work.)

Problem

My problem is that when I try to resize the ViewGroup programmatically, its child view is not getting resized properly along with it. I would like the EditText to match the size of the parent ViewGroup so that it appears to be a single view.

MCVE

I made a new project to show the problem. The button increases the width of the ViewGroup (shown in red). The images show the project start (with everything working fine) and two width increments. The EditText is white and is not getting resized even though the width and height are set to match_parent

enter image description here

The full project code is below.

MongolViewGroup.java (Custom ViewGroup that rotates and mirrors its content)

public class MongolViewGroup extends ViewGroup {

    private int angle = 90;
    private final Matrix rotateMatrix = new Matrix();
    private final Rect viewRectRotated = new Rect();
    private final RectF tempRectF1 = new RectF();
    private final RectF tempRectF2 = new RectF();
    private final float[] viewTouchPoint = new float[2];
    private final float[] childTouchPoint = new float[2];
    private boolean angleChanged = true;

    public MongolViewGroup(Context context) {
        this(context, null);
    }

    public MongolViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        setWillNotDraw(false);
    }

    public View getView() {
        return getChildAt(0);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final View view = getView();
        if (view != null) {
            measureChild(view, heightMeasureSpec, widthMeasureSpec);
            setMeasuredDimension(resolveSize(view.getMeasuredHeight(), widthMeasureSpec),
                    resolveSize(view.getMeasuredWidth(), heightMeasureSpec));
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (angleChanged) {
            final RectF layoutRect = tempRectF1;
            final RectF layoutRectRotated = tempRectF2;
            layoutRect.set(0, 0, right - left, bottom - top);
            rotateMatrix.setRotate(angle, layoutRect.centerX(), layoutRect.centerY());
            rotateMatrix.postScale(-1, 1);
            rotateMatrix.mapRect(layoutRectRotated, layoutRect);
            layoutRectRotated.round(viewRectRotated);
            angleChanged = false;
        }
        final View view = getView();
        if (view != null) {
            view.layout(viewRectRotated.left, viewRectRotated.top, viewRectRotated.right,
                    viewRectRotated.bottom);
        }
    }


    @Override
    protected void dispatchDraw(Canvas canvas) {

        canvas.save();
        canvas.rotate(-angle, getWidth() / 2f, getHeight() / 2f);
        canvas.scale(-1, 1);
        super.dispatchDraw(canvas);
        canvas.restore();
    }

    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        invalidate();
        return super.invalidateChildInParent(location, dirty);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        viewTouchPoint[0] = event.getX();
        viewTouchPoint[1] = event.getY();
        rotateMatrix.mapPoints(childTouchPoint, viewTouchPoint);
        event.setLocation(childTouchPoint[0], childTouchPoint[1]);
        boolean result = super.dispatchTouchEvent(event);
        event.setLocation(viewTouchPoint[0], viewTouchPoint[1]);
        return result;
    }

}

MainActivity.java

public class MainActivity extends AppCompatActivity {

    MongolViewGroup viewGroup;
    EditText editText;
    int newWidth = 300;

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

        viewGroup = (MongolViewGroup) findViewById(R.id.viewGroup);
        editText = (EditText) findViewById(R.id.editText);
    }

    public void buttonClicked(View view) {

        newWidth += 200;
        ViewGroup.LayoutParams params = viewGroup.getLayoutParams();
        params.width=newWidth;
        viewGroup.setLayoutParams(params);
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    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="com.example.mongolviewgrouptest.MainActivity">

    <com.example.mongolviewgrouptest.MongolViewGroup
        android:id="@+id/viewGroup"
        android:layout_width="100dp"
        android:layout_height="200dp"
        android:background="@color/colorAccent">

        <EditText
            android:id="@+id/editText"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:textColor="@android:color/black"
            android:background="@android:color/white"/>

    </com.example.mongolviewgrouptest.MongolViewGroup>

    <Button
        android:text="Button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button"
        android:onClick="buttonClicked"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"/>


</RelativeLayout>
1

There are 1 best solutions below

1
On BEST ANSWER
  • You're not recalculating viewRectRotated for your EditText when the ViewGroup's onLayout(...) method is called again.
  • Since angleChanged is set to false (and never changes) after your ViewGroups first layout, then the part that calculates the left, right, top and bottom values for your EditText is skipped any time after the first time when your ViewGroup requestsLayout (when you change its height or width).
  • As such, your EditText is still laid out with the same left,right,top and bottom values it was initially laid out with.

Do away with the angleChanged and it should work just fine. Like so:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    final RectF layoutRect = tempRectF1;
    final RectF layoutRectRotated = tempRectF2;
    layoutRect.set(0, 0, right - left, bottom - top);
    rotateMatrix.setRotate(angle, layoutRect.centerX(), layoutRect.centerY());
    rotateMatrix.postScale(-1, 1);
    rotateMatrix.mapRect(layoutRectRotated, layoutRect);
    layoutRectRotated.round(viewRectRotated);
    final View view = getView();
    if (view != null) {
        view.layout(viewRectRotated.left, viewRectRotated.top, viewRectRotated.right,
                viewRectRotated.bottom);
    }
}

I've tested this and it works just fine this way.

If you need angleChanged for any reason, then just make sure it's changed back to true inside your ViewGroup's onMeasure method so that viewRectRotated is recalculated again. However I wouldn't recommend that.