LoginSignup
4

More than 5 years have passed since last update.

Leanback Support Libraryを25.0.1に更新した話

Last updated at Posted at 2016-11-17

Leanback以外は対応不要でした

仕事で担当しているAndroidアプリ(Android TVにも対応)のSupport Libraryを更新したので、そのときに対応したことのメモです。
モバイルとTVで同一apkをビルドしているのですが、今回の更新でコード変更が必要だったのはLeanback関連のみで、それ以外のSupport Libraryは特に対応する必要がありませんでした。

更新前後のバージョン

23.2.125.0.1 に更新しました。

24系をスキップしてます

最初まずワンクッションおくために 24.2.1 に上げようとしたのですが、以下のエラーが出まして

Error: Attribute "dotRadius" already defined with incompatible format.

で、調べたところ、どうやらsupport.wearablesupport.v17.leanbackを同時に使用しているとこのエラーが出てビルドが失敗する模様。25で解消したようなので、24はすっとばして25に上げました。

対応した点もろもろ

今回の更新にあたり、コードの変更が必要になった点と対応内容です。あくまで私が対応した内容なので、これ以外にも対応が必要な部分はあると思います。
なお、以下でLeanback提供のXxxFragmentと記述している部分は、すべてXxxSupportFragmentと読み替えていただいても構いません(私が実際に使っているのはXxxSupportFragmentのほうです)。

OnItemViewClickedListener / OnItemViewSelectedListener

変更された点

OnItemViewClickedListenerOnItemViewSelectedListener、どちらも変更内容は同じなので、以下、OnItemViewClickedListenerを例にして書きます。

OnItemViewClickedListenerの親interfaceとしてBaseOnItemViewClickedListener<T>が定義されました。

BaseOnItemViewClickedListener.java
public interface BaseOnItemViewClickedListener<T> {

    public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
                              RowPresenter.ViewHolder rowViewHolder, T row);
}
OnItemViewClickedListener.java
public interface OnItemViewClickedListener extends BaseOnItemViewClickedListener<Row> {

}

そして、RowsFragmentを始めとした各FragmentのsetOnItemViewClickedListenerの引数の型もBaseOnItemViewClickedListenerになっています。普通にコードを書いている場合は何も影響はない変更ですが、setOnItemViewClickedListenerコール時にLambdaを使用していると、ここがBaseOnItemViewClickedListenerとして評価されてしまうので、リスナーメソッドの第4引数であるrowの型が不定でObjectとして判断されます。
例えば、以下のように書いている場合があると思いますが、これは23ではOKで25ではコンパイルエラーになるコードです。

MyRowsFragment.java
@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をあきらめました。

MyRowsFragment.java
@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領域も考慮されてないし、該当のRowHeaderItemが設定されててもHeaderItem部分が表示されない(フォーカス行のコンテンツ部が画面最上段になるので、ヘッダ部は画面外になってしまう)しで、これは意図した変更なのか疑問です。

フォーカス行を画面上部に設定しているのはRowsFragmentのこの部分で、23の時点ではメソッド名は違いますが、同様の機能のメソッドはすでに実装済みではありました。

RowsFragment.java
@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内で呼ばれるように変更が加えられています。

RowsFragment.java
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
    // ・・・

    setAlignment(mAlignedTop);

    // ・・・
}

コードを読む限りではmAlignedTopmAlignedTopに代入して、VerticalGridViewが取得できたらalignmentOffsetを設定する・・・という感じで、実装者がなにもしなければmAlignedTopは初期値0のままなので、画面上部に張り付くのも納得です。

対応内容

原因は特定できたので、23と同じような動きをしたければ、該当のメソッドを無効化するしかなさそうです。 RowsFragment#onCreateViewから呼ばれちゃってるので、呼ばないという選択肢がとれないのはもどかしいですね・・・。

MyRowsFragment.java
@Override
public void setAlignment(int windowAlignOffsetFromTop) {
    // no-op
}

もしくは、こんな感じでベースのRowsFragmentを作っておけば、静的にフォーカス行の位置を指定したい場合でも動きそうです。

