ひらい ぶらり Hi-Library

ぷろぐらみんぐについて。ときどきどうでもいいことについて。

FragmentPagerAdapterでnotifyDataSetChangedを呼んでも更新されない

結論

FragmentStatePagerAdapterを使え、でも問題はたくさんあるから油断をしてはいけない

※追記あり

FragmentStatePagerAdapterを使えと言ったな、あれは嘘だ - ひらい ぶらり Hi-Library

解決する内容

FragmentStatePagerAdapterは、destroyItemでfragmentをremoveしているので、notifyDataSetChangedを呼べば新しくgetItemで呼ばれたデータを元にFragmentが生成される。

解決しない内容

ただしsavedStateは残り続ける。以下のような場合カオスなことになる。

  1. Fragment内部にEditViewがあり、1ページ目に『ほげ』と入力された。
  2. notyfyDataSetChanged()
  3. Fragmentが再生成される。ただしsavedStateを渡して再生成されるので、Fragment内部に1と同じIDを持つEditViewがあれば『ほげ』が入力された状態で生成される

書いてて思ったけど、Fragmentの挙動としては正しい・・・のか? ともかく、気にしていないとわけも分からず想定しない動きをするのでFragment周りはソースをよく見ておいた方がいいということだけは肝に銘じておこうと思った。 viewのIDを変えれば回避できることだし、fragment的に考えたら再生成されたら復元されるのだろう。(大体余計なお世話であることのほうが多いけど)

結論その2

保持するアイテム数が少なく、絶対に更新されない場合→FragmentPagerAdapterを使う それ以外→FragmentStatePagerAdapterを使う

メモリを圧迫せず、データも更新されないならばFragmentPagerAdapterを使ったほうが、動作は早くなるハズ。でも大体FragmentStatePagerAdapterを使うことになると思う。

ここからチラシ裏

問題を解決するために色々見てたら以下の記事を見つけた。

outofmem.hatenablog.com

この記事はFragmentAdapterの使い方と言うよりは、FragmentPagerAdapter is クソみたいな話をします。

全面的に同意で、思ったことは自前でmakeFragmentNameメソッドを作らずにinstantiateItemメソッドを使えばいいんじゃないかなぐらい。書かれた時期が1年以上前なので何を今更感はあるかも。

あと、会社のパイセンと話していたらsupport libraryがバージョンアップして新たに getItemIdメソッドが実装されていることを教えてもらった。(4.1.2_r1から追加されてた) getItemIdでタグの生成をviewPager.getId() + position から、getItemで取れるなにがしかのIDをに変えてやれば問題は解決した。でもFragmentはマネージャーによって保持され続けるので、よっぽどのことがなければFragmentStatePagerAdapterを使うほうがよさそう。

Fragment周りはしょっちゅうトラブルが起きて、理解が浅いんだなーと思って調べれば調べるほどキモい気もするし、もしかしてこういうこと?あれ?ひょっとしてFragmentってちゃんと使えばすごく便利なん・・・そんなことねーやの繰り返しなきもしますが、これからも頑張ってFragmentと付き合っていこうと思います。

特定の辺だけにstrokeを付ける場合の補足

qiita.com

こちらの記事で丁寧に書かれて居るのだけれども、Javaで書いた場合とXMLで書いた場合に微妙な差異があって、機種によってはJavaで書いた場合に意図しない挙動を起こす可能性があるので補足。

元記事

private int BORDER_WEIGHT = 2;

/* 中略 */

// 白(半透明),太さ2pxのボーダーをつける
GradientDrawable borderDrawable = new GradientDrawable();
borderDrawable.setStroke(BORDER_WEIGHT, 0x55ffffff);

// LayerDrawableにボーダーを付けたDrawableをセット
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[]{borderDrawable});
// ボーダーがいらない辺にオフセット(負値)をつける
layerDrawable.setLayerInset(0, 0, -BORDER_WEIGHT, 0, -BORDER_WEIGHT);

// ボーダーを付けたいViewにセットする
view.setBackground(layerDrawable);

修正後

private int BORDER_WEIGHT = 2;

/* 中略 */

// 白(半透明),太さ2pxのボーダーをつける
GradientDrawable borderDrawable = new GradientDrawable();
borderDrawable.setStroke(BORDER_WEIGHT, 0x55ffffff);
// ボーダーの背景を透明にする 
borderDrawable.setStroke(0x00000000);

// LayerDrawableにボーダーを付けたDrawableをセット
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[]{borderDrawable});
// ボーダーがいらない辺にオフセット(負値)をつける
layerDrawable.setLayerInset(0, 0, -BORDER_WEIGHT, 0, -BORDER_WEIGHT);
// ボーダーを付けたいViewにセットする
view.setBackground(layerDrawable);

