BrowseFragment.java revision 32ba8fc7b148485db84aee7e37c0c1bca8260006
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 * Returns browse parameters. 167 */ 168 public Params getBrowseParams() { 169 return mParams; 170 } 171 172 /** 173 * Sets the list of rows for the fragment. 174 */ 175 public void setAdapter(ObjectAdapter adapter) { 176 mAdapter = adapter; 177 if (mRowsFragment != null) { 178 mRowsFragment.setAdapter(adapter); 179 mHeadersFragment.setAdapter(adapter); 180 } 181 } 182 183 /** 184 * Returns the list of rows. 185 */ 186 public ObjectAdapter getAdapter() { 187 return mAdapter; 188 } 189 190 /** 191 * Sets an item selection listener. 192 */ 193 public void setOnItemSelectedListener(OnItemSelectedListener listener) { 194 mExternalOnItemSelectedListener = listener; 195 } 196 197 /** 198 * Sets an item clicked listener on the fragment. 199 * OnItemClickedListener will override {@link View.OnClickListener} that 200 * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}. 201 * So in general, developer should choose one of the listeners but not both. 202 */ 203 public void setOnItemClickedListener(OnItemClickedListener listener) { 204 mOnItemClickedListener = listener; 205 if (mRowsFragment != null) { 206 mRowsFragment.setOnItemClickedListener(listener); 207 } 208 } 209 210 /** 211 * Returns the item Clicked listener. 212 */ 213 public OnItemClickedListener getOnItemClickedListener() { 214 return mOnItemClickedListener; 215 } 216 217 /** 218 * Sets a click listener for the search affordance. 219 * 220 * The presence of a listener will change the visibility of the search affordance in the 221 * title area. When set to non-null the title area will contain a call to search action. 222 * 223 * The listener onClick method will be invoked when the user click on the search action. 224 * 225 * @param listener The listener. 226 */ 227 public void setOnSearchClickedListener(View.OnClickListener listener) { 228 mExternalOnSearchClickedListener = listener; 229 if (mSearchOrbView != null) { 230 mSearchOrbView.setOnOrbClickedListener(listener); 231 } 232 } 233 234 private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener = 235 new BrowseFrameLayout.OnFocusSearchListener() { 236 @Override 237 public View onFocusSearch(View focused, int direction) { 238 // If fastlane is disabled, just return null. 239 if (!mCanShowHeaders) return null; 240 241 if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction); 242 if (!mShowingHeaders && direction == View.FOCUS_LEFT) { 243 mTransitionHelper.runTransition(TransitionHelper.SCENE_WITH_HEADERS); 244 mShowingHeaders = true; 245 return mHeadersFragment.getVerticalGridView(); 246 247 } else if (mShowingHeaders && direction == View.FOCUS_RIGHT) { 248 mTransitionHelper.runTransition(TransitionHelper.SCENE_WITHOUT_HEADERS); 249 mShowingHeaders = false; 250 return mRowsFragment.getVerticalGridView(); 251 252 } else if (focused == mSearchOrbView && direction == View.FOCUS_DOWN) { 253 return mShowingHeaders ? mHeadersFragment.getVerticalGridView() : 254 mRowsFragment.getVerticalGridView(); 255 256 } else if (focused != mSearchOrbView && mSearchOrbView.getVisibility() == View.VISIBLE 257 && direction == View.FOCUS_UP) { 258 return mSearchOrbView; 259 260 } else { 261 return null; 262 } 263 } 264 }; 265 266 @Override 267 public void onCreate(Bundle savedInstanceState) { 268 super.onCreate(savedInstanceState); 269 TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme); 270 mContainerListMarginLeft = (int) ta.getDimension( 271 R.styleable.LeanbackTheme_browseRowsMarginStart, 0); 272 mContainerListAlignTop = (int) ta.getDimension( 273 R.styleable.LeanbackTheme_browseRowsMarginTop, 0); 274 ta.recycle(); 275 } 276 277 @Override 278 public View onCreateView(LayoutInflater inflater, ViewGroup container, 279 Bundle savedInstanceState) { 280 if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) { 281 mRowsFragment = new RowsFragment(); 282 mHeadersFragment = new HeadersFragment(); 283 getChildFragmentManager().beginTransaction() 284 .replace(R.id.browse_headers_dock, mHeadersFragment) 285 .replace(R.id.browse_container_dock, mRowsFragment).commit(); 286 } else { 287 mHeadersFragment = (HeadersFragment) getChildFragmentManager() 288 .findFragmentById(R.id.browse_headers_dock); 289 mRowsFragment = (RowsFragment) getChildFragmentManager() 290 .findFragmentById(R.id.browse_container_dock); 291 } 292 mRowsFragment.setOnItemSelectedListener(mRowSelectedListener); 293 mHeadersFragment.setOnItemSelectedListener(mHeaderSelectedListener); 294 mHeadersFragment.setOnHeaderClickListener(mHeaderClickListener); 295 mRowsFragment.setOnItemClickedListener(mOnItemClickedListener); 296 mRowsFragment.setAdapter(mAdapter); 297 mHeadersFragment.setAdapter(mAdapter); 298 299 View root = inflater.inflate(R.layout.lb_browse_fragment, container, false); 300 301 mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame); 302 mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener); 303 304 mBrowseTitle = (ViewGroup) root.findViewById(R.id.browse_title_group); 305 mBadgeView = (ImageView) mBrowseTitle.findViewById(R.id.browse_badge); 306 mTitleView = (TextView) mBrowseTitle.findViewById(R.id.browse_title); 307 mSearchOrbView = (SearchOrbView) mBrowseTitle.findViewById(R.id.browse_orb); 308 if (mExternalOnSearchClickedListener != null) { 309 mSearchOrbView.setOnOrbClickedListener(mExternalOnSearchClickedListener); 310 } 311 312 readArguments(getArguments()); 313 if (mParams != null) { 314 setBadgeDrawable(mParams.mBadgeDrawable); 315 setTitle(mParams.mTitle); 316 setHeadersState(mParams.mHeadersState); 317 } 318 319 mTransitionHelper = new TransitionHelper(getActivity()); 320 mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITH_TITLE, mBrowseFrame, 321 new Runnable() { 322 @Override 323 public void run() { 324 showTitle(true); 325 } 326 }); 327 mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITHOUT_TITLE, mBrowseFrame, 328 new Runnable() { 329 @Override 330 public void run() { 331 showTitle(false); 332 } 333 }); 334 mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITH_HEADERS, mBrowseFrame, 335 new Runnable() { 336 @Override 337 public void run() { 338 showHeaders(true); 339 } 340 }); 341 mTransitionHelper.addSceneRunnable(TransitionHelper.SCENE_WITHOUT_HEADERS, mBrowseFrame, 342 new Runnable() { 343 @Override 344 public void run() { 345 showHeaders(false); 346 } 347 }); 348 349 return root; 350 } 351 352 private void showTitle(boolean show) { 353 mBrowseTitle.setVisibility(show ? View.VISIBLE : View.GONE); 354 } 355 356 private void showHeaders(boolean show) { 357 if (DEBUG) Log.v(TAG, "showHeaders " + show); 358 View headerList = mHeadersFragment.getView(); 359 View containerList = mRowsFragment.getView(); 360 MarginLayoutParams lp; 361 362 headerList.setVisibility(show ? View.VISIBLE : View.GONE); 363 lp = (MarginLayoutParams) containerList.getLayoutParams(); 364 lp.leftMargin = show ? mContainerListMarginLeft : 0; 365 containerList.setLayoutParams(lp); 366 367 mRowsFragment.setExpand(!show); 368 } 369 370 private HeaderPresenter.OnHeaderClickListener mHeaderClickListener = 371 new HeaderPresenter.OnHeaderClickListener() { 372 @Override 373 public void onHeaderClicked() { 374 if (!mCanShowHeaders || !mShowingHeaders) return; 375 376 mTransitionHelper.runTransition(TransitionHelper.SCENE_WITHOUT_HEADERS); 377 mShowingHeaders = false; 378 mRowsFragment.getVerticalGridView().requestFocus(); 379 } 380 }; 381 382 private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() { 383 @Override 384 public void onItemSelected(Object item, Row row) { 385 int position = mRowsFragment.getVerticalGridView().getSelectedPosition(); 386 if (DEBUG) Log.v(TAG, "row selected position " + position); 387 onRowSelected(position); 388 if (mExternalOnItemSelectedListener != null) { 389 mExternalOnItemSelectedListener.onItemSelected(item, row); 390 } 391 } 392 }; 393 394 private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() { 395 @Override 396 public void onItemSelected(Object item, Row row) { 397 int position = mHeadersFragment.getVerticalGridView().getSelectedPosition(); 398 if (DEBUG) Log.v(TAG, "header selected position " + position); 399 onRowSelected(position); 400 } 401 }; 402 403 private void onRowSelected(int position) { 404 if (position != mSelectedPosition) { 405 mSetSelectionRunnable.mPosition = position; 406 mBrowseFrame.getHandler().post(mSetSelectionRunnable); 407 408 if (position == 0) { 409 if (!mShowingTitle) { 410 mTransitionHelper.runTransition(TransitionHelper.SCENE_WITH_TITLE); 411 mShowingTitle = true; 412 } 413 } else if (mShowingTitle) { 414 mTransitionHelper.runTransition(TransitionHelper.SCENE_WITHOUT_TITLE); 415 mShowingTitle = false; 416 } 417 } 418 } 419 420 private class SetSelectionRunnable implements Runnable { 421 int mPosition; 422 @Override 423 public void run() { 424 setSelection(mPosition); 425 } 426 } 427 428 private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 429 430 private void setSelection(int position) { 431 if (position != NO_POSITION) { 432 mRowsFragment.setSelectedPosition(position); 433 mHeadersFragment.setSelectedPosition(position); 434 } 435 mSelectedPosition = position; 436 } 437 438 private void setVerticalVerticalGridViewLayout(VerticalGridView listview) { 439 // align the top edge of item to a fixed position 440 listview.setItemAlignmentOffset(0); 441 listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); 442 listview.setWindowAlignmentOffset(mContainerListAlignTop); 443 listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 444 listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 445 } 446 447 /** 448 * Setup dimensions that are only meaningful when the child Fragments are inside 449 * BrowseFragment. 450 */ 451 private void setupChildFragmentsLayout() { 452 VerticalGridView headerList = mHeadersFragment.getVerticalGridView(); 453 VerticalGridView containerList = mRowsFragment.getVerticalGridView(); 454 455 // Both fragments list view has the same alignment 456 setVerticalVerticalGridViewLayout(headerList); 457 setVerticalVerticalGridViewLayout(containerList); 458 } 459 460 @Override 461 public void onStart() { 462 super.onStart(); 463 setupChildFragmentsLayout(); 464 if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) { 465 mHeadersFragment.getView().requestFocus(); 466 } else if ((!mCanShowHeaders || !mShowingHeaders) 467 && mRowsFragment.getView() != null) { 468 mRowsFragment.getView().requestFocus(); 469 } 470 showHeaders(mCanShowHeaders && mShowingHeaders); 471 } 472 473 private void readArguments(Bundle args) { 474 if (args == null) { 475 return; 476 } 477 if (args.containsKey(ARG_TITLE)) { 478 setTitle(args.getString(ARG_TITLE)); 479 } 480 481 if (args.containsKey(ARG_BADGE_URI)) { 482 setBadgeUri(args.getString(ARG_BADGE_URI)); 483 } 484 485 if (args.containsKey(ARG_HEADERS_STATE)) { 486 setHeadersState(args.getInt(ARG_HEADERS_STATE)); 487 } 488 } 489 490 private void setBadgeUri(String badgeUri) { 491 // TODO - need a drawable downloader 492 } 493 494 private void setBadgeDrawable(Drawable drawable) { 495 if (mBadgeView == null) { 496 return; 497 } 498 mBadgeView.setImageDrawable(drawable); 499 if (drawable != null) { 500 mBadgeView.setVisibility(View.VISIBLE); 501 } else { 502 mBadgeView.setVisibility(View.GONE); 503 } 504 } 505 506 private void setTitle(String title) { 507 if (mTitleView != null) { 508 mTitleView.setText(title); 509 } 510 } 511 512 private void setHeadersState(int headersState) { 513 if (DEBUG) Log.v(TAG, "setHeadersState " + headersState); 514 switch (headersState) { 515 case HEADERS_ENABLED: 516 mCanShowHeaders = true; 517 mShowingHeaders = true; 518 break; 519 case HEADERS_HIDDEN: 520 mCanShowHeaders = true; 521 mShowingHeaders = false; 522 break; 523 case HEADERS_DISABLED: 524 mCanShowHeaders = false; 525 mShowingHeaders = false; 526 break; 527 default: 528 Log.w(TAG, "Unknown headers state: " + headersState); 529 break; 530 } 531 } 532} 533