MyBaseRowsFragment.java
@Override
public void setAlignment(int windowAlignOffsetFromTop) {
    if (windowAlignOffsetFromTop <= 0) {
        return;
    }
    super.setAlignment(int windowAlignOffsetFromTop);
}

BrowseFragment

変更された点

ObjectAdapterVerticalGridViewに設定するタイミングが変更されています。
具体的には、以下のようにBrowseFragment#setAdapterを呼んでいると、ObjectAdapterがセットされず画面に何も表示されません。

MyBrowseFragment.java
@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;
}

これはObjectAdapterVerticalGridViewが関連づけられてない、という状態になるので、あとからいくらnotifyItemChanged系のメソッドを呼んでも無駄です。
理由はBrowseFragmentのコードを読むとわかります。

BrowseFragment.java
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が設定済みの場合はそこでmAdaptermHeadersFragmentに設定されます。また、BrowseFragment#setAdapterが呼ばれた時点でBrowseFragment#getViewnull以外を返せば、その場合も設定されます。
BrowseFragmentを継承したクラスで、super.onCreateViewの後で、かつthis.onCreateViewが終了していないタイミングでBrowseFragment#setAdapterを呼ぶと、変数mAdapterに代入されるだけで、実際にデータを表示するための手続きが実行されません。

対応内容

BrowseFragment#setAdapterの実行タイミングを変更しました。

MyBrowseFragment.java
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    mAdapter = new new ArrayObjectAdapter(new ListRowPresenter());
    setAdapter(mAdapter);
}

PlaybackOverlayFragment

変更された点

デフォルトのpaddingTopがめっちゃでかくなってました。

values.xml
<dimen name="lb_playback_controls_padding_top">216dp</dimen>

TVの画面高さが540dpなので、デフォルトだと上2/5がpadding領域になりますね。

対応内容

私の場合は画面全体にコンテンツを描画するような画面でしたので、適切な値でpaddingTopを上書きました。

GuidedStepFragment

変更された点

レイアウトが大幅に変更されました。

23系
guided-step-23.png

25系
guided-step-25.png

デフォルトのGuidanceStylist.Guidanceを使っていれば特に問題ないと思いますが、レイアウトを自作してGuidanceStylistを独自実装しているような場合は一度見直しが必要になりそうです。

対応内容

自作レイアウトけっこう使ってたのですが、今後こんな感じで変更されると厳しいので基本デフォルトで実装するようにしました。

TitleView

変更された点

23時点ではTitleViewクラスの型として扱われていたため、カスタマイズする場合はTitleViewを継承しなければいけませんでしたが、25ではTitleViewAdapter.Providerインターフェースを実装したViewとして扱うようになったため、カスタマイズの自由度が上がりました。
TitleViewAdapter.Providerは以下で定義されており、これまでTitleViewが直接操作を受け付けていた各メソッドを、TitleViewAdapterが担保する形になっています。

TitleViewAdapter.java

public abstract class TitleViewAdapter {

    public interface Provider {
        TitleViewAdapter getTitleViewAdapter();
    }
}

TitleViewとして扱いたいViewgetTitleViewAdapter()を実装し、そのメソッドで返すTitleViewAdapterを継承した実装クラスで、TitleViewとして扱うViewに対するアクセッサを実装します。

また、以前はTitleViewの取得や設定に使用するメソッドが、BrandedFragment内でpackage privateなメソッドとして定義されていたため、自作アプリからTitleViewにアクセスするためにはrefrection使ったりゴニョゴニョしたりする必要があったのですが、上記のTitleViewの定義変更と同時にこれらのアクセッサメソッドもpublicになっていますので、全体的にTitleViewカスタマイズの自由度が向上しています。

対応内容

TitleViewを継承して自作TitleViewを使用していたのですが(検索ボタン部分に別コンテンツを表示するような要件があったので)、全体的に書き直しました。これは本家TitleViewのコードを参考にすれば特に問題はないと思います。

旧実装

MyTitleView.java
public class MyTitleView extends TitleView {

    // TitleViewの各メソッドをオーバーライドする。
    // 独自レイアウトを組む場合は、一度TitleViewでinflateされたViewを破棄する必要があった。
}

25での実装

MyTitleView.java
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アプリ作ろうよ!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4