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