Leanback以外は対応不要でした
仕事で担当しているAndroidアプリ(Android TVにも対応)のSupport Libraryを更新したので、そのときに対応したことのメモです。
モバイルとTVで同一apkをビルドしているのですが、今回の更新でコード変更が必要だったのはLeanback関連のみで、それ以外のSupport Libraryは特に対応する必要がありませんでした。
更新前後のバージョン
23.2.1
→ 25.0.1
に更新しました。
24系をスキップしてます
最初まずワンクッションおくために 24.2.1
に上げようとしたのですが、以下のエラーが出まして
Error: Attribute "dotRadius" already defined with incompatible format.
で、調べたところ、どうやらsupport.wearable
とsupport.v17.leanback
を同時に使用しているとこのエラーが出てビルドが失敗する模様。25で解消したようなので、24はすっとばして25に上げました。
対応した点もろもろ
今回の更新にあたり、コードの変更が必要になった点と対応内容です。あくまで私が対応した内容なので、これ以外にも対応が必要な部分はあると思います。
なお、以下でLeanback提供のXxxFragment
と記述している部分は、すべてXxxSupportFragment
と読み替えていただいても構いません(私が実際に使っているのはXxxSupportFragment
のほうです)。
OnItemViewClickedListener / OnItemViewSelectedListener
変更された点
OnItemViewClickedListener
、OnItemViewSelectedListener
、どちらも変更内容は同じなので、以下、OnItemViewClickedListener
を例にして書きます。
OnItemViewClickedListener
の親interfaceとしてBaseOnItemViewClickedListener<T>
が定義されました。
public interface BaseOnItemViewClickedListener<T> {
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, T row);
}
public interface OnItemViewClickedListener extends BaseOnItemViewClickedListener<Row> {
}
そして、RowsFragment
を始めとした各FragmentのsetOnItemViewClickedListener
の引数の型もBaseOnItemViewClickedListener
になっています。普通にコードを書いている場合は何も影響はない変更ですが、setOnItemViewClickedListener
コール時にLambdaを使用していると、ここがBaseOnItemViewClickedListener
として評価されてしまうので、リスナーメソッドの第4引数であるrow
の型が不定でObject
として判断されます。
例えば、以下のように書いている場合があると思いますが、これは23ではOKで25ではコンパイルエラーになるコードです。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
// set listener.
setOnItemViewClickedListener((itemViewHolder, item, rowViewHolder, row) -> {
switch ((int) row.getId()) { // 25ではrowがObject型になるのでエラー
// ・・・
}
});
return view;
}
対応内容
ここはおとなしく、setOnItemViewClickedListener
ではLambdaをあきらめました。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
// set listener.
setOnItemViewClickedListener(new OnItemViewClickedListener() {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
switch ((int) row.getId()) { // OK
// ・・・
}
}
});
return view;
}
Row rowObj = (Row) row;
とか書くよりは、おとなしくLambda使わないほうがいいですよね・・・。
RowsFragment
変更された点
フォーカスが当たっているRow
の表示位置が、23では画面中央だったのが、25でWindow最上部になってしまいました。これOverscan領域も考慮されてないし、該当のRow
にHeaderItem
が設定されててもHeaderItem
部分が表示されない(フォーカス行のコンテンツ部が画面最上段になるので、ヘッダ部は画面外になってしまう)しで、これは意図した変更なのか疑問です。
フォーカス行を画面上部に設定しているのはRowsFragment
のこの部分で、23の時点ではメソッド名は違いますが、同様の機能のメソッドはすでに実装済みではありました。
@Override
public void setAlignment(int windowAlignOffsetFromTop) {
mAlignedTop = windowAlignOffsetFromTop;
final VerticalGridView gridView = getVerticalGridView();
if (gridView != null) {
gridView.setItemAlignmentOffset(0);
gridView.setItemAlignmentOffsetPercent(
VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
gridView.setItemAlignmentOffsetWithPadding(true);
gridView.setWindowAlignmentOffset(mAlignedTop);
// align to a fixed position from top
gridView.setWindowAlignmentOffsetPercent(
VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
gridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
}
}
ただ、23の時点では実装者が明示的にメソッドを呼び出さないと実行されていませんでしたが、25ではRowsFragment#onCreateView
内で呼ばれるように変更が加えられています。
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
// ・・・
setAlignment(mAlignedTop);
// ・・・
}
コードを読む限りではmAlignedTop
をmAlignedTop
に代入して、VerticalGridView
が取得できたらalignmentOffset
を設定する・・・という感じで、実装者がなにもしなければmAlignedTop
は初期値0のままなので、画面上部に張り付くのも納得です。
対応内容
原因は特定できたので、23と同じような動きをしたければ、該当のメソッドを無効化するしかなさそうです。 RowsFragment#onCreateView
から呼ばれちゃってるので、呼ばないという選択肢がとれないのはもどかしいですね・・・。
@Override
public void setAlignment(int windowAlignOffsetFromTop) {
// no-op
}
もしくは、こんな感じでベースのRowsFragment
を作っておけば、静的にフォーカス行の位置を指定したい場合でも動きそうです。
@Override
public void setAlignment(int windowAlignOffsetFromTop) {
if (windowAlignOffsetFromTop <= 0) {
return;
}
super.setAlignment(int windowAlignOffsetFromTop);
}
BrowseFragment
変更された点
ObjectAdapter
をVerticalGridView
に設定するタイミングが変更されています。
具体的には、以下のようにBrowseFragment#setAdapter
を呼んでいると、ObjectAdapter
がセットされず画面に何も表示されません。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
mAdapter = new new ArrayObjectAdapter(new ListRowPresenter());
setAdapter(mAdapter);
return view;
}
これはObjectAdapter
とVerticalGridView
が関連づけられてない、という状態になるので、あとからいくらnotifyItemChanged
系のメソッドを呼んでも無駄です。
理由はBrowseFragment
のコードを読むとわかります。
HeadersFragment mHeadersFragment;
private ObjectAdapter mAdapter;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// ・・・
mHeadersFragment.setAdapter(mAdapter);
// ・・・
}
public void setAdapter(ObjectAdapter adapter) {
mAdapter = adapter;
createAndSetWrapperPresenter();
if (getView() == null) {
return;
}
replaceMainFragment(mSelectedPosition);
if (adapter != null) {
if (mMainFragmentRowsAdapter != null) {
mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(adapter));
}
mHeadersFragment.setAdapter(adapter);
}
}
BrowseFragment#onCreateView
が呼ばれた時点でmAdapter
が設定済みの場合はそこでmAdapter
がmHeadersFragment
に設定されます。また、BrowseFragment#setAdapter
が呼ばれた時点でBrowseFragment#getView
がnull
以外を返せば、その場合も設定されます。
BrowseFragment
を継承したクラスで、super.onCreateView
の後で、かつthis.onCreateView
が終了していないタイミングでBrowseFragment#setAdapter
を呼ぶと、変数mAdapter
に代入されるだけで、実際にデータを表示するための手続きが実行されません。
対応内容
BrowseFragment#setAdapter
の実行タイミングを変更しました。
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mAdapter = new new ArrayObjectAdapter(new ListRowPresenter());
setAdapter(mAdapter);
}
PlaybackOverlayFragment
変更された点
デフォルトのpaddingTop
がめっちゃでかくなってました。
<dimen name="lb_playback_controls_padding_top">216dp</dimen>
TVの画面高さが540dpなので、デフォルトだと上2/5がpadding
領域になりますね。
対応内容
私の場合は画面全体にコンテンツを描画するような画面でしたので、適切な値でpaddingTop
を上書きました。
GuidedStepFragment
変更された点
レイアウトが大幅に変更されました。
デフォルトのGuidanceStylist.Guidance
を使っていれば特に問題ないと思いますが、レイアウトを自作してGuidanceStylist
を独自実装しているような場合は一度見直しが必要になりそうです。
対応内容
自作レイアウトけっこう使ってたのですが、今後こんな感じで変更されると厳しいので基本デフォルトで実装するようにしました。
TitleView
変更された点
23時点ではTitleView
クラスの型として扱われていたため、カスタマイズする場合はTitleView
を継承しなければいけませんでしたが、25ではTitleViewAdapter.Provider
インターフェースを実装したView
として扱うようになったため、カスタマイズの自由度が上がりました。
TitleViewAdapter.Provider
は以下で定義されており、これまでTitleView
が直接操作を受け付けていた各メソッドを、TitleViewAdapter
が担保する形になっています。
public abstract class TitleViewAdapter {
public interface Provider {
TitleViewAdapter getTitleViewAdapter();
}
}
TitleView
として扱いたいView
でgetTitleViewAdapter()
を実装し、そのメソッドで返すTitleViewAdapter
を継承した実装クラスで、TitleView
として扱うView
に対するアクセッサを実装します。
また、以前はTitleView
の取得や設定に使用するメソッドが、BrandedFragment
内でpackage private
なメソッドとして定義されていたため、自作アプリからTitleView
にアクセスするためにはrefrection使ったりゴニョゴニョしたりする必要があったのですが、上記のTitleView
の定義変更と同時にこれらのアクセッサメソッドもpublic
になっていますので、全体的にTitleView
カスタマイズの自由度が向上しています。
対応内容
TitleView
を継承して自作TitleView
を使用していたのですが(検索ボタン部分に別コンテンツを表示するような要件があったので)、全体的に書き直しました。これは本家TitleView
のコードを参考にすれば特に問題はないと思います。
旧実装
public class MyTitleView extends TitleView {
// TitleViewの各メソッドをオーバーライドする。
// 独自レイアウトを組む場合は、一度TitleViewでinflateされたViewを破棄する必要があった。
}
25での実装
public class MyTitleView extends FrameLayout implements TitleViewAdapter.Provider {
// TitleViewの操作はTitleViewAdapterを介して行う
private final TitleViewAdapter mTitleViewAdapter = new TitleViewAdapter() {
// TitleViewAdapterの各メソッドを実装して、MyTitleViewのリソースにアクセスする
}
// TitleViewAdapter.Providerで実装が必要なメソッドは以下のひとつだけ。
@Override
public TitleViewAdapter getTitleViewAdapter() {
return mTitleViewAdapter;
}
}
おわりに
こんな感じで更新対応を行いました。Leanbackは比較的新しめのライブラリなので、メジャーバージョンが上がるとpublicなメソッドもいろいろ変更されてて厳しいですね。こまめに更新対応していこうと思います。
Android TVの記事は需要がほとんど感じられないのでアレですがw、もし同じように困ってる方の助けになれば幸いです。
みんなTVアプリ作ろうよ!