OHMS BLOG

Wednesday, November 02, 2011

code

Adventures in Android, Part IV: Vertical SeekBars

One user interface element that I wanted to include in my activities was a vertically-oriented SeekBar. While Android has provided View.setRotation() since API level 11, since I am targeting API level 7 I do not have that functionality available to me. I knew that this could probably be achievable by manually applying a transformation matrix to a View, but I had no idea how difficult it would end up being. I attempted numerous different approaches in an effort to achieve my desired results. Much to my frustration, I kept finding limitations in the APIs that made every option either unfeasible or difficult.

  1. My first attempt was to try something subclassing SeekBar similarly to this StackOverflow post. It overrides View.onDraw() and applies a transformation to the Canvas. It also sends fake size dimensions up to the SeekBar superclass and provides a custom View.onTouchEvent() handler. My concerns were as follows:
    • A custom event handler must call SeekBar.setProgress() to update the SeekBar's state. Since this is being done programmatically, any listeners will be told that this change did not come from the user, even though indirectly it actually did.
    • This custom event handler did not (and cannot) propagate touch events up to the SeekBar in a way that it will be able to redraw itself during the touch event. In particular, the SeekBar thumb was not being highlighted while the view was being touched.

  2. Figure 1: SeekBar with thumb drawn at incorrect location
    My second attempt was an extension to the first. Instead of completely overriding onTouchEvent(), I decided to use onTouchEvent() to perturb the touch coordinates of the provided MotionEvent, then call up into the superclass. While this fixed the issues from the first option, it still didn't look right.
    • Notice that in Figure 1, the SeekBar's progress indicator is at 100%, yet the thumb is located near the bottom. It turns out that the SeekBar's drawing code calls getWidth() to figure out where to position the thumb. Since the SeekBar is now in a vertical orientation, it should be using the height instead of the width.
  3. Finally we reach the third, definitive option: I wrote a derivative of ViewGroup called RotatedLayout that does a perfect transformation of the child View. From the child's perspective, it is operating using its regular orientation. The RotatedLayout class transforms coordinates for drawing, measuring, layout, invalidation, touch events and key events between its parent and its child. This allows me to provide my users with a pixel-perfect vertical SeekBar!
    • It was annoying to deal with invalidation; Android goes to great lengths to prevent you from tinkering with it. I had no choice, however: any invalidation rectangles generated by the SeekBar need to be transformed from the SeekBar's coordinate system to the parent view's coordinates. Figure 2 illustrates what happens if invalidation isn't transformed: Only a small region of pixels at the top of the SeekBar are redrawn. This happens because that small region happens to be exactly the same height as the underlying horizontal SeekBar. Figure 3 overlays a horizontal SeekBar with the misdrawn vertical SeekBar to illustrate.

Figure 2: Vertical SeekBar with no invalidation transformation

Figure 3: Invalidated region from Figure 2 overlaid with horizontal SeekBar

Figure 4: Vertical SeekBars in Audio Auto-adjust.
Are you interested in what RotatedLayout can do for your app? Drop me a line: ohmsblog at teamohms dot org

Release 7.0; Copyright © 1996-2012 Aaron Klotz. All Rights Reserved.