RubotoでActionBar上にPopupMenuを作る

RubotoのTutorials and examplesには「Tutorial: Add a menu」「Tutorial: Add a context menu」とあります。
ただ、ActionBar上のPopup Menuに関するチュートリアルがなかったため、試してみました。

■環境

Platform JDK ant Ruby ruboto jruby-jars Device API level
Windows7 x64 1.7.0_25 1.9.1 RubyInstaller 1.9.3-p448 0.13.0 1.7.4 Nexus7 2012 android-17

■注意

RubotoのIssue443にある通り、「onCreateOptionsMenu」が初回に起動しないことの影響で、ActionBar上のメニューが初回起動時には表示されません (二回目以降はきちんと起動します)。
onCreateOptionsMenu (and other callbacks) not called on first time startup · Issue #443 · ruboto/ruboto · GitHub



■アプリの動き

  1. ActionBarの「Action」メニューをタップ
  2. Popupメニューが表示されるので、メニューをタップ
    • toastの場合、toastが表示される
    • intentの場合、Chromeが起動して、RubotoのWebページが表示される

■アプリの生成

アプリにJRubyを含めて生成します。

ruboto gen app --package com.example.popupmenu --target android-17 --with-jruby


Windowsなので、メモリサイズを変更します。

  • popupmenu\rakelib\ruboto.rake の52行目あたり
# MINIMUM_DX_HEAP_SIZE = 1600
MINIMUM_DX_HEAP_SIZE = 768
  • \androidsdk\build-tools\17.0.0\dx.bat の53行目あたり
rem defaultXmx=-Xmx1600M
set defaultXmx=-Xmx768M


アプリをインストールして、起動します。

cd popupmenu
rake install start


なお、エミュレータを使う場合は、別のコマンドプロンプトを起動し、以下のコマンドでエミュレータを起動しておきます。

cd popupmenu
ruboto emulator -t android-17


エミュレータでインストールがうまくいかない場合は、再インストールやスクリプトのインストールにします。

rake reinstall start
rake update_scripts:restart

■レイアウト用xmlの作成

今回はレイアウトをRubyのコードではなく、xmlにて指定するようにしました。
xmlによる方法は、RubotoのSpinnerのサンプルを参考にしました。
Example: spinner · ruboto/ruboto Wiki · GitHub



メイン画面のレイアウト

  • popupmenu\res\layout\main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:id="@+id/Popup"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Popup Sample" 
        />
</LinearLayout>


ActionBarに表示するメニューのレイアウト

  • popupmenu\res\layout\action_bar_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/action_bar"
        android:title="action"
        android:showAsAction="ifRoom|withText"
        />
</menu>


ActionBarのメニューをタッチした時に表示されるPopupMenuのレイアウト

  • popupmenu\res\layout\popup_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/toast"
        android:title="toast"
    	/>
    <item
        android:id="@+id/intent"
        android:title="intent"
    	/>
</menu>

■Activityの修正

popupmenu\src\popupmenu_activity.rb を修正していきます。


java_importの記述

Javaのクラスを利用するために必要なものを java_import します。
「import」と「java_import」のどちらを使えばよいかで悩みましたが、以下の記事を見て「java_import」を使用することにしました。


なお、Android Debug Monitorにログを残したかったことから、android.util.Log も java_import しています。



on_xxx系の実装名について

JavaのonCreateOptionsMenuをRubyで書くとしたら、

  • スネークケース: on_create_options_menu
  • キャメルケース: onCreateOptionsMenu

どちらが良いのか分からなかったため、Rubotoのソースを読んだところ、どちらでも良さそうでした。
今回はRubyでよく目にする、スネークケースを使うことにしました。


参考 - Rubotoのソース:popupmenu\src\org\ruboto\RubotoActivity.java の256行目あたりから

