Tutorial: fragments

Scott Moyer edited this page Nov 28, 2013 · 24 revisions
Clone this wiki locally

Our goal

Use fragments in a Ruboto activity.

Prerequisites

This tutorial has been tested with the following setups:

ruboto RubotoCore Device Android Android SDK Tester
0.15.0 0.5.6 Emulator 4.1.2 22.2.1 donv

Create your project

ruboto gen app --package org.ruboto.example.fragments -t android-11
cd fragments

Load the XML layout in the activity

Note: Referring to the fragment from the layout XML currently does not work. Feel free to add a fix :) To see how to do it programmatically, scroll down to the end of this tutorial.

Change src/fragments_activity.rb to this

require 'ruboto/package'
require 'titles_fragment'
require 'details_fragment'

class FragmentsActivity
  def onCreate(savedInstanceState)
    super
    setContentView($package.R.layout.fragment_layout)
  rescue Exception
    puts "Exception creating activity: #{$!}"
    puts $!.backtrace.join("\n")
  end
end

Add the main activity layout XMLs

Add a new file res/layout-land/fragment_layout.xml for the landscape layout. You will have to create the new directory res/layout-land.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent" android:layout_height="match_parent">

    <fragment class="RubyTitlesFragment"
            android:id="@+id/titles" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent" />

    <FrameLayout android:id="@+id/details" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent"
            android:background="?android:attr/detailsElementBackground" />

</LinearLayout>

Add a new file res/layout/fragment_layout.xml for layout for other configurations.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
    <fragment class="RubyTitlesFragment"
            android:id="@+id/titles"
            android:layout_width="match_parent" android:layout_height="match_parent" />
</FrameLayout>

Add the titles fragment class file

Add a new file src/titles_fragment.rb with the following content:

require 'ruboto/base'
require 'jruby/core_ext'
require 'shakespeare'

import android.widget.ArrayAdapter

class TitlesFragment < android.app.ListFragment
  def onActivityCreated(savedInstanceState)
    @curCheckPosition = 0
    super

    # Populate list with our static array of titles.
    setListAdapter(ArrayAdapter.new(activity,
        android.R.layout.simple_list_item_activated_1, Shakespeare::TITLES))

    # Check to see if we have a frame in which to embed the details
    # fragment directly in the containing UI.
    detailsFrame = activity.findViewById($package.R.id.details)
    @dualPane = detailsFrame != nil && detailsFrame.visibility == android.view.View::VISIBLE

    if savedInstanceState != nil
      # Restore last state for checked position.
      @curCheckPosition = savedInstanceState.getInt("curChoice", 0)
    end

    if @dualPane
      # In dual-pane mode, the list view highlights the selected item.
      getListView().setChoiceMode(android.widget.ListView::CHOICE_MODE_SINGLE)
      # Make sure our UI is in the correct state.
      showDetails(@curCheckPosition)
    end
  end

  def onSaveInstanceState(outState)
    super
    outState.putInt("curChoice", @curCheckPosition)
  end

  def onListItemClick(l, v, position, id)
    showDetails(position)
  end

  # Helper function to show the details of a selected item, either by
  # displaying a fragment in-place in the current UI, or starting a
  # whole new activity in which it is displayed.
  def showDetails(index)
    @curCheckPosition = index

    if @dualPane
      # We can display everything in-place with fragments, so update
      # the list to highlight the selected item and show the data.
      getListView().setItemChecked(index, true)

      # Check what fragment is currently shown, replace if needed.
      details = getFragmentManager().findFragmentById($package.R.id.details)
      if details == nil || details.getShownIndex() != index
        # Make new fragment to show this selection.
        details = DetailsFragment.newInstance(index)

        # Execute a transaction, replacing any existing fragment
        # with this one inside the frame.
        ft = fragment_manager.beginTransaction
        ft.replace($package.R.id.details, details)
        ft.setTransition(android.app.FragmentTransaction::TRANSIT_FRAGMENT_FADE)
        ft.commit
      end

    else
      # Otherwise we need to launch a new activity to display
      # the dialog fragment with selected text.
      intent = Intent.new
      intent.setClass(getActivity(), DetailsActivity.class)
      intent.putExtra("index", index)
      startActivity(intent)
    end
  end
