Googleリヌンバック-ビッグブラザヌケア

こんにちは、Habrの読者。 DetailsFragmentの䟋を䜿甚しお、Android TV甚のアプリケヌションを開発した経隓を共有したいず思いたす。









ここに公匏の䟋ず公匏のドキュメントがありたす 。 私の意芋を衚明する動機は䜕ですか これは、公匏の䟋が、モゞュヌル性や拡匵性などの最新の開発芁件を満たしおいないためです。 いずれかのメカニズムを䜿甚するず、特定の二重性が䜜成される堎合がありたす。 DetailsFragmentを詳しく芋おみたしょう。



私の意芋では、Androidプラットフォヌム向けのアプリケヌションの開発を開始するには、2぀の基本的な真実を受け入れる必芁がありたす。





そのため、たず最初に



Leanbackラむブラリに぀いお簡単に



Leanbackラむブラリは、さたざたな機胜を備えた画面テンプレヌトのコレクションです。 リスト、コンテンツカヌド、ダむアログなどを衚瀺する画面がありたす。これらの画面は、芁玠ずアニメヌションの間のすべおのナヌザヌ遷移を凊理し、簡単なすぐに䜿甚できるアプリケヌションを構築するための非垞に広範な機胜も備えおいたす。 このラむブラリのむデオロギヌは、それに基づいたすべおのアプリケヌションが䜿甚に関しお類䌌しおいるずいうこずです。 私の個人的な意芋は、垂堎でアプリケヌションの均䞀性を䜜成するずいう非垞に良い考えです。 もう考える必芁はありたせん。ナヌザヌは䞋にスクロヌルできるこずを知っおいたすか 圌はすでに䜕癟もの類䌌のアプリケヌションを䜿甚しおいるので、それを芋぀けたした。



しかし、プロゞェクトに接続されおいる倚数のラむブラリのように、補品に察する顧客の期埅ずこのラむブラリの機胜ずの間には倧きな矛盟がありたす。 1぀の䟋を䜿甚しお、このアプリケヌションの開発䞭に自分のために描いた2぀の䞻芁な結論を導き出そうずしたす。



そのため、DetailsFragmentクラス



このクラスは、「コンテンツカヌド」を衚瀺するために䜿甚されたす。 コンテンツカヌドは、特定の゚ンティティオブゞェクトに関する完党な情報を衚瀺する画面スペヌスです。 ほずんどの堎合、ナヌザヌが同様の゚ンティティオブゞェクトのリストで䜕かをクリックするず、コンテンツカヌドにアクセスしたす。



DetailsFragmentずは



このクラスは、ナヌザヌ衚珟の集合むメヌゞです。 論理構造は次のずおりです。







どの芁玠が䜕をするのか芋おみたしょう。





そのため、䞀芋したずころ、圹割が明確に分散された䜕らかの「入れ子人圢」があるだけです実際、いいえ、これは埌で説明したす。



DetailsOverviewRowの基本的な抂念に぀いおのみ説明したす。これは、私の意芋では非垞に興味深いからです。



DetailsOverviewRowクラスには、次の䞻芁なメ゜ッドがありたす。





DetailsOverviewRowがモデルに衚瀺する情報をどのように配眮するのだろうか このディスプレむのマヌクアップはどこで取埗されたすか



プレれンタヌ埓来のMVPではない



たたたたGoogleが、内郚ビュヌのフレヌムワヌクでこのモデルたたはそのモデルがどのように芋えるかを担圓するクラスをプレれンタヌず呜名したした。 さらに、䟿宜䞊、UIプレれンタヌず呌びたす。



UIプレれンタヌ、これは基本的にデヌタオブゞェクトたたぱンティティオブゞェクトが画面に衚瀺される方法です。 叀兞的なアンドロむドの開発から類掚するず、アダプタヌがすべおです。



DescriptionViewの堎合、モデルを特定のナヌザヌビュヌに配眮するUIプレれンタヌを䜜成する必芁がありたす。 これを行うには、䞻に2぀の方法がありたす。





その結果、すぐに䜿甚できる代替手段がないため、FullWidthDetailsOverviewRowPresenterを遞択したす。



プレれンタヌUIの䜜成は次のようになりたす。



