BrowseFragment.java revision 9e8e482b545c9c7d8db82c05993850d2f9038c5b
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.widget.Presenter; 18import android.support.v17.leanback.widget.VerticalGridView; 19import android.support.v17.leanback.widget.Row; 20import android.support.v17.leanback.widget.ObjectAdapter; 21import android.support.v17.leanback.widget.OnItemSelectedListener; 22import android.support.v17.leanback.widget.OnItemClickedListener; 23import android.support.v17.leanback.widget.SearchOrbView; 24import android.util.Log; 25import android.app.Fragment; 26import android.os.Bundle; 27import android.view.LayoutInflater; 28import android.view.View; 29import android.view.ViewGroup; 30import android.view.ViewGroup.MarginLayoutParams; 31import android.widget.ImageView; 32import android.widget.TextView; 33import android.graphics.drawable.Drawable; 34 35import static android.support.v7.widget.RecyclerView.NO_POSITION; 36 37/** 38 * Wrapper fragment for leanback browse screens. Composed of a 39 * RowsFragment and a HeadersFragment. 40 * 41 */ 42public class BrowseFragment extends Fragment { 43 private static final String TAG = "BrowseFragment"; 44 private static boolean DEBUG = false; 45 46 /** The fastlane navigation panel is enabled and shown by default. */ 47 public static final int HEADERS_ENABLED = 1; 48 49 /** The fastlane navigation panel is enabled and hidden by default. */ 50 public static final int HEADERS_HIDDEN = 2; 51 52 /** The fastlane navigation panel is disabled and will never be shown. */ 53 public static final int HEADERS_DISABLED = 3; 54 55 private final RowsFragment mRowsFragment = new RowsFragment(); 56 private final HeadersFragment mHeadersFragment = new HeadersFragment(); 57 58 private Params mParams; 59 private BrowseFrameLayout mBrowseFrame; 60 private ImageView mBadgeView; 61 private TextView mTitleView; 62 private ViewGroup mBrowseTitle; 63 private SearchOrbView mSearchOrbView; 64 private boolean mShowingTitle = true; 65 private boolean mShowingHeaders = true; 66 private boolean mCanShowHeaders = true; 67 private int mContainerListMarginLeft; 68 private int mContainerListWidth; 69 private int mContainerListAlignTop; 70 private TransitionHelper mTransitionHelper; 71 private OnItemSelectedListener mExternalOnItemSelectedListener; 72 private int mSelectedPosition = -1; 73 74 private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title"; 75 private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge"; 76 private static final String ARG_HEADERS_STATE = 77 BrowseFragment.class.getCanonicalName() + ".headersState"; 78 79 /** 80 * @param args Bundle to use for the arguments, if null a new Bundle will be created. 81 */ 82 public static Bundle createArgs(Bundle args, String title, String badgeUri) { 83 return createArgs(args, title, badgeUri, HEADERS_ENABLED); 84 } 85 86 public static Bundle createArgs(Bundle args, String title, String badgeUri, int headersState) { 87 if (args == null) { 88 args = new Bundle(); 89 } 90 args.putString(ARG_TITLE, title); 91 args.putString(ARG_BADGE_URI, badgeUri); 92 args.putInt(ARG_HEADERS_STATE, headersState); 93 return args; 94 } 95 96 public static class Params { 97 private String mTitle; 98 private Drawable mBadgeDrawable; 99 private int mHeadersState; 100 101 /** 102 * Sets the badge image. 103 */ 104 public void setBadgeImage(Drawable drawable) { 105 mBadgeDrawable = drawable; 106 } 107 108 /** 109 * Returns the badge image. 110 */ 111 public Drawable getBadgeImage() { 112 return mBadgeDrawable; 113 } 114 115 /** 116 * Sets a title for the browse fragment. 117 */ 118 public void setTitle(String title) { 119 mTitle = title; 120 } 121 122 /** 123 * Returns the title for the browse fragment. 124 */ 125 public String getTitle() { 126 return mTitle; 127 } 128 129 /** 130 * Sets the state for the headers column in the browse fragment. 131 */ 132 public void setHeadersState(int headersState) { 133 if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) { 134 Log.e(TAG, "Invalid headers state: " + headersState 135 + ", default to enabled and shown."); 136 mHeadersState = HEADERS_ENABLED; 137 } else { 138 mHeadersState = headersState; 139 } 140 } 141 142 /** 143 * Returns the state for the headers column in the browse fragment. 144 */ 145 public int getHeadersState() { 146 return mHeadersState; 147 } 148 } 149 150 /** 151 * Set browse parameters. 152 */ 153 public void setBrowseParams(Params params) { 154 mParams = params; 155 setBadgeDrawable(mParams.mBadgeDrawable); 156 setTitle(mParams.mTitle); 157 setHeadersState(mParams.mHeadersState); 158 } 159 160 /** 161 * Set background parameters. 162 * @deprecated Use BackgroundManager instead 163 */ 164 @Deprecated 165 public void setBackgroundParams(BackgroundParams params) { 166 } 167 168 /** 169 * Returns browse parameters. 170 */ 171 public Params getBrowseParams() { 172 return mParams; 173 } 174 175 /** 176 * Returns the background parameters. 177 * @deprecated Use BackgroundManager instead 178 */ 179 @Deprecated 180 public BackgroundParams getBackgroundParams() { 181 return new BackgroundParams(); 182 } 183 184 /** 185 * Sets the list of rows for the fragment. 186 */ 187 public void setAdapter(ObjectAdapter adapter) { 188 mRowsFragment.setAdapter(adapter); 189 mHeadersFragment.setAdapter(adapter); 190 } 191 192 /** 193 * Returns the list of rows. 194 */ 195 public ObjectAdapter getAdapter() { 196 return mRowsFragment.getAdapter(); 197 } 198 199 /** 200 * Sets an item selection listener. 201 */ 202 public void setOnItemSelectedListener(OnItemSelectedListener listener) { 203 mExternalOnItemSelectedListener = listener; 204 } 205 206 /** 207 * Sets an item clicked listener on the fragment. 208 * OnItemClickedListener will override {@link View.OnClickListener} that 209 * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}. 210 * So in general, developer should choose one of the listeners but not both. 211 */ 212 public void setOnItemClickedListener(OnItemClickedListener listener) { 213 mRowsFragment.setOnItemClickedListener(listener); 214 } 215 216 /** 217 * Returns the item Clicked listener. 218 */ 219 public OnItemClickedListener getOnItemClickedListener() { 220 return mRowsFragment.getOnItemClickedListener(); 221 } 222 223 /** 224 * Sets a click listener for the search "affordance". 225 * 226 * The presence of a listener will change the visibility of the search "affordance" in the 227 * title area. When set to non null the title area will contain a call to search action. 228 * 229 * The listener onClick method will be invoked when the user click on the search action. 230 * 231 * @param listener The listener. 232 */ 233 public void setOnSearchClickedListener(View.OnClickListener listener) { 234 mSearchOrbView.setOnOrbClickedListener(listener); 235 } 236 237 private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener = 238 new BrowseFrameLayout.OnFocusSearchListener() { 239 @Override 240 public View onFocusSearch(View focused, int direction) { 241 // If fastlane is disabled, just return null. 242 if (!mCanShowHeaders) return null; 243 244 if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction); 245 if (!mShowingHeaders && direction == View.FOCUS_LEFT) { 246 mTransitionHelper.runTransition(TransitionHelper.SCENE_WITH_HEADERS); 247 mShowingHeaders = true; 248 return mHeadersFragment.getVerticalGridView(); 249 250 } else if (mShowingHeaders && direction == View.FOCUS_RIGHT) { 251 mTransitionHelper.runTransition(TransitionHelper.SCENE_WITHOUT_HEADERS); 252 mShowingHeaders = false; 253 return mRowsFragment.getVerticalGridView(); 254 } else if (mSearchOrbView.getVisibility() == View.VISIBLE 255 && direction == View.FOCUS_DOWN) { 256 return mRowsFragment.getVerticalGridView(); 257 } else if (mSearchOrbView.getVisibility() == View.VISIBLE 258 && direction == View.FOCUS_UP) { 259 return mSearchOrbView; 260 } else { 261 return null; 262 } 263 } 264 }; 265 266 @Override 267 public void onCreate(Bundle savedInstanceState) { 268 super.onCreate(savedInstanceState); 269 270 mHeadersFragment.setOnHeaderClickListener(mHeaderClickListener); 271 272 mContainerListMarginLeft = (int) getResources().getDimension( 273 R.dimen.lb_browse_rows_margin_left); 274 mContainerListWidth = getResources().getDimensionPixelSize(R.dimen.lb_browse_rows_width); 275 mContainerListAlignTop = 276 getResources().getDimensionPixelSize(R.dimen.lb_browse_rows_align_top); 277 } 278 279 @Override 280 public View onCreateView(LayoutInflater inflater, ViewGroup container, 281 Bundle savedInstanceState) { 282 View root = inflater.inflate(R.layout.lb_browse_fragment, container, false); 283 284 mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame); 285 mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener); 286 287 mBrowseTitle = (ViewGroup) root.findViewById(R.id.browse_title_group); 288 mBadgeView = (ImageView) mBrowseTitle.findViewById(R.id.browse_badge); 289 mTitleView = (TextView) mBrowseTitle.findViewById(R.id.browse_title); 290 mSearchOrbView = (SearchOrbView) mBrowseTitle.findViewById(R.id.browse_orb); 291 292 readArguments(getArguments()); 293 if (mParams != null) { 294 setBadgeDrawable(mParams.mBadgeDrawable); 295 setTitle(mParams.mTitle); 296 setHeadersState(mParams.mHeadersState); 297 } 298 299 mTransitionHelper = new TransitionHelper(getActivity()); 300 mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITH_TITLE, mBrowseFrame, 301 new Runnable() { 302 @Override 303 public void run() { 304 showTitle(true); 305 } 306 }); 307 mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITHOUT_TITLE, mBrowseFrame, 308 new Runnable() { 309 @Override 310 public void run() { 311 showTitle(false); 312 } 313 }); 314 mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITH_HEADERS, mBrowseFrame, 315 new Runnable() { 316 @Override 317 public void run() { 318 showHeaders(true); 319 } 320 }); 321 mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITHOUT_HEADERS, mBrowseFrame, 322 new Runnable() { 323 @Override 324 public void run() { 325 showHeaders(false); 326 } 327 }); 328 329 return root; 330 } 331 332 private void showTitle(boolean show) { 333 mBrowseTitle.setVisibility(show ? View.VISIBLE : View.GONE); 334 } 335 336 private void showHeaders(boolean show) { 337 if (DEBUG) Log.v(TAG, "showHeaders " + show); 338 View headerList = mHeadersFragment.getView(); 339 View containerList = mRowsFragment.getView(); 340 MarginLayoutParams lp; 341 342 headerList.setVisibility(show ? View.VISIBLE : View.GONE); 343 lp = (MarginLayoutParams) containerList.getLayoutParams(); 344 lp.leftMargin = show ? mContainerListMarginLeft : 0; 345 containerList.setLayoutParams(lp); 346 347 mRowsFragment.setExpand(!show); 348 } 349 350 private HeaderPresenter.OnHeaderClickListener mHeaderClickListener = 351 new HeaderPresenter.OnHeaderClickListener() { 352 @Override 353 public void onHeaderClicked() { 354 if (!mCanShowHeaders || !mShowingHeaders) return; 355 356 mTransitionHelper.runTransition(TransitionHelper.SCENE_WITHOUT_HEADERS); 357 mShowingHeaders = false; 358 mRowsFragment.getVerticalGridView().requestFocus(); 359 } 360 }; 361 362 private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() { 363 @Override 364 public void onItemSelected(Object item, Row row) { 365 int position = mRowsFragment.getVerticalGridView().getSelectedPosition(); 366 if (DEBUG) Log.v(TAG, "row selected position " + position); 367 onRowSelected(position); 368 if (mExternalOnItemSelectedListener != null) { 369 mExternalOnItemSelectedListener.onItemSelected(item, row); 370 } 371 } 372 }; 373 374 private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() { 375 @Override 376 public void onItemSelected(Object item, Row row) { 377 int position = mHeadersFragment.getVerticalGridView().getSelectedPosition(); 378 if (DEBUG) Log.v(TAG, "header selected position " + position); 379 onRowSelected(position); 380 } 381 }; 382 383 private void onRowSelected(int position) { 384 if (position != mSelectedPosition) { 385 mSetSelectionRunnable.mPosition = position; 386 mBrowseFrame.getHandler().post(mSetSelectionRunnable); 387 388 if (position == 0) { 389 if (!mShowingTitle) { 390 mTransitionHelper.runTransition(TransitionHelper.SCENE_WITH_TITLE); 391 mShowingTitle = true; 392 } 393 } else if (mShowingTitle) { 394 mTransitionHelper.runTransition(TransitionHelper.SCENE_WITHOUT_TITLE); 395 mShowingTitle = false; 396 } 397 } 398 } 399 400 private class SetSelectionRunnable implements Runnable { 401 int mPosition; 402 @Override 403 public void run() { 404 setSelection(mPosition); 405 } 406 } 407 408 private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 409 410 private void setSelection(int position) { 411 if (position != NO_POSITION) { 412 mRowsFragment.setSelectedPosition(position); 413 mHeadersFragment.setSelectedPosition(position); 414 } 415 mSelectedPosition = position; 416 } 417 418 @Override 419 public void onActivityCreated(Bundle savedInstanceState) { 420 super.onActivityCreated(savedInstanceState); 421 422 if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) { 423 getChildFragmentManager().beginTransaction() 424 .replace(R.id.browse_headers_dock, mHeadersFragment) 425 .replace(R.id.browse_container_dock, mRowsFragment).commit(); 426 mRowsFragment.setOnItemSelectedListener(mRowSelectedListener); 427 mHeadersFragment.setOnItemSelectedListener(mHeaderSelectedListener); 428 } 429 } 430 431 private void setVerticalVerticalGridViewLayout(VerticalGridView listview) { 432 // align the top edge of item to a fixed position 433 listview.setItemAlignmentOffset(0); 434 listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); 435 listview.setWindowAlignmentOffset(mContainerListAlignTop); 436 listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 437 listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 438 } 439 440 /** 441 * Setup dimensions that are only meaningful when the child Fragments are inside 442 * BrowseFragment. 443 */ 444 private void setupChildFragmentsLayout() { 445 VerticalGridView headerList = mHeadersFragment.getVerticalGridView(); 446 VerticalGridView containerList = mRowsFragment.getVerticalGridView(); 447 448 // Both fragments list view has the same alignment 449 setVerticalVerticalGridViewLayout(headerList); 450 setVerticalVerticalGridViewLayout(containerList); 451 452 mRowsFragment.getVerticalGridView().getLayoutParams().width = mContainerListWidth; 453 mRowsFragment.getVerticalGridView().requestLayout(); 454 } 455 456 @Override 457 public void onStart() { 458 super.onStart(); 459 setupChildFragmentsLayout(); 460 if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) { 461 mHeadersFragment.getView().requestFocus(); 462 } else if ((!mCanShowHeaders || !mShowingHeaders) 463 && mRowsFragment.getView() != null) { 464 mRowsFragment.getView().requestFocus(); 465 } 466 showHeaders(mCanShowHeaders && mShowingHeaders); 467 } 468 469 private void readArguments(Bundle args) { 470 if (args == null) { 471 return; 472 } 473 if (args.containsKey(ARG_TITLE)) { 474 setTitle(args.getString(ARG_TITLE)); 475 } 476 477 if (args.containsKey(ARG_BADGE_URI)) { 478 setBadgeUri(args.getString(ARG_BADGE_URI)); 479 } 480 481 if (args.containsKey(ARG_HEADERS_STATE)) { 482 setHeadersState(args.getInt(ARG_HEADERS_STATE)); 483 } 484 } 485 486 private void setBadgeUri(String badgeUri) { 487 // TODO - need a drawable downloader 488 } 489 490 private void setBadgeDrawable(Drawable drawable) { 491 if (mBadgeView == null) { 492 return; 493 } 494 mBadgeView.setImageDrawable(drawable); 495 if (drawable != null) { 496 mBadgeView.setVisibility(View.VISIBLE); 497 } else { 498 mBadgeView.setVisibility(View.GONE); 499 } 500 } 501 502 private void setTitle(String title) { 503 if (mTitleView != null) { 504 mTitleView.setText(title); 505 } 506 } 507 508 private void setHeadersState(int headersState) { 509 if (DEBUG) Log.v(TAG, "setHeadersState " + headersState); 510 switch (headersState) { 511 case HEADERS_ENABLED: 512 mCanShowHeaders = true; 513 mShowingHeaders = true; 514 break; 515 case HEADERS_HIDDEN: 516 mCanShowHeaders = true; 517 mShowingHeaders = false; 518 break; 519 case HEADERS_DISABLED: 520 mCanShowHeaders = false; 521 mShowingHeaders = false; 522 break; 523 default: 524 Log.w(TAG, "Unknown headers state: " + headersState); 525 break; 526 } 527 } 528} 529