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