1 /* 2 * Copyright (C) 2013 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 android.transition; 18 19 import com.android.internal.R; 20 21 import android.animation.Animator; 22 import android.animation.Animator.AnimatorListener; 23 import android.animation.Animator.AnimatorPauseListener; 24 import android.content.Context; 25 import android.content.res.TypedArray; 26 import android.util.AttributeSet; 27 import android.view.View; 28 import android.view.ViewGroup; 29 30 /** 31 * This transition tracks changes to the visibility of target views in the 32 * start and end scenes. Visibility is determined not just by the 33 * {@link View#setVisibility(int)} state of views, but also whether 34 * views exist in the current view hierarchy. The class is intended to be a 35 * utility for subclasses such as {@link Fade}, which use this visibility 36 * information to determine the specific animations to run when visibility 37 * changes occur. Subclasses should implement one or both of the methods 38 * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}, 39 * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)} or 40 * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}, 41 * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}. 42 */ 43 public abstract class Visibility extends Transition { 44 45 static final String PROPNAME_VISIBILITY = "android:visibility:visibility"; 46 private static final String PROPNAME_PARENT = "android:visibility:parent"; 47 private static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation"; 48 49 /** 50 * Mode used in {@link #setMode(int)} to make the transition 51 * operate on targets that are appearing. Maybe be combined with 52 * {@link #MODE_OUT} to target Visibility changes both in and out. 53 */ 54 public static final int MODE_IN = 0x1; 55 56 /** 57 * Mode used in {@link #setMode(int)} to make the transition 58 * operate on targets that are disappearing. Maybe be combined with 59 * {@link #MODE_IN} to target Visibility changes both in and out. 60 */ 61 public static final int MODE_OUT = 0x2; 62 63 private static final String[] sTransitionProperties = { 64 PROPNAME_VISIBILITY, 65 PROPNAME_PARENT, 66 }; 67 68 private static class VisibilityInfo { 69 boolean visibilityChange; 70 boolean fadeIn; 71 int startVisibility; 72 int endVisibility; 73 ViewGroup startParent; 74 ViewGroup endParent; 75 } 76 77 private int mMode = MODE_IN | MODE_OUT; 78 79 private int mForcedStartVisibility = -1; 80 private int mForcedEndVisibility = -1; 81 Visibility()82 public Visibility() {} 83 Visibility(Context context, AttributeSet attrs)84 public Visibility(Context context, AttributeSet attrs) { 85 super(context, attrs); 86 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VisibilityTransition); 87 int mode = a.getInt(R.styleable.VisibilityTransition_transitionVisibilityMode, 0); 88 a.recycle(); 89 if (mode != 0) { 90 setMode(mode); 91 } 92 } 93 94 /** 95 * Changes the transition to support appearing and/or disappearing Views, depending 96 * on <code>mode</code>. 97 * 98 * @param mode The behavior supported by this transition, a combination of 99 * {@link #MODE_IN} and {@link #MODE_OUT}. 100 * @attr ref android.R.styleable#VisibilityTransition_transitionVisibilityMode 101 */ setMode(int mode)102 public void setMode(int mode) { 103 if ((mode & ~(MODE_IN | MODE_OUT)) != 0) { 104 throw new IllegalArgumentException("Only MODE_IN and MODE_OUT flags are allowed"); 105 } 106 mMode = mode; 107 } 108 109 /** 110 * Returns whether appearing and/or disappearing Views are supported. 111 * 112 * Returns whether appearing and/or disappearing Views are supported. A combination of 113 * {@link #MODE_IN} and {@link #MODE_OUT}. 114 * @attr ref android.R.styleable#VisibilityTransition_transitionVisibilityMode 115 */ getMode()116 public int getMode() { 117 return mMode; 118 } 119 120 @Override getTransitionProperties()121 public String[] getTransitionProperties() { 122 return sTransitionProperties; 123 } 124 captureValues(TransitionValues transitionValues, int forcedVisibility)125 private void captureValues(TransitionValues transitionValues, int forcedVisibility) { 126 int visibility; 127 if (forcedVisibility != -1) { 128 visibility = forcedVisibility; 129 } else { 130 visibility = transitionValues.view.getVisibility(); 131 } 132 transitionValues.values.put(PROPNAME_VISIBILITY, visibility); 133 transitionValues.values.put(PROPNAME_PARENT, transitionValues.view.getParent()); 134 int[] loc = new int[2]; 135 transitionValues.view.getLocationOnScreen(loc); 136 transitionValues.values.put(PROPNAME_SCREEN_LOCATION, loc); 137 } 138 139 @Override captureStartValues(TransitionValues transitionValues)140 public void captureStartValues(TransitionValues transitionValues) { 141 captureValues(transitionValues, mForcedStartVisibility); 142 } 143 144 @Override captureEndValues(TransitionValues transitionValues)145 public void captureEndValues(TransitionValues transitionValues) { 146 captureValues(transitionValues, mForcedEndVisibility); 147 } 148 149 /** @hide */ 150 @Override forceVisibility(int visibility, boolean isStartValue)151 public void forceVisibility(int visibility, boolean isStartValue) { 152 if (isStartValue) { 153 mForcedStartVisibility = visibility; 154 } else { 155 mForcedEndVisibility = visibility; 156 } 157 } 158 159 /** 160 * Returns whether the view is 'visible' according to the given values 161 * object. This is determined by testing the same properties in the values 162 * object that are used to determine whether the object is appearing or 163 * disappearing in the {@link 164 * Transition#createAnimator(ViewGroup, TransitionValues, TransitionValues)} 165 * method. This method can be called by, for example, subclasses that want 166 * to know whether the object is visible in the same way that Visibility 167 * determines it for the actual animation. 168 * 169 * @param values The TransitionValues object that holds the information by 170 * which visibility is determined. 171 * @return True if the view reference by <code>values</code> is visible, 172 * false otherwise. 173 */ isVisible(TransitionValues values)174 public boolean isVisible(TransitionValues values) { 175 if (values == null) { 176 return false; 177 } 178 int visibility = (Integer) values.values.get(PROPNAME_VISIBILITY); 179 View parent = (View) values.values.get(PROPNAME_PARENT); 180 181 return visibility == View.VISIBLE && parent != null; 182 } 183 getVisibilityChangeInfo(TransitionValues startValues, TransitionValues endValues)184 private static VisibilityInfo getVisibilityChangeInfo(TransitionValues startValues, 185 TransitionValues endValues) { 186 final VisibilityInfo visInfo = new VisibilityInfo(); 187 visInfo.visibilityChange = false; 188 visInfo.fadeIn = false; 189 if (startValues != null && startValues.values.containsKey(PROPNAME_VISIBILITY)) { 190 visInfo.startVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY); 191 visInfo.startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT); 192 } else { 193 visInfo.startVisibility = -1; 194 visInfo.startParent = null; 195 } 196 if (endValues != null && endValues.values.containsKey(PROPNAME_VISIBILITY)) { 197 visInfo.endVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY); 198 visInfo.endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT); 199 } else { 200 visInfo.endVisibility = -1; 201 visInfo.endParent = null; 202 } 203 if (startValues != null && endValues != null) { 204 if (visInfo.startVisibility == visInfo.endVisibility && 205 visInfo.startParent == visInfo.endParent) { 206 return visInfo; 207 } else { 208 if (visInfo.startVisibility != visInfo.endVisibility) { 209 if (visInfo.startVisibility == View.VISIBLE) { 210 visInfo.fadeIn = false; 211 visInfo.visibilityChange = true; 212 } else if (visInfo.endVisibility == View.VISIBLE) { 213 visInfo.fadeIn = true; 214 visInfo.visibilityChange = true; 215 } 216 // no visibilityChange if going between INVISIBLE and GONE 217 } else if (visInfo.startParent != visInfo.endParent) { 218 if (visInfo.endParent == null) { 219 visInfo.fadeIn = false; 220 visInfo.visibilityChange = true; 221 } else if (visInfo.startParent == null) { 222 visInfo.fadeIn = true; 223 visInfo.visibilityChange = true; 224 } 225 } 226 } 227 } else if (startValues == null && visInfo.endVisibility == View.VISIBLE) { 228 visInfo.fadeIn = true; 229 visInfo.visibilityChange = true; 230 } else if (endValues == null && visInfo.startVisibility == View.VISIBLE) { 231 visInfo.fadeIn = false; 232 visInfo.visibilityChange = true; 233 } 234 return visInfo; 235 } 236 237 @Override createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)238 public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, 239 TransitionValues endValues) { 240 VisibilityInfo visInfo = getVisibilityChangeInfo(startValues, endValues); 241 if (visInfo.visibilityChange 242 && (visInfo.startParent != null || visInfo.endParent != null)) { 243 if (visInfo.fadeIn) { 244 return onAppear(sceneRoot, startValues, visInfo.startVisibility, 245 endValues, visInfo.endVisibility); 246 } else { 247 return onDisappear(sceneRoot, startValues, visInfo.startVisibility, 248 endValues, visInfo.endVisibility 249 ); 250 } 251 } 252 return null; 253 } 254 255 /** 256 * The default implementation of this method calls 257 * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}. 258 * Subclasses should override this method or 259 * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}. 260 * if they need to create an Animator when targets appear. 261 * The method should only be called by the Visibility class; it is 262 * not intended to be called from external classes. 263 * 264 * @param sceneRoot The root of the transition hierarchy 265 * @param startValues The target values in the start scene 266 * @param startVisibility The target visibility in the start scene 267 * @param endValues The target values in the end scene 268 * @param endVisibility The target visibility in the end scene 269 * @return An Animator to be started at the appropriate time in the 270 * overall transition for this scene change. A null value means no animation 271 * should be run. 272 */ onAppear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility)273 public Animator onAppear(ViewGroup sceneRoot, 274 TransitionValues startValues, int startVisibility, 275 TransitionValues endValues, int endVisibility) { 276 if ((mMode & MODE_IN) != MODE_IN || endValues == null) { 277 return null; 278 } 279 if (startValues == null) { 280 VisibilityInfo parentVisibilityInfo = null; 281 View endParent = (View) endValues.view.getParent(); 282 TransitionValues startParentValues = getMatchedTransitionValues(endParent, 283 false); 284 TransitionValues endParentValues = getTransitionValues(endParent, false); 285 parentVisibilityInfo = 286 getVisibilityChangeInfo(startParentValues, endParentValues); 287 if (parentVisibilityInfo.visibilityChange) { 288 return null; 289 } 290 } 291 final boolean isForcedVisibility = mForcedStartVisibility != -1 || 292 mForcedEndVisibility != -1; 293 if (isForcedVisibility) { 294 // Make sure that we reverse the effect of onDisappear's setTransitionAlpha(0) 295 endValues.view.setTransitionAlpha(1); 296 } 297 return onAppear(sceneRoot, endValues.view, startValues, endValues); 298 } 299 300 /** 301 * The default implementation of this method returns a null Animator. Subclasses should 302 * override this method to make targets appear with the desired transition. The 303 * method should only be called from 304 * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}. 305 * 306 * @param sceneRoot The root of the transition hierarchy 307 * @param view The View to make appear. This will be in the target scene's View hierarchy and 308 * will be VISIBLE. 309 * @param startValues The target values in the start scene 310 * @param endValues The target values in the end scene 311 * @return An Animator to be started at the appropriate time in the 312 * overall transition for this scene change. A null value means no animation 313 * should be run. 314 */ onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)315 public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, 316 TransitionValues endValues) { 317 return null; 318 } 319 320 /** 321 * Subclasses should override this method or 322 * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)} 323 * if they need to create an Animator when targets disappear. 324 * The method should only be called by the Visibility class; it is 325 * not intended to be called from external classes. 326 * <p> 327 * The default implementation of this method attempts to find a View to use to call 328 * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}, 329 * based on the situation of the View in the View hierarchy. For example, 330 * if a View was simply removed from its parent, then the View will be added 331 * into a {@link android.view.ViewGroupOverlay} and passed as the <code>view</code> 332 * parameter in {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}. 333 * If a visible View is changed to be {@link View#GONE} or {@link View#INVISIBLE}, 334 * then it can be used as the <code>view</code> and the visibility will be changed 335 * to {@link View#VISIBLE} for the duration of the animation. However, if a View 336 * is in a hierarchy which is also altering its visibility, the situation can be 337 * more complicated. In general, if a view that is no longer in the hierarchy in 338 * the end scene still has a parent (so its parent hierarchy was removed, but it 339 * was not removed from its parent), then it will be left alone to avoid side-effects from 340 * improperly removing it from its parent. The only exception to this is if 341 * the previous {@link Scene} was {@link Scene#getSceneForLayout(ViewGroup, int, 342 * android.content.Context) created from a layout resource file}, then it is considered 343 * safe to un-parent the starting scene view in order to make it disappear.</p> 344 * 345 * @param sceneRoot The root of the transition hierarchy 346 * @param startValues The target values in the start scene 347 * @param startVisibility The target visibility in the start scene 348 * @param endValues The target values in the end scene 349 * @param endVisibility The target visibility in the end scene 350 * @return An Animator to be started at the appropriate time in the 351 * overall transition for this scene change. A null value means no animation 352 * should be run. 353 */ onDisappear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility)354 public Animator onDisappear(ViewGroup sceneRoot, 355 TransitionValues startValues, int startVisibility, 356 TransitionValues endValues, int endVisibility) { 357 if ((mMode & MODE_OUT) != MODE_OUT) { 358 return null; 359 } 360 361 View startView = (startValues != null) ? startValues.view : null; 362 View endView = (endValues != null) ? endValues.view : null; 363 View overlayView = null; 364 View viewToKeep = null; 365 if (endView == null || endView.getParent() == null) { 366 if (endView != null) { 367 // endView was removed from its parent - add it to the overlay 368 overlayView = endView; 369 } else if (startView != null) { 370 // endView does not exist. Use startView only under certain 371 // conditions, because placing a view in an overlay necessitates 372 // it being removed from its current parent 373 if (startView.getParent() == null) { 374 // no parent - safe to use 375 overlayView = startView; 376 } else if (startView.getParent() instanceof View) { 377 View startParent = (View) startView.getParent(); 378 TransitionValues startParentValues = getTransitionValues(startParent, true); 379 TransitionValues endParentValues = getMatchedTransitionValues(startParent, 380 true); 381 VisibilityInfo parentVisibilityInfo = 382 getVisibilityChangeInfo(startParentValues, endParentValues); 383 if (!parentVisibilityInfo.visibilityChange) { 384 overlayView = TransitionUtils.copyViewImage(sceneRoot, startView, 385 startParent); 386 } else if (startParent.getParent() == null) { 387 int id = startParent.getId(); 388 if (id != View.NO_ID && sceneRoot.findViewById(id) != null 389 && mCanRemoveViews) { 390 // no parent, but its parent is unparented but the parent 391 // hierarchy has been replaced by a new hierarchy with the same id 392 // and it is safe to un-parent startView 393 overlayView = startView; 394 } 395 } 396 } 397 } 398 } else { 399 // visibility change 400 if (endVisibility == View.INVISIBLE) { 401 viewToKeep = endView; 402 } else { 403 // Becoming GONE 404 if (startView == endView) { 405 viewToKeep = endView; 406 } else { 407 overlayView = startView; 408 } 409 } 410 } 411 final int finalVisibility = endVisibility; 412 final ViewGroup finalSceneRoot = sceneRoot; 413 414 if (overlayView != null) { 415 // TODO: Need to do this for general case of adding to overlay 416 int[] screenLoc = (int[]) startValues.values.get(PROPNAME_SCREEN_LOCATION); 417 int screenX = screenLoc[0]; 418 int screenY = screenLoc[1]; 419 int[] loc = new int[2]; 420 sceneRoot.getLocationOnScreen(loc); 421 overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft()); 422 overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop()); 423 sceneRoot.getOverlay().add(overlayView); 424 Animator animator = onDisappear(sceneRoot, overlayView, startValues, endValues); 425 if (animator == null) { 426 sceneRoot.getOverlay().remove(overlayView); 427 } else { 428 final View finalOverlayView = overlayView; 429 addListener(new TransitionListenerAdapter() { 430 @Override 431 public void onTransitionEnd(Transition transition) { 432 finalSceneRoot.getOverlay().remove(finalOverlayView); 433 } 434 }); 435 } 436 return animator; 437 } 438 439 if (viewToKeep != null) { 440 int originalVisibility = -1; 441 final boolean isForcedVisibility = mForcedStartVisibility != -1 || 442 mForcedEndVisibility != -1; 443 if (!isForcedVisibility) { 444 originalVisibility = viewToKeep.getVisibility(); 445 viewToKeep.setTransitionVisibility(View.VISIBLE); 446 } 447 Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues); 448 if (animator != null) { 449 DisappearListener disappearListener = new DisappearListener(viewToKeep, 450 finalVisibility, isForcedVisibility); 451 animator.addListener(disappearListener); 452 animator.addPauseListener(disappearListener); 453 addListener(disappearListener); 454 } else if (!isForcedVisibility) { 455 viewToKeep.setTransitionVisibility(originalVisibility); 456 } 457 return animator; 458 } 459 return null; 460 } 461 462 @Override isTransitionRequired(TransitionValues startValues, TransitionValues newValues)463 public boolean isTransitionRequired(TransitionValues startValues, TransitionValues newValues) { 464 if (startValues == null && newValues == null) { 465 return false; 466 } 467 if (startValues != null && newValues != null && 468 newValues.values.containsKey(PROPNAME_VISIBILITY) != 469 startValues.values.containsKey(PROPNAME_VISIBILITY)) { 470 // The transition wasn't targeted in either the start or end, so it couldn't 471 // have changed. 472 return false; 473 } 474 VisibilityInfo changeInfo = getVisibilityChangeInfo(startValues, newValues); 475 return changeInfo.visibilityChange && (changeInfo.startVisibility == View.VISIBLE || 476 changeInfo.endVisibility == View.VISIBLE); 477 } 478 479 /** 480 * The default implementation of this method returns a null Animator. Subclasses should 481 * override this method to make targets disappear with the desired transition. The 482 * method should only be called from 483 * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}. 484 * 485 * @param sceneRoot The root of the transition hierarchy 486 * @param view The View to make disappear. This will be in the target scene's View 487 * hierarchy or in an {@link android.view.ViewGroupOverlay} and will be 488 * VISIBLE. 489 * @param startValues The target values in the start scene 490 * @param endValues The target values in the end scene 491 * @return An Animator to be started at the appropriate time in the 492 * overall transition for this scene change. A null value means no animation 493 * should be run. 494 */ onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)495 public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, 496 TransitionValues endValues) { 497 return null; 498 } 499 500 private static class DisappearListener 501 extends TransitionListenerAdapter implements AnimatorListener, AnimatorPauseListener { 502 private final boolean mIsForcedVisibility; 503 private final View mView; 504 private final int mFinalVisibility; 505 private final ViewGroup mParent; 506 507 private boolean mLayoutSuppressed; 508 private boolean mFinalVisibilitySet = false; 509 boolean mCanceled = false; 510 DisappearListener(View view, int finalVisibility, boolean isForcedVisibility)511 public DisappearListener(View view, int finalVisibility, boolean isForcedVisibility) { 512 this.mView = view; 513 this.mIsForcedVisibility = isForcedVisibility; 514 this.mFinalVisibility = finalVisibility; 515 this.mParent = (ViewGroup) view.getParent(); 516 // Prevent a layout from including mView in its calculation. 517 suppressLayout(true); 518 } 519 520 @Override onAnimationPause(Animator animation)521 public void onAnimationPause(Animator animation) { 522 if (!mCanceled && !mIsForcedVisibility) { 523 mView.setTransitionVisibility(mFinalVisibility); 524 } 525 } 526 527 @Override onAnimationResume(Animator animation)528 public void onAnimationResume(Animator animation) { 529 if (!mCanceled && !mIsForcedVisibility) { 530 mView.setTransitionVisibility(View.VISIBLE); 531 } 532 } 533 534 @Override onAnimationCancel(Animator animation)535 public void onAnimationCancel(Animator animation) { 536 mCanceled = true; 537 } 538 539 @Override onAnimationRepeat(Animator animation)540 public void onAnimationRepeat(Animator animation) { 541 } 542 543 @Override onAnimationStart(Animator animation)544 public void onAnimationStart(Animator animation) { 545 } 546 547 @Override onAnimationEnd(Animator animation)548 public void onAnimationEnd(Animator animation) { 549 hideViewWhenNotCanceled(); 550 } 551 552 @Override onTransitionEnd(Transition transition)553 public void onTransitionEnd(Transition transition) { 554 hideViewWhenNotCanceled(); 555 } 556 557 @Override onTransitionPause(Transition transition)558 public void onTransitionPause(Transition transition) { 559 suppressLayout(false); 560 } 561 562 @Override onTransitionResume(Transition transition)563 public void onTransitionResume(Transition transition) { 564 suppressLayout(true); 565 } 566 hideViewWhenNotCanceled()567 private void hideViewWhenNotCanceled() { 568 if (!mCanceled) { 569 if (mIsForcedVisibility) { 570 mView.setTransitionAlpha(0); 571 } else if (!mFinalVisibilitySet) { 572 // Recreate the parent's display list in case it includes mView. 573 mView.setTransitionVisibility(mFinalVisibility); 574 if (mParent != null) { 575 mParent.invalidate(); 576 } 577 mFinalVisibilitySet = true; 578 } 579 } 580 // Layout is allowed now that the View is in its final state 581 suppressLayout(false); 582 } 583 suppressLayout(boolean suppress)584 private void suppressLayout(boolean suppress) { 585 if (mLayoutSuppressed != suppress && mParent != null && !mIsForcedVisibility) { 586 mLayoutSuppressed = suppress; 587 mParent.suppressLayout(suppress); 588 } 589 } 590 } 591 } 592