Quantcast
Viewing latest article 12
Browse Latest Browse All 13

Dynamically-inflated UI and menus

  I finally managed to get up to speed working on the project. From the beginning I started redesigning the existing neko.ui system which to this time worked in compile time. I was quite proud of it, because it was a fast way to do UI generation compared even to native XML approach, since all transformations had happened before the application even launched.

  The method had a downside though - you had to have all UI-defining trees declared in compile-time. This means that you could not just generate a Clojure data structure and call `make-ui` on it. Apparently the lack of this feature tied hands in situations where UI element style heavily depended upon the state of the application.

  The UI system being macro-based was also harder to reason about. You couldn't have functions as attributes - you needed to quote them so they don't get evaluated too soon. The traits, which are handlers for specific attributes of the widget, didn't just do their stuff - they had to generate code for doing it and add it to other code. Then there was this inconsistency in compile order - if you wanted to extend the UI system (add new elements, traits etc.) you somehow had to make sure the extension code gets compiled before the code that actually uses these extensions.

While being absolutely easier to understand, dynamic inflation is much harder to pull off. In the old system, keywords from attributes were converted to method calls by a macro, and the compiler could work out the exact method afterwards. In the case of dynamic generation, you have either to use reflection or invent something different. My immediate idea was to generate XML from Clojure data structures, and then just offload all the work to Android's native utilities. As it turned out, you can't just make a random XML and feed it to inflater - the XML file has to be first processed by *aapt* and what not. Reusing the facilities that operate on data after it's parsed was also not an option - they proved to be exceptionally coupled and reliant on the exact classes, with no possibility to get wedged into.

neko.ui

   So, the reflection path then. At first, I was a bit worried about the performance of this solution because of the notoriety of reflection in Clojure world. But I did some benchmarks, and manual reflection-based system showed some decent performance, and I have some further plans to cache reflected methods. Overall, the syntax of `make-ui` didn't change after the rewrite, with some minor deviations. Now you don't have to quote your functions and objects, but you do have to syntax-quote symbols for `:def` attribute.

   But the main change is that now you can manipulate your UI tree as much as you like before calling `make-ui`. It looks like this.

(defn make-button [name visibility]
[:button {:text name
:text-size [20 :sp]
:padding [5 :px]
:visible visibility
:on-click (fn [_] (toast (str "My name's " name)))}])

(def data {:foo true, :bar false, :baz false,
:qux true, :quux true})

...

(let [layout [:linear-layout {:def `top-layout
:orientation :vertical}]
buttons (for [[name visibility] data]
(make-button name visibility))]
(make-ui (concat layout button)))

The example above pretty much shows it all. You can have UI generation as flexible as in, say, Hiccup. You can postpone the creation of actual UI objects until the very moment you need them, and before that just pass around vectors and maps.

Note how text size and padding are specified. I like it this way better than strings which you then have to parse.

Menus

   With the introduction of action bar most Android applications started using it to host access to some secondary (and sometimes even primary) functionality. This is done in a form of menu that is right-aligned, usually contain some buttons, a search field and an overflow button (under which elements are hidden when there isn't enough room for them).

Image may be NSFW.
Clik here to view.
Menu on the action bar
   In conventional Android the menus are described in XML as well. They get a separate folder for the configuration files and a special inflater. The ability to fully describe menu in XML is somewhat limited though, and most of the time you have to pass some configuration action to Java code. For instance, a menu item can contain an arbitrary View, and if you want to indicate this in XML, you have to do it in layouts/ folder, so your item definition gets split. Then, there is no convenient way to set a handler, so Android people rather use centralized handlers which are not the greatest design decision as well.

   With new `make-menu` you can create menus as easily as general user interface. The implementation relies on existing neko.ui infrastructure, so it was actually pretty easy to write. The tree you pass to it resembles XML for the most part, with a few pleasant exceptions.

   First, you can specify handlers in the menu tree, like in `make-ui`. Second, you can specify custom action views by providing UI tree to the `:action-view` attribute of the item. An example to follow.

(defn MainActivity-onCreateOptionsMenu [this menu]
(.superOnCreateOptionsMenu this menu)
(make-menu
menu [[:item {:title "First"
:icon R$drawable/ic_launcher
:show-as-action [:if-room :with-text]
:on-click (fn [_] (toast "Menu item clicked"))}]
[:item {:show-as-action [:always :collapse-action-view]
:icon android.R$drawable/ic_menu_search
:action-view [:search-view {:iconified-by-default true
:on-query-text-submit
(fn [q item]
(toast (str "Searched for: " q))
(.collapseActionView item)
)}]}]
[:menu {:title "Submenu"
:icon android.R$drawable/ic_menu_more
:show-as-action :always}
[:item {:title "Second"}]]
[:group {:id :not-important}
[:item {:title "Third"
:icon android.R$drawable/ic_menu_camera
:show-as-action :if-room}]
[:item {:title "Four"
:show-as-action :never}]]])
true)

   The result of this you can see on a screenshot. Notice how we defined a search menu item in the same tree, without splitting our attention.

Action modes

Image may be NSFW.
Clik here to view.
Contextual menu
   Action modes (also known as contextual menus) are a method of presenting additional actions when the user selects something in the application (action modes can be used for virtually any purposes, but most of the time they deal with selected stuff). Before 4.0 the usual approach was to display a conventional dialog-like menu when the user selected something by long-clicking; this approach falls short when user wants to select a couple of items, and is generally distracting.

   Android's own implementation of action modes had one bothersome property - you can't indicate if the action mode is active unless you manually keep track of it. I decided to fight it by having a map of activities to action modes, and automatically populating and depopulating this map with current action mode. Apart from that, the implementation of contextual menus was straightforward and reliant on `make-menu`. Here is an example:
(neko.ui.menu/start-action-mode activity
:on-create
(fn [mode menu]
(make-menu menu
[[:item {:icon android.R$drawable/ic_menu_share
:on-click (fn [_]
(toast (str "Selected items: "
(string/join (get-selected adapter)))))}]
[:item {:icon android.R$drawable/ic_menu_revert}]
[:item {:icon android.R$drawable/ic_menu_edit}]]))
:on-destroy (fn [mode]
(clear-selected adapter)))

   Screenshot #2 shows how it looks like.

   That's it so far. For now I will continue porting the most useful Android features to Clojure. Stay tuned!

Viewing latest article 12
Browse Latest Browse All 13

Trending Articles