Building a UI

Scott Moyer edited this page Nov 25, 2013 · 4 revisions
Clone this wiki locally

You're going to need a UI for that new Activity you've created. The standard method for creating UIs on Android is through an XML file that gets wrapped into a resource in your application at compile time. You could use that same method to add UI to your Ruboto application, but then you'd lose the dynamism of Ruby. Generally you'll want to build your UI through your Ruby script. This is very doable, but making the Android API calls to build a UI gets old fast. ruboto/widget.rb provides a Ruby wrapper around all those Android API calls, providing a simple DSL for building your UI.

Let's start by looking at the old way of doing things (still works if you want to do it). Our demo UI will be a simple single line EditText with a "Search" Button to its right. Below we'll add a ListView that we imagine would be filled in by the search.

We'll start by importing in the necessary UI widgets along with ViewGroup to get some layout constants:

java_import "android.widget.EditText"
java_import "android.widget.Button"
java_import "android.widget.ListView"
java_import "android.widget.LinearLayout"
java_import "android.view.ViewGroup"

Then, at the time the UI elements are needed, we'll make all the Android API calls:

  def onCreate(bundle)
    super
    # The outer layout will fill the Activity screen
    outer_ll = LinearLayout.new(@ruboto_java_instance)
    outer_ll.orientation = LinearLayout::VERTICAL
    outer_ll.set_padding 5, 5, 5, 5

    # The inner layout will sit at the top of the outer layout
    inner_ll = LinearLayout.new(@ruboto_java_instance)
    outer_ll.add_view inner_ll 
    params = inner_ll.get_layout_params
    params.width = ViewGroup::LayoutParams::FILL_PARENT
    params.height = ViewGroup::LayoutParams::WRAP_CONTENT

    # The search EditText will sit to the left of the inner layout
    search_et = EditText.new(@ruboto_java_instance)
    search_et.single_line = true
    search_et.hint = "Enter search criteria"
    inner_ll.add_view search_et
    params = search_et.get_layout_params
    params.width = ViewGroup::LayoutParams::FILL_PARENT
    params.height = ViewGroup::LayoutParams::WRAP_CONTENT
    params.weight = 1.0

    # The search Button will sit to the right of the inner layout
    search_b = Button.new(@ruboto_java_instance)
    search_b.text = "Search"
    search_b.on_click_listener = proc{|view| search_et.text = "do_something"} 
    inner_ll.add_view search_b
    params = search_b.get_layout_params
    params.width = ViewGroup::LayoutParams::WRAP_CONTENT
    params.height = ViewGroup::LayoutParams::WRAP_CONTENT

    # The results ListView will sit below the inner layout
    results_lv = ListView.new(@ruboto_java_instance)
    # Set up the ListView contents
    outer_ll.add_view results_lv
    params = results_lv.get_layout_params
    params.width = ViewGroup::LayoutParams::FILL_PARENT
    params.height = ViewGroup::LayoutParams::FILL_PARENT
    params.weight = 1.0

    setContentView outer_ll
  end

You have to admit that the code above is hardly fun to write. There has to be a better way...the Ruboto way. We start by importing the widgets (ViewGroup comes in for free):

ruboto_import_widgets :EditText, :Button, :ListView, :LinearLayout

Within your on_create method you can call setContentView (or self.content_view =) to set your view hierarchy. Build that hierarchy as follows:

linear_layout(:orientation => :vertical, :padding => [5,5,5,5]) do
  linear_layout(:layout => {:width => :fill_parent, :height => :wrap_content}) do
    edit_text(:single_line => true, :hint => "Enter search criteria", 
                :layout => {:width => :fill_parent, :height => :wrap_content, :weight => 1.0})
    button(:text => "Search",
                :on_click_listener => RubotoOnClickListener.new.handle_click{|view| do_something},
                :layout => {:width => :wrap_content, :height => :wrap_content})
  end
  # Still need to set up the contents of the ListView, but that will have to be covered elsewhere
  list_view(:layout => {:width => :fill_parent, :height => :fill_parent, :weight => 1.0})
end

Better! Here's what's happening behind the scenes:

  • ruboto_import_widgets imports the widget and sets up a method on activities to build the UI element
  • Each widget then takes a hash of parameters. The keys gets converted into a method call (either with "=" or a direct method call) on the new widget.
  • You can either specify a single value (e.g., :orientation => :vertical) or an array for methods that take multiple parameters (e.g., :padding => [5, 5, 5, 5])
  • The :layout key of the hash holds special properties to be passed to the LayoutParams (after addView). Note: Up to 0.16.0, you needed to add an "=" for directly setting values (e.g., :width= => :fill_parent). As of 0.16.1, that restriction has been removed (e.g., :width => :fill_parent) works.
  • Layouts can take advantage of a block to add widgets created in the block as child views
  • The method or block returns the new widget so it can be sent to setContentView, used to addView, and/or captured in a variable.

Remember, you can always do it the old way if needed. In older versions of Ruboto you needed to do this to add a default style to a widget constructor:

Button.new(@ruboto_java_instance, nil, R::attr::buttonStyleSmall)

Now (with Ruboto version 0.8.0) you can use the :default_style key:

button :default_style => R::attr::buttonStyleSmall

Ruboto version 0.8.0 also introduces two other new keys: :parent and :parent_index. Use these to force the parent and/or index within the parent's views. This comes in handy if you have stack problems due to view hierarchy depth:

linear_layout do
  button
end

# could also be done as
ll = linear_layout
button :parent => ll

The convenience methods (e.g., edit_text) are added to the activity, so you can use them inside a method:

def create_button(text)
  button :text => text, ## other default settings ##
end

create_button "Hello"
create_button "World"

If you add that method to another object, you'll need to send along the context:

class Fred
  def create_button(context, text)
    context.button :text => text, ## other default settings ##
  end
end

f = Fred.new
f.create_button context, "Hello"
f.create_button context, "World"

You may find a time when you want to use a widget from outside of the "android.widget" package. If so, you can import it too:

ruboto_import_widget :MyWidget, "org.package.name"

Sometime you'll want to make additional calls to UI elements or keep a reference to them for later use. Just make sure you always pass the root layout to setContentView:

ll = linear_layout(:orientation => :vertical) do
  @button1 = button(:text => "Fred")
end
registerForContextMenu(@button1)

setContentView(ll)
# or: self.content_view = ll

Here's a more complete script sample:

require "ruboto/widget"

ruboto_import_widgets :EditText, :TextView, :Button, :LinearLayout

class RubotoDemoActivity
  def on_create(bundle)
    super
    # Create a proc to act as the buttons's click listener
    handle_click = proc do |view|
      @tv.append "\n#{view.getText}"
      @et.text = view.getText
    end
    setContentView(
      linear_layout(:orientation => :vertical) do
        @et = edit_text
        linear_layout do
          button :text => "Hello, World", :on_click_listener => handle_click
          button :text => "Hello, Ruboto", :on_click_listener => handle_click
        end
        @tv = text_view :text => "Click buttons or menu items:"
      end
    )
  end
end