Android: PreferenceFragment with custom Preference lifecycle inconsistency? - android

I'm trying to create a custom Preference to be shown in PreferenceFragment as described here: Building a Custom Preference. My custom Preference should look and function as SwitchPreference, but have one additional TextView for error reporting.
I got everything implemented and UI looks fine, but I can't initialize this Preference when my PreferenceFragment is shown!
Documentation for Preference.onBindView() states that:
This is a good place to grab references to custom Views in the layout
and set properties on them.
So I did:
#Override
protected void onBindView(View view) {
super.onBindView(view);
txtError = (TextView) view.findViewById(R.id.error);
}
public void setError(String errorMessage) {
txtError.setText(errorMessage);
notifyChanged();
}
However, when I call CustomSwitchPreference.setError(String) in PreferenceFragment.onResume(), I get NPE because txtError is null.
I tried to find some workaround, but it looks like there is no lifecycle method in PreferenceFragment which is guaranteed to be called AFTER all the underlying Preferences had their Views initialized (I checked both Preference.onBindView(View) and Preference.onCreateView(ViewGroup)).
This behavior doesn't make any sense - there should be some way to initialize UIs of the underlying Preferences when PreferenceFragment is shown. How can I achieve this?
Note: calls to customPreference.setTitle(String) and customPreference.setSummary(String() in CustomPreferenceFragment.onResume() work fine. It is just the additional TextView which I can't grab a reference to...
CustomSwitchPreference.java:
public class CustomSwitchPreference extends SwitchPreference {
private TextView txtError;
public CustomSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public CustomSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomSwitchPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomSwitchPreference(Context context) {
super(context);
}
#Override
protected View onCreateView(ViewGroup parent) {
setLayoutResource(R.layout.custom_switch_preference_layout);
return super.onCreateView(parent);
}
#Override
protected void onBindView(View view) {
super.onBindView(view);
txtError = (TextView) view.findViewById(R.id.error);
}
public void setError(String errorMessage) {
txtError.setText(errorMessage);
notifyChanged();
}
}
CustomPreferenceFragment.java:
public class CustomPreferenceFragment extends PreferenceFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getPreferenceManager().setSharedPreferencesName(PREFERENCES_FILE_NAME);
addPreferencesFromResource(R.xml.application_settings);
}
#Override
public void onResume() {
super.onResume();
Preference preference = findPreference("CUSTOM_PREF");
if (preference == null ||
!CustomSwitchPreference.class.isAssignableFrom(preference.getClass()))
throw new RuntimeException("couldn't get a valid reference to custom preference");
CustomSwitchPreference customPreference = (CustomSwitchPreference) preference;
customPreference.setError("error");
}
}
custom_switch_preference_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="#android:id/widget_frame">
<TextView
android:id="#android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lines="1"/>
<TextView
android:id="#android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="3"/>
<TextView
android:id="#+id/error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="3"/>
</LinearLayout>
<FrameLayout
android:id="#android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"/>
</RelativeLayout>
application_settings.xml:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<com.example.settings.CustomSwitchPreference
android:key="CUSTOM_PREF"/>
</PreferenceScreen>

I couldn't find a proper solution for this issue - this inconsistency feels like a life-cycle bug in AOSP, but I'm not 100% sure about this.
As a workaround, I defined a callback interface that CustomSwitchPreference invokes in onBindView method in order to notify the containing PreferenceFragment that it had been initialized:
#Override
protected void onBindView(View view) {
super.onBindView(view);
txtError = (TextView) view.findViewById(R.id.error);
initializationListener.onInitialized(CustomSwitchPreference.this);
}
and all the manipulations on this CustomSwitchPreference that I wanted to perform in onResume now get performed in onInitialized callback. This is an ugly workaround that requires a considerable amount of boilerplate, but it seems to work.

Related

Custom component in Android Studio

