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.support.v17.leanback.R; 17import android.support.v17.leanback.transition.TransitionHelper; 18import android.support.v17.leanback.widget.BrowseFrameLayout; 19import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter; 20import android.support.v17.leanback.widget.ItemAlignmentFacet; 21import android.support.v17.leanback.widget.ItemBridgeAdapter; 22import android.support.v17.leanback.widget.ObjectAdapter; 23import android.support.v17.leanback.widget.BaseOnItemViewClickedListener; 24import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener; 25import android.support.v17.leanback.widget.Presenter; 26import android.support.v17.leanback.widget.PresenterSelector; 27import android.support.v17.leanback.widget.RowPresenter; 28import android.support.v17.leanback.widget.TitleHelper; 29import android.support.v17.leanback.widget.VerticalGridView; 30import android.os.Bundle; 31import android.util.Log; 32import android.util.TypedValue; 33import android.view.LayoutInflater; 34import android.view.View; 35import android.view.ViewGroup; 36 37/** 38 * A fragment for creating Leanback details screens. 39 * 40 * <p> 41 * A DetailsFragment renders the elements of its {@link ObjectAdapter} as a set 42 * of rows in a vertical list.The Adapter's {@link PresenterSelector} must maintain subclasses 43 * of {@link RowPresenter}. 44 * </p> 45 * 46 * When {@link FullWidthDetailsOverviewRowPresenter} is found in adapter, DetailsFragment will 47 * setup default behavior of the DetailsOverviewRow: 48 * <li> 49 * The alignment of FullWidthDetailsOverviewRowPresenter is setup in 50 * {@link #setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter)}. 51 * </li> 52 * <li> 53 * The view status switching of FullWidthDetailsOverviewRowPresenter is done in 54 * {@link #onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter, 55 * FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int)}. 56 * </li> 57 * 58 * <p> 59 * The recommended activity themes to use with a DetailsFragment are 60 * <li> 61 * {@link android.support.v17.leanback.R.style#Theme_Leanback_Details} with activity 62 * shared element transition for {@link FullWidthDetailsOverviewRowPresenter}. 63 * </li> 64 * <li> 65 * {@link android.support.v17.leanback.R.style#Theme_Leanback_Details_NoSharedElementTransition} 66 * if shared element transition is not needed, for example if first row is not rendered by 67 * {@link FullWidthDetailsOverviewRowPresenter}. 68 * </li> 69 * </p> 70 */ 71public class DetailsFragment extends BaseFragment { 72 static final String TAG = "DetailsFragment"; 73 static boolean DEBUG = false; 74 75 private class SetSelectionRunnable implements Runnable { 76 int mPosition; 77 boolean mSmooth = true; 78 79 SetSelectionRunnable() { 80 } 81 82 @Override 83 public void run() { 84 if (mRowsFragment == null) { 85 return; 86 } 87 mRowsFragment.setSelectedPosition(mPosition, mSmooth); 88 } 89 } 90 91 RowsFragment mRowsFragment; 92 93 private ObjectAdapter mAdapter; 94 private int mContainerListAlignTop; 95 BaseOnItemViewSelectedListener mExternalOnItemViewSelectedListener; 96 private BaseOnItemViewClickedListener mOnItemViewClickedListener; 97 98 private Object mSceneAfterEntranceTransition; 99 100 private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 101 102 private final BaseOnItemViewSelectedListener<Object> mOnItemViewSelectedListener = 103 new BaseOnItemViewSelectedListener<Object>() { 104 @Override 105 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 106 RowPresenter.ViewHolder rowViewHolder, Object row) { 107 int position = mRowsFragment.getVerticalGridView().getSelectedPosition(); 108 int subposition = mRowsFragment.getVerticalGridView().getSelectedSubPosition(); 109 if (DEBUG) Log.v(TAG, "row selected position " + position 110 + " subposition " + subposition); 111 onRowSelected(position, subposition); 112 if (mExternalOnItemViewSelectedListener != null) { 113 mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, 114 rowViewHolder, row); 115 } 116 } 117 }; 118 119 /** 120 * Sets the list of rows for the fragment. 121 */ 122 public void setAdapter(ObjectAdapter adapter) { 123 mAdapter = adapter; 124 Presenter[] presenters = adapter.getPresenterSelector().getPresenters(); 125 if (presenters != null) { 126 for (int i = 0; i < presenters.length; i++) { 127 setupPresenter(presenters[i]); 128 } 129 } else { 130 Log.e(TAG, "PresenterSelector.getPresenters() not implemented"); 131 } 132 if (mRowsFragment != null) { 133 mRowsFragment.setAdapter(adapter); 134 } 135 } 136 137 /** 138 * Returns the list of rows. 139 */ 140 public ObjectAdapter getAdapter() { 141 return mAdapter; 142 } 143 144 /** 145 * Sets an item selection listener. 146 */ 147 public void setOnItemViewSelectedListener(BaseOnItemViewSelectedListener listener) { 148 mExternalOnItemViewSelectedListener = listener; 149 } 150 151 /** 152 * Sets an item clicked listener. 153 */ 154 public void setOnItemViewClickedListener(BaseOnItemViewClickedListener listener) { 155 if (mOnItemViewClickedListener != listener) { 156 mOnItemViewClickedListener = listener; 157 if (mRowsFragment != null) { 158 mRowsFragment.setOnItemViewClickedListener(listener); 159 } 160 } 161 } 162 163 /** 164 * Returns the item clicked listener. 165 */ 166 public BaseOnItemViewClickedListener getOnItemViewClickedListener() { 167 return mOnItemViewClickedListener; 168 } 169 170 @Override 171 public void onCreate(Bundle savedInstanceState) { 172 super.onCreate(savedInstanceState); 173 174 mContainerListAlignTop = 175 getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top); 176 } 177 178 @Override 179 public View onCreateView(LayoutInflater inflater, ViewGroup container, 180 Bundle savedInstanceState) { 181 View view = inflater.inflate(R.layout.lb_details_fragment, container, false); 182 ViewGroup fragment_root = (ViewGroup) view.findViewById(R.id.details_fragment_root); 183 installTitleView(inflater, fragment_root, savedInstanceState); 184 mRowsFragment = (RowsFragment) getChildFragmentManager().findFragmentById( 185 R.id.details_rows_dock); 186 if (mRowsFragment == null) { 187 mRowsFragment = new RowsFragment(); 188 getChildFragmentManager().beginTransaction() 189 .replace(R.id.details_rows_dock, mRowsFragment).commit(); 190 } 191 mRowsFragment.setAdapter(mAdapter); 192 mRowsFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener); 193 mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener); 194 195 mSceneAfterEntranceTransition = TransitionHelper.createScene( 196 (ViewGroup) view, new Runnable() { 197 @Override 198 public void run() { 199 mRowsFragment.setEntranceTransitionState(true); 200 } 201 }); 202 return view; 203 } 204 205 /** 206 * @deprecated override {@link #onInflateTitleView(LayoutInflater,ViewGroup,Bundle)} instead. 207 */ 208 @Deprecated 209 protected View inflateTitle(LayoutInflater inflater, ViewGroup parent, 210 Bundle savedInstanceState) { 211 return super.onInflateTitleView(inflater, parent, savedInstanceState); 212 } 213 214 @Override 215 public View onInflateTitleView(LayoutInflater inflater, ViewGroup parent, 216 Bundle savedInstanceState) { 217 return inflateTitle(inflater, parent, savedInstanceState); 218 } 219 220 void setVerticalGridViewLayout(VerticalGridView listview) { 221 // align the top edge of item to a fixed position 222 listview.setItemAlignmentOffset(-mContainerListAlignTop); 223 listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); 224 listview.setWindowAlignmentOffset(0); 225 listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 226 listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 227 } 228 229 /** 230 * Called to setup each Presenter of Adapter passed in {@link #setAdapter(ObjectAdapter)}. Note 231 * that setup should only change the Presenter behavior that is meaningful in DetailsFragment. For 232 * example how a row is aligned in details Fragment. The default implementation invokes 233 * {@link #setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter)} 234 * 235 */ 236 protected void setupPresenter(Presenter rowPresenter) { 237 if (rowPresenter instanceof FullWidthDetailsOverviewRowPresenter) { 238 setupDetailsOverviewRowPresenter((FullWidthDetailsOverviewRowPresenter) rowPresenter); 239 } 240 } 241 242 /** 243 * Called to setup {@link FullWidthDetailsOverviewRowPresenter}. The default implementation 244 * adds two alignment positions({@link ItemAlignmentFacet}) for ViewHolder of 245 * FullWidthDetailsOverviewRowPresenter to align in fragment. 246 */ 247 protected void setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter presenter) { 248 ItemAlignmentFacet facet = new ItemAlignmentFacet(); 249 // by default align details_frame to half window height 250 ItemAlignmentFacet.ItemAlignmentDef alignDef1 = new ItemAlignmentFacet.ItemAlignmentDef(); 251 alignDef1.setItemAlignmentViewId(R.id.details_frame); 252 alignDef1.setItemAlignmentOffset(- getResources() 253 .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_actions)); 254 alignDef1.setItemAlignmentOffsetPercent(0); 255 // when description is selected, align details_frame to top edge 256 ItemAlignmentFacet.ItemAlignmentDef alignDef2 = new ItemAlignmentFacet.ItemAlignmentDef(); 257 alignDef2.setItemAlignmentViewId(R.id.details_frame); 258 alignDef2.setItemAlignmentFocusViewId(R.id.details_overview_description); 259 alignDef2.setItemAlignmentOffset(- getResources() 260 .getDimensionPixelSize(R.dimen.lb_details_v2_align_pos_for_description)); 261 alignDef2.setItemAlignmentOffsetPercent(0); 262 ItemAlignmentFacet.ItemAlignmentDef[] defs = 263 new ItemAlignmentFacet.ItemAlignmentDef[] {alignDef1, alignDef2}; 264 facet.setAlignmentDefs(defs); 265 presenter.setFacet(ItemAlignmentFacet.class, facet); 266 } 267 268 VerticalGridView getVerticalGridView() { 269 return mRowsFragment == null ? null : mRowsFragment.getVerticalGridView(); 270 } 271 272 /** 273 * Gets embedded RowsFragment showing multiple rows for DetailsFragment. If view of 274 * DetailsFragment is not created, the method returns null. 275 * @return Embedded RowsFragment showing multiple rows for DetailsFragment. 276 */ 277 public RowsFragment getRowsFragment() { 278 return mRowsFragment; 279 } 280 281 /** 282 * Setup dimensions that are only meaningful when the child Fragments are inside 283 * DetailsFragment. 284 */ 285 private void setupChildFragmentLayout() { 286 setVerticalGridViewLayout(mRowsFragment.getVerticalGridView()); 287 } 288 289 private void setupFocusSearchListener() { 290 TitleHelper titleHelper = getTitleHelper(); 291 if (titleHelper != null) { 292 BrowseFrameLayout browseFrameLayout = (BrowseFrameLayout) getView().findViewById( 293 R.id.details_fragment_root); 294 browseFrameLayout.setOnFocusSearchListener(titleHelper.getOnFocusSearchListener()); 295 } 296 } 297 298 /** 299 * Sets the selected row position with smooth animation. 300 */ 301 public void setSelectedPosition(int position) { 302 setSelectedPosition(position, true); 303 } 304 305 /** 306 * Sets the selected row position. 307 */ 308 public void setSelectedPosition(int position, boolean smooth) { 309 mSetSelectionRunnable.mPosition = position; 310 mSetSelectionRunnable.mSmooth = smooth; 311 if (getView() != null && getView().getHandler() != null) { 312 getView().getHandler().post(mSetSelectionRunnable); 313 } 314 } 315 316 void onRowSelected(int selectedPosition, int selectedSubPosition) { 317 ObjectAdapter adapter = getAdapter(); 318 if (adapter == null || adapter.size() == 0 || 319 (selectedPosition == 0 && selectedSubPosition == 0)) { 320 showTitle(true); 321 } else { 322 showTitle(false); 323 } 324 if (adapter != null && adapter.size() > selectedPosition) { 325 final VerticalGridView gridView = getVerticalGridView(); 326 final int count = gridView.getChildCount(); 327 for (int i = 0; i < count; i++) { 328 ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder) 329 gridView.getChildViewHolder(gridView.getChildAt(i)); 330 RowPresenter rowPresenter = (RowPresenter) bridgeViewHolder.getPresenter(); 331 onSetRowStatus(rowPresenter, 332 rowPresenter.getRowViewHolder(bridgeViewHolder.getViewHolder()), 333 bridgeViewHolder.getAdapterPosition(), 334 selectedPosition, selectedSubPosition); 335 } 336 } 337 } 338 339 /** 340 * Called on every visible row to change view status when current selected row position 341 * or selected sub position changed. Subclass may override. The default 342 * implementation calls {@link #onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter, 343 * FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int)} if presenter is 344 * instance of {@link FullWidthDetailsOverviewRowPresenter}. 345 * 346 * @param presenter The presenter used to create row ViewHolder. 347 * @param viewHolder The visible (attached) row ViewHolder, note that it may or may not 348 * be selected. 349 * @param adapterPosition The adapter position of viewHolder inside adapter. 350 * @param selectedPosition The adapter position of currently selected row. 351 * @param selectedSubPosition The sub position within currently selected row. This is used 352 * When a row has multiple alignment positions. 353 */ 354 protected void onSetRowStatus(RowPresenter presenter, RowPresenter.ViewHolder viewHolder, int 355 adapterPosition, int selectedPosition, int selectedSubPosition) { 356 if (presenter instanceof FullWidthDetailsOverviewRowPresenter) { 357 onSetDetailsOverviewRowStatus((FullWidthDetailsOverviewRowPresenter) presenter, 358 (FullWidthDetailsOverviewRowPresenter.ViewHolder) viewHolder, 359 adapterPosition, selectedPosition, selectedSubPosition); 360 } 361 } 362 363 /** 364 * Called to change DetailsOverviewRow view status when current selected row position 365 * or selected sub position changed. Subclass may override. The default 366 * implementation switches between three states based on the positions: 367 * {@link FullWidthDetailsOverviewRowPresenter#STATE_HALF}, 368 * {@link FullWidthDetailsOverviewRowPresenter#STATE_FULL} and 369 * {@link FullWidthDetailsOverviewRowPresenter#STATE_SMALL}. 370 * 371 * @param presenter The presenter used to create row ViewHolder. 372 * @param viewHolder The visible (attached) row ViewHolder, note that it may or may not 373 * be selected. 374 * @param adapterPosition The adapter position of viewHolder inside adapter. 375 * @param selectedPosition The adapter position of currently selected row. 376 * @param selectedSubPosition The sub position within currently selected row. This is used 377 * When a row has multiple alignment positions. 378 */ 379 protected void onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter presenter, 380 FullWidthDetailsOverviewRowPresenter.ViewHolder viewHolder, int adapterPosition, 381 int selectedPosition, int selectedSubPosition) { 382 if (selectedPosition > adapterPosition) { 383 presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_HALF); 384 } else if (selectedPosition == adapterPosition && selectedSubPosition == 1) { 385 presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_HALF); 386 } else if (selectedPosition == adapterPosition && selectedSubPosition == 0){ 387 presenter.setState(viewHolder, FullWidthDetailsOverviewRowPresenter.STATE_FULL); 388 } else { 389 presenter.setState(viewHolder, 390 FullWidthDetailsOverviewRowPresenter.STATE_SMALL); 391 } 392 } 393 394 @Override 395 public void onStart() { 396 super.onStart(); 397 setupChildFragmentLayout(); 398 setupFocusSearchListener(); 399 if (isEntranceTransitionEnabled()) { 400 mRowsFragment.setEntranceTransitionState(false); 401 } 402 } 403 404 @Override 405 protected Object createEntranceTransition() { 406 return TransitionHelper.loadTransition(getActivity(), 407 R.transition.lb_details_enter_transition); 408 } 409 410 @Override 411 protected void runEntranceTransition(Object entranceTransition) { 412 TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition); 413 } 414 415 @Override 416 protected void onEntranceTransitionEnd() { 417 mRowsFragment.onTransitionEnd(); 418 } 419 420 @Override 421 protected void onEntranceTransitionPrepare() { 422 mRowsFragment.onTransitionPrepare(); 423 } 424 425 @Override 426 protected void onEntranceTransitionStart() { 427 mRowsFragment.onTransitionStart(); 428 } 429} 430