道産子エンジニア

毎週好きなこと書きます。

「トータル・リコール ディック短篇傑作選」読んだ

トータル・リコール ディック短篇傑作選

トータル・リコール ディック短篇傑作選

先の早川書房SF小説セールで購入した中の一つだった。

目次をみて欲しい。二つの名作に挟まれて、いくつかの短編が紹介されている。

  • トータル・リコール
  • 出口はどこかへの入り口
  • 地球防衛軍
  • 訪問者
  • 世界をわが手に
  • ミスター・スペースシップ
  • 非0(null)
  • フード・メーカー
  • 吊るされたよそ者
  • マイノリティ・リポート

映画にもなっているトータル・リコールとマイノリティ・リポートはやはりズバ抜けて面白い。でもそれ以外の作品も面白いものが多かった。 ディックらしい作品を紹介するとき、どうしてもディック節とも言えるガジェットに目が行きがちだが、これらの短編集には細やかなガジェットは出てくるが気にしなくていい。 短編なので細かいものは頭の隅においておいて、作品を通じてディックが描きたかったことがわかりやすく表現されている。 思えばこれまでディックの短編を読んだことがなかったのだが、短編はぎゅっとメッセージや描きたいことが詰まっていて読みやすい。

あまりディックの作品を読んでない状態で短編を読むと難しいかな?と思っていたが杞憂だった。読んで思ったことを載せる。

トータル・リコール

映画をみているので全貌を知っているけれど、文章で読むとまた違った良さがあるので読んでよかった。 ディックらしい作品の中でも上位にくる作品だ。たった数ページの中に面白さが溢れかえっている。これをきっかけにディックを読み始めてもいいのではと思うくらい、先が気になって一気に読んでしまう。これまでに二回映画化されていて、初めは1990年で主演がアーノルド・シュワルツェネガーなのだがそちらはみたことがない。自分がみた2012年版では主演がコリンファレルになっていて、より原作に近い内容になっている。ディックの作品は映像技術が進むほどどんどんリメイクされていくべきだと思う。

俺の思うトータル・リコールのテーマであり一番面白いところは、 記憶を完璧に植えつけられるようになった世界で主人公が本当の自分に気が付いていくところ だ。その主題を肉付けていく、火星、異星人、惑星間刑事警察機構(インタープラン)がまたディックらしくていい。孤高な捜査官と巨大組織の陰謀の構図はよくあるのだが、それを夢と記憶に絡めて展開されているのがスリリングで面白い。読み終えたとき、どこからが本当で、どこからが植えつけられた記憶なのかともう一度読み返したくなる中毒性がある。登場人物の細かな動きなども後になって繋がってきたりしてさらに楽しめる。

映画「インセプション」をみた人は、最後のコマが回り続ける印象的なシーンを覚えているだろうか。ちょうど読み終えたときアレを思い出したのだが、現実と夢が溶け合うところにaffinityを感じたのかもしれない。また映画をみてみようと思う。

AIに仕事が奪われる...?

「出口はどこかへの入り口」の内容はまあそれなりなので紹介しないのだけれど、面白いことを書いてる部分があってそこを紹介したい。 最近もはや軽い気持ちでAIという言葉を使うと笑われるくらいには、猫も杓子も人工知能について語り始める世の中になってきた。そんな中でよく言われる「AIに仕事が奪われる」というのがいつも気になっていて、今の考えをここにあやかって書いておきたい。というのもこの短編の最初の節に昼食を頼む主人公がロボット労働者について紹介しているのが面白かったのだ。

軽食販売員とうい職業は、人間のなりてがいない。賃金体系のうんと底辺に位置しているからだ。

