くろニャァ ~ UIの基本

前置き

ClojureでAndroidアプリを開発するシリーズ。今回の主題はUIです。盛り沢山なので、個々のUI部品の詳細までは踏み込みません。

UIを定義する

UIツリー

UIツリーは、UI階層の各UI要素をベクタで表現したものです。例えば、以下の場面で使います。

  • Activity#setContentView()に指定するviewオブジェクトを作る
  • Adapter#getView()で返すviewオブジェクトを作る
  • ActionBarやダイアログで使うfragmentオブジェクトを作る

1つのUI要素を表現するベクタは以下のように構成されます。

ELEMENT = [TYPE ATTRIBUTE ELEMENT*]
   TYPE:      UI要素の種類を示すキーワード(:buttonとか:linear-layoutとか)
   ATTRIBUTE: UI要素の属性を示すマップ
   ELEMENT*:  UI要素の子供たち(もしあるなら)

例えば、LinearLayoutの中にTextViewとButtonが1つずつあるなら、以下のようになります。

[:linear-layout {:orientation vertical :layout-height :fill}
 [:text-view {:text "Hello"}]
 [:button {:text "Push me"}]]

UIツリーは、ベクタとマップなどからなる標準のClojureデータ構造なので、リテラルで書き下さずに、動的に(つまりconjやconcatなどを使って)作っても構いません。

UI要素の種類

UIツリーのTYPEとして使えるキーワードは、以下の通りです。

  • :view
  • :linear-layout
  • :relative-layout
  • :text-view
  • :edit-text
  • :button
  • :image-view
  • :view-group
  • :list-view
  • :search-view
  • :action-bar
  • :action-bar-tab ...ActionBar.Tabに対応
  • :item ...MenuItemに対応
  • :progress-dialog
  • :web-view

それぞれのTYPEが、どのviewクラスに対応するかは、名前から明らかでしょう。

TYPE間には継承関係があります。:viewと:view-groupは継承されることを目的としたTYPEなので、UIツリーの中で直接使うことはありません。また、TYPEの継承関係は、viewクラスの継承関係とマッチするとは限りません。例えば :edit-textは、:text-viewではなく、:viewを継承したTYPEです。

継承関係
:view
  :view-group
    :linear-layout
    :relative-layout
    :list-view
    :search-view

  :text-view
    :button
  :edit-text
  :image-view
  :web-view

:progress-dialog
:action-bar
:action-bar-tab
:item

UI要素のTYPEは、独自に定義することも可能です。CheckBoxやSpinnerなど、頻繁に使うUI要素が欠けてますね。

UI要素の属性

UIツリーのATTRIBUTEマップには、属性名と属性値のペアを記述します。この情報は、デフォルトでは、標準のsetterを使ってviewオブジェクトへセットされます。例えば、{:foo "foo" :bar-buz 123}なら、setFoo("foo")とsetBarBuz(123)を指示したことになります。

属性名に指定するキーワードの一部は、特別扱いされます。これをtraitと呼びます。TYPEに応じて、使えるtraitが異なります。例えば、:text-viewには、:text-sizeというtraitがあり、この値には、数値か、ベクタ[数値 単位]を指定することができます。数値を指定した場合はsetTextSize(float size)でセットされ、 ベクタを指定した場合はsetTextSize(int unit, float size)でセットされます。

TYPEとtraitの対応は、neko.doc/describe関数で調べることができます。

(neko.doc/describe)               ; 全TYPEと、対応するtraitを表示。
(neko.doc/describe :text-view)    ; :text-viewのtraiを表示。
(neko.doc/describe :text-view :verbose)

describeから十分な情報が得られない場合は、nekoの内部データを直接見るしかないですね。具体的には、neko.ui.mapping/keyword-mappingです。

