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