SettingsLayoutFragment.java revision 4ae5c62834131ebd8931ca0d5feae8a7f219065e
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.tv.settings.dialog; 18 19import android.animation.Animator; 20import android.animation.AnimatorInflater; 21import android.animation.AnimatorListenerAdapter; 22import android.animation.AnimatorSet; 23import android.animation.ObjectAnimator; 24import android.animation.TimeInterpolator; 25import android.animation.ValueAnimator; 26import android.app.Activity; 27import android.app.Fragment; 28import android.app.FragmentManager; 29import android.app.FragmentTransaction; 30import android.content.Context; 31import android.content.Intent; 32import android.content.pm.PackageManager; 33import android.content.res.Resources; 34import android.graphics.Bitmap; 35import android.graphics.Color; 36import android.graphics.drawable.ColorDrawable; 37import android.graphics.drawable.Drawable; 38import android.net.Uri; 39import android.os.Bundle; 40import android.os.Handler; 41import android.os.Parcel; 42import android.os.Parcelable; 43import android.support.v17.leanback.R; 44import android.support.v17.leanback.widget.VerticalGridView; 45import android.support.v4.view.ViewCompat; 46import android.support.v7.widget.RecyclerView; 47import android.view.LayoutInflater; 48import android.view.View; 49import android.view.ViewGroup; 50import android.view.ViewPropertyAnimator; 51import android.view.ViewTreeObserver; 52import android.view.ViewGroup.LayoutParams; 53import android.view.animation.DecelerateInterpolator; 54import android.view.animation.Interpolator; 55import android.widget.ImageView; 56import android.widget.RelativeLayout; 57import android.widget.TextView; 58 59import java.util.ArrayList; 60import java.util.List; 61 62import com.android.tv.settings.dialog.Layout; 63import com.android.tv.settings.util.AccessibilityHelper; 64 65/** 66 * Displays content on the left and actions on the right. 67 */ 68public class SettingsLayoutFragment extends Fragment implements Layout.LayoutNodeRefreshListener { 69 70 public static final String TAG_LEAN_BACK_DIALOG_FRAGMENT = "leanBackSettingsLayoutFragment"; 71 private static final String EXTRA_CONTENT_TITLE = "title"; 72 private static final String EXTRA_CONTENT_BREADCRUMB = "breadcrumb"; 73 private static final String EXTRA_CONTENT_DESCRIPTION = "description"; 74 private static final String EXTRA_CONTENT_ICON = "icon"; 75 private static final String EXTRA_CONTENT_ICON_URI = "iconUri"; 76 private static final String EXTRA_CONTENT_ICON_BITMAP = "iconBitmap"; 77 private static final String EXTRA_CONTENT_ICON_BACKGROUND = "iconBackground"; 78 private static final String EXTRA_ACTION_NAME = "name"; 79 private static final String EXTRA_ACTION_SELECTED_INDEX = "selectedIndex"; 80 private static final String EXTRA_ENTRY_TRANSITION_PERFORMED = "entryTransitionPerformed"; 81 private static final int ANIMATION_FRAGMENT_ENTER = 1; 82 private static final int ANIMATION_FRAGMENT_EXIT = 2; 83 private static final int ANIMATION_FRAGMENT_ENTER_POP = 3; 84 private static final int ANIMATION_FRAGMENT_EXIT_POP = 4; 85 private static final float WINDOW_ALIGNMENT_OFFSET_PERCENT = 50f; 86 private static final float FADE_IN_ALPHA_START = 0f; 87 private static final float FADE_IN_ALPHA_FINISH = 1f; 88 private static final float SLIDE_OUT_ANIMATOR_LEFT = 0f; 89 private static final float SLIDE_OUT_ANIMATOR_RIGHT = 200f; 90 private static final float SLIDE_OUT_ANIMATOR_START_ALPHA = 0f; 91 private static final float SLIDE_OUT_ANIMATOR_END_ALPHA = 1f; 92 93 public interface Listener { 94 void onActionClicked(Layout.Action action); 95 }; 96 97 /** 98 * Builds a SettingsLayoutFragment object. 99 */ 100 public static class Builder { 101 102 private String mContentTitle; 103 private String mContentBreadcrumb; 104 private String mContentDescription; 105 private Drawable mIcon; 106 private Uri mIconUri; 107 private Bitmap mIconBitmap; 108 private int mIconBackgroundColor = Color.TRANSPARENT; 109 private String mName; 110 111 public SettingsLayoutFragment build() { 112 SettingsLayoutFragment fragment = new SettingsLayoutFragment(); 113 Bundle args = new Bundle(); 114 args.putString(EXTRA_CONTENT_TITLE, mContentTitle); 115 args.putString(EXTRA_CONTENT_BREADCRUMB, mContentBreadcrumb); 116 args.putString(EXTRA_CONTENT_DESCRIPTION, mContentDescription); 117 //args.putParcelable(EXTRA_CONTENT_ICON, mIcon); 118 fragment.mIcon = mIcon; 119 args.putParcelable(EXTRA_CONTENT_ICON_URI, mIconUri); 120 args.putParcelable(EXTRA_CONTENT_ICON_BITMAP, mIconBitmap); 121 args.putInt(EXTRA_CONTENT_ICON_BACKGROUND, mIconBackgroundColor); 122 args.putString(EXTRA_ACTION_NAME, mName); 123 fragment.setArguments(args); 124 return fragment; 125 } 126 127 public Builder title(String title) { 128 mContentTitle = title; 129 return this; 130 } 131 132 public Builder breadcrumb(String breadcrumb) { 133 mContentBreadcrumb = breadcrumb; 134 return this; 135 } 136 137 public Builder description(String description) { 138 mContentDescription = description; 139 return this; 140 } 141 142 public Builder icon(Drawable icon) { 143 mIcon = icon; 144 return this; 145 } 146 147 public Builder iconUri(Uri iconUri) { 148 mIconUri = iconUri; 149 return this; 150 } 151 152 public Builder iconBitmap(Bitmap iconBitmap) { 153 mIconBitmap = iconBitmap; 154 return this; 155 } 156 157 public Builder iconBackgroundColor(int iconBackgroundColor) { 158 mIconBackgroundColor = iconBackgroundColor; 159 return this; 160 } 161 162 public Builder name(String name) { 163 mName = name; 164 return this; 165 } 166 } 167 168 public static void add(FragmentManager fm, SettingsLayoutFragment f) { 169 boolean hasDialog = fm.findFragmentByTag(TAG_LEAN_BACK_DIALOG_FRAGMENT) != null; 170 FragmentTransaction ft = fm.beginTransaction(); 171 172 if (hasDialog) { 173 ft.setCustomAnimations(ANIMATION_FRAGMENT_ENTER, 174 ANIMATION_FRAGMENT_EXIT, ANIMATION_FRAGMENT_ENTER_POP, 175 ANIMATION_FRAGMENT_EXIT_POP); 176 ft.addToBackStack(null); 177 } 178 ft.replace(android.R.id.content, f, TAG_LEAN_BACK_DIALOG_FRAGMENT).commit(); 179 } 180 181 private SettingsLayoutAdapter mAdapter; 182 private VerticalGridView mListView; 183 private String mTitle; 184 private String mBreadcrumb; 185 private String mDescription; 186 private Drawable mIcon; 187 private Uri mIconUri; 188 private Bitmap mIconBitmap; 189 private int mIconBackgroundColor = Color.TRANSPARENT; 190 private Layout mLayout; 191 private String mName; 192 private int mSelectedIndex = -1; 193 private boolean mEntryTransitionPerformed; 194 private boolean mIntroAnimationInProgress; 195 private int mAnimateInDuration; 196 private int mAnimateDelay; 197 private int mSecondaryAnimateDelay; 198 private int mSlideInStagger; 199 private int mSlideInDistance; 200 private Handler refreshViewHandler = new Handler(); 201 202 private final Runnable mRefreshViewRunnable = new Runnable() { 203 @Override 204 public void run() { 205 mLayout.setSelectedIndex(mListView.getSelectedPosition()); 206 mLayout.reloadLayoutRows(); 207 mAdapter.setLayoutRows(mLayout.getLayoutRows()); 208 mAdapter.setNoAnimateMode(); 209 mAdapter.notifyDataSetChanged(); 210 mListView.setSelectedPositionSmooth(mLayout.getSelectedIndex()); 211 } 212 }; 213 214 private final SettingsLayoutAdapter.Listener mLayoutViewRowClicked = 215 new SettingsLayoutAdapter.Listener() { 216 @Override 217 public void onRowClicked(Layout.LayoutRow layoutRow) { 218 onRowViewClicked(layoutRow); 219 } 220 }; 221 222 private final SettingsLayoutAdapter.OnFocusListener mLayoutViewOnFocus = 223 new SettingsLayoutAdapter.OnFocusListener() { 224 @Override 225 public void onActionFocused(Layout.LayoutRow action) { 226 if (getActivity() instanceof SettingsLayoutAdapter.OnFocusListener) { 227 SettingsLayoutAdapter.OnFocusListener listener = 228 (SettingsLayoutAdapter.OnFocusListener) getActivity(); 229 listener.onActionFocused(action); 230 } 231 } 232 }; 233 234 @Override 235 public void onCreate(Bundle savedInstanceState) { 236 super.onCreate(savedInstanceState); 237 Bundle state = (savedInstanceState != null) ? savedInstanceState : getArguments(); 238 mTitle = state.getString(EXTRA_CONTENT_TITLE); 239 mBreadcrumb = state.getString(EXTRA_CONTENT_BREADCRUMB); 240 mDescription = state.getString(EXTRA_CONTENT_DESCRIPTION); 241 //mIcon = state.getParcelable(EXTRA_CONTENT_ICON_RESOURCE_ID, 0); 242 mIconUri = state.getParcelable(EXTRA_CONTENT_ICON_URI); 243 mIconBitmap = state.getParcelable(EXTRA_CONTENT_ICON_BITMAP); 244 mIconBackgroundColor = state.getInt(EXTRA_CONTENT_ICON_BACKGROUND, Color.TRANSPARENT); 245 mName = state.getString(EXTRA_ACTION_NAME); 246 mSelectedIndex = state.getInt(EXTRA_ACTION_SELECTED_INDEX, -1); 247 mEntryTransitionPerformed = state.getBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, false); 248 } 249 250 @Override 251 public View onCreateView(LayoutInflater inflater, ViewGroup container, 252 Bundle savedInstanceState) { 253 254 View v = inflater.inflate(R.layout.lb_dialog_fragment, container, false); 255 256 View contentContainer = v.findViewById(R.id.content_fragment); 257 View content = inflater.inflate(R.layout.lb_dialog_content, container, false); 258 ((ViewGroup) contentContainer).addView(content); 259 initializeContentView(content); 260 v.setTag(R.id.content_fragment, content); 261 262 View actionContainer = v.findViewById(R.id.action_fragment); 263 View action = inflater.inflate(R.layout.lb_dialog_action_list, container, false); 264 ((ViewGroup) actionContainer).addView(action); 265 setActionView(action); 266 v.setTag(R.id.action_fragment, action); 267 268 Resources res = getActivity().getResources(); 269 mAnimateInDuration = res.getInteger(R.integer.animate_in_duration); 270 mAnimateDelay = res.getInteger(R.integer.animate_delay); 271 mSecondaryAnimateDelay = res.getInteger(R.integer.secondary_animate_delay); 272 mSlideInStagger = res.getInteger(R.integer.slide_in_stagger); 273 mSlideInDistance = res.getInteger(R.integer.slide_in_distance); 274 275 return v; 276 } 277 278 @Override 279 public void onSaveInstanceState(Bundle outState) { 280 super.onSaveInstanceState(outState); 281 outState.putString(EXTRA_CONTENT_TITLE, mTitle); 282 outState.putString(EXTRA_CONTENT_BREADCRUMB, mBreadcrumb); 283 outState.putString(EXTRA_CONTENT_DESCRIPTION, mDescription); 284 //outState.putInt(EXTRA_CONTENT_ICON_RESOURCE_ID, mIconResourceId); 285 outState.putParcelable(EXTRA_CONTENT_ICON_URI, mIconUri); 286 outState.putParcelable(EXTRA_CONTENT_ICON_BITMAP, mIconBitmap); 287 outState.putInt(EXTRA_CONTENT_ICON_BACKGROUND, mIconBackgroundColor); 288 outState.putInt(EXTRA_ACTION_SELECTED_INDEX, 289 (mListView != null) ? mListView.getSelectedPosition() : -1); 290 outState.putString(EXTRA_ACTION_NAME, mName); 291 outState.putBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, mEntryTransitionPerformed); 292 } 293 294 @Override 295 public void onStart() { 296 super.onStart(); 297 if (!mEntryTransitionPerformed) { 298 mEntryTransitionPerformed = true; 299 performEntryTransition(); 300 } 301 } 302 303 public void setLayout(Layout layout) { 304 mLayout = layout; 305 mLayout.setRefreshViewListener(this); 306 } 307 308 // TODO refactor to get this call as the result of a callback from the Layout. 309 private void updateViews() { 310 View dialogView = getView(); 311 View contentView = (View) dialogView.getTag(R.id.content_fragment); 312 313 mBreadcrumb = mLayout.getBreadcrumb(); 314 TextView breadcrumbView = (TextView) contentView.getTag(R.id.breadcrumb); 315 breadcrumbView.setText(mBreadcrumb); 316 317 mTitle = mLayout.getTitle(); 318 TextView titleView = (TextView) contentView.getTag(R.id.title); 319 titleView.setText(mTitle); 320 321 mDescription = mLayout.getDescription(); 322 TextView descriptionView = (TextView) contentView.getTag(R.id.description); 323 descriptionView.setText(mDescription); 324 325 mAdapter.setLayoutRows(mLayout.getLayoutRows()); 326 mAdapter.notifyDataSetChanged(); 327 mAdapter.setFocusListenerEnabled(false); 328 mListView.setSelectedPositionSmooth(mLayout.getSelectedIndex()); 329 mAdapter.setFocusListenerEnabled(true); 330 } 331 332 public void setIcon(int resId) { 333 View dialogView = getView(); 334 View contentView = (View) dialogView.getTag(R.id.content_fragment); 335 ImageView iconView = (ImageView) contentView.findViewById(R.id.icon); 336 if (iconView != null) { 337 iconView.setImageResource(resId); 338 } 339 } 340 341 /** 342 * Notification that a part of the model antecedent to the visibile view has changed. 343 */ 344 @Override 345 public void onRefreshView() { 346 refreshViewHandler.removeCallbacks(mRefreshViewRunnable); 347 refreshViewHandler.post(mRefreshViewRunnable); 348 } 349 350 /** 351 * Return the currently selected node. The return value may be null, if this is called before 352 * the layout has been rendered for the first time. Clients should check the return value 353 * before using. 354 */ 355 @Override 356 public Layout.Node getSelectedNode() { 357 int index = mListView.getSelectedPosition(); 358 ArrayList<Layout.LayoutRow> layoutRows = mLayout.getLayoutRows(); 359 if (index < layoutRows.size()) { 360 return layoutRows.get(index).getNode(); 361 } else { 362 return null; 363 } 364 } 365 366 /** 367 * Process forward key press. 368 */ 369 void onRowViewClicked(Layout.LayoutRow layoutRow) { 370 if (layoutRow.isGoBack()) { 371 onBackPressed(); 372 } else { 373 Layout.Action action = layoutRow.getUserAction(); 374 if (action != null) { 375 Listener actionListener = (Listener) getActivity(); 376 if (actionListener != null) { 377 actionListener.onActionClicked(action); 378 } 379 } else if (mLayout.onClickNavigate(layoutRow)) { 380 mLayout.setParentSelectedIndex(mListView.getSelectedPosition()); 381 updateViews(); 382 } 383 } 384 } 385 386 /** 387 * Process back key press. 388 */ 389 public boolean onBackPressed() { 390 if (mLayout.goBack()) { 391 updateViews(); 392 return true; 393 } else { 394 return false; 395 } 396 } 397 398 /** 399 * Client has requested header with {@param title} be selected. If there is no such header 400 * return to the first row. 401 */ 402 protected void goBackToTitle(String title) { 403 mLayout.goToTitle(title); 404 updateViews(); 405 } 406 407 @Override 408 public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { 409 View dialogView = getView(); 410 View contentView = (View) dialogView.getTag(R.id.content_fragment); 411 View actionView = (View) dialogView.getTag(R.id.action_fragment); 412 View actionContainerView = dialogView.findViewById(R.id.action_fragment); 413 View titleView = (View) contentView.getTag(R.id.title); 414 View breadcrumbView = (View) contentView.getTag(R.id.breadcrumb); 415 View descriptionView = (View) contentView.getTag(R.id.description); 416 View iconView = (View) contentView.getTag(R.id.icon); 417 View listView = (View) actionView.getTag(R.id.list); 418 View selectorView = (View) actionView.getTag(R.id.selector); 419 420 ArrayList<Animator> animators = new ArrayList<Animator>(); 421 422 switch (nextAnim) { 423 case ANIMATION_FRAGMENT_ENTER: 424 animators.add(createSlideInFromEndAnimator(titleView)); 425 animators.add(createSlideInFromEndAnimator(breadcrumbView)); 426 animators.add(createSlideInFromEndAnimator(descriptionView)); 427 animators.add(createSlideInFromEndAnimator(iconView)); 428 animators.add(createSlideInFromEndAnimator(listView)); 429 animators.add(createSlideInFromEndAnimator(selectorView)); 430 break; 431 case ANIMATION_FRAGMENT_EXIT: 432 animators.add(createSlideOutToStartAnimator(titleView)); 433 animators.add(createSlideOutToStartAnimator(breadcrumbView)); 434 animators.add(createSlideOutToStartAnimator(descriptionView)); 435 animators.add(createSlideOutToStartAnimator(iconView)); 436 animators.add(createSlideOutToStartAnimator(listView)); 437 animators.add(createSlideOutToStartAnimator(selectorView)); 438 animators.add(createFadeOutAnimator(actionContainerView)); 439 break; 440 case ANIMATION_FRAGMENT_ENTER_POP: 441 animators.add(createSlideInFromStartAnimator(titleView)); 442 animators.add(createSlideInFromStartAnimator(breadcrumbView)); 443 animators.add(createSlideInFromStartAnimator(descriptionView)); 444 animators.add(createSlideInFromStartAnimator(iconView)); 445 animators.add(createSlideInFromStartAnimator(listView)); 446 animators.add(createSlideInFromStartAnimator(selectorView)); 447 break; 448 case ANIMATION_FRAGMENT_EXIT_POP: 449 animators.add(createSlideOutToEndAnimator(titleView)); 450 animators.add(createSlideOutToEndAnimator(breadcrumbView)); 451 animators.add(createSlideOutToEndAnimator(descriptionView)); 452 animators.add(createSlideOutToEndAnimator(iconView)); 453 animators.add(createSlideOutToEndAnimator(listView)); 454 animators.add(createSlideOutToEndAnimator(selectorView)); 455 animators.add(createFadeOutAnimator(actionContainerView)); 456 break; 457 default: 458 return super.onCreateAnimator(transit, enter, nextAnim); 459 } 460 461 mEntryTransitionPerformed = true; 462 return createDummyAnimator(dialogView, animators); 463 } 464 465 /** 466 * Called when intro animation is finished. 467 * <p> 468 * If a subclass is going to alter the view, should wait until this is 469 * called. 470 */ 471 public void onIntroAnimationFinished() { 472 mIntroAnimationInProgress = false; 473 474 // Display the selector view. 475 View focusedChild = mListView.getFocusedChild(); 476 if (focusedChild != null) { 477 View actionView = (View) getView().getTag(R.id.action_fragment); 478 int height = focusedChild.getHeight (); 479 View selectorView = actionView.findViewById(R.id.selector); 480 LayoutParams lp = selectorView.getLayoutParams(); 481 lp.height = height; 482 selectorView.setLayoutParams(lp); 483 selectorView.setAlpha (1f); 484 } 485 } 486 487 public boolean isIntroAnimationInProgress() { 488 return mIntroAnimationInProgress; 489 } 490 491 private void initializeContentView(View content) { 492 TextView titleView = (TextView) content.findViewById(R.id.title); 493 TextView breadcrumbView = (TextView) content.findViewById(R.id.breadcrumb); 494 TextView descriptionView = (TextView) content.findViewById(R.id.description); 495 titleView.setText(mTitle); 496 breadcrumbView.setText(mBreadcrumb); 497 descriptionView.setText(mDescription); 498 final ImageView iconImageView = (ImageView) content.findViewById(R.id.icon); 499 iconImageView.setBackgroundColor(mIconBackgroundColor); 500 501 // Force text fields to be focusable when accessibility is enabled. 502 if (AccessibilityHelper.forceFocusableViews(getActivity())) { 503 titleView.setFocusable(true); 504 titleView.setFocusableInTouchMode(true); 505 descriptionView.setFocusable(true); 506 descriptionView.setFocusableInTouchMode(true); 507 breadcrumbView.setFocusable(true); 508 breadcrumbView.setFocusableInTouchMode(true); 509 } 510 511 if (mIcon != null) { 512 iconImageView.setImageDrawable(mIcon); 513 updateViewSize(iconImageView); 514 } else if (mIconBitmap != null) { 515 iconImageView.setImageBitmap(mIconBitmap); 516 updateViewSize(iconImageView); 517 } else if (mIconUri != null) { 518 iconImageView.setVisibility(View.INVISIBLE); 519 /* 520 521 BitmapDownloader bitmapDownloader = BitmapDownloader.getInstance( 522 content.getContext()); 523 mBitmapCallBack = new BitmapCallback() { 524 @Override 525 public void onBitmapRetrieved(Bitmap bitmap) { 526 if (bitmap != null) { 527 mIconBitmap = bitmap; 528 iconImageView.setVisibility(View.VISIBLE); 529 iconImageView.setImageBitmap(bitmap); 530 updateViewSize(iconImageView); 531 } 532 } 533 }; 534 535 bitmapDownloader.getBitmap(new BitmapWorkerOptions.Builder( 536 content.getContext()).resource(mIconUri) 537 .width(iconImageView.getLayoutParams().width).build(), 538 mBitmapCallBack); 539 */ 540 } else { 541 iconImageView.setVisibility(View.GONE); 542 } 543 544 content.setTag(R.id.title, titleView); 545 content.setTag(R.id.breadcrumb, breadcrumbView); 546 content.setTag(R.id.description, descriptionView); 547 content.setTag(R.id.icon, iconImageView); 548 } 549 550 private void setActionView(View action) { 551 mAdapter = new SettingsLayoutAdapter(mLayoutViewRowClicked, mLayoutViewOnFocus); 552 mAdapter.setLayoutRows(mLayout.getLayoutRows()); 553 if (action instanceof VerticalGridView) { 554 mListView = (VerticalGridView) action; 555 } else { 556 mListView = (VerticalGridView) action.findViewById(R.id.list); 557 if (mListView == null) { 558 throw new IllegalArgumentException("No ListView exists."); 559 } 560 mListView.setWindowAlignmentOffset(0); 561 mListView.setWindowAlignmentOffsetPercent(WINDOW_ALIGNMENT_OFFSET_PERCENT); 562 mListView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 563 View selectorView = action.findViewById(R.id.selector); 564 if (selectorView != null) { 565 mListView.setOnScrollListener(new SelectorAnimator(selectorView, mListView)); 566 } 567 } 568 569 mListView.requestFocusFromTouch(); 570 mListView.setAdapter(mAdapter); 571 int initialSelectedIndex; 572 if (mSelectedIndex >= 0 && mSelectedIndex < mLayout.getLayoutRows().size()) { 573 // "mSelectedIndex" is a valid index and so must have been initialized from a Bundle in 574 // the "onCreate" member and the only way it could be a valid index is if it was saved 575 // by "onSaveInstanceState" since it is initialized to "-1" (an invalid value) in the 576 // constructor. 577 initialSelectedIndex = mSelectedIndex; 578 } else { 579 // First time this fragment is being instantiated, i.e. did not reach here via the 580 // "onSaveInstanceState" route. Initialize the index from the starting index defined 581 // in the "Layout". 582 initialSelectedIndex = mLayout.getSelectedIndex(); 583 } 584 mListView.setSelectedPositionSmooth(initialSelectedIndex); 585 action.setTag(R.id.list, mListView); 586 action.setTag(R.id.selector, action.findViewById(R.id.selector)); 587 } 588 589 private void updateViewSize(ImageView iconView) { 590 int intrinsicWidth = iconView.getDrawable().getIntrinsicWidth(); 591 LayoutParams lp = iconView.getLayoutParams(); 592 if (intrinsicWidth > 0) { 593 lp.height = lp.width * iconView.getDrawable().getIntrinsicHeight() 594 / intrinsicWidth; 595 } else { 596 // If no intrinsic width, then just mke this a square. 597 lp.height = lp.width; 598 } 599 } 600 601 private void fadeIn(View v) { 602 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", FADE_IN_ALPHA_START, 603 FADE_IN_ALPHA_FINISH); 604 alphaAnimator.setDuration(v.getContext().getResources().getInteger( 605 android.R.integer.config_mediumAnimTime)); 606 alphaAnimator.start(); 607 } 608 609 private void performEntryTransition() { 610 final View dialogView = getView(); 611 final View contentView = (View) dialogView.getTag(R.id.content_fragment); 612 final View actionContainerView = dialogView.findViewById(R.id.action_fragment); 613 614 mIntroAnimationInProgress = true; 615 616 // Fade out the old activity. 617 getActivity().overridePendingTransition(0, R.anim.lb_dialog_fade_out); 618 619 int bgColor = contentView.getContext().getResources() 620 .getColor(R.color.lb_dialog_activity_background); 621 final ColorDrawable bgDrawable = new ColorDrawable(); 622 bgDrawable.setColor(bgColor); 623 bgDrawable.setAlpha(0); 624 dialogView.setBackground(bgDrawable); 625 dialogView.setVisibility(View.INVISIBLE); 626 627 // We need to defer the remainder of the animation preparation until the first layout has 628 // occurred, as we don't yet know the final location of the icon. 629 contentView.getViewTreeObserver().addOnGlobalLayoutListener( 630 new ViewTreeObserver.OnGlobalLayoutListener() { 631 @Override 632 public void onGlobalLayout() { 633 contentView.getViewTreeObserver().removeOnGlobalLayoutListener(this); 634 // if we buildLayer() at this time, the texture is 635 // actually not created delay a little so we can make 636 // sure all hardware layer is created before animation, 637 // in that way we can avoid the jittering of start 638 // animation 639 contentView.postOnAnimationDelayed(mEntryAnimationRunnable, mAnimateDelay); 640 } 641 642 Runnable mEntryAnimationRunnable = new Runnable() { 643 @Override 644 public void run() { 645 if (!isAdded()) { 646 // We have been detached before this could run, so just bail. 647 return; 648 } 649 650 dialogView.setVisibility(View.VISIBLE); 651 652 // Fade in the activity background protection 653 ObjectAnimator oa = ObjectAnimator.ofInt(bgDrawable, "alpha", 255); 654 oa.setDuration(mAnimateInDuration); 655 oa.setStartDelay(mSecondaryAnimateDelay); 656 oa.setInterpolator(new DecelerateInterpolator(1.0f)); 657 oa.start(); 658 659 boolean isRtl = ViewCompat.getLayoutDirection(contentView) == 660 View.LAYOUT_DIRECTION_RTL; 661 int startDist = isRtl ? mSlideInDistance : -mSlideInDistance; 662 int endDist = isRtl ? -actionContainerView.getMeasuredWidth() : 663 actionContainerView.getMeasuredWidth(); 664 665 // Fade in and slide in the ContentFragment TextViews from the start. 666 prepareAndAnimateView((View) contentView.getTag(R.id.title), 667 startDist, false); 668 prepareAndAnimateView((View) contentView.getTag(R.id.breadcrumb), 669 startDist, false); 670 prepareAndAnimateView((View) contentView.getTag(R.id.description), 671 startDist, false); 672 673 // Fade in and slide in the ActionFragment from the end. 674 prepareAndAnimateView(actionContainerView, 675 endDist, false); 676 prepareAndAnimateView((View) contentView.getTag(R.id.icon), 677 startDist, true); 678 } 679 }; 680 }); 681 } 682 683 private void prepareAndAnimateView(final View v, float initTransX, 684 final boolean notifyAnimationFinished) { 685 v.setLayerType(View.LAYER_TYPE_HARDWARE, null); 686 v.buildLayer(); 687 v.setAlpha(0); 688 v.setTranslationX(initTransX); 689 v.animate().alpha(1f).translationX(0).setDuration(mAnimateInDuration) 690 .setStartDelay(mSecondaryAnimateDelay); 691 v.animate().setInterpolator(new DecelerateInterpolator(1.0f)); 692 v.animate().setListener(new AnimatorListenerAdapter() { 693 @Override 694 public void onAnimationEnd(Animator animation) { 695 v.setLayerType(View.LAYER_TYPE_NONE, null); 696 if (notifyAnimationFinished) { 697 onIntroAnimationFinished(); 698 } 699 } 700 }); 701 v.animate().start(); 702 } 703 704 private Animator createDummyAnimator(final View v, ArrayList<Animator> animators) { 705 final AnimatorSet animatorSet = new AnimatorSet(); 706 animatorSet.playTogether(animators); 707 return new UntargetableAnimatorSet(animatorSet); 708 } 709 710 private Animator createAnimator(View v, int resourceId) { 711 Animator animator = AnimatorInflater.loadAnimator(v.getContext(), resourceId); 712 animator.setTarget(v); 713 return animator; 714 } 715 716 private Animator createSlideOutToStartAnimator(View v) { 717 boolean isRtl = ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_RTL; 718 float toX = isRtl ? SLIDE_OUT_ANIMATOR_RIGHT : -SLIDE_OUT_ANIMATOR_RIGHT; 719 return createTranslateAlphaAnimator(v, SLIDE_OUT_ANIMATOR_LEFT, toX, 720 SLIDE_OUT_ANIMATOR_END_ALPHA, SLIDE_OUT_ANIMATOR_START_ALPHA); 721 } 722 723 private Animator createSlideInFromEndAnimator(View v) { 724 boolean isRtl = ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_RTL; 725 float fromX = isRtl ? -SLIDE_OUT_ANIMATOR_RIGHT : SLIDE_OUT_ANIMATOR_RIGHT; 726 return createTranslateAlphaAnimator(v, fromX, SLIDE_OUT_ANIMATOR_LEFT, 727 SLIDE_OUT_ANIMATOR_START_ALPHA, SLIDE_OUT_ANIMATOR_END_ALPHA); 728 } 729 730 private Animator createSlideInFromStartAnimator(View v) { 731 boolean isRtl = ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_RTL; 732 float fromX = isRtl ? SLIDE_OUT_ANIMATOR_RIGHT : -SLIDE_OUT_ANIMATOR_RIGHT; 733 return createTranslateAlphaAnimator(v, fromX, SLIDE_OUT_ANIMATOR_LEFT, 734 SLIDE_OUT_ANIMATOR_START_ALPHA, SLIDE_OUT_ANIMATOR_END_ALPHA); 735 } 736 737 private Animator createSlideOutToEndAnimator(View v) { 738 boolean isRtl = ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_RTL; 739 float toX = isRtl ? -SLIDE_OUT_ANIMATOR_RIGHT : SLIDE_OUT_ANIMATOR_RIGHT; 740 return createTranslateAlphaAnimator(v, SLIDE_OUT_ANIMATOR_LEFT, toX, 741 SLIDE_OUT_ANIMATOR_END_ALPHA, SLIDE_OUT_ANIMATOR_START_ALPHA); 742 } 743 744 private Animator createFadeOutAnimator(View v) { 745 return createAlphaAnimator(v, SLIDE_OUT_ANIMATOR_END_ALPHA, SLIDE_OUT_ANIMATOR_START_ALPHA); 746 } 747 748 private Animator createTranslateAlphaAnimator(View v, float fromTranslateX, float toTranslateX, 749 float fromAlpha, float toAlpha) { 750 ObjectAnimator translateAnimator = ObjectAnimator.ofFloat(v, "translationX", fromTranslateX, 751 toTranslateX); 752 translateAnimator.setDuration( 753 getResources().getInteger(android.R.integer.config_longAnimTime)); 754 Animator alphaAnimator = createAlphaAnimator(v, fromAlpha, toAlpha); 755 AnimatorSet animatorSet = new AnimatorSet(); 756 animatorSet.play(translateAnimator).with(alphaAnimator); 757 return animatorSet; 758 } 759 760 private Animator createAlphaAnimator(View v, float fromAlpha, float toAlpha) { 761 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", fromAlpha, toAlpha); 762 alphaAnimator.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime)); 763 return alphaAnimator; 764 } 765 766 private static class SelectorAnimator extends RecyclerView.OnScrollListener { 767 768 private final View mSelectorView; 769 private final ViewGroup mParentView; 770 private final int mAnimationDuration; 771 private volatile boolean mFadedOut = true; 772 773 SelectorAnimator(View selectorView, ViewGroup parentView) { 774 mSelectorView = selectorView; 775 mParentView = parentView; 776 mAnimationDuration = selectorView.getContext() 777 .getResources().getInteger(R.integer.lb_dialog_animation_duration); 778 } 779 780 /** 781 * We want to fade in the selector if we've stopped scrolling on it. If we're scrolling, we 782 * want to ensure to dim the selector if we haven't already. We dim the last highlighted 783 * view so that while a user is scrolling, nothing is highlighted. 784 */ 785 @Override 786 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 787 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 788 // The selector starts with a height of 0. In order to scale up from 0 we first 789 // need the set the height to 1 and scale form there. 790 int selectorHeight = mSelectorView.getHeight(); 791 if (selectorHeight == 0) { 792 LayoutParams lp = mSelectorView.getLayoutParams(); 793 lp.height = selectorHeight = mSelectorView.getContext().getResources() 794 .getDimensionPixelSize(R.dimen.lb_action_fragment_selector_min_height); 795 mSelectorView.setLayoutParams(lp); 796 } 797 View focusedChild = mParentView.getFocusedChild(); 798 if (focusedChild != null) { 799 float scaleY = (float) focusedChild.getHeight() / selectorHeight; 800 ViewPropertyAnimator animation = mSelectorView.animate() 801 .alpha(1f) 802 .setDuration(mAnimationDuration) 803 .setInterpolator(new DecelerateInterpolator(2f)); 804 if (mFadedOut) { 805 // Selector is completely faded out, so we can just scale before fading in. 806 mSelectorView.setScaleY(scaleY); 807 } else { 808 // Selector is not faded out, so we must animate the scale as we fade in. 809 animation.scaleY(scaleY); 810 } 811 animation.start(); 812 } 813 } else { 814 mSelectorView.animate() 815 .alpha(0f) 816 .setDuration(mAnimationDuration) 817 .setInterpolator(new DecelerateInterpolator(2f)) 818 .start(); 819 } 820 } 821 } 822 823 private static class UntargetableAnimatorSet extends Animator { 824 825 private final AnimatorSet mAnimatorSet; 826 827 UntargetableAnimatorSet(AnimatorSet animatorSet) { 828 mAnimatorSet = animatorSet; 829 } 830 831 @Override 832 public void addListener(Animator.AnimatorListener listener) { 833 mAnimatorSet.addListener(listener); 834 } 835 836 @Override 837 public void cancel() { 838 mAnimatorSet.cancel(); 839 } 840 841 @Override 842 public Animator clone() { 843 return mAnimatorSet.clone(); 844 } 845 846 @Override 847 public void end() { 848 mAnimatorSet.end(); 849 } 850 851 @Override 852 public long getDuration() { 853 return mAnimatorSet.getDuration(); 854 } 855 856 @Override 857 public ArrayList<Animator.AnimatorListener> getListeners() { 858 return mAnimatorSet.getListeners(); 859 } 860 861 @Override 862 public long getStartDelay() { 863 return mAnimatorSet.getStartDelay(); 864 } 865 866 @Override 867 public boolean isRunning() { 868 return mAnimatorSet.isRunning(); 869 } 870 871 @Override 872 public boolean isStarted() { 873 return mAnimatorSet.isStarted(); 874 } 875 876 @Override 877 public void removeAllListeners() { 878 mAnimatorSet.removeAllListeners(); 879 } 880 881 @Override 882 public void removeListener(Animator.AnimatorListener listener) { 883 mAnimatorSet.removeListener(listener); 884 } 885 886 @Override 887 public Animator setDuration(long duration) { 888 return mAnimatorSet.setDuration(duration); 889 } 890 891 @Override 892 public void setInterpolator(TimeInterpolator value) { 893 mAnimatorSet.setInterpolator(value); 894 } 895 896 @Override 897 public void setStartDelay(long startDelay) { 898 mAnimatorSet.setStartDelay(startDelay); 899 } 900 901 @Override 902 public void setTarget(Object target) { 903 // ignore 904 } 905 906 @Override 907 public void setupEndValues() { 908 mAnimatorSet.setupEndValues(); 909 } 910 911 @Override 912 public void setupStartValues() { 913 mAnimatorSet.setupStartValues(); 914 } 915 916 @Override 917 public void start() { 918 mAnimatorSet.start(); 919 } 920 } 921 922} 923