I am trying to create a custom component that is made up of lots of other components. I need to be able to use it the same way I would a TextView or EditText type of component. I cannot seem to find any tutorials online on how to do this and I'm not really sure what to even look for. I have several that I need to make but here is an example of one of them:
input_textbox.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_margin="5dp"
android:background="#drawable/bg_form_info"
android:layout_width="wrap_content"
android:layout_height="#dimen/form_info_height">
<TextView
android:id="#+id/label"
android:hint="#string/label"
android:textSize="#dimen/label_text_size"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<EditText
android:id="#+id/display"
android:hint="#string/display"
android:textSize="#dimen/form_info_text_size"
android:layout_alignParentBottom="true"
android:background="#android:color/transparent"
android:paddingLeft="#dimen/input_horizontal_padding"
android:paddingRight="#dimen/input_horizontal_padding"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ImageButton
android:id="#+id/error_icon"
android:contentDescription="#string/error"
android:src="#drawable/ic_error"
android:background="#android:color/transparent"
android:clickable="true"
android:focusable="false"
android:visibility="visible"
android:paddingRight="#dimen/input_horizontal_padding"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
Textbox.java
public class Textbox extends [WHAT GOES HERE] {
TextView inputLabel;
EditText inputField;
ImageButton errorIcon;
String errorMessage;
public Textbox(Context context, String label) {
super(context);
init(label);
}
public Textbox(Context context, AttributeSet attrs) {
super(context, attrs);
init(""); // I don't know how to pass the label here
}
public Textbox(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(""); // or here
}
private void init(String label) {
inputLabel = findViewById(R.id.label); // How do I connect this class to the xml?
inputLabel.setText(label);
inputField = findViewById(R.id.input_field);
inputField.setHint(label);
inputField.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
inputLabel.setVisibility((count > 0) ? View.VISIBLE : View.GONE);
}
#Override
public void afterTextChanged(Editable s) { }
});
errorIcon = findViewById(R.id.error_icon);
errorIcon.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
showErrorMessage();
}
});
}
public String getErrorMessage() {
return errorMessage;
}
public void showErrorMessage() {
showToast(getContext(), errorMessage);
}
public String getValue() {
return inputField.getText().toString();
}
public void setValue(String value) {
inputField.setText(value);
}
public void showErrorIcon(String errorMessage) {
this.errorMessage = errorMessage;
showErrorMessage();
errorIcon.setVisibility(VISIBLE);
}
public void hideErrorIcon() {
this.errorMessage = "";
errorIcon.setVisibility(GONE);
}
}
activity_main.xml
...
<com.example.application.inputs.Textbox
android:id="#+id/test_textbox"
android:layout_below="#id/end_date_display"
[HOW DO I ADD THE LABEL AND OTHER OPTIONS?]
android:layout_width="match_parent"
android:layout_height="wrap_content" />
...
Any help would be great or even a link to a Youtube video is better then nothing.
You can init your custom Textbox on Activity class like
public TextBox mTextBox = new TextBox(label)
add add it to the parent view.
If you want to set the label of the view in the xml,
add the public method in your custom class like
public void setTag(String input) {
inputLabel.setText(input)
}
and set the tag from the code.
public textBoxReferred = findViewById(R.id.test_testbox)
textBoxReferred.setText("THE TAG")

Check if a custom progress DialogFragment is shown or not using Espresso?