ここだ。晩年のディックが1979年にこうしたことを書いているのに、人間はまだその考えを現実社会に浸透させれないでいる。僕らは仕事が奪われると本当に困るだろうか。失業者はどうやってご飯を食べていくんだと言われるが、人が何かをしなくなってよくなった分、他にできることが増えると考えれば、何か見つけられるはずだ。少なくとも、マクドナルドが全店舗のレジ業務を機械に変えたとて、世界中に働けない人が溢れかえって困ったりはしないはずだ。(スマイルはメニューから消えるかもしれないが)人にしかできないことはまだまだたくさんある。本質的に人間がやらなくてもいいと思うことが減ってなぜ困るのだろう。自分はやらなくていいことが増えるのになぜ困るのだろう。だから、奪いはせずに「賃金を底辺にする」ってのはすごくいい考えだなと思った。結局日銭を稼いでいくしかない日々を過ごそうと考えてしかいないのであれば、何をしてもお金さえもらえればいい。逆に言えば、お金がもらえないなら勝手にやらなくなるのだ。機械が仕事を奪うのではなく、人がやるには割に合わない仕事にしていくことが目指すべき社会なのかもしれない。

なんて妄想がこの一文から妄想された。

公共機関というものの持つ暗黙のメッセージは、”おまえが心理的に権威と解釈するものに服従せよ”なの。

これもまた風刺が聞いたいい文章だと感じた。短編に自分の持つ社会への価値観をふんだんに盛り込んでくるディックのやりかたは結構好きだ。この作品の話自体は普通だったのだけれど。笑

地球防衛軍

テーマは 機械が人間より賢く?なったとき、というか人間が地球環境に害だと機械が判断したとき、機械はどうするのだろうか。 映画マトリックスでは、機械が悪役となり人間を電池として栽培する世界を描いていたが、何もその判断をするからといって機械が悪とは限らない。そんなポジティブな作品で好きだった。少なくとも争うことでしか、奪い合うことでしか平和を見つけられなかった歴史を辿った人間が、これ以上の根本的解決は生み出せないかもしれない絶望を受け入れた世界感だった。アメリカ vs ロシアもディックが大好きなテーマであるが、この作品ではそれを滑稽に描いているのが好きだった。人間にしかわからない既得権益を機械が思いもよらない方向でうまく利用していくのだ。こんな世界はむしろ幸せに感じるし、余計な不安をかかえがちな自分たちを正義の味方ことロボットたちが助けるなんて作品はもっと増えていいんじゃないだろうか。

マイノリティ・リポート

映像に乗っかると迫力がすごくて映画も大好きなのだけど、短編でも十分スリリングで迫力が衰えない展開だった。これに関しては原作でも映画でも二度美味しいのでどちらも見るべきだと思う。 映画では手で空中のディスプレイを操作するコーンソール、銃、乗り物、クモ型の捜査マシーンとかとにかくかっこいいガジェットがふんだんに出てくるのが最高なんだけど、テーマは全く同じなので、どちらのオチや話の展開もすごくいい。

ここでのテーマは 「犯罪を未然に防ぐことができるようになった社会」であり、それを実現する犯罪の予知夢をみる三人のプレコグとシステムを管理する犯罪予防局側の人間が犯す罪のパラドックス である。犯罪が起きる前に犯人が捕まる世界は素晴らしいが恐ろしい。そして矛盾がある。犯罪を未然に防げるが、誰でも潜在的に犯人となり得るし、それを知った上で行動が変わってしまう可能性があるからだ。アニメ「サイコパス」では、最強の中央演算装置シュビラシステムが犯罪係数を弾き出すという内容だったが共通点が多い。現在の警察も起きた事件の犯人は捕まえられるし、アンパンマンはバイキンマンが悪さをしてからやってくる。未然に防げるなら最高だけれど、本当にそんな社会は問題なく回るだろうかという皮肉が込められていて、禅問答のような面白さがある。


シュミラクラを読んでいたのだけど、電子書籍の方が読み進めるのが早くてこちらが先行した。やはり年末年始は思ったように読書が進まない。

読書所用時間:約6時間
執筆時間:約2時間(3390文字)
オススメ度:★★★★☆

