Search code examples
androidandroid-layoutandroid-fragmentsfragmentmanager

FragmentManager needing extra requestLayout


I built a view that contains a EditText as a submit button, therefore it's not focusable in touch mode, the text is centered and it has a background color. Then, I created a fragment with this button.

When this fragment is the first one to be placed in the container to be drawn, the text is centered in the edit text as expected. However, if use an click event to replace this fragment with another fragment that contains it's own submit button, the text is not centered, let alone aligned to the left or to the right. It's not aligned at all.

After put a Hander to add a delay for requesting the layout after 5ms where I create view to be placed in the fragment, the text get centered properly. I realize that for some reason the fragment transaction is needing an extra call for requestLayout.

I isolated the code that is causing the problem. By doing this I realized that the problem is somehow related with the way I handle typefaces in my TestTextField.java. I pasted right bellow.

What could be possibly causing that? Why? If it is something wrong with my code, why it works to the first fragment I placed in the screen, but it does not work with the other ones?

TestActivity.java

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import java.lang.reflect.Method;

/**
 * Created by eduardoj on 2017-07-19.
 */

public class TestActivity extends Activity
        implements TestFragmentA.Listener, TestFragmentB.Listener {

    private FrameLayout fragmentContainer;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ViewGroup.LayoutParams matchParent = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
        );

        fragmentContainer = new FrameLayout(this);
        fragmentContainer.setId(View.generateViewId());
        fragmentContainer.setLayoutParams(matchParent);
        fragmentContainer.setFocusableInTouchMode(true);
        setContentView(fragmentContainer);

        FragmentManager manager = getFragmentManager();
        FragmentTransaction transaction = manager.beginTransaction();
        transaction.add(fragmentContainer.getId(), TestFragmentA.newInstance());
        transaction.commit();
    }

    @Override
    public void onASubmitButtonClick() {
        FragmentTransaction transaction = getFragmentManager().beginTransaction();
        transaction.add(fragmentContainer.getId(), TestFragmentB.newInstance());
        transaction.commit();
    }

    @Override
    public void onBSubmitButtonClick() {

    }
}

TestFragmentA.java

import android.app.Fragment;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

/**
 * Created by eduardoj on 2017-07-19.
 */

public class TestFragmentA extends Fragment {

    public interface Listener {
        void onASubmitButtonClick();
    }

    private Listener listener;

    public static TestFragmentA newInstance() {
        Bundle args = new Bundle();
        TestFragmentA fragment = new TestFragmentA();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        /*
         * This makes sure that the container context has implemented the
         * callback interface. If not, it throws an exception.
         */
        try {
            listener = (Listener) context;
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString()
                    + " must implement TestFragmentA.Listener");
        }
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {

        ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
        );

        FrameLayout layout = new FrameLayout(getActivity());
        layout.setId(View.generateViewId());
        layout.setLayoutParams(params);
        layout.setBackgroundColor(Color.CYAN);

        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
                600,
                FrameLayout.LayoutParams.WRAP_CONTENT,
                Gravity.CENTER
        );

        TestTextField view = new TestTextField(getActivity());
        view.setId(View.generateViewId());
        view.setLayoutParams(lp);
        view.setText("Submit A");
        view.setBackgroundColor(Color.MAGENTA);
        view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
        view.setEnableTextEditing(false);
        view.addListener(new TestTextField.Listener() {
            @Override
            protected void onClick(TestTextField textField, String text) {
                super.onClick(textField, text);
                listener.onASubmitButtonClick();
            }
        });

        layout.addView(view);

        return layout;
    }
}

TestFragmentB.java

import android.app.Fragment;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

/**
 * Created by eduardoj on 2017-07-19.
 */

public class TestFragmentB extends Fragment {

    public interface Listener {
        void onBSubmitButtonClick();
    }

    private Listener listener;

    public static TestFragmentB newInstance() {
        Bundle args = new Bundle();
        TestFragmentB fragment = new TestFragmentB();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        /*
         * This makes sure that the container context has implemented the
         * callback interface. If not, it throws an exception.
         */
        try {
            listener = (Listener) context;
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString()
                    + " must implement TestFragmentA.Listener");
        }
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {

        ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
        );