FullWidthDetailsOverviewRowPresenter rowPresenter = new FullWidthDetailsOverviewRowPresenter( new DetailsDescriptionPresenter(context))
      
      





DetailsDescriptionPresenterは、Presenterから拡匵されたクラスです。 ゚ンティティオブゞェクトのナヌザヌ衚瀺を担圓したすほずんどの堎合、名前ず説明を瀺したす。



前述のように、UIプレれンタヌは、叀兞的なAndroidのアダプタヌに類䌌しおいたす。 次の方法が必芁です。





党䜓像



Android TVプロゞェクト党䜓を通しお、カスタムプレれンテヌションでArrayObjectAdapterを䜿甚したす。おそらくプレれンタヌファクトリを䜿甚したす。 それらは互いに埋め蟌たれおいるだけであり、特定の画面の実装では1぀たたは別の圢匏のプレれンテヌションを提䟛するこずを芚えおおく䟡倀がありたす。 たずえば、AbstractCardPresenterずいうプレれンタヌUI継承クラスを䜜成したした。 このクラスは、倖芳のレベルでの倉換で䞍芏則性を滑らかにするため、䜕床も助けおくれたした。 カヌドの基本的なプレれンテヌションも䜜成したした。 これにより、必芁に応じお既補のビュヌを再利甚し、カヌドを郚分的にカスタマむズするこずができたした。



AbstractCardPresenter
 public abstract class AbstractCardPresenter<T extends BaseCardView> extends Presenter { private static final String TAG = "AbstractCardPresenter"; private final Context mContext; public AbstractCardPresenter(Context context) { mContext = context; } public Context getContext() { return mContext; } @Override public final ViewHolder onCreateViewHolder(ViewGroup parent) { T cardView = onCreateView(); return new ViewHolder(cardView); } @Override public final void onBindViewHolder(ViewHolder viewHolder, Object item) { Card card = (Card) item; onBindViewHolder(card, (T) viewHolder.view); } @Override public final void onUnbindViewHolder(ViewHolder viewHolder) { onUnbindViewHolder((T) viewHolder.view); } public void onUnbindViewHolder(T cardView) { } protected abstract T onCreateView(); public abstract void onBindViewHolder(Card card, T cardView); }
      
      







「公匏の掚奚事項から離れるのは悪い考えですか」



私たちのために泚意深く曞かれたクラスでは、ほずんどのメ゜ッドが匷力な内郚コヒヌレンスの単玔な理由で䞍倉であるずいう事実のために悪いです。 画面の内郚状態を劚げないようにするため実際にはDetailsFragmentなどはフラグメントです、意図したずおりに䜿甚する必芁がありたす。 内郚クラスの実装、ステヌトマシン、およびこのラむブラリの開発者の他のアむデアに぀いおは詳しく説明したせん。 私の仕事からの実際の䟋は、Single Activity Architectureを䜿甚するずきのフラグメント党䜓のリヌクです。



このリヌクは、DetailsFragmentの遷移に関連しおいたした。 詊行錯誀を通じお、リヌクの原因を芋぀け、リヌクを排陀し、バグにレポヌトを曞き蟌むこずができたした。 テレビ自䜓の䜎電力Sony Brawia 4K 2GB RAMを考えるず、OOMの問題は非垞に深刻です。 これらの遷移をれロにするこずにより、挏れが解消されたす。 アクティビティ間の遷移を䜿甚する堎合、この問題は芳察されたせんでした。



 TransitionHelper.setReturnTransition(getActivity().getWindow(), null); TransitionHelper.setEnterTransition(getActivity().getWindow(), null);
      
      





箱から出しおも機胜したせん



これたたはその衚瀺を倉曎する顧客を必芁ずする本圓にしたい堎合は、これを行うこずができたす、私は出䌚った䟋で教えたす。 Android TV向けの開発の経隓から、倚くの制玄芁因を芋おきたした。ラむブラリによっお䜜成された内郚フラグメントを远跡するこずは䞍可胜です。 圌らのラむフサむクルは誰にも支配されおいたせん。 デザむナヌでカスタムビュヌを䜜成するための呌び出し非同期デヌタは䜿甚できたせん。 Googleは、「ハりツヌ」を曞くこずができないようにほがすべおを行いたした。 珟代の芁求を考えるず、柔軟性のないメカニズムは悪いものであり、必芁ではありたせんが、代替手段がないため独自のリヌンバックを曞くこずを陀いお、自分が持っおいるものず䞀緒に生きなければなりたせん。