I have a custom progress dialog with a progressbar and a message displayed during network calls.(For e.g. Logging in..., Fetching data etc., ).
I want to write a test to verify the dialogfragment with given text is displayed or not.
CustomProgressDialog.java
public class CustomProgressDialog extends DialogFragment {
private static final String KEY_MESSAGE = "message";
public static final String TAG_PROGRESS_DIALOG = "progress_dialog";
#BindView(R.id.progressbar) ProgressBar mProgressBar;
#BindView(R.id.progress_textview) TextView mProgressTextView;
private Unbinder mUnbinder;
public static CustomProgressDialog start(String progressMessage) {
CustomProgressDialog dialog = new CustomProgressDialog();
Bundle bundle = new Bundle();
bundle.putString(KEY_MESSAGE, progressMessage);
dialog.setArguments(bundle);
return dialog;
}
#Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
setCancelable(false);
}
#Override public Dialog onCreateDialog(Bundle savedInstanceState) {
LayoutInflater inflater = getActivity().getLayoutInflater();
Bundle bundle = getArguments();
String mProgressMessage;
if (bundle != null && bundle.containsKey(KEY_MESSAGE)) {
mProgressMessage = bundle.getString(KEY_MESSAGE);
} else {
mProgressMessage = getActivity().getString(R.string.progress_loading);
}
View view = inflater.inflate(R.layout.dialog_custom_progress, null);
mUnbinder = ButterKnife.bind(this, view);
mProgressTextView.setText(mProgressMessage);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(view);
return builder.create();
}
/**
* Helper method to show a progress dialog with a message.
*/
public static void showDialog(FragmentActivity activity, String message) {
showDialog(activity, message, TAG_PROGRESS_DIALOG);
}
/**
* Helper method to show a progress dialog with a message.
*/
public static void showDialog(FragmentActivity activity, String message, String tag) {
CustomProgressDialog progressDialog = CustomProgressDialog.start(message);
progressDialog.show(activity.getSupportFragmentManager(), tag);
}
public static void hideDialog(FragmentActivity activity) {
hideDialog(activity, TAG_PROGRESS_DIALOG);
}
/**
* Helper method to hide a progress dialog.
*/
public static void hideDialog(FragmentActivity activity, String tag) {
FragmentManager fragmentManager = activity.getSupportFragmentManager();
DialogFragment dialogFragment = (DialogFragment) fragmentManager.findFragmentByTag(tag);
if (dialogFragment != null) {
Timber.d("Gonna dismiss the dialog.");
dialogFragment.dismiss();
}
}
#Override public void onDestroyView() {
if (getDialog() != null && getRetainInstance()) {
getDialog().setDismissMessage(null);
mUnbinder.unbind();
}
super.onDestroyView();
}
}
dialog_custom_progress.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/root_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/white"
android:layout_marginTop="#dimen/spacing_small"
android:layout_marginBottom="#dimen/spacing_small"
android:padding="#dimen/spacing_xlarge"
>
<ProgressBar
android:id="#+id/progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:indeterminate="true"
android:indeterminateTint="#color/primary_dark"
android:indeterminateTintMode="src_in"
/>
<TextView
android:id="#+id/progress_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:gravity="left"
android:layout_marginLeft="#dimen/spacing_medium"
android:layout_toRightOf="#+id/progressbar"
android:maxLines="3"
tools:text="#string/placeholder_progress_text"
style="#style/ProgressTextStyle"
/>
</RelativeLayout>
LoginFragment.java
private void doLogin() {
//blah.. blah ..
CustomProgressDialog.showDialog(getActivity(),
getActivity().getString(R.string.progress_logging_in));
}
Sample testcase
#Test public void checkProgressBar_displayedWhileLoggingIn() throws Exception {
// GIVEN
......
// WHEN
onView(LOGIN_BUTTON).perform(click());
// THEN
// TODO check for progress bar is displayed.
//onView(withText(R.string.progress_logging_in)).check(matches(isDisplayed()));
onView(withId(R.id.progress_textview)).check(matches(isDisplayed()));
}
I'd got the following error for the above testcase:
android.support.test.espresso.NoMatchingViewException: No views in hierarchy found matching: with id: com.custom.android.internal.debug:id/progress_textview
Although progress_textview is displayed on the screen.
Espresso doesn't work well when indeterminate progressbar is enabled.
Refer:
1. Testing progress bar on Android with Espresso.
2. ProgressBars and Espresso
Based on the solutions given in the posts and by following the gist, I created two different versions of progressbar for each productFlavor like below:
productFlavor(Debug)/ProgressBar.java
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.animation.Animation;
/**
* Progressbar that is used in UI Tests
* Prevents the progressbar from ever showing and animating
* Thus allowing Espresso to continue with tests and Espresso won't be blocked
*/
public class ProgressBar extends android.widget.ProgressBar {
public ProgressBar(Context context) {
super(context);
setUpView();
}
public ProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
setUpView();
}
public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setUpView();
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setUpView();
}
private void setUpView() {
this.setVisibility(GONE);
}
#Override
public void setVisibility(int v) {
// Progressbar should never show
v = GONE;
super.setVisibility(v);
}
#Override
public void startAnimation(Animation animation) {
// Do nothing in test cases, to not block ui thread
}
}
productFlavor(Production)/ProgressBar.java
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
/**
* Progressbar that just calls the default implementation of Progressbar
* Should always be used instead of {#link android.widget.ProgressBar}
*/
public class ProgressBar extends android.widget.ProgressBar {
public ProgressBar(Context context) {
super(context);
}
public ProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
And modified the layout dialog_custom_progress.xml progressbar with:
<com.projectname.android.custom.ProgressBar
android:id="#+id/progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:indeterminate="true"
android:indeterminateTint="#color/primary_dark"
android:indeterminateTintMode="src_in"
/>
Finally test case runs successfully.

Cannot inject view to custom class with RoboGuice

I started to use RoboGuice within my project. I can easily inject views inside fragments and activites but i have some trouble with cusom views.
I got null ptr exception every time.
According to RoboGuice's example i did the same with my custom class:
TestActivity
#ContentView(R.layout.test_layout)
public class TestActivity extends RoboActivity {
#InjectView(R.id.testView_1) TestView testView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
TestView
public class TestView extends LinearLayout {
#InjectView(R.id.log_in_tab) View logInTab;
public TestView(Context context) {
super(context);
initView();
}
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
#Override
public void onFinishInflate() {
super.onFinishInflate();
if (logInTab == null)
Toast.makeText(getContext(), "Still NULL", Toast.LENGTH_LONG).show();
else
Toast.makeText(getContext(), "Ok", Toast.LENGTH_LONG).show();
}
public void initView() {
inflate(getContext(), R.layout.login_view, this);
RoboGuice.injectMembers(getContext(), this);
}
}
Login view's xml is in pastebin here.
Test layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<view
android:layout_width="wrap_content"
android:layout_height="wrap_content"
class="hu.illion.kwindoo.view.test.TestView"
android:id="#+id/testView_1"/>
</LinearLayout>
Toast always says that logInTab is null.
Please help if you can.
I don't know why there is no code examples of that but when i have to inject custom views i use injectViewMembers.
Hope this work for you:
public void initView() {
inflate(getContext(), R.layout.login_view, this);
RoboGuice.injectMembers(getContext(), this);
RoboGuice.getInjector(getContext()).injectViewMembers(this);
}
In addition to the previous answer, you should use the following method to actually start using the injected views:
#Override
protected void onFinishInflate() {
super.onFinishInflate();
someTextView.setText("Some text");
}

TextView - setCustomSelectionActionModeCallback how create ActionMode.Callback for when the text is selected to many TextView

I have several TextView in my systems and most of them I have to call my custom ActionMode.Callback.
The question is how do I create a TextView with custom ActionMode.Callback?
today my code is that way
mTxOne.setCustomSelectionActionModeCallback(new MarkTextSelectionActionModeCallback());
mTxTwo.setCustomSelectionActionModeCallback(new MarkTextSelectionActionModeCallback());
...
public class TextViewA extends TextView {
#Override
protected void onCreateContextMenu(ContextMenu menu) {
super.onCreateContextMenu(menu);
setCustomSelectionActionModeCallback(new MarkTextSelectionActionModeCallback());
}
public TextViewA(Context context) {
super(context);
}
public TextViewA(Context context,AttributeSet attrs) {
super(context,attrs);
}
public TextViewA(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
Here the xml
<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" >
<br.com.vrbsm.textviewexample.TextViewA
android:id="#+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:textSize="12dp"
/>
</RelativeLayout>
here main
public class MainActivity extends Activity{
//
private TextViewA textview;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
textview = (TextViewA)findViewById(R.id.textView2);
textview.setText("Android is crazy");
}
public class MarkTextSelectionActionModeCallback implements Callback {
.
.
.}
Try this:
public class CustomActionModeTextView extends TextView{
//...implement your constructors as you may want to use...//
#Override
public ActionMode.Callback getCustomSelectionActionModeCallback (){
return new MarkTextSelectionActionModeCallback();
}
}
Create your own TextView and override the method getCustomSelectionActioModeCallback to return always an instance of your custom action mode callback. This way you don't have to set it everytime in your views.
Remember
Use your custom class in your XML layout files;
When casting it, do the cast to your custom class and not TextView;
You may wanna consider keeping a reference to your custom callback in your class. You could initialize it during some constructor and return it in the get method. This would be just to avoid creating new instances on every method call.

Creating custom control with overriding onclick

I am working on an android app and I have a custom GUI component which extends a TextView.
I want to have my custom control do a task when clicked from my custom control class and my overridden onclick method.
For example my class that extends the TextView implements the OnClick listener and writes a log to the log cat.
Then in my activity, I set an onclick listener to my custom control, and this shows a toast notification.
What I want to happen, is when my custom control is clicked, my activities overridden onclick shows the toast and the custom control class on click method also is run to show the log. But I can only seem to get one working or the other, for example, if I don't run myCustom.setOnClickListener(myListener) then the classes onclick is used and does the log, if I set the onClick listener then I only get the toast not the log.
Below is my custom control class
public class NavTextView extends TextView implements View.OnClickListener
{
public NavTextView(Context context) {
super(context);
setOnClickListener(this);
}
public NavTextView(Context context, AttributeSet attrs) {
super(context, attrs);
setOnClickListener(this);
}
public NavTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOnClickListener(this);
}
public NavTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setOnClickListener(this);
}
#Override
public void onClick(View v) {
Log.d("NavTextView", "This has been clicked");
}
}
Below is my activities onCreate method
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
navTextView = (NavTextView)findViewById(R.id.navTextView);
navTextView.setOnClickListener(mClickListener);
}
Hope this makes sense
A View can only have one OnClickListener. In your NavTextView you are setting it there. If you later call setOnClickListener again, you are replacing the previous listener.
What you can do is override setOnClickListener in your custom View, then wrap the OnClickListener and call both.
public class MyTextView extends TextView implements View.OnClickListener
{
OnClickListener _wrappedOnClickListener;
public MyTextView(Context context) {
super(context);
super.setOnClickListener(this);
}
#Override
public void onClick(View view) {
Log.d("NavTextView", "This has been clicked");
if (_wrappedOnClickListener != null)
_wrappedOnClickListener.onClick(view);
}
#Override
public void setOnClickListener(OnClickListener l) {
_wrappedOnClickListener = l;
}
}

Categories

Resources