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