【AndroidTV】android.support.v17.leanbackの中を読む ~part2. TitleView編~

前回に引き続き、AndroidTVのleanbackを読んでいきます。 今回はAndroidTVのアプリ開発をするといつも必要になる「TitleView」のカスタマイズについてです。

これを読んでいる方は、AndroidTVで下のようなレイアウトを実現できるでしょうか。

youtu.be

もしできるのであれば、今回の内容はもう理解されていると思いますので、あまり役に立たないかもしれません。笑

普段Androidアプリの開発をしていて、AndroidTVアプリの開発をしたことがない人には「こんな簡単なこともすぐにできないのか!」と知っていただきたくて載せました。 多分、この辺でつまづいた人も少なくないと思います。「そうそう、それがやりたかったんだよ!」という方はぜひ一読していただけると嬉しいです。読み終わる頃には仕組みが理解できて、すぐに実装できるようになっています。

少しでもAndroidTVで開発するエンジニアの助けになれたら幸いです。

さて、本題に入ります。

TitleViewとは

TitleViewは BrandedFragment が持つタイトルを表示するためのViewです。アプリや画面の情報を載せておけるViewの一つです。 なので今回の内容はBrandedFragmentを継承したクラスには適応可能です。前回載せたクラス群で確認しましょう。

Fragment
 ...
 ├─BrandedFragment(BrandedSupportFragment)
 │ ├─ErrorFragment(ErrorSupportFragment)
 │ └─BaseFragment(BaseSupportFragment)
 │   ├─BrowseFragment(BrowseSupportFragment)
 │   ├─DetailsFragment(DetailsFragmentBackgroundController)
 │   └─VerticalGridFragment(VerticalGridSupportFragment)
 ...

特によく使うコンポーネントの例をあげればBrowseFragmentVerticalGridFragmentでしょう。それぞれにおけるTitleViewは赤枠で囲った部分になります。

BrowseFragment

f:id:yuichi31:20171216105836p:plain

VerticalGridFragment

f:id:yuichi31:20171216105851p:plain

なお、バッジを設定することもできて、バッジを設定するとタイトルの文字は隠されます。

バッジを設定したとき

f:id:yuichi31:20171216110255p:plain

デフォルトの機能で開発者が変更可能なのは

  • タイトルの文字の変更
  • バッジの設定
  • SearchOrb(検索ボタン)の設定

のみです。カスタマイズしないとタイトルの位置を変えるAPIも存在しません。 ですが、便利なのはTitleView自体をコンテンツの表示に合わせてshow/hideしてくれたりすることです。

しかし、カスタムに対応しなくては大抵のデザイン要件にマッチしない or デザイナーに俺のゴーストがGoogleの意思だと囁いていると説明してお願いして、仕様を変更してもらうしかないですね。 ここを乗り越えて、いい感じにカスタムできると少し周りより違ったアプリになりそうです。今回は最初の動画のようにVirticalGridFragmentをカスタマイズする例を取り上げます。

TitleViewAdapterを使いカスタムTitleViewを作る

まずはBrandedFragmentを読むとTitleViewAdapterというクラスがTitleViewへ操作してることがわかります。

  • BrandedFragment.java
/**
     * Sets the view that implemented {@link TitleViewAdapter}.
     * @param titleView The view that implemented {@link TitleViewAdapter.Provider}.
     */
    public void setTitleView(View titleView) {
        mTitleView = titleView;
        if (mTitleView == null) {
            mTitleViewAdapter = null;
            mTitleHelper = null;
        } else {
            mTitleViewAdapter = ((TitleViewAdapter.Provider) mTitleView).getTitleViewAdapter();
            mTitleViewAdapter.setTitle(mTitle);
            mTitleViewAdapter.setBadgeDrawable(mBadgeDrawable);
            if (mSearchAffordanceColorSet) {
                mTitleViewAdapter.setSearchAffordanceColors(mSearchAffordanceColors);
            }
    ...
         }
    }

