BrowseFragment.java revision 02e411c2c69d20aab138f1a162a24ea650eff7a1
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 // TODO: deal fragment destroy view properly 253 VerticalGridView rowsGridView = mRowsFragment.getVerticalGridView(); 254 if (rowsGridView != null) { 255 rowsGridView.setAnimateChildLayout(true); 256 rowsGridView.setFocusSearchDisabled(false); 257 } 258 VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView(); 259 if (headerGridView != null) { 260 headerGridView.setFocusSearchDisabled(false); 261 } 262 } 263 264 private boolean isVerticalScrolling() { 265 // don't run transition 266 return mHeadersFragment.getVerticalGridView().getScrollState() 267 != HorizontalGridView.SCROLL_STATE_IDLE 268 || mRowsFragment.getVerticalGridView().getScrollState() 269 != HorizontalGridView.SCROLL_STATE_IDLE; 270 } 271 272 private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener = 273 new BrowseFrameLayout.OnFocusSearchListener() { 274 @Override 275 public View onFocusSearch(View focused, int direction) { 276 // If fastlane is disabled, just return null. 277 if (!mCanShowHeaders) return null; 278 279 // if fast lane is running transition, focus stays 280 if (mHeadersTransitionRunning) return focused; 281 if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction); 282 if (!mShowingHeaders && direction == View.FOCUS_LEFT) { 283 if (isVerticalScrolling()) { 284 return focused; 285 } 286 onHeadersTransitionStart(); 287 mHeadersFragment.attachGridView(); 288 mBrowseFrame.postOnAnimationDelayed(new Runnable() { 289 @Override 290 public void run() { 291 mHeadersFragment.detachGridView(); 292 mTransitionHelper.runTransition(mSceneWithHeaders, mHeadersTransition); 293 } 294 }, 0); 295 mShowingHeaders = true; 296 return mHeadersFragment.getVerticalGridView(); 297 } else if (mShowingHeaders && direction == View.FOCUS_RIGHT) { 298 if (isVerticalScrolling()) { 299 return focused; 300 } 301 onHeadersTransitionStart(); 302 mHeadersFragment.attachGridView(); 303 mBrowseFrame.postOnAnimationDelayed(new Runnable() { 304 @Override 305 public void run() { 306 mTransitionHelper.runTransition(mSceneWithoutHeaders, mHeadersTransition); 307 } 308 }, 0); 309 mShowingHeaders = false; 310 return mRowsFragment.getVerticalGridView(); 311 } else if (focused == mSearchOrbView && direction == View.FOCUS_DOWN) { 312 return mShowingHeaders ? mHeadersFragment.getVerticalGridView() : 313 mRowsFragment.getVerticalGridView(); 314 315 } else if (focused != mSearchOrbView && mSearchOrbView.getVisibility() == View.VISIBLE 316 && direction == View.FOCUS_UP) { 317 return mSearchOrbView; 318 319 } else { 320 return null; 321 } 322 } 323 }; 324 325 @Override 326 public void onCreate(Bundle savedInstanceState) { 327 super.onCreate(savedInstanceState); 328 TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme); 329 mContainerListMarginLeft = (int) ta.getDimension( 330 R.styleable.LeanbackTheme_browseRowsMarginStart, 0); 331 mContainerListAlignTop = (int) ta.getDimension( 332 R.styleable.LeanbackTheme_browseRowsMarginTop, 0); 333 ta.recycle(); 334 } 335 336 @Override 337 public View onCreateView(LayoutInflater inflater, ViewGroup container, 338 Bundle savedInstanceState) { 339 if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) { 340 mRowsFragment = new RowsFragment(); 341 mHeadersFragment = new HeadersFragment(); 342 getChildFragmentManager().beginTransaction() 343 .replace(R.id.browse_headers_dock, mHeadersFragment) 344 .replace(R.id.browse_container_dock, mRowsFragment).commit(); 345 } else { 346 mHeadersFragment = (HeadersFragment) getChildFragmentManager() 347 .findFragmentById(R.id.browse_headers_dock); 348 mRowsFragment = (RowsFragment) getChildFragmentManager() 349 .findFragmentById(R.id.browse_container_dock); 350 } 351 mRowsFragment.setAdapter(mAdapter); 352 mHeadersFragment.setAdapter(mAdapter); 353 354 mRowsFragment.setOnItemSelectedListener(mRowSelectedListener); 355 mHeadersFragment.setOnItemSelectedListener(mHeaderSelectedListener); 356 mHeadersFragment.setOnHeaderClickListener(mHeaderClickListener); 357 mRowsFragment.setOnItemClickedListener(mOnItemClickedListener); 358 359 View root = inflater.inflate(R.layout.lb_browse_fragment, container, false); 360 361 mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame); 362 mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener); 363 364 mBrowseTitle = (ViewGroup) root.findViewById(R.id.browse_title_group); 365 mBadgeView = (ImageView) mBrowseTitle.findViewById(R.id.browse_badge); 366 mTitleView = (TextView) mBrowseTitle.findViewById(R.id.browse_title); 367 mSearchOrbView = (SearchOrbView) mBrowseTitle.findViewById(R.id.browse_orb); 368 if (mExternalOnSearchClickedListener != null) { 369 mSearchOrbView.setOnOrbClickedListener(mExternalOnSearchClickedListener); 370 } 371 372 readArguments(getArguments()); 373 if (mParams != null) { 374 setBadgeDrawable(mParams.mBadgeDrawable); 375 setTitle(mParams.mTitle); 376 setHeadersState(mParams.mHeadersState); 377 } 378 379 mTransitionHelper = new TransitionHelper(getActivity()); 380 mSceneWithTitle = mTransitionHelper.createScene(mBrowseFrame, new Runnable() { 381 @Override 382 public void run() { 383 showTitle(true); 384 } 385 }); 386 mSceneWithoutTitle = mTransitionHelper.createScene(mBrowseFrame, new Runnable() { 387 @Override 388 public void run() { 389 showTitle(false); 390 } 391 }); 392 mSceneWithHeaders = mTransitionHelper.createScene(mBrowseFrame, new Runnable() { 393 @Override 394 public void run() { 395 showHeaders(true); 396 } 397 }); 398 mSceneWithoutHeaders = mTransitionHelper.createScene(mBrowseFrame, new Runnable() { 399 @Override 400 public void run() { 401 showHeaders(false); 402 } 403 }); 404 mTitleTransition = mTransitionHelper.createAutoTransition(); 405 mHeadersTransition = mTransitionHelper.createAutoTransition(); 406 mTransitionHelper.excludeChildren(mHeadersTransition, R.id.browse_title_group, true); 407 mTransitionHelper.excludeChildren(mTitleTransition, R.id.browse_headers, true); 408 mTransitionHelper.excludeChildren(mTitleTransition, R.id.container_list, true); 409 mTransitionHelper.setTransitionCompleteListener(mHeadersTransition, new Runnable() { 410 @Override 411 public void run() { 412 onHeadersTransitionComplete(); 413 } 414 }); 415 return root; 416 } 417 418 private void showTitle(boolean show) { 419 mBrowseTitle.setVisibility(show ? View.VISIBLE : View.GONE); 420 } 421 422 private void showHeaders(boolean show) { 423 if (DEBUG) Log.v(TAG, "showHeaders " + show); 424 View headerList = mHeadersFragment.getView(); 425 View containerList = mRowsFragment.getView(); 426 MarginLayoutParams lp; 427 428 if (show) { 429 mHeadersFragment.attachGridView(); 430 mHeadersFragment.getView().requestFocus(); 431 } else { 432 mHeadersFragment.detachGridView(); 433 } 434 lp = (MarginLayoutParams) containerList.getLayoutParams(); 435 lp.leftMargin = show ? mContainerListMarginLeft : 0; 436 containerList.setLayoutParams(lp); 437 438 mRowsFragment.setExpand(!show); 439 } 440 441 private HeaderPresenter.OnHeaderClickListener mHeaderClickListener = 442 new HeaderPresenter.OnHeaderClickListener() { 443 @Override 444 public void onHeaderClicked() { 445 if (!mCanShowHeaders || !mShowingHeaders) return; 446 447 if (mHeadersTransitionRunning) { 448 return; 449 } 450 onHeadersTransitionStart(); 451 mTransitionHelper.runTransition(mSceneWithoutHeaders, mHeadersTransition); 452 mShowingHeaders = false; 453 mRowsFragment.getVerticalGridView().requestFocus(); 454 } 455 }; 456 457 private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() { 458 @Override 459 public void onItemSelected(Object item, Row row) { 460 int position = mRowsFragment.getVerticalGridView().getSelectedPosition(); 461 if (DEBUG) Log.v(TAG, "row selected position " + position); 462 onRowSelected(position); 463 if (mExternalOnItemSelectedListener != null) { 464 mExternalOnItemSelectedListener.onItemSelected(item, row); 465 } 466 } 467 }; 468 469 private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() { 470 @Override 471 public void onItemSelected(Object item, Row row) { 472 int position = mHeadersFragment.getVerticalGridView().getSelectedPosition(); 473 if (DEBUG) Log.v(TAG, "header selected position " + position); 474 onRowSelected(position); 475 } 476 }; 477 478 private void onRowSelected(int position) { 479 if (position != mSelectedPosition) { 480 mSetSelectionRunnable.mPosition = position; 481 mBrowseFrame.getHandler().post(mSetSelectionRunnable); 482 483 if (position == 0) { 484 if (!mShowingTitle) { 485 mTransitionHelper.runTransition(mSceneWithTitle, mTitleTransition); 486 mShowingTitle = true; 487 } 488 } else if (mShowingTitle) { 489 mTransitionHelper.runTransition(mSceneWithoutTitle, mTitleTransition); 490 mShowingTitle = false; 491 } 492 } 493 } 494 495 private class SetSelectionRunnable implements Runnable { 496 int mPosition; 497 @Override 498 public void run() { 499 setSelection(mPosition); 500 } 501 } 502 503 private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 504 505 private void setSelection(int position) { 506 if (position != NO_POSITION) { 507 mRowsFragment.setSelectedPosition(position); 508 mHeadersFragment.setSelectedPosition(position); 509 } 510 mSelectedPosition = position; 511 } 512 513 private void setVerticalVerticalGridViewLayout(VerticalGridView listview) { 514 // align the top edge of item to a fixed position 515 listview.setItemAlignmentOffset(0); 516 listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); 517 listview.setWindowAlignmentOffset(mContainerListAlignTop); 518 listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 519 listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 520 } 521 522 /** 523 * Setup dimensions that are only meaningful when the child Fragments are inside 524 * BrowseFragment. 525 */ 526 private void setupChildFragmentsLayout() { 527 VerticalGridView headerList = mHeadersFragment.getVerticalGridView(); 528 VerticalGridView containerList = mRowsFragment.getVerticalGridView(); 529 530 // Both fragments list view has the same alignment 531 setVerticalVerticalGridViewLayout(headerList); 532 setVerticalVerticalGridViewLayout(containerList); 533 } 534 535 @Override 536 public void onStart() { 537 super.onStart(); 538 setupChildFragmentsLayout(); 539 if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) { 540 mHeadersFragment.getView().requestFocus(); 541 } else if ((!mCanShowHeaders || !mShowingHeaders) 542 && mRowsFragment.getView() != null) { 543 mRowsFragment.getView().requestFocus(); 544 } 545 showHeaders(mCanShowHeaders && mShowingHeaders); 546 } 547 548 private void readArguments(Bundle args) { 549 if (args == null) { 550 return; 551 } 552 if (args.containsKey(ARG_TITLE)) { 553 setTitle(args.getString(ARG_TITLE)); 554 } 555 556 if (args.containsKey(ARG_BADGE_URI)) { 557 setBadgeUri(args.getString(ARG_BADGE_URI)); 558 } 559 560 if (args.containsKey(ARG_HEADERS_STATE)) { 561 setHeadersState(args.getInt(ARG_HEADERS_STATE)); 562 } 563 } 564 565 private void setBadgeUri(String badgeUri) { 566 // TODO - need a drawable downloader 567 } 568 569 private void setBadgeDrawable(Drawable drawable) { 570 if (mBadgeView == null) { 571 return; 572 } 573 mBadgeView.setImageDrawable(drawable); 574 if (drawable != null) { 575 mBadgeView.setVisibility(View.VISIBLE); 576 } else { 577 mBadgeView.setVisibility(View.GONE); 578 } 579 } 580 581 private void setTitle(String title) { 582 if (mTitleView != null) { 583 mTitleView.setText(title); 584 } 585 } 586 587 private void setHeadersState(int headersState) { 588 if (DEBUG) Log.v(TAG, "setHeadersState " + headersState); 589 switch (headersState) { 590 case HEADERS_ENABLED: 591 mCanShowHeaders = true; 592 mShowingHeaders = true; 593 break; 594 case HEADERS_HIDDEN: 595 mCanShowHeaders = true; 596 mShowingHeaders = false; 597 break; 598 case HEADERS_DISABLED: 599 mCanShowHeaders = false; 600 mShowingHeaders = false; 601 break; 602 default: 603 Log.w(TAG, "Unknown headers state: " + headersState); 604 break; 605 } 606 } 607} 608