(clojure.pprint/pprint @@#'neko.ui.mapping/keyword-mapping)
    ; keyword-mappingはprivateなので、#'でVarを得てから@でderefする。
    ; 実体はatomなので、もう一度derefしてからpprint。

ATTRIBUTEの属性値にもキーワードを使うことができます。この場合、デフォルトでは当該viewクラスのstaticフィールドと見なされます。例えば、[:linear-layout {:orientation :horizontal} ...]とした場合、:horizontalは、LinearLayout.HORIZONTALと解釈されます。

属性名と同様、属性値に指定するキーワードの一部は、特別扱いされます。これについては別の記事(traitのカスタムメイド)で説明します。

共通trait

どのUI要素にも共通して使えるtraitを紹介します。

:def

これを指定すると、当該viewオブジェクトをグローバル変数にdefします。属性値には、完全修飾(fully qualified)シンボルを指定して下さい。シンタックスクオートを使うのが良いでしょう。

[:edit-text {:def `your-name}]

どこからでも、your-nameでこのEditTextオブジェクトを参照できます。グローバル変数なので、このviewオブジェクトはGCされません。メモリ不足を誘発する危険があると思います。

レイアウト系

以下は、お馴染みなので説明不要でしょう。

  • :layout-width
  • :layout-height
  • :layout-weight
  • :layout-gravity
  • :layout-margin-top/left/right/bottom

幅と高さの属性値として、:wrapと:fillを使うことができます(:matchは無いようです)。

:id-holderと:id

nekoは、JavaなAndroid開発で言う「ViewHolderパターン」に相当する仕組みを提供します。

この仕組みを使うための準備は、以下の2つです。

  • コンテナ系のview(:linear-layoutとか)の、:id-holder属性をtrueにする
  • あとから参照したい子viewに、:id属性を指定する

例えば、こんな感じです。

[:linear-layout {:id-holder true :def `lay}
 [:text-view {:id ::name}]
 [:button {:text "OK" :on-click (fn [v] ...)}]]

こうしておくと、以下のコードにより、TextViewオブジェクトが手に入ります。

(::name (.getTag lay))  ; getTag()が返すのは、Clojureのマップ。

layをdefしたくない場合、どうやってLinearLayoutオブジェクトを得るかが悩ましいですね。on-clickハンドラの引数vがButtonオブジェクトなので、getParent()するのは簡単ですが…。

リスナ系

UI要素が、:on-xxxのような名前のtraitを持つ場合、その属性値にはリスナのメソッドに相当する関数を指定できます。それを使ってnekoがリスナを生成し、viewオブジェクトにセットしてくれます。

traitの名前から、対応するリスナインターフェイスとメソッドが想像できると思うので、属性値に指定する関数のシグニチャ(引数)の説明は不要でしょう。

もし使いたいリスナが当該UI要素のtraitに無い場合でも、自力でリスナをセットすることができます。これについては、後述します。

:textと:image

文字列とイメージ用をリソースとして定義しているなら、:textと:imageが便利でしょう。:text-viewを継承したUI要素なら、:text属性の値に、文字列かリソース名を指定できます。同様に、:image-viewを継承したUI要素なら、:image属性の値に、drawableオブジェクトかリソース名を指定できます。

[:linear-layout {}
 [:button {:text :app-name}]      ; buttonは:text-viewを継承。
 [:image-view {:image :android/btn-star-big-on}]]

viewオブジェクトの生成と変更

生成

neko.ui名前空間には、viewオブジェクトを作るための関数が定義されています。

  • make-ui ...UIツリーに基づいてviewオブジェクトを作る
  • make-ui-element ...make-uiと同じだが、options引数を取る

options引数については、別の記事(traitのカスタムメイド)で説明します。

以下に、make-uiの使用例を示します。

(defactivity jp.dip.gpsoft.clonya.MainActivity
  :on-create
  (fn [this bundle]
    (on-ui
      (set-content-view! this
        (make-ui [:linear-layout {}
                  [:text-view {:text "Hello from Clojure!"}]])))))

viewクラスのコンストラクタは、第1引数にContextオブジェクトを要求します。この面倒は、make-uiやmake-ui-elementが見てくれますが、さらに追加の引数を指定したい場合は、UIツリーの属性に記述して下さい。

  • 属性名は、:constructor-args
  • 属性値は、引数のベクタ

例えば、[:text-view {:constructor-args [1 2]}]と記述すると、new TextView(context, 1, 2) によりviewオブジェクトが生成されます。

ただ、残念ながら :constructor-argsは、私の実験ではうまく機能しませんでした。プリミティブ型を引数に取るコンストラクタをうまくreflectできないようで、Spinner(Context, int)を呼ぼうとすると、「Spinner(Context, Integer)が見つからない」と言われて落ちてしまします。{:constructor-args [^Integer.TYPE Spinner/MODE_DIALOG]}のようにメタ情報で型を明示してもダメでした。

変更

作ったviewオブジェクトを後から変更するには、config!関数を使います。

(config! my-button
  :text "OK"
  :on-click (fn [_] (...)))

UIスレッドルール

Androidには、「UIの変更はUIスレッド(つまりメインスレッド)上で行え」という掟があります。これをサポートする機能が、neko.threading名前空間から提供されています。

例えば、いまUIスレッド上にいるかどうかを調べたいなら、on-ui-thread?関数が教えてくれます。

また、以下のマクロは、UIスレッド上での非同期実行を実現してくれます。

  • on-ui
  • post
  • post-delayed

UIスレッド上で実行したいコードがあるなら、単純に、on-uiで囲むだけでOKです。既にUIスレッド上にいるなら、その場で実行します。

(on-ui
  (.setText my-button "OK"))

コードをその場で実行するのではなく、メッセージキューにポストしたいなら、postマクロを使いましょう。

(post my-button
  (.setText ok-button "OK"))

post-delayedなら、ディレイを付けることもできます。

(post-delayed my-button 3000   ;msec指定。
  (.setText ok-button "OK"))

これらのマクロには、コードではなく関数を引数に取るバージョンもあります。

 on-ui*
 post*
 post-delayed*

自力でリスナをセットする

使いたいリスナをUI要素のtraitとして設定できなかったとしても、neko.listeners名前空間の関数/マクロを使えば、自力でリスナを登録することができます。

リスナオブジェクトを作る方法には、マクロ版と関数版の2種類があります。ButtonオブジェクトにOnClickListenerをセットするコードを例に説明します。

ButtonオブジェクトにOnClickListenerをセットする
;; マクロ版。
(.setOnClickListener my-button
  (on-click
    (toast "Yeah!" :short)))

;; 関数版。
(.setOnClickListener my-button
  (on-click-call
    (fn [v]
      (toast "Yeah!" :short)))) ; ②

どちらを使っても構いません。マクロ版の場合、コードの中では、OnClickListener.onClick(View)の引数を view という名前で参照できます。いわゆるアナフォリック・マクロですね。これに対し、関数版の場合、引数の名前は自由に決めることができます(上記コードの場合は v とした)。

nekoが提供するリスナ生成用関数は以下の通りです。

  • Viewクラス(neko.listeners.view)
    • on-click-call
    • on-create-context-menu-call
    • on-drag-call
    • on-focus-change-call
    • on-key-call
    • on-long-click-call
    • on-touch-call
    • on-layout-change-call
    • on-system-ui-visibility-change-call
  • TextViewクラス(neko.listeners.text-view)
    • on-editor-action-call
  • AdapterViewクラス(neko.listeners.adapter-view)
    • on-item-click-call
    • on-item-long-click-call
    • on-item-selected-call
  • Dialogクラス(neko.listeners.dialog)
    • on-cancel-call
    • on-click-call
    • on-dismiss-call
    • on-key-call
    • on-multi-choice-click-call
  • SearchViewクラス(neko.listeners.search-view)
    • on-query-text-call

末尾の"-call"を削除すれば、マクロ版の名前になります。どういうわけか、on-query-text-callだけは、マクロ版がありません。

またマクロ版で、リスナコールバックの引数が何という名前で露出するかは、各マクロのdocstringを見れば分かります。

(clojure.repl/doc on-editor-action)
-------------------------
neko.listeners.text-view/on-editor-action
([& body])
Macro
  Takes a body of expressions and yields a TextView.OnEditorActionListener
  object that will invoke the body.  The body takes the following implicit
  arguments:

  view       the view that was clicked
  action-id  identifier of the action, this will be either the identifier you
             supplied or EditorInfo/IME_NULL if being called to the enter key
             being pressed
  key-event  if triggered by an enter key, this is the event; otherwise, this
             is nil

  The body should evaluate to a logical true value if it has consumed the
  action, otherwise logical false.

くろニャア

くろニャアのレイアウトを変えてみました。

メインレイアウト
(defn- main-layout [act]
  [:linear-layout {:orientation :vertical}
   (make-header-label :welcome)
   [:linear-layout {:orientation :horizontal}
    (make-transit-button act :caption-ku-bt "KuActivity")
    (make-transit-button act :caption-ta-bt "TaActivity")
    ]
   [:linear-layout {:orientation :horizontal
                    :layout-width :fill
                    :id-holder true}
    [:edit-text {:hint (get-string :hint-your-name)
                 :layout-weight 1
                 :id ::your-name}]
    [:button {:text :caption-ok-bt
              :on-click (fn [v] (safe-for-ui (on-ok-fn act v)))}]
    ]
   ])

OKボタンを押すたびに、toastを出して、TextViewを追加します。

ボタン押下ハンドラ
(defn- on-ok-fn [act v]
  (let [ll (.getParent v)             ; linear-layout
        et (::your-name (.getTag ll)) ; edit-text
        yn (str (.getText et))        ; your-name string
        tv (make-ui act [:text-view {:text yn}])]   ; text-view
      (.setText et "")
      (toast yn :short)
      (.addView (.getParent ll) tv)))

スクリーンショットです。

hungry

Last modified:2014/01/31 19:32:15
Keyword(s):
References:[Clojure meets Android]
This page is frozen.