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