開発者はTitleViewAdapter使うことで独自のTitleViewが作成できます。より正確に言えば、独自のViewに対してTitleViewAdapter.Providerを実装することで実現できます。クラスのドキュメントを見るとSearchOrb(検索ボタン)は必ず配置しないといけないと書いてありますが、今回は説明を簡単にするためにSearchOrbは省いています。 (必ず配置というわりにはnullを返しても大丈夫なので、隠せてしまうのは設計ミスかもしれないです)

先ほどの動画で用いていたCustomTitleViewのレイアウトはこちらです。databinding使っています。

  • layout_custom_title.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  >
  <RelativeLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >

      <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:layout_marginStart="4dp"
        android:layout_marginTop="24dp"
        android:textColor="@android:color/white"
        android:textSize="24sp"
        tools:text="Title"
        />

      <TextView
        android:id="@+id/button_1"
        android:layout_width="wrap_content"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:focusable="true"
        android:gravity="center"
        android:paddingLeft="24dp"
        android:paddingRight="24dp"
        android:layout_below="@id/title"
        android:text="button1"
        android:textColor="@android:color/white"
        android:textSize="18sp"
        />

      <TextView
        android:id="@+id/button_2"
        android:layout_width="wrap_content"
        android:layout_height="48dp"
        android:layout_marginStart="4dp"
        android:focusable="true"
        android:gravity="center"
        android:paddingLeft="24dp"
        android:paddingRight="24dp"
        android:layout_below="@id/title"
        android:layout_toEndOf="@id/button_1"
        android:text="button2"
        android:textColor="@android:color/white"
        android:textSize="18sp"
        />

  </RelativeLayout>

</layout>

フォーカスしたいViewにはandroid:focusable="true"を忘れないでください。
今回作るクラスはCustomTitleViewとします。

  • CustomTitleView.kt
class CustomTitleView @JvmOverloads constructor(
    c: Context, attrs:
    AttributeSet? = null,
    defStyle: Int = 0)
  : LinearLayout(c, attrs, defStyle), TitleViewAdapter.Provider {

  private val titleTextView: TextView
  var title: CharSequence
    get() = titleTextView.text
    set(titleText) {
      titleTextView.text = titleText
    }

  init {
    val binding: LayoutCustomTitleBinding = DataBindingUtil.inflate(
        LayoutInflater.from(context), R.layout.layout_custom_title,
        this, false)
    titleTextView = binding.root.findViewById(R.id.title)
    title = titleTextView.text
    addView(binding.root)
  }

  private val adapter = object : TitleViewAdapter() {

    override fun getSearchAffordanceView(): View? = null

    override fun setTitle(titleText: CharSequence?) {
      this@CustomTitleView.title = titleText.toString()
    }

    override fun getTitle(): CharSequence = this@CustomTitleView.title
  }

  override fun getTitleViewAdapter(): TitleViewAdapter = adapter
}

set/getTitleというメソッドをオーバーライドしないといけないため、backing propertyなどでゴニョゴニョしていますが、重要なのは以下です。

  • addViewで独自のViewを追加する
  • TitleViewAdapterのメンバ(プロパティ)を持つ
  • TitleViewAdapter.ProvidergetTitleViewAdapterメソッドでそれを返す

今回はSearchOrbを消していると書きましたが、

 override fun getSearchAffordanceView(): View? = null

の部分を変更すればカスタマイズ可能です。次はCustomTitleViewBrandedFragmentに設定しましょう。

onInflateTitleViewCustomTitleViewを渡す

今回はVerticalGridFragmentですが、BrandedFragmentを継承しているクラスならonInflateTitleViewというメソッドをオーバーライドできます。 そこでCustomTitleViewを呼び出したレイアウトのxmlを返すことで適応されます。

  • view_custom_title.xml
