1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14package android.support.v17.leanback.app; 15 16import android.app.Activity; 17import android.app.Fragment; 18import android.app.FragmentTransaction; 19import android.graphics.Rect; 20import android.graphics.drawable.Drawable; 21import android.os.Build; 22import android.os.Bundle; 23import android.support.annotation.CallSuper; 24import android.support.v17.leanback.R; 25import android.support.v17.leanback.transition.TransitionHelper; 26import android.support.v17.leanback.transition.TransitionListener; 27import android.support.v17.leanback.util.StateMachine.Event; 28import android.support.v17.leanback.util.StateMachine.State; 29import android.support.v17.leanback.widget.BaseOnItemViewClickedListener; 30import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener; 31import android.support.v17.leanback.widget.BrowseFrameLayout; 32import android.support.v17.leanback.widget.DetailsParallax; 33import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter; 34import android.support.v17.leanback.widget.ItemAlignmentFacet; 35import android.support.v17.leanback.widget.ItemBridgeAdapter; 36import android.support.v17.leanback.widget.ObjectAdapter; 37import android.support.v17.leanback.widget.Presenter; 38import android.support.v17.leanback.widget.PresenterSelector; 39import android.support.v17.leanback.widget.RowPresenter; 40import android.support.v17.leanback.widget.VerticalGridView; 41import android.util.Log; 42import android.view.KeyEvent; 43import android.view.LayoutInflater; 44import android.view.View; 45import android.view.ViewGroup; 46import android.view.Window; 47 48import java.lang.ref.WeakReference; 49 50/** 51 * A fragment for creating Leanback details screens. 52 * 53 * <p> 54 * A DetailsFragment renders the elements of its {@link ObjectAdapter} as a set 55 * of rows in a vertical list.The Adapter's {@link PresenterSelector} must maintain subclasses 56 * of {@link RowPresenter}. 57 * </p> 58 * 59 * When {@link FullWidthDetailsOverviewRowPresenter} is found in adapter, DetailsFragment will 60 * setup default behavior of the DetailsOverviewRow: 61 * <li> 62 * The alignment of FullWidthDetailsOverviewRowPresenter is setup in 63 * {@link #setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter)}. 64 * </li> 65 * <li> 66 * The view status switching of FullWidthDetailsOverviewRowPresenter is done in 67 * {@link #onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter, 68 * FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int)}. 69 * </li> 70 * 71 * <p> 72 * The recommended activity themes to use with a DetailsFragment are 73 * <li> 74 * {@link android.support.v17.leanback.R.style#Theme_Leanback_Details} with activity 75 * shared element transition for {@link FullWidthDetailsOverviewRowPresenter}. 76 * </li> 77 * <li> 78 * {@link android.support.v17.leanback.R.style#Theme_Leanback_Details_NoSharedElementTransition} 79 * if shared element transition is not needed, for example if first row is not rendered by 80 * {@link FullWidthDetailsOverviewRowPresenter}. 81 * </li> 82 * </p> 83 * 84 * <p> 85 * DetailsFragment can use {@link DetailsFragmentBackgroundController} to add a parallax drawable 86 * background and embedded video playing fragment. 87 * </p> 88 */ 89public class DetailsFragment extends BaseFragment { 90 static final String TAG = "DetailsFragment"; 91 static boolean DEBUG = false; 92 93 final State STATE_SET_ENTRANCE_START_STATE = new State("STATE_SET_ENTRANCE_START_STATE") { 94 @Override 95 public void run() { 96 mRowsFragment.setEntranceTransitionState(false); 97 } 98 }; 99 100 final State STATE_ENTER_TRANSITION_INIT = new State("STATE_ENTER_TRANSIITON_INIT"); 101 102 void switchToVideoBeforeVideoFragmentCreated() { 103 // if the video fragment is not ready: immediately fade out covering drawable, 104 // hide title and mark mPendingFocusOnVideo and set focus on it later. 105 mDetailsBackgroundController.switchToVideoBeforeCreate(); 106 showTitle(false); 107 mPendingFocusOnVideo = true; 108 slideOutGridView(); 109 } 110 111 final State STATE_SWITCH_TO_VIDEO_IN_ON_CREATE = new State("STATE_SWITCH_TO_VIDEO_IN_ON_CREATE", 112 false, false) { 113 @Override 114 public void run() { 115 switchToVideoBeforeVideoFragmentCreated(); 116 } 117 }; 118 119 final State STATE_ENTER_TRANSITION_CANCEL = new State("STATE_ENTER_TRANSITION_CANCEL", 120 false, false) { 121 @Override 122 public void run() { 123 if (mWaitEnterTransitionTimeout != null) { 124 mWaitEnterTransitionTimeout.mRef.clear(); 125 } 126 // clear the activity enter/sharedElement transition, return transitions are kept. 127 // keep the return transitions and clear enter transition 128 if (getActivity() != null) { 129 Window window = getActivity().getWindow(); 130 Object returnTransition = TransitionHelper.getReturnTransition(window); 131 Object sharedReturnTransition = TransitionHelper 132 .getSharedElementReturnTransition(window); 133 TransitionHelper.setEnterTransition(window, null); 134 TransitionHelper.setSharedElementEnterTransition(window, null); 135 TransitionHelper.setReturnTransition(window, returnTransition); 136 TransitionHelper.setSharedElementReturnTransition(window, sharedReturnTransition); 137 } 138 } 139 }; 140 141 final State STATE_ENTER_TRANSITION_COMPLETE = new State("STATE_ENTER_TRANSIITON_COMPLETE", 142 true, false); 143 144 final State STATE_ENTER_TRANSITION_ADDLISTENER = new State("STATE_ENTER_TRANSITION_PENDING") { 145 @Override 146 public void run() { 147 Object transition = TransitionHelper.getEnterTransition(getActivity().getWindow()); 148 TransitionHelper.addTransitionListener(transition, mEnterTransitionListener); 149 } 150 }; 151 152 final State STATE_ENTER_TRANSITION_PENDING = new State("STATE_ENTER_TRANSITION_PENDING") { 153 @Override 154 public void run() { 155 if (mWaitEnterTransitionTimeout == null) { 156 new WaitEnterTransitionTimeout(DetailsFragment.this); 157 } 158 } 159 }; 160 161 /** 162 * Start this task when first DetailsOverviewRow is created, if there is no entrance transition 163 * started, it will clear PF_ENTRANCE_TRANSITION_PENDING. 164 */ 165 static class WaitEnterTransitionTimeout implements Runnable { 166 static final long WAIT_ENTERTRANSITION_START = 200; 167 168 final WeakReference<DetailsFragment> mRef; 169 170 WaitEnterTransitionTimeout(DetailsFragment f) { 171 mRef = new WeakReference<>(f); 172 f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START); 173 } 174 175 @Override 176 public void run() { 177 DetailsFragment f = mRef.get(); 178 if (f != null) { 179 f.mStateMachine.fireEvent(f.EVT_ENTER_TRANSIITON_DONE); 180 } 181 } 182 } 183 184 final State STATE_ON_SAFE_START = new State("STATE_ON_SAFE_START") { 185 @Override 186 public void run() { 187 onSafeStart(); 188 } 189 }; 190 191 final Event EVT_ONSTART = new Event("onStart"); 192 193 final Event EVT_NO_ENTER_TRANSITION = new Event("EVT_NO_ENTER_TRANSITION"); 194 195 final Event EVT_DETAILS_ROW_LOADED = new Event("onFirstRowLoaded"); 196 197 final Event EVT_ENTER_TRANSIITON_DONE = new Event("onEnterTransitionDone"); 198 199 final Event EVT_SWITCH_TO_VIDEO = new Event("switchToVideo"); 200 201 @Override 202 void createStateMachineStates() { 203 super.createStateMachineStates(); 204 mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE); 205 mStateMachine.addState(STATE_ON_SAFE_START); 206 mStateMachine.addState(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE); 207 mStateMachine.addState(STATE_ENTER_TRANSITION_INIT); 208 mStateMachine.addState(STATE_ENTER_TRANSITION_ADDLISTENER); 209 mStateMachine.addState(STATE_ENTER_TRANSITION_CANCEL); 210 mStateMachine.addState(STATE_ENTER_TRANSITION_PENDING); 211 mStateMachine.addState(STATE_ENTER_TRANSITION_COMPLETE); 212 } 213 214 @Override 215 void createStateMachineTransitions() { 216 super.createStateMachineTransitions(); 217 /** 218 * Part 1: Processing enter transitions after fragment.onCreate 219 */ 220 mStateMachine.addTransition(STATE_START, STATE_ENTER_TRANSITION_INIT, EVT_ON_CREATE); 221 // if transition is not supported, skip to complete 222 mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE, 223 COND_TRANSITION_NOT_SUPPORTED); 224 // if transition is not set on Activity, skip to complete 225 mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE, 226 EVT_NO_ENTER_TRANSITION); 227 // if switchToVideo is called before EVT_ON_CREATEVIEW, clear enter transition and skip to 228 // complete. 229 mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_CANCEL, 230 EVT_SWITCH_TO_VIDEO); 231 mStateMachine.addTransition(STATE_ENTER_TRANSITION_CANCEL, STATE_ENTER_TRANSITION_COMPLETE); 232 // once after onCreateView, we cannot skip the enter transition, add a listener and wait 233 // it to finish 234 mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_ADDLISTENER, 235 EVT_ON_CREATEVIEW); 236 // when enter transition finishes, go to complete, however this might never happen if 237 // the activity is not giving transition options in startActivity, there is no API to query 238 // if this activity is started in a enter transition mode. So we rely on a timer below: 239 mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER, 240 STATE_ENTER_TRANSITION_COMPLETE, EVT_ENTER_TRANSIITON_DONE); 241 // we are expecting app to start delayed enter transition shortly after details row is 242 // loaded, so create a timer and wait for enter transition start. 243 mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER, 244 STATE_ENTER_TRANSITION_PENDING, EVT_DETAILS_ROW_LOADED); 245 // if enter transition not started in the timer, skip to DONE, this can be also true when 246 // startActivity is not giving transition option. 247 mStateMachine.addTransition(STATE_ENTER_TRANSITION_PENDING, STATE_ENTER_TRANSITION_COMPLETE, 248 EVT_ENTER_TRANSIITON_DONE); 249 250 /** 251 * Part 2: modification to the entrance transition defined in BaseFragment 252 */ 253 // Must finish enter transition before perform entrance transition. 254 mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ENTRANCE_PERFORM); 255 // Calling switch to video would hide immediately and skip entrance transition 256 mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE, 257 EVT_SWITCH_TO_VIDEO); 258 mStateMachine.addTransition(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE, STATE_ENTRANCE_COMPLETE); 259 // if the entrance transition is skipped to complete by COND_TRANSITION_NOT_SUPPORTED, we 260 // still need to do the switchToVideo. 261 mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE, 262 EVT_SWITCH_TO_VIDEO); 263 264 // for once the view is created in onStart and prepareEntranceTransition was called, we 265 // could setEntranceStartState: 266 mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, 267 STATE_SET_ENTRANCE_START_STATE, EVT_ONSTART); 268 269 /** 270 * Part 3: onSafeStart() 271 */ 272 // for onSafeStart: the condition is onStart called, entrance transition complete 273 mStateMachine.addTransition(STATE_START, STATE_ON_SAFE_START, EVT_ONSTART); 274 mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_ON_SAFE_START); 275 mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ON_SAFE_START); 276 } 277 278 private class SetSelectionRunnable implements Runnable { 279 int mPosition; 280 boolean mSmooth = true; 281 282 SetSelectionRunnable() { 283 } 284 285 @Override 286 public void run() { 287 if (mRowsFragment == null) { 288 return; 289 } 290 mRowsFragment.setSelectedPosition(mPosition, mSmooth); 291 } 292 } 293 294 TransitionListener mEnterTransitionListener = new TransitionListener() { 295 @Override 296 public void onTransitionStart(Object transition) { 297 if (mWaitEnterTransitionTimeout != null) { 298 // cancel task of WaitEnterTransitionTimeout, we will clearPendingEnterTransition 299 // when transition finishes. 300 mWaitEnterTransitionTimeout.mRef.clear(); 301 } 302 } 303 304 @Override 305 public void onTransitionCancel(Object transition) { 306 mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE); 307 } 308 309 @Override 310 public void onTransitionEnd(Object transition) { 311 mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE); 312 } 313 }; 314 315 TransitionListener mReturnTransitionListener = new TransitionListener() { 316 @Override 317 public void onTransitionStart(Object transition) { 318 onReturnTransitionStart(); 319 } 320 }; 321 322 BrowseFrameLayout mRootView; 323 View mBackgroundView; 324 Drawable mBackgroundDrawable; 325 Fragment mVideoFragment; 326 DetailsParallax mDetailsParallax; 327 RowsFragment mRowsFragment; 328 ObjectAdapter mAdapter; 329 int mContainerListAlignTop; 330 BaseOnItemViewSelectedListener mExternalOnItemViewSelectedListener; 331 BaseOnItemViewClickedListener mOnItemViewClickedListener; 332 DetailsFragmentBackgroundController mDetailsBackgroundController; 333 334 // A temporarily flag when switchToVideo() is called in onCreate(), if mPendingFocusOnVideo is 335 // true, we will focus to VideoFragment immediately after video fragment's view is created. 336 boolean mPendingFocusOnVideo = false; 337 338 WaitEnterTransitionTimeout mWaitEnterTransitionTimeout; 339 340 Object mSceneAfterEntranceTransition; 341 342 final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 343 344 final BaseOnItemViewSelectedListener<Object> mOnItemViewSelectedListener = 345 new BaseOnItemViewSelectedListener<Object>() { 346 @Override 347 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 348 RowPresenter.ViewHolder rowViewHolder, Object row) { 349 int position = mRowsFragment.getVerticalGridView().getSelectedPosition(); 350 int subposition = mRowsFragment.getVerticalGridView().getSelectedSubPosition(); 351 if (DEBUG) Log.v(TAG, "row selected position " + position 352 + " subposition " + subposition); 353 onRowSelected(position, subposition); 354 if (mExternalOnItemViewSelectedListener != null) { 355 mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, 356 rowViewHolder, row); 357 } 358 } 359 }; 360 361 /** 362 * Sets the list of rows for the fragment. 363 */ 364 public void setAdapter(ObjectAdapter adapter) { 365 mAdapter = adapter; 366 Presenter[] presenters = adapter.getPresenterSelector().getPresenters(); 367 if (presenters != null) { 368 for (int i = 0; i < presenters.length; i++) { 369 setupPresenter(presenters[i]); 370 } 371 } else { 372 Log.e(TAG, "PresenterSelector.getPresenters() not implemented"); 373 } 374 if (mRowsFragment != null) { 375 mRowsFragment.setAdapter(adapter); 376 } 377 } 378 379 /** 380 * Returns the list of rows. 381 */ 382 public ObjectAdapter getAdapter() { 383 return mAdapter; 384 } 385 386 /** 387 * Sets an item selection listener. 388 */ 389 public void setOnItemViewSelectedListener(BaseOnItemViewSelectedListener listener) { 390 mExternalOnItemViewSelectedListener = listener; 391 } 392 393 /** 394 * Sets an item clicked listener. 395 */ 396 public void setOnItemViewClickedListener(BaseOnItemViewClickedListener listener) { 397 if (mOnItemViewClickedListener != listener) { 398 mOnItemViewClickedListener = listener; 399 if (mRowsFragment != null) { 400 mRowsFragment.setOnItemViewClickedListener(listener); 401 } 402 } 403 } 404 405 /** 406 * Returns the item clicked listener. 407 */ 408 public BaseOnItemViewClickedListener getOnItemViewClickedListener() { 409 return mOnItemViewClickedListener; 410 } 411 412 @Override 413 public void onCreate(Bundle savedInstanceState) { 414 super.onCreate(savedInstanceState); 415 mContainerListAlignTop = 416 getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top); 417 418 Activity activity = getActivity(); 419 if (activity != null) { 420 Object transition = TransitionHelper.getEnterTransition(activity.getWindow()); 421 if (transition == null) { 422 mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION); 423 } 424 transition = TransitionHelper.getReturnTransition(activity.getWindow()); 425 if (transition != null) { 426 TransitionHelper.addTransitionListener(transition, mReturnTransitionListener); 427 } 428 } else { 429 mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION); 430 } 431 } 432 433 @Override 434 public View onCreateView(LayoutInflater inflater, ViewGroup container, 435 Bundle savedInstanceState) { 436 mRootView = (BrowseFrameLayout) inflater.inflate( 437 R.layout.lb_details_fragment, container, false); 438 mBackgroundView = mRootView.findViewById(R.id.details_background_view); 439 if (mBackgroundView != null) { 440 mBackgroundView.setBackground(mBackgroundDrawable); 441 } 442 mRowsFragment = (RowsFragment) getChildFragmentManager().findFragmentById( 443 R.id.details_rows_dock); 444 if (mRowsFragment == null) { 445 mRowsFragment = new RowsFragment(); 446 getChildFragmentManager().beginTransaction() 447 .replace(R.id.details_rows_dock, mRowsFragment).commit(); 448 } 449 installTitleView(inflater, mRootView, savedInstanceState); 450 mRowsFragment.setAdapter(mAdapter); 451 mRowsFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener); 452 mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener); 453 454 mSceneAfterEntranceTransition = TransitionHelper.createScene(mRootView, new Runnable() { 455 @Override 456 public void run() { 457 mRowsFragment.setEntranceTransitionState(true); 458 } 459 }); 460 461 setupDpadNavigation(); 462 463 if (Build.VERSION.SDK_INT >= 21) { 464 // Setup adapter listener to work with ParallaxTransition (>= API 21). 465 mRowsFragment.setExternalAdapterListener(new ItemBridgeAdapter.AdapterListener() { 466 @Override 467 public void onCreate(ItemBridgeAdapter.ViewHolder vh) { 468 if (mDetailsParallax != null && vh.getViewHolder() 469 instanceof FullWidthDetailsOverviewRowPresenter.ViewHolder) { 470 FullWidthDetailsOverviewRowPresenter.ViewHolder rowVh = 471 (FullWidthDetailsOverviewRowPresenter.ViewHolder) 472 vh.getViewHolder(); 473 rowVh.getOverviewView().setTag(R.id.lb_parallax_source, 474 mDetailsParallax); 475 } 476 } 477 }); 478 } 479 return mRootView; 480 } 481 482 /** 483 * @deprecated override {@link #onInflateTitleView(LayoutInflater,ViewGroup,Bundle)} instead. 484 */ 485 @Deprecated 486 protected View inflateTitle(LayoutInflater inflater, ViewGroup parent, 487 Bundle savedInstanceState) { 488 return super.onInflateTitleView(inflater, parent, savedInstanceState); 489 } 490 491 @Override 492 public View onInflateTitleView(LayoutInflater inflater, ViewGroup parent, 493 Bundle savedInstanceState) { 494 return inflateTitle(inflater, parent, savedInstanceState); 495 } 496 497 void setVerticalGridViewLayout(VerticalGridView listview) { 498 // align the top edge of item to a fixed position 499 listview.setItemAlignmentOffset(-mContainerListAlignTop); 500 listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); 501 listview.setWindowAlignmentOffset(0); 502 listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 503 listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 504 } 505 506 /** 507 * Called to setup each Presenter of Adapter passed in {@link #setAdapter(ObjectAdapter)}.Note 508 * that setup should only change the Presenter behavior that is meaningful in DetailsFragment. 509 * For example how a row is aligned in details Fragment. The default implementation invokes 510 * {@link #setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter)} 511 * 512 */ 513 protected void setupPresenter(Presenter rowPresenter) { 514 if (rowPresenter instanceof FullWidthDetailsOverviewRowPresenter) { 515 setupDetailsOverviewRowPresenter((FullWidthDetailsOverviewRowPresenter) rowPresenter); 516 } 517 } 518 519 /** 520 * Called to setup {@link FullWidthDetailsOverviewRowPresenter}. The default implementation 521 * adds two alignment positions({@link ItemAlignmentFacet}) for ViewHolder of 522 * FullWidthDetailsOverviewRowPresenter to align in fragment. 523 */ 524 protected void setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter presenter) { 525 ItemAlignmentFacet facet = new ItemAlignmentFacet(); 526 // by default align details_frame to half window height 527 ItemAlignmentFacet.ItemAlignmentDef alignDef1 = new ItemAlignmentFacet.ItemAlignmentDef(); 528 alignDef1.setItemAlignmentViewId(R.id.details_frame); 529 alignDef1.setItemAlignmentOffset(- getResources() 530 .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_actions)); 531 alignDef1.setItemAlignmentOffsetPercent(0); 532 // when description is selected, align details_frame to top edge 533 ItemAlignmentFacet.ItemAlignmentDef alignDef2 = new ItemAlignmentFacet.ItemAlignmentDef(); 534 alignDef2.setItemAlignmentViewId(R.id.details_frame); 535 alignDef2.setItemAlignmentFocusViewId(R.id.details_overview_description); 536 alignDef2.setItemAlignmentOffset(- getResources() 537 .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_description)); 538 alignDef2.setItemAlignmentOffsetPercent(0); 539 ItemAlignmentFacet.ItemAlignmentDef[] defs = 540 new ItemAlignmentFacet.ItemAlignmentDef[] {alignDef1, alignDef2}; 541 facet.setAlignmentDefs(defs); 542 presenter.setFacet(ItemAlignmentFacet.class, facet); 543 } 544 545 VerticalGridView getVerticalGridView() { 546 return mRowsFragment == null ? null : mRowsFragment.getVerticalGridView(); 547 } 548 549 /** 550 * Gets embedded RowsFragment showing multiple rows for DetailsFragment. If view of 551 * DetailsFragment is not created, the method returns null. 552 * @return Embedded RowsFragment showing multiple rows for DetailsFragment. 553 */ 554 public RowsFragment getRowsFragment() { 555 return mRowsFragment; 556 } 557 558 /** 559 * Setup dimensions that are only meaningful when the child Fragments are inside 560 * DetailsFragment. 561 */ 562 private void setupChildFragmentLayout() { 563 setVerticalGridViewLayout(mRowsFragment.getVerticalGridView()); 564 } 565 566 /** 567 * Sets the selected row position with smooth animation. 568 */ 569 public void setSelectedPosition(int position) { 570 setSelectedPosition(position, true); 571 } 572 573 /** 574 * Sets the selected row position. 575 */ 576 public void setSelectedPosition(int position, boolean smooth) { 577 mSetSelectionRunnable.mPosition = position; 578 mSetSelectionRunnable.mSmooth = smooth; 579 if (getView() != null && getView().getHandler() != null) { 580 getView().getHandler().post(mSetSelectionRunnable); 581 } 582 } 583 584 void switchToVideo() { 585 if (mVideoFragment != null && mVideoFragment.getView() != null) { 586 mVideoFragment.getView().requestFocus(); 587 } else { 588 mStateMachine.fireEvent(EVT_SWITCH_TO_VIDEO); 589 } 590 } 591 592 void switchToRows() { 593 mPendingFocusOnVideo = false; 594 VerticalGridView verticalGridView = getVerticalGridView(); 595 if (verticalGridView != null && verticalGridView.getChildCount() > 0) { 596 verticalGridView.requestFocus(); 597 } 598 } 599 600 /** 601 * This method asks DetailsFragmentBackgroundController to add a fragment for rendering video. 602 * In case the fragment is already there, it will return the existing one. The method must be 603 * called after calling super.onCreate(). App usually does not call this method directly. 604 * 605 * @return Fragment the added or restored fragment responsible for rendering video. 606 * @see DetailsFragmentBackgroundController#onCreateVideoFragment() 607 */ 608 final Fragment findOrCreateVideoFragment() { 609 if (mVideoFragment != null) { 610 return mVideoFragment; 611 } 612 Fragment fragment = getChildFragmentManager() 613 .findFragmentById(R.id.video_surface_container); 614 if (fragment == null && mDetailsBackgroundController != null) { 615 FragmentTransaction ft2 = getChildFragmentManager().beginTransaction(); 616 ft2.add(android.support.v17.leanback.R.id.video_surface_container, 617 fragment = mDetailsBackgroundController.onCreateVideoFragment()); 618 ft2.commit(); 619 if (mPendingFocusOnVideo) { 620 // wait next cycle for Fragment view created so we can focus on it. 621 // This is a bit hack eventually we will do commitNow() which get view immediately. 622 getView().post(new Runnable() { 623 @Override 624 public void run() { 625 if (getView() != null) { 626 switchToVideo(); 627 } 628 mPendingFocusOnVideo = false; 629 } 630 }); 631 } 632 } 633 mVideoFragment = fragment; 634 return mVideoFragment; 635 } 636 637 void onRowSelected(int selectedPosition, int selectedSubPosition) { 638 ObjectAdapter adapter = getAdapter(); 639 if (( mRowsFragment != null && mRowsFragment.getView() != null 640 && mRowsFragment.getView().hasFocus() && !mPendingFocusOnVideo) 641 && (adapter == null || adapter.size() == 0 642 || (getVerticalGridView().getSelectedPosition() == 0 643 && getVerticalGridView().getSelectedSubPosition() == 0))) { 644 showTitle(true); 645 } else { 646 showTitle(false); 647 } 648 if (adapter != null && adapter.size() > selectedPosition) { 649 final VerticalGridView gridView = getVerticalGridView(); 650 final int count = gridView.getChildCount(); 651 if (count > 0) { 652 mStateMachine.fireEvent(EVT_DETAILS_ROW_LOADED); 653 } 654 for (int i = 0; i < count; i++) { 655 ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder) 656 gridView.getChildViewHolder(gridView.getChildAt(i)); 657 RowPresenter rowPresenter = (RowPresenter) bridgeViewHolder.getPresenter(); 658 onSetRowStatus(rowPresenter, 659 rowPresenter.getRowViewHolder(bridgeViewHolder.getViewHolder()), 660 bridgeViewHolder.getAdapterPosition(), 661 selectedPosition, selectedSubPosition); 662 } 663 } 664 } 665 666 /** 667 * Called when onStart and enter transition (postponed/none postponed) and entrance transition 668 * are all finished. 669 */ 670 @CallSuper 671 void onSafeStart() { 672 if (mDetailsBackgroundController != null) { 673 mDetailsBackgroundController.onStart(); 674 } 675 } 676 677 @CallSuper 678 void onReturnTransitionStart() { 679 if (mDetailsBackgroundController != null) { 680 // first disable parallax effect that auto-start PlaybackGlue. 681 boolean isVideoVisible = mDetailsBackgroundController.disableVideoParallax(); 682 // if video is not visible we can safely remove VideoFragment, 683 // otherwise let video playing during return transition. 684 if (!isVideoVisible && mVideoFragment != null) { 685 FragmentTransaction ft2 = getChildFragmentManager().beginTransaction(); 686 ft2.remove(mVideoFragment); 687 ft2.commit(); 688 mVideoFragment = null; 689 } 690 } 691 } 692 693 @Override 694 public void onStop() { 695 if (mDetailsBackgroundController != null) { 696 mDetailsBackgroundController.onStop(); 697 } 698 super.onStop(); 699 } 700 701 /** 702 * Called on every visible row to change view status when current selected row position 703 * or selected sub position changed. Subclass may override. The default 704 * implementation calls {@link #onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter, 705 * FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int)} if presenter is 706 * instance of {@link FullWidthDetailsOverviewRowPresenter}. 707 * 708 * @param presenter The presenter used to create row ViewHolder. 709 * @param viewHolder The visible (attached) row ViewHolder, note that it may or may not 710 * be selected. 711 * @param adapterPosition The adapter position of viewHolder inside adapter. 712 * @param selectedPosition The adapter position of currently selected row. 713 * @param selectedSubPosition The sub position within currently selected row. This is used 714 * When a row has multiple alignment positions. 715 */ 716 protected void onSetRowStatus(RowPresenter presenter, RowPresenter.ViewHolder viewHolder, int 717 adapterPosition, int selectedPosition, int selectedSubPosition) { 718 if (presenter instanceof FullWidthDetailsOverviewRowPresenter) { 719 onSetDetailsOverviewRowStatus((FullWidthDetailsOverviewRowPresenter) presenter, 720 (FullWidthDetailsOverviewRowPresenter.ViewHolder) viewHolder, 721 adapterPosition, selectedPosition, selectedSubPosition); 722 } 723 } 724 725 /** 726 * Called to change DetailsOverviewRow view status when current selected row position 727 * or selected sub position changed. Subclass may override. The default 728 * implementation switches between three states based on the positions: 729 * {@link FullWidthDetailsOverviewRowPresenter#STATE_HALF}, 730 * {@link FullWidthDetailsOverviewRowPresenter#STATE_FULL} and 731 * {@link FullWidthDetailsOverviewRowPresenter#STATE_SMALL}. 732 * 733 * @param presenter The presenter used to create row ViewHolder. 734 * @param viewHolder The visible (attached) row ViewHolder, note that it may or may not 735 * be selected. 736 * @param adapterPosition The adapter position of viewHolder inside adapter. 737 * @param selectedPosition The adapter position of currently selected row. 738 * @param selectedSubPosition The sub position within currently selected row. This is used 739 * When a row has multiple alignment positions. 740 */ 741 protected void onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter presenter, 742 FullWidthDetailsOverviewRowPresenter.ViewHolder viewHolder, int adapterPosition, 743 int selectedPosition, int selectedSubPosition) { 744 if (selectedPosition > adapterPosition) { 745 presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_HALF); 746 } else if (selectedPosition == adapterPosition && selectedSubPosition == 1) { 747 presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_HALF); 748 } else if (selectedPosition == adapterPosition && selectedSubPosition == 0){ 749 presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_FULL); 750 } else { 751 presenter.setState(viewHolder, 752 FullWidthDetailsOverviewRowPresenter.STATE_SMALL); 753 } 754 } 755 756 @Override 757 public void onStart() { 758 super.onStart(); 759 760 setupChildFragmentLayout(); 761 mStateMachine.fireEvent(EVT_ONSTART); 762 if (mDetailsParallax != null) { 763 mDetailsParallax.setRecyclerView(mRowsFragment.getVerticalGridView()); 764 } 765 if (mPendingFocusOnVideo) { 766 slideOutGridView(); 767 } else if (!getView().hasFocus()) { 768 mRowsFragment.getVerticalGridView().requestFocus(); 769 } 770 } 771 772 @Override 773 protected Object createEntranceTransition() { 774 return TransitionHelper.loadTransition(FragmentUtil.getContext(this), 775 R.transition.lb_details_enter_transition); 776 } 777 778 @Override 779 protected void runEntranceTransition(Object entranceTransition) { 780 TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition); 781 } 782 783 @Override 784 protected void onEntranceTransitionEnd() { 785 mRowsFragment.onTransitionEnd(); 786 } 787 788 @Override 789 protected void onEntranceTransitionPrepare() { 790 mRowsFragment.onTransitionPrepare(); 791 } 792 793 @Override 794 protected void onEntranceTransitionStart() { 795 mRowsFragment.onTransitionStart(); 796 } 797 798 /** 799 * Returns the {@link DetailsParallax} instance used by 800 * {@link DetailsFragmentBackgroundController} to configure parallax effect of background and 801 * control embedded video playback. App usually does not use this method directly. 802 * App may use this method for other custom parallax tasks. 803 * 804 * @return The DetailsParallax instance attached to the DetailsFragment. 805 */ 806 public DetailsParallax getParallax() { 807 if (mDetailsParallax == null) { 808 mDetailsParallax = new DetailsParallax(); 809 if (mRowsFragment != null && mRowsFragment.getView() != null) { 810 mDetailsParallax.setRecyclerView(mRowsFragment.getVerticalGridView()); 811 } 812 } 813 return mDetailsParallax; 814 } 815 816 /** 817 * Set background drawable shown below foreground rows UI and above 818 * {@link #findOrCreateVideoFragment()}. 819 * 820 * @see DetailsFragmentBackgroundController 821 */ 822 void setBackgroundDrawable(Drawable drawable) { 823 if (mBackgroundView != null) { 824 mBackgroundView.setBackground(drawable); 825 } 826 mBackgroundDrawable = drawable; 827 } 828 829 /** 830 * This method does the following 831 * <ul> 832 * <li>sets up focus search handling logic in the root view to enable transitioning between 833 * half screen/full screen/no video mode.</li> 834 * 835 * <li>Sets up the key listener in the root view to intercept events like UP/DOWN and 836 * transition to appropriate mode like half/full screen video.</li> 837 * </ul> 838 */ 839 void setupDpadNavigation() { 840 mRootView.setOnChildFocusListener(new BrowseFrameLayout.OnChildFocusListener() { 841 842 @Override 843 public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 844 return false; 845 } 846 847 @Override 848 public void onRequestChildFocus(View child, View focused) { 849 if (child != mRootView.getFocusedChild()) { 850 if (child.getId() == R.id.details_fragment_root) { 851 if (!mPendingFocusOnVideo) { 852 slideInGridView(); 853 showTitle(true); 854 } 855 } else if (child.getId() == R.id.video_surface_container) { 856 slideOutGridView(); 857 showTitle(false); 858 } else { 859 showTitle(true); 860 } 861 } 862 } 863 }); 864 mRootView.setOnFocusSearchListener(new BrowseFrameLayout.OnFocusSearchListener() { 865 @Override 866 public View onFocusSearch(View focused, int direction) { 867 if (mRowsFragment.getVerticalGridView() != null 868 && mRowsFragment.getVerticalGridView().hasFocus()) { 869 if (direction == View.FOCUS_UP) { 870 if (mDetailsBackgroundController != null 871 && mDetailsBackgroundController.canNavigateToVideoFragment() 872 && mVideoFragment != null && mVideoFragment.getView() != null) { 873 return mVideoFragment.getView(); 874 } else if (getTitleView() != null && getTitleView().hasFocusable()) { 875 return getTitleView(); 876 } 877 } 878 } else if (getTitleView() != null && getTitleView().hasFocus()) { 879 if (direction == View.FOCUS_DOWN) { 880 if (mRowsFragment.getVerticalGridView() != null) { 881 return mRowsFragment.getVerticalGridView(); 882 } 883 } 884 } 885 return focused; 886 } 887 }); 888 889 // If we press BACK on remote while in full screen video mode, we should 890 // transition back to half screen video playback mode. 891 mRootView.setOnDispatchKeyListener(new View.OnKeyListener() { 892 @Override 893 public boolean onKey(View v, int keyCode, KeyEvent event) { 894 // This is used to check if we are in full screen video mode. This is somewhat 895 // hacky and relies on the behavior of the video helper class to update the 896 // focusability of the video surface view. 897 if (mVideoFragment != null && mVideoFragment.getView() != null 898 && mVideoFragment.getView().hasFocus()) { 899 if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { 900 if (getVerticalGridView().getChildCount() > 0) { 901 getVerticalGridView().requestFocus(); 902 return true; 903 } 904 } 905 } 906 907 return false; 908 } 909 }); 910 } 911 912 /** 913 * Slides vertical grid view (displaying media item details) out of the screen from below. 914 */ 915 void slideOutGridView() { 916 if (getVerticalGridView() != null) { 917 getVerticalGridView().animateOut(); 918 } 919 } 920 921 void slideInGridView() { 922 if (getVerticalGridView() != null) { 923 getVerticalGridView().animateIn(); 924 } 925 } 926} 927