11 October 2014

AutoCompleteTextView Hell

Today, I ran into a weird "feature" of Android. I was working on an AutoCompleteTextView with the dropdown list having section dividers. It all works well in portrait mode, but gets all messed up in landscape.

I made a sample app to illustrate the point of this blog [Github repo]. Clone it, run it, rotate the phone, select an item from the suggested auto-complete results, and get your mind blown. Or your heart stabbed. Or your stomach sucker-punched. Your choice.

So what was happening? Here's what.

When we tell the app to perform a filter, we construct an array of the resulting matches. The example is pretty straightforward, just look for countries that start with whatever the user has typed in.

// Filter by start of string
String country = mCountries.get(i);
if(country.toLowerCase().startsWith(constraint.toString().toLowerCase())) {
    mFilteredData.add(country);
}
You can make this filtering as complicated as you like or need, just make sure to pass in whatever the user needs to see.

To illustrate having section headers (aka disabled items), we insert dummy text every fifth place in the list. The sample app does not care if there are more results after a section header, we still insert anyway.

So. Let's filter. Typing in "pa" will give us this set of data:
10-11 02:45:46.028  16282-20011/com.blogspot.droidista.autocompletetextviewhell D/AutoCompleteFragment﹕ 
Filtered results: [Section!, Pakistan, Palestine, Panama, Papua New Guinea, Section!, Paraguay]

There are two sections and five countries. Remember this.
Position in listValue
0Section!
1Pakistan
2Palestine
3Panama
4Papua New Guinea
5Section!
6Paraguay

This screenshot shows how the results are rendered in portrait mode. So far, so good. Selecting an item from the list populates the EditText with the country's name.

Now let's try rotating our phone. This is where things get juicy. First off, there are no section headers. Second, the results are not in alphabetical order anymore. If we debug all over the adapter, we see what is written on the screenshot: Item on the left is position = 1, item in the middle is position = 0, and item on the right is position = 2.

Remember the result set we have? "Pakistan" is definitely NOT in position = 0, it should have been a section header! If we go ahead and select "Pakistan" in the suggestions above the keyboard, the EditText will populate with the item in position = 0 of the result set, i.e. "Section!". Definitely not good.


The AutoCompleteTextView widget has been around since API level 1, but I haven't messed around with it as much as I did today. Googling returns very, very sparse results on this topic.
  • Does this mean people do not have the same problem as I did?
  • No one uses section headers in AutoCompleteTextViews?
  • No one uses this screen mode (EditText in full screen) in landscape without setting IME flags?
  • Is there a secret trick to making this work out-of-the-box?
Or the most plausible of all,
  • Am I being stupid?

17 June 2014

Swipe, not Pull, to Refresh

I have recently came across this new View in the support library package that allows your app to have built-in support for pull swipe to refresh. This is pretty cool, since we don't have to use any of the libraries out there. Admittedly, very little customization can be done, but then what else can we customize, right?

Anyway, here's a short demo of using this nifty little view.
Initial list
While refreshing
After refreshing

The app is a simple ListView that shows a list of countries. Swiping down on the list will simulate a long-running activity (like connecting to a server, for example) and afterwards updating the list with new data.

Adding support for swipe to refresh is pretty straightforward. You would have to wrap the swipe-able layout in a SwipeRefreshLayout. Here is my XML file:

<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:ignore="MergeRootFrame" >

   <ListView
          android:id="@android:id/list"
          android:layout_width="match_parent"
          android:layout_height="match_parent" />

</android.support.v4.widget.SwipeRefreshLayout>

In your Fragment, you would then have to put an onRefreshListener on this SwipeRefreshLayout. In my Fragment's onCreateView, I have this:

// Configure the swipe refresh layout
mSwipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.container);
mSwipeRefreshLayout.setOnRefreshListener(this);
mSwipeRefreshLayout.setColorScheme(
   R.color.swipe_color_1, R.color.swipe_color_2,
   R.color.swipe_color_3, R.color.swipe_color_4);

The colors are defined in a colors.xml file in my res/values folder. To show or hide the refresh animation, you would have to call setRefreshing(boolean). This means that if you have to kick off an AsyncTask when the user swipes down, call setRefreshing(true) in onPreExecute, and call setRefreshing(false) in onPostExecute.

The implementation of onRefresh in the demo app is pretty simple, it simply grabs the next bunch of countries from a pre-defined list.

