How We Made a Super-Smooth Content Creation Experience on Android
At Quizlet, we strive to make study tools with great, intuitive user experiences, even if that means dealing with the unique issues that building complex Android UIs produce. While building the feature that allows users to create sets of content on the Quizlet Android app, the ideal UI we designed forced us to work through a number of complex issues we’d never seen before. Most notably, we dealt with touches/gestures, keyboard issues, cursor issues, and cell recycling issues.
From the moment we launched the Quizlet Android app, users wanted to be able to create their own study sets just like they could on the web or iOS app. Since all of Quizlet’s content is user generated, this feature was obviously a top priority after launch.
Initial Designs
Based on our initial brainstorming, user feedback, and design ideas, we wanted a design that could show users multiple terms at once, allow for easy input (autocorrect, Gesture Typing, speech-to-text, etc.), and allow users to move quickly between different fields (e.g. single tap to enter a cell instead of a double tap, or repeatedly hitting a next button). This is the design as it was first mocked up:
Making a UI with two EditTexts in a row and multiple rows in a ListView sounds simple, right? We thought so too, but once we dove into the implementation details, we discovered that actually doing this was much more complex than we first thought. This was due to issues with touches and gestures, different keyboards, and cursor issues which manifested themselves differently in the many versions of Android running on devices made by many different manufacturers.
Touches and Gestures
There were numerous touches and gestures that we wanted to respond to. We wanted to make gestures for deleting a term row, undoing a delete, scrolling within a cell, scrolling within a ListView, and all the standard text editing functionality like cut, copy, and paste.
We decided that swipe to delete would be the most elegant and user friendly way to delete terms, so we started there. We began by making swipe-to-delete work using a 3rd party library, but we found ourselves trying to answer “What constitutes a swipe?” We realized that there were two parameters we needed to consider — what kind of touch we were seeing, and where the touch was occurring. If you pressed on the screen for a long time and then swiped, was it a long press or a swipe? If you were swiping from a middle of the word, was that different then swiping at the edge of a row? EditTexts were contained in a row which was contained in a ListView, so figuring out which element in the layout was supposed to respond to a touch was quite a challenge.
We decided that the swipe was interfering with other gestures we wanted to make, so we chose to abandon swipe-to-delete and instead allow you to delete by long-pressing on a row. The long-press would stop you from being able to edit, and put you in a mode where you could delete the row. So we built this functionality, only to realize that it conflicted with long press to paste, which we felt was necessary to preserve because it’s the default Android behavior.
By this point, we had spent a good amount of time making deleting functionality, and we had nothing to show for it. We decided to step back and break out the touches we were looking to deal with in a need-to-have and a nice-to-have list.
We need the ability to drag a cursor around in a cell, scroll within a cell, scroll within the list of terms, select text (for cut/copy), and long press to paste. The delete and undo gestures, whether a long press, or swipe, or something else, were nice-to-haves.
Given these constraints, it would be difficult to make swipe-to-delete work (because it was interfering with select text). It would also be tough to make long-press-to-delete work because that would interfere with paste. So we decided to make a separate interface where a remove button appeared at the bottom of the screen anytime the cursor was in a specific row, and clicking the button would remove the row.
We then looked a series of applications that allowed you to undo row deletions. Inspired by how gmail worked at the time (before Snackbars were released), we decided to make a PopupWindow for the undo interface. That way people could easily undo their deletion if they wanted to, and just as easily redo it again.
Keyboards
Once we finally had our UI functioning at a basic level, we started testing it more thoroughly. It took little effort to run into some big issues. We quickly realized that whenever the keyboard came up or down, any of the data that had been input to the EditTexts would be lost. We initially had the set the windowSoftInputMode property in the manifest to be adjustResize, but every time the keyboard appeared or disappeared, the view would redraw itself, thus redrawing each cell, causing our data to be lost.
We first decided to save the data coming in to the view in our local database, so if cells were redrawn, or the app were to crash, your data would not be lost. Then we decided to set the windowSoftInputMode to adjustPan instead of adjustResize so the view would never redraw itself. However, since the screen was not redrawn with adjustPan, when the keyboard went up, either the action bar, the bottom bar, or both, would not be seen. We needed the bottom bar (which contained the buttons for adding and removing rows) to be visible, and we needed the action bar (which contained the buttons to move to the next screen), to be visible.
We concluded that we should go back to using adjustResize and just save and restore all possible state (cursor position, text, scroll position, etc.). We overrode the onMeasure method such that every time the keyboard height caused the screen to redraw, we would restore all of the previous state. This was an expensive operation, but seemed to work quite well. We were all but set to claim victory against keyboards, when we decided to test some keyboards in non-English languages. Most tests worked fine, but the Japanese keyboard brought us back to square one. The standard Google Japanese keyboard brings up an extra suggestion bar based on the characters you are typing, which goes up and down with every new word being typed. By resetting the cursor on every character, the keyboard lost the context, so words that you wanted to type were now impossible to type. We tried a number of hacks and finally concluded that this was just not going to work with our current system.
At this point, we went back to the drawing board and asked ourselves why all of our views were empty. They were not getting destroyed and rebuilt, they were getting recycled. Thus we concluded that if we did not allow the EditTexts to change the content that they contained on a redraw of the screen, then we would not seen any of the problems that we were facing. Thus we added the following line of code in the getView method of the adapter behind the ListView that contains the EditTexts.
if (this.mTerm == term && this.mPosition == position) {
return; //Don’t unnecessarily repopulate the fields in the row
}
We say that if the term that the view was adapting on stayed the same, and the position of the row has not changed, then do not update the view’s content. Instead just return the view untouched. In that case, the content and cursor position will not change, thus resolving our issues.
Cursors
The last major issue we faced was related to cursors. Because we had multiple EditTexts in our ListView, we ran into all kinds of issues. We sometimes had multiple cursors at once. Sometimes our cursor was invisible. Sometimes it was a weird weight. And sometimes it would show up in other views because of cell recycling issues. Each manufacturer seemed to have its own problems, different from all problems we had seen before. Each hack we did to resolve one issue on one manufacturer’s devices would have no effect on another device. To resolve all of our issues, we overrode the OnFocusChangeListener so that whenever an EditText gained focus, we would call:
editText.setCursorVisible(false);
editText.setCursorVisible(true);
And if we lost focus, we would call
editText.setCursorVisible(false);
By manually overriding the OnFocusChangeListeners and forcing them to be visible or invisible, we were able to resolve our cursor issues on all Android devices.
Conclusion
This project was quite a challenge, but when it launched, we were confident that we were putting out a great feature that we could be proud of. With intuitive gestures and no strange manufacturer-dependent issues, we have had hundreds of thousands of sets created with this feature.
At Quizlet, we try to make high quality, intuitive, and delightful user experiences for all of our users across all platforms. We often face challenges like the ones described here, but we find it’s worth the time to get the experience right. If you’d like to see me speaking about these challenges in more detail, you can check out this video. If you love making awesome user experiences on Android, then send a note to jobs@quizlet.com. We’d love to chat with you.