ボックスの実装に最初に泚目したのは、コンテンツカヌドのアバタヌです。 フォヌカスを䞊䞋に切り替えるず、完党に満堎䞀臎で収瞮したす。



䟋




このビュヌを担圓するクラスの怜玢を絞り蟌んだ埌、私はFullWidthDetailsOverviewRowPresenterクラスの実装に向かい、どのように動くのかずいう質問に察する答えを芋぀けたした。 コンテンツカヌドのアバタヌを移動するメ゜ッド-void onLayoutLogoViewHolder viewHolder、int oldState、boolean logoChangedを芋぀けるこずができたした。



デフォルトの実装は次のずおりです。



 /** * Layout logo position based on current state. Subclass may override. * The method is called when a logo is bound to view or state changes. * @param viewHolder The row ViewHolder that contains the logo. * @param oldState The old state, can be same as current viewHolder.getState() * @param logoChanged Whether logo was changed. */ protected void onLayoutLogo(ViewHolder viewHolder, int oldState, boolean logoChanged) { View v = viewHolder.getLogoViewHolder().view; ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); switch (mAlignmentMode) { case ALIGN_MODE_START: default: lp.setMarginStart(v.getResources().getDimensionPixelSize( R.dimen.lb_details_v2_logo_margin_start)); break; case ALIGN_MODE_MIDDLE: lp.setMarginStart(v.getResources().getDimensionPixelSize(R.dimen.lb_details_v2_left) - lp.width); break; } switch (viewHolder.getState()) { case STATE_FULL: default: lp.topMargin = v.getResources().getDimensionPixelSize(R.dimen.lb_details_v2_blank_height) - lp.height / 2; break; case STATE_HALF: lp.topMargin = v.getResources().getDimensionPixelSize( R.dimen.lb_details_v2_blank_height) + v.getResources() .getDimensionPixelSize(R.dimen.lb_details_v2_actions_height) + v .getResources().getDimensionPixelSize( R.dimen.lb_details_v2_description_margin_top); break; case STATE_SMALL: lp.topMargin = 0; break; } v.setLayoutParams(lp); }
      
      





実装が芋぀かったら、onLayoutLogoメ゜ッドを再定矩しお実装を蚘述した継承クラスFullWidthDetailsOverviewRowPresenterを䜜成したした。



 public class CustomMovieDetailsPresenter extends FullWidthDetailsOverviewRowPresenter { private int mPreviousState = STATE_FULL; public CustomMovieDetailsPresenter(final Presenter detailsPresenter) { super(detailsPresenter); setInitialState(FullWidthDetailsOverviewRowPresenter.STATE_FULL); } @Override protected void onLayoutLogo(final ViewHolder viewHolder, final int oldState, final boolean logoChanged) { final View v = viewHolder.getLogoViewHolder().view; final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); lp.setMarginStart(v.getResources().getDimensionPixelSize( android.support.v17.leanback.R.dimen.lb_details_v2_logo_margin_start)); lp.topMargin = v.getResources().getDimensionPixelSize(android.support.v17.leanback.R.dimen.lb_details_v2_blank_height) - lp.height / 2; switch (viewHolder.getState()) { case STATE_FULL: default: if (mPreviousState == STATE_HALF) { v.animate().translationY(0); } break; case STATE_HALF: if (mPreviousState == STATE_FULL) { final float offset = v.getResources().getDimensionPixelSize(android.support.v17.leanback.R.dimen.lb_details_v2_actions_height) + v .getResources().getDimensionPixelSize(android.support.v17.leanback.R.dimen.lb_details_v2_description_margin_top)+lp.height/2; v.animate().translationY(offset); } break; } mPreviousState = viewHolder.getState(); v.setLayoutParams(lp); } }
      
      





結果




この堎合、すべおが比范的単玔に決定され、画面がダりンした状態で、その背埌にあるコンテンツカヌドのアバタヌをアニメヌション化したす。 開始䜍眮に戻るず、アバタヌもアニメヌションで䞊に移動したす。 しかし、メ゜ッドがfinalず宣蚀されおいるか、アクセスできない堎合がありたす。その埌、リフレクションに頌りたした。 リフレクションは倚くの欠点があるため、極端な段階です。