end

TitlesFragment.become_java!

puts "$LOAD_PATH: #{$LOAD_PATH}"
puts __FILE__
puts "X TitlesFragment: #{TitlesFragment}"
puts "Java::ruby.TitlesFragment: #{Java::ruby.TitlesFragment}"
puts "Java::RubyTitlesFragment: #{Java::RubyTitlesFragment}"

Add the details fragment class file

Add a new file src/details_fragment.rb with the following content:

class DetailsFragment < android.app.Fragment
  # Create a new instance of DetailsFragment, initialized to
  # show the text at 'index'.
  def self.newInstance(index)
    f = DetailsFragment.new

    # Supply index input as an argument.
    args = android.os.Bundle.new
    args.putInt("index", index)
    f.setArguments(args)

    return f
  end

  def getShownIndex
    return getArguments().getInt("index", 0)
  end

  def onCreateView(inflater, container, savedInstanceState)
    if container == nil
      # We have different layouts, and in one of them this
      # fragment's containing frame doesn't exist.  The fragment
      # may still be created from its saved state, but there is
      # no reason to try to create its view hierarchy because it
      # won't be displayed.  Note this is not needed -- we could
      # just run the code below, where we would create and return
      # the view hierarchy; it would just never be used.
      return nil
    end

    scroller = android.widget.ScrollView.new(getActivity())
    text = android.widget.TextView.new(getActivity())
    padding = android.util.TypedValue.applyDimension(android.util.TypedValue::COMPLEX_UNIT_DIP,
                4, getActivity().getResources().getDisplayMetrics())
    text.setPadding(padding, padding, padding, padding)
    scroller.addView(text)
    text.setText(Shakespeare::DIALOGUE[getShownIndex()])
    return scroller
  end
end

Add the details activity

Add a new file src/details_activity.rb with the following content:

class DetailsActivity < android.app.Activity
  def onCreate(savedInstanceState)
    super

    if resources.configuration.orientation == Configuration::ORIENTATION_LANDSCAPE
      # If the screen is now in landscape mode, we can show the
      # dialog in-line with the list so we don't need this activity.
      finish
      return
    end

    if savedInstanceState == nil
      # During initial setup, plug in the details fragment.
      details = DetailsFragment.new
      details.setArguments(getIntent().getExtras())
      getFragmentManager().beginTransaction().add(android.R.id.content, details).commit()
    end
  end
end

Add the actual content

The example in the Android developer documentation refers to a Shakespear class. We have reconstructed it here. Feel free to add more to it.

class Shakespeare
  TITLES = [
      'Title 1',
      'Title 2',
      'Title 3',
      'Title 4',
      'Title 5',
      'Title 6',
      'Title 7',
      'Title 8',
  ]
  DIALOGUE = [
      'Dialogue 1',
      'Dialogue 2',
      'Dialogue 3',
      'Dialogue 4',
      'Dialogue 5',
      'Dialogue 6',
      'Dialogue 7',
      'Dialogue 8',
  ]
end

Try it!

rake install start

Hate XML?

The above example should work, but you may prefer building your UI programmatically instead of using XML. Builsing the UI using Ruby code enables a rapid change cycle by just reloading the Ruby code without having to building, installing, and restarting the app.

Change src/fragments_activity.rb to this

require 'ruboto/widget'
require 'titles_fragment'
require 'details_fragment'

ruboto_import_widgets :LinearLayout, :FrameLayout

class FragmentsActivity
  def onCreate(savedInstanceState)
    super
    self.content_view = linear_layout :orientation => :horizontal, :id => 42 do
      frame_layout :id=>$package.R.id.titles,
                   :layout => {:weight => 1,:width => 0, :height => :match_parent}

      frame_layout :id=>$package.R.id.details,
                   :layout => {:weight => 1,:width => 0, :height => :match_parent}

    end
    fragmentTransaction = fragment_manager.begin_transaction
    tf = TitlesFragment.new
    fragmentTransaction.add($package.R.id.titles, tf)
    fragmentTransaction.commit
  rescue Exception
    puts "Exception creating activity: #{$!}"
    puts $!.backtrace.join("\n")
  end
end

Try it again

rake update_scripts_restart