@Override
public void onRefresh() {
    // Start showing the refresh animation
    mSwipeRefreshLayout.setRefreshing(true);
   
    // Simulate a long running activity
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            updateCountries();
        }
    }, 5000);
}

That's it really.  Nice and short. The code for this demo is in Github.

08 June 2014

Quick Tip: Understanding Alternate Resources

Trying to support as many devices as possible the best way possible is a very daunting task indeed. You will usually need to provide a lot of different layouts, strings, or dimensions (among others) to make your app look great whatever the user's device is. And then you start chaining resource qualifiers and testing which resource is being loaded by the OS can become a nightmare very quickly.

Here's a trick I started using which seemed to work quite well. Create a string (the app name works well if you have an Action Bar) that you can display on-screen (or just throw into a Log or a Toast) that will quickly let you know from which of those very many /res folders your resources are being pulled out of.

For this demo, I have the following /res/values-xxxxx folders:

As you can see, there are strings.xml files in each of the configurations I want to support. Each of these files will contain whatever description we want to see on the device or in the Logs. In my case, I have custom strings for each form factor and orientation, which allows me to validate things easier. But this is particularly useful if the changes per form factor and orientation is subtle, such as dimensions.

Here's what it looks like for phones:


And here are the outputs for tablets:

Each of the strings.xml files contains just these two strings (sample taken from /res/values-sw800dp):


    MultiDeviceSupport - Tablet Portrait
    Hello world on a tablet!


Have fun debugging!

05 June 2014

Adding attributes to a custom view

There are times when using the default Android Views just doesn't cut it and you need to create your own version of a View. So how exactly do you do that? It's as simple as subclassing the View! But what if you want to add customizable attributes? Here's how.

Let's say I am creating a form-filling application and I want some of the EditTexts in the form to be required. However, I am so tired of having to implement the error checking for each and every one of those fields. What I will do is create my own EditText that will do the validation for me if that particular field is required. Let's do it.

Step 1: Create your custom view and create fields for the attributes you want. In this case, I want an EditText that will show the default EditText error display if the user has not put in any value.
public class RequiredEditText extends EditText {
   private boolean mRequired;
   private String mErrorMessage;

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

   /**
   * Set this EditText's requirement validation. The error message
   * will be set to null by default if not provided.
   * 
   * @param required
   * @param errorMessage (optional)
   */
   public void setRequired(boolean required, String errorMessage) {
      this.mRequired = required;
      this.mErrorMessage = errorMessage;
  
      invalidate();
      requestLayout();
   }
 
   public void setRequired(boolean required) {
      setRequired(required, null);
   }

   /**
   * Lets you know if this field is set as required or not
   * @return
   */
   public boolean isRequiredField() {
   return mRequired;
   }
}
Step 2: If a field is required, we want the default error message to appear (with our own error message, of course). If the user fills in the EditText, we want the error to disappear.
public class RequiredEditText extends EditText {
   private boolean mRequired;
   private String mErrorMessage;

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

   /**
   * Set this EditText's requirement validation. The error message
   * will be set to null by default if not provided.
   * 
   * @param required
   * @param errorMessage (optional)
   */
   public void setRequired(boolean required, String errorMessage) {
      this.mRequired = required;
      this.mErrorMessage = errorMessage;
  
      manageRequiredField(required);
  
      invalidate();
      requestLayout();
   }
 
   public void setRequired(boolean required) {
      setRequired(required, null);
   }

   private void manageRequiredField(boolean required) {
      // If we are required, set the listeners
      if(required) {
         setOnFocusChangeListener(mFocusChangeListener);
         addTextChangedListener(mTextWatcher);
      } else {
         // In case there is an error message already, clear it
         setError(null);
   
         // Remove the listeners
         setOnFocusChangeListener(null);
         removeTextChangedListener(mTextWatcher);
      }
   }

   /**
   * Lets you know if this field is set as required or not
   * @return
   */
   public boolean isRequiredField() {
      return mRequired;
   }

   OnFocusChangeListener mFocusChangeListener = new OnFocusChangeListener() {

      @Override
      public void onFocusChange(View v, boolean hasFocus) {
         // If the focus was removed from the field and it IS required,
         // check if the user has put in something
         if(!hasFocus && mRequired){
            isRequiredFieldFilled();
         }
      }
   };
 