        FrameLayout layout = new FrameLayout(getActivity());
        layout.setId(View.generateViewId());
        layout.setLayoutParams(params);
        layout.setBackgroundColor(Color.LTGRAY);

        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
                600,
                FrameLayout.LayoutParams.WRAP_CONTENT,
                Gravity.CENTER
        );

        TestTextField view = new TestTextField(getActivity());
        view.setId(View.generateViewId());
        view.setLayoutParams(lp);
        view.setText("Submit B");
        view.setBackgroundColor(Color.MAGENTA);
        view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
        view.setEnableTextEditing(false);
        view.addListener(new TestTextField.Listener() {
            @Override
            protected void onClick(TestTextField textField, String text) {
                super.onClick(textField, text);
                listener.onBSubmitButtonClick();
            }
        });

        layout.addView(view);

        return layout;
    }
}

TestTextField.java

import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Handler;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;

import java.util.ArrayList;
import java.util.List;

/*
 * Created by eduardoj on 2017-07-13.
 */
public class TestTextField extends LinearLayout {

    public static class Listener {
        protected void onClick(TestTextField textField, String text) {}
    }

    private int BACKGROUND_COLOR = Color.MAGENTA;
    private String HINT = "Enter text here";
    private boolean isToUpdateMinHeight;

    private EditText editText;
    private Typeface textTypeface;
    private Typeface hintTypeface;

    private List<Listener> listeners;

    public TestTextField(Context context) {
        super(context);
        listeners = new ArrayList<>();

        /* Initializing text field */
        initView();
        bindListeners();
    }

    public void addListener(Listener listener) {
        if (listener != null) {
            listeners.add(listener);
        }
    }

    public void setEnableTextEditing(boolean enable) {
        editText.setFocusableInTouchMode(enable);
    }

    public void setHintTypeface(Typeface typeface) {
        hintTypeface = typeface;
        isToUpdateMinHeight = true;
        updateTypeface();
    }

    public void setText(CharSequence text) {
        editText.setText(text);
        updateTypeface();
    }

    @Override
    public void setTextAlignment(int textAlignment) {
        editText.setTextAlignment(textAlignment);
    }

    public void setTypeface(Typeface typeface) {
        isToUpdateMinHeight = true;
        textTypeface = typeface;
        updateTypeface();
    }

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

    private void initView() {
        setOrientation(HORIZONTAL);

        LayoutParams params = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1);
        editText = new EditText(getContext());
        editText.setId(generateViewId());
        editText.setLayoutParams(params);
        editText.setHint(HINT);
        editText.setBackgroundColor(Color.TRANSPARENT); // Removes underline
        addView(editText);
        updateTypeface();

        setBackgroundColor(BACKGROUND_COLOR);
        // Custom Typefaces: you have to set the custom typeset to reproduce the problem.
//        setTypeface(G.Font.getVegurRegular(getContext()));
//        setHintTypeface(G.Font.getVegurLight(getContext()));


        /* This is the current work around for this Issue */
        (new Handler()).postDelayed(new Runnable() {
            @Override
            public void run() {
                requestLayout();
            }
        }, 15);
    }

    private void bindListeners() {
        final TestTextField textFieldInstance = this;

        editText.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                for (Listener listener : listeners) {
                    listener.onClick(
                            textFieldInstance,
                            editText.getText().toString()
                    );
                }
            }
        });
    }

    /**
     * Updates the minimum height to the size of the considering the biggest
     * typeface.
     */
    private void updateMinHeight() {
        if (!isToUpdateMinHeight) {
            return;
        }

        Typeface original = editText.getTypeface();

        editText.setTypeface(textTypeface);
        editText.measure(0, 0);
        int textHeight = editText.getMeasuredHeight();

        editText.setTypeface(hintTypeface);
        editText.measure(0, 0);
        int hintHeight = editText.getMeasuredHeight();

        int minHeight = textHeight > hintHeight ? textHeight : hintHeight;

        editText.setMinimumHeight(minHeight);
        editText.setTypeface(original);
        isToUpdateMinHeight = false;
    }

    private void updateTypeface() {
        boolean hasText = editText.length() > 0;
        Typeface typeface = hasText ? textTypeface : hintTypeface;
        editText.setTypeface(typeface);
    }
}

Solution

  • I came to a solution that doesn't require to call an extra requestLayout, I believe this is the right way to solve this problem.

    updateMinHeight and super.onMeasure are in the wrong order in the onMeasure method. The correct way is:

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

    For me, this order above makes sense since updateMinHeight can actually change the measured size of the widget. Though this answer shows how to solve, I can't understand why the problem happens and how would this affect only the text position in very specific cases, such as to the first fragment displayed.