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