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);
}
}
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.