<la.kaelae.customtitleviewsample.CustomTitleView
  android:id="@+id/browse_title_group"
  style="?attr/browseTitleViewStyle"
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  />

ここで重要なのはidをbrowse_title_groupにすることです。BrandedFragment内のタイトルを設定する処理はinstallTitleViewメソッド内で行われ、 そのときsetTitleView(titleLayoutRoot.findViewById(R.id.browse_title_group))するためです。

  • BrandedFragment.java
public void installTitleView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
        View titleLayoutRoot = onInflateTitleView(inflater, parent, savedInstanceState);
        if (titleLayoutRoot != null) {
            parent.addView(titleLayoutRoot);
            setTitleView(titleLayoutRoot.findViewById(R.id.browse_title_group));
        } else {
            setTitleView(null);
        }
    }

ではview_custom_title.xmlを実際にinflateしましょう。

  • VerticalGridFragment.kt
override fun onInflateTitleView(inflater: LayoutInflater, parent: ViewGroup, savedInstanceState: Bundle?): View {
    customTitleView = inflater.inflate(R.layout.view_custom_title, parent, false)
    val c = parent.context

    val button1 = customTitleView.findViewById<TextView>(R.id.button_1)
    button1
        .setOnClickListener { _ -> Toast.makeText(c, "button1", Toast.LENGTH_SHORT).show() }

    val button2 = customTitleView.findViewById<TextView>(R.id.button_2)
    button2
        .setOnClickListener { _ -> Toast.makeText(c, "button2", Toast.LENGTH_SHORT).show() }
    return customTitleView
  }

上記のコードでは省略していますが、AndroidTVの場合、フォーカスが当たったときにUIを強調する色の変更などを追加するべきです。 View単体でのフォーカス制御を行なっているwidgetに、SearchOrbViewがあるのでコードは載せませんが参考にしてみてください。

ここまでのコードを書くことで、独自のレイアウトをタイトルに設定することが可能です。しかしこれだけでは思ったように動きません。 ボタンへのフォーカスがうまく動かないはずです。AndroidTVではフォーカスの仕組みが思うような操作を邪魔します。

TitleView内のフォーカスを制御する

そこでTitleView内のフォーカスをどこで制御しているのかを見つけるのがポイントになります。 まずはVerticalGridFragmentを読んでみるとsetupFocusSearchListenerという怪しいメソッドを見つけます。leanback内では度々登場するのがFocusSearchというキーワードで、フォーカスがうまく行かないなというときはこれを検索してみるのがおすすめです。

  • VerticalGridFragment.java
private void setupFocusSearchListener() {
        BrowseFrameLayout browseFrameLayout = (BrowseFrameLayout) getView().findViewById(R.id.grid_frame);
        browseFrameLayout.setOnFocusSearchListener(getTitleHelper().getOnFocusSearchListener());
}

onStartするたびにここを呼び出していますが、わからない部分がいくつか出てきました。BrowseFrameLayoutとはなんでしょうか。 そこでVertivalGridFragmentのレイアウトを確認します。

  • lb_vertical_grid_fragment.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/browse_dummy"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <android.support.v17.leanback.widget.BrowseFrameLayout
        android:id="@+id/grid_frame"
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:descendantFocusability="afterDescendants"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <FrameLayout
            android:id="@+id/browse_grid_dock"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </android.support.v17.leanback.widget.BrowseFrameLayout>
</FrameLayout>

このようにbrowse_grid_dockというFrameLayoutをラップするように配置してあります。そしてBrowseFrameLayoutのクラスドキュメントには

/**
 * A ViewGroup for managing focus behavior between overlapping views.
 */

とあるため、ここでラップしたViewのフォーカスを制御していそうです。あと少しですね。BrowseFrameLayoutsetOnFocusSearchListenerを読むとmListenerというメンバに代入しています。そこでmListenerの処理をしている箇所を探すと、focusSearchを見つけます。

  • BrowseFrameLayout.java
