Context メモ

Android にはいくつかの Context がある。

Activity から参照可能なのは:

  • this : Acrivity
  • getBaseContext() : ContextImpl
  • getApplicationContext() : Applicaiton

となる。

Activity を Context として持ちまわるのは、Activity のリークにつながるので、getApplicationContext() で取得した Application を持ちまわる、というのが慣習になっていると思っていたが、Application だと問題が発生することがある。

具体的に遭遇した問題は:

View view = inflate(context, R.layout.sample, null);
addView(view);

という具合に、inflate を用いて View を生成する際に使用する Context が Application だった場合、生成された View にアプリのテーマが適切に設定されず、EditText の背景が不正になることがあった。

getBaseContext() で取得した ContextImpl を使用した場合は適切なテーマが設定されたため、これがワークアラウンドなんだと思う。

GridView メモ

備忘録。

GridView に表示する View に、Button が存在すると、そのボタンをタップしたかどうかにかかわらず、タッチイベントがボタンに取られてしまい、GridView の OnItemClickListener が通知されなくなる。

ViewPager メモ

備忘録。

Android の ViewPager は、単純な使い方ならかなり簡単でかつ便利なんだけど、ちょっと複雑なことをしようとすると、とたんにトラップが牙を剥く。

1.一度表示させた Fragment を取り替えることができない

例えば、3 ページ持つ ViewPager があったとする。
1 ページ目の入力で 2 ページ目に表示させる Fragment を動的に変える、というようなことが難しい。
表示を変えることはできるが、一度表示させた Fragment は ViewPager 内でキャッシュされ、またキャッシュされた Fragment を開放する方法を ViewPager が提供していないためである。

2.スワイプさせて表示した Fragment 内の View#onMeasure(int, int) が呼び出されない

ViewPager は初期状態で、setCurrentItem(int) で設定されたページと左右両隣のページの Fragment を要求し、このタイミングで生成された Fragment 内の View#onMeasure(int, int) は適切に呼び出される。ただし、指でスワイプしたり、後から setCurrentItem(int) などで違う Fragment を表示させる場合において、何故か View#onMeasure(int, int) が呼び出されない。

View#onMeasure(int, int) は、カスタム View を作る際にお世話になるメソッドで、場合によっては必要不可欠だったりするんだけど、ViewPager では機能しないため、対策が必要になる。

最もわかりやすい対策は、初期表示でなら適切にレイアウトされるので、まずすべてのページを初期状態で表示させ、その際使用した View をキャッシュしておき、改めて本番でそれらを使う、というものだ。キャッシュをどのように行うかがキモなんだけど、親 Activity などに持たせるのが賢明なんだと思う。

Animation メモ

FrameLayout に addView(View) をするタイミングで TranslateAnimatoin でスライドインさせようとした場合に、以下のケースでチラツキが発生したので、その解決法のメモ。

FrameLayout に元からある View を removeView(View) でアニメーションを伴わせ削除しつつ、addView(View) でアニメーションしつつ追加する場合はチラツキは発生しない。

しかし、FrameLayout に View を残したまま新しい View をアニメーションを伴わせつつ追加すると、一瞬アニメーション最終状態で表示され、アニメーション開始されてしまう。いわゆるチラツキ。

これを回避するために、以下の様なコードを書く。

        child.setVisibility(View.GONE);
        animation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                child.setVisibility(View.VISIBLE);
            }

            @Override
            public void onAnimationEnd(Animation animation) {
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }
        });
        child.startAnimation(animation);
        parent.addView(child);

こんな感じ。こちらで見つけた。

android - How to avoid blink of View when using animation? - Stack Overflow

要するに、ちらつく最初は GONE で非表示にさせておき、アニメーションが開始したら VISIBLE に戻す、というもの。

根本的な解決にはなっておらず、かつ実際のアニメーション期間が少し短くなるが、ちらつくよりは良いかと思う。

プロキシサーバを作りたい

Web 開発を行っていて、僕の環境だけ LiveReload が期待通りに機能せず、500 エラーが頻発するという現象に見舞われた。

原因は LiveReload で、HTML から LiveReload 関連の script タグをいくつか取り除くだけでエラーが出なくなるので、ソースツリーには手を加えずに script タグを取り除く、ということをしたい。

試したのは Chrome Extension で取り除く方法。

これは:

chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
  if (tab.url.startsWith("http://ubuntu:8080/")) {
    chrome.tabs.sendMessage(tab.id, {
      command: "remove_livereload"
    },
    function(msg) {
      if (msg !== undefined) {
        alert(msg);
      }
    });
  }
});

のような感じでコールバックを仕込んで:

chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
  if (msg.command === "remove_livereload") {
    $("script").each(function(index, element) {
      if ($(element).attr("type") === "text/javascript") {
        if ($(element).text().indexOf('WEB_SOCKET_SWF_LOCATION = "/__rack/WebSocketMain.swf";') !== -1 ||
            $(element).attr("src") === "/__rack/swfobject.js" ||
            $(element).attr("src") === "/__rack/web_socket.js" ||
            $(element).text().indexOf("RACK_LIVERELOAD_PORT = 35729;") !== -1 ||
            $(element).attr("src") === "/__rack/livereload.js?host=0.0.0.0&port=35729") {
          $(element).remove();
        }
      }
    });
  }
});

のような感じで DOM 操作することで、目的の script タグを除去することができる。


ただし、chrome.tabs.onUpdated.addListener というコールバック名からも分かる通り、HTML をパースして DOM ツリーが完成した後で呼び出されるので、このタイミングだと JavaScript は動いてしまっている。

Chrome API リファレンスをくまなく見てみたところ、以下のイベントコールバックを見つけた。

chrome.webNavigation.onDOMContentLoaded.addListener

これは DOM をパースし終えて、まだ JS の処理を行う前に呼び出されるコールバックなんだけど、このコールバックで通知されるのは、JS がアクセスしようとしている URL だったりして、またちょっと目的を果たせない。

このコールバックでちょっと遊んでみたんだけど、世の中は広告だらけだなぁ、と改めて実感した。Chrome で新規タブを開くだけで、ChromeGoogle の広告サーバにアクセスしてるのがわかった。

さて、Chrome Extension の仕様として、コールバック内で DOM 操作などは行えず、サンプルのように sendMessage でメッセージを送り、onMessage コールバック内で行う必要がある。つまり非同期なので、いくら最適なタイミングでコールバックが呼び出されたとしても、実際の処理を行うタイミングで処理が進んでしまっているので、JS が行う通信を完全に防ぐことは Chrome Extension ではできない。


もしかしたら Firefox なら出来るのかもしれないけど、ブラウザ拡張でどうにかするのは諦めて、プロキシサーバを立てて見ようと思う。Nginx が簡単そうだけど、Haskell で汎用的に作りたいような気分。