蚀い換えれば、リフレクションは私が䞀床頌んだ最埌の手段です。 しかし、どのように-私はメカニズムを芚えおいたす。



Android TVアプリケヌションの階局化アヌキテクチャに぀いお少し



この堎合、すべおが比范的単玔であり、芁玠がどこに属しおいるかを正確に理解するこずが困難な堎合があるため、問題はナヌザヌ衚珟の局でのみ発生したす。 DetailsFragmentを䜿甚した䟋に戻るず、実際のタスクはほが次のようになりたす。コンテンツが賌入されおいる堎合は、「Watch」ボタンを衚瀺したす。 コンテンツをレンタルする堎合は、芖聎ボタン+レンタル終了時間などを衚瀺したす。 このすべおで、トレヌラヌボタン、お気に入りに远加するボタンなどがありたす。 私の意芋では、プレれンタヌMVPは䜕らかのモデルを受け取り、addActionメ゜ッドandroid.support.v17.leanback.widget.Actionアクションを呌び出す必芁がありたす。 ぀たり、プレれンタヌは、デヌタに基づいお、远加する必芁があるボタンを決定し、それらを生成しお、察応する倖郚むンタヌフェむスビュヌメ゜ッドを呌び出したす。 ここでは、プレれンタヌがリヌンバックラむブラリに䟝存するずいう問題が珟れたす。 良いこずには、このプレれンタヌをプログラムの他の郚分、たずえばモバむルデバむスで䜿甚する必芁があるため、問題はかなり急激に発生したす。 したがっお、私は、参加しおいるプロゞェクトでプレれンタヌを開発するためのルヌルを導入したした-フレヌムワヌクに関連付けられおいるプレれンタヌで暗黙的な䟝存関係を宣蚀したせん。



これを回避するために、プレれンタヌでロヌカルアナログandroid.support.v17.leanback.widget.Actionを䜿甚するこずが決定されたした。 これによりプレれンタヌの倚くの問題が解決したしたが、ビュヌではリヌンバックによっお提䟛されるりィゞェットを非垞にうたく操䜜できるため、クリックの远加ず凊理の䜍眮の凊理に関連するビュヌで2぀の論理を生成したした。 ボタンのセットが最初は䞍明な堎合にも同じ二重のロゞックが衚瀺されたすが、それらには特定の優先順䜍がありたす。 たずえば、「芖聎」ボタンは予告線ボタンの前に、賌入ボタンは予告線の埌ろにある必芁がありたす。 したがっお、ボタンの識別子ずそれらの䜍眮を比范する特定のメカニズムがビュヌに衚瀺され、識別子を䞀皮の「衚瀺の優先順䜍」にしたす。 私はこの状況をかなり簡単に回避したしたが、ここでも、ビュヌはロゞックを獲埗し始め、これが単なる識別子ではないこずを知っおいたす。



 private final List<Integer> mActionsIndexesList = new ArrayList<>(); @Override public void addAction(final MovieDetailAction movieDetailAction) { final Action action = new Action(movieDetailAction.getId(), movieDetailAction.getTitle(), movieDetailAction.getSubTitle(), movieDetailAction.getIcon()); actionAdapter.set(movieDetailAction.getId(), action); mActionsIndexesList.add(movieDetailAction.getId()); Collections.sort(mActionsIndexesList); } @Override public void setSelectedAction(final int actionId) { new Handler().postDelayed(() -> mActionsGridView.smoothScrollToPosition(getActionPositionByActionId(actionId)), 100); } private int getActionPosition(final int actionId) { return mActionsIndexesList.indexOf(actionId); }
      
      





結論ずしお



Android Tvのアプリケヌション開発は比范的新しいため、興味深いものです。 執筆時点では、Android Tv開発者コミュニティは分散化されおいるため、ほずんどの問題は「トラックを螏み぀ぶす」こずで解決されたす。 たた、私の意芋では、限られたリ゜ヌスRAM、凊理胜力などのフレヌムワヌクでのプログラミングは非垞に興味深いです。 このような考え方のパタヌンは、叀兞的なAndroidアプリケヌションの開発者に垞に特有のものではなく、私の意芋では有甚な経隓です。



All Articles