@Override
    public View focusSearch(View focused, int direction) {
        if (mListener != null) {
            View view = mListener.onFocusSearch(focused, direction);
            if (view != null) {
                return view;
            }
        }
        return super.focusSearch(focused, direction);
    }

このfocusSearchで返却するViewにフォーカスが当たる仕組みですね。ではVerticalGridFragmentで渡していたOnFocusSearchListenerを探すとなぜうまく動かないのかわかりそうです。 もう一度確認すると

  • VerticalGridFragment.java
private void setupFocusSearchListener() {
        BrowseFrameLayout browseFrameLayout = (BrowseFrameLayout) getView().findViewById(R.id.grid_frame);
        browseFrameLayout.setOnFocusSearchListener(getTitleHelper().getOnFocusSearchListener());
}

となっており、getTitleHelper().getOnFocusSearchListener()なのでTitleHelperが設定しているOnFocusSearchListenerがそれに当たりそうです。

// When moving focus off the TitleView, this focus search listener assumes that the view that
// should take focus comes before the TitleView in a focus search starting at the scene root.
private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener = new BrowseFrameLayout.OnFocusSearchListener() {
        @Override
        public View onFocusSearch(View focused, int direction) {
            if (focused != mTitleView && direction == View.FOCUS_UP) {
                return mTitleView;
            }
            final boolean isRtl = ViewCompat.getLayoutDirection(focused)
                    == ViewCompat.LAYOUT_DIRECTION_RTL;
            final int forward = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
            if (mTitleView.hasFocus() && (direction == View.FOCUS_DOWN || direction == forward)) {
                return mSceneRoot;
            }
            return null;
        }
};

見つけました。引数にあるfocusedは直前にフォーカスされていたViewであり、directionはリモコンキー操作の向きです。直前にTitleViewにフォーカスが当たっていないとき、上方向のキー操作を行うとTitleViewにフォーカスが合うような実装になっています。mSceneRootTitleHelper初期化のときに設定する、TitleViewを持つ親のViewGroupです。この処理を変更すれば、自分が思うようなフォーカス制御ができそうです。

BrowseFrameLayoutCustomTitleViewのフォーカスの動きを定義したonFocusSearchListenerを設定します。

  • VerticalGridFragment.kt
override fun onResume() {
    super.onResume()
    val browseFrameLayout = view.findViewById<BrowseFrameLayout>(R.id.grid_frame)
    browseFrameLayout.setOnFocusSearchListener { focused, direction ->
      val button1 = customTitleView.findViewById<TextView>(R.id.button_1)
      val button2 = customTitleView.findViewById<TextView>(R.id.button_2)
      if ((focused !== customTitleView && direction == View.FOCUS_UP) ||
          (button2.hasFocus() && (direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT))) {
        return@setOnFocusSearchListener button1
      }
      if (button1.hasFocus() && (direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
        return@setOnFocusSearchListener button2
      }
      if ((button1.hasFocus() || button2.hasFocus()) && direction == View.FOCUS_DOWN) {
        return@setOnFocusSearchListener browseFrameLayout
      }
      browseFrameLayout
    }
  }

簡単にするためRTLサポートの処理は外しました。lb_vertical_grid_fragmentでidを確認していたのでBrowseFrameLayoutをfindしてから、独自のonFocusSearchListenerを設定すれば思い通りの動きにできます。ここではボタンが二つあり、それぞれにフォーカスがあるときの横移動は互いのViewへの移動になっています。リストを追加したり、より複雑な処理をするときもこれを拡張すれば良さそうです。ですが、タイトルの部分であまり複雑な処理を行う必要があるかは場合によると思います。下にRowやGridが配置されることになるので、リストの並び替え、フィルターなどくらいに留めて置くのが良いと思います。今回はonResumeに書いていますが、Viewが取得できる適切なタイミングならどこでも良いと思います。

以上です。