public boolean onCreateOptionsMenu(android.view.Menu menu) {
    if (ScriptLoader.isCalledFromJRuby()) return super.onCreateOptionsMenu(menu);
    if (!JRubyAdapter.isInitialized()) {
      Log.i("Method called before JRuby runtime was initialized: RubotoActivity#onCreateOptionsMenu");
      return super.onCreateOptionsMenu(menu);
    }
    String rubyClassName = scriptInfo.getRubyClassName();
    if (rubyClassName == null) return super.onCreateOptionsMenu(menu);
    if ((Boolean)JRubyAdapter.runScriptlet(rubyClassName + ".instance_methods(false).any?{|m| m.to_sym == :on_create_options_menu}")) {
      return (Boolean) JRubyAdapter.runRubyMethod(Boolean.class, scriptInfo.getRubyInstance(), "on_create_options_menu", menu);
    } else {
      if ((Boolean)JRubyAdapter.runScriptlet(rubyClassName + ".instance_methods(false).any?{|m| m.to_sym == :onCreateOptionsMenu}")) {
        return (Boolean) JRubyAdapter.runRubyMethod(Boolean.class, scriptInfo.getRubyInstance(), "onCreateOptionsMenu", menu);
      } else {
        return super.onCreateOptionsMenu(menu);
      }
    }
  }
on_xxx系の実装について

以下の3つを実装しました。

  • on_create
  • on_create_options_menu: ActionBarへのメニュー表示
  • on_options_item_selected: ActionBarのメニューをタップした時の動作と、PopupMenu用のイベントリスナー(OnMenuItemClickListener)を登録
MenuItemClickListenerクラスの作成

OnMenuItemClickListener のイベントリスナーとして登録したクラスを作成します。
なお、今回の作り方では、toast/intentに関する処理はこのクラスに実装しても動作しないため、Activityクラスにメソッド(tap_popup_menu)を実装しています。



popupmenu_activity.rbのソース
require 'ruboto/widget'
require 'ruboto/activity'
require 'ruboto/util/toast'

# intent start用
java_import "android.content.Intent"
java_import "android.net.Uri"

# popup用
java_import 'android.widget.PopupMenu'

# Log用
java_import 'android.util.Log'


class PopupmenuActivity
  def on_create(bundle)
    super

    Log.v('POPUPMENU', 'on_create called')

    self.setContentView(Ruboto::R::layout::main)
  end


  def on_create_options_menu(menu)
    Log.v('POPUPMENU', 'on_create_options_menu called')

    self.getMenuInflater().inflate(Ruboto::R::layout::action_bar_menu, menu)

    true
  end


  def on_options_item_selected(item)
    Log.v('POPUPMENU', 'onOptionsItemSelected called')

    view = findViewById(item.getItemId())
    popup = PopupMenu.new(self, view)
    popup.getMenuInflater().inflate(Ruboto::R::layout::popup_menu, popup.getMenu())

    popup.setOnMenuItemClickListener(MenuItemClickListener.new(self))

    popup.show()

    true
  end


  def tap_popup_menu(itemId)
    case itemId
    when Ruboto::R::id::toast
      toast 'select toast'

    when Ruboto::R::id::intent
      intent = Intent.new(Intent::ACTION_VIEW)
      intent.setData(Uri.parse("http://ruboto.org/"))
      startActivity(intent)

    else
      toast 'Popup Selected' + itemId.to_s
    end
  end
end


class MenuItemClickListener
  def initialize(activity)
    @activity = activity
  end

  def onMenuItemClick(item)
    @activity.tap_popup_menu(item.getItemId())
  end
end

■インストールと起動

あとは、アプリを再インストールして開始します。

rake reinstall start


初回起動時はActionBarのメニューが表示されないため、一度終了し、再度起動します。



スクリーンショット

初回起動時

二回目起動時

ActionBarのメニューをタッチした時

PopupMenuからtoastを選択した時

PopupMenuからintentを選択した時



■ソース

上記のxmlとrbファイルをGistに上げました。
RubotoでActionBar上にPopupMenuを作った時のサンプルコード