   TextWatcher mTextWatcher = new TextWatcher() {

      @Override
      public void onTextChanged(CharSequence s, int start, int before, int count) {
         // Once the user types in something, remove the error
         setError(null);
      }

      @Override
      public void beforeTextChanged(CharSequence s, int start, int count, int after) { /* do nothing */ }

      @Override
      public void afterTextChanged(Editable s) { /* do nothing */ }   
   };

   private boolean isRequiredFieldFilled() {
      // If the EditText is empty, show the error message
      if(TextUtils.isEmpty(getText().toString().trim())){
         showRequiredErrorDrawable();
         return false;
      }
      return true;
   }

   private void showRequiredErrorDrawable() {
      setError(mErrorMessage);
   }
}
Step 3: Now it's time to add the fields to the Layout Editor. Create an attrs.xml file in /res if there isn't one already. Declare a styleable and include the attributes you want to appear in the Layout Editor.

   
       
       
   

The name of the styleable does not need to match your custom view class name, but doing so makes it more readable and maintainable.

Step 4: Go back to your custom view implementation and add constructors that take in an AttributeSet.
public RequiredEditText(Context context, AttributeSet attrs, int defStyle) {
   super(context, attrs, defStyle);
   init(attrs);
}

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

private void init(AttributeSet attrs) { 
   TypedArray a=getContext().obtainStyledAttributes(
      attrs,
      R.styleable.RequiredEditText);

   try {
      setRequired(a.getBoolean(R.styleable.RequiredEditText_required, false), 
            a.getString(R.styleable.RequiredEditText_errorMessage));
   } finally {
      //Don't forget this, we need to recycle
      a.recycle();
   }
}
Step 5: Go to the Layout Editor and look for "Custom & Library Views" in the Palette (you may have to click on "Refresh" several times before your custom view appears in the list). Add the custom view to your layout and check out the properties panel!
 

 As always, the code is in GitHub.

17 May 2014

Quick Tip: git Auto-complete

When I started using git, it peeved me that there is no auto-complete. More so when you have to manually do a git add manually.

Thank heavens I found this little gem. Making auto-complete work for git:

In terminal:
curl https://raw.github.com/git/git/master/contrib/completion/git-completion.bash -o ~/.git-completion.bash


And then you'd need to "activate" it in your .bash_profile:
if [ -f ~/.git-completion.bash ]; then
  . ~/.git-completion.bash
fi

13 May 2014

Quick Tip: Updating the location update frequency

When using Google Play's Location Services and you want to change the frequency of the updates, make sure to do these in order:

stopLocationUpdates();
// This method should implement mLocationClient.removeLocationUpdates()

// Set the new frequency in your location client
updateLocationClient(frequency);

startLocationUpdates();
// This method should implement mLocationClient.requestLocationUpdates()
// which means it should check for isConnected() as well!

If you do not remove the updates before updating the frequency, it looks like the old frequency update is still active BUT a new one is started.

09 May 2014

Setting up the SeekBar

So we want to use the SeekBar. We want the minimum value to be 10 and the maximum value to be 100, and it should increment by 10.

Thing is, SeekBar by default always starts at 0, and the increment is always an int. It is definitely possible to get what we want, but we need to do some simple math first.

Compute how many increments you will need from your minimum up to your maximum:
numberOfIncrements = maximum - minimum = 90

Then divide it by the amount of each increment we want:
seekBarMaximum = numberOfIncrements / 10 = 9

This means we should set up the SeekBar to have max = 9 and increment = 1. Then in our code, we have to figure out how to get the actual progress that we want.
SeekBar.OnSeekBarChangeListener mSeekbarListener = new OnSeekBarChangeListener() {
			
	@Override
	public void onStopTrackingTouch(SeekBar seekBar) { /* Do nothing*/ }
			
	@Override
	public void onStartTrackingTouch(SeekBar seekBar) { /* Do nothing*/ }
			
	@Override
	public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
		mProgressDisplay.setText("Seekbar is at: " + getProgressToDisplay(progress));
	}
};


private String getProgressToDisplay(int progress) {
        // We are multiplying by 10 since it is our actual increment
	int actualProgress = (progress + 1) * 10;
	return String.valueOf(actualProgress);
}

Another example:
minimum = 1, maximum = 10, increment = 0.5
numberOfIncrements = 9
seekBarMaximum = 18

In this case, the contents of getProgressToDisplay() will change since the increment is not a whole number.
private String getProgressToDisplay(int progress) {
	float actualProgress = (progress + 1) - (progress * 0.5f);
	return String.valueOf(actualProgress);
}