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.BitmapDrawable; 37 import android.graphics.drawable.ColorDrawable; 38 import android.graphics.drawable.Drawable; 39 import android.net.Uri; 40 import android.os.Bundle; 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.util.Log; 48 import android.view.LayoutInflater; 49 import android.view.View; 50 import android.view.ViewGroup; 51 import android.view.ViewPropertyAnimator; 52 import android.view.ViewTreeObserver; 53 import android.view.ViewGroup.LayoutParams; 54 import android.view.animation.DecelerateInterpolator; 55 import android.view.animation.Interpolator; 56 import android.widget.ImageView; 57 import android.widget.RelativeLayout; 58 import android.widget.TextView; 59 60 import com.android.tv.settings.util.AccessibilityHelper; 61 import com.android.tv.settings.widget.BitmapWorkerOptions; 62 import com.android.tv.settings.widget.DrawableDownloader; 63 import com.android.tv.settings.widget.DrawableDownloader.BitmapCallback; 64 65 import java.util.ArrayList; 66 import java.util.List; 67 68 /** 69 * Displays content on the left and actions on the right. 70 */ 71 public class DialogFragment extends Fragment { 72 73 private static final String TAG_LEAN_BACK_DIALOG_FRAGMENT = "leanBackDialogFragment"; 74 private static final String EXTRA_CONTENT_TITLE = "title"; 75 private static final String EXTRA_CONTENT_BREADCRUMB = "breadcrumb"; 76 private static final String EXTRA_CONTENT_DESCRIPTION = "description"; 77 private static final String EXTRA_CONTENT_ICON_RESOURCE_ID = "iconResourceId"; 78 private static final String EXTRA_CONTENT_ICON_URI = "iconUri"; 79 private static final String EXTRA_CONTENT_ICON_BITMAP = "iconBitmap"; 80 private static final String EXTRA_CONTENT_ICON_BACKGROUND = "iconBackground"; 81 private static final String EXTRA_ACTION_NAME = "name"; 82 private static final String EXTRA_ACTION_ACTIONS = "actions"; 83 private static final String EXTRA_ACTION_SELECTED_INDEX = "selectedIndex"; 84 private static final String EXTRA_ENTRY_TRANSITION_PERFORMED = "entryTransitionPerformed"; 85 private static final int ANIMATE_IN_DURATION = 250; 86 private static final int ANIMATE_DELAY = 550; 87 private static final int SECONDARY_ANIMATE_DELAY = 120; 88 private static final int SLIDE_IN_STAGGER = 100; 89 private static final int SLIDE_IN_DISTANCE = 120; 90 private static final int ANIMATION_FRAGMENT_ENTER = 1; 91 private static final int ANIMATION_FRAGMENT_EXIT = 2; 92 private static final int ANIMATION_FRAGMENT_ENTER_POP = 3; 93 private static final int ANIMATION_FRAGMENT_EXIT_POP = 4; 94 95 /** 96 * Builds a LeanBackDialogFragment object. 97 */ 98 public static class Builder { 99 100 private String mContentTitle; 101 private String mContentBreadcrumb; 102 private String mContentDescription; 103 private int mIconResourceId; 104 private Uri mIconUri; 105 private Bitmap mIconBitmap; 106 private int mIconBackgroundColor = Color.TRANSPARENT; 107 private ArrayList<Action> mActions; 108 private String mName; 109 private int mSelectedIndex; 110 111 public DialogFragment build() { 112 DialogFragment fragment = new DialogFragment(); 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.putInt(EXTRA_CONTENT_ICON_RESOURCE_ID, mIconResourceId); 118 args.putParcelable(EXTRA_CONTENT_ICON_URI, mIconUri); 119 args.putParcelable(EXTRA_CONTENT_ICON_BITMAP, mIconBitmap); 120 args.putInt(EXTRA_CONTENT_ICON_BACKGROUND, mIconBackgroundColor); 121 args.putParcelableArrayList(EXTRA_ACTION_ACTIONS, mActions); 122 args.putString(EXTRA_ACTION_NAME, mName); 123 args.putInt(EXTRA_ACTION_SELECTED_INDEX, mSelectedIndex); 124 fragment.setArguments(args); 125 return fragment; 126 } 127 128 public Builder title(String title) { 129 mContentTitle = title; 130 return this; 131 } 132 133 public Builder breadcrumb(String breadcrumb) { 134 mContentBreadcrumb = breadcrumb; 135 return this; 136 } 137 138 public Builder description(String description) { 139 mContentDescription = description; 140 return this; 141 } 142 143 public Builder iconResourceId(int iconResourceId) { 144 mIconResourceId = iconResourceId; 145 return this; 146 } 147 148 public Builder iconUri(Uri iconUri) { 149 mIconUri = iconUri; 150 return this; 151 } 152 153 public Builder iconBitmap(Bitmap iconBitmap) { 154 mIconBitmap = iconBitmap; 155 return this; 156 } 157 158 public Builder iconBackgroundColor(int iconBackgroundColor) { 159 mIconBackgroundColor = iconBackgroundColor; 160 return this; 161 } 162 163 public Builder actions(ArrayList<Action> actions) { 164 mActions = actions; 165 return this; 166 } 167 168 public Builder name(String name) { 169 mName = name; 170 return this; 171 } 172 173 public Builder selectedIndex(int selectedIndex) { 174 mSelectedIndex = selectedIndex; 175 return this; 176 } 177 } 178 179 public static void add(FragmentManager fm, DialogFragment f) { 180 boolean hasDialog = fm.findFragmentByTag(TAG_LEAN_BACK_DIALOG_FRAGMENT) != null; 181 FragmentTransaction ft = fm.beginTransaction(); 182 183 if (hasDialog) { 184 ft.setCustomAnimations(ANIMATION_FRAGMENT_ENTER, 185 ANIMATION_FRAGMENT_EXIT, ANIMATION_FRAGMENT_ENTER_POP, 186 ANIMATION_FRAGMENT_EXIT_POP); 187 ft.addToBackStack(null); 188 } 189 ft.replace(android.R.id.content, f, TAG_LEAN_BACK_DIALOG_FRAGMENT).commit(); 190 } 191 192 private DialogActionAdapter mAdapter; 193 private SelectorAnimator mSelectorAnimator; 194 private VerticalGridView mListView; 195 private Action.Listener mListener; 196 private String mTitle; 197 private String mBreadcrumb; 198 private String mDescription; 199 private int mIconResourceId; 200 private Uri mIconUri; 201 private Bitmap mIconBitmap; 202 private int mIconBackgroundColor = Color.TRANSPARENT; 203 private ArrayList<Action> mActions; 204 private String mName; 205 private int mSelectedIndex = -1; 206 private boolean mEntryTransitionPerformed; 207 private boolean mIntroAnimationInProgress; 208 private BitmapCallback mBitmapCallBack; 209 210 @Override 211 public void onCreate(Bundle savedInstanceState) { 212 android.util.Log.v("DialogFragment", "onCreate"); 213 super.onCreate(savedInstanceState); 214 Bundle state = (savedInstanceState != null) ? savedInstanceState : getArguments(); 215 if (mTitle == null) { 216 mTitle = state.getString(EXTRA_CONTENT_TITLE); 217 } 218 if (mBreadcrumb == null) { 219 mBreadcrumb = state.getString(EXTRA_CONTENT_BREADCRUMB); 220 } 221 if (mDescription == null) { 222 mDescription = state.getString(EXTRA_CONTENT_DESCRIPTION); 223 } 224 if (mIconResourceId == 0) { 225 mIconResourceId = state.getInt(EXTRA_CONTENT_ICON_RESOURCE_ID, 0); 226 } 227 if (mIconUri == null) { 228 mIconUri = state.getParcelable(EXTRA_CONTENT_ICON_URI); 229 } 230 if (mIconBitmap == null) { 231 mIconBitmap = state.getParcelable(EXTRA_CONTENT_ICON_BITMAP); 232 } 233 if (mIconBackgroundColor == Color.TRANSPARENT) { 234 mIconBackgroundColor = state.getInt(EXTRA_CONTENT_ICON_BACKGROUND, Color.TRANSPARENT); 235 } 236 if (mActions == null) { 237 mActions = state.getParcelableArrayList(EXTRA_ACTION_ACTIONS); 238 } 239 if (mName == null) { 240 mName = state.getString(EXTRA_ACTION_NAME); 241 } 242 if (mSelectedIndex == -1) { 243 mSelectedIndex = state.getInt(EXTRA_ACTION_SELECTED_INDEX, -1); 244 } 245 mEntryTransitionPerformed = state.getBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, false); 246 } 247 248 @Override 249 public View onCreateView(LayoutInflater inflater, ViewGroup container, 250 Bundle savedInstanceState) { 251 252 View v = inflater.inflate(R.layout.lb_dialog_fragment, container, false); 253 254 View contentContainer = v.findViewById(R.id.content_fragment); 255 View content = inflater.inflate(R.layout.lb_dialog_content, container, false); 256 ((ViewGroup) contentContainer).addView(content); 257 setContentView(content); 258 v.setTag(R.id.content_fragment, content); 259 260 View actionContainer = v.findViewById(R.id.action_fragment); 261 View action = inflater.inflate(R.layout.lb_dialog_action_list, container, false); 262 ((ViewGroup) actionContainer).addView(action); 263 setActionView(action); 264 v.setTag(R.id.action_fragment, action); 265 266 return v; 267 } 268 269 @Override 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.putParcelableArrayList(EXTRA_ACTION_ACTIONS, mActions); 280 outState.putInt(EXTRA_ACTION_SELECTED_INDEX, 281 (mListView != null) ? getSelectedItemPosition() : mSelectedIndex); 282 outState.putString(EXTRA_ACTION_NAME, mName); 283 outState.putBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, mEntryTransitionPerformed); 284 } 285 286 @Override 287 public void onStart() { 288 super.onStart(); 289 if (!mEntryTransitionPerformed) { 290 mEntryTransitionPerformed = true; 291 performEntryTransition(); 292 } else { 293 performSelectorTransition(); 294 } 295 } 296 297 @Override 298 public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { 299 View dialogView = getView(); 300 View contentView = (View) dialogView.getTag(R.id.content_fragment); 301 View actionView = (View) dialogView.getTag(R.id.action_fragment); 302 View actionContainerView = dialogView.findViewById(R.id.action_fragment); 303 View titleView = (View) contentView.getTag(R.id.title); 304 View breadcrumbView = (View) contentView.getTag(R.id.breadcrumb); 305 View descriptionView = (View) contentView.getTag(R.id.description); 306 View iconView = (View) contentView.getTag(R.id.icon); 307 View listView = (View) actionView.getTag(R.id.list); 308 View selectorView = (View) actionView.getTag(R.id.selector); 309 310 ArrayList<Animator> animators = new ArrayList<Animator>(); 311 312 switch (nextAnim) { 313 case ANIMATION_FRAGMENT_ENTER: 314 animators.add(createSlideInFromEndAnimator(titleView)); 315 animators.add(createSlideInFromEndAnimator(breadcrumbView)); 316 animators.add(createSlideInFromEndAnimator(descriptionView)); 317 animators.add(createSlideInFromEndAnimator(iconView)); 318 animators.add(createSlideInFromEndAnimator(listView)); 319 animators.add(createSlideInFromEndAnimator(selectorView)); 320 break; 321 case ANIMATION_FRAGMENT_EXIT: 322 animators.add(createSlideOutToStartAnimator(titleView)); 323 animators.add(createSlideOutToStartAnimator(breadcrumbView)); 324 animators.add(createSlideOutToStartAnimator(descriptionView)); 325 animators.add(createSlideOutToStartAnimator(iconView)); 326 animators.add(createSlideOutToStartAnimator(listView)); 327 animators.add(createSlideOutToStartAnimator(selectorView)); 328 animators.add(createFadeOutAnimator(actionContainerView)); 329 break; 330 case ANIMATION_FRAGMENT_ENTER_POP: 331 animators.add(createSlideInFromStartAnimator(titleView)); 332 animators.add(createSlideInFromStartAnimator(breadcrumbView)); 333 animators.add(createSlideInFromStartAnimator(descriptionView)); 334 animators.add(createSlideInFromStartAnimator(iconView)); 335 animators.add(createSlideInFromStartAnimator(listView)); 336 animators.add(createSlideInFromStartAnimator(selectorView)); 337 break; 338 case ANIMATION_FRAGMENT_EXIT_POP: 339 animators.add(createSlideOutToEndAnimator(titleView)); 340 animators.add(createSlideOutToEndAnimator(breadcrumbView)); 341 animators.add(createSlideOutToEndAnimator(descriptionView)); 342 animators.add(createSlideOutToEndAnimator(iconView)); 343 animators.add(createSlideOutToEndAnimator(listView)); 344 animators.add(createSlideOutToEndAnimator(selectorView)); 345 animators.add(createFadeOutAnimator(actionContainerView)); 346 break; 347 default: 348 return super.onCreateAnimator(transit, enter, nextAnim); 349 } 350 351 mEntryTransitionPerformed = true; 352 return createDummyAnimator(dialogView, animators); 353 } 354 355 public void setIcon(int iconResourceId) { 356 mIconResourceId = iconResourceId; 357 View v = getView(); 358 if (v != null) { 359 final ImageView iconImageView = (ImageView) v.findViewById(R.id.icon); 360 if (iconImageView != null) { 361 if (iconResourceId != 0) { 362 iconImageView.setImageResource(iconResourceId); 363 iconImageView.setVisibility(View.VISIBLE); 364 updateViewSize(iconImageView); 365 } 366 } 367 } 368 } 369 370 /** 371 * Fragments need to call this method in its {@link #onResume()} to set the 372 * custom listener. <br/> 373 * Activities do not need to call this method 374 * 375 * @param listener 376 */ 377 public void setListener(Action.Listener listener) { 378 mListener = listener; 379 } 380 381 public boolean hasListener() { 382 return mListener != null; 383 } 384 385 public ArrayList<Action> getActions() { 386 return mActions; 387 } 388 389 public void setActions(ArrayList<Action> actions) { 390 mActions = actions; 391 if (mAdapter != null) { 392 mAdapter.setActions(mActions); 393 } 394 } 395 396 public View getItemView(int position) { 397 return mListView.getChildAt(position); 398 } 399 400 public void setSelectedPosition(int position) { 401 mListView.setSelectedPosition(position); 402 } 403 404 public int getSelectedItemPosition() { 405 return mListView.indexOfChild(mListView.getFocusedChild()); 406 } 407 408 /** 409 * Called when intro animation is finished. 410 * <p> 411 * If a subclass is going to alter the view, should wait until this is 412 * called. 413 */ 414 public void onIntroAnimationFinished() { 415 mIntroAnimationInProgress = false; 416 } 417 418 public boolean isIntroAnimationInProgress() { 419 return mIntroAnimationInProgress; 420 } 421 422 private void setContentView(View content) { 423 TextView titleView = (TextView) content.findViewById(R.id.title); 424 TextView breadcrumbView = (TextView) content.findViewById(R.id.breadcrumb); 425 TextView descriptionView = (TextView) content.findViewById(R.id.description); 426 titleView.setText(mTitle); 427 breadcrumbView.setText(mBreadcrumb); 428 descriptionView.setText(mDescription); 429 final ImageView iconImageView = (ImageView) content.findViewById(R.id.icon); 430 if (mIconBackgroundColor != Color.TRANSPARENT) { 431 iconImageView.setBackgroundColor(mIconBackgroundColor); 432 } 433 434 if (AccessibilityHelper.forceFocusableViews(getActivity())) { 435 titleView.setFocusable(true); 436 titleView.setFocusableInTouchMode(true); 437 descriptionView.setFocusable(true); 438 descriptionView.setFocusableInTouchMode(true); 439 breadcrumbView.setFocusable(true); 440 breadcrumbView.setFocusableInTouchMode(true); 441 } 442 443 if (mIconResourceId != 0) { 444 iconImageView.setImageResource(mIconResourceId); 445 updateViewSize(iconImageView); 446 } else { 447 if (mIconBitmap != null) { 448 iconImageView.setImageBitmap(mIconBitmap); 449 updateViewSize(iconImageView); 450 } else { 451 if (mIconUri != null) { 452 iconImageView.setVisibility(View.INVISIBLE); 453 454 DrawableDownloader bitmapDownloader = DrawableDownloader.getInstance( 455 content.getContext()); 456 mBitmapCallBack = new BitmapCallback() { 457 @Override 458 public void onBitmapRetrieved(Drawable bitmap) { 459 if (bitmap != null) { 460 mIconBitmap = (bitmap instanceof BitmapDrawable) ? ((BitmapDrawable) bitmap) 461 .getBitmap() 462 : null; 463 iconImageView.setVisibility(View.VISIBLE); 464 iconImageView.setImageDrawable(bitmap); 465 updateViewSize(iconImageView); 466 } 467 } 468 }; 469 470 bitmapDownloader.getBitmap(new BitmapWorkerOptions.Builder( 471 content.getContext()).resource(mIconUri) 472 .width(iconImageView.getLayoutParams().width).build(), 473 mBitmapCallBack); 474 } else { 475 iconImageView.setVisibility(View.GONE); 476 } 477 } 478 } 479 480 content.setTag(R.id.title, titleView); 481 content.setTag(R.id.breadcrumb, breadcrumbView); 482 content.setTag(R.id.description, descriptionView); 483 content.setTag(R.id.icon, iconImageView); 484 } 485 486 private void setActionView(View action) { 487 mAdapter = new DialogActionAdapter(new Action.Listener() { 488 @Override 489 public void onActionClicked(Action action) { 490 // eat events if action is disabled or only displays info 491 if (!action.isEnabled() || action.infoOnly()) { 492 return; 493 } 494 495 /** 496 * If the custom lister has been set using 497 * {@link #setListener(DialogActionAdapter.Listener)}, use it. 498 * If not, use the activity's default listener. 499 */ 500 if (mListener != null) { 501 mListener.onActionClicked(action); 502 } else if (getActivity() instanceof Action.Listener) { 503 Action.Listener listener = (Action.Listener) getActivity(); 504 listener.onActionClicked(action); 505 } 506 } 507 }, new Action.OnFocusListener() { 508 @Override 509 public void onActionFocused(Action action) { 510 if (getActivity() instanceof Action.OnFocusListener) { 511 Action.OnFocusListener listener = (Action.OnFocusListener) getActivity(); 512 listener.onActionFocused(action); 513 } 514 } 515 }, mActions); 516 517 if (action instanceof VerticalGridView) { 518 mListView = (VerticalGridView) action; 519 } else { 520 mListView = (VerticalGridView) action.findViewById(R.id.list); 521 if (mListView == null) { 522 throw new IllegalStateException("No ListView exists."); 523 } 524 // mListView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 525 // mListView.setWindowAlignmentOffsetPercent(0.5f); 526 // mListView.setItemAlignmentOffset(0); 527 // mListView.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); 528 mListView.setWindowAlignmentOffset(0); 529 mListView.setWindowAlignmentOffsetPercent(50f); 530 mListView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 531 View selectorView = action.findViewById(R.id.selector); 532 if (selectorView != null) { 533 mSelectorAnimator = new SelectorAnimator(selectorView, mListView); 534 mListView.setOnScrollListener(mSelectorAnimator); 535 } 536 } 537 538 mListView.requestFocusFromTouch(); 539 mListView.setAdapter(mAdapter); 540 mListView.setSelectedPosition( 541 (mSelectedIndex >= 0 && mSelectedIndex < mActions.size()) ? mSelectedIndex 542 : getFirstCheckedAction()); 543 544 action.setTag(R.id.list, mListView); 545 action.setTag(R.id.selector, action.findViewById(R.id.selector)); 546 } 547 548 private int getFirstCheckedAction() { 549 for (int i = 0, size = mActions.size(); i < size; i++) { 550 if (mActions.get(i).isChecked()) { 551 return i; 552 } 553 } 554 return 0; 555 } 556 557 private void updateViewSize(ImageView iconView) { 558 int intrinsicWidth = iconView.getDrawable().getIntrinsicWidth(); 559 LayoutParams lp = iconView.getLayoutParams(); 560 if (intrinsicWidth > 0) { 561 lp.height = lp.width * iconView.getDrawable().getIntrinsicHeight() 562 / intrinsicWidth; 563 } else { 564 // If no intrinsic width, then just mke this a square. 565 lp.height = lp.width; 566 } 567 } 568 569 private void fadeIn(View v) { 570 v.setAlpha(0f); 571 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", 1f); 572 alphaAnimator.setDuration(v.getContext().getResources().getInteger( 573 android.R.integer.config_mediumAnimTime)); 574 alphaAnimator.start(); 575 } 576 577 private void runDelayedAnim(final Runnable runnable) { 578 final View dialogView = getView(); 579 final View contentView = (View) dialogView.getTag(R.id.content_fragment); 580 581 contentView.getViewTreeObserver().addOnGlobalLayoutListener( 582 new ViewTreeObserver.OnGlobalLayoutListener() { 583 @Override 584 public void onGlobalLayout() { 585 contentView.getViewTreeObserver().removeOnGlobalLayoutListener(this); 586 // if we buildLayer() at this time, the texture is 587 // actually not created delay a little so we can make 588 // sure all hardware layer is created before animation, 589 // in that way we can avoid the jittering of start 590 // animation 591 contentView.postOnAnimationDelayed(runnable, ANIMATE_DELAY); 592 } 593 }); 594 595 } 596 597 private void performSelectorTransition() { 598 runDelayedAnim(new Runnable() { 599 @Override 600 public void run() { 601 // Fade in the selector. 602 if (mSelectorAnimator != null) { 603 mSelectorAnimator.fadeIn(); 604 } 605 } 606 }); 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 runDelayedAnim(new Runnable() { 628 @Override 629 public void run() { 630 if (!isAdded()) { 631 // We have been detached before this could run, 632 // so just bail 633 return; 634 } 635 636 dialogView.setVisibility(View.VISIBLE); 637 638 // Fade in the activity background protection 639 ObjectAnimator oa = ObjectAnimator.ofInt(bgDrawable, "alpha", 255); 640 oa.setDuration(ANIMATE_IN_DURATION); 641 oa.setStartDelay(SECONDARY_ANIMATE_DELAY); 642 oa.setInterpolator(new DecelerateInterpolator(1.0f)); 643 oa.start(); 644 645 boolean isRtl = ViewCompat.getLayoutDirection(contentView) == 646 View.LAYOUT_DIRECTION_RTL; 647 int startDist = isRtl ? SLIDE_IN_DISTANCE : -SLIDE_IN_DISTANCE; 648 int endDist = isRtl ? -actionContainerView.getMeasuredWidth() : 649 actionContainerView.getMeasuredWidth(); 650 651 // Fade in and slide in the ContentFragment 652 // TextViews from the start. 653 prepareAndAnimateView((View) contentView.getTag(R.id.title), 654 startDist, false); 655 prepareAndAnimateView((View) contentView.getTag(R.id.breadcrumb), 656 startDist, false); 657 prepareAndAnimateView((View) contentView.getTag(R.id.description), 658 startDist, false); 659 660 // Fade in and slide in the ActionFragment from the 661 // end. 662 prepareAndAnimateView(actionContainerView, 663 endDist, false); 664 prepareAndAnimateView((View) contentView.getTag(R.id.icon), 665 startDist, true); 666 667 // Fade in the selector. 668 if (mSelectorAnimator != null) { 669 mSelectorAnimator.fadeIn(); 670 } 671 } 672 }); 673 } 674 675 private void prepareAndAnimateView(final View v, float initTransX, 676 final boolean notifyAnimationFinished) { 677 v.setLayerType(View.LAYER_TYPE_HARDWARE, null); 678 v.buildLayer(); 679 v.setAlpha(0); 680 v.setTranslationX(initTransX); 681 v.animate().alpha(1f).translationX(0).setDuration(ANIMATE_IN_DURATION) 682 .setStartDelay(SECONDARY_ANIMATE_DELAY); 683 v.animate().setInterpolator(new DecelerateInterpolator(1.0f)); 684 v.animate().setListener(new AnimatorListenerAdapter() { 685 @Override 686 public void onAnimationEnd(Animator animation) { 687 v.setLayerType(View.LAYER_TYPE_NONE, null); 688 if (notifyAnimationFinished) { 689 onIntroAnimationFinished(); 690 } 691 } 692 }); 693 v.animate().start(); 694 } 695 696 private Animator createDummyAnimator(final View v, ArrayList<Animator> animators) { 697 final AnimatorSet animatorSet = new AnimatorSet(); 698 animatorSet.playTogether(animators); 699 return new UntargetableAnimatorSet(animatorSet); 700 } 701 702 private Animator createAnimator(View v, int resourceId) { 703 Animator animator = AnimatorInflater.loadAnimator(v.getContext(), resourceId); 704 animator.setTarget(v); 705 return animator; 706 } 707 708 private Animator createSlideOutToStartAnimator(View v) { 709 boolean isRtl = ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_RTL; 710 return createTranslateAlphaAnimator(v, 0, isRtl ? 200f : -200f, 1f, 0); 711 } 712 713 private Animator createSlideInFromEndAnimator(View v) { 714 boolean isRtl = ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_RTL; 715 return createTranslateAlphaAnimator(v, isRtl ? -200f : 200f, 0, 0, 1f); 716 } 717 718 private Animator createSlideInFromStartAnimator(View v) { 719 boolean isRtl = ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_RTL; 720 return createTranslateAlphaAnimator(v, isRtl ? 200f : -200f, 0, 0, 1f); 721 } 722 723 private Animator createSlideOutToEndAnimator(View v) { 724 boolean isRtl = ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_RTL; 725 return createTranslateAlphaAnimator(v, 0, isRtl ? -200f : 200f, 1f, 0); 726 } 727 728 private Animator createFadeOutAnimator(View v) { 729 return createAlphaAnimator(v, 1f, 0); 730 } 731 732 private Animator createTranslateAlphaAnimator(View v, float fromTranslateX, float toTranslateX, 733 float fromAlpha, float toAlpha) { 734 ObjectAnimator translateAnimator = ObjectAnimator.ofFloat(v, "translationX", fromTranslateX, 735 toTranslateX); 736 translateAnimator.setDuration( 737 getResources().getInteger(android.R.integer.config_longAnimTime)); 738 Animator alphaAnimator = createAlphaAnimator(v, fromAlpha, toAlpha); 739 AnimatorSet animatorSet = new AnimatorSet(); 740 animatorSet.play(translateAnimator).with(alphaAnimator); 741 return animatorSet; 742 } 743 744 private Animator createAlphaAnimator(View v, float fromAlpha, float toAlpha) { 745 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", fromAlpha, toAlpha); 746 alphaAnimator.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime)); 747 return alphaAnimator; 748 } 749 750 private static class SelectorAnimator extends RecyclerView.OnScrollListener { 751 752 private final View mSelectorView; 753 private final ViewGroup mParentView; 754 private final int mAnimationDuration; 755 private volatile boolean mFadedOut = true; 756 757 SelectorAnimator(View selectorView, ViewGroup parentView) { 758 mSelectorView = selectorView; 759 mParentView = parentView; 760 mAnimationDuration = selectorView.getContext() 761 .getResources().getInteger(R.integer.lb_dialog_animation_duration); 762 } 763 764 // We want to fade in the selector if we've stopped scrolling on it. If 765 // we're scrolling, we want to ensure to dim the selector if we haven't 766 // already. We dim the last highlighted view so that while a user is 767 // scrolling, nothing is highlighted. 768 @Override 769 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 770 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 771 fadeIn(); 772 } else { 773 fadeOut(); 774 } 775 } 776 777 public void fadeIn() { 778 // The selector starts with a height of 0. In order to scale up 779 // from 780 // 0 we first need the set the height to 1 and scale form there. 781 int selectorHeight = mSelectorView.getHeight(); 782 if (selectorHeight == 0) { 783 LayoutParams lp = mSelectorView.getLayoutParams(); 784 lp.height = selectorHeight = mSelectorView.getContext().getResources() 785 .getDimensionPixelSize(R.dimen.lb_action_fragment_selector_min_height); 786 mSelectorView.setLayoutParams(lp); 787 } 788 View focusedChild = mParentView.getFocusedChild(); 789 if (focusedChild != null) { 790 float scaleY = (float) focusedChild.getHeight() / selectorHeight; 791 ViewPropertyAnimator animation = mSelectorView.animate() 792 .alpha(1f) 793 .setListener(new Listener(false)) 794 .setDuration(mAnimationDuration) 795 .setInterpolator(new DecelerateInterpolator(2f)); 796 if (mFadedOut) { 797 // selector is completely faded out, so we can just 798 // scale 799 // before fading in. 800 mSelectorView.setScaleY(scaleY); 801 } else { 802 // selector is not faded out, so we must animate the 803 // scale 804 // as we fade in. 805 animation.scaleY(scaleY); 806 } 807 animation.start(); 808 } 809 } 810 811 public void fadeOut() { 812 mSelectorView.animate() 813 .alpha(0f) 814 .setDuration(mAnimationDuration) 815 .setInterpolator(new DecelerateInterpolator(2f)) 816 .setListener(new Listener(true)) 817 .start(); 818 } 819 820 /** 821 * Sets {@link BaseScrollAdapterFragment#mFadedOut} 822 * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff 823 * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0 824 * (faded out). If false the view either has an alpha of 1 (visible) or 825 * is in the process of animating. 826 */ 827 private class Listener implements Animator.AnimatorListener { 828 private boolean mFadingOut; 829 private boolean mCanceled; 830 831 public Listener(boolean fadingOut) { 832 mFadingOut = fadingOut; 833 } 834 835 @Override 836 public void onAnimationStart(Animator animation) { 837 if (!mFadingOut) { 838 mFadedOut = false; 839 } 840 } 841 842 @Override 843 public void onAnimationEnd(Animator animation) { 844 if (!mCanceled && mFadingOut) { 845 mFadedOut = true; 846 } 847 } 848 849 @Override 850 public void onAnimationCancel(Animator animation) { 851 mCanceled = true; 852 } 853 854 @Override 855 public void onAnimationRepeat(Animator animation) { 856 } 857 } 858 } 859 860 private static class UntargetableAnimatorSet extends Animator { 861 862 private final AnimatorSet mAnimatorSet; 863 864 UntargetableAnimatorSet(AnimatorSet animatorSet) { 865 mAnimatorSet = animatorSet; 866 } 867 868 @Override 869 public void addListener(Animator.AnimatorListener listener) { 870 mAnimatorSet.addListener(listener); 871 } 872 873 @Override 874 public void cancel() { 875 mAnimatorSet.cancel(); 876 } 877 878 @Override 879 public Animator clone() { 880 return mAnimatorSet.clone(); 881 } 882 883 @Override 884 public void end() { 885 mAnimatorSet.end(); 886 } 887 888 @Override 889 public long getDuration() { 890 return mAnimatorSet.getDuration(); 891 } 892 893 @Override 894 public ArrayList<Animator.AnimatorListener> getListeners() { 895 return mAnimatorSet.getListeners(); 896 } 897 898 @Override 899 public long getStartDelay() { 900 return mAnimatorSet.getStartDelay(); 901 } 902 903 @Override 904 public boolean isRunning() { 905 return mAnimatorSet.isRunning(); 906 } 907 908 @Override 909 public boolean isStarted() { 910 return mAnimatorSet.isStarted(); 911 } 912 913 @Override 914 public void removeAllListeners() { 915 mAnimatorSet.removeAllListeners(); 916 } 917 918 @Override 919 public void removeListener(Animator.AnimatorListener listener) { 920 mAnimatorSet.removeListener(listener); 921 } 922 923 @Override 924 public Animator setDuration(long duration) { 925 return mAnimatorSet.setDuration(duration); 926 } 927 928 @Override 929 public void setInterpolator(TimeInterpolator value) { 930 mAnimatorSet.setInterpolator(value); 931 } 932 933 @Override 934 public void setStartDelay(long startDelay) { 935 mAnimatorSet.setStartDelay(startDelay); 936 } 937 938 @Override 939 public void setTarget(Object target) { 940 // ignore 941 } 942 943 @Override 944 public void setupEndValues() { 945 mAnimatorSet.setupEndValues(); 946 } 947 948 @Override 949 public void setupStartValues() { 950 mAnimatorSet.setupStartValues(); 951 } 952 953 @Override 954 public void start() { 955 mAnimatorSet.start(); 956 } 957 } 958 959 /** 960 * An data class which represents an action within an 961 * {@link DialogFragment}. A list of Actions represent a list of choices the 962 * user can make, a radio-button list of configuration options, or just a 963 * list of information. 964 */ 965 public static class Action implements Parcelable { 966 967 private static final String TAG = "Action"; 968 969 public static final int NO_DRAWABLE = 0; 970 public static final int NO_CHECK_SET = 0; 971 public static final int DEFAULT_CHECK_SET_ID = 1; 972 973 /** 974 * Object listening for adapter events. 975 */ 976 public interface Listener { 977 978 /** 979 * Called when the user clicks on an action. 980 */ 981 public void onActionClicked(Action action); 982 } 983 984 public interface OnFocusListener { 985 986 /** 987 * Called when the user focuses on an action. 988 */ 989 public void onActionFocused(Action action); 990 } 991 992 /** 993 * Builds a Action object. 994 */ 995 public static class Builder { 996 private String mKey; 997 private String mTitle; 998 private String mDescription; 999 private Intent mIntent; 1000 private String mResourcePackageName; 1001 private int mDrawableResource = NO_DRAWABLE; 1002 private Uri mIconUri; 1003 private boolean mChecked; 1004 private boolean mMultilineDescription; 1005 private boolean mHasNext; 1006 private boolean mInfoOnly; 1007 private int mCheckSetId = NO_CHECK_SET; 1008 private boolean mEnabled = true; 1009 1010 public Action build() { 1011 Action action = new Action(); 1012 action.mKey = mKey; 1013 action.mTitle = mTitle; 1014 action.mDescription = mDescription; 1015 action.mIntent = mIntent; 1016 action.mResourcePackageName = mResourcePackageName; 1017 action.mDrawableResource = mDrawableResource; 1018 action.mIconUri = mIconUri; 1019 action.mChecked = mChecked; 1020 action.mMultilineDescription = mMultilineDescription; 1021 action.mHasNext = mHasNext; 1022 action.mInfoOnly = mInfoOnly; 1023 action.mCheckSetId = mCheckSetId; 1024 action.mEnabled = mEnabled; 1025 return action; 1026 } 1027 1028 public Builder key(String key) { 1029 mKey = key; 1030 return this; 1031 } 1032 1033 public Builder title(String title) { 1034 mTitle = title; 1035 return this; 1036 } 1037 1038 public Builder description(String description) { 1039 mDescription = description; 1040 return this; 1041 } 1042 1043 public Builder intent(Intent intent) { 1044 mIntent = intent; 1045 return this; 1046 } 1047 1048 public Builder resourcePackageName(String resourcePackageName) { 1049 mResourcePackageName = resourcePackageName; 1050 return this; 1051 } 1052 1053 public Builder drawableResource(int drawableResource) { 1054 mDrawableResource = drawableResource; 1055 return this; 1056 } 1057 1058 public Builder iconUri(Uri iconUri) { 1059 mIconUri = iconUri; 1060 return this; 1061 } 1062 1063 public Builder checked(boolean checked) { 1064 mChecked = checked; 1065 return this; 1066 } 1067 1068 public Builder multilineDescription(boolean multilineDescription) { 1069 mMultilineDescription = multilineDescription; 1070 return this; 1071 } 1072 1073 public Builder hasNext(boolean hasNext) { 1074 mHasNext = hasNext; 1075 return this; 1076 } 1077 1078 public Builder infoOnly(boolean infoOnly) { 1079 mInfoOnly = infoOnly; 1080 return this; 1081 } 1082 1083 public Builder checkSetId(int checkSetId) { 1084 mCheckSetId = checkSetId; 1085 return this; 1086 } 1087 1088 public Builder enabled(boolean enabled) { 1089 mEnabled = enabled; 1090 return this; 1091 } 1092 } 1093 1094 private String mKey; 1095 private String mTitle; 1096 private String mDescription; 1097 private Intent mIntent; 1098 1099 /** 1100 * If not {@code null}, the package name to use to retrieve 1101 * {@link #mDrawableResource}. 1102 */ 1103 private String mResourcePackageName; 1104 1105 private int mDrawableResource; 1106 private Uri mIconUri; 1107 private boolean mChecked; 1108 private boolean mMultilineDescription; 1109 private boolean mHasNext; 1110 private boolean mInfoOnly; 1111 private int mCheckSetId; 1112 private boolean mEnabled; 1113 1114 private Action() { 1115 } 1116 1117 public String getKey() { 1118 return mKey; 1119 } 1120 1121 public String getTitle() { 1122 return mTitle; 1123 } 1124 1125 public String getDescription() { 1126 return mDescription; 1127 } 1128 1129 public void setDescription(String description) { 1130 mDescription = description; 1131 } 1132 1133 public Intent getIntent() { 1134 return mIntent; 1135 } 1136 1137 public boolean isChecked() { 1138 return mChecked; 1139 } 1140 1141 public int getDrawableResource() { 1142 return mDrawableResource; 1143 } 1144 1145 public Uri getIconUri() { 1146 return mIconUri; 1147 } 1148 1149 public String getResourcePackageName() { 1150 return mResourcePackageName; 1151 } 1152 1153 /** 1154 * Returns the check set id this action is a part of. All actions in the 1155 * same list with the same check set id are considered linked. When one 1156 * of the actions within that set is selected that action becomes 1157 * checked while all the other actions become unchecked. 1158 * 1159 * @return an integer representing the check set this action is a part 1160 * of or {@link NO_CHECK_SET} if this action isn't a 1161 * part of a check set. 1162 */ 1163 public int getCheckSetId() { 1164 return mCheckSetId; 1165 } 1166 1167 public boolean hasMultilineDescription() { 1168 return mMultilineDescription; 1169 } 1170 1171 public boolean isEnabled() { 1172 return mEnabled; 1173 } 1174 1175 public void setChecked(boolean checked) { 1176 mChecked = checked; 1177 } 1178 1179 public void setEnabled(boolean enabled) { 1180 mEnabled = enabled; 1181 } 1182 1183 /** 1184 * @return true if the action will request further user input when 1185 * selected (such as showing another dialog or launching a new 1186 * activity). False, otherwise. 1187 */ 1188 public boolean hasNext() { 1189 return mHasNext; 1190 } 1191 1192 /** 1193 * @return true if the action will only display information and is thus 1194 * unactionable. If both this and {@link #hasNext()} are true, 1195 * infoOnly takes precedence. (default is false) e.g. the amount 1196 * of storage a document uses or cost of an app. 1197 */ 1198 public boolean infoOnly() { 1199 return mInfoOnly; 1200 } 1201 1202 /** 1203 * Returns an indicator to be drawn. If null is returned, no space for 1204 * the indicator will be made. 1205 * 1206 * @param context the context of the Activity this Action belongs to 1207 * @return an indicator to draw or null if no indicator space should 1208 * exist. 1209 */ 1210 public Drawable getIndicator(Context context) { 1211 if (mDrawableResource == NO_DRAWABLE) { 1212 return null; 1213 } 1214 if (mResourcePackageName == null) { 1215 return context.getResources().getDrawable(mDrawableResource); 1216 } 1217 // If we get to here, need to load the resources. 1218 Drawable icon = null; 1219 try { 1220 Context packageContext = context.createPackageContext(mResourcePackageName, 0); 1221 icon = packageContext.getResources().getDrawable(mDrawableResource); 1222 } catch (PackageManager.NameNotFoundException e) { 1223 if (Log.isLoggable(TAG, Log.WARN)) { 1224 Log.w(TAG, "No icon for this action."); 1225 } 1226 } catch (Resources.NotFoundException e) { 1227 if (Log.isLoggable(TAG, Log.WARN)) { 1228 Log.w(TAG, "No icon for this action."); 1229 } 1230 } 1231 return icon; 1232 } 1233 1234 public static Parcelable.Creator<Action> CREATOR = new Parcelable.Creator<Action>() { 1235 1236 @Override 1237 public Action createFromParcel(Parcel source) { 1238 1239 return new Action.Builder() 1240 .key(source.readString()) 1241 .title(source.readString()) 1242 .description(source.readString()) 1243 .intent((Intent) source.readParcelable(Intent.class.getClassLoader())) 1244 .resourcePackageName(source.readString()) 1245 .drawableResource(source.readInt()) 1246 .iconUri((Uri) source.readParcelable(Uri.class.getClassLoader())) 1247 .checked(source.readInt() != 0) 1248 .multilineDescription(source.readInt() != 0) 1249 .checkSetId(source.readInt()) 1250 .build(); 1251 } 1252 1253 @Override 1254 public Action[] newArray(int size) { 1255 return new Action[size]; 1256 } 1257 }; 1258 1259 @Override 1260 public int describeContents() { 1261 return 0; 1262 } 1263 1264 @Override 1265 public void writeToParcel(Parcel dest, int flags) { 1266 dest.writeString(mKey); 1267 dest.writeString(mTitle); 1268 dest.writeString(mDescription); 1269 dest.writeParcelable(mIntent, flags); 1270 dest.writeString(mResourcePackageName); 1271 dest.writeInt(mDrawableResource); 1272 dest.writeParcelable(mIconUri, flags); 1273 dest.writeInt(mChecked ? 1 : 0); 1274 dest.writeInt(mMultilineDescription ? 1 : 0); 1275 dest.writeInt(mCheckSetId); 1276 } 1277 } 1278 } 1279