元記事のXMLの方ではsolidで透明を指定していたけど、Javaの方にはなかったのでこのままだと京セラの端末とかで背景が真っ黒になる為、律儀に透明を指定してやる必要がある。指定しないの端末が勝手に設定してくださるので真っ黒と書いたけど、そこは機種依存になるので注意。

manifestのproviderのandroid:authorityesにvalues/strings.xmlに定義した文字列を使うとインストールに失敗する

BuildTypeをreleaseでapeをビルドしようとしたら、lintでMissingTranslationとか出てコケた。 values-jaとか作ったら、全てのkeyがvaluesにあるものと一致しないとコケるようだ。そこはよろしくvaluesの値を使って欲しい。

どうやらこの警告は無視してビルドすることも出来るようだけど、気持ち悪いのでやりたくない。 仕方ないので、valuesと一致するように全ての項目をvalues-jaにも記述してビルドしてみる。

INSTALL_PARSE_FAILED_MANIFEST_MALFORMED

( ´゚д゚`)

チクチク原因を探っていったところ、manifest.xmlproviderタグのauthoritiesにstrings.xmlに記述している値を使っているところがダメらしい。 values-ja ディレクトリにあるauthoritiesに使用している項目を削除したところ動作した。 authoritiesに指定する文字列は多言語化されて一意に特定出来ない場合はエラーにされるっぽい。

とはいえ、この項目はProductFlavor毎に分けたいところだったりするので、strings.xmlに書けないのツライ。仕方ないので今回はbuild.gradleの方に書くことで避ける事にしたけどこれはなんかキモい。 どうにかならんもんか・・・

androidでfont変えると色々と弊害があるっぽい

怪奇現象が色々発生した

  1. maxLinesとか指定するとずれる

なんだか2行目の文字が1行目に食い込んだりする

  1. 消える

もはや表示されない。

  1. 端末によって挙動が違う

消えたり、消えなかったり、ずれたり、ずれなかったり

噂によるとheightをしてしたり、layout_gravityにtop|bottomを指定するといいらしいけど効果がなかった。 なんじゃこりゃ・・・

フォントサイズ自動調節TextView メモ

フォントを変更した場合には一手間加える必要がある

参考

http://dev.classmethod.jp/smartphone/android-font-fit-text-view/ https://gist.github.com/STAR-ZERO/2934490

mPaint.setFaceType(getFaceType()) // フォントをPaintオブジェクトに渡してあげる
mPaint.setTextSize(textSize);

これをやっておかないと、違うフォントでサイズを取ってしまうのでずれる。 あと高さをwrap_contentにすると、調整処理が入った場合二行以上になってからサイズが調節される関係で高さがおかしくなるのでTextViewの高さは指定しておいたほうがいい。

androidでTextViewに埋め込みフォントを使う

結論

API Level9以降(2.3)ならCaligraphyを使う

chrisjenx/Calligraphy · GitHub

疑問

マテリアルガイドラインによると、RobotoフォントとかNotoフォントを使えって書いたある。 ちょろっと調べて思い浮かんだ疑問

  1. assetsにファイルサイズ制限なかったっけ?フォントは圧縮するかダウンロードさせるみたいな記事が沢山ある
  2. TextViewを拡張してコンストラクタでTypeFaceを生成してセットする方法が良く書かれてるけど、ListViewなんか使った日には画面内に生成されるTextView分だけフォント読み込む処理がはしるんです?やだー!

調査してみる

1. assetsにファイルサイズ制限なかったっけ?フォントは圧縮するかダウンロードさせるみたいな記事が沢山ある

assetsにファイルサイズ制限があるのはAPI Level8まで。 ソースコード漁ってもAPI Levet9以降のasste.hにはUNCOMPRESS_DATA_MAXは存在しなかった。

2.TextViewを拡張してコンストラクタでTypeFaceを生成してセットする方法が良く書かれてるけど、ListViewなんか使った日には画面内に生成されるTextView分だけフォント読み込む処理がはしるんです?やだー!

こういうの

seesaakyoto.seesaa.net

この方法は間違ってないし、このTextViewが大量に生成されなければ大した問題じゃない。 が、ListViewとかで使ってしまうと大変なことに。いくらViewがリサイクルされると言っても画面内に同時に表示される分はTextViewが作られるので、その分だけassetsからフォントを読み込んでFaceTypeオブジェクトが作られまくってる(多分)。4.4の端末でOOMで落ちたの久しぶり。

できれば一度読み込んだFaceTypeはActivityかApplication辺りでキャッシュしておきたい。 各TextViewではそれらを使いまわすのが理想。っていうか後からTextViewを全部オリジナルのTextViewに書き換えるの面倒くさい。名前空間の宣言すら面倒くさい。

そこでCaligraphy

唸ってたら同僚からこんなこと書いてあるよってQiitのページを教えてもらった。

font - AndroidでNotoフォント・Robotoフォントを使う - Qiita

Caligraphy。そういうのもあるのか。 使い方を見るとすごく簡単そう。何やってるか気になったので簡単にソース読んでみたのでその内容をざっくりまとめます。 使い方は↑のサイトとかCaligraphyのgithub見るといいと思います。

Context生成時にweapしてる

attachBaseContextをオーバーライドして、CalligraphyContextWrapperでcontextをラップしてる。 これはContext.getSystemService()をオーバーライドする為にやっているようだ。 こうすることで、LAYOUT_INFLATER_SERVICEが呼ばれた時に、CalligraphyLayoutInflaterを使用するようになる。

で、どうやってTextViewにFontをセットしてるの?

CalligraphyLayoutInflaterの内部でinflateされるViewがTextViewだった場合にFontがセットされているようです。 また、TypeFaceもActivity毎にキャッシュされているので、ListViewで大量にTextViewが生成されてもロードされるのは1種類につき1回だけですね。

これだ!これを求めたいたのだ!

最悪自分で実装しようと思ってたので、こんだけドンピシャなライブラリがあったのは僥倖でした。 FAQに「カスタムIDを使うのにaarを使う必要があるから、jarでは提供しないよ、っていうかGradleつかってないの?」って書いてある点も個人的にポイント高めです。 「android TextView custom font」とかでググっても出てこなかったのが残念。っていうか古い情報ばっか出てきて混乱した。僕は全然知らなかったんですが、Calligraphyって有名なんですかね?日本じゃ有名じゃないだけなのか、僕が知らないだけなのか・・・もうちょっと情報収集頑張らねば

Volleyで302リダレクトできなかった話

どうも、バグを生み出すプロ、しんばしさんです。

なんやて工藤!

非同期のHTTP通信ライブラリのVolleyを使用していたのですが、302リダイレクトするAPIへアクセスしたところ com.android.volley.NoConnectionError: java.io.IOException: Could not retrieve response code from HttpUrlConnection とかでてうまく動きませんでした。開発機では問題なかったのですが、ロードバランサを経由する本番機では発生するという状態。ロードバランサ経由の場合HTTP/1.1でリクエストするとConnection:keep-aliveをヘッダで返してくるというのが原因でした。

解決策としては HurlStack を利用していた部分をこちらのGistを参考にOkHttpClientを使う自作のHttpStackを使うように切り替えて解決。

以下だらだらとそこまでの経緯。

せやかて工藤!

VolleyはGingerbread以前ならHttpClient、それ以降はHttpURLConnectionを内部的に使っています。今回はVolleyに含まれているHurlStackを使っていたのでHttpURLConnectionを使っていました。minSdkVersionもICS以降なのでHttpURLConnectionのバグも解消されているハズ・・・!と思っていたらそうでもありませんでした。

今回の原因はHttpURLConnection.getResponseCodeが呼ばれる時にレスポンスヘッダにConnection: keep-aliveが含まれているとそこでIOExceptionが発生してしまうことでした。なんかandroidはkeep-aliveにまつわるバグが多い割にデフォルトでkeep-aliveはtrueなのだとか。

System.setProperty("http.keepAlive", "false");

とりあえず上記のコードを呼んで見たけど効果なし。ぐぬぬ。 バージョンが上がって過去にあったバグが直った!とか言われてますが、HttpClientもHttpURLConnectionもどうも期待した動きをしてくれない模様。なので今後はOkHttpClientを使っていこうかなぁ・・・。

リクエスト時にHTTP/1.0でリクエストするようにしたHttpStackを作るというのも考えましたが、なんかわざわざ古い方に合わせるのも気持ち悪いので却下。

バージョンによってバグがあるからHttpClientとHttpURLConnectionが使い分けられるのも、今後発生するかもしれないバグの見極めがむずがしそうなので、今回はOkHttpClientを使って自作のHttpStackを使うという結論に至りましたが、なんか他にいい方法ないかなぁ・・・。

ざっくり手順

  1. OkHttpClientを読み込む

OkHttp

  1. OkHttpStackを作る

https://gist.github.com/bryanstern/4e8f1cb5a8e14c202750

  1. Volley.newRequestQueue時にOkHttpStackを渡す
OkHttpStack stack = new OkHttpStack(new OkHttpClient());
RequestQueue = Volley.